@vgai/editor 0.4.0 → 0.4.1

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 (137) hide show
  1. package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
  2. package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
  3. package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
  4. package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
  5. package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
  6. package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
  7. package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
  8. package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
  9. package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
  10. package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
  11. package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
  12. package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
  13. package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
  14. package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
  15. package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
  16. package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
  17. package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
  18. package/dist/assets/index-DiUacMXR.css +1 -0
  19. package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
  20. package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
  21. package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
  22. package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
  23. package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/dist-server/packaged.mjs +27125 -1075
  26. package/package.json +2 -2
  27. package/server/asset-history-snapshots.ts +189 -0
  28. package/server/asset-library-routes.ts +404 -53
  29. package/server/catalog-import-recipe.ts +97 -0
  30. package/server/catalog-reimport.ts +141 -0
  31. package/server/cloud-asset-catalog.ts +71 -5
  32. package/server/dev.ts +1 -1
  33. package/server/editor-server.ts +106 -51
  34. package/server/editor-sse.ts +5 -0
  35. package/server/generative-execution-context.ts +45 -0
  36. package/server/model-import-conversion.ts +345 -0
  37. package/server/packaged.ts +1 -1
  38. package/server/project-hmr-files.ts +3 -5
  39. package/server/project-output-writer.ts +440 -36
  40. package/server/project-tools.ts +529 -0
  41. package/server/server-utils.ts +14 -11
  42. package/src/EditorContext.tsx +4 -4
  43. package/src/action-registry.ts +12 -0
  44. package/src/asset-editor-context.tsx +9 -0
  45. package/src/asset-events.ts +3 -15
  46. package/src/asset-selection.ts +3 -0
  47. package/src/asset-workflow/asset-capabilities.ts +40 -0
  48. package/src/asset-workflow/asset-import-jobs.ts +108 -0
  49. package/src/asset-workflow/asset-library-query.ts +8 -1
  50. package/src/asset-workflow/asset-workflow-quality.ts +2 -0
  51. package/src/asset-workflow/import-contract.ts +2 -0
  52. package/src/asset-workflow/model-inspection.ts +92 -1
  53. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  54. package/src/asset-workflow/project-asset-commands.ts +1 -0
  55. package/src/asset-workflow/project-asset-health.ts +3 -6
  56. package/src/asset-workflow/project-asset-operations.ts +119 -4
  57. package/src/command-listener.ts +37 -9
  58. package/src/components/ApplicationMenus.tsx +13 -15
  59. package/src/components/AssetBrowser.tsx +301 -26
  60. package/src/components/AssetImportDetails.tsx +207 -92
  61. package/src/components/AssetImportJobStatus.tsx +55 -0
  62. package/src/components/CenterDocuments.tsx +28 -1
  63. package/src/components/CommandPalette.tsx +2 -2
  64. package/src/components/DefaultEditorLayout.tsx +4 -0
  65. package/src/components/InspectorToolSection.tsx +12 -10
  66. package/src/components/OnlineAssetBrowser.tsx +197 -11
  67. package/src/components/ToolHost.tsx +8 -8
  68. package/src/components/ViewportOverlay.tsx +13 -26
  69. package/src/components/ViewportPanel.tsx +19 -14
  70. package/src/components/ViewportShadingMenu.tsx +103 -0
  71. package/src/components/WorkspaceDock.tsx +103 -17
  72. package/src/components/asset-documents.tsx +38 -11
  73. package/src/components/asset-editor-persistence.ts +300 -5
  74. package/src/components/asset-selection-section.tsx +83 -9
  75. package/src/components/asset-viewers/AudioViewer.tsx +4 -7
  76. package/src/components/asset-viewers/ModelViewer.tsx +168 -5
  77. package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
  78. package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
  79. package/src/components/asset-workflow.css +30 -3
  80. package/src/components/primitives/editor-icons.ts +1 -0
  81. package/src/components/project-tool-documents.tsx +298 -0
  82. package/src/components/status-contributions.tsx +38 -0
  83. package/src/components/tool-documents.tsx +20 -17
  84. package/src/components/tool-schema-form.tsx +258 -0
  85. package/src/components/workspace-dock.css +16 -0
  86. package/src/editor-api.ts +159 -22
  87. package/src/editor-store.ts +6 -5
  88. package/src/editor-viewport.ts +12 -7
  89. package/src/game-viewport-state.ts +28 -0
  90. package/src/model-thumbnail.ts +97 -9
  91. package/src/play-mode.ts +30 -3
  92. package/src/project-tool-discovery.ts +5 -5
  93. package/src/{project-operations.ts → project-tools.ts} +39 -26
  94. package/src/scene-sync.ts +0 -67
  95. package/src/storage/handle-storage.ts +5 -1
  96. package/src/storage/http-storage.ts +7 -2
  97. package/src/storage/mem-storage.ts +7 -1
  98. package/src/storage/types.ts +2 -0
  99. package/src/theme-css.ts +17 -7
  100. package/src/tool-loader.ts +152 -241
  101. package/src/viewport-shading-boundary.ts +15 -0
  102. package/src/widgets/index.ts +2 -1
  103. package/src/workspace-dock-controller.ts +8 -0
  104. package/src/workspace-document-registry.ts +4 -4
  105. package/src/workspace-state-persistence.ts +36 -10
  106. package/template/.claude/skills/editor/SKILL.md +13 -3
  107. package/template/AGENTS.md +100 -83
  108. package/template/package.json +16 -0
  109. package/template/public/scenes/default.vscn.json +1 -2
  110. package/template/scripts/playtest.ts +7 -37
  111. package/template/server/rooms/game-room.ts +2 -2
  112. package/template/src/data/README.md +3 -2
  113. package/template/src/data/tuning.data.json +1 -3
  114. package/template/src/data/tuning.schema.json +0 -12
  115. package/template/src/data/tuning.schema.ts +2 -9
  116. package/template/src/data/tuning.ts +1 -1
  117. package/template/src/scripts/registry.ts +0 -3
  118. package/template/src/tools/README.md +151 -16
  119. package/template/src/tools/autoplay.tool.ts +31 -0
  120. package/template/src/tools/autoplay.ts +15 -0
  121. package/template/src/tools/tuning.document.tsx +107 -0
  122. package/template/src/tools/tuning.tool.ts +24 -0
  123. package/template/src/tools/tuning.ts +9 -0
  124. package/template/src/ui/Button.stories.tsx +1 -1
  125. package/template/tests/acceptance/example.spec.ts +4 -59
  126. package/template/vgai.game.json +1 -1
  127. package/dist/assets/index-bAGhEnnO.css +0 -1
  128. package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
  129. package/dist/project-scripts.bundle.js +0 -3917
  130. package/server/project-operations.ts +0 -343
  131. package/src/components/project-operation-documents.tsx +0 -226
  132. package/template/src/operations/README.md +0 -88
  133. package/template/src/scripts/components/data-spinner.ts +0 -86
  134. package/template/src/scripts/components/spin-lap-math.ts +0 -27
  135. package/template/src/tools/example.tool.tsx +0 -97
  136. package/template/src/tools/spin.tool.tsx +0 -101
  137. package/template/tests/logic/example.test.ts +0 -31
