@vgai/editor 0.4.0-canary.20260715.1 → 0.4.0
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-CyiTVRi6.js → axe-BJjvBeOZ.js} +1 -1
- package/dist/assets/{browserAll-fGZ_P1P8.js → browserAll-Dn0pgD87.js} +1 -1
- package/dist/assets/{create-runtime-BzSHBTYw.js → create-runtime-BF9OD-U8.js} +1 -1
- package/dist/assets/{game-NUAnyPkS.js → game-B346oj_f.js} +1 -1
- package/dist/assets/{game-uR_lv3Mp.js → game-BCNN63uV.js} +1 -1
- package/dist/assets/{game-HqybI_X7.js → game-BR0GBNgF.js} +1 -1
- package/dist/assets/{game-DQ7YHNS8.js → game-CLNm2g0y.js} +1 -1
- package/dist/assets/{game-DX6Obomf.js → game-DQxhCYsZ.js} +1 -1
- package/dist/assets/{game-DoUJj8vY.js → game-DeTGdgYt.js} +1 -1
- package/dist/assets/{game-loop-o2CcBMFY.js → game-loop-t5W_oqxz.js} +1 -1
- package/dist/assets/{game-source-C8HM7gbP.js → game-source-DzE9taML.js} +1 -1
- package/dist/assets/{game-ck_dNIE6.js → game-u6c2Patu.js} +1 -1
- package/dist/assets/{game-hIf5LD4E.js → game-umrHIqFB.js} +1 -1
- package/dist/assets/{index-CoSRCKPS.js → index-CgcH1RpF.js} +1 -1
- package/dist/assets/{index-D93bBuUl.js → index-D3tcNB2T.js} +1 -1
- package/dist/assets/{index-B-HA1uu3.js → index-DbQzGLxv.js} +769 -769
- package/dist/assets/{index-CSBAZwMx.js → index-Fc8Ezr20.js} +1 -1
- package/dist/assets/index-bAGhEnnO.css +1 -0
- package/dist/assets/{ingest-siblings-CCkkE2h7.js → ingest-siblings-CW_xbvrF.js} +1 -1
- package/dist/assets/{mount-manifest-B2ECssU2.js → mount-manifest-DgUscyaD.js} +1 -1
- package/dist/assets/{react-18-Bmj05S8J.js → react-18-C4tVCii_.js} +1 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +1 -0
- package/dist/assets/{world-documents-DXPcAEA0.js → world-documents-BSaA5YIF.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -3
- package/src/asset-editor-context.tsx +130 -0
- package/src/asset-preview.ts +4 -0
- package/src/asset-selection.ts +9 -4
- package/src/asset-workflow/thumbnail-system.ts +16 -7
- package/src/components/AssetBrowser.tsx +15 -7
- package/src/components/AssetEditorShell.tsx +42 -112
- package/src/components/AssetImportDetails.tsx +24 -6
- package/src/components/AssetLibraryPanel.tsx +1 -1
- package/src/components/GameHierarchy.tsx +26 -1
- package/src/components/Inspector.tsx +38 -2
- package/src/components/OnlineAssetBrowser.tsx +40 -409
- package/src/components/WorkspaceDock.tsx +6 -12
- package/src/components/asset-documents.tsx +41 -12
- package/src/components/asset-selection-section.tsx +34 -1
- package/src/components/asset-viewers/ModelViewer.tsx +7 -4
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +233 -205
- package/src/components/asset-workflow.css +39 -220
- package/src/editor-api.ts +1 -1
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bu01fqch.css +0 -1
- package/dist/assets/webworkerAll-BC6gMOLZ.js +0 -1
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { clearSelectedAsset, getSelectedAsset, setSelectedAsset } from '../asset-selection';
|
|
3
4
|
import {
|
|
4
5
|
type AssetLibrarySort,
|
|
5
6
|
assetLibraryStateKey,
|
|
6
7
|
onlineAssetKey,
|
|
7
8
|
queryAssetLibrary,
|
|
8
9
|
} from '../asset-workflow/asset-library-query';
|
|
9
|
-
import {
|
|
10
|
-
Button,
|
|
11
|
-
Checkbox,
|
|
12
|
-
EditorIcon,
|
|
13
|
-
Select,
|
|
14
|
-
StateSurface,
|
|
15
|
-
TextInput,
|
|
16
|
-
text,
|
|
17
|
-
} from '../design-system';
|
|
10
|
+
import { Button, Checkbox, EditorIcon, Select, StateSurface, TextInput } from '../design-system';
|
|
18
11
|
import { useEditorStore } from '../EditorContext';
|
|
19
12
|
import {
|
|
20
13
|
getAssetLibraryCategories,
|
|
@@ -26,11 +19,9 @@ import {
|
|
|
26
19
|
} from '../editor-api';
|
|
27
20
|
import { getCurrentProject } from '../project-manager';
|
|
28
21
|
import { openOnlineAssetDocument } from './asset-documents';
|
|
29
|
-
import { OnlineAssetDetail } from './asset-viewers/OnlineAssetDetail';
|
|
30
22
|
import './asset-workflow.css';
|
|
31
23
|
|
|
32
24
|
type Source = 'local' | 'polyhaven' | 'ambientcg';
|
|
33
|
-
type Density = 'compact' | 'standard' | 'large';
|
|
34
25
|
type Sort = AssetLibrarySort;
|
|
35
26
|
|
|
36
27
|
interface SourceOption {
|
|
@@ -74,30 +65,20 @@ interface PersistedLibraryState {
|
|
|
74
65
|
source: Source;
|
|
75
66
|
assetType: string;
|
|
76
67
|
query: string;
|
|
77
|
-
density: Density;
|
|
78
68
|
sort: Sort;
|
|
79
69
|
favorites: string[];
|
|
80
70
|
savedSearches: string[];
|
|
81
71
|
recent: string[];
|
|
82
|
-
sourceWidth: number;
|
|
83
|
-
detailsWidth: number;
|
|
84
|
-
sourceCollapsed: boolean;
|
|
85
|
-
detailsCollapsed: boolean;
|
|
86
72
|
}
|
|
87
73
|
|
|
88
74
|
const DEFAULT_STATE: PersistedLibraryState = {
|
|
89
75
|
source: 'local',
|
|
90
76
|
assetType: 'all',
|
|
91
77
|
query: '',
|
|
92
|
-
density: 'standard',
|
|
93
78
|
sort: 'relevance',
|
|
94
79
|
favorites: [],
|
|
95
80
|
savedSearches: [],
|
|
96
81
|
recent: [],
|
|
97
|
-
sourceWidth: 210,
|
|
98
|
-
detailsWidth: 360,
|
|
99
|
-
sourceCollapsed: false,
|
|
100
|
-
detailsCollapsed: false,
|
|
101
82
|
};
|
|
102
83
|
|
|
103
84
|
function readState(): PersistedLibraryState {
|
|
@@ -161,7 +142,10 @@ function SourcePane(props: {
|
|
|
161
142
|
.filter((value) => value.toLowerCase().includes(props.categoryQuery.toLowerCase()))
|
|
162
143
|
.slice(0, 80);
|
|
163
144
|
return (
|
|
164
|
-
<aside
|
|
145
|
+
<aside
|
|
146
|
+
className="vgai-asset-library-navigator__source-filters"
|
|
147
|
+
aria-label="Library sources and filters"
|
|
148
|
+
>
|
|
165
149
|
<div className="vgai-asset-facet-title">Sources</div>
|
|
166
150
|
{SOURCES.map((entry) => (
|
|
167
151
|
<Button
|
|
@@ -333,180 +317,6 @@ function AssetCardThumbnail({ asset }: { asset: OnlineAsset }) {
|
|
|
333
317
|
);
|
|
334
318
|
}
|
|
335
319
|
|
|
336
|
-
function VirtualAssetGrid(props: {
|
|
337
|
-
assets: readonly OnlineAsset[];
|
|
338
|
-
selectedId: string | null;
|
|
339
|
-
density: Density;
|
|
340
|
-
loading: boolean;
|
|
341
|
-
hasMore: boolean;
|
|
342
|
-
isFavorite(asset: OnlineAsset): boolean;
|
|
343
|
-
onSelect(asset: OnlineAsset): void;
|
|
344
|
-
onOpen(asset: OnlineAsset): void;
|
|
345
|
-
onLoadMore(): void;
|
|
346
|
-
}) {
|
|
347
|
-
const viewportRef = useRef<HTMLDivElement>(null);
|
|
348
|
-
const typeahead = useRef({ value: '', timer: 0 });
|
|
349
|
-
const [metrics, setMetrics] = useState({ width: 800, height: 600, scrollTop: 0 });
|
|
350
|
-
const cardWidth = props.density === 'compact' ? 136 : props.density === 'large' ? 208 : 168;
|
|
351
|
-
const rowHeight = props.density === 'compact' ? 178 : props.density === 'large' ? 254 : 214;
|
|
352
|
-
const gap = 10;
|
|
353
|
-
const columns = Math.max(1, Math.floor((metrics.width - gap) / (cardWidth + gap)));
|
|
354
|
-
const rows = Math.ceil(props.assets.length / columns);
|
|
355
|
-
const firstRow = Math.max(0, Math.floor(metrics.scrollTop / rowHeight) - 2);
|
|
356
|
-
const lastRow = Math.min(rows, Math.ceil((metrics.scrollTop + metrics.height) / rowHeight) + 3);
|
|
357
|
-
const firstIndex = firstRow * columns;
|
|
358
|
-
const lastIndex = Math.min(props.assets.length, lastRow * columns);
|
|
359
|
-
const visible = props.assets.slice(firstIndex, lastIndex);
|
|
360
|
-
|
|
361
|
-
useEffect(() => {
|
|
362
|
-
const element = viewportRef.current;
|
|
363
|
-
if (!element || typeof ResizeObserver === 'undefined') return;
|
|
364
|
-
const observer = new ResizeObserver(([entry]) => {
|
|
365
|
-
if (!entry) return;
|
|
366
|
-
setMetrics((current) => ({
|
|
367
|
-
...current,
|
|
368
|
-
width: entry.contentRect.width,
|
|
369
|
-
height: entry.contentRect.height,
|
|
370
|
-
}));
|
|
371
|
-
});
|
|
372
|
-
observer.observe(element);
|
|
373
|
-
return () => observer.disconnect();
|
|
374
|
-
}, []);
|
|
375
|
-
|
|
376
|
-
useEffect(() => () => window.clearTimeout(typeahead.current.timer), []);
|
|
377
|
-
|
|
378
|
-
const { hasMore, loading, onLoadMore } = props;
|
|
379
|
-
useEffect(() => {
|
|
380
|
-
if (hasMore && !loading && rows > 0 && lastRow >= rows - 2) onLoadMore();
|
|
381
|
-
}, [hasMore, lastRow, loading, onLoadMore, rows]);
|
|
382
|
-
|
|
383
|
-
const selectedIndex = props.assets.findIndex((asset) => asset.id === props.selectedId);
|
|
384
|
-
const moveSelection = (nextIndex: number) => {
|
|
385
|
-
const asset = props.assets[Math.max(0, Math.min(props.assets.length - 1, nextIndex))];
|
|
386
|
-
if (!asset) return;
|
|
387
|
-
props.onSelect(asset);
|
|
388
|
-
const row = Math.floor(Math.max(0, nextIndex) / columns);
|
|
389
|
-
if (viewportRef.current) viewportRef.current.scrollTop = row * rowHeight;
|
|
390
|
-
};
|
|
391
|
-
const onGridKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
392
|
-
const pageSize = Math.max(columns, Math.floor(metrics.height / rowHeight) * columns);
|
|
393
|
-
switch (event.key) {
|
|
394
|
-
case 'Enter':
|
|
395
|
-
if (selectedIndex >= 0) props.onOpen(props.assets[selectedIndex]!);
|
|
396
|
-
else return;
|
|
397
|
-
break;
|
|
398
|
-
case 'ArrowRight':
|
|
399
|
-
moveSelection(selectedIndex + 1);
|
|
400
|
-
break;
|
|
401
|
-
case 'ArrowLeft':
|
|
402
|
-
moveSelection(selectedIndex - 1);
|
|
403
|
-
break;
|
|
404
|
-
case 'ArrowDown':
|
|
405
|
-
moveSelection(selectedIndex + columns);
|
|
406
|
-
break;
|
|
407
|
-
case 'ArrowUp':
|
|
408
|
-
moveSelection(selectedIndex - columns);
|
|
409
|
-
break;
|
|
410
|
-
case 'Home':
|
|
411
|
-
moveSelection(0);
|
|
412
|
-
break;
|
|
413
|
-
case 'End':
|
|
414
|
-
moveSelection(props.assets.length - 1);
|
|
415
|
-
break;
|
|
416
|
-
case 'PageDown':
|
|
417
|
-
moveSelection(selectedIndex + pageSize);
|
|
418
|
-
break;
|
|
419
|
-
case 'PageUp':
|
|
420
|
-
moveSelection(selectedIndex - pageSize);
|
|
421
|
-
break;
|
|
422
|
-
default: {
|
|
423
|
-
if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
424
|
-
window.clearTimeout(typeahead.current.timer);
|
|
425
|
-
typeahead.current.value += event.key.toLowerCase();
|
|
426
|
-
typeahead.current.timer = window.setTimeout(() => {
|
|
427
|
-
typeahead.current.value = '';
|
|
428
|
-
}, 700);
|
|
429
|
-
const match = props.assets.findIndex((asset) =>
|
|
430
|
-
asset.name.toLowerCase().startsWith(typeahead.current.value),
|
|
431
|
-
);
|
|
432
|
-
if (match < 0) return;
|
|
433
|
-
moveSelection(match);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
event.preventDefault();
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
return (
|
|
440
|
-
<div
|
|
441
|
-
ref={viewportRef}
|
|
442
|
-
className="vgai-asset-library__grid"
|
|
443
|
-
role="listbox"
|
|
444
|
-
aria-label="Asset results"
|
|
445
|
-
aria-busy={props.loading}
|
|
446
|
-
tabIndex={0}
|
|
447
|
-
onScroll={(event) =>
|
|
448
|
-
setMetrics((current) => ({ ...current, scrollTop: event.currentTarget.scrollTop }))
|
|
449
|
-
}
|
|
450
|
-
onKeyDown={onGridKeyDown}
|
|
451
|
-
>
|
|
452
|
-
<div
|
|
453
|
-
style={{ position: 'relative', height: Math.max(metrics.height, rows * rowHeight + gap) }}
|
|
454
|
-
>
|
|
455
|
-
{visible.map((asset, localIndex) => {
|
|
456
|
-
const index = firstIndex + localIndex;
|
|
457
|
-
const column = index % columns;
|
|
458
|
-
const row = Math.floor(index / columns);
|
|
459
|
-
const downloaded = getDownloadedAssetPath(asset.source, asset.id) != null;
|
|
460
|
-
return (
|
|
461
|
-
<Button
|
|
462
|
-
key={onlineAssetKey(asset)}
|
|
463
|
-
type="button"
|
|
464
|
-
role="option"
|
|
465
|
-
aria-selected={props.selectedId === asset.id}
|
|
466
|
-
aria-label={`${asset.name}, ${asset.type}, ${asset.source}`}
|
|
467
|
-
className="vgai-asset-card"
|
|
468
|
-
style={{ left: gap + column * (cardWidth + gap), top: gap + row * rowHeight }}
|
|
469
|
-
onClick={() => props.onSelect(asset)}
|
|
470
|
-
onDoubleClick={() => props.onOpen(asset)}
|
|
471
|
-
>
|
|
472
|
-
<AssetCardThumbnail asset={asset} />
|
|
473
|
-
<span className="vgai-asset-card__name" data-testid="online-asset-name">
|
|
474
|
-
{asset.name}
|
|
475
|
-
</span>
|
|
476
|
-
<span className="vgai-asset-card__meta">
|
|
477
|
-
<span>{asset.type}</span>
|
|
478
|
-
<span>·</span>
|
|
479
|
-
<span>{asset.source === 'local' ? 'Catalog' : asset.source}</span>
|
|
480
|
-
</span>
|
|
481
|
-
<span className="vgai-asset-card__badges">
|
|
482
|
-
{downloaded && <span className="vgai-asset-badge">In project</span>}
|
|
483
|
-
{!downloaded && asset.cloudHosted && (
|
|
484
|
-
<span className="vgai-asset-badge">
|
|
485
|
-
<EditorIcon icon={faCloud} /> Cloud
|
|
486
|
-
</span>
|
|
487
|
-
)}
|
|
488
|
-
{(asset.variantCount ?? 0) > 1 && (
|
|
489
|
-
<span className="vgai-asset-badge">{asset.variantCount} variants</span>
|
|
490
|
-
)}
|
|
491
|
-
{props.isFavorite(asset) && <span className="vgai-asset-badge">Favorite</span>}
|
|
492
|
-
</span>
|
|
493
|
-
</Button>
|
|
494
|
-
);
|
|
495
|
-
})}
|
|
496
|
-
{props.loading && (
|
|
497
|
-
<div role="status" style={{ position: 'absolute', left: 16, top: rows * rowHeight + 12 }}>
|
|
498
|
-
<EditorIcon icon={faSpinner} spin /> Loading assets…
|
|
499
|
-
</div>
|
|
500
|
-
)}
|
|
501
|
-
</div>
|
|
502
|
-
</div>
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
interface OnlineAssetBrowserProps {
|
|
507
|
-
layout?: 'workspace' | 'navigator';
|
|
508
|
-
}
|
|
509
|
-
|
|
510
320
|
function VirtualAssetList(props: {
|
|
511
321
|
assets: readonly OnlineAsset[];
|
|
512
322
|
selectedId: string | null;
|
|
@@ -777,7 +587,7 @@ function NavigatorAssetLibrary(props: {
|
|
|
777
587
|
);
|
|
778
588
|
}
|
|
779
589
|
|
|
780
|
-
export function OnlineAssetBrowser(
|
|
590
|
+
export function OnlineAssetBrowser() {
|
|
781
591
|
const store = useEditorStore();
|
|
782
592
|
const initial = useMemo(readState, []);
|
|
783
593
|
const [source, setSource] = useState<Source>(initial.source);
|
|
@@ -785,15 +595,10 @@ export function OnlineAssetBrowser({ layout = 'workspace' }: OnlineAssetBrowserP
|
|
|
785
595
|
const [category, setCategory] = useState('');
|
|
786
596
|
const [categoryQuery, setCategoryQuery] = useState('');
|
|
787
597
|
const [searchQuery, setSearchQuery] = useState(initial.query);
|
|
788
|
-
const [density, setDensity] = useState<Density>(initial.density);
|
|
789
598
|
const [sort, setSort] = useState<Sort>(initial.sort);
|
|
790
|
-
const [favorites
|
|
599
|
+
const [favorites] = useState(new Set(initial.favorites));
|
|
791
600
|
const [savedSearches, setSavedSearches] = useState(initial.savedSearches);
|
|
792
601
|
const [recent, setRecent] = useState(initial.recent);
|
|
793
|
-
const [sourceWidth, setSourceWidth] = useState(initial.sourceWidth);
|
|
794
|
-
const [detailsWidth, setDetailsWidth] = useState(initial.detailsWidth);
|
|
795
|
-
const [sourceCollapsed, setSourceCollapsed] = useState(initial.sourceCollapsed);
|
|
796
|
-
const [detailsCollapsed, setDetailsCollapsed] = useState(initial.detailsCollapsed);
|
|
797
602
|
const [navigatorFiltersOpen, setNavigatorFiltersOpen] = useState(false);
|
|
798
603
|
const [favoritesOnly, setFavoritesOnly] = useState(false);
|
|
799
604
|
const [runtimeReadyOnly, setRuntimeReadyOnly] = useState(false);
|
|
@@ -819,37 +624,30 @@ export function OnlineAssetBrowser({ layout = 'workspace' }: OnlineAssetBrowserP
|
|
|
819
624
|
source,
|
|
820
625
|
assetType,
|
|
821
626
|
query: searchQuery,
|
|
822
|
-
density,
|
|
823
627
|
sort,
|
|
824
628
|
favorites: [...favorites],
|
|
825
629
|
savedSearches,
|
|
826
630
|
recent,
|
|
827
|
-
sourceWidth,
|
|
828
|
-
detailsWidth,
|
|
829
|
-
sourceCollapsed,
|
|
830
|
-
detailsCollapsed,
|
|
831
631
|
} satisfies PersistedLibraryState),
|
|
832
632
|
);
|
|
833
|
-
}, [
|
|
834
|
-
assetType,
|
|
835
|
-
density,
|
|
836
|
-
detailsCollapsed,
|
|
837
|
-
detailsWidth,
|
|
838
|
-
favorites,
|
|
839
|
-
recent,
|
|
840
|
-
savedSearches,
|
|
841
|
-
searchQuery,
|
|
842
|
-
sort,
|
|
843
|
-
source,
|
|
844
|
-
sourceCollapsed,
|
|
845
|
-
sourceWidth,
|
|
846
|
-
]);
|
|
633
|
+
}, [assetType, favorites, recent, savedSearches, searchQuery, sort, source]);
|
|
847
634
|
|
|
848
635
|
useEffect(() => {
|
|
849
636
|
loadDownloadedAssets();
|
|
850
637
|
return onAssetDownloaded(() => setAssets((current) => [...current]));
|
|
851
638
|
}, []);
|
|
852
639
|
|
|
640
|
+
useEffect(() => {
|
|
641
|
+
const openSelected = (event: Event) => {
|
|
642
|
+
openOnlineAssetDocument(
|
|
643
|
+
store,
|
|
644
|
+
(event as CustomEvent<ReturnType<typeof toOnlineInfo>>).detail,
|
|
645
|
+
);
|
|
646
|
+
};
|
|
647
|
+
window.addEventListener('editor:open-online-asset', openSelected);
|
|
648
|
+
return () => window.removeEventListener('editor:open-online-asset', openSelected);
|
|
649
|
+
}, [store]);
|
|
650
|
+
|
|
853
651
|
useEffect(() => {
|
|
854
652
|
if (source === 'polyhaven' || source === 'local') {
|
|
855
653
|
getAssetLibraryCategories(assetType, source)
|
|
@@ -892,6 +690,7 @@ export function OnlineAssetBrowser({ layout = 'workspace' }: OnlineAssetBrowserP
|
|
|
892
690
|
const timer = window.setTimeout(() => {
|
|
893
691
|
setOffset(0);
|
|
894
692
|
setSelectedId(null);
|
|
693
|
+
if (getSelectedAsset()?.origin === 'library') clearSelectedAsset();
|
|
895
694
|
void doSearch(0, false);
|
|
896
695
|
}, 250);
|
|
897
696
|
return () => window.clearTimeout(timer);
|
|
@@ -951,9 +750,23 @@ export function OnlineAssetBrowser({ layout = 'workspace' }: OnlineAssetBrowserP
|
|
|
951
750
|
health: count(assets.map((asset) => asset.capabilities?.health ?? 'unsupported')),
|
|
952
751
|
};
|
|
953
752
|
}, [assets]);
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
753
|
+
const onSelect = (asset: OnlineAsset) => {
|
|
754
|
+
setSelectedId(asset.id);
|
|
755
|
+
const localPath = getDownloadedAssetPath(asset.source, asset.id);
|
|
756
|
+
const online = toOnlineInfo(asset);
|
|
757
|
+
setSelectedAsset({
|
|
758
|
+
path: localPath ? `/${localPath.replace(/^\/+/, '')}` : `online:${asset.source}:${asset.id}`,
|
|
759
|
+
name: asset.name,
|
|
760
|
+
kind:
|
|
761
|
+
asset.type === 'model'
|
|
762
|
+
? 'model'
|
|
763
|
+
: asset.type === 'animation' || asset.type === 'source'
|
|
764
|
+
? 'source'
|
|
765
|
+
: 'image',
|
|
766
|
+
origin: 'library',
|
|
767
|
+
online,
|
|
768
|
+
});
|
|
769
|
+
};
|
|
957
770
|
const onOpen = (asset: OnlineAsset) => {
|
|
958
771
|
openOnlineAssetDocument(store, toOnlineInfo(asset));
|
|
959
772
|
const key = onlineAssetKey(asset);
|
|
@@ -964,15 +777,6 @@ export function OnlineAssetBrowser({ layout = 'workspace' }: OnlineAssetBrowserP
|
|
|
964
777
|
setAssetType(SOURCES.find((entry) => entry.value === nextSource)!.types[0]!.value);
|
|
965
778
|
setCategory('');
|
|
966
779
|
};
|
|
967
|
-
const toggleFavorite = (asset: OnlineAsset) => {
|
|
968
|
-
const key = onlineAssetKey(asset);
|
|
969
|
-
setFavorites((current) => {
|
|
970
|
-
const next = new Set(current);
|
|
971
|
-
if (next.has(key)) next.delete(key);
|
|
972
|
-
else next.add(key);
|
|
973
|
-
return next;
|
|
974
|
-
});
|
|
975
|
-
};
|
|
976
780
|
const sourcePane = (
|
|
977
781
|
<SourcePane
|
|
978
782
|
source={source}
|
|
@@ -1043,178 +847,5 @@ export function OnlineAssetBrowser({ layout = 'workspace' }: OnlineAssetBrowserP
|
|
|
1043
847
|
/>
|
|
1044
848
|
);
|
|
1045
849
|
|
|
1046
|
-
|
|
1047
|
-
<div
|
|
1048
|
-
className="vgai-asset-library"
|
|
1049
|
-
data-density={density}
|
|
1050
|
-
data-source-collapsed={sourceCollapsed}
|
|
1051
|
-
data-details-collapsed={detailsCollapsed}
|
|
1052
|
-
style={
|
|
1053
|
-
{
|
|
1054
|
-
'--asset-source-width': `${sourceCollapsed ? 0 : sourceWidth}px`,
|
|
1055
|
-
'--asset-details-width': `${detailsCollapsed ? 0 : detailsWidth}px`,
|
|
1056
|
-
} as React.CSSProperties
|
|
1057
|
-
}
|
|
1058
|
-
>
|
|
1059
|
-
{sourcePane}
|
|
1060
|
-
<main className="vgai-asset-library__results">
|
|
1061
|
-
<div className="vgai-asset-library__toolbar">
|
|
1062
|
-
<Button
|
|
1063
|
-
className="vgai-asset-library__filters-toggle"
|
|
1064
|
-
variant="ghost"
|
|
1065
|
-
size="comfortable"
|
|
1066
|
-
onClick={() => setSourceCollapsed((value) => !value)}
|
|
1067
|
-
>
|
|
1068
|
-
{sourceCollapsed ? 'Show filters' : 'Hide filters'}
|
|
1069
|
-
</Button>
|
|
1070
|
-
<TextInput
|
|
1071
|
-
className="vgai-input"
|
|
1072
|
-
aria-label="Search asset library"
|
|
1073
|
-
placeholder="Search models, packs, authors, clips…"
|
|
1074
|
-
value={searchQuery}
|
|
1075
|
-
onChange={(event) => setSearchQuery(event.target.value)}
|
|
1076
|
-
style={{ flex: '1 1 220px' }}
|
|
1077
|
-
/>
|
|
1078
|
-
<Select
|
|
1079
|
-
className="vgai-select vgai-asset-library__sort"
|
|
1080
|
-
aria-label="Sort assets"
|
|
1081
|
-
value={sort}
|
|
1082
|
-
onChange={(event) => setSort(event.target.value as Sort)}
|
|
1083
|
-
>
|
|
1084
|
-
<option value="relevance">Relevance</option>
|
|
1085
|
-
<option value="name">Name</option>
|
|
1086
|
-
<option value="source">Source</option>
|
|
1087
|
-
<option value="size">Size</option>
|
|
1088
|
-
<option value="recently-added">Recently added</option>
|
|
1089
|
-
<option value="recently-used">Recently used</option>
|
|
1090
|
-
</Select>
|
|
1091
|
-
<Select
|
|
1092
|
-
className="vgai-select vgai-asset-library__density"
|
|
1093
|
-
aria-label="Card density"
|
|
1094
|
-
value={density}
|
|
1095
|
-
onChange={(event) => setDensity(event.target.value as Density)}
|
|
1096
|
-
>
|
|
1097
|
-
<option value="compact">Compact</option>
|
|
1098
|
-
<option value="standard">Standard</option>
|
|
1099
|
-
<option value="large">Large</option>
|
|
1100
|
-
</Select>
|
|
1101
|
-
<Button
|
|
1102
|
-
className="vgai-asset-library__save-search"
|
|
1103
|
-
variant="ghost"
|
|
1104
|
-
size="comfortable"
|
|
1105
|
-
disabled={!searchQuery || savedSearches.includes(searchQuery)}
|
|
1106
|
-
onClick={() => setSavedSearches((current) => [...current, searchQuery])}
|
|
1107
|
-
>
|
|
1108
|
-
Save search
|
|
1109
|
-
</Button>
|
|
1110
|
-
<Button
|
|
1111
|
-
className="vgai-asset-library__details-toggle"
|
|
1112
|
-
variant="ghost"
|
|
1113
|
-
size="comfortable"
|
|
1114
|
-
onClick={() => setDetailsCollapsed((value) => !value)}
|
|
1115
|
-
>
|
|
1116
|
-
{detailsCollapsed ? 'Show details' : 'Hide details'}
|
|
1117
|
-
</Button>
|
|
1118
|
-
<span
|
|
1119
|
-
className="vgai-asset-library__status"
|
|
1120
|
-
role="status"
|
|
1121
|
-
aria-live="polite"
|
|
1122
|
-
style={{ fontSize: 11, color: text[3] }}
|
|
1123
|
-
>
|
|
1124
|
-
{filteredAssets.length} loaded · {total.toLocaleString()} total
|
|
1125
|
-
</span>
|
|
1126
|
-
</div>
|
|
1127
|
-
{error ? (
|
|
1128
|
-
<div role="alert" style={{ padding: 32, color: text[2], textAlign: 'center' }}>
|
|
1129
|
-
<div>{error}</div>
|
|
1130
|
-
<Button
|
|
1131
|
-
variant="ghost"
|
|
1132
|
-
size="comfortable"
|
|
1133
|
-
onClick={() => {
|
|
1134
|
-
setOffset(0);
|
|
1135
|
-
void doSearch(0, false);
|
|
1136
|
-
}}
|
|
1137
|
-
>
|
|
1138
|
-
Retry
|
|
1139
|
-
</Button>
|
|
1140
|
-
</div>
|
|
1141
|
-
) : filteredAssets.length === 0 && !loading ? (
|
|
1142
|
-
<div role="status" style={{ padding: 32, color: text[3], textAlign: 'center' }}>
|
|
1143
|
-
No assets match the current search and filters.
|
|
1144
|
-
</div>
|
|
1145
|
-
) : (
|
|
1146
|
-
<VirtualAssetGrid
|
|
1147
|
-
assets={filteredAssets}
|
|
1148
|
-
selectedId={selectedId}
|
|
1149
|
-
density={density}
|
|
1150
|
-
loading={loading}
|
|
1151
|
-
hasMore={assets.length < total}
|
|
1152
|
-
isFavorite={(asset) => favorites.has(onlineAssetKey(asset))}
|
|
1153
|
-
onSelect={onSelect}
|
|
1154
|
-
onOpen={onOpen}
|
|
1155
|
-
onLoadMore={loadMore}
|
|
1156
|
-
/>
|
|
1157
|
-
)}
|
|
1158
|
-
</main>
|
|
1159
|
-
<aside className="vgai-asset-library__details" aria-label="Selected asset details">
|
|
1160
|
-
<div className="vgai-asset-pane-size">
|
|
1161
|
-
<label>
|
|
1162
|
-
Detail width
|
|
1163
|
-
<TextInput
|
|
1164
|
-
aria-label="Detail pane width"
|
|
1165
|
-
type="range"
|
|
1166
|
-
min="280"
|
|
1167
|
-
max="640"
|
|
1168
|
-
value={detailsWidth}
|
|
1169
|
-
onChange={(event) => setDetailsWidth(Number(event.target.value))}
|
|
1170
|
-
/>
|
|
1171
|
-
</label>
|
|
1172
|
-
</div>
|
|
1173
|
-
{selected ? (
|
|
1174
|
-
<>
|
|
1175
|
-
<div
|
|
1176
|
-
style={{
|
|
1177
|
-
display: 'flex',
|
|
1178
|
-
gap: 8,
|
|
1179
|
-
padding: '8px 10px',
|
|
1180
|
-
borderBottom: '1px solid var(--vgai-border-1)',
|
|
1181
|
-
}}
|
|
1182
|
-
>
|
|
1183
|
-
<Button variant="solid" size="comfortable" onClick={() => onOpen(selected)}>
|
|
1184
|
-
Open in Asset Editor
|
|
1185
|
-
</Button>
|
|
1186
|
-
<Button variant="ghost" size="comfortable" onClick={() => toggleFavorite(selected)}>
|
|
1187
|
-
{favorites.has(onlineAssetKey(selected)) ? 'Remove favorite' : 'Add favorite'}
|
|
1188
|
-
</Button>
|
|
1189
|
-
{getDownloadedAssetPath(selected.source, selected.id) && (
|
|
1190
|
-
<span className="vgai-asset-badge">
|
|
1191
|
-
<EditorIcon icon={faCheck} /> In project
|
|
1192
|
-
</span>
|
|
1193
|
-
)}
|
|
1194
|
-
</div>
|
|
1195
|
-
<OnlineAssetDetail
|
|
1196
|
-
online={toOnlineInfo(selected)}
|
|
1197
|
-
onResolved={() => setAssets((current) => [...current])}
|
|
1198
|
-
/>
|
|
1199
|
-
</>
|
|
1200
|
-
) : (
|
|
1201
|
-
<div role="status" style={{ padding: 24, color: text[3] }}>
|
|
1202
|
-
Select an asset to inspect its preview, variants, license, and project status.
|
|
1203
|
-
</div>
|
|
1204
|
-
)}
|
|
1205
|
-
</aside>
|
|
1206
|
-
{!sourceCollapsed && (
|
|
1207
|
-
<TextInput
|
|
1208
|
-
className="vgai-asset-source-size"
|
|
1209
|
-
aria-label="Source pane width"
|
|
1210
|
-
type="range"
|
|
1211
|
-
min="176"
|
|
1212
|
-
max="360"
|
|
1213
|
-
value={sourceWidth}
|
|
1214
|
-
onChange={(event) => setSourceWidth(Number(event.target.value))}
|
|
1215
|
-
/>
|
|
1216
|
-
)}
|
|
1217
|
-
</div>
|
|
1218
|
-
);
|
|
1219
|
-
return { navigator: navigatorLayout, workspace: workspaceLayout }[layout];
|
|
850
|
+
return navigatorLayout;
|
|
1220
851
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type DockviewApi,
|
|
3
3
|
DockviewDefaultTab,
|
|
4
|
-
type DockviewPanelApi,
|
|
5
4
|
DockviewReact,
|
|
6
5
|
type DockviewTheme,
|
|
7
6
|
type IDockviewPanelHeaderProps,
|
|
@@ -116,16 +115,6 @@ export const vgaiDockviewTheme: DockviewTheme = {
|
|
|
116
115
|
tabAnimation: 'smooth',
|
|
117
116
|
};
|
|
118
117
|
|
|
119
|
-
function usePanelActive(api: DockviewPanelApi): boolean {
|
|
120
|
-
const [active, setActive] = useState(api.isActive);
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
setActive(api.isActive);
|
|
123
|
-
const disposable = api.onDidActiveChange((event) => setActive(event.isActive));
|
|
124
|
-
return () => disposable.dispose();
|
|
125
|
-
}, [api]);
|
|
126
|
-
return active;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
118
|
function StaticPanelContent({ staticKind }: { staticKind: StaticPanelKind }) {
|
|
130
119
|
const onOpenScene = useContext(OpenSceneContext);
|
|
131
120
|
switch (staticKind) {
|
|
@@ -145,7 +134,6 @@ function StaticPanelContent({ staticKind }: { staticKind: StaticPanelKind }) {
|
|
|
145
134
|
}
|
|
146
135
|
|
|
147
136
|
function WorkspacePanel({ api, params }: IDockviewPanelProps<WorkspacePanelParams>) {
|
|
148
|
-
const active = usePanelActive(api);
|
|
149
137
|
useSyncExternalStore(
|
|
150
138
|
subscribeWorkspaceDocuments,
|
|
151
139
|
workspaceDocumentRegistryVersion,
|
|
@@ -192,6 +180,12 @@ function WorkspacePanel({ api, params }: IDockviewPanelProps<WorkspacePanelParam
|
|
|
192
180
|
if (!document) return <div className="vgai-dock-empty">Restoring document…</div>;
|
|
193
181
|
const Content = document.descriptor.Content;
|
|
194
182
|
const Toolbar = document.descriptor.Toolbar;
|
|
183
|
+
// A bottom utility can own keyboard focus while the center document stays
|
|
184
|
+
// visibly selected in its own group. Dockview's `api.isActive` is global
|
|
185
|
+
// focus, not document activation; using it here suspended previews and
|
|
186
|
+
// cleared their Inspector/Hierarchy context whenever Console or Controls
|
|
187
|
+
// opened. The document registry is the semantic center-tab authority.
|
|
188
|
+
const active = activeWorkspaceDocumentId() === params.documentId;
|
|
195
189
|
return (
|
|
196
190
|
<div
|
|
197
191
|
className="vgai-dock-document"
|