@certe/atmos-editor 0.1.0

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 (234) hide show
  1. package/LICENCE +674 -0
  2. package/README.md +206 -0
  3. package/dist/asset-browser-client.d.ts +24 -0
  4. package/dist/asset-browser-client.d.ts.map +1 -0
  5. package/dist/asset-browser-client.js +60 -0
  6. package/dist/asset-browser-client.js.map +1 -0
  7. package/dist/asset-types.d.ts +33 -0
  8. package/dist/asset-types.d.ts.map +1 -0
  9. package/dist/asset-types.js +2 -0
  10. package/dist/asset-types.js.map +1 -0
  11. package/dist/bootstrap/default-factories.d.ts +38 -0
  12. package/dist/bootstrap/default-factories.d.ts.map +1 -0
  13. package/dist/bootstrap/default-factories.js +268 -0
  14. package/dist/bootstrap/default-factories.js.map +1 -0
  15. package/dist/bootstrap/editor-physics.d.ts +81 -0
  16. package/dist/bootstrap/editor-physics.d.ts.map +1 -0
  17. package/dist/bootstrap/editor-physics.js +512 -0
  18. package/dist/bootstrap/editor-physics.js.map +1 -0
  19. package/dist/bootstrap/geometry-cache.d.ts +5 -0
  20. package/dist/bootstrap/geometry-cache.d.ts.map +1 -0
  21. package/dist/bootstrap/geometry-cache.js +18 -0
  22. package/dist/bootstrap/geometry-cache.js.map +1 -0
  23. package/dist/bootstrap/keyboard-shortcuts.d.ts +4 -0
  24. package/dist/bootstrap/keyboard-shortcuts.d.ts.map +1 -0
  25. package/dist/bootstrap/keyboard-shortcuts.js +43 -0
  26. package/dist/bootstrap/keyboard-shortcuts.js.map +1 -0
  27. package/dist/bootstrap/model-import.d.ts +9 -0
  28. package/dist/bootstrap/model-import.d.ts.map +1 -0
  29. package/dist/bootstrap/model-import.js +55 -0
  30. package/dist/bootstrap/model-import.js.map +1 -0
  31. package/dist/bootstrap/start-editor.d.ts +3 -0
  32. package/dist/bootstrap/start-editor.d.ts.map +1 -0
  33. package/dist/bootstrap/start-editor.js +506 -0
  34. package/dist/bootstrap/start-editor.js.map +1 -0
  35. package/dist/bootstrap/start-player.d.ts +23 -0
  36. package/dist/bootstrap/start-player.d.ts.map +1 -0
  37. package/dist/bootstrap/start-player.js +205 -0
  38. package/dist/bootstrap/start-player.js.map +1 -0
  39. package/dist/bootstrap/types.d.ts +160 -0
  40. package/dist/bootstrap/types.d.ts.map +1 -0
  41. package/dist/bootstrap/types.js +2 -0
  42. package/dist/bootstrap/types.js.map +1 -0
  43. package/dist/camera-frustum-renderer.d.ts +15 -0
  44. package/dist/camera-frustum-renderer.d.ts.map +1 -0
  45. package/dist/camera-frustum-renderer.js +110 -0
  46. package/dist/camera-frustum-renderer.js.map +1 -0
  47. package/dist/camera-presets.d.ts +10 -0
  48. package/dist/camera-presets.d.ts.map +1 -0
  49. package/dist/camera-presets.js +15 -0
  50. package/dist/camera-presets.js.map +1 -0
  51. package/dist/collider-gizmo-renderer.d.ts +13 -0
  52. package/dist/collider-gizmo-renderer.d.ts.map +1 -0
  53. package/dist/collider-gizmo-renderer.js +217 -0
  54. package/dist/collider-gizmo-renderer.js.map +1 -0
  55. package/dist/color-utils.d.ts +5 -0
  56. package/dist/color-utils.d.ts.map +1 -0
  57. package/dist/color-utils.js +13 -0
  58. package/dist/color-utils.js.map +1 -0
  59. package/dist/components/asset-browser-panel.d.ts +14 -0
  60. package/dist/components/asset-browser-panel.d.ts.map +1 -0
  61. package/dist/components/asset-browser-panel.js +247 -0
  62. package/dist/components/asset-browser-panel.js.map +1 -0
  63. package/dist/components/context-menu.d.ts +14 -0
  64. package/dist/components/context-menu.d.ts.map +1 -0
  65. package/dist/components/context-menu.js +48 -0
  66. package/dist/components/context-menu.js.map +1 -0
  67. package/dist/components/editor-shell.d.ts +27 -0
  68. package/dist/components/editor-shell.d.ts.map +1 -0
  69. package/dist/components/editor-shell.js +327 -0
  70. package/dist/components/editor-shell.js.map +1 -0
  71. package/dist/components/fields/boolean-field.d.ts +8 -0
  72. package/dist/components/fields/boolean-field.d.ts.map +1 -0
  73. package/dist/components/fields/boolean-field.js +11 -0
  74. package/dist/components/fields/boolean-field.js.map +1 -0
  75. package/dist/components/fields/color-field.d.ts +8 -0
  76. package/dist/components/fields/color-field.d.ts.map +1 -0
  77. package/dist/components/fields/color-field.js +34 -0
  78. package/dist/components/fields/color-field.js.map +1 -0
  79. package/dist/components/fields/decimal-input.d.ts +13 -0
  80. package/dist/components/fields/decimal-input.d.ts.map +1 -0
  81. package/dist/components/fields/decimal-input.js +49 -0
  82. package/dist/components/fields/decimal-input.js.map +1 -0
  83. package/dist/components/fields/enum-field.d.ts +12 -0
  84. package/dist/components/fields/enum-field.d.ts.map +1 -0
  85. package/dist/components/fields/enum-field.js +20 -0
  86. package/dist/components/fields/enum-field.js.map +1 -0
  87. package/dist/components/fields/game-object-ref-field.d.ts +11 -0
  88. package/dist/components/fields/game-object-ref-field.d.ts.map +1 -0
  89. package/dist/components/fields/game-object-ref-field.js +73 -0
  90. package/dist/components/fields/game-object-ref-field.js.map +1 -0
  91. package/dist/components/fields/material-asset-field.d.ts +10 -0
  92. package/dist/components/fields/material-asset-field.d.ts.map +1 -0
  93. package/dist/components/fields/material-asset-field.js +114 -0
  94. package/dist/components/fields/material-asset-field.js.map +1 -0
  95. package/dist/components/fields/number-field.d.ts +10 -0
  96. package/dist/components/fields/number-field.d.ts.map +1 -0
  97. package/dist/components/fields/number-field.js +21 -0
  98. package/dist/components/fields/number-field.js.map +1 -0
  99. package/dist/components/fields/quat-field.d.ts +8 -0
  100. package/dist/components/fields/quat-field.d.ts.map +1 -0
  101. package/dist/components/fields/quat-field.js +43 -0
  102. package/dist/components/fields/quat-field.js.map +1 -0
  103. package/dist/components/fields/string-field.d.ts +7 -0
  104. package/dist/components/fields/string-field.d.ts.map +1 -0
  105. package/dist/components/fields/string-field.js +20 -0
  106. package/dist/components/fields/string-field.js.map +1 -0
  107. package/dist/components/fields/vec3-field.d.ts +8 -0
  108. package/dist/components/fields/vec3-field.d.ts.map +1 -0
  109. package/dist/components/fields/vec3-field.js +30 -0
  110. package/dist/components/fields/vec3-field.js.map +1 -0
  111. package/dist/components/hierarchy-node.d.ts +18 -0
  112. package/dist/components/hierarchy-node.d.ts.map +1 -0
  113. package/dist/components/hierarchy-node.js +77 -0
  114. package/dist/components/hierarchy-node.js.map +1 -0
  115. package/dist/components/hierarchy-panel.d.ts +14 -0
  116. package/dist/components/hierarchy-panel.d.ts.map +1 -0
  117. package/dist/components/hierarchy-panel.js +228 -0
  118. package/dist/components/hierarchy-panel.js.map +1 -0
  119. package/dist/components/inspector-panel.d.ts +14 -0
  120. package/dist/components/inspector-panel.d.ts.map +1 -0
  121. package/dist/components/inspector-panel.js +288 -0
  122. package/dist/components/inspector-panel.js.map +1 -0
  123. package/dist/components/material-inspector.d.ts +10 -0
  124. package/dist/components/material-inspector.d.ts.map +1 -0
  125. package/dist/components/material-inspector.js +130 -0
  126. package/dist/components/material-inspector.js.map +1 -0
  127. package/dist/components/post-process-panel.d.ts +9 -0
  128. package/dist/components/post-process-panel.d.ts.map +1 -0
  129. package/dist/components/post-process-panel.js +70 -0
  130. package/dist/components/post-process-panel.js.map +1 -0
  131. package/dist/components/project-gate.d.ts +8 -0
  132. package/dist/components/project-gate.d.ts.map +1 -0
  133. package/dist/components/project-gate.js +87 -0
  134. package/dist/components/project-gate.js.map +1 -0
  135. package/dist/components/settings-panel.d.ts +8 -0
  136. package/dist/components/settings-panel.d.ts.map +1 -0
  137. package/dist/components/settings-panel.js +108 -0
  138. package/dist/components/settings-panel.js.map +1 -0
  139. package/dist/components/use-splitter.d.ts +4 -0
  140. package/dist/components/use-splitter.d.ts.map +1 -0
  141. package/dist/components/use-splitter.js +22 -0
  142. package/dist/components/use-splitter.js.map +1 -0
  143. package/dist/editor-mount.d.ts +36 -0
  144. package/dist/editor-mount.d.ts.map +1 -0
  145. package/dist/editor-mount.js +161 -0
  146. package/dist/editor-mount.js.map +1 -0
  147. package/dist/editor-state.d.ts +55 -0
  148. package/dist/editor-state.d.ts.map +1 -0
  149. package/dist/editor-state.js +181 -0
  150. package/dist/editor-state.js.map +1 -0
  151. package/dist/gizmo-meshes.d.ts +9 -0
  152. package/dist/gizmo-meshes.d.ts.map +1 -0
  153. package/dist/gizmo-meshes.js +229 -0
  154. package/dist/gizmo-meshes.js.map +1 -0
  155. package/dist/gizmo-renderer.d.ts +16 -0
  156. package/dist/gizmo-renderer.d.ts.map +1 -0
  157. package/dist/gizmo-renderer.js +77 -0
  158. package/dist/gizmo-renderer.js.map +1 -0
  159. package/dist/gizmo-state.d.ts +25 -0
  160. package/dist/gizmo-state.d.ts.map +1 -0
  161. package/dist/gizmo-state.js +269 -0
  162. package/dist/gizmo-state.js.map +1 -0
  163. package/dist/index.d.ts +33 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +22 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/joint-gizmo-renderer.d.ts +13 -0
  168. package/dist/joint-gizmo-renderer.d.ts.map +1 -0
  169. package/dist/joint-gizmo-renderer.js +133 -0
  170. package/dist/joint-gizmo-renderer.js.map +1 -0
  171. package/dist/material-manager.d.ts +22 -0
  172. package/dist/material-manager.d.ts.map +1 -0
  173. package/dist/material-manager.js +156 -0
  174. package/dist/material-manager.js.map +1 -0
  175. package/dist/object-picker.d.ts +11 -0
  176. package/dist/object-picker.d.ts.map +1 -0
  177. package/dist/object-picker.js +104 -0
  178. package/dist/object-picker.js.map +1 -0
  179. package/dist/orbit-camera.d.ts +38 -0
  180. package/dist/orbit-camera.d.ts.map +1 -0
  181. package/dist/orbit-camera.js +180 -0
  182. package/dist/orbit-camera.js.map +1 -0
  183. package/dist/overlay-renderer.d.ts +23 -0
  184. package/dist/overlay-renderer.d.ts.map +1 -0
  185. package/dist/overlay-renderer.js +95 -0
  186. package/dist/overlay-renderer.js.map +1 -0
  187. package/dist/player-entry.d.ts +6 -0
  188. package/dist/player-entry.d.ts.map +1 -0
  189. package/dist/player-entry.js +4 -0
  190. package/dist/player-entry.js.map +1 -0
  191. package/dist/project-fs.d.ts +28 -0
  192. package/dist/project-fs.d.ts.map +1 -0
  193. package/dist/project-fs.js +258 -0
  194. package/dist/project-fs.js.map +1 -0
  195. package/dist/project-seed.d.ts +3 -0
  196. package/dist/project-seed.d.ts.map +1 -0
  197. package/dist/project-seed.js +35 -0
  198. package/dist/project-seed.js.map +1 -0
  199. package/dist/project-settings.d.ts +29 -0
  200. package/dist/project-settings.d.ts.map +1 -0
  201. package/dist/project-settings.js +69 -0
  202. package/dist/project-settings.js.map +1 -0
  203. package/dist/property-setters.d.ts +4 -0
  204. package/dist/property-setters.d.ts.map +1 -0
  205. package/dist/property-setters.js +58 -0
  206. package/dist/property-setters.js.map +1 -0
  207. package/dist/scene-operations.d.ts +14 -0
  208. package/dist/scene-operations.d.ts.map +1 -0
  209. package/dist/scene-operations.js +195 -0
  210. package/dist/scene-operations.js.map +1 -0
  211. package/dist/scene-snapshot.d.ts +28 -0
  212. package/dist/scene-snapshot.d.ts.map +1 -0
  213. package/dist/scene-snapshot.js +97 -0
  214. package/dist/scene-snapshot.js.map +1 -0
  215. package/dist/script-discovery.d.ts +12 -0
  216. package/dist/script-discovery.d.ts.map +1 -0
  217. package/dist/script-discovery.js +81 -0
  218. package/dist/script-discovery.js.map +1 -0
  219. package/dist/selection-utils.d.ts +4 -0
  220. package/dist/selection-utils.d.ts.map +1 -0
  221. package/dist/selection-utils.js +19 -0
  222. package/dist/selection-utils.js.map +1 -0
  223. package/dist/simple-material-loader.d.ts +17 -0
  224. package/dist/simple-material-loader.d.ts.map +1 -0
  225. package/dist/simple-material-loader.js +85 -0
  226. package/dist/simple-material-loader.js.map +1 -0
  227. package/dist/wireframe-renderer.d.ts +18 -0
  228. package/dist/wireframe-renderer.d.ts.map +1 -0
  229. package/dist/wireframe-renderer.js +106 -0
  230. package/dist/wireframe-renderer.js.map +1 -0
  231. package/package.json +65 -0
  232. package/src/index.ts +48 -0
  233. package/vite-plugin.d.ts +15 -0
  234. package/vite-plugin.mjs +395 -0