@@ -0,0 +1,45 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { ProjectProviderExecution } from '../../vgai-sdk/src/types';
3
+
4
+ export const GENERATIVE_EXECUTION_RECORDER = Symbol.for('vgai.generative-execution-recorder.v1');
5
+
6
+ type ExecutionFacts = ProjectProviderExecution;
7
+
8
+ interface ExecutionCapture {
9
+ pending: ExecutionFacts[];
10
+ }
11
+
12
+ const captures = new AsyncLocalStorage<ExecutionCapture>();
13
+
14
+ function validFacts(value: unknown): value is ExecutionFacts {
15
+ if (!value || typeof value !== 'object') return false;
16
+ const candidate = value as Partial<ExecutionFacts>;
17
+ return (
18
+ (candidate.mode === 'mock' || candidate.mode === 'direct' || candidate.mode === 'managed') &&
19
+ typeof candidate.provider === 'string' &&
20
+ candidate.provider.length > 0
21
+ );
22
+ }
23
+
24
+ function installRecorder(): void {
25
+ const globals = globalThis as typeof globalThis & { [GENERATIVE_EXECUTION_RECORDER]?: unknown };
26
+ if (globals[GENERATIVE_EXECUTION_RECORDER] !== undefined) return;
27
+ globals[GENERATIVE_EXECUTION_RECORDER] = (facts: unknown) => {
28
+ if (!validFacts(facts)) return;
29
+ captures.getStore()?.pending.push({ ...facts });
30
+ };
31
+ }
32
+
33
+ installRecorder();
34
+
35
+ /** Runs one project tool with provider execution capture isolated across async work. */
36
+ export function runWithGenerativeExecutionCapture<T>(work: () => Promise<T>): Promise<T> {
37
+ return captures.run({ pending: [] }, work);
38
+ }
39
+
40
+ /** Consumes the ordered native provider execution chain associated with the next output commit. */
41
+ export function consumeGenerativeExecutions(): ExecutionFacts[] {
42
+ const capture = captures.getStore();
43
+ if (!capture || capture.pending.length === 0) return [];
44
+ return capture.pending.splice(0);
45
+ }
@@ -0,0 +1,345 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, extname } from 'node:path';
6
+ import * as THREE from 'three';
7
+ import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
8
+ import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
9
+ import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
10
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
11
+ import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
12
+ import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
13
+ import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
14
+ import { STLLoader } from 'three/addons/loaders/STLLoader.js';
15
+ import { TDSLoader } from 'three/addons/loaders/TDSLoader.js';
16
+ import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';
17
+ import {
18
+ type AssetImportSettings,
19
+ assetImportSettingsSchema,
20
+ } from '../src/asset-workflow/import-contract';
21
+
22
+ export const SERVER_CONVERTIBLE_MODEL_FORMATS = new Set(['fbx', 'obj', 'dae', 'stl', 'ply', '3ds']);
23
+
24
+ /** Convert a staged source model to a runtime GLB without requiring Blender. */
25
+ export async function convertStagedModelToGlb(
26
+ primaryPath: string,
27
+ format: string,
28
+ rawSettings: Partial<AssetImportSettings> | number = {},
29
+ ): Promise<Uint8Array> {
30
+ const settings = assetImportSettingsSchema.parse(
31
+ typeof rawSettings === 'number' ? { scale: rawSettings } : rawSettings,
32
+ );
33
+ installNodeDom();
34
+ const bytes = new Uint8Array(await readFile(primaryPath));
35
+ const object = await parseSourceModel(primaryPath, format.toLowerCase(), bytes);
36
+ discardUnloadedTextures(object);
37
+ applyModelImportSettings(object, settings);
38
+ installNodeFileReader();
39
+ try {
40
+ const result = await new Promise<ArrayBuffer>((resolve, reject) => {
41
+ new GLTFExporter().parse(
42
+ object,
43
+ (output) =>
44
+ output instanceof ArrayBuffer
45
+ ? resolve(output)
46
+ : reject(new Error('Model conversion produced JSON instead of binary GLB.')),
47
+ reject,
48
+ { binary: true, animations: object.animations, onlyVisible: false },
49
+ );
50
+ });
51
+ const output = new Uint8Array(result);
52
+ await validateConvertedGlb(output);
53
+ return output;
54
+ } finally {
55
+ disposeObject(object);
56
+ }
57
+ }
58
+
59
+ const materialTextureSlots = [
60
+ 'map',
61
+ 'alphaMap',
62
+ 'aoMap',
63
+ 'bumpMap',
64
+ 'displacementMap',
65
+ 'emissiveMap',
66
+ 'envMap',
67
+ 'lightMap',
68
+ 'metalnessMap',
69
+ 'normalMap',
70
+ 'roughnessMap',
71
+ 'specularMap',
72
+ ] as const;
73
+
74
+ /** Apply the persisted conversion settings before the runtime GLB is exported. */
75
+ export function applyModelImportSettings(
76
+ object: THREE.Object3D,
77
+ settings: AssetImportSettings,
78
+ ): void {
79
+ object.scale.multiplyScalar(settings.scale * sourceUnitScale(settings.sourceUnits));
80
+ object.quaternion.premultiply(importAxisRotation(settings.upAxis, settings.forwardAxis));
81
+
82
+ const discardedTextures = new Set<THREE.Texture>();
83
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one traversal deliberately applies the persisted mesh, material, texture, normal, tangent, and optimization settings to each converted renderable.
84
+ object.traverse((node) => {
85
+ const mesh = node as THREE.Mesh;
86
+ if (!mesh.geometry) return;
87
+ let geometry = mesh.geometry;
88
+ if (settings.meshOptimization !== 'none') {
89
+ const optimized = mergeVertices(
90
+ geometry,
91
+ settings.meshOptimization === 'aggressive' ? 1e-3 : 1e-6,
92
+ );
93
+ if (optimized !== geometry) {
94
+ mesh.geometry = optimized;
95
+ geometry.dispose();
96
+ geometry = optimized;
97
+ }
98
+ }
99
+ if (settings.normals !== 'preserve') {
100
+ geometry.deleteAttribute('normal');
101
+ geometry.computeVertexNormals();
102
+ }
103
+ if (settings.tangents === 'discard') geometry.deleteAttribute('tangent');
104
+ if (
105
+ settings.tangents === 'generate' &&
106
+ geometry.index &&
107
+ geometry.hasAttribute('position') &&
108
+ geometry.hasAttribute('normal') &&
109
+ geometry.hasAttribute('uv')
110
+ ) {
111
+ geometry.computeTangents();
112
+ }
113
+ geometry.computeBoundingBox();
114
+ geometry.computeBoundingSphere();
115
+
116
+ const priorMaterials = Array.isArray(mesh.material)
117
+ ? mesh.material
118
+ : mesh.material
119
+ ? [mesh.material]
120
+ : [];
121
+ if (settings.materials === 'discard') {
122
+ mesh.material = Array.isArray(mesh.material)
123
+ ? priorMaterials.map(() => new THREE.MeshStandardMaterial())
124
+ : new THREE.MeshStandardMaterial();
125
+ for (const material of priorMaterials) material.dispose();
126
+ return;
127
+ }
128
+ if (settings.textures !== 'discard') return;
129
+ for (const material of priorMaterials) {
130
+ const textured = material as THREE.Material & Record<string, unknown>;
131
+ for (const slot of materialTextureSlots) {
132
+ const texture = textured[slot];
133
+ if (texture instanceof THREE.Texture) discardedTextures.add(texture);
134
+ textured[slot] = null;
135
+ }
136
+ material.needsUpdate = true;
137
+ }
138
+ });
139
+ for (const texture of discardedTextures) texture.dispose();
140
+
141
+ if (settings.animations.length > 0) {
142
+ const requested = new Set(settings.animations);
143
+ const selected = object.animations.filter((clip) => requested.has(clip.name));
144
+ const missing = settings.animations.filter(
145
+ (name) => !selected.some((clip) => clip.name === name),
146
+ );
147
+ if (missing.length > 0) throw new Error(`Animation clips not found: ${missing.join(', ')}.`);
148
+ object.animations = selected;
149
+ }
150
+ object.updateWorldMatrix(true, true);
151
+ }
152
+
153
+ function sourceUnitScale(units: AssetImportSettings['sourceUnits']): number {
154
+ return { auto: 1, mm: 0.001, cm: 0.01, m: 1, in: 0.0254, ft: 0.3048 }[units];
155
+ }
156
+
157
+ function importAxisRotation(
158
+ upAxis: AssetImportSettings['upAxis'],
159
+ forwardAxis: AssetImportSettings['forwardAxis'],
160
+ ): THREE.Quaternion {
161
+ const up = upAxis === 'auto' ? null : axisVector(upAxis);
162
+ const forward = forwardAxis === 'auto' ? null : axisVector(forwardAxis);
163
+ if (up && forward) {
164
+ if (Math.abs(up.dot(forward)) > 0.001)
165
+ throw new Error('Import up and forward axes must be perpendicular.');
166
+ const right = new THREE.Vector3().crossVectors(forward, up).normalize();
167
+ const correctedForward = new THREE.Vector3().crossVectors(up, right).normalize();
168
+ const sourceBasis = new THREE.Matrix4().makeBasis(right, up, correctedForward.clone().negate());
169
+ return new THREE.Quaternion().setFromRotationMatrix(sourceBasis.invert());
170
+ }
171
+ if (up) return new THREE.Quaternion().setFromUnitVectors(up, new THREE.Vector3(0, 1, 0));
172
+ if (forward)
173
+ return new THREE.Quaternion().setFromUnitVectors(forward, new THREE.Vector3(0, 0, -1));
174
+ return new THREE.Quaternion();
175
+ }
176
+
177
+ function axisVector(axis: Exclude<AssetImportSettings['forwardAxis'], 'auto'> | 'x' | 'y' | 'z') {
178
+ const sign = axis.startsWith('-') ? -1 : 1;
179
+ const name = axis.replace('-', '');
180
+ return new THREE.Vector3(
181
+ name === 'x' ? sign : 0,
182
+ name === 'y' ? sign : 0,
183
+ name === 'z' ? sign : 0,
184
+ );
185
+ }
186
+
187
+ async function parseSourceModel(
188
+ primaryPath: string,
189
+ format: string,
190
+ bytes: Uint8Array,
191
+ ): Promise<THREE.Object3D> {
192
+ const resourcePath = `${dirname(primaryPath)}/`;
193
+ switch (format) {
194
+ case 'fbx':
195
+ return new FBXLoader().parse(bytes.buffer as ArrayBuffer, resourcePath);
196
+ case 'obj': {
197
+ const loader = new OBJLoader();
198
+ const materialPath = primaryPath.replace(/\.obj$/i, '.mtl');
199
+ try {
200
+ const materials = new MTLLoader().parse(await readFile(materialPath, 'utf8'), resourcePath);
201
+ materials.preload();
202
+ loader.setMaterials(materials);
203
+ } catch {
204
+ // Geometry-only OBJ conversion remains valid when no MTL is present.
205
+ }
206
+ return loader.parse(new TextDecoder().decode(bytes));
207
+ }
208
+ case 'dae': {
209
+ return new ColladaLoader().parse(new TextDecoder().decode(bytes), resourcePath).scene;
210
+ }
211
+ case 'stl': {
212
+ const geometry = new STLLoader().parse(bytes.buffer as ArrayBuffer);
213
+ return new THREE.Mesh(
214
+ geometry,
215
+ new THREE.MeshStandardMaterial({ color: 0x8fb5da, roughness: 0.75 }),
216
+ );
217
+ }
218
+ case 'ply': {
219
+ const geometry = new PLYLoader().parse(bytes.buffer as ArrayBuffer);
220
+ if (!geometry.hasAttribute('normal')) geometry.computeVertexNormals();
221
+ return new THREE.Mesh(
222
+ geometry,
223
+ new THREE.MeshStandardMaterial({
224
+ color: geometry.hasAttribute('color') ? 0xffffff : 0xb8d09b,
225
+ vertexColors: geometry.hasAttribute('color'),
226
+ roughness: 0.8,
227
+ side: THREE.DoubleSide,
228
+ }),
229
+ );
230
+ }
231
+ case '3ds':
232
+ return new TDSLoader().parse(bytes.buffer as ArrayBuffer, resourcePath);
233
+ default:
234
+ throw new Error(`Built-in conversion does not support ${format || extname(primaryPath)}.`);
235
+ }
236
+ }
237
+
238
+ /** Three's source loaders construct browser image elements even when the
239
+ * conversion runs in Node. JSDOM supplies that parser boundary; unresolved
240
+ * asynchronous images are removed below rather than passed to GLTFExporter
241
+ * as invalid zero-sized image objects. */
242
+ function installNodeDom(): void {
243
+ if (typeof document !== 'undefined' && typeof DOMParser !== 'undefined') return;
244
+ const { JSDOM } = createRequire(import.meta.url)('jsdom') as {
245
+ JSDOM: new () => {
246
+ window: {
247
+ document: Document;
248
+ DOMParser: typeof DOMParser;
249
+ HTMLImageElement: typeof HTMLImageElement;
250
+ HTMLCanvasElement: typeof HTMLCanvasElement;
251
+ Image: typeof Image;
252
+ };
253
+ };
254
+ };
255
+ const window = new JSDOM().window;
256
+ Object.assign(globalThis, {
257
+ document: window.document,
258
+ DOMParser: window.DOMParser,
259
+ HTMLImageElement: window.HTMLImageElement,
260
+ HTMLCanvasElement: window.HTMLCanvasElement,
261
+ Image: window.Image,
262
+ });
263
+ }
264
+
265
+ function discardUnloadedTextures(root: THREE.Object3D): void {
266
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one bounded traversal removes only unresolved image slots across every supported material shape.
267
+ root.traverse((object) => {
268
+ const mesh = object as THREE.Mesh;
269
+ const materials = Array.isArray(mesh.material)
270
+ ? mesh.material
271
+ : mesh.material
272
+ ? [mesh.material]
273
+ : [];
274
+ for (const material of materials) {
275
+ const textured = material as THREE.Material & Record<string, unknown>;
276
+ for (const slot of materialTextureSlots) {
277
+ const value = textured[slot];
278
+ if (!(value instanceof THREE.Texture)) continue;
279
+ const image = value.image as { width?: number; height?: number } | null;
280
+ if (image && (image.width ?? 0) > 0 && (image.height ?? 0) > 0) continue;
281
+ textured[slot] = null;
282
+ value.dispose();
283
+ }
284
+ material.needsUpdate = true;
285
+ }
286
+ });
287
+ }
288
+
289
+ async function validateConvertedGlb(bytes: Uint8Array): Promise<void> {
290
+ const parsed = await new GLTFLoader().parseAsync(Uint8Array.from(bytes).buffer, '');
291
+ let renderable = false;
292
+ parsed.scene.traverse((object) => {
293
+ renderable ||= Boolean((object as THREE.Mesh).isMesh || (object as THREE.Points).isPoints);
294
+ });
295
+ disposeObject(parsed.scene);
296
+ if (!renderable && parsed.animations.length === 0) {
297
+ throw new Error('Converted GLB contains neither renderable geometry nor animation clips.');
298
+ }
299
+ }
300
+
301
+ function installNodeFileReader(): void {
302
+ if (typeof FileReader !== 'undefined') return;
303
+ class NodeFileReader {
304
+ result: string | ArrayBuffer | null = null;
305
+ error: Error | null = null;
306
+ onloadend: ((event: ProgressEvent<FileReader>) => void) | null = null;
307
+ onerror: ((event: ProgressEvent<FileReader>) => void) | null = null;
308
+ readAsArrayBuffer(blob: Blob): void {
309
+ void blob.arrayBuffer().then(
310
+ (result) => {
311
+ this.result = result;
312
+ this.onloadend?.({ target: this } as unknown as ProgressEvent<FileReader>);
313
+ },
314
+ (error) => {
315
+ this.error = error instanceof Error ? error : new Error(String(error));
316
+ this.onerror?.({ target: this } as unknown as ProgressEvent<FileReader>);
317
+ },
318
+ );
319
+ }
320
+ readAsDataURL(blob: Blob): void {
321
+ void blob.arrayBuffer().then((result) => {
322
+ this.result = `data:${blob.type || 'application/octet-stream'};base64,${Buffer.from(result).toString('base64')}`;
323
+ this.onloadend?.({ target: this } as unknown as ProgressEvent<FileReader>);
324
+ });
325
+ }
326
+ }
327
+ Object.assign(globalThis, { FileReader: NodeFileReader });
328
+ }
329
+
330
+ function disposeObject(root: THREE.Object3D): void {
331
+ root.traverse((object) => {
332
+ const renderable = object as THREE.Mesh;
333
+ renderable.geometry?.dispose();
334
+ const materials = Array.isArray(renderable.material)
335
+ ? renderable.material
336
+ : renderable.material
337
+ ? [renderable.material]
338
+ : [];
339
+ for (const material of materials) {
340
+ for (const value of Object.values(material))
341
+ if (value instanceof THREE.Texture) value.dispose();
342
+ material.dispose();
343
+ }
344
+ });
345
+ }
@@ -543,7 +543,7 @@ async function main(): Promise<void> {
543
543
  const editorRouter = createEditorServer({
544
544
  engineRoot: editorPackageRoot,
545
545
  projectPath,
546
- loadProjectModule: (moduleId) => vite.ssrLoadModule(moduleId),
546
+ loadProjectModule: (moduleId: string) => vite.ssrLoadModule(moduleId),
547
547
  onProjectOpened(newProjectPath: string) {
548
548
  const allowed = vite.config.server.fs.allow;
549
549
  if (!allowed.includes(newProjectPath)) {
@@ -28,11 +28,9 @@ export function classifyProjectHotUpdate(
28
28
  if (relative === '' || relative === '..' || relative.startsWith('../')) return 'outside';
29
29
  if (relative === 'vgai.game.json') return 'restart';
30
30
  if (!relative.startsWith('src/')) return 'ignore';
31
- // Project-operation wrappers are editor/server tooling. A game that wants
32
- // shared generation imports the underlying ordinary module, not this
33
- // discoverability wrapper, so editing an operation must not request a full
34
- // game restart.
35
- if (/^src\/operations\/.*\.operation\.(ts|js)$/.test(relative)) return 'ignore';
31
+ // Registered tool definitions/surfaces are editor/server tooling. Game
32
+ // code imports their ordinary underlying functions directly.
33
+ if (/^src\/tools\/.*\.(tool\.(ts|js)|surface\.(tsx|jsx))$/.test(relative)) return 'ignore';
36
34
  if (/\.data\.json$/.test(relative)) return 'stock-data';
37
35
  if (!/\.(ts|tsx|js|jsx)$/.test(relative)) return 'ignore';
38
36
  if (/\bextends\s+(?:[A-Za-z_$][\w$]*\.)?GameComponent\b/.test(source)) return 'component';