@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.
- package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
- package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
- package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
- package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
- package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
- package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
- package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
- package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
- package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
- package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
- package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
- package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
- package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
- package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
- package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
- package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
- package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
- package/dist/assets/index-DiUacMXR.css +1 -0
- package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
- package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
- package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
- package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
- package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +27125 -1075
- package/package.json +2 -2
- package/server/asset-history-snapshots.ts +189 -0
- package/server/asset-library-routes.ts +404 -53
- package/server/catalog-import-recipe.ts +97 -0
- package/server/catalog-reimport.ts +141 -0
- package/server/cloud-asset-catalog.ts +71 -5
- package/server/dev.ts +1 -1
- package/server/editor-server.ts +106 -51
- package/server/editor-sse.ts +5 -0
- package/server/generative-execution-context.ts +45 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/packaged.ts +1 -1
- package/server/project-hmr-files.ts +3 -5
- package/server/project-output-writer.ts +440 -36
- package/server/project-tools.ts +529 -0
- package/server/server-utils.ts +14 -11
- package/src/EditorContext.tsx +4 -4
- package/src/action-registry.ts +12 -0
- package/src/asset-editor-context.tsx +9 -0
- package/src/asset-events.ts +3 -15
- package/src/asset-selection.ts +3 -0
- package/src/asset-workflow/asset-capabilities.ts +40 -0
- package/src/asset-workflow/asset-import-jobs.ts +108 -0
- package/src/asset-workflow/asset-library-query.ts +8 -1
- package/src/asset-workflow/asset-workflow-quality.ts +2 -0
- package/src/asset-workflow/import-contract.ts +2 -0
- package/src/asset-workflow/model-inspection.ts +92 -1
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +1 -0
- package/src/asset-workflow/project-asset-health.ts +3 -6
- package/src/asset-workflow/project-asset-operations.ts +119 -4
- package/src/command-listener.ts +37 -9
- package/src/components/ApplicationMenus.tsx +13 -15
- package/src/components/AssetBrowser.tsx +301 -26
- package/src/components/AssetImportDetails.tsx +207 -92
- package/src/components/AssetImportJobStatus.tsx +55 -0
- package/src/components/CenterDocuments.tsx +28 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DefaultEditorLayout.tsx +4 -0
- package/src/components/InspectorToolSection.tsx +12 -10
- package/src/components/OnlineAssetBrowser.tsx +197 -11
- package/src/components/ToolHost.tsx +8 -8
- package/src/components/ViewportOverlay.tsx +13 -26
- package/src/components/ViewportPanel.tsx +19 -14
- package/src/components/ViewportShadingMenu.tsx +103 -0
- package/src/components/WorkspaceDock.tsx +103 -17
- package/src/components/asset-documents.tsx +38 -11
- package/src/components/asset-editor-persistence.ts +300 -5
- package/src/components/asset-selection-section.tsx +83 -9
- package/src/components/asset-viewers/AudioViewer.tsx +4 -7
- package/src/components/asset-viewers/ModelViewer.tsx +168 -5
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
- package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
- package/src/components/asset-workflow.css +30 -3
- package/src/components/primitives/editor-icons.ts +1 -0
- package/src/components/project-tool-documents.tsx +298 -0
- package/src/components/status-contributions.tsx +38 -0
- package/src/components/tool-documents.tsx +20 -17
- package/src/components/tool-schema-form.tsx +258 -0
- package/src/components/workspace-dock.css +16 -0
- package/src/editor-api.ts +159 -22
- package/src/editor-store.ts +6 -5
- package/src/editor-viewport.ts +12 -7
- package/src/game-viewport-state.ts +28 -0
- package/src/model-thumbnail.ts +97 -9
- package/src/play-mode.ts +30 -3
- package/src/project-tool-discovery.ts +5 -5
- package/src/{project-operations.ts → project-tools.ts} +39 -26
- package/src/scene-sync.ts +0 -67
- package/src/storage/handle-storage.ts +5 -1
- package/src/storage/http-storage.ts +7 -2
- package/src/storage/mem-storage.ts +7 -1
- package/src/storage/types.ts +2 -0
- package/src/theme-css.ts +17 -7
- package/src/tool-loader.ts +152 -241
- package/src/viewport-shading-boundary.ts +15 -0
- package/src/widgets/index.ts +2 -1
- package/src/workspace-dock-controller.ts +8 -0
- package/src/workspace-document-registry.ts +4 -4
- package/src/workspace-state-persistence.ts +36 -10
- package/template/.claude/skills/editor/SKILL.md +13 -3
- package/template/AGENTS.md +100 -83
- package/template/package.json +16 -0
- package/template/public/scenes/default.vscn.json +1 -2
- package/template/scripts/playtest.ts +7 -37
- package/template/server/rooms/game-room.ts +2 -2
- package/template/src/data/README.md +3 -2
- package/template/src/data/tuning.data.json +1 -3
- package/template/src/data/tuning.schema.json +0 -12
- package/template/src/data/tuning.schema.ts +2 -9
- package/template/src/data/tuning.ts +1 -1
- package/template/src/scripts/registry.ts +0 -3
- package/template/src/tools/README.md +151 -16
- package/template/src/tools/autoplay.tool.ts +31 -0
- package/template/src/tools/autoplay.ts +15 -0
- package/template/src/tools/tuning.document.tsx +107 -0
- package/template/src/tools/tuning.tool.ts +24 -0
- package/template/src/tools/tuning.ts +9 -0
- package/template/src/ui/Button.stories.tsx +1 -1
- package/template/tests/acceptance/example.spec.ts +4 -59
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bAGhEnnO.css +0 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
- package/dist/project-scripts.bundle.js +0 -3917
- package/server/project-operations.ts +0 -343
- package/src/components/project-operation-documents.tsx +0 -226
- package/template/src/operations/README.md +0 -88
- package/template/src/scripts/components/data-spinner.ts +0 -86
- package/template/src/scripts/components/spin-lap-math.ts +0 -27
- package/template/src/tools/example.tool.tsx +0 -97
- package/template/src/tools/spin.tool.tsx +0 -101
- package/template/tests/logic/example.test.ts +0 -31
|
@@ -1,8 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AssetImportPlan,
|
|
3
|
+
assetImportPlanSchema,
|
|
4
|
+
importRecipePath,
|
|
5
|
+
} from '../asset-workflow/import-contract';
|
|
6
|
+
import {
|
|
7
|
+
type AppliedAssetImportHistory,
|
|
8
|
+
captureProjectAssetHistory,
|
|
9
|
+
type DownloadOnlineAssetResult,
|
|
10
|
+
downloadOnlineAsset,
|
|
11
|
+
reimportProjectAsset,
|
|
12
|
+
restoreProjectAssetHistory,
|
|
13
|
+
} from '../editor-api';
|
|
14
|
+
import { editorConsole } from '../editor-console';
|
|
15
|
+
import type { HistoryService } from '../history/history-service';
|
|
1
16
|
import { getProjectFileHistory } from '../history/project-file-history';
|
|
2
|
-
import
|
|
17
|
+
import { sha256Hex } from '../history/snapshot-store';
|
|
18
|
+
import type { ResourceDriver, ResourceKind } from '../history/types';
|
|
3
19
|
import { getStorageBackend } from '../storage';
|
|
4
20
|
import { workspaceHistoryService } from './workspace-history';
|
|
5
21
|
|
|
22
|
+
interface ImportedAssetHistoryResource {
|
|
23
|
+
driver: ResourceDriver;
|
|
24
|
+
revision: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const importedAssetHistoryResources = new WeakMap<
|
|
28
|
+
HistoryService,
|
|
29
|
+
Map<string, ImportedAssetHistoryResource>
|
|
30
|
+
>();
|
|
31
|
+
|
|
32
|
+
function notifyAssetChanged(assetPath: string): void {
|
|
33
|
+
if (typeof window === 'undefined') return;
|
|
34
|
+
window.dispatchEvent(
|
|
35
|
+
new CustomEvent('editor:assets-changed', {
|
|
36
|
+
detail: { paths: [projectAssetPath(assetPath)] },
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
window.dispatchEvent(
|
|
40
|
+
new CustomEvent('editor:asset-mutation', {
|
|
41
|
+
detail: { type: 'change', paths: [projectAssetPath(assetPath)] },
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function importedAssetHistoryResource(
|
|
47
|
+
history: HistoryService,
|
|
48
|
+
assetPath: string,
|
|
49
|
+
): ImportedAssetHistoryResource {
|
|
50
|
+
const normalized = projectAssetPath(assetPath);
|
|
51
|
+
let resources = importedAssetHistoryResources.get(history);
|
|
52
|
+
if (!resources) {
|
|
53
|
+
resources = new Map();
|
|
54
|
+
importedAssetHistoryResources.set(history, resources);
|
|
55
|
+
}
|
|
56
|
+
const existing = resources.get(normalized);
|
|
57
|
+
if (existing) return existing;
|
|
58
|
+
const descriptor = history.registry.registerProject(
|
|
59
|
+
'project-file',
|
|
60
|
+
`.vgai/history/imported-assets/${normalized}`,
|
|
61
|
+
normalized.split('/').pop(),
|
|
62
|
+
);
|
|
63
|
+
const resource: ImportedAssetHistoryResource = {
|
|
64
|
+
revision: 0,
|
|
65
|
+
driver: undefined as unknown as ResourceDriver,
|
|
66
|
+
};
|
|
67
|
+
const capture = async () => {
|
|
68
|
+
const token = await captureProjectAssetHistory(normalized);
|
|
69
|
+
const bytes = new TextEncoder().encode(token);
|
|
70
|
+
return {
|
|
71
|
+
revision: resource.revision,
|
|
72
|
+
contentType: 'application/vnd.vgai.asset-history-token',
|
|
73
|
+
bytes,
|
|
74
|
+
sha256: await sha256Hex(bytes),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
resource.driver = {
|
|
78
|
+
descriptor,
|
|
79
|
+
capture,
|
|
80
|
+
preflight: async (expected) => {
|
|
81
|
+
const current = await capture();
|
|
82
|
+
return current.sha256 === expected.sha256
|
|
83
|
+
? { ok: true }
|
|
84
|
+
: {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: 'content-conflict',
|
|
87
|
+
actualRevision: resource.revision,
|
|
88
|
+
actualSha256: current.sha256,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
restore: async (snapshot) => {
|
|
92
|
+
const token = new TextDecoder().decode(history.snapshots.read(snapshot).bytes);
|
|
93
|
+
await restoreProjectAssetHistory(normalized, token);
|
|
94
|
+
resource.revision++;
|
|
95
|
+
notifyAssetChanged(normalized);
|
|
96
|
+
},
|
|
97
|
+
estimateBytes: (snapshot) => snapshot.byteLength,
|
|
98
|
+
};
|
|
99
|
+
history.registerDriver(resource.driver);
|
|
100
|
+
resources.set(normalized, resource);
|
|
101
|
+
return resource;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function historyTokenBytes(token: string): Uint8Array {
|
|
105
|
+
return new TextEncoder().encode(token);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Journal already-committed server acquisitions as one exact project transaction. */
|
|
109
|
+
export async function recordAppliedAssetImportsWithHistory(
|
|
110
|
+
imports: readonly AppliedAssetImportHistory[],
|
|
111
|
+
label: string,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
if (imports.length === 0) return;
|
|
114
|
+
const history = workspaceHistoryService();
|
|
115
|
+
if (!history) return;
|
|
116
|
+
const resources = imports.map((entry) => ({
|
|
117
|
+
entry,
|
|
118
|
+
resource: importedAssetHistoryResource(history, entry.assetPath),
|
|
119
|
+
}));
|
|
120
|
+
try {
|
|
121
|
+
await history.recordAppliedTransaction(
|
|
122
|
+
{
|
|
123
|
+
label,
|
|
124
|
+
detail: imports.map((entry) => projectAssetPath(entry.assetPath)).join(', '),
|
|
125
|
+
resources: resources.map(({ resource }) => resource.driver.descriptor.key),
|
|
126
|
+
scope: 'project',
|
|
127
|
+
},
|
|
128
|
+
resources.map(({ entry, resource }) => ({
|
|
129
|
+
resource: resource.driver.descriptor.key,
|
|
130
|
+
beforeBytes: historyTokenBytes(entry.beforeToken),
|
|
131
|
+
afterBytes: historyTokenBytes(entry.afterToken),
|
|
132
|
+
contentType: 'application/vnd.vgai.asset-history-token',
|
|
133
|
+
})),
|
|
134
|
+
);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const compensation = await Promise.allSettled(
|
|
137
|
+
imports.map((entry) => restoreProjectAssetHistory(entry.assetPath, entry.beforeToken)),
|
|
138
|
+
);
|
|
139
|
+
for (const entry of imports) notifyAssetChanged(entry.assetPath);
|
|
140
|
+
const compensationFailure = compensation.find((result) => result.status === 'rejected');
|
|
141
|
+
if (compensationFailure?.status === 'rejected') {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Project history could not record undo/redo and rollback failed: ${compensationFailure.reason instanceof Error ? compensationFailure.reason.message : String(compensationFailure.reason)}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Assets were rolled back because project history could not record undo/redo: ${error instanceof Error ? error.message : String(error)}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Acquire one catalog asset and add the committed mutation to project history. */
|
|
153
|
+
export async function downloadOnlineAssetWithHistory(
|
|
154
|
+
params: Parameters<typeof downloadOnlineAsset>[0],
|
|
155
|
+
label = `Import ${params.name}`,
|
|
156
|
+
): Promise<DownloadOnlineAssetResult> {
|
|
157
|
+
const result = await downloadOnlineAsset(params);
|
|
158
|
+
if (result.ok && result.history) {
|
|
159
|
+
await recordAppliedAssetImportsWithHistory([result.history], label);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
6
164
|
/** Convert an asset's public serving URL into the StorageBackend path. */
|
|
7
165
|
export function projectAssetPath(assetPath: string): string {
|
|
8
166
|
const path = assetPath.split(/[?#]/, 1)[0]!.replace(/^\/+/, '');
|
|
@@ -10,6 +168,122 @@ export function projectAssetPath(assetPath: string): string {
|
|
|
10
168
|
return path;
|
|
11
169
|
}
|
|
12
170
|
|
|
171
|
+
/** Load and validate the durable recipe adjacent to an imported project asset. */
|
|
172
|
+
export async function readProjectAssetImportPlan(assetPath: string): Promise<AssetImportPlan> {
|
|
173
|
+
const recipe = await getStorageBackend().read(importRecipePath(projectAssetPath(assetPath)));
|
|
174
|
+
return assetImportPlanSchema.parse(JSON.parse(recipe));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Reacquire and atomically rebuild a project asset from its durable recipe. */
|
|
178
|
+
export async function reimportStoredProjectAsset(assetPath: string): Promise<{
|
|
179
|
+
path?: string;
|
|
180
|
+
provenanceOperationId?: string;
|
|
181
|
+
}> {
|
|
182
|
+
const plan = await readProjectAssetImportPlan(assetPath);
|
|
183
|
+
return reimportProjectAssetWithHistory(assetPath, plan);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Reimport multiple recipe-backed assets as one compensating history transaction. */
|
|
187
|
+
export async function reimportStoredProjectAssets(assetPaths: readonly string[]): Promise<void> {
|
|
188
|
+
const paths = [...new Set(assetPaths.map(projectAssetPath))];
|
|
189
|
+
if (paths.length === 0) return;
|
|
190
|
+
const plans = await Promise.all(paths.map(readProjectAssetImportPlan));
|
|
191
|
+
const beforeTokens = await Promise.all(paths.map(captureProjectAssetHistory));
|
|
192
|
+
try {
|
|
193
|
+
for (let index = 0; index < paths.length; index++) {
|
|
194
|
+
const result = await reimportProjectAsset(paths[index]!, plans[index]!);
|
|
195
|
+
if (!result.ok) throw new Error(result.error ?? `Reimport failed for ${paths[index]}.`);
|
|
196
|
+
}
|
|
197
|
+
const afterTokens = await Promise.all(paths.map(captureProjectAssetHistory));
|
|
198
|
+
await recordAppliedAssetImportsWithHistory(
|
|
199
|
+
paths.map((assetPath, index) => ({
|
|
200
|
+
assetPath,
|
|
201
|
+
beforeToken: beforeTokens[index]!,
|
|
202
|
+
afterToken: afterTokens[index]!,
|
|
203
|
+
})),
|
|
204
|
+
`Reimport ${paths.length} assets`,
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
await Promise.allSettled(
|
|
208
|
+
paths.map((assetPath, index) => restoreProjectAssetHistory(assetPath, beforeTokens[index]!)),
|
|
209
|
+
);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
for (const path of paths) notifyAssetChanged(path);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Reimport a validated draft and journal the already-applied server mutation. */
|
|
216
|
+
export async function reimportProjectAssetWithHistory(
|
|
217
|
+
assetPath: string,
|
|
218
|
+
plan: AssetImportPlan,
|
|
219
|
+
): Promise<{ path?: string; provenanceOperationId?: string }> {
|
|
220
|
+
const normalized = projectAssetPath(assetPath);
|
|
221
|
+
const history = workspaceHistoryService();
|
|
222
|
+
const resource = history ? importedAssetHistoryResource(history, normalized) : null;
|
|
223
|
+
const before = resource ? await resource.driver.capture() : null;
|
|
224
|
+
const result = await reimportProjectAsset(normalized, plan);
|
|
225
|
+
if (!result.ok) throw new Error(result.error ?? 'Reimport was rejected.');
|
|
226
|
+
if (history && resource && before) {
|
|
227
|
+
const after = await resource.driver.capture();
|
|
228
|
+
try {
|
|
229
|
+
await history.recordAppliedTransaction(
|
|
230
|
+
{
|
|
231
|
+
label: `Reimport ${plan.identity.displayName ?? plan.identity.familyId}`,
|
|
232
|
+
detail: normalized,
|
|
233
|
+
resources: [resource.driver.descriptor.key],
|
|
234
|
+
scope: 'project',
|
|
235
|
+
},
|
|
236
|
+
[
|
|
237
|
+
{
|
|
238
|
+
resource: resource.driver.descriptor.key,
|
|
239
|
+
beforeBytes: before.bytes,
|
|
240
|
+
afterBytes: after.bytes,
|
|
241
|
+
contentType: after.contentType,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Reimport committed, but project history could not record undo/redo: ${error instanceof Error ? error.message : String(error)}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
notifyAssetChanged(normalized);
|
|
252
|
+
return {
|
|
253
|
+
...(result.path ? { path: result.path } : {}),
|
|
254
|
+
...(result.provenanceOperationId
|
|
255
|
+
? { provenanceOperationId: result.provenanceOperationId }
|
|
256
|
+
: {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Install the shared command/event bridge used by Project Assets and the Inspector. */
|
|
261
|
+
export function connectProjectAssetReimportEvents(): () => void {
|
|
262
|
+
if (typeof window === 'undefined') return () => {};
|
|
263
|
+
const onReimport = (event: Event) => {
|
|
264
|
+
const detail = (event as CustomEvent<{ path?: string; plan?: unknown }>).detail;
|
|
265
|
+
if (!detail?.path) return;
|
|
266
|
+
const path = detail.path;
|
|
267
|
+
void (async () => {
|
|
268
|
+
try {
|
|
269
|
+
if (detail.plan) {
|
|
270
|
+
await reimportProjectAssetWithHistory(path, assetImportPlanSchema.parse(detail.plan));
|
|
271
|
+
} else {
|
|
272
|
+
await reimportStoredProjectAsset(path);
|
|
273
|
+
}
|
|
274
|
+
editorConsole.log(`Reimported ${projectAssetPath(path)}.`, 'assets');
|
|
275
|
+
} catch (error) {
|
|
276
|
+
editorConsole.error(
|
|
277
|
+
`Could not reimport ${projectAssetPath(path)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
278
|
+
'assets',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
})();
|
|
282
|
+
};
|
|
283
|
+
window.addEventListener('editor:reimport-project-asset', onReimport);
|
|
284
|
+
return () => window.removeEventListener('editor:reimport-project-asset', onReimport);
|
|
285
|
+
}
|
|
286
|
+
|
|
13
287
|
/** Persist one authored asset through the project-wide undo history. */
|
|
14
288
|
export async function writeProjectAsset(
|
|
15
289
|
assetPath: string,
|
|
@@ -36,9 +310,30 @@ export async function writeProjectAsset(
|
|
|
36
310
|
/** Refresh an open editor after undo/redo or another surface writes its file. */
|
|
37
311
|
export function subscribeProjectAsset(assetPath: string, listener: () => void): () => void {
|
|
38
312
|
const history = workspaceHistoryService();
|
|
39
|
-
if (!history) return () => {};
|
|
40
313
|
const path = projectAssetPath(assetPath);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
314
|
+
let queued = false;
|
|
315
|
+
const notify = () => {
|
|
316
|
+
if (queued) return;
|
|
317
|
+
queued = true;
|
|
318
|
+
queueMicrotask(() => {
|
|
319
|
+
queued = false;
|
|
320
|
+
listener();
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
const unsubscribeHistory = history
|
|
324
|
+
? getProjectFileHistory(history).subscribeAll((changedPath) => {
|
|
325
|
+
if (changedPath === path) notify();
|
|
326
|
+
})
|
|
327
|
+
: () => {};
|
|
328
|
+
const onAssetChanged = (event: Event) => {
|
|
329
|
+
const paths = (event as CustomEvent<{ paths?: readonly string[] }>).detail?.paths;
|
|
330
|
+
if (!paths || paths.some((candidate) => projectAssetPath(candidate) === path)) notify();
|
|
331
|
+
};
|
|
332
|
+
if (typeof window !== 'undefined')
|
|
333
|
+
window.addEventListener('editor:assets-changed', onAssetChanged);
|
|
334
|
+
return () => {
|
|
335
|
+
unsubscribeHistory();
|
|
336
|
+
if (typeof window !== 'undefined')
|
|
337
|
+
window.removeEventListener('editor:assets-changed', onAssetChanged);
|
|
338
|
+
};
|
|
44
339
|
}
|
|
@@ -25,9 +25,10 @@ import { useEffect, useState } from 'react';
|
|
|
25
25
|
import { getSelectedAsset, type SelectedAsset, setSelectedAsset } from '../asset-selection';
|
|
26
26
|
import { assetDocumentKind } from '../asset-workflow/asset-capabilities';
|
|
27
27
|
import { PROJECT_ASSET_COMMANDS } from '../asset-workflow/project-asset-commands';
|
|
28
|
-
import { Button, bg, border, radius, text } from '../design-system';
|
|
28
|
+
import { Button, bg, border, danger, radius, text } from '../design-system';
|
|
29
29
|
import { registerInspectorSections } from '../inspector-section-registry';
|
|
30
30
|
import { getModelThumbnailRenderer } from '../model-thumbnail';
|
|
31
|
+
import { reimportStoredProjectAsset } from './asset-editor-persistence';
|
|
31
32
|
import { validateInputMapForViewer } from './asset-viewers/InputMapViewer';
|
|
32
33
|
import { OnlineAssetDetail } from './asset-viewers/OnlineAssetDetail';
|
|
33
34
|
|
|
@@ -226,6 +227,12 @@ function MaterialSummary({ asset }: { asset: SelectedAsset }) {
|
|
|
226
227
|
* (the registration's `match` gates on the same store read). */
|
|
227
228
|
export function AssetSelectionSection() {
|
|
228
229
|
const asset = getSelectedAsset();
|
|
230
|
+
const [reimportStatus, setReimportStatus] = useState<'idle' | 'running' | 'done'>('idle');
|
|
231
|
+
const [reimportError, setReimportError] = useState<string | null>(null);
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
setReimportStatus('idle');
|
|
234
|
+
setReimportError(null);
|
|
235
|
+
}, [asset?.path]);
|
|
229
236
|
if (!asset) return null;
|
|
230
237
|
const ext = asset.name.includes('.') ? (asset.name.split('.').pop() ?? '') : '';
|
|
231
238
|
if (asset.online) {
|
|
@@ -244,6 +251,7 @@ export function AssetSelectionSection() {
|
|
|
244
251
|
<OnlineAssetDetail
|
|
245
252
|
online={online}
|
|
246
253
|
mode="inspector"
|
|
254
|
+
showPreview
|
|
247
255
|
onResolved={(localPath) => setSelectedAsset({ ...asset, path: localPath })}
|
|
248
256
|
/>
|
|
249
257
|
<div style={{ padding: '0 8px 8px' }}>
|
|
@@ -286,7 +294,15 @@ export function AssetSelectionSection() {
|
|
|
286
294
|
)}
|
|
287
295
|
<MetaRow label="Path" value={asset.path} />
|
|
288
296
|
{asset.selectionCount && asset.selectionCount > 1 && (
|
|
289
|
-
|
|
297
|
+
<>
|
|
298
|
+
<MetaRow label="Selection" value={`${asset.selectionCount} assets`} />
|
|
299
|
+
{asset.selectionTotalBytes !== undefined && (
|
|
300
|
+
<MetaRow label="Total size" value={formatSize(asset.selectionTotalBytes)} />
|
|
301
|
+
)}
|
|
302
|
+
{asset.selectionFormats && (
|
|
303
|
+
<MetaRow label="Formats" value={asset.selectionFormats.join(', ')} />
|
|
304
|
+
)}
|
|
305
|
+
</>
|
|
290
306
|
)}
|
|
291
307
|
{ext && <MetaRow label="Format" value={ext.toUpperCase()} />}
|
|
292
308
|
{asset.sizeBytes !== undefined && (
|
|
@@ -342,17 +358,75 @@ export function AssetSelectionSection() {
|
|
|
342
358
|
<Button
|
|
343
359
|
type="button"
|
|
344
360
|
data-command-id={PROJECT_ASSET_COMMANDS.reimport.id}
|
|
345
|
-
disabled={
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
new CustomEvent('editor:reimport-project-asset', { detail: { path: asset.path } }),
|
|
350
|
-
)
|
|
361
|
+
disabled={
|
|
362
|
+
asset.selectionCount && asset.selectionCount > 1
|
|
363
|
+
? !asset.selectionReimportableCount
|
|
364
|
+
: !asset.sourcePath || reimportStatus === 'running'
|
|
351
365
|
}
|
|
366
|
+
title={asset.sourcePath ? 'Run the saved import recipe again' : 'No import recipe'}
|
|
367
|
+
onClick={async () => {
|
|
368
|
+
if (asset.selectionCount && asset.selectionCount > 1) {
|
|
369
|
+
window.dispatchEvent(
|
|
370
|
+
new CustomEvent('editor:project-asset-command', {
|
|
371
|
+
detail: { id: PROJECT_ASSET_COMMANDS.reimport.id },
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
setReimportStatus('running');
|
|
377
|
+
setReimportError(null);
|
|
378
|
+
try {
|
|
379
|
+
await reimportStoredProjectAsset(asset.path);
|
|
380
|
+
setReimportStatus('done');
|
|
381
|
+
} catch (cause) {
|
|
382
|
+
setReimportStatus('idle');
|
|
383
|
+
setReimportError(cause instanceof Error ? cause.message : String(cause));
|
|
384
|
+
}
|
|
385
|
+
}}
|
|
352
386
|
>
|
|
353
|
-
|
|
387
|
+
{reimportStatus === 'running'
|
|
388
|
+
? 'Reimporting…'
|
|
389
|
+
: asset.selectionCount && asset.selectionCount > 1
|
|
390
|
+
? `Reimport ${asset.selectionReimportableCount ?? 0}`
|
|
391
|
+
: 'Reimport'}
|
|
354
392
|
</Button>
|
|
393
|
+
{asset.selectionCount && asset.selectionCount > 1 && (
|
|
394
|
+
<>
|
|
395
|
+
<Button
|
|
396
|
+
type="button"
|
|
397
|
+
data-command-id={PROJECT_ASSET_COMMANDS.move.id}
|
|
398
|
+
onClick={() =>
|
|
399
|
+
window.dispatchEvent(
|
|
400
|
+
new CustomEvent('editor:project-asset-command', {
|
|
401
|
+
detail: { id: PROJECT_ASSET_COMMANDS.move.id },
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
>
|
|
406
|
+
Move selected
|
|
407
|
+
</Button>
|
|
408
|
+
<Button
|
|
409
|
+
type="button"
|
|
410
|
+
data-command-id={PROJECT_ASSET_COMMANDS.delete.id}
|
|
411
|
+
onClick={() =>
|
|
412
|
+
window.dispatchEvent(
|
|
413
|
+
new CustomEvent('editor:project-asset-command', {
|
|
414
|
+
detail: { id: PROJECT_ASSET_COMMANDS.delete.id },
|
|
415
|
+
}),
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
>
|
|
419
|
+
Delete selected
|
|
420
|
+
</Button>
|
|
421
|
+
</>
|
|
422
|
+
)}
|
|
355
423
|
</div>
|
|
424
|
+
{reimportStatus === 'done' && <div role="status">Reimport committed.</div>}
|
|
425
|
+
{reimportError && (
|
|
426
|
+
<div role="alert" style={{ color: danger, fontSize: 11 }}>
|
|
427
|
+
{reimportError}
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
356
430
|
<div style={{ fontSize: 10, color: text[3] }}>
|
|
357
431
|
Double-click (or press Enter) in the asset browser to open the full viewer.
|
|
358
432
|
</div>
|
|
@@ -69,12 +69,10 @@ export function AudioViewer({ assetPath }: { assetPath: string }) {
|
|
|
69
69
|
>
|
|
70
70
|
{/* Audio icon */}
|
|
71
71
|
<svg viewBox="0 0 24 24" width={48} height={48}>
|
|
72
|
-
|
|
73
|
-
— a deliberate category-palette literal, not chrome. */}
|
|
74
|
-
<rect x="3" y="3" width="18" height="18" rx="2" fill="#34d399" />
|
|
72
|
+
<rect x="3" y="3" width="18" height="18" rx="2" fill="var(--asset-icon-audio)" />
|
|
75
73
|
<path
|
|
76
74
|
d="M9 8l4-2v12l-4-2H7V10h2zm6 1v6m2-5v4"
|
|
77
|
-
stroke="
|
|
75
|
+
stroke="var(--vgai-text-1)"
|
|
78
76
|
strokeWidth="1.5"
|
|
79
77
|
fill="none"
|
|
80
78
|
strokeLinecap="round"
|
|
@@ -116,8 +114,7 @@ export function AudioViewer({ assetPath }: { assetPath: string }) {
|
|
|
116
114
|
style={{
|
|
117
115
|
width: `${(currentTime / duration) * 100}%`,
|
|
118
116
|
height: '100%',
|
|
119
|
-
|
|
120
|
-
background: '#34d399',
|
|
117
|
+
background: 'var(--asset-icon-audio)',
|
|
121
118
|
borderRadius: themeVars.shape.small,
|
|
122
119
|
}}
|
|
123
120
|
/>
|
|
@@ -137,7 +134,7 @@ export function AudioViewer({ assetPath }: { assetPath: string }) {
|
|
|
137
134
|
style={{
|
|
138
135
|
flex: 1,
|
|
139
136
|
height: `${20 + ((index * 37) % 75)}%`,
|
|
140
|
-
background: '
|
|
137
|
+
background: 'var(--asset-icon-audio)',
|
|
141
138
|
opacity: index / 48 <= currentTime / (duration || 1) ? 1 : 0.3,
|
|
142
139
|
}}
|
|
143
140
|
/>
|