@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
@@ -4,14 +4,16 @@ import {
4
4
  assetImportPlanSchema,
5
5
  importRecipePath,
6
6
  } from '../asset-workflow/import-contract';
7
- import { Button, danger, SectionHeader, Select, TextInput, text } from '../design-system';
7
+ import { Button, Checkbox, danger, SectionHeader, Select, TextInput, text } from '../design-system';
8
+ import { revealInFinder } from '../editor-api';
8
9
  import { getStorageBackend } from '../storage';
9
- import { writeProjectAsset } from './asset-editor-persistence';
10
+ import { reimportProjectAssetWithHistory, writeProjectAsset } from './asset-editor-persistence';
10
11
 
11
12
  export function AssetImportDetails({ assetPath }: { assetPath: string }) {
12
13
  const [saved, setSaved] = useState<AssetImportPlan | null>(null);
13
14
  const [draft, setDraft] = useState<AssetImportPlan | null>(null);
14
15
  const [error, setError] = useState<string | null>(null);
16
+ const [reimporting, setReimporting] = useState(false);
15
17
  useEffect(() => {
16
18
  let cancelled = false;
17
19
  const path = importRecipePath(assetPath);
@@ -77,91 +79,185 @@ export function AssetImportDetails({ assetPath }: { assetPath: string }) {
77
79
  setDraft((current) =>
78
80
  current ? { ...current, settings: { ...current.settings, [key]: value } } : current,
79
81
  );
82
+ const conversionSettingsSupported = draft.backend === 'threejs';
80
83
  return (
81
84
  <>
82
85
  <SectionHeader label="Import settings">
83
- <label>
84
- Scale{' '}
85
- <TextInput
86
- aria-label="Import scale"
87
- type="number"
88
- min="0.0001"
89
- step="0.01"
90
- value={draft.settings.scale}
91
- onChange={(event) => setSetting('scale', Number(event.target.value))}
92
- />
93
- </label>
94
- <label>
95
- Units{' '}
96
- <Select
97
- value={draft.settings.sourceUnits}
98
- onChange={(event) =>
99
- setSetting(
100
- 'sourceUnits',
101
- event.target.value as AssetImportPlan['settings']['sourceUnits'],
102
- )
103
- }
104
- >
105
- {['auto', 'mm', 'cm', 'm', 'in', 'ft'].map((value) => (
106
- <option key={value}>{value}</option>
107
- ))}
108
- </Select>
109
- </label>
110
- <label>
111
- Up axis{' '}
112
- <Select
113
- value={draft.settings.upAxis}
114
- onChange={(event) =>
115
- setSetting('upAxis', event.target.value as AssetImportPlan['settings']['upAxis'])
116
- }
117
- >
118
- {['auto', 'x', 'y', 'z'].map((value) => (
119
- <option key={value}>{value}</option>
120
- ))}
121
- </Select>
122
- </label>
123
- <label>
124
- Normals{' '}
125
- <Select
126
- value={draft.settings.normals}
127
- onChange={(event) =>
128
- setSetting('normals', event.target.value as AssetImportPlan['settings']['normals'])
129
- }
130
- >
131
- {['preserve', 'generate', 'recalculate'].map((value) => (
132
- <option key={value}>{value}</option>
133
- ))}
134
- </Select>
135
- </label>
136
- <label>
137
- Textures{' '}
138
- <Select
139
- value={draft.settings.textures}
140
- onChange={(event) =>
141
- setSetting('textures', event.target.value as AssetImportPlan['settings']['textures'])
142
- }
143
- >
144
- {['copy', 'embed', 'discard'].map((value) => (
145
- <option key={value}>{value}</option>
146
- ))}
147
- </Select>
148
- </label>
149
- <label>
150
- Optimization{' '}
151
- <Select
152
- value={draft.settings.meshOptimization}
153
- onChange={(event) =>
154
- setSetting(
155
- 'meshOptimization',
156
- event.target.value as AssetImportPlan['settings']['meshOptimization'],
157
- )
158
- }
159
- >
160
- {['none', 'safe', 'aggressive'].map((value) => (
161
- <option key={value}>{value}</option>
162
- ))}
163
- </Select>
164
- </label>
86
+ {conversionSettingsSupported ? (
87
+ <>
88
+ <label>
89
+ Scale{' '}
90
+ <TextInput
91
+ aria-label="Import scale"
92
+ type="number"
93
+ min="0.0001"
94
+ step="0.01"
95
+ value={draft.settings.scale}
96
+ onChange={(event) => setSetting('scale', Number(event.target.value))}
97
+ />
98
+ </label>
99
+ <label>
100
+ Units{' '}
101
+ <Select
102
+ aria-label="Source units"
103
+ value={draft.settings.sourceUnits}
104
+ onChange={(event) =>
105
+ setSetting(
106
+ 'sourceUnits',
107
+ event.target.value as AssetImportPlan['settings']['sourceUnits'],
108
+ )
109
+ }
110
+ >
111
+ {['auto', 'mm', 'cm', 'm', 'in', 'ft'].map((value) => (
112
+ <option key={value}>{value}</option>
113
+ ))}
114
+ </Select>
115
+ </label>
116
+ <label>
117
+ Up axis{' '}
118
+ <Select
119
+ aria-label="Source up axis"
120
+ value={draft.settings.upAxis}
121
+ onChange={(event) =>
122
+ setSetting('upAxis', event.target.value as AssetImportPlan['settings']['upAxis'])
123
+ }
124
+ >
125
+ {['auto', 'x', 'y', 'z'].map((value) => (
126
+ <option key={value}>{value}</option>
127
+ ))}
128
+ </Select>
129
+ </label>
130
+ <label>
131
+ Forward axis{' '}
132
+ <Select
133
+ aria-label="Source forward axis"
134
+ value={draft.settings.forwardAxis}
135
+ onChange={(event) =>
136
+ setSetting(
137
+ 'forwardAxis',
138
+ event.target.value as AssetImportPlan['settings']['forwardAxis'],
139
+ )
140
+ }
141
+ >
142
+ {['auto', 'x', '-x', 'y', '-y', 'z', '-z'].map((value) => (
143
+ <option key={value}>{value}</option>
144
+ ))}
145
+ </Select>
146
+ </label>
147
+ <label>
148
+ Normals{' '}
149
+ <Select
150
+ aria-label="Normal handling"
151
+ value={draft.settings.normals}
152
+ onChange={(event) =>
153
+ setSetting(
154
+ 'normals',
155
+ event.target.value as AssetImportPlan['settings']['normals'],
156
+ )
157
+ }
158
+ >
159
+ {['preserve', 'generate', 'recalculate'].map((value) => (
160
+ <option key={value}>{value}</option>
161
+ ))}
162
+ </Select>
163
+ </label>
164
+ <label>
165
+ Tangents{' '}
166
+ <Select
167
+ aria-label="Tangent handling"
168
+ value={draft.settings.tangents}
169
+ onChange={(event) =>
170
+ setSetting(
171
+ 'tangents',
172
+ event.target.value as AssetImportPlan['settings']['tangents'],
173
+ )
174
+ }
175
+ >
176
+ {['preserve', 'generate', 'discard'].map((value) => (
177
+ <option key={value}>{value}</option>
178
+ ))}
179
+ </Select>
180
+ </label>
181
+ <label>
182
+ Materials{' '}
183
+ <Select
184
+ aria-label="Material handling"
185
+ value={draft.settings.materials}
186
+ onChange={(event) =>
187
+ setSetting(
188
+ 'materials',
189
+ event.target.value as AssetImportPlan['settings']['materials'],
190
+ )
191
+ }
192
+ >
193
+ {['import', 'discard'].map((value) => (
194
+ <option key={value}>{value}</option>
195
+ ))}
196
+ </Select>
197
+ </label>
198
+ <label>
199
+ Textures{' '}
200
+ <Select
201
+ aria-label="Texture handling"
202
+ value={draft.settings.textures}
203
+ onChange={(event) =>
204
+ setSetting(
205
+ 'textures',
206
+ event.target.value as AssetImportPlan['settings']['textures'],
207
+ )
208
+ }
209
+ >
210
+ {['copy', 'embed', 'discard'].map((value) => (
211
+ <option key={value}>{value}</option>
212
+ ))}
213
+ </Select>
214
+ </label>
215
+ <label>
216
+ Animation clips{' '}
217
+ <TextInput
218
+ aria-label="Animation clips"
219
+ placeholder="All clips"
220
+ value={draft.settings.animations.join(', ')}
221
+ onChange={(event) =>
222
+ setSetting(
223
+ 'animations',
224
+ event.target.value
225
+ .split(',')
226
+ .map((value) => value.trim())
227
+ .filter(Boolean),
228
+ )
229
+ }
230
+ />
231
+ </label>
232
+ <label>
233
+ Optimization{' '}
234
+ <Select
235
+ aria-label="Mesh optimization"
236
+ value={draft.settings.meshOptimization}
237
+ onChange={(event) =>
238
+ setSetting(
239
+ 'meshOptimization',
240
+ event.target.value as AssetImportPlan['settings']['meshOptimization'],
241
+ )
242
+ }
243
+ >
244
+ {['none', 'safe', 'aggressive'].map((value) => (
245
+ <option key={value}>{value}</option>
246
+ ))}
247
+ </Select>
248
+ </label>
249
+ <label>
250
+ <Checkbox
251
+ aria-label="Retain source files"
252
+ checked={draft.settings.retainSource}
253
+ onChange={(event) => setSetting('retainSource', event.target.checked)}
254
+ />{' '}
255
+ Retain source files
256
+ </label>
257
+ </>
258
+ ) : (
259
+ <div>This runtime-native asset is copied without conversion settings.</div>
260
+ )}
165
261
  <div style={{ color: changes.length ? text[1] : text[3] }}>
166
262
  Dry-run impact:{' '}
167
263
  {changes.length ? `rebuild output; changed ${changes.join(', ')}` : 'no output changes'}
@@ -186,15 +282,34 @@ export function AssetImportDetails({ assetPath }: { assetPath: string }) {
186
282
  </Button>
187
283
  <Button
188
284
  type="button"
189
- onClick={() =>
190
- window.dispatchEvent(
191
- new CustomEvent('editor:reimport-project-asset', {
192
- detail: { path: assetPath, plan: draft },
193
- }),
194
- )
195
- }
285
+ disabled={reimporting}
286
+ onClick={async () => {
287
+ setReimporting(true);
288
+ setError(null);
289
+ try {
290
+ await reimportProjectAssetWithHistory(assetPath, draft);
291
+ setSaved(draft);
292
+ } catch (cause) {
293
+ setError(cause instanceof Error ? cause.message : String(cause));
294
+ } finally {
295
+ setReimporting(false);
296
+ }
297
+ }}
298
+ >
299
+ {reimporting ? 'Reimporting…' : 'Reimport'}
300
+ </Button>
301
+ <Button
302
+ type="button"
303
+ onClick={async () => {
304
+ setError(null);
305
+ try {
306
+ await revealInFinder(assetPath);
307
+ } catch (cause) {
308
+ setError(cause instanceof Error ? cause.message : String(cause));
309
+ }
310
+ }}
196
311
  >
197
- Reimport
312
+ Reveal source
198
313
  </Button>
199
314
  </div>
200
315
  {error && <div style={{ color: danger }}>{error}</div>}
@@ -0,0 +1,55 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import {
3
+ assetImportJobs,
4
+ assetImportJobsVersion,
5
+ cancelAssetImportJob,
6
+ clearFinishedAssetImportJobs,
7
+ subscribeAssetImportJobs,
8
+ } from '../asset-workflow/asset-import-jobs';
9
+ import { Button } from '../design-system';
10
+
11
+ export function AssetImportJobStatus() {
12
+ useSyncExternalStore(subscribeAssetImportJobs, assetImportJobsVersion);
13
+ const jobs = assetImportJobs();
14
+ if (!jobs.length) return null;
15
+ const summary: Record<string, number> = {};
16
+ for (const job of jobs) summary[job.status] = (summary[job.status] ?? 0) + 1;
17
+ return (
18
+ <div role="status" aria-live="polite" className="vgai-asset-import-jobs">
19
+ <strong>Imports</strong> ·{' '}
20
+ {Object.entries(summary)
21
+ .map(([status, count]) => `${count} ${status}`)
22
+ .join(' · ')}
23
+ {jobs.map((job) => (
24
+ <span key={job.id} title={job.message} data-status={job.status}>
25
+ {job.label}: {job.stage}{' '}
26
+ {job.total ? `${Math.round((job.loaded / job.total) * 100)}%` : job.status}
27
+ {job.status === 'running' && (
28
+ <Button
29
+ type="button"
30
+ variant="ghost"
31
+ size="compact"
32
+ aria-label={`Cancel ${job.label}`}
33
+ onClick={() => cancelAssetImportJob(job.id)}
34
+ >
35
+ Cancel
36
+ </Button>
37
+ )}
38
+ {(job.status === 'failed' || job.status === 'cancelled') && job.retry && (
39
+ <Button type="button" variant="ghost" size="compact" onClick={job.retry}>
40
+ Retry {job.label}
41
+ </Button>
42
+ )}
43
+ {job.status === 'failed' && job.message && (
44
+ <span className="vgai-asset-import-error">{job.message}</span>
45
+ )}
46
+ </span>
47
+ ))}
48
+ {jobs.some((job) => job.status !== 'running') && (
49
+ <Button type="button" variant="ghost" size="compact" onClick={clearFinishedAssetImportJobs}>
50
+ Clear
51
+ </Button>
52
+ )}
53
+ </div>
54
+ );
55
+ }
@@ -47,7 +47,19 @@ import {
47
47
  import { DisclosureIcon } from '../design-system';
48
48
  import { useEditorStore } from '../EditorContext';
49
49
  import { editorConsole } from '../editor-console';
50
- import { getGameProfiler, resizeGame, setGameContainer } from '../play-mode';
50
+ import {
51
+ gameViewportShadingMode,
52
+ gameViewportShadingVersion,
53
+ setGameDocumentShadingMode,
54
+ subscribeGameViewportShading,
55
+ } from '../game-viewport-state';
56
+ import {
57
+ gameViewportShadingSupported,
58
+ getGameProfiler,
59
+ resizeGame,
60
+ setGameContainer,
61
+ setGameViewportShadingMode,
62
+ } from '../play-mode';
51
63
  import { GAME_DOCUMENT_ID, SCENE_DOCUMENT_ID } from '../workspace-document-ids';
52
64
  import {
53
65
  activateWorkspaceDocument,
@@ -64,6 +76,7 @@ import { openDebuggerPanel } from './debugger-panel-state';
64
76
  import { GamePerformanceOverlay } from './GamePerformanceOverlay';
65
77
  import { MuteButton, type Resolution, ResolutionPicker } from './PlayBar';
66
78
  import { ViewportPanel } from './ViewportPanel';
79
+ import { ViewportShadingMenu } from './ViewportShadingMenu';
67
80
 
68
81
  /** Stable identity (§7.1) of the single active-scene center document. The
69
82
  * SLOT is stable while the scene it shows can change (root activation,
@@ -286,7 +299,10 @@ function GameStatsMenu() {
286
299
  * Transport (play/pause/stop) deliberately stays in the global `PlayBar`.
287
300
  */
288
301
  export function GameDocumentToolbar(_props: WorkspaceDocumentContentProps) {
302
+ const store = useEditorStore();
303
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
289
304
  useSyncExternalStore(subscribeGameView, gameViewVersion);
305
+ useSyncExternalStore(subscribeGameViewportShading, gameViewportShadingVersion);
290
306
  return (
291
307
  <div
292
308
  data-testid="game-doc-toolbar"
@@ -304,6 +320,11 @@ export function GameDocumentToolbar(_props: WorkspaceDocumentContentProps) {
304
320
  scale={_gameScale}
305
321
  title="Change game resolution"
306
322
  />
323
+ <ViewportShadingMenu
324
+ mode={gameViewportShadingMode()}
325
+ onChange={setGameDocumentShadingMode}
326
+ disabled={store.playState !== 'stopped' && !gameViewportShadingSupported()}
327
+ />
307
328
  <MuteButton />
308
329
  <GameStatsMenu />
309
330
  </div>
@@ -372,6 +393,12 @@ function GamePanel({ style }: { style?: React.CSSProperties }) {
372
393
  setGameScale(scale);
373
394
  }, [scale]);
374
395
 
396
+ // A new play session mounts fresh adapter instances. Re-apply this Game
397
+ // viewport's own session-local diagnostic mode after every stopped→running edge.
398
+ useEffect(() => {
399
+ if (isRunning) setGameViewportShadingMode(gameViewportShadingMode());
400
+ }, [isRunning]);
401
+
375
402
  return (
376
403
  <div style={{ overflow: 'hidden', position: 'relative', flexDirection: 'column', ...style }}>
377
404
  <div
@@ -22,7 +22,7 @@ import {
22
22
  import { useEditorStore, useHistoryCommandSnapshot, useHistoryCommands } from '../EditorContext';
23
23
  import { zIndex } from '../z-index';
24
24
  import { buildDataAssetActions } from './data-documents';
25
- import { buildProjectOperationActions } from './project-operation-documents';
25
+ import { buildProjectToolActions } from './project-tool-documents';
26
26
  import { buildStoryActions } from './story-documents';
27
27
  import { buildToolActions } from './tool-documents';
28
28
  import { buildXStateMachineActions } from './xstate-documents';
@@ -70,7 +70,7 @@ export function CommandPalette({ onClose }: CommandPaletteProps) {
70
70
  // machines. All read live snapshots; the palette mounts fresh per open.
71
71
  const documents = [
72
72
  ...buildDataAssetActions(store),
73
- ...buildProjectOperationActions(store),
73
+ ...buildProjectToolActions(store),
74
74
  ...buildStoryActions(store),
75
75
  ...buildToolActions(store),
76
76
  ...buildXStateMachineActions(),
@@ -9,6 +9,7 @@ import {
9
9
  decideExternalSceneChangeAction,
10
10
  } from '../asset-events';
11
11
  import { installAssetSelectionAutoClear } from '../asset-selection';
12
+ import { connectAssetImportProgressEvents } from '../asset-workflow/asset-import-jobs';
12
13
  import { getActiveAuthoring, hasAuthoringOverride } from '../authoring/active-adapter';
13
14
  import {
14
15
  switchFocusedWorld,
@@ -32,6 +33,7 @@ import { getProjectFileHistory } from '../history/project-file-history';
32
33
  import { getStorageBackend } from '../storage';
33
34
  import { installAuxiliaryEvents } from '../workspace-aux-commands';
34
35
  import { installUtilityAutoOpen, installUtilityEvents } from '../workspace-utility-commands';
36
+ import { connectProjectAssetReimportEvents } from './asset-editor-persistence';
35
37
  import { ensureBuildContributionsRegistered } from './build-documents';
36
38
  import { useCenterDocuments } from './CenterDocuments';
37
39
  import { CommandPalette } from './CommandPalette';
@@ -168,6 +170,8 @@ export function DefaultEditorLayout({
168
170
  // W2 (§5.1) — an ENTITY selection change drops the asset selection, so the
169
171
  // Inspector goes back to being purely entity-contextual (asset-selection.ts).
170
172
  useEffect(() => installAssetSelectionAutoClear(store), [store]);
173
+ useEffect(() => connectAssetImportProgressEvents(), []);
174
+ useEffect(() => connectProjectAssetReimportEvents(), []);
171
175
  const [prefabSaveState, setPrefabSaveState] = useState<{ entityId: string; name: string } | null>(
172
176
  null,
173
177
  );
@@ -1,11 +1,11 @@
1
- import { themeVars } from '../design-system';
1
+ import { EditorIcon, editorIcons, themeVars } from '../design-system';
2
2
  /**
3
3
  * Inspector-tool section host (W3, docs/DATA-TOOLS-DESIGN.md §3.2/§3.4) —
4
4
  * renders one project tool (`placement: 'inspector'`) as a section in the
5
5
  * ONE inspector shell (`Inspector.tsx`), via `inspector-section-registry`.
6
6
  *
7
- * The chrome is the editor's (§3.4): a bordered section with the ⚙-marked
8
- * title row (the same project-tool marker the dock tabs use, §3.5), matching
7
+ * The chrome is the editor's (§3.4): a bordered section with a semantic tool
8
+ * icon in its title row, matching
9
9
  * the shell's own section styling (`Inspector.tsx`'s Stories block). What the
10
10
  * tool renders inside is its own business — plain React, ideally built from
11
11
  * `@editor/widgets` so fields sit flush with the built-in sections.
@@ -26,20 +26,23 @@ import { themeVars } from '../design-system';
26
26
 
27
27
  import type { AuthoringAdapter } from '@engine/adapter';
28
28
  import { EditorSelectionProvider } from '@engine/react/use-selection';
29
- import type { InspectorToolProps } from '@engine/tools/define-tool';
29
+ import type { ToolInspectorContributionProps } from '@vgai/editor-sdk/contributions';
30
30
  import type { ComponentType } from 'react';
31
+ import type { ProjectToolCatalogEntry } from '../project-tools';
32
+ import { getToolContributionClient } from '../tool-loader';
31
33
  import { groupLabelStyle } from './inspector-widgets/shared';
32
34
  import { ToolErrorBoundary } from './ToolHost';
33
35
 
34
36
  export interface InspectorToolSectionProps {
35
37
  /** `ToolConfig.id` — the section's `data-testid` suffix (`inspector-tool-<id>`). */
36
38
  id: string;
37
- /** `ToolConfig.title` the section header, after the marker. */
39
+ /** Contribution title shown after the semantic tool icon. */
38
40
  title: string;
39
41
  /** Project-relative source file, shown in crash/teaching messages. */
40
42
  file: string;
41
43
  /** The tool module's default export. */
42
- Component: ComponentType<InspectorToolProps>;
44
+ tool: ProjectToolCatalogEntry;
45
+ Component: ComponentType<ToolInspectorContributionProps>;
43
46
  /** Live adapter + selection, as the section registry hands them over. */
44
47
  adapter: AuthoringAdapter;
45
48
  nodeId: string | null;
@@ -50,6 +53,7 @@ export function InspectorToolSection({
50
53
  id,
51
54
  title,
52
55
  file,
56
+ tool,
53
57
  Component,
54
58
  adapter,
55
59
  nodeId,
@@ -61,14 +65,12 @@ export function InspectorToolSection({
61
65
  style={{ padding: '8px', borderTop: `1px solid ${themeVars.boundary.default}`, marginTop: 4 }}
62
66
  >
63
67
  <div style={{ ...groupLabelStyle, marginBottom: 4 }}>
64
- <span aria-hidden="true" style={{ marginRight: 4 }}>
65
-
66
- </span>
68
+ <EditorIcon icon={editorIcons.tool.extension} tone="muted" style={{ marginRight: 4 }} />
67
69
  {title}
68
70
  </div>
69
71
  <ToolErrorBoundary file={file}>
70
72
  <EditorSelectionProvider adapter={adapter} nodeId={nodeId}>
71
- <Component node={node} nodeId={nodeId} />
73
+ <Component tool={tool} client={getToolContributionClient()} node={node} nodeId={nodeId} />
72
74
  </EditorSelectionProvider>
73
75
  </ToolErrorBoundary>
74
76
  </div>