@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
|
@@ -22,6 +22,19 @@ export type AssetFormat =
|
|
|
22
22
|
| 'ogg'
|
|
23
23
|
| 'wav'
|
|
24
24
|
| 'flac'
|
|
25
|
+
| 'ts'
|
|
26
|
+
| 'tsx'
|
|
27
|
+
| 'js'
|
|
28
|
+
| 'jsx'
|
|
29
|
+
| 'css'
|
|
30
|
+
| 'html'
|
|
31
|
+
| 'md'
|
|
32
|
+
| 'txt'
|
|
33
|
+
| 'mtl'
|
|
34
|
+
| 'csv'
|
|
35
|
+
| 'xml'
|
|
36
|
+
| 'yaml'
|
|
37
|
+
| 'yml'
|
|
25
38
|
| 'vscn.json'
|
|
26
39
|
| 'prefab.json'
|
|
27
40
|
| 'mat.json'
|
|
@@ -64,6 +77,21 @@ const MODEL_PREVIEW_FORMATS = ['glb', 'gltf', 'fbx', 'obj', 'dae', 'stl', 'ply',
|
|
|
64
77
|
const SOURCE_MODEL_FORMATS = ['blend', 'usd', 'usda', 'usdc'] as const;
|
|
65
78
|
const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg'] as const;
|
|
66
79
|
const AUDIO_FORMATS = ['mp3', 'ogg', 'wav', 'flac'] as const;
|
|
80
|
+
const TEXT_SOURCE_FORMATS = [
|
|
81
|
+
'txt',
|
|
82
|
+
'md',
|
|
83
|
+
'ts',
|
|
84
|
+
'tsx',
|
|
85
|
+
'js',
|
|
86
|
+
'jsx',
|
|
87
|
+
'css',
|
|
88
|
+
'html',
|
|
89
|
+
'mtl',
|
|
90
|
+
'csv',
|
|
91
|
+
'xml',
|
|
92
|
+
'yaml',
|
|
93
|
+
'yml',
|
|
94
|
+
] as const;
|
|
67
95
|
|
|
68
96
|
function row(
|
|
69
97
|
format: AssetFormat,
|
|
@@ -130,6 +158,18 @@ export const ASSET_CAPABILITY_REGISTRY: ReadonlyMap<AssetFormat, AssetCapabiliti
|
|
|
130
158
|
editor: 'audio',
|
|
131
159
|
}),
|
|
132
160
|
),
|
|
161
|
+
...TEXT_SOURCE_FORMATS.map((format) =>
|
|
162
|
+
row(format, {
|
|
163
|
+
kind: 'source',
|
|
164
|
+
previewable: true,
|
|
165
|
+
importable: true,
|
|
166
|
+
runtimeReady: false,
|
|
167
|
+
placeable: false,
|
|
168
|
+
animated: false,
|
|
169
|
+
importStrategy: 'copy-project',
|
|
170
|
+
editor: 'source',
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
133
173
|
row('vscn.json', {
|
|
134
174
|
kind: 'scene',
|
|
135
175
|
previewable: false,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { connectEvents } from '../editor-api';
|
|
2
|
+
import { getEditorMode } from '../editor-mode';
|
|
3
|
+
import type { AssetImportStage } from './import-contract';
|
|
4
|
+
|
|
5
|
+
export interface AssetImportJob {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
stage: AssetImportStage;
|
|
9
|
+
loaded: number;
|
|
10
|
+
total: number;
|
|
11
|
+
status: 'running' | 'imported' | 'skipped' | 'failed' | 'cancelled';
|
|
12
|
+
message?: string;
|
|
13
|
+
controller: AbortController;
|
|
14
|
+
retry?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const jobs = new Map<string, AssetImportJob>();
|
|
18
|
+
const listeners = new Set<() => void>();
|
|
19
|
+
let version = 0;
|
|
20
|
+
function notify(): void {
|
|
21
|
+
version++;
|
|
22
|
+
for (const listener of listeners) listener();
|
|
23
|
+
}
|
|
24
|
+
export function assetImportJobsVersion(): number {
|
|
25
|
+
return version;
|
|
26
|
+
}
|
|
27
|
+
export function subscribeAssetImportJobs(listener: () => void): () => void {
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => {
|
|
30
|
+
listeners.delete(listener);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function assetImportJobs(): readonly AssetImportJob[] {
|
|
34
|
+
return [...jobs.values()];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function beginAssetImportJob(id: string, label: string): AssetImportJob {
|
|
38
|
+
const existing = jobs.get(id);
|
|
39
|
+
if (existing?.status === 'running') return existing;
|
|
40
|
+
const job: AssetImportJob = {
|
|
41
|
+
id,
|
|
42
|
+
label,
|
|
43
|
+
stage: 'fetch',
|
|
44
|
+
loaded: 0,
|
|
45
|
+
total: 0,
|
|
46
|
+
status: 'running',
|
|
47
|
+
controller: new AbortController(),
|
|
48
|
+
};
|
|
49
|
+
jobs.set(id, job);
|
|
50
|
+
notify();
|
|
51
|
+
return job;
|
|
52
|
+
}
|
|
53
|
+
export function updateAssetImportJob(
|
|
54
|
+
id: string,
|
|
55
|
+
patch: Partial<Omit<AssetImportJob, 'id' | 'controller'>>,
|
|
56
|
+
): void {
|
|
57
|
+
const job = jobs.get(id);
|
|
58
|
+
if (!job) return;
|
|
59
|
+
Object.assign(job, patch);
|
|
60
|
+
notify();
|
|
61
|
+
}
|
|
62
|
+
export function cancelAssetImportJob(id: string): void {
|
|
63
|
+
const job = jobs.get(id);
|
|
64
|
+
if (!job || job.status !== 'running') return;
|
|
65
|
+
job.controller.abort(new DOMException('Import cancelled', 'AbortError'));
|
|
66
|
+
job.status = 'cancelled';
|
|
67
|
+
job.message = 'Cancelled before project commit.';
|
|
68
|
+
notify();
|
|
69
|
+
}
|
|
70
|
+
export function clearFinishedAssetImportJobs(): void {
|
|
71
|
+
for (const [id, job] of jobs) if (job.status !== 'running') jobs.delete(id);
|
|
72
|
+
notify();
|
|
73
|
+
}
|
|
74
|
+
export function resetAssetImportJobs(): void {
|
|
75
|
+
jobs.clear();
|
|
76
|
+
notify();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Keep jobs progressing independently of whether the Library document is mounted. */
|
|
80
|
+
export function connectAssetImportProgressEvents(): () => void {
|
|
81
|
+
if (getEditorMode() === 'browser') return () => {};
|
|
82
|
+
const source = connectEvents();
|
|
83
|
+
const listener = (event: MessageEvent) => {
|
|
84
|
+
try {
|
|
85
|
+
const update = JSON.parse(event.data as string) as {
|
|
86
|
+
jobId?: string;
|
|
87
|
+
stage?: AssetImportStage;
|
|
88
|
+
loaded?: number;
|
|
89
|
+
total?: number;
|
|
90
|
+
message?: string;
|
|
91
|
+
};
|
|
92
|
+
if (!update.jobId || !update.stage) return;
|
|
93
|
+
updateAssetImportJob(update.jobId, {
|
|
94
|
+
stage: update.stage,
|
|
95
|
+
loaded: Math.max(0, update.loaded ?? 0),
|
|
96
|
+
total: Math.max(0, update.total ?? 0),
|
|
97
|
+
...(update.message ? { message: update.message } : {}),
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
// Optional progress cannot break a running import.
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
source.addEventListener('asset-import-progress', listener);
|
|
104
|
+
return () => {
|
|
105
|
+
source.removeEventListener('asset-import-progress', listener);
|
|
106
|
+
source.close();
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -89,6 +89,10 @@ function compareAssetSort(
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function compareText(left: string, right: string): number {
|
|
93
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
export function queryAssetLibrary(
|
|
93
97
|
assets: readonly OnlineAsset[],
|
|
94
98
|
filters: AssetLibraryFilters,
|
|
@@ -102,6 +106,9 @@ export function queryAssetLibrary(
|
|
|
102
106
|
return [...filtered].sort((left, right) => {
|
|
103
107
|
const order = compareAssetSort(left, right, sort, recentRank);
|
|
104
108
|
if (order !== 0) return order;
|
|
105
|
-
|
|
109
|
+
// Catalog identifiers and authored names are already Unicode strings. A
|
|
110
|
+
// locale collation here is both surprising (the order changes with the
|
|
111
|
+
// host locale) and disproportionately expensive across 12k+ entries.
|
|
112
|
+
return compareText(left.name, right.name) || compareText(left.id, right.id);
|
|
106
113
|
});
|
|
107
114
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export const ASSET_WORKFLOW_BUDGETS = {
|
|
2
2
|
firstResultsMs: 500,
|
|
3
3
|
filterResponseMs: 50,
|
|
4
|
+
scrollFrameMs: 16.7,
|
|
4
5
|
importProgressLatencyMs: 100,
|
|
5
6
|
maxMountedLibraryCards: 120,
|
|
6
7
|
maxPreviewConcurrency: 2,
|
|
7
8
|
maxActiveAssetEditorWebGlContexts: 1,
|
|
9
|
+
maxRetainedAssetEditorResources: 0,
|
|
8
10
|
maxTelemetryEntries: 200,
|
|
9
11
|
} as const;
|
|
10
12
|
|
|
@@ -39,6 +39,8 @@ export const assetImportPlanSchema = z
|
|
|
39
39
|
familyId: z.string().min(1),
|
|
40
40
|
variantId: z.string().min(1),
|
|
41
41
|
deliveryId: z.string().min(1),
|
|
42
|
+
source: z.string().min(1).optional(),
|
|
43
|
+
displayName: z.string().min(1).optional(),
|
|
42
44
|
}),
|
|
43
45
|
sourceFiles: z.array(importFileSchema).min(1),
|
|
44
46
|
generatedFiles: z.array(safePathSchema),
|
|
@@ -16,6 +16,22 @@ export interface ModelInspection {
|
|
|
16
16
|
hierarchy: Array<{ id: string; name: string; type: string; depth: number }>;
|
|
17
17
|
materialNames: string[];
|
|
18
18
|
textureNames: string[];
|
|
19
|
+
materialSlots: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
type: string;
|
|
23
|
+
textureSlots: string[];
|
|
24
|
+
}>;
|
|
25
|
+
textureBindings: Array<{
|
|
26
|
+
materialId: string;
|
|
27
|
+
materialName: string;
|
|
28
|
+
slot: string;
|
|
29
|
+
textureName: string;
|
|
30
|
+
width: number | null;
|
|
31
|
+
height: number | null;
|
|
32
|
+
format: number;
|
|
33
|
+
colorSpace: string;
|
|
34
|
+
}>;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
const textureKeys = [
|
|
@@ -37,6 +53,8 @@ const textureKeys = [
|
|
|
37
53
|
export function inspectModel(root: THREE.Object3D): ModelInspection {
|
|
38
54
|
const materials = new Set<THREE.Material>();
|
|
39
55
|
const textures = new Set<THREE.Texture>();
|
|
56
|
+
const materialSlots = new Map<THREE.Material, string[]>();
|
|
57
|
+
const textureBindings: ModelInspection['textureBindings'] = [];
|
|
40
58
|
const hierarchy: ModelInspection['hierarchy'] = [];
|
|
41
59
|
let nodes = 0;
|
|
42
60
|
let meshes = 0;
|
|
@@ -83,10 +101,31 @@ export function inspectModel(root: THREE.Object3D): ModelInspection {
|
|
|
83
101
|
: [];
|
|
84
102
|
for (const material of objectMaterials) {
|
|
85
103
|
materials.add(material);
|
|
104
|
+
const slots = materialSlots.get(material) ?? [];
|
|
86
105
|
for (const key of textureKeys) {
|
|
87
106
|
const texture = (material as THREE.Material & Record<string, unknown>)[key];
|
|
88
|
-
if (texture instanceof THREE.Texture)
|
|
107
|
+
if (!(texture instanceof THREE.Texture)) continue;
|
|
108
|
+
textures.add(texture);
|
|
109
|
+
if (!slots.includes(key)) slots.push(key);
|
|
110
|
+
const image = texture.image as { width?: number; height?: number } | undefined;
|
|
111
|
+
if (
|
|
112
|
+
!textureBindings.some(
|
|
113
|
+
(binding) => binding.materialId === material.uuid && binding.slot === key,
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
textureBindings.push({
|
|
117
|
+
materialId: material.uuid,
|
|
118
|
+
materialName: material.name || material.type,
|
|
119
|
+
slot: key,
|
|
120
|
+
textureName: texture.name || texture.source.data?.src || 'Texture',
|
|
121
|
+
width: image?.width ?? null,
|
|
122
|
+
height: image?.height ?? null,
|
|
123
|
+
format: texture.format,
|
|
124
|
+
colorSpace: texture.colorSpace || 'none',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
89
127
|
}
|
|
128
|
+
materialSlots.set(material, slots);
|
|
90
129
|
}
|
|
91
130
|
});
|
|
92
131
|
for (const texture of textures) {
|
|
@@ -113,6 +152,13 @@ export function inspectModel(root: THREE.Object3D): ModelInspection {
|
|
|
113
152
|
textureNames: [...textures].map(
|
|
114
153
|
(texture) => texture.name || texture.source.data?.src || 'Texture',
|
|
115
154
|
),
|
|
155
|
+
materialSlots: [...materials].map((material) => ({
|
|
156
|
+
id: material.uuid,
|
|
157
|
+
name: material.name || material.type,
|
|
158
|
+
type: material.type,
|
|
159
|
+
textureSlots: materialSlots.get(material) ?? [],
|
|
160
|
+
})),
|
|
161
|
+
textureBindings,
|
|
116
162
|
};
|
|
117
163
|
}
|
|
118
164
|
|
|
@@ -183,3 +229,48 @@ export function supportedModelDiagnosticModes(root: THREE.Object3D): Array<{
|
|
|
183
229
|
{ mode: 'lod', available: lod, ...(!lod ? { reason: 'No LOD groups' } : {}) },
|
|
184
230
|
];
|
|
185
231
|
}
|
|
232
|
+
|
|
233
|
+
/** Extract an animation clip's authored root translation samples for diagnostics. */
|
|
234
|
+
export function rootMotionTrailPoints(clip: THREE.AnimationClip): THREE.Vector3[] {
|
|
235
|
+
const positionTracks = clip.tracks.filter(
|
|
236
|
+
(track): track is THREE.VectorKeyframeTrack =>
|
|
237
|
+
track instanceof THREE.VectorKeyframeTrack && track.name.toLowerCase().endsWith('.position'),
|
|
238
|
+
);
|
|
239
|
+
const track =
|
|
240
|
+
positionTracks.find((candidate) => /(^|[./])(root|hips|armature)[./]/i.test(candidate.name)) ??
|
|
241
|
+
positionTracks[0];
|
|
242
|
+
if (!track || track.values.length < 6) return [];
|
|
243
|
+
const points: THREE.Vector3[] = [];
|
|
244
|
+
for (let offset = 0; offset + 2 < track.values.length; offset += 3) {
|
|
245
|
+
points.push(
|
|
246
|
+
new THREE.Vector3(
|
|
247
|
+
track.values[offset]!,
|
|
248
|
+
track.values[offset + 1]!,
|
|
249
|
+
track.values[offset + 2]!,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return points;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Resolve a hierarchy/material row to the preview objects it represents. */
|
|
257
|
+
export function modelSelectionObjects(
|
|
258
|
+
root: THREE.Object3D,
|
|
259
|
+
selection: { kind: 'node' | 'material'; id: string },
|
|
260
|
+
): THREE.Object3D[] {
|
|
261
|
+
if (selection.kind === 'node') {
|
|
262
|
+
const object = root.getObjectByProperty('uuid', selection.id);
|
|
263
|
+
return object ? [object] : [];
|
|
264
|
+
}
|
|
265
|
+
const selected: THREE.Object3D[] = [];
|
|
266
|
+
root.traverse((object) => {
|
|
267
|
+
const mesh = object as THREE.Mesh;
|
|
268
|
+
const materials = Array.isArray(mesh.material)
|
|
269
|
+
? mesh.material
|
|
270
|
+
: mesh.material
|
|
271
|
+
? [mesh.material]
|
|
272
|
+
: [];
|
|
273
|
+
if (materials.some((material) => material.uuid === selection.id)) selected.push(mesh);
|
|
274
|
+
});
|
|
275
|
+
return selected;
|
|
276
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type PreviewResourceKind =
|
|
2
|
+
| 'webglContext'
|
|
3
|
+
| 'animationFrame'
|
|
4
|
+
| 'resizeObserver'
|
|
5
|
+
| 'objectUrl';
|
|
6
|
+
|
|
7
|
+
export interface PreviewResourceSnapshot {
|
|
8
|
+
webglContext: number;
|
|
9
|
+
animationFrame: number;
|
|
10
|
+
resizeObserver: number;
|
|
11
|
+
objectUrl: number;
|
|
12
|
+
peaks: Record<PreviewResourceKind, number>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const current: Record<PreviewResourceKind, number> = {
|
|
16
|
+
webglContext: 0,
|
|
17
|
+
animationFrame: 0,
|
|
18
|
+
resizeObserver: 0,
|
|
19
|
+
objectUrl: 0,
|
|
20
|
+
};
|
|
21
|
+
const peaks = { ...current };
|
|
22
|
+
|
|
23
|
+
/** Local diagnostic counter used by stress tests and the Debugger; never uploads identifiers. */
|
|
24
|
+
export function registerPreviewResource(kind: PreviewResourceKind): () => void {
|
|
25
|
+
current[kind]++;
|
|
26
|
+
peaks[kind] = Math.max(peaks[kind], current[kind]);
|
|
27
|
+
let released = false;
|
|
28
|
+
return () => {
|
|
29
|
+
if (released) return;
|
|
30
|
+
released = true;
|
|
31
|
+
current[kind] = Math.max(0, current[kind] - 1);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function previewResourceSnapshot(): PreviewResourceSnapshot {
|
|
36
|
+
return { ...current, peaks: { ...peaks } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resetPreviewResourceCounters(): void {
|
|
40
|
+
for (const key of Object.keys(current) as PreviewResourceKind[]) {
|
|
41
|
+
current[key] = 0;
|
|
42
|
+
peaks[key] = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const PROJECT_ASSET_COMMANDS = {
|
|
2
2
|
open: { id: 'project-asset.open', shortcut: 'Enter' },
|
|
3
3
|
rename: { id: 'project-asset.rename', shortcut: 'F2' },
|
|
4
|
+
move: { id: 'project-asset.move', shortcut: null },
|
|
4
5
|
duplicate: { id: 'project-asset.duplicate', shortcut: 'Mod+D' },
|
|
5
6
|
delete: { id: 'project-asset.delete', shortcut: 'Delete' },
|
|
6
7
|
copyPath: { id: 'project-asset.copy-path', shortcut: 'Mod+Shift+C' },
|
|
@@ -73,12 +73,9 @@ export function inspectProjectAssetHealth(input: {
|
|
|
73
73
|
|
|
74
74
|
const dependencies = externalDependencies(assetPath, asset?.text);
|
|
75
75
|
if (dependencies.some((path) => !byPath.has(path))) codes.add('missing-dependency');
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
) {
|
|
80
|
-
codes.add('missing-attribution');
|
|
81
|
-
}
|
|
76
|
+
// Catalog attribution is project-authoring state in .vgai/provenance.json,
|
|
77
|
+
// outside this public/-rooted storage snapshot. Do not infer it is missing
|
|
78
|
+
// from the deliberate absence of a legacy per-directory sidecar.
|
|
82
79
|
if (input.recipe && input.recipe.recordedSourceHash !== input.recipe.currentSourceHash) {
|
|
83
80
|
codes.add('stale-import');
|
|
84
81
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { applyAssetRename } from '@engine/scene/asset-paths';
|
|
2
|
+
import { parsePrefabFile, parseSceneFile } from '@engine/scene/parse';
|
|
1
3
|
import type { HistoryService } from '../history/history-service';
|
|
2
4
|
import { getProjectFileHistory } from '../history/project-file-history';
|
|
3
5
|
import type { StorageBackend } from '../storage/types';
|
|
@@ -6,6 +8,10 @@ export type ProjectAssetOperation =
|
|
|
6
8
|
| { readonly type: 'create-folder'; readonly path: string }
|
|
7
9
|
| { readonly type: 'duplicate'; readonly source: string; readonly destination: string }
|
|
8
10
|
| { readonly type: 'move'; readonly source: string; readonly destination: string }
|
|
11
|
+
| {
|
|
12
|
+
readonly type: 'move-many';
|
|
13
|
+
readonly moves: readonly { readonly source: string; readonly destination: string }[];
|
|
14
|
+
}
|
|
9
15
|
| { readonly type: 'delete'; readonly paths: readonly string[] }
|
|
10
16
|
| { readonly type: 'import'; readonly files: readonly { path: string; bytes: Uint8Array }[] }
|
|
11
17
|
| { readonly type: 'reimport'; readonly files: readonly { path: string; bytes: Uint8Array }[] };
|
|
@@ -42,6 +48,62 @@ async function assertDestinationsFree(backend: StorageBackend, paths: readonly s
|
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
async function listAssetReferenceDocuments(
|
|
52
|
+
backend: StorageBackend,
|
|
53
|
+
directory = '',
|
|
54
|
+
): Promise<string[]> {
|
|
55
|
+
const documents: string[] = [];
|
|
56
|
+
for (const entry of await backend.list(directory)) {
|
|
57
|
+
if (entry.type === 'dir') {
|
|
58
|
+
documents.push(...(await listAssetReferenceDocuments(backend, entry.path)));
|
|
59
|
+
} else if (entry.path.endsWith('.vscn.json') || entry.path.endsWith('.prefab.json')) {
|
|
60
|
+
documents.push(entry.path);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return documents;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function rewriteAssetReferenceDocument(
|
|
67
|
+
path: string,
|
|
68
|
+
raw: string,
|
|
69
|
+
moves: readonly { source: string; destination: string }[],
|
|
70
|
+
): string {
|
|
71
|
+
if (path.endsWith('.prefab.json')) {
|
|
72
|
+
const prefab = parsePrefabFile(JSON.parse(raw), path);
|
|
73
|
+
let scene = parseSceneFile(
|
|
74
|
+
{ version: prefab.version, name: prefab.name, entities: [prefab.root] },
|
|
75
|
+
path,
|
|
76
|
+
);
|
|
77
|
+
for (const move of moves) scene = applyAssetRename(scene, move.source, move.destination);
|
|
78
|
+
const root = scene.entities[0];
|
|
79
|
+
if (!root) throw new Error('Prefab reference rewrite produced no root entity.');
|
|
80
|
+
const renamed = { ...prefab, root };
|
|
81
|
+
return JSON.stringify(renamed, null, 2);
|
|
82
|
+
}
|
|
83
|
+
let scene = parseSceneFile(JSON.parse(raw), path);
|
|
84
|
+
for (const move of moves) scene = applyAssetRename(scene, move.source, move.destination);
|
|
85
|
+
return JSON.stringify(scene, null, 2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function assetReferenceUpdates(
|
|
89
|
+
backend: StorageBackend,
|
|
90
|
+
moves: readonly { source: string; destination: string }[],
|
|
91
|
+
): Promise<{ path: string; data: string }[]> {
|
|
92
|
+
const updates: { path: string; data: string }[] = [];
|
|
93
|
+
for (const path of await listAssetReferenceDocuments(backend)) {
|
|
94
|
+
const raw = await backend.read(path);
|
|
95
|
+
if (!moves.some((move) => raw.includes(move.source))) continue;
|
|
96
|
+
try {
|
|
97
|
+
updates.push({ path, data: rewriteAssetReferenceDocument(path, raw, moves) });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Cannot move an asset while referenced document ${path} is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return updates;
|
|
105
|
+
}
|
|
106
|
+
|
|
45
107
|
/** Executes all writes through exact-byte project history; no UI owns filesystem mutation. */
|
|
46
108
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the tagged operation union is intentionally centralized so every project mutation shares one atomic history boundary.
|
|
47
109
|
export async function executeProjectAssetOperation(
|
|
@@ -69,16 +131,69 @@ export async function executeProjectAssetOperation(
|
|
|
69
131
|
const destination = validPath(operation.destination);
|
|
70
132
|
await assertDestinationsFree(dependencies.backend, [destination]);
|
|
71
133
|
const bytes = await dependencies.backend.readBytes(source);
|
|
134
|
+
const references = await assetReferenceUpdates(dependencies.backend, [
|
|
135
|
+
{ source, destination },
|
|
136
|
+
]);
|
|
137
|
+
const changes = new Map<string, string | Uint8Array | null>([
|
|
138
|
+
[destination, bytes],
|
|
139
|
+
[source, null],
|
|
140
|
+
]);
|
|
141
|
+
for (const reference of references) {
|
|
142
|
+
const referencePath = reference.path === source ? destination : reference.path;
|
|
143
|
+
changes.set(referencePath, reference.data);
|
|
144
|
+
}
|
|
72
145
|
const changed = await files.mutate(
|
|
73
|
-
[
|
|
74
|
-
{ path: destination, data: bytes },
|
|
75
|
-
{ path: source, data: null },
|
|
76
|
-
],
|
|
146
|
+
[...changes].map(([path, data]) => ({ path, data })),
|
|
77
147
|
{ label: `Move ${source}` },
|
|
78
148
|
);
|
|
79
149
|
if (changed) emitAssetMutation({ type: 'move', oldPath: source, newPath: destination });
|
|
80
150
|
return changed;
|
|
81
151
|
}
|
|
152
|
+
case 'move-many': {
|
|
153
|
+
const moves = operation.moves.map((move) => ({
|
|
154
|
+
source: validPath(move.source),
|
|
155
|
+
destination: validPath(move.destination),
|
|
156
|
+
}));
|
|
157
|
+
if (moves.length === 0) return false;
|
|
158
|
+
const sources = new Set(moves.map((move) => move.source));
|
|
159
|
+
const destinations = moves.map((move) => move.destination);
|
|
160
|
+
if (new Set(destinations).size !== destinations.length) {
|
|
161
|
+
throw new Error('Batch move destinations must be unique.');
|
|
162
|
+
}
|
|
163
|
+
for (const destination of destinations) {
|
|
164
|
+
if (!sources.has(destination) && (await dependencies.backend.exists(destination))) {
|
|
165
|
+
throw new Error(`An asset already exists at ${destination}.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const payloads = await Promise.all(
|
|
169
|
+
moves.map(async (move) => ({
|
|
170
|
+
...move,
|
|
171
|
+
bytes: await dependencies.backend.readBytes(move.source),
|
|
172
|
+
})),
|
|
173
|
+
);
|
|
174
|
+
const references = await assetReferenceUpdates(dependencies.backend, moves);
|
|
175
|
+
const destinationBySource = new Map(
|
|
176
|
+
moves.map(({ source, destination }) => [source, destination] as const),
|
|
177
|
+
);
|
|
178
|
+
const changes = new Map<string, string | Uint8Array | null>();
|
|
179
|
+
for (const { destination, bytes } of payloads) changes.set(destination, bytes);
|
|
180
|
+
for (const { source } of payloads) {
|
|
181
|
+
if (!destinations.includes(source)) changes.set(source, null);
|
|
182
|
+
}
|
|
183
|
+
for (const reference of references) {
|
|
184
|
+
changes.set(destinationBySource.get(reference.path) ?? reference.path, reference.data);
|
|
185
|
+
}
|
|
186
|
+
const changed = await files.mutate(
|
|
187
|
+
[...changes].map(([path, data]) => ({ path, data })),
|
|
188
|
+
{ label: `Move ${moves.length} assets` },
|
|
189
|
+
);
|
|
190
|
+
if (changed) {
|
|
191
|
+
for (const move of moves) {
|
|
192
|
+
emitAssetMutation({ type: 'move', oldPath: move.source, newPath: move.destination });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return changed;
|
|
196
|
+
}
|
|
82
197
|
case 'delete': {
|
|
83
198
|
const paths = [...new Set(operation.paths.map(validPath))];
|
|
84
199
|
const changed = await files.mutate(
|