@@ -0,0 +1,395 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const DEFAULT_EXCLUDE = new Set([
5
+ 'node_modules', '.git', 'dist', '.vite', '.turbo', '__pycache__',
6
+ ]);
7
+
8
+ function scanDirectory(dirPath, relativeTo, exclude) {
9
+ const entries = [];
10
+ let items;
11
+ try {
12
+ items = fs.readdirSync(dirPath, { withFileTypes: true });
13
+ } catch {
14
+ return entries;
15
+ }
16
+ for (const item of items) {
17
+ if (exclude.has(item.name) || item.name.startsWith('.')) continue;
18
+ const fullPath = path.join(dirPath, item.name);
19
+ const relPath = path.relative(relativeTo, fullPath).replace(/\\/g, '/');
20
+ if (item.isDirectory()) {
21
+ entries.push({
22
+ path: relPath,
23
+ name: item.name,
24
+ kind: 'directory',
25
+ extension: '',
26
+ children: scanDirectory(fullPath, relativeTo, exclude),
27
+ });
28
+ } else {
29
+ const ext = path.extname(item.name).slice(1);
30
+ entries.push({
31
+ path: relPath,
32
+ name: item.name,
33
+ kind: 'file',
34
+ extension: ext,
35
+ });
36
+ }
37
+ }
38
+ return entries.sort((a, b) => {
39
+ if (a.kind !== b.kind) return a.kind === 'directory' ? -1 : 1;
40
+ return a.name.localeCompare(b.name);
41
+ });
42
+ }
43
+
44
+ function generateEditorHtml(entry) {
45
+ return `<!DOCTYPE html>
46
+ <html lang="en">
47
+ <head>
48
+ <meta charset="UTF-8" />
49
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
50
+ <title>Atmos Editor</title>
51
+ </head>
52
+ <body>
53
+ <script type="module" src="/${entry}"></script>
54
+ </body>
55
+ </html>`;
56
+ }
57
+
58
+ function generatePlayerHtml(entry) {
59
+ return `<!DOCTYPE html>
60
+ <html lang="en">
61
+ <head>
62
+ <meta charset="UTF-8" />
63
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
64
+ <title>Atmos Game</title>
65
+ <style>
66
+ * { margin: 0; padding: 0; box-sizing: border-box; }
67
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
68
+ #atmos-container { position: relative; width: 100%; height: 100%; }
69
+ #atmos-canvas { width: 100%; height: 100%; display: block; }
70
+ #atmos-ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
71
+ #atmos-ui * { pointer-events: auto; }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div id="atmos-container">
76
+ <canvas id="atmos-canvas"></canvas>
77
+ <div id="atmos-ui"></div>
78
+ </div>
79
+ <script type="module" src="/${entry}"></script>
80
+ </body>
81
+ </html>`;
82
+ }
83
+
84
+ const VIRTUAL_BUILD_ENTRY = 'virtual:atmos-build-entry';
85
+ const RESOLVED_BUILD_ENTRY = '\0virtual:atmos-build-entry';
86
+
87
+ function copyDirSync(src, dest) {
88
+ fs.mkdirSync(dest, { recursive: true });
89
+ for (const item of fs.readdirSync(src, { withFileTypes: true })) {
90
+ const srcPath = path.join(src, item.name);
91
+ const destPath = path.join(dest, item.name);
92
+ if (item.isDirectory()) {
93
+ copyDirSync(srcPath, destPath);
94
+ } else {
95
+ fs.copyFileSync(srcPath, destPath);
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Collect body data from an IncomingMessage. */
101
+ function collectBody(req) {
102
+ return new Promise((resolve, reject) => {
103
+ const chunks = [];
104
+ req.on('data', (c) => chunks.push(c));
105
+ req.on('end', () => resolve(Buffer.concat(chunks)));
106
+ req.on('error', reject);
107
+ });
108
+ }
109
+
110
+ /** @param {import('./vite-plugin.d.ts').AtmosPluginOptions} [options] */
111
+ export function atmosPlugin(options) {
112
+ const include = options?.include ?? ['src'];
113
+ const exclude = new Set([...DEFAULT_EXCLUDE, ...(options?.exclude ?? [])]);
114
+ const entry = options?.entry ?? 'src/main.ts';
115
+ let root = '';
116
+ let generatedIndex = '';
117
+ let isBuild = false;
118
+
119
+ return {
120
+ name: 'atmos-editor',
121
+
122
+ config(cfg, { command }) {
123
+ root = cfg.root || process.cwd();
124
+ isBuild = command === 'build';
125
+
126
+ if (isBuild) {
127
+ // In build mode, generate player HTML (not editor)
128
+ const indexPath = path.resolve(root, 'index.html');
129
+ const userHasIndex = fs.existsSync(indexPath);
130
+ if (!userHasIndex) {
131
+ fs.writeFileSync(indexPath, generatePlayerHtml(VIRTUAL_BUILD_ENTRY));
132
+ generatedIndex = indexPath;
133
+ }
134
+ return {
135
+ build: {
136
+ target: cfg.build?.target ?? 'esnext',
137
+ rollupOptions: {
138
+ ...cfg.build?.rollupOptions,
139
+ },
140
+ },
141
+ };
142
+ }
143
+
144
+ // Dev mode — generate editor HTML if no index.html exists
145
+ const indexPath = path.resolve(root, 'index.html');
146
+ if (!fs.existsSync(indexPath)) {
147
+ fs.writeFileSync(indexPath, generateEditorHtml(entry));
148
+ generatedIndex = indexPath;
149
+ }
150
+ return {
151
+ build: {
152
+ target: cfg.build?.target ?? 'esnext',
153
+ },
154
+ };
155
+ },
156
+
157
+ resolveId(id) {
158
+ if (id === VIRTUAL_BUILD_ENTRY || id === '/' + VIRTUAL_BUILD_ENTRY) {
159
+ return RESOLVED_BUILD_ENTRY;
160
+ }
161
+ },
162
+
163
+ load(id) {
164
+ if (id !== RESOLVED_BUILD_ENTRY) return;
165
+ let sceneName = 'main';
166
+ try {
167
+ const settingsPath = path.join(root, 'project-settings.json');
168
+ if (fs.existsSync(settingsPath)) {
169
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
170
+ if (settings.defaultScene) sceneName = settings.defaultScene;
171
+ }
172
+ } catch { /* use default */ }
173
+ return `
174
+ import { startPlayer, createEditorPhysics } from '@certe/atmos-editor/player';
175
+ const scriptModules = import.meta.glob('/src/scripts/*.ts', { eager: true });
176
+ try {
177
+ const physics = await createEditorPhysics();
178
+ const app = await startPlayer({
179
+ scene: 'scenes/${sceneName}.scene.json',
180
+ physics,
181
+ scriptModules,
182
+ });
183
+ } catch (err) {
184
+ document.body.style.background = '#111';
185
+ document.body.style.color = '#f88';
186
+ document.body.style.padding = '2em';
187
+ document.body.style.fontFamily = 'monospace';
188
+ document.body.textContent = 'Atmos: ' + (err instanceof Error ? err.message : String(err));
189
+ }
190
+ `;
191
+ },
192
+
193
+ transformIndexHtml: {
194
+ order: 'pre',
195
+ handler(html) {
196
+ if (!isBuild) return html;
197
+ // Replace any existing script src with the virtual build entry
198
+ const hasScript = /<script\s+type="module"\s+src="[^"]*"[^>]*><\/script>/.test(html);
199
+ if (hasScript) {
200
+ return html.replace(
201
+ /<script\s+type="module"\s+src="[^"]*"([^>]*)><\/script>/,
202
+ `<script type="module" src="/${VIRTUAL_BUILD_ENTRY}"$1></script>`,
203
+ );
204
+ }
205
+ // No script tag — inject one before </body>
206
+ return html.replace('</body>', ` <script type="module" src="/${VIRTUAL_BUILD_ENTRY}"></script>\n</body>`);
207
+ },
208
+ },
209
+
210
+ closeBundle() {
211
+ if (generatedIndex) {
212
+ try { fs.unlinkSync(generatedIndex); } catch { /* ignore */ }
213
+ generatedIndex = '';
214
+ }
215
+ },
216
+
217
+ writeBundle(options) {
218
+ if (!isBuild) return;
219
+ const outDir = options.dir || path.resolve(root, 'dist');
220
+ const assetDirs = ['scenes', 'materials', 'textures', 'models'];
221
+ for (const dir of assetDirs) {
222
+ const src = path.resolve(root, dir);
223
+ if (fs.existsSync(src)) {
224
+ copyDirSync(src, path.join(outDir, dir));
225
+ }
226
+ }
227
+ },
228
+
229
+ configureServer(server) {
230
+ root = server.config.root;
231
+
232
+ // Auto-generate index.html when none exists
233
+ const indexPath = path.resolve(root, 'index.html');
234
+ if (!fs.existsSync(indexPath)) {
235
+ server.middlewares.use((req, res, next) => {
236
+ if (req.url === '/' || req.url === '/index.html') {
237
+ const rawHtml = generateEditorHtml(entry);
238
+ server.transformIndexHtml(req.url, rawHtml).then((html) => {
239
+ res.statusCode = 200;
240
+ res.setHeader('Content-Type', 'text/html');
241
+ res.end(html);
242
+ });
243
+ return;
244
+ }
245
+ next();
246
+ });
247
+ }
248
+
249
+ // Serve asset tree as JSON
250
+ server.middlewares.use('/__atmos_assets', (_req, res) => {
251
+ const allEntries = [];
252
+ for (const dir of include) {
253
+ const absDir = path.resolve(root, dir);
254
+ if (fs.existsSync(absDir)) {
255
+ allEntries.push(...scanDirectory(absDir, root, exclude));
256
+ }
257
+ }
258
+ res.setHeader('Content-Type', 'application/json');
259
+ res.end(JSON.stringify({ root, entries: allEntries }));
260
+ });
261
+
262
+ // ── Project filesystem endpoints ──────────────────
263
+
264
+ server.middlewares.use((req, res, next) => {
265
+ if (!req.url || !req.url.startsWith('/__atmos_fs/')) { next(); return; }
266
+
267
+ const url = new URL(req.url, 'http://localhost');
268
+ const action = url.pathname.slice('/__atmos_fs'.length); // e.g. /read, /info
269
+ const filePath = url.searchParams.get('path') ?? '';
270
+
271
+ // /info and /tree don't need a file path
272
+ if (action === '/info') {
273
+ res.setHeader('Content-Type', 'application/json');
274
+ res.end(JSON.stringify({ name: path.basename(root), root }));
275
+ return;
276
+ }
277
+ if (action === '/tree') {
278
+ const allEntries = scanDirectory(root, root, exclude);
279
+ res.setHeader('Content-Type', 'application/json');
280
+ res.end(JSON.stringify(allEntries));
281
+ return;
282
+ }
283
+
284
+ // Prevent path traversal for file operations
285
+ const absPath = path.resolve(root, filePath);
286
+ if (!absPath.startsWith(root)) {
287
+ res.statusCode = 403;
288
+ res.end('Forbidden');
289
+ return;
290
+ }
291
+
292
+ // Handle async actions (write needs body collection)
293
+ handleFsAction(action, absPath, filePath, root, exclude, req, res, server).catch((err) => {
294
+ res.statusCode = 500;
295
+ res.end(String(err));
296
+ });
297
+ });
298
+
299
+ // Watch for file changes and push HMR custom events
300
+ server.watcher.on('all', (event, filePath) => {
301
+ if (event !== 'add' && event !== 'change' && event !== 'unlink') return;
302
+ const relPath = path.relative(root, filePath).replace(/\\/g, '/');
303
+ const inScope = include.some((dir) => relPath.startsWith(dir));
304
+ if (inScope) {
305
+ server.hot.send('atmos:asset-change', { kind: event, path: relPath });
306
+ }
307
+ // Also notify for project files (materials, scenes, etc.)
308
+ if (relPath.startsWith('materials/') || relPath.startsWith('scenes/') || relPath.startsWith('textures/')) {
309
+ server.hot.send('atmos:project-change', { kind: event, path: relPath });
310
+ }
311
+ });
312
+ },
313
+ };
314
+ }
315
+
316
+ async function handleFsAction(action, absPath, filePath, root, exclude, req, res, server) {
317
+ switch (action) {
318
+ case '/read': {
319
+ const data = fs.readFileSync(absPath);
320
+ res.setHeader('Content-Type', 'application/octet-stream');
321
+ res.end(data);
322
+ break;
323
+ }
324
+ case '/read-text': {
325
+ const text = fs.readFileSync(absPath, 'utf-8');
326
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
327
+ res.end(text);
328
+ break;
329
+ }
330
+ case '/write': {
331
+ const body = await collectBody(req);
332
+ const dir = path.dirname(absPath);
333
+ fs.mkdirSync(dir, { recursive: true });
334
+ fs.writeFileSync(absPath, body);
335
+ res.end('ok');
336
+ notifyFileChange(server, root, filePath, 'change');
337
+ break;
338
+ }
339
+ case '/delete': {
340
+ fs.unlinkSync(absPath);
341
+ res.end('ok');
342
+ notifyFileChange(server, root, filePath, 'unlink');
343
+ break;
344
+ }
345
+ case '/exists': {
346
+ res.setHeader('Content-Type', 'application/json');
347
+ res.end(JSON.stringify(fs.existsSync(absPath)));
348
+ break;
349
+ }
350
+ case '/mkdir': {
351
+ fs.mkdirSync(absPath, { recursive: true });
352
+ res.end('ok');
353
+ break;
354
+ }
355
+ case '/list': {
356
+ const dir = filePath || '.';
357
+ const absDir = path.resolve(root, dir);
358
+ const files = listRecursive(absDir, filePath ? `${filePath}/` : '');
359
+ res.setHeader('Content-Type', 'application/json');
360
+ res.end(JSON.stringify(files));
361
+ break;
362
+ }
363
+ default:
364
+ res.statusCode = 404;
365
+ res.end('Unknown action');
366
+ }
367
+ }
368
+
369
+ function notifyFileChange(server, root, filePath, kind) {
370
+ // Send HMR event immediately (don't wait for chokidar watcher)
371
+ server.hot.send('atmos:project-change', { kind, path: filePath });
372
+ }
373
+
374
+ function listRecursive(dirPath, prefix) {
375
+ const results = [];
376
+ let items;
377
+ try {
378
+ items = fs.readdirSync(dirPath, { withFileTypes: true });
379
+ } catch {
380
+ return results;
381
+ }
382
+ for (const item of items) {
383
+ if (item.name.startsWith('.')) continue;
384
+ const full = path.join(dirPath, item.name);
385
+ if (item.isFile()) {
386
+ results.push(prefix + item.name);
387
+ } else if (item.isDirectory()) {
388
+ results.push(...listRecursive(full, `${prefix}${item.name}/`));
389
+ }
390
+ }
391
+ return results.sort();
392
+ }
393
+
394
+ /** @deprecated Use atmosPlugin() instead */
395
+ export const atmosAssetsPlugin = atmosPlugin;