@vgai/editor 0.4.0-canary.20260715.0 → 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-C7YBtOuM.js → axe-BJjvBeOZ.js} +1 -1
- package/dist/assets/{browserAll-DDtGGE48.js → browserAll-Dn0pgD87.js} +1 -1
- package/dist/assets/{create-runtime-DbTlBlKG.js → create-runtime-BF9OD-U8.js} +1 -1
- package/dist/assets/{game-Dch9sAPc.js → game-B346oj_f.js} +1 -1
- package/dist/assets/{game-BE0r7DMI.js → game-BCNN63uV.js} +1 -1
- package/dist/assets/{game-SAFn90pA.js → game-BR0GBNgF.js} +1 -1
- package/dist/assets/{game-Cqzj_lpD.js → game-CLNm2g0y.js} +1 -1
- package/dist/assets/{game-CcHfYxjP.js → game-DQxhCYsZ.js} +1 -1
- package/dist/assets/{game-BdTHR2_X.js → game-DeTGdgYt.js} +1 -1
- package/dist/assets/{game-loop-DMIgSMV-.js → game-loop-t5W_oqxz.js} +1 -1
- package/dist/assets/{game-source-Dk83w26t.js → game-source-DzE9taML.js} +1 -1
- package/dist/assets/{game-DSZdeCHu.js → game-u6c2Patu.js} +1 -1
- package/dist/assets/{game-C3iQGSqo.js → game-umrHIqFB.js} +1 -1
- package/dist/assets/{index-C-LhOTBN.js → index-CgcH1RpF.js} +1 -1
- package/dist/assets/{index-CDkxceqE.js → index-D3tcNB2T.js} +1 -1
- package/dist/assets/{index-CQEfi217.js → index-DbQzGLxv.js} +771 -771
- package/dist/assets/{index-4idIsHHJ.js → index-Fc8Ezr20.js} +1 -1
- package/dist/assets/index-bAGhEnnO.css +1 -0
- package/dist/assets/{ingest-siblings-s0P5I1Ub.js → ingest-siblings-CW_xbvrF.js} +1 -1
- package/dist/assets/{mount-manifest-DXMOlF79.js → mount-manifest-DgUscyaD.js} +1 -1
- package/dist/assets/{react-18-C9eHn8x1.js → react-18-C4tVCii_.js} +1 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +1 -0
- package/dist/assets/{world-documents-GM0a9Kv7.js → world-documents-BSaA5YIF.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +21 -1
- package/package.json +3 -3
- package/server/packaged.ts +20 -0
- 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 +330 -539
- package/src/components/AssetEditorShell.tsx +42 -112
- package/src/components/AssetImportDetails.tsx +24 -6
- package/src/components/GameHierarchy.tsx +26 -1
- package/src/components/Inspector.tsx +38 -2
- package/src/components/OnlineAssetBrowser.tsx +295 -343
- 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 +197 -264
- package/src/editor-api.ts +1 -1
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-Dvi-V0NE.css +0 -1
- package/dist/assets/webworkerAll-Fn0e-ouF.js +0 -1
|
@@ -1,12 +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 { Button, Checkbox, EditorIcon, Select,
|
|
10
|
+
import { Button, Checkbox, EditorIcon, Select, StateSurface, TextInput } from '../design-system';
|
|
10
11
|
import { useEditorStore } from '../EditorContext';
|
|
11
12
|
import {
|
|
12
13
|
getAssetLibraryCategories,
|
|
@@ -18,11 +19,9 @@ import {
|
|
|
18
19
|
} from '../editor-api';
|
|
19
20
|
import { getCurrentProject } from '../project-manager';
|
|
20
21
|
import { openOnlineAssetDocument } from './asset-documents';
|
|
21
|
-
import { OnlineAssetDetail } from './asset-viewers/OnlineAssetDetail';
|
|
22
22
|
import './asset-workflow.css';
|
|
23
23
|
|
|
24
24
|
type Source = 'local' | 'polyhaven' | 'ambientcg';
|
|
25
|
-
type Density = 'compact' | 'standard' | 'large';
|
|
26
25
|
type Sort = AssetLibrarySort;
|
|
27
26
|
|
|
28
27
|
interface SourceOption {
|
|
@@ -66,30 +65,20 @@ interface PersistedLibraryState {
|
|
|
66
65
|
source: Source;
|
|
67
66
|
assetType: string;
|
|
68
67
|
query: string;
|
|
69
|
-
density: Density;
|
|
70
68
|
sort: Sort;
|
|
71
69
|
favorites: string[];
|
|
72
70
|
savedSearches: string[];
|
|
73
71
|
recent: string[];
|
|
74
|
-
sourceWidth: number;
|
|
75
|
-
detailsWidth: number;
|
|
76
|
-
sourceCollapsed: boolean;
|
|
77
|
-
detailsCollapsed: boolean;
|
|
78
72
|
}
|
|
79
73
|
|
|
80
74
|
const DEFAULT_STATE: PersistedLibraryState = {
|
|
81
75
|
source: 'local',
|
|
82
76
|
assetType: 'all',
|
|
83
77
|
query: '',
|
|
84
|
-
density: 'standard',
|
|
85
78
|
sort: 'relevance',
|
|
86
79
|
favorites: [],
|
|
87
80
|
savedSearches: [],
|
|
88
81
|
recent: [],
|
|
89
|
-
sourceWidth: 210,
|
|
90
|
-
detailsWidth: 360,
|
|
91
|
-
sourceCollapsed: false,
|
|
92
|
-
detailsCollapsed: false,
|
|
93
82
|
};
|
|
94
83
|
|
|
95
84
|
function readState(): PersistedLibraryState {
|
|
@@ -153,7 +142,10 @@ function SourcePane(props: {
|
|
|
153
142
|
.filter((value) => value.toLowerCase().includes(props.categoryQuery.toLowerCase()))
|
|
154
143
|
.slice(0, 80);
|
|
155
144
|
return (
|
|
156
|
-
<aside
|
|
145
|
+
<aside
|
|
146
|
+
className="vgai-asset-library-navigator__source-filters"
|
|
147
|
+
aria-label="Library sources and filters"
|
|
148
|
+
>
|
|
157
149
|
<div className="vgai-asset-facet-title">Sources</div>
|
|
158
150
|
{SOURCES.map((entry) => (
|
|
159
151
|
<Button
|
|
@@ -325,79 +317,55 @@ function AssetCardThumbnail({ asset }: { asset: OnlineAsset }) {
|
|
|
325
317
|
);
|
|
326
318
|
}
|
|
327
319
|
|
|
328
|
-
function
|
|
320
|
+
function VirtualAssetList(props: {
|
|
329
321
|
assets: readonly OnlineAsset[];
|
|
330
322
|
selectedId: string | null;
|
|
331
|
-
density: Density;
|
|
332
323
|
loading: boolean;
|
|
333
324
|
hasMore: boolean;
|
|
334
|
-
isFavorite(asset: OnlineAsset): boolean;
|
|
335
325
|
onSelect(asset: OnlineAsset): void;
|
|
336
326
|
onOpen(asset: OnlineAsset): void;
|
|
337
327
|
onLoadMore(): void;
|
|
338
328
|
}) {
|
|
339
329
|
const viewportRef = useRef<HTMLDivElement>(null);
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const firstRow = Math.max(0, Math.floor(metrics.scrollTop / rowHeight) - 2);
|
|
348
|
-
const lastRow = Math.min(rows, Math.ceil((metrics.scrollTop + metrics.height) / rowHeight) + 3);
|
|
349
|
-
const firstIndex = firstRow * columns;
|
|
350
|
-
const lastIndex = Math.min(props.assets.length, lastRow * columns);
|
|
330
|
+
const [metrics, setMetrics] = useState({ height: 320, scrollTop: 0 });
|
|
331
|
+
const rowHeight = 52;
|
|
332
|
+
const firstIndex = Math.max(0, Math.floor(metrics.scrollTop / rowHeight) - 3);
|
|
333
|
+
const lastIndex = Math.min(
|
|
334
|
+
props.assets.length,
|
|
335
|
+
Math.ceil((metrics.scrollTop + metrics.height) / rowHeight) + 4,
|
|
336
|
+
);
|
|
351
337
|
const visible = props.assets.slice(firstIndex, lastIndex);
|
|
338
|
+
const selectedIndex = props.assets.findIndex((asset) => asset.id === props.selectedId);
|
|
352
339
|
|
|
353
340
|
useEffect(() => {
|
|
354
341
|
const element = viewportRef.current;
|
|
355
342
|
if (!element || typeof ResizeObserver === 'undefined') return;
|
|
356
343
|
const observer = new ResizeObserver(([entry]) => {
|
|
357
|
-
if (
|
|
358
|
-
setMetrics((current) => ({
|
|
359
|
-
...current,
|
|
360
|
-
width: entry.contentRect.width,
|
|
361
|
-
height: entry.contentRect.height,
|
|
362
|
-
}));
|
|
344
|
+
if (entry) setMetrics((current) => ({ ...current, height: entry.contentRect.height }));
|
|
363
345
|
});
|
|
364
346
|
observer.observe(element);
|
|
365
347
|
return () => observer.disconnect();
|
|
366
348
|
}, []);
|
|
367
349
|
|
|
368
|
-
useEffect(() => () => window.clearTimeout(typeahead.current.timer), []);
|
|
369
|
-
|
|
370
350
|
const { hasMore, loading, onLoadMore } = props;
|
|
371
351
|
useEffect(() => {
|
|
372
|
-
if (hasMore && !loading &&
|
|
373
|
-
}, [hasMore,
|
|
352
|
+
if (hasMore && !loading && lastIndex >= props.assets.length - 4) onLoadMore();
|
|
353
|
+
}, [hasMore, lastIndex, loading, onLoadMore, props.assets.length]);
|
|
374
354
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
const asset = props.assets[
|
|
355
|
+
const moveSelection = (index: number) => {
|
|
356
|
+
const nextIndex = Math.max(0, Math.min(props.assets.length - 1, index));
|
|
357
|
+
const asset = props.assets[nextIndex];
|
|
378
358
|
if (!asset) return;
|
|
379
359
|
props.onSelect(asset);
|
|
380
|
-
|
|
381
|
-
if (viewportRef.current) viewportRef.current.scrollTop = row * rowHeight;
|
|
360
|
+
if (viewportRef.current) viewportRef.current.scrollTop = nextIndex * rowHeight;
|
|
382
361
|
};
|
|
383
|
-
const
|
|
384
|
-
const pageSize = Math.max(columns, Math.floor(metrics.height / rowHeight) * columns);
|
|
362
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
385
363
|
switch (event.key) {
|
|
386
|
-
case 'Enter':
|
|
387
|
-
if (selectedIndex >= 0) props.onOpen(props.assets[selectedIndex]!);
|
|
388
|
-
else return;
|
|
389
|
-
break;
|
|
390
|
-
case 'ArrowRight':
|
|
391
|
-
moveSelection(selectedIndex + 1);
|
|
392
|
-
break;
|
|
393
|
-
case 'ArrowLeft':
|
|
394
|
-
moveSelection(selectedIndex - 1);
|
|
395
|
-
break;
|
|
396
364
|
case 'ArrowDown':
|
|
397
|
-
moveSelection(selectedIndex +
|
|
365
|
+
moveSelection(selectedIndex + 1);
|
|
398
366
|
break;
|
|
399
367
|
case 'ArrowUp':
|
|
400
|
-
moveSelection(selectedIndex -
|
|
368
|
+
moveSelection(selectedIndex < 0 ? 0 : selectedIndex - 1);
|
|
401
369
|
break;
|
|
402
370
|
case 'Home':
|
|
403
371
|
moveSelection(0);
|
|
@@ -406,24 +374,13 @@ function VirtualAssetGrid(props: {
|
|
|
406
374
|
moveSelection(props.assets.length - 1);
|
|
407
375
|
break;
|
|
408
376
|
case 'PageDown':
|
|
409
|
-
moveSelection(selectedIndex +
|
|
377
|
+
moveSelection(selectedIndex + Math.max(1, Math.floor(metrics.height / rowHeight)));
|
|
410
378
|
break;
|
|
411
379
|
case 'PageUp':
|
|
412
|
-
moveSelection(selectedIndex -
|
|
380
|
+
moveSelection(selectedIndex - Math.max(1, Math.floor(metrics.height / rowHeight)));
|
|
413
381
|
break;
|
|
414
|
-
default:
|
|
415
|
-
|
|
416
|
-
window.clearTimeout(typeahead.current.timer);
|
|
417
|
-
typeahead.current.value += event.key.toLowerCase();
|
|
418
|
-
typeahead.current.timer = window.setTimeout(() => {
|
|
419
|
-
typeahead.current.value = '';
|
|
420
|
-
}, 700);
|
|
421
|
-
const match = props.assets.findIndex((asset) =>
|
|
422
|
-
asset.name.toLowerCase().startsWith(typeahead.current.value),
|
|
423
|
-
);
|
|
424
|
-
if (match < 0) return;
|
|
425
|
-
moveSelection(match);
|
|
426
|
-
}
|
|
382
|
+
default:
|
|
383
|
+
return;
|
|
427
384
|
}
|
|
428
385
|
event.preventDefault();
|
|
429
386
|
};
|
|
@@ -431,24 +388,22 @@ function VirtualAssetGrid(props: {
|
|
|
431
388
|
return (
|
|
432
389
|
<div
|
|
433
390
|
ref={viewportRef}
|
|
434
|
-
className="vgai-asset-
|
|
391
|
+
className="vgai-asset-library-navigator__results"
|
|
435
392
|
role="listbox"
|
|
436
393
|
aria-label="Asset results"
|
|
437
394
|
aria-busy={props.loading}
|
|
438
395
|
tabIndex={0}
|
|
396
|
+
onKeyDown={onKeyDown}
|
|
439
397
|
onScroll={(event) =>
|
|
440
398
|
setMetrics((current) => ({ ...current, scrollTop: event.currentTarget.scrollTop }))
|
|
441
399
|
}
|
|
442
|
-
onKeyDown={onGridKeyDown}
|
|
443
400
|
>
|
|
444
401
|
<div
|
|
445
|
-
|
|
402
|
+
className="vgai-asset-library-navigator__result-space"
|
|
403
|
+
style={{ height: Math.max(metrics.height, props.assets.length * rowHeight) }}
|
|
446
404
|
>
|
|
447
405
|
{visible.map((asset, localIndex) => {
|
|
448
406
|
const index = firstIndex + localIndex;
|
|
449
|
-
const column = index % columns;
|
|
450
|
-
const row = Math.floor(index / columns);
|
|
451
|
-
const downloaded = getDownloadedAssetPath(asset.source, asset.id) != null;
|
|
452
407
|
return (
|
|
453
408
|
<Button
|
|
454
409
|
key={onlineAssetKey(asset)}
|
|
@@ -456,45 +411,182 @@ function VirtualAssetGrid(props: {
|
|
|
456
411
|
role="option"
|
|
457
412
|
aria-selected={props.selectedId === asset.id}
|
|
458
413
|
aria-label={`${asset.name}, ${asset.type}, ${asset.source}`}
|
|
459
|
-
className="vgai-asset-
|
|
460
|
-
style={{
|
|
414
|
+
className="vgai-asset-library-row"
|
|
415
|
+
style={{ top: index * rowHeight }}
|
|
461
416
|
onClick={() => props.onSelect(asset)}
|
|
462
417
|
onDoubleClick={() => props.onOpen(asset)}
|
|
418
|
+
onKeyDown={(event) => {
|
|
419
|
+
if (event.key !== 'Enter') return;
|
|
420
|
+
props.onOpen(asset);
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
}}
|
|
463
423
|
>
|
|
464
424
|
<AssetCardThumbnail asset={asset} />
|
|
465
|
-
<span className="vgai-asset-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<span
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
</span>
|
|
473
|
-
<span className="vgai-asset-card__badges">
|
|
474
|
-
{downloaded && <span className="vgai-asset-badge">In project</span>}
|
|
475
|
-
{!downloaded && asset.cloudHosted && (
|
|
476
|
-
<span className="vgai-asset-badge">
|
|
477
|
-
<EditorIcon icon={faCloud} /> Cloud
|
|
478
|
-
</span>
|
|
479
|
-
)}
|
|
480
|
-
{(asset.variantCount ?? 0) > 1 && (
|
|
481
|
-
<span className="vgai-asset-badge">{asset.variantCount} variants</span>
|
|
482
|
-
)}
|
|
483
|
-
{props.isFavorite(asset) && <span className="vgai-asset-badge">Favorite</span>}
|
|
425
|
+
<span className="vgai-asset-library-row__copy">
|
|
426
|
+
<span className="vgai-asset-library-row__name" data-testid="online-asset-name">
|
|
427
|
+
{asset.name}
|
|
428
|
+
</span>
|
|
429
|
+
<span className="vgai-asset-library-row__meta">
|
|
430
|
+
{asset.type} · {asset.source === 'local' ? 'Catalog' : asset.source}
|
|
431
|
+
</span>
|
|
484
432
|
</span>
|
|
485
433
|
</Button>
|
|
486
434
|
);
|
|
487
435
|
})}
|
|
488
436
|
{props.loading && (
|
|
489
|
-
<
|
|
437
|
+
<span className="vgai-asset-library-navigator__loading" role="status">
|
|
490
438
|
<EditorIcon icon={faSpinner} spin /> Loading assets…
|
|
491
|
-
</
|
|
439
|
+
</span>
|
|
492
440
|
)}
|
|
493
441
|
</div>
|
|
494
442
|
</div>
|
|
495
443
|
);
|
|
496
444
|
}
|
|
497
445
|
|
|
446
|
+
function NavigatorAssetLibrary(props: {
|
|
447
|
+
assets: readonly OnlineAsset[];
|
|
448
|
+
selectedId: string | null;
|
|
449
|
+
source: Source;
|
|
450
|
+
assetType: string;
|
|
451
|
+
sort: Sort;
|
|
452
|
+
searchQuery: string;
|
|
453
|
+
total: number;
|
|
454
|
+
loading: boolean;
|
|
455
|
+
error: string | null;
|
|
456
|
+
hasMore: boolean;
|
|
457
|
+
filtersOpen: boolean;
|
|
458
|
+
filters: React.ReactNode;
|
|
459
|
+
canSaveSearch: boolean;
|
|
460
|
+
onSource(source: Source): void;
|
|
461
|
+
onType(type: string): void;
|
|
462
|
+
onSort(sort: Sort): void;
|
|
463
|
+
onSearch(query: string): void;
|
|
464
|
+
onSelect(asset: OnlineAsset): void;
|
|
465
|
+
onOpen(asset: OnlineAsset): void;
|
|
466
|
+
onLoadMore(): void;
|
|
467
|
+
onRetry(): void;
|
|
468
|
+
onSaveSearch(): void;
|
|
469
|
+
onToggleFilters(): void;
|
|
470
|
+
}) {
|
|
471
|
+
const sourceConfig = SOURCES.find((entry) => entry.value === props.source)!;
|
|
472
|
+
return (
|
|
473
|
+
<div className="vgai-asset-library-navigator">
|
|
474
|
+
<div className="vgai-asset-library-navigator__controls">
|
|
475
|
+
<div className="vgai-asset-library-navigator__search-row">
|
|
476
|
+
<TextInput
|
|
477
|
+
className="vgai-input"
|
|
478
|
+
aria-label="Search asset library"
|
|
479
|
+
placeholder="Search Library"
|
|
480
|
+
value={props.searchQuery}
|
|
481
|
+
onChange={(event) => props.onSearch(event.target.value)}
|
|
482
|
+
/>
|
|
483
|
+
<Button
|
|
484
|
+
variant="ghost"
|
|
485
|
+
size="compact"
|
|
486
|
+
aria-expanded={props.filtersOpen}
|
|
487
|
+
onClick={props.onToggleFilters}
|
|
488
|
+
>
|
|
489
|
+
Filters
|
|
490
|
+
</Button>
|
|
491
|
+
</div>
|
|
492
|
+
<div className="vgai-asset-library-navigator__filters">
|
|
493
|
+
<Select
|
|
494
|
+
className="vgai-select"
|
|
495
|
+
aria-label="Library source"
|
|
496
|
+
value={props.source}
|
|
497
|
+
onChange={(event) => props.onSource(event.target.value as Source)}
|
|
498
|
+
>
|
|
499
|
+
{SOURCES.map((entry) => (
|
|
500
|
+
<option key={entry.value} value={entry.value}>
|
|
501
|
+
{entry.label}
|
|
502
|
+
</option>
|
|
503
|
+
))}
|
|
504
|
+
</Select>
|
|
505
|
+
<Select
|
|
506
|
+
className="vgai-select"
|
|
507
|
+
aria-label="Library asset type"
|
|
508
|
+
value={props.assetType}
|
|
509
|
+
onChange={(event) => props.onType(event.target.value)}
|
|
510
|
+
>
|
|
511
|
+
{sourceConfig.types.map((entry) => (
|
|
512
|
+
<option key={entry.value} value={entry.value}>
|
|
513
|
+
{entry.label}
|
|
514
|
+
</option>
|
|
515
|
+
))}
|
|
516
|
+
</Select>
|
|
517
|
+
<Select
|
|
518
|
+
className="vgai-select"
|
|
519
|
+
aria-label="Sort assets"
|
|
520
|
+
value={props.sort}
|
|
521
|
+
onChange={(event) => props.onSort(event.target.value as Sort)}
|
|
522
|
+
>
|
|
523
|
+
<option value="relevance">Relevant</option>
|
|
524
|
+
<option value="name">Name</option>
|
|
525
|
+
<option value="source">Source</option>
|
|
526
|
+
<option value="size">Size</option>
|
|
527
|
+
<option value="recently-added">Newest</option>
|
|
528
|
+
<option value="recently-used">Recent</option>
|
|
529
|
+
</Select>
|
|
530
|
+
</div>
|
|
531
|
+
<span className="vgai-asset-library-navigator__status" role="status" aria-live="polite">
|
|
532
|
+
{props.loading && props.assets.length === 0
|
|
533
|
+
? 'Loading…'
|
|
534
|
+
: `${props.assets.length.toLocaleString()} of ${props.total.toLocaleString()}`}
|
|
535
|
+
</span>
|
|
536
|
+
</div>
|
|
537
|
+
{props.filtersOpen && (
|
|
538
|
+
<div className="vgai-asset-library-navigator__filter-drawer">
|
|
539
|
+
<div className="vgai-asset-library-navigator__filter-heading">
|
|
540
|
+
<span>Filters and saved views</span>
|
|
541
|
+
<Button variant="ghost" size="compact" onClick={props.onToggleFilters}>
|
|
542
|
+
Done
|
|
543
|
+
</Button>
|
|
544
|
+
</div>
|
|
545
|
+
{props.canSaveSearch && (
|
|
546
|
+
<Button
|
|
547
|
+
className="vgai-asset-library-navigator__save-search"
|
|
548
|
+
variant="ghost"
|
|
549
|
+
size="compact"
|
|
550
|
+
onClick={props.onSaveSearch}
|
|
551
|
+
>
|
|
552
|
+
Save current search
|
|
553
|
+
</Button>
|
|
554
|
+
)}
|
|
555
|
+
{props.filters}
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
558
|
+
{props.error ? (
|
|
559
|
+
<StateSurface
|
|
560
|
+
compact
|
|
561
|
+
title="Library unavailable"
|
|
562
|
+
description={props.error}
|
|
563
|
+
action={
|
|
564
|
+
<Button variant="ghost" size="compact" onClick={props.onRetry}>
|
|
565
|
+
Retry
|
|
566
|
+
</Button>
|
|
567
|
+
}
|
|
568
|
+
/>
|
|
569
|
+
) : props.assets.length === 0 && !props.loading ? (
|
|
570
|
+
<StateSurface
|
|
571
|
+
compact
|
|
572
|
+
title="No matching assets"
|
|
573
|
+
description="Try another search or type."
|
|
574
|
+
/>
|
|
575
|
+
) : (
|
|
576
|
+
<VirtualAssetList
|
|
577
|
+
assets={props.assets}
|
|
578
|
+
selectedId={props.selectedId}
|
|
579
|
+
loading={props.loading}
|
|
580
|
+
hasMore={props.hasMore}
|
|
581
|
+
onSelect={props.onSelect}
|
|
582
|
+
onOpen={props.onOpen}
|
|
583
|
+
onLoadMore={props.onLoadMore}
|
|
584
|
+
/>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
498
590
|
export function OnlineAssetBrowser() {
|
|
499
591
|
const store = useEditorStore();
|
|
500
592
|
const initial = useMemo(readState, []);
|
|
@@ -503,15 +595,11 @@ export function OnlineAssetBrowser() {
|
|
|
503
595
|
const [category, setCategory] = useState('');
|
|
504
596
|
const [categoryQuery, setCategoryQuery] = useState('');
|
|
505
597
|
const [searchQuery, setSearchQuery] = useState(initial.query);
|
|
506
|
-
const [density, setDensity] = useState<Density>(initial.density);
|
|
507
598
|
const [sort, setSort] = useState<Sort>(initial.sort);
|
|
508
|
-
const [favorites
|
|
599
|
+
const [favorites] = useState(new Set(initial.favorites));
|
|
509
600
|
const [savedSearches, setSavedSearches] = useState(initial.savedSearches);
|
|
510
601
|
const [recent, setRecent] = useState(initial.recent);
|
|
511
|
-
const [
|
|
512
|
-
const [detailsWidth, setDetailsWidth] = useState(initial.detailsWidth);
|
|
513
|
-
const [sourceCollapsed, setSourceCollapsed] = useState(initial.sourceCollapsed);
|
|
514
|
-
const [detailsCollapsed, setDetailsCollapsed] = useState(initial.detailsCollapsed);
|
|
602
|
+
const [navigatorFiltersOpen, setNavigatorFiltersOpen] = useState(false);
|
|
515
603
|
const [favoritesOnly, setFavoritesOnly] = useState(false);
|
|
516
604
|
const [runtimeReadyOnly, setRuntimeReadyOnly] = useState(false);
|
|
517
605
|
const [animatedOnly, setAnimatedOnly] = useState(false);
|
|
@@ -536,37 +624,30 @@ export function OnlineAssetBrowser() {
|
|
|
536
624
|
source,
|
|
537
625
|
assetType,
|
|
538
626
|
query: searchQuery,
|
|
539
|
-
density,
|
|
540
627
|
sort,
|
|
541
628
|
favorites: [...favorites],
|
|
542
629
|
savedSearches,
|
|
543
630
|
recent,
|
|
544
|
-
sourceWidth,
|
|
545
|
-
detailsWidth,
|
|
546
|
-
sourceCollapsed,
|
|
547
|
-
detailsCollapsed,
|
|
548
631
|
} satisfies PersistedLibraryState),
|
|
549
632
|
);
|
|
550
|
-
}, [
|
|
551
|
-
assetType,
|
|
552
|
-
density,
|
|
553
|
-
detailsCollapsed,
|
|
554
|
-
detailsWidth,
|
|
555
|
-
favorites,
|
|
556
|
-
recent,
|
|
557
|
-
savedSearches,
|
|
558
|
-
searchQuery,
|
|
559
|
-
sort,
|
|
560
|
-
source,
|
|
561
|
-
sourceCollapsed,
|
|
562
|
-
sourceWidth,
|
|
563
|
-
]);
|
|
633
|
+
}, [assetType, favorites, recent, savedSearches, searchQuery, sort, source]);
|
|
564
634
|
|
|
565
635
|
useEffect(() => {
|
|
566
636
|
loadDownloadedAssets();
|
|
567
637
|
return onAssetDownloaded(() => setAssets((current) => [...current]));
|
|
568
638
|
}, []);
|
|
569
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
|
+
|
|
570
651
|
useEffect(() => {
|
|
571
652
|
if (source === 'polyhaven' || source === 'local') {
|
|
572
653
|
getAssetLibraryCategories(assetType, source)
|
|
@@ -609,6 +690,7 @@ export function OnlineAssetBrowser() {
|
|
|
609
690
|
const timer = window.setTimeout(() => {
|
|
610
691
|
setOffset(0);
|
|
611
692
|
setSelectedId(null);
|
|
693
|
+
if (getSelectedAsset()?.origin === 'library') clearSelectedAsset();
|
|
612
694
|
void doSearch(0, false);
|
|
613
695
|
}, 250);
|
|
614
696
|
return () => window.clearTimeout(timer);
|
|
@@ -668,9 +750,23 @@ export function OnlineAssetBrowser() {
|
|
|
668
750
|
health: count(assets.map((asset) => asset.capabilities?.health ?? 'unsupported')),
|
|
669
751
|
};
|
|
670
752
|
}, [assets]);
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
};
|
|
674
770
|
const onOpen = (asset: OnlineAsset) => {
|
|
675
771
|
openOnlineAssetDocument(store, toOnlineInfo(asset));
|
|
676
772
|
const key = onlineAssetKey(asset);
|
|
@@ -681,219 +777,75 @@ export function OnlineAssetBrowser() {
|
|
|
681
777
|
setAssetType(SOURCES.find((entry) => entry.value === nextSource)!.types[0]!.value);
|
|
682
778
|
setCategory('');
|
|
683
779
|
};
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
780
|
+
const sourcePane = (
|
|
781
|
+
<SourcePane
|
|
782
|
+
source={source}
|
|
783
|
+
assetType={assetType}
|
|
784
|
+
category={category}
|
|
785
|
+
categoryQuery={categoryQuery}
|
|
786
|
+
categories={categories}
|
|
787
|
+
savedSearches={savedSearches}
|
|
788
|
+
favoritesOnly={favoritesOnly}
|
|
789
|
+
animatedOnly={animatedOnly}
|
|
790
|
+
runtimeReadyOnly={runtimeReadyOnly}
|
|
791
|
+
downloaded={downloaded}
|
|
792
|
+
license={license}
|
|
793
|
+
delivery={delivery}
|
|
794
|
+
health={health}
|
|
795
|
+
licenseCounts={facetCounts.licenses}
|
|
796
|
+
deliveryCounts={facetCounts.deliveries}
|
|
797
|
+
healthCounts={facetCounts.health}
|
|
798
|
+
onSource={onSource}
|
|
799
|
+
onType={(value) => {
|
|
800
|
+
setAssetType(value);
|
|
801
|
+
setCategory('');
|
|
802
|
+
}}
|
|
803
|
+
onCategory={setCategory}
|
|
804
|
+
onCategoryQuery={setCategoryQuery}
|
|
805
|
+
onSavedSearch={setSearchQuery}
|
|
806
|
+
onFavoritesOnly={setFavoritesOnly}
|
|
807
|
+
onAnimatedOnly={setAnimatedOnly}
|
|
808
|
+
onRuntimeReadyOnly={setRuntimeReadyOnly}
|
|
809
|
+
onDownloaded={setDownloaded}
|
|
810
|
+
onLicense={setLicense}
|
|
811
|
+
onDelivery={setDelivery}
|
|
812
|
+
onHealth={setHealth}
|
|
813
|
+
/>
|
|
814
|
+
);
|
|
693
815
|
|
|
694
|
-
|
|
695
|
-
<
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
setAssetType(value);
|
|
727
|
-
setCategory('');
|
|
728
|
-
}}
|
|
729
|
-
onCategory={setCategory}
|
|
730
|
-
onCategoryQuery={setCategoryQuery}
|
|
731
|
-
onSavedSearch={setSearchQuery}
|
|
732
|
-
onFavoritesOnly={setFavoritesOnly}
|
|
733
|
-
onAnimatedOnly={setAnimatedOnly}
|
|
734
|
-
onRuntimeReadyOnly={setRuntimeReadyOnly}
|
|
735
|
-
onDownloaded={setDownloaded}
|
|
736
|
-
onLicense={setLicense}
|
|
737
|
-
onDelivery={setDelivery}
|
|
738
|
-
onHealth={setHealth}
|
|
739
|
-
/>
|
|
740
|
-
<main className="vgai-asset-library__results">
|
|
741
|
-
<div className="vgai-asset-library__toolbar">
|
|
742
|
-
<Button
|
|
743
|
-
className="vgai-asset-library__filters-toggle"
|
|
744
|
-
variant="ghost"
|
|
745
|
-
size="comfortable"
|
|
746
|
-
onClick={() => setSourceCollapsed((value) => !value)}
|
|
747
|
-
>
|
|
748
|
-
{sourceCollapsed ? 'Show filters' : 'Hide filters'}
|
|
749
|
-
</Button>
|
|
750
|
-
<TextInput
|
|
751
|
-
className="vgai-input"
|
|
752
|
-
aria-label="Search asset library"
|
|
753
|
-
placeholder="Search models, packs, authors, clips…"
|
|
754
|
-
value={searchQuery}
|
|
755
|
-
onChange={(event) => setSearchQuery(event.target.value)}
|
|
756
|
-
style={{ flex: '1 1 220px' }}
|
|
757
|
-
/>
|
|
758
|
-
<Select
|
|
759
|
-
className="vgai-select vgai-asset-library__sort"
|
|
760
|
-
aria-label="Sort assets"
|
|
761
|
-
value={sort}
|
|
762
|
-
onChange={(event) => setSort(event.target.value as Sort)}
|
|
763
|
-
>
|
|
764
|
-
<option value="relevance">Relevance</option>
|
|
765
|
-
<option value="name">Name</option>
|
|
766
|
-
<option value="source">Source</option>
|
|
767
|
-
<option value="size">Size</option>
|
|
768
|
-
<option value="recently-added">Recently added</option>
|
|
769
|
-
<option value="recently-used">Recently used</option>
|
|
770
|
-
</Select>
|
|
771
|
-
<Select
|
|
772
|
-
className="vgai-select vgai-asset-library__density"
|
|
773
|
-
aria-label="Card density"
|
|
774
|
-
value={density}
|
|
775
|
-
onChange={(event) => setDensity(event.target.value as Density)}
|
|
776
|
-
>
|
|
777
|
-
<option value="compact">Compact</option>
|
|
778
|
-
<option value="standard">Standard</option>
|
|
779
|
-
<option value="large">Large</option>
|
|
780
|
-
</Select>
|
|
781
|
-
<Button
|
|
782
|
-
className="vgai-asset-library__save-search"
|
|
783
|
-
variant="ghost"
|
|
784
|
-
size="comfortable"
|
|
785
|
-
disabled={!searchQuery || savedSearches.includes(searchQuery)}
|
|
786
|
-
onClick={() => setSavedSearches((current) => [...current, searchQuery])}
|
|
787
|
-
>
|
|
788
|
-
Save search
|
|
789
|
-
</Button>
|
|
790
|
-
<Button
|
|
791
|
-
className="vgai-asset-library__details-toggle"
|
|
792
|
-
variant="ghost"
|
|
793
|
-
size="comfortable"
|
|
794
|
-
onClick={() => setDetailsCollapsed((value) => !value)}
|
|
795
|
-
>
|
|
796
|
-
{detailsCollapsed ? 'Show details' : 'Hide details'}
|
|
797
|
-
</Button>
|
|
798
|
-
<span
|
|
799
|
-
className="vgai-asset-library__status"
|
|
800
|
-
role="status"
|
|
801
|
-
aria-live="polite"
|
|
802
|
-
style={{ fontSize: 11, color: text[3] }}
|
|
803
|
-
>
|
|
804
|
-
{filteredAssets.length} loaded · {total.toLocaleString()} total
|
|
805
|
-
</span>
|
|
806
|
-
</div>
|
|
807
|
-
{error ? (
|
|
808
|
-
<div role="alert" style={{ padding: 32, color: text[2], textAlign: 'center' }}>
|
|
809
|
-
<div>{error}</div>
|
|
810
|
-
<Button
|
|
811
|
-
variant="ghost"
|
|
812
|
-
size="comfortable"
|
|
813
|
-
onClick={() => {
|
|
814
|
-
setOffset(0);
|
|
815
|
-
void doSearch(0, false);
|
|
816
|
-
}}
|
|
817
|
-
>
|
|
818
|
-
Retry
|
|
819
|
-
</Button>
|
|
820
|
-
</div>
|
|
821
|
-
) : filteredAssets.length === 0 && !loading ? (
|
|
822
|
-
<div role="status" style={{ padding: 32, color: text[3], textAlign: 'center' }}>
|
|
823
|
-
No assets match the current search and filters.
|
|
824
|
-
</div>
|
|
825
|
-
) : (
|
|
826
|
-
<VirtualAssetGrid
|
|
827
|
-
assets={filteredAssets}
|
|
828
|
-
selectedId={selectedId}
|
|
829
|
-
density={density}
|
|
830
|
-
loading={loading}
|
|
831
|
-
hasMore={assets.length < total}
|
|
832
|
-
isFavorite={(asset) => favorites.has(onlineAssetKey(asset))}
|
|
833
|
-
onSelect={onSelect}
|
|
834
|
-
onOpen={onOpen}
|
|
835
|
-
onLoadMore={loadMore}
|
|
836
|
-
/>
|
|
837
|
-
)}
|
|
838
|
-
</main>
|
|
839
|
-
<aside className="vgai-asset-library__details" aria-label="Selected asset details">
|
|
840
|
-
<div className="vgai-asset-pane-size">
|
|
841
|
-
<label>
|
|
842
|
-
Detail width
|
|
843
|
-
<TextInput
|
|
844
|
-
aria-label="Detail pane width"
|
|
845
|
-
type="range"
|
|
846
|
-
min="280"
|
|
847
|
-
max="640"
|
|
848
|
-
value={detailsWidth}
|
|
849
|
-
onChange={(event) => setDetailsWidth(Number(event.target.value))}
|
|
850
|
-
/>
|
|
851
|
-
</label>
|
|
852
|
-
</div>
|
|
853
|
-
{selected ? (
|
|
854
|
-
<>
|
|
855
|
-
<div
|
|
856
|
-
style={{
|
|
857
|
-
display: 'flex',
|
|
858
|
-
gap: 8,
|
|
859
|
-
padding: '8px 10px',
|
|
860
|
-
borderBottom: '1px solid var(--vgai-border-1)',
|
|
861
|
-
}}
|
|
862
|
-
>
|
|
863
|
-
<Button variant="solid" size="comfortable" onClick={() => onOpen(selected)}>
|
|
864
|
-
Open in Asset Editor
|
|
865
|
-
</Button>
|
|
866
|
-
<Button variant="ghost" size="comfortable" onClick={() => toggleFavorite(selected)}>
|
|
867
|
-
{favorites.has(onlineAssetKey(selected)) ? 'Remove favorite' : 'Add favorite'}
|
|
868
|
-
</Button>
|
|
869
|
-
{getDownloadedAssetPath(selected.source, selected.id) && (
|
|
870
|
-
<span className="vgai-asset-badge">
|
|
871
|
-
<EditorIcon icon={faCheck} /> In project
|
|
872
|
-
</span>
|
|
873
|
-
)}
|
|
874
|
-
</div>
|
|
875
|
-
<OnlineAssetDetail
|
|
876
|
-
online={toOnlineInfo(selected)}
|
|
877
|
-
onResolved={() => setAssets((current) => [...current])}
|
|
878
|
-
/>
|
|
879
|
-
</>
|
|
880
|
-
) : (
|
|
881
|
-
<div role="status" style={{ padding: 24, color: text[3] }}>
|
|
882
|
-
Select an asset to inspect its preview, variants, license, and project status.
|
|
883
|
-
</div>
|
|
884
|
-
)}
|
|
885
|
-
</aside>
|
|
886
|
-
{!sourceCollapsed && (
|
|
887
|
-
<TextInput
|
|
888
|
-
className="vgai-asset-source-size"
|
|
889
|
-
aria-label="Source pane width"
|
|
890
|
-
type="range"
|
|
891
|
-
min="176"
|
|
892
|
-
max="360"
|
|
893
|
-
value={sourceWidth}
|
|
894
|
-
onChange={(event) => setSourceWidth(Number(event.target.value))}
|
|
895
|
-
/>
|
|
896
|
-
)}
|
|
897
|
-
</div>
|
|
816
|
+
const navigatorLayout = (
|
|
817
|
+
<NavigatorAssetLibrary
|
|
818
|
+
assets={filteredAssets}
|
|
819
|
+
selectedId={selectedId}
|
|
820
|
+
source={source}
|
|
821
|
+
assetType={assetType}
|
|
822
|
+
sort={sort}
|
|
823
|
+
searchQuery={searchQuery}
|
|
824
|
+
total={total}
|
|
825
|
+
loading={loading}
|
|
826
|
+
error={error}
|
|
827
|
+
hasMore={assets.length < total}
|
|
828
|
+
filtersOpen={navigatorFiltersOpen}
|
|
829
|
+
filters={sourcePane}
|
|
830
|
+
canSaveSearch={Boolean(searchQuery && !savedSearches.includes(searchQuery))}
|
|
831
|
+
onSource={onSource}
|
|
832
|
+
onType={(value) => {
|
|
833
|
+
setAssetType(value);
|
|
834
|
+
setCategory('');
|
|
835
|
+
}}
|
|
836
|
+
onSort={setSort}
|
|
837
|
+
onSearch={setSearchQuery}
|
|
838
|
+
onSelect={onSelect}
|
|
839
|
+
onOpen={onOpen}
|
|
840
|
+
onLoadMore={loadMore}
|
|
841
|
+
onSaveSearch={() => setSavedSearches((current) => [...current, searchQuery])}
|
|
842
|
+
onToggleFilters={() => setNavigatorFiltersOpen((value) => !value)}
|
|
843
|
+
onRetry={() => {
|
|
844
|
+
setOffset(0);
|
|
845
|
+
void doSearch(0, false);
|
|
846
|
+
}}
|
|
847
|
+
/>
|
|
898
848
|
);
|
|
849
|
+
|
|
850
|
+
return navigatorLayout;
|
|
899
851
|
}
|