@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,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
faArrowLeft,
|
|
3
3
|
faBoxArchive,
|
|
4
|
+
faChevronRight,
|
|
4
5
|
faCubes,
|
|
5
6
|
faEllipsis,
|
|
6
7
|
faFile,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
faPaintbrush,
|
|
13
14
|
faPuzzlePiece,
|
|
14
15
|
faImage as faSceneIcon,
|
|
16
|
+
faTable,
|
|
15
17
|
faTableCellsLarge,
|
|
16
18
|
} from '@fortawesome/free-solid-svg-icons';
|
|
17
19
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
assetDocumentKind,
|
|
23
25
|
} from '../asset-workflow/asset-capabilities';
|
|
24
26
|
import { confirmAssetAction } from '../asset-workflow/asset-workflow-quality';
|
|
27
|
+
import { importRecipePath } from '../asset-workflow/import-contract';
|
|
25
28
|
import {
|
|
26
29
|
PROJECT_ASSET_COMMANDS,
|
|
27
30
|
projectAssetBrowserStateKey,
|
|
@@ -56,6 +59,7 @@ import { getModelThumbnailRenderer, modelThumbnailFormat } from '../model-thumbn
|
|
|
56
59
|
import { getCurrentProject } from '../project-manager';
|
|
57
60
|
import { getStorageBackend } from '../storage';
|
|
58
61
|
import { openAssetDocument } from './asset-documents';
|
|
62
|
+
import { reimportStoredProjectAssets } from './asset-editor-persistence';
|
|
59
63
|
import './asset-workflow.css';
|
|
60
64
|
|
|
61
65
|
// --- Helpers ---
|
|
@@ -81,16 +85,16 @@ function getAssetKind(entry: AssetEntry): AssetKind {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
const assetIconColors: Record<BrowserKind, string> = {
|
|
84
|
-
folder: '
|
|
85
|
-
model: '
|
|
86
|
-
scene: '
|
|
87
|
-
prefab: '
|
|
88
|
-
material: '
|
|
89
|
-
audio: '
|
|
90
|
-
image: '
|
|
91
|
-
json: '
|
|
92
|
-
unknown: '
|
|
93
|
-
source: '
|
|
88
|
+
folder: 'var(--asset-icon-folder)',
|
|
89
|
+
model: 'var(--asset-icon-model)',
|
|
90
|
+
scene: 'var(--asset-icon-scene)',
|
|
91
|
+
prefab: 'var(--asset-icon-prefab)',
|
|
92
|
+
material: 'var(--asset-icon-material)',
|
|
93
|
+
audio: 'var(--asset-icon-audio)',
|
|
94
|
+
image: 'var(--asset-icon-image)',
|
|
95
|
+
json: 'var(--asset-icon-json)',
|
|
96
|
+
unknown: 'var(--asset-icon-unknown)',
|
|
97
|
+
source: 'var(--asset-icon-source)',
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
const assetIconMap: Record<BrowserKind, typeof faFolder> = {
|
|
@@ -250,6 +254,7 @@ interface PersistedAssetBrowserState {
|
|
|
250
254
|
sort: AssetSort;
|
|
251
255
|
filter: AssetFilter;
|
|
252
256
|
density: number;
|
|
257
|
+
expanded: string[];
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
const DEFAULT_BROWSER_STATE: PersistedAssetBrowserState = {
|
|
@@ -258,6 +263,7 @@ const DEFAULT_BROWSER_STATE: PersistedAssetBrowserState = {
|
|
|
258
263
|
sort: 'name',
|
|
259
264
|
filter: 'all',
|
|
260
265
|
density: 96,
|
|
266
|
+
expanded: ['public'],
|
|
261
267
|
};
|
|
262
268
|
|
|
263
269
|
function readAssetBrowserState(): PersistedAssetBrowserState {
|
|
@@ -276,7 +282,7 @@ function readAssetBrowserState(): PersistedAssetBrowserState {
|
|
|
276
282
|
// public asset root, so those old locations migrate home once.
|
|
277
283
|
currentPath:
|
|
278
284
|
legacyRoot === undefined || legacyRoot === 'public' ? (parsed.currentPath ?? '') : '',
|
|
279
|
-
viewMode: legacyViewMode === 'columns'
|
|
285
|
+
viewMode: legacyViewMode === 'columns' || legacyViewMode === 'list' ? legacyViewMode : 'grid',
|
|
280
286
|
};
|
|
281
287
|
} catch {
|
|
282
288
|
return DEFAULT_BROWSER_STATE;
|
|
@@ -290,8 +296,8 @@ interface AssetBrowserProps {
|
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
type AssetRoot = 'public';
|
|
293
|
-
type AssetViewMode = 'grid' | 'list';
|
|
294
|
-
type AssetSort = 'name' | 'type' | 'size' | 'health';
|
|
299
|
+
type AssetViewMode = 'grid' | 'list' | 'columns';
|
|
300
|
+
type AssetSort = 'name' | 'type' | 'size' | 'modified' | 'health';
|
|
295
301
|
type AssetFilter =
|
|
296
302
|
| 'all'
|
|
297
303
|
| 'runtime-ready'
|
|
@@ -367,6 +373,8 @@ function compareAssetEntries(
|
|
|
367
373
|
if (left.entry.type !== right.entry.type) return left.entry.type === 'directory' ? -1 : 1;
|
|
368
374
|
if (sort === 'size' && left.entry.size !== right.entry.size)
|
|
369
375
|
return right.entry.size - left.entry.size;
|
|
376
|
+
if (sort === 'modified' && left.entry.modifiedAt !== right.entry.modifiedAt)
|
|
377
|
+
return right.entry.modifiedAt - left.entry.modifiedAt;
|
|
370
378
|
if (sort === 'type') {
|
|
371
379
|
const order = getAssetKind(left.entry).localeCompare(getAssetKind(right.entry));
|
|
372
380
|
if (order !== 0) return order;
|
|
@@ -468,7 +476,7 @@ function TypedAssetThumbnail({ kind, name }: { kind: BrowserKind; name: string }
|
|
|
468
476
|
height: 44,
|
|
469
477
|
borderRadius: '50%',
|
|
470
478
|
background: `radial-gradient(circle at 32% 28%, white 0, ${color} 18%, var(--vgai-bg-inset) 72%)`,
|
|
471
|
-
boxShadow: '
|
|
479
|
+
boxShadow: 'var(--vgai-shadow-md)',
|
|
472
480
|
}}
|
|
473
481
|
/>
|
|
474
482
|
);
|
|
@@ -819,6 +827,16 @@ function AssetBrowserToolbar({
|
|
|
819
827
|
<EditorIcon icon={faList} />
|
|
820
828
|
</IconButton>
|
|
821
829
|
</Tooltip>
|
|
830
|
+
<Tooltip text="Column view" position="top">
|
|
831
|
+
<IconButton
|
|
832
|
+
aria-label="Column view"
|
|
833
|
+
data-testid="asset-view-columns"
|
|
834
|
+
aria-pressed={viewMode === 'columns'}
|
|
835
|
+
onClick={() => onViewModeChange('columns')}
|
|
836
|
+
>
|
|
837
|
+
<EditorIcon icon={faTable} />
|
|
838
|
+
</IconButton>
|
|
839
|
+
</Tooltip>
|
|
822
840
|
</ToolbarGroup>
|
|
823
841
|
<Select
|
|
824
842
|
className="vgai-select vgai-project-assets-toolbar__select"
|
|
@@ -829,6 +847,7 @@ function AssetBrowserToolbar({
|
|
|
829
847
|
<option value="name">Name</option>
|
|
830
848
|
<option value="type">Type</option>
|
|
831
849
|
<option value="size">Size</option>
|
|
850
|
+
<option value="modified">Modified</option>
|
|
832
851
|
<option value="health">Health</option>
|
|
833
852
|
</Select>
|
|
834
853
|
<Select
|
|
@@ -913,6 +932,98 @@ function AssetBrowserToolbar({
|
|
|
913
932
|
);
|
|
914
933
|
}
|
|
915
934
|
|
|
935
|
+
function ProjectAssetTree(props: {
|
|
936
|
+
currentPath: string;
|
|
937
|
+
expanded: ReadonlySet<string>;
|
|
938
|
+
revision: number;
|
|
939
|
+
onExpanded(path: string): void;
|
|
940
|
+
onNavigate(path: string): void;
|
|
941
|
+
}) {
|
|
942
|
+
const [folders, setFolders] = useState(new Map<string, string[]>());
|
|
943
|
+
|
|
944
|
+
useEffect(() => {
|
|
945
|
+
setFolders(new Map());
|
|
946
|
+
}, [props.revision]);
|
|
947
|
+
|
|
948
|
+
useEffect(() => {
|
|
949
|
+
for (const key of props.expanded) {
|
|
950
|
+
if (!key.startsWith('public') || folders.has(key)) continue;
|
|
951
|
+
const path = key === 'public' ? '' : key.slice('public/'.length);
|
|
952
|
+
void listAssets('public', path).then((entries) => {
|
|
953
|
+
setFolders((current) => {
|
|
954
|
+
const next = new Map(current);
|
|
955
|
+
next.set(
|
|
956
|
+
key,
|
|
957
|
+
entries.filter((entry) => entry.type === 'directory').map((entry) => entry.name),
|
|
958
|
+
);
|
|
959
|
+
return next;
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
}, [folders, props.expanded]);
|
|
964
|
+
|
|
965
|
+
const renderFolder = (path: string, depth: number): React.ReactNode => {
|
|
966
|
+
const key = path ? `public/${path}` : 'public';
|
|
967
|
+
const open = props.expanded.has(key);
|
|
968
|
+
const name = path.split('/').pop() || 'public';
|
|
969
|
+
return (
|
|
970
|
+
<div key={key}>
|
|
971
|
+
<div className="vgai-asset-tree-row" style={{ paddingLeft: 6 + depth * 14 }}>
|
|
972
|
+
<IconButton
|
|
973
|
+
className="vgai-asset-tree-toggle"
|
|
974
|
+
aria-label={`${open ? 'Collapse' : 'Expand'} ${name}`}
|
|
975
|
+
aria-expanded={open}
|
|
976
|
+
onClick={() => props.onExpanded(key)}
|
|
977
|
+
>
|
|
978
|
+
<EditorIcon
|
|
979
|
+
icon={faChevronRight}
|
|
980
|
+
style={{ transform: open ? 'rotate(90deg)' : undefined }}
|
|
981
|
+
/>
|
|
982
|
+
</IconButton>
|
|
983
|
+
<Button
|
|
984
|
+
variant="ghost"
|
|
985
|
+
size="compact"
|
|
986
|
+
className="vgai-asset-tree-target"
|
|
987
|
+
aria-current={props.currentPath === path ? 'page' : undefined}
|
|
988
|
+
onClick={() => props.onNavigate(path)}
|
|
989
|
+
>
|
|
990
|
+
<EditorIcon icon={faFolder} />
|
|
991
|
+
{name}
|
|
992
|
+
</Button>
|
|
993
|
+
</div>
|
|
994
|
+
{open &&
|
|
995
|
+
folders
|
|
996
|
+
.get(key)
|
|
997
|
+
?.map((child) => renderFolder(path ? `${path}/${child}` : child, depth + 1))}
|
|
998
|
+
</div>
|
|
999
|
+
);
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<aside className="vgai-asset-tree" aria-label="Project asset sources">
|
|
1004
|
+
<div className="vgai-asset-facet-title">Project</div>
|
|
1005
|
+
{renderFolder('', 0)}
|
|
1006
|
+
<div className="vgai-asset-facet-title">References</div>
|
|
1007
|
+
<Button
|
|
1008
|
+
variant="ghost"
|
|
1009
|
+
size="compact"
|
|
1010
|
+
className="vgai-asset-tree-target"
|
|
1011
|
+
onClick={() => props.onNavigate('asset-library/local')}
|
|
1012
|
+
>
|
|
1013
|
+
Imported
|
|
1014
|
+
</Button>
|
|
1015
|
+
<Button
|
|
1016
|
+
variant="ghost"
|
|
1017
|
+
size="compact"
|
|
1018
|
+
className="vgai-asset-tree-target"
|
|
1019
|
+
onClick={() => props.onNavigate('.generated')}
|
|
1020
|
+
>
|
|
1021
|
+
Generated
|
|
1022
|
+
</Button>
|
|
1023
|
+
</aside>
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
916
1027
|
export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
917
1028
|
const store = useEditorStore();
|
|
918
1029
|
const history = useHistoryService();
|
|
@@ -925,8 +1036,12 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
925
1036
|
const [density, setDensity] = useState(initial.density);
|
|
926
1037
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
927
1038
|
const [entries, setEntries] = useState<AssetEntry[]>([]);
|
|
1039
|
+
const [expanded, setExpanded] = useState(new Set(initial.expanded));
|
|
1040
|
+
const [treeRevision, setTreeRevision] = useState(0);
|
|
928
1041
|
const [searchQuery, setSearchQuery] = useState('');
|
|
929
1042
|
const [selectedFiles, setSelectedFiles] = useState(new Set<string>());
|
|
1043
|
+
const [reimportableSelectedPaths, setReimportableSelectedPaths] = useState<string[]>([]);
|
|
1044
|
+
const [operationError, setOperationError] = useState<string | null>(null);
|
|
930
1045
|
const [selectionAnchor, setSelectionAnchor] = useState<string | null>(null);
|
|
931
1046
|
const [contextMenu, setContextMenu] = useState<{
|
|
932
1047
|
x: number;
|
|
@@ -946,9 +1061,10 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
946
1061
|
sort,
|
|
947
1062
|
filter,
|
|
948
1063
|
density,
|
|
1064
|
+
expanded: [...expanded],
|
|
949
1065
|
} satisfies PersistedAssetBrowserState),
|
|
950
1066
|
);
|
|
951
|
-
}, [currentPath, density, filter, sort, viewMode]);
|
|
1067
|
+
}, [currentPath, density, expanded, filter, sort, viewMode]);
|
|
952
1068
|
|
|
953
1069
|
const fetchEntries = useCallback(async (root: AssetRoot, dir: string) => {
|
|
954
1070
|
const key = `${root}:${dir}`;
|
|
@@ -1110,6 +1226,7 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1110
1226
|
|
|
1111
1227
|
// --- Refresh ---
|
|
1112
1228
|
const onRefresh = useCallback(() => {
|
|
1229
|
+
setTreeRevision((revision) => revision + 1);
|
|
1113
1230
|
cacheRef.current.clear();
|
|
1114
1231
|
fetchEntries(currentRoot, currentPath);
|
|
1115
1232
|
}, [currentRoot, currentPath, fetchEntries]);
|
|
@@ -1146,11 +1263,38 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1146
1263
|
),
|
|
1147
1264
|
[currentPath, selectedItems],
|
|
1148
1265
|
);
|
|
1266
|
+
const selectedProjectPaths = useMemo(
|
|
1267
|
+
() => selectedAssetPaths.map((path) => `public/${path}`),
|
|
1268
|
+
[selectedAssetPaths],
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
useEffect(() => {
|
|
1272
|
+
let cancelled = false;
|
|
1273
|
+
void Promise.all(
|
|
1274
|
+
selectedAssetPaths.map(async (path) => ({
|
|
1275
|
+
path,
|
|
1276
|
+
reimportable: await getStorageBackend().exists(importRecipePath(path)),
|
|
1277
|
+
})),
|
|
1278
|
+
).then((checks) => {
|
|
1279
|
+
if (!cancelled) {
|
|
1280
|
+
setReimportableSelectedPaths(
|
|
1281
|
+
checks.filter((check) => check.reimportable).map((check) => check.path),
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
return () => {
|
|
1286
|
+
cancelled = true;
|
|
1287
|
+
};
|
|
1288
|
+
}, [selectedAssetPaths]);
|
|
1149
1289
|
|
|
1150
1290
|
useEffect(() => {
|
|
1151
1291
|
const available = new Set(browserEntries.map((item) => item.key));
|
|
1152
1292
|
setSelectedFiles((current) => {
|
|
1153
1293
|
const next = new Set([...current].filter((key) => available.has(key)));
|
|
1294
|
+
if (current.size > 0 && next.size === 0) {
|
|
1295
|
+
setSelectionAnchor(null);
|
|
1296
|
+
clearSelectedAsset();
|
|
1297
|
+
}
|
|
1154
1298
|
return next.size === current.size ? current : next;
|
|
1155
1299
|
});
|
|
1156
1300
|
}, [browserEntries]);
|
|
@@ -1174,8 +1318,42 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1174
1318
|
)
|
|
1175
1319
|
return;
|
|
1176
1320
|
await finishOperation({ type: 'delete', paths: selectedAssetPaths });
|
|
1321
|
+
setSelectedFiles(new Set());
|
|
1322
|
+
setSelectionAnchor(null);
|
|
1323
|
+
clearSelectedAsset();
|
|
1177
1324
|
}, [finishOperation, selectedAssetPaths]);
|
|
1178
1325
|
|
|
1326
|
+
const moveSelected = useCallback(async () => {
|
|
1327
|
+
if (selectedAssetPaths.length === 0) return;
|
|
1328
|
+
const destinationFolder = window.prompt('Move selected assets to folder', currentPath);
|
|
1329
|
+
if (destinationFolder === null) return;
|
|
1330
|
+
const folder = destinationFolder.replace(/^\/+|\/+$/g, '');
|
|
1331
|
+
setOperationError(null);
|
|
1332
|
+
try {
|
|
1333
|
+
await finishOperation({
|
|
1334
|
+
type: 'move-many',
|
|
1335
|
+
moves: selectedAssetPaths.map((source) => ({
|
|
1336
|
+
source,
|
|
1337
|
+
destination: folder ? `${folder}/${source.split('/').pop()}` : source.split('/').pop()!,
|
|
1338
|
+
})),
|
|
1339
|
+
});
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
setOperationError(error instanceof Error ? error.message : String(error));
|
|
1342
|
+
}
|
|
1343
|
+
}, [currentPath, finishOperation, selectedAssetPaths]);
|
|
1344
|
+
|
|
1345
|
+
const reimportSelected = useCallback(async () => {
|
|
1346
|
+
if (reimportableSelectedPaths.length === 0) return;
|
|
1347
|
+
setOperationError(null);
|
|
1348
|
+
try {
|
|
1349
|
+
await reimportStoredProjectAssets(reimportableSelectedPaths);
|
|
1350
|
+
cacheRef.current.clear();
|
|
1351
|
+
await fetchEntries(currentRoot, currentPath);
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
setOperationError(error instanceof Error ? error.message : String(error));
|
|
1354
|
+
}
|
|
1355
|
+
}, [currentPath, currentRoot, fetchEntries, reimportableSelectedPaths]);
|
|
1356
|
+
|
|
1179
1357
|
const createFolder = useCallback(async () => {
|
|
1180
1358
|
if (currentRoot !== 'public') return;
|
|
1181
1359
|
const name = window.prompt('Folder name');
|
|
@@ -1225,6 +1403,15 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1225
1403
|
[currentPath, finishOperation],
|
|
1226
1404
|
);
|
|
1227
1405
|
|
|
1406
|
+
const toggleExpanded = useCallback((path: string) => {
|
|
1407
|
+
setExpanded((current) => {
|
|
1408
|
+
const next = new Set(current);
|
|
1409
|
+
if (next.has(path)) next.delete(path);
|
|
1410
|
+
else next.add(path);
|
|
1411
|
+
return next;
|
|
1412
|
+
});
|
|
1413
|
+
}, []);
|
|
1414
|
+
|
|
1228
1415
|
useEffect(() => {
|
|
1229
1416
|
const handler = (event: Event) => {
|
|
1230
1417
|
const id = (event as CustomEvent<{ id?: string }>).detail?.id;
|
|
@@ -1232,13 +1419,15 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1232
1419
|
if (id === PROJECT_ASSET_COMMANDS.refresh.id) onRefresh();
|
|
1233
1420
|
else if (id === PROJECT_ASSET_COMMANDS.import.id) importInputRef.current?.click();
|
|
1234
1421
|
else if (id === PROJECT_ASSET_COMMANDS.createFolder.id) void createFolder();
|
|
1422
|
+
else if (id === PROJECT_ASSET_COMMANDS.move.id) void moveSelected();
|
|
1423
|
+
else if (id === PROJECT_ASSET_COMMANDS.reimport.id) void reimportSelected();
|
|
1235
1424
|
else if (id === PROJECT_ASSET_COMMANDS.delete.id) void deleteSelected();
|
|
1236
1425
|
else if (id === PROJECT_ASSET_COMMANDS.rename.id && item) void renameItem(item);
|
|
1237
1426
|
else if (id === PROJECT_ASSET_COMMANDS.duplicate.id && item) void duplicateItem(item);
|
|
1238
|
-
else if (id === PROJECT_ASSET_COMMANDS.copyPath.id &&
|
|
1239
|
-
void navigator.clipboard.writeText(
|
|
1240
|
-
else if (id === PROJECT_ASSET_COMMANDS.reveal.id)
|
|
1241
|
-
void revealInFinder(
|
|
1427
|
+
else if (id === PROJECT_ASSET_COMMANDS.copyPath.id && selectedProjectPaths[0])
|
|
1428
|
+
void navigator.clipboard.writeText(selectedProjectPaths[0]);
|
|
1429
|
+
else if (id === PROJECT_ASSET_COMMANDS.reveal.id && selectedProjectPaths[0])
|
|
1430
|
+
void revealInFinder(selectedProjectPaths[0]);
|
|
1242
1431
|
};
|
|
1243
1432
|
window.addEventListener('editor:project-asset-command', handler);
|
|
1244
1433
|
return () => window.removeEventListener('editor:project-asset-command', handler);
|
|
@@ -1247,14 +1436,17 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1247
1436
|
currentPath,
|
|
1248
1437
|
deleteSelected,
|
|
1249
1438
|
duplicateItem,
|
|
1439
|
+
moveSelected,
|
|
1250
1440
|
onRefresh,
|
|
1441
|
+
reimportSelected,
|
|
1251
1442
|
renameItem,
|
|
1252
|
-
selectedAssetPaths,
|
|
1253
1443
|
selectedItems,
|
|
1444
|
+
selectedProjectPaths,
|
|
1254
1445
|
]);
|
|
1255
1446
|
|
|
1256
1447
|
return (
|
|
1257
1448
|
<div
|
|
1449
|
+
className="vgai-asset-browser"
|
|
1258
1450
|
onPointerDown={() => setActiveScope('asset-browser')}
|
|
1259
1451
|
onKeyDown={(event) => {
|
|
1260
1452
|
if (isEditableTarget(event.target)) return;
|
|
@@ -1273,12 +1465,18 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1273
1465
|
}}
|
|
1274
1466
|
style={{
|
|
1275
1467
|
display: 'flex',
|
|
1276
|
-
flexDirection: 'column',
|
|
1277
1468
|
flex: 1,
|
|
1278
1469
|
minHeight: 0,
|
|
1279
1470
|
position: 'relative',
|
|
1280
1471
|
}}
|
|
1281
1472
|
>
|
|
1473
|
+
<ProjectAssetTree
|
|
1474
|
+
currentPath={currentPath}
|
|
1475
|
+
expanded={expanded}
|
|
1476
|
+
revision={treeRevision}
|
|
1477
|
+
onExpanded={toggleExpanded}
|
|
1478
|
+
onNavigate={(path) => navigate('public', path)}
|
|
1479
|
+
/>
|
|
1282
1480
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
|
1283
1481
|
<TextInput
|
|
1284
1482
|
ref={importInputRef}
|
|
@@ -1313,19 +1511,62 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1313
1511
|
onRefresh={onRefresh}
|
|
1314
1512
|
/>
|
|
1315
1513
|
|
|
1514
|
+
{operationError && (
|
|
1515
|
+
<div role="alert" className="vgai-asset-operation-error">
|
|
1516
|
+
{operationError}
|
|
1517
|
+
</div>
|
|
1518
|
+
)}
|
|
1519
|
+
{selectedAssetPaths.length > 0 && (
|
|
1520
|
+
<EditorToolbar label="Selected asset actions" compact>
|
|
1521
|
+
<span>{selectedAssetPaths.length} selected</span>
|
|
1522
|
+
<Button variant="ghost" size="compact" onClick={() => void moveSelected()}>
|
|
1523
|
+
Move selected
|
|
1524
|
+
</Button>
|
|
1525
|
+
<Button
|
|
1526
|
+
variant="ghost"
|
|
1527
|
+
size="compact"
|
|
1528
|
+
disabled={reimportableSelectedPaths.length === 0}
|
|
1529
|
+
title={
|
|
1530
|
+
reimportableSelectedPaths.length
|
|
1531
|
+
? 'Run the saved recipes as one batch'
|
|
1532
|
+
: 'No selected asset has a saved import recipe'
|
|
1533
|
+
}
|
|
1534
|
+
onClick={() => void reimportSelected()}
|
|
1535
|
+
>
|
|
1536
|
+
Reimport selected
|
|
1537
|
+
</Button>
|
|
1538
|
+
<Button
|
|
1539
|
+
variant="ghost"
|
|
1540
|
+
size="compact"
|
|
1541
|
+
aria-label="Delete selected assets"
|
|
1542
|
+
onClick={() => void deleteSelected()}
|
|
1543
|
+
>
|
|
1544
|
+
Delete
|
|
1545
|
+
</Button>
|
|
1546
|
+
</EditorToolbar>
|
|
1547
|
+
)}
|
|
1548
|
+
|
|
1316
1549
|
{/* One file-browser surface; grid/list are presentation modes, not navigation. */}
|
|
1317
1550
|
<div
|
|
1318
1551
|
className="vgai-project-assets-results"
|
|
1319
1552
|
data-view={viewMode}
|
|
1553
|
+
data-testid="project-asset-results"
|
|
1554
|
+
role="listbox"
|
|
1555
|
+
aria-label="Project assets"
|
|
1556
|
+
aria-multiselectable="true"
|
|
1320
1557
|
style={{
|
|
1321
1558
|
flex: 1,
|
|
1322
1559
|
overflowY: 'auto',
|
|
1323
1560
|
padding: 8,
|
|
1324
|
-
display: viewMode === '
|
|
1561
|
+
display: viewMode === 'list' ? 'flex' : 'grid',
|
|
1325
1562
|
gridTemplateColumns:
|
|
1326
|
-
viewMode === 'grid'
|
|
1563
|
+
viewMode === 'grid'
|
|
1564
|
+
? `repeat(auto-fill, minmax(${density}px, 1fr))`
|
|
1565
|
+
: viewMode === 'columns'
|
|
1566
|
+
? 'repeat(auto-fill, minmax(190px, 1fr))'
|
|
1567
|
+
: undefined,
|
|
1327
1568
|
gridAutoRows: viewMode === 'grid' ? 'min-content' : undefined,
|
|
1328
|
-
flexDirection: viewMode === '
|
|
1569
|
+
flexDirection: viewMode === 'list' ? 'column' : undefined,
|
|
1329
1570
|
gap: viewMode === 'grid' ? 6 : 1,
|
|
1330
1571
|
alignContent: 'start',
|
|
1331
1572
|
}}
|
|
@@ -1409,12 +1650,45 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1409
1650
|
>
|
|
1410
1651
|
Duplicate <kbd>⌘D</kbd>
|
|
1411
1652
|
</MenuItem>
|
|
1653
|
+
<MenuItem
|
|
1654
|
+
type="button"
|
|
1655
|
+
role="menuitem"
|
|
1656
|
+
data-command-id={PROJECT_ASSET_COMMANDS.move.id}
|
|
1657
|
+
disabled={selectedAssetPaths.length === 0}
|
|
1658
|
+
title={selectedAssetPaths.length ? 'Move selected assets' : 'Select a file to move'}
|
|
1659
|
+
onClick={() => {
|
|
1660
|
+
void moveSelected();
|
|
1661
|
+
setContextMenu(null);
|
|
1662
|
+
}}
|
|
1663
|
+
>
|
|
1664
|
+
Move selected…
|
|
1665
|
+
</MenuItem>
|
|
1666
|
+
<MenuItem
|
|
1667
|
+
type="button"
|
|
1668
|
+
role="menuitem"
|
|
1669
|
+
data-command-id={PROJECT_ASSET_COMMANDS.reimport.id}
|
|
1670
|
+
disabled={reimportableSelectedPaths.length === 0}
|
|
1671
|
+
title={
|
|
1672
|
+
reimportableSelectedPaths.length
|
|
1673
|
+
? 'Run the saved recipes as one batch'
|
|
1674
|
+
: 'No selected asset has a saved import recipe'
|
|
1675
|
+
}
|
|
1676
|
+
onClick={() => {
|
|
1677
|
+
void reimportSelected();
|
|
1678
|
+
setContextMenu(null);
|
|
1679
|
+
}}
|
|
1680
|
+
>
|
|
1681
|
+
Reimport selected
|
|
1682
|
+
</MenuItem>
|
|
1412
1683
|
<MenuItem
|
|
1413
1684
|
type="button"
|
|
1414
1685
|
role="menuitem"
|
|
1415
1686
|
data-command-id={PROJECT_ASSET_COMMANDS.copyPath.id}
|
|
1687
|
+
disabled={!selectedProjectPaths[0]}
|
|
1416
1688
|
onClick={() => {
|
|
1417
|
-
|
|
1689
|
+
if (selectedProjectPaths[0]) {
|
|
1690
|
+
void navigator.clipboard.writeText(selectedProjectPaths[0]);
|
|
1691
|
+
}
|
|
1418
1692
|
setContextMenu(null);
|
|
1419
1693
|
}}
|
|
1420
1694
|
>
|
|
@@ -1424,8 +1698,9 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1424
1698
|
type="button"
|
|
1425
1699
|
role="menuitem"
|
|
1426
1700
|
data-command-id={PROJECT_ASSET_COMMANDS.reveal.id}
|
|
1701
|
+
disabled={!selectedProjectPaths[0]}
|
|
1427
1702
|
onClick={() => {
|
|
1428
|
-
void revealInFinder(
|
|
1703
|
+
if (selectedProjectPaths[0]) void revealInFinder(selectedProjectPaths[0]);
|
|
1429
1704
|
setContextMenu(null);
|
|
1430
1705
|
}}
|
|
1431
1706
|
>
|