@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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context contributed by the active typed Asset Editor to the existing editor
|
|
3
|
+
* shell. Asset documents own content; the workspace owns placement. Keeping
|
|
4
|
+
* this as a tiny external store lets Dockview's persistent Hierarchy,
|
|
5
|
+
* Inspector, bottom drawer, and status bar render the active document's
|
|
6
|
+
* context without an Asset Editor recreating those panes inside the center.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
import { useSyncExternalStore } from 'react';
|
|
11
|
+
import { themeVars } from './design-system';
|
|
12
|
+
import { createHmrRegistrationGroup } from './hmr-registration-group';
|
|
13
|
+
import { registerWorkspaceStatus } from './workspace-status-registry';
|
|
14
|
+
import { registerWorkspaceUtility } from './workspace-utility-registry';
|
|
15
|
+
|
|
16
|
+
export const ASSET_EDITOR_CONTROLS_UTILITY_ID = 'asset-editor-controls';
|
|
17
|
+
|
|
18
|
+
export interface AssetEditorContextValue {
|
|
19
|
+
readonly documentId: string;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly type: string;
|
|
22
|
+
readonly structure?: ReactNode;
|
|
23
|
+
readonly details?: ReactNode;
|
|
24
|
+
readonly status?: string;
|
|
25
|
+
readonly controls?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ActiveAssetEditorContext extends AssetEditorContextValue {
|
|
29
|
+
readonly token: symbol;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let active: ActiveAssetEditorContext | null = null;
|
|
33
|
+
let version = 0;
|
|
34
|
+
const listeners = new Set<() => void>();
|
|
35
|
+
|
|
36
|
+
function notifyChanged(): void {
|
|
37
|
+
version++;
|
|
38
|
+
for (const listener of listeners) listener();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function subscribeAssetEditorContext(listener: () => void): () => void {
|
|
42
|
+
listeners.add(listener);
|
|
43
|
+
return () => listeners.delete(listener);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assetEditorContextVersion(): number {
|
|
47
|
+
return version;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getActiveAssetEditorContext(): AssetEditorContextValue | null {
|
|
51
|
+
return active;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Activate one document contribution. The returned disposer only clears the
|
|
55
|
+
* exact activation it created, so an older tab unmount cannot erase a newer
|
|
56
|
+
* active tab's context. */
|
|
57
|
+
export function activateAssetEditorContext(value: AssetEditorContextValue): () => void {
|
|
58
|
+
const token = Symbol(value.documentId);
|
|
59
|
+
active = { ...value, token };
|
|
60
|
+
notifyChanged();
|
|
61
|
+
return () => {
|
|
62
|
+
if (active?.token !== token) return;
|
|
63
|
+
active = null;
|
|
64
|
+
notifyChanged();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function AssetEditorControlsUtility() {
|
|
69
|
+
useSyncExternalStore(subscribeAssetEditorContext, assetEditorContextVersion);
|
|
70
|
+
const context = getActiveAssetEditorContext();
|
|
71
|
+
if (!context?.controls) return null;
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-testid="asset-editor-controls"
|
|
75
|
+
style={{
|
|
76
|
+
minHeight: 0,
|
|
77
|
+
padding: 8,
|
|
78
|
+
color: themeVars.content.primary,
|
|
79
|
+
pointerEvents: 'auto',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{context.controls}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function AssetEditorStatus() {
|
|
88
|
+
useSyncExternalStore(subscribeAssetEditorContext, assetEditorContextVersion);
|
|
89
|
+
const context = getActiveAssetEditorContext();
|
|
90
|
+
return context?.status ? (
|
|
91
|
+
<span data-testid="status-asset-editor" style={{ color: themeVars.content.muted }}>
|
|
92
|
+
{context.status}
|
|
93
|
+
</span>
|
|
94
|
+
) : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const registrationGroup = createHmrRegistrationGroup(
|
|
98
|
+
import.meta.hot,
|
|
99
|
+
'asset-editor-shell-contributions',
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
export function ensureAssetEditorShellContributionsRegistered(): void {
|
|
103
|
+
registrationGroup.ensure((track) => {
|
|
104
|
+
track(
|
|
105
|
+
registerWorkspaceUtility({
|
|
106
|
+
id: ASSET_EDITOR_CONTROLS_UTILITY_ID,
|
|
107
|
+
title: 'Asset Controls',
|
|
108
|
+
Content: AssetEditorControlsUtility,
|
|
109
|
+
available: () => getActiveAssetEditorContext()?.controls != null,
|
|
110
|
+
visibleByDefault: false,
|
|
111
|
+
closeable: true,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
track(
|
|
115
|
+
registerWorkspaceStatus({
|
|
116
|
+
id: 'asset-editor-status',
|
|
117
|
+
align: 'left',
|
|
118
|
+
order: 40,
|
|
119
|
+
Content: AssetEditorStatus,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ensureAssetEditorShellContributionsRegistered();
|
|
126
|
+
|
|
127
|
+
export function __resetAssetEditorContextForTest(): void {
|
|
128
|
+
active = null;
|
|
129
|
+
notifyChanged();
|
|
130
|
+
}
|
package/src/asset-preview.ts
CHANGED
|
@@ -785,6 +785,10 @@ function parseGltfData(
|
|
|
785
785
|
data,
|
|
786
786
|
new URL('.', sourceUrl).href,
|
|
787
787
|
(gltf) => {
|
|
788
|
+
// GLTFLoader keeps clips on the GLTF result rather than its scene.
|
|
789
|
+
// Asset Editor's model contract is Object3D-shaped, so preserve those
|
|
790
|
+
// clips instead of silently degrading animated assets into static ones.
|
|
791
|
+
gltf.scene.animations = [...gltf.animations];
|
|
788
792
|
if (!finish(() => resolve(gltf.scene))) {
|
|
789
793
|
disposeOwnedModel(gltf.scene);
|
|
790
794
|
}
|
package/src/asset-selection.ts
CHANGED
|
@@ -13,19 +13,19 @@
|
|
|
13
13
|
* (mirrors `inspector-section-registry.ts`): version counter + subscribe +
|
|
14
14
|
* `__resetForTest`. It deliberately does NOT touch the EditorStore's entity
|
|
15
15
|
* selection — clicking an asset must never deselect/reselect scene entities
|
|
16
|
-
* (§6.1 "selection never unexpectedly..."); instead the Inspector
|
|
17
|
-
*
|
|
16
|
+
* (§6.1 "selection never unexpectedly..."); instead the Inspector gives the
|
|
17
|
+
* asset domain precedence while retaining that entity selection, and the asset is auto-cleared
|
|
18
18
|
* when the ENTITY selection changes (the user moved on — see
|
|
19
19
|
* `installAssetSelectionAutoClear`).
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { AssetCapabilities } from './asset-workflow/asset-capabilities';
|
|
23
|
-
import type { AssetKind } from './editor-store';
|
|
23
|
+
import type { AssetKind, OnlineAssetInfo } from './editor-store';
|
|
24
24
|
|
|
25
25
|
/** The §5.1 compact-selection payload — enough for the Inspector section to
|
|
26
26
|
* render preview + metadata without re-deriving browser state. */
|
|
27
27
|
export interface SelectedAsset {
|
|
28
|
-
/**
|
|
28
|
+
/** Project serving path, or a stable `online:<source>:<id>` document key. */
|
|
29
29
|
readonly path: string;
|
|
30
30
|
/** Source-owned file name as authored (§8 — `crate.png`). */
|
|
31
31
|
readonly name: string;
|
|
@@ -39,6 +39,11 @@ export interface SelectedAsset {
|
|
|
39
39
|
readonly dependencies?: readonly string[];
|
|
40
40
|
readonly references?: readonly { ownerPath: string; jsonPath: string }[];
|
|
41
41
|
readonly healthCodes?: readonly string[];
|
|
42
|
+
/** Selection came from the external acquisition Library rather than the
|
|
43
|
+
* project's owned file tree. `online` carries the provider identity and is
|
|
44
|
+
* intentionally absent from project assets. */
|
|
45
|
+
readonly origin?: 'project' | 'library';
|
|
46
|
+
readonly online?: OnlineAssetInfo;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
let _selected: SelectedAsset | null = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getStorageBackend } from '../storage';
|
|
2
|
+
import { getStorageBackend, type StorageBackend } from '../storage';
|
|
3
3
|
import type { AssetCatalog } from './asset-types';
|
|
4
4
|
|
|
5
5
|
export const THUMBNAIL_PROFILE = {
|
|
@@ -39,15 +39,24 @@ export const thumbnailManifestSchema = z.object({
|
|
|
39
39
|
export type ThumbnailManifest = z.infer<typeof thumbnailManifestSchema>;
|
|
40
40
|
|
|
41
41
|
let manifestPromise: Promise<ThumbnailManifest | null> | null = null;
|
|
42
|
+
|
|
43
|
+
/** Optional project thumbnail metadata must never block live fallback previews.
|
|
44
|
+
* HTTP-backed projects can return the Vite HTML shell for an absent file, so
|
|
45
|
+
* existence and parsing are both guarded at this storage boundary. */
|
|
46
|
+
export async function readProjectThumbnailManifest(
|
|
47
|
+
backend: Pick<StorageBackend, 'read'>,
|
|
48
|
+
): Promise<ThumbnailManifest | null> {
|
|
49
|
+
try {
|
|
50
|
+
return thumbnailManifestSchema.parse(JSON.parse(await backend.read('.vgai/thumbnails.json')));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
export function loadProjectThumbnailManifest(): Promise<ThumbnailManifest | null> {
|
|
43
57
|
if (!manifestPromise) {
|
|
44
58
|
const backend = getStorageBackend() as ReturnType<typeof getStorageBackend> | undefined;
|
|
45
|
-
manifestPromise = backend
|
|
46
|
-
? backend.read('.vgai/thumbnails.json').then(
|
|
47
|
-
(content) => thumbnailManifestSchema.parse(JSON.parse(content)),
|
|
48
|
-
() => null,
|
|
49
|
-
)
|
|
50
|
-
: Promise.resolve(null);
|
|
59
|
+
manifestPromise = backend ? readProjectThumbnailManifest(backend) : Promise.resolve(null);
|
|
51
60
|
}
|
|
52
61
|
return manifestPromise;
|
|
53
62
|
}
|
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
ThumbnailJobQueue,
|
|
34
34
|
} from '../asset-workflow/thumbnail-system';
|
|
35
35
|
import {
|
|
36
|
-
accent,
|
|
37
36
|
Button,
|
|
38
37
|
border,
|
|
39
38
|
EditorIcon,
|
|
@@ -119,7 +118,12 @@ function AssetIcon({ kind, size = 24 }: { kind: BrowserKind; size?: number }) {
|
|
|
119
118
|
const projectThumbnailQueue = new ThumbnailJobQueue<string>((url) => {
|
|
120
119
|
const format = modelThumbnailFormat(url);
|
|
121
120
|
if (!format) return Promise.reject(new Error(`No model preview loader for ${url}.`));
|
|
122
|
-
return getModelThumbnailRenderer().render({
|
|
121
|
+
return getModelThumbnailRenderer().render({
|
|
122
|
+
url,
|
|
123
|
+
format,
|
|
124
|
+
background: 'neutral',
|
|
125
|
+
output: 'webp',
|
|
126
|
+
});
|
|
123
127
|
}, 2);
|
|
124
128
|
|
|
125
129
|
function ModelThumbnail({ url }: { url: string }) {
|
|
@@ -175,7 +179,7 @@ function ModelThumbnail({ url }: { url: string }) {
|
|
|
175
179
|
setManifestPath(null);
|
|
176
180
|
setManifestRejected(true);
|
|
177
181
|
}}
|
|
178
|
-
style={{
|
|
182
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
179
183
|
/>
|
|
180
184
|
);
|
|
181
185
|
}
|
|
@@ -209,7 +213,7 @@ function ModelThumbnail({ url }: { url: string }) {
|
|
|
209
213
|
<img
|
|
210
214
|
src={src}
|
|
211
215
|
alt="model preview"
|
|
212
|
-
style={{
|
|
216
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
|
213
217
|
/>
|
|
214
218
|
);
|
|
215
219
|
}
|
|
@@ -521,8 +525,9 @@ function BrowserEntryCard({
|
|
|
521
525
|
|
|
522
526
|
return (
|
|
523
527
|
<div
|
|
524
|
-
className="vgai-selectable"
|
|
528
|
+
className="vgai-selectable vgai-project-asset-entry"
|
|
525
529
|
data-selected={selected || undefined}
|
|
530
|
+
data-view={viewMode}
|
|
526
531
|
role="option"
|
|
527
532
|
aria-selected={selected}
|
|
528
533
|
tabIndex={0}
|
|
@@ -543,14 +548,15 @@ function BrowserEntryCard({
|
|
|
543
548
|
...(grid ? GRID_ENTRY_STYLE : LIST_ENTRY_STYLE),
|
|
544
549
|
borderRadius: radius.sm,
|
|
545
550
|
cursor,
|
|
546
|
-
border: selected ? `1px solid ${accent}` : '1px solid transparent',
|
|
547
551
|
userSelect: 'none',
|
|
548
552
|
}}
|
|
549
553
|
>
|
|
550
554
|
<div
|
|
555
|
+
className="vgai-project-asset-entry__preview"
|
|
556
|
+
data-testid="asset-entry-preview"
|
|
551
557
|
style={{
|
|
552
558
|
...(grid
|
|
553
|
-
? { width:
|
|
559
|
+
? { width: '100%', aspectRatio: '4 / 3', margin: '0 auto' }
|
|
554
560
|
: { width: 18, height: 18, margin: 0 }),
|
|
555
561
|
display: 'flex',
|
|
556
562
|
alignItems: 'center',
|
|
@@ -1309,6 +1315,8 @@ export function AssetBrowser({ onOpenScene }: AssetBrowserProps) {
|
|
|
1309
1315
|
|
|
1310
1316
|
{/* One file-browser surface; grid/list are presentation modes, not navigation. */}
|
|
1311
1317
|
<div
|
|
1318
|
+
className="vgai-project-assets-results"
|
|
1319
|
+
data-view={viewMode}
|
|
1312
1320
|
style={{
|
|
1313
1321
|
flex: 1,
|
|
1314
1322
|
overflowY: 'auto',
|
|
@@ -1,141 +1,71 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { useEffect
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
structureWidth: number;
|
|
10
|
-
detailsWidth: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const defaults: AssetEditorLayout = {
|
|
14
|
-
structureCollapsed: false,
|
|
15
|
-
detailsCollapsed: false,
|
|
16
|
-
structureWidth: 240,
|
|
17
|
-
detailsWidth: 300,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function assetEditorLayoutKey(
|
|
21
|
-
type: string,
|
|
22
|
-
projectId = getCurrentProject()?.rootPath ?? '__unscoped__',
|
|
23
|
-
): string {
|
|
24
|
-
return `vgai:asset-editor-layout:${projectId}:${type}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function readAssetEditorLayout(type: string): AssetEditorLayout {
|
|
28
|
-
if (typeof localStorage === 'undefined') return defaults;
|
|
29
|
-
try {
|
|
30
|
-
return { ...defaults, ...JSON.parse(localStorage.getItem(assetEditorLayoutKey(type)) ?? '{}') };
|
|
31
|
-
} catch {
|
|
32
|
-
return defaults;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
ASSET_EDITOR_CONTROLS_UTILITY_ID,
|
|
5
|
+
activateAssetEditorContext,
|
|
6
|
+
} from '../asset-editor-context';
|
|
7
|
+
import { Button } from '../design-system';
|
|
8
|
+
import { showWorkspaceUtility } from '../workspace-dock-controller';
|
|
35
9
|
|
|
36
10
|
export function AssetEditorShell({
|
|
11
|
+
documentId,
|
|
12
|
+
active = true,
|
|
37
13
|
type,
|
|
38
14
|
title,
|
|
39
15
|
toolbar,
|
|
40
16
|
structure,
|
|
41
17
|
details,
|
|
42
18
|
status,
|
|
19
|
+
controls,
|
|
43
20
|
children,
|
|
44
21
|
}: {
|
|
22
|
+
documentId: string;
|
|
23
|
+
active?: boolean;
|
|
45
24
|
type: string;
|
|
46
25
|
title: string;
|
|
47
26
|
toolbar?: ReactNode;
|
|
48
27
|
structure?: ReactNode;
|
|
49
28
|
details?: ReactNode;
|
|
50
|
-
status?:
|
|
29
|
+
status?: string;
|
|
30
|
+
controls?: ReactNode;
|
|
51
31
|
children: ReactNode;
|
|
52
32
|
}) {
|
|
53
|
-
const [layout, setLayout] = useState(() => readAssetEditorLayout(type));
|
|
54
33
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
if (!active) return;
|
|
35
|
+
return activateAssetEditorContext({
|
|
36
|
+
documentId,
|
|
37
|
+
title,
|
|
38
|
+
type,
|
|
39
|
+
...(structure ? { structure } : {}),
|
|
40
|
+
...(details ? { details } : {}),
|
|
41
|
+
...(status ? { status } : {}),
|
|
42
|
+
...(controls ? { controls } : {}),
|
|
43
|
+
});
|
|
44
|
+
}, [active, controls, details, documentId, status, structure, title, type]);
|
|
45
|
+
|
|
46
|
+
const hasToolbar = toolbar != null || controls != null;
|
|
57
47
|
return (
|
|
58
48
|
<section
|
|
59
49
|
className="vgai-asset-editor"
|
|
60
50
|
data-testid={`asset-editor:${type}`}
|
|
61
|
-
|
|
62
|
-
data-details-collapsed={layout.detailsCollapsed}
|
|
63
|
-
style={
|
|
64
|
-
{
|
|
65
|
-
'--asset-editor-structure-width': `${layout.structureWidth}px`,
|
|
66
|
-
'--asset-editor-details-width': `${layout.detailsWidth}px`,
|
|
67
|
-
} as React.CSSProperties
|
|
68
|
-
}
|
|
51
|
+
aria-label={`${title} Asset Editor`}
|
|
69
52
|
>
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{details && (
|
|
85
|
-
<Button
|
|
86
|
-
type="button"
|
|
87
|
-
aria-pressed={!layout.detailsCollapsed}
|
|
88
|
-
onClick={() =>
|
|
89
|
-
setLayout((value) => ({ ...value, detailsCollapsed: !value.detailsCollapsed }))
|
|
90
|
-
}
|
|
91
|
-
>
|
|
92
|
-
Details
|
|
93
|
-
</Button>
|
|
94
|
-
)}
|
|
95
|
-
</header>
|
|
96
|
-
{structure && (
|
|
97
|
-
<aside className="vgai-asset-editor__structure" aria-label="Asset structure">
|
|
98
|
-
{structure}
|
|
99
|
-
<label className="vgai-asset-editor__resize">
|
|
100
|
-
Structure width
|
|
101
|
-
<TextInput
|
|
102
|
-
aria-label="Asset structure pane width"
|
|
103
|
-
type="range"
|
|
104
|
-
min={180}
|
|
105
|
-
max={420}
|
|
106
|
-
value={layout.structureWidth}
|
|
107
|
-
onChange={(event) =>
|
|
108
|
-
setLayout((value) => ({ ...value, structureWidth: Number(event.target.value) }))
|
|
109
|
-
}
|
|
110
|
-
/>
|
|
111
|
-
</label>
|
|
112
|
-
</aside>
|
|
53
|
+
{hasToolbar && (
|
|
54
|
+
<header className="vgai-asset-editor__toolbar">
|
|
55
|
+
<div className="vgai-asset-editor__commands">{toolbar}</div>
|
|
56
|
+
{controls != null && (
|
|
57
|
+
<Button
|
|
58
|
+
type="button"
|
|
59
|
+
variant="ghost"
|
|
60
|
+
size="compact"
|
|
61
|
+
onClick={() => showWorkspaceUtility(ASSET_EDITOR_CONTROLS_UTILITY_ID)}
|
|
62
|
+
>
|
|
63
|
+
Controls
|
|
64
|
+
</Button>
|
|
65
|
+
)}
|
|
66
|
+
</header>
|
|
113
67
|
)}
|
|
114
68
|
<main className="vgai-asset-editor__viewport">{children}</main>
|
|
115
|
-
{details && (
|
|
116
|
-
<aside className="vgai-asset-editor__details" aria-label="Asset details">
|
|
117
|
-
{details}
|
|
118
|
-
<label className="vgai-asset-editor__resize">
|
|
119
|
-
Details width
|
|
120
|
-
<TextInput
|
|
121
|
-
aria-label="Asset details pane width"
|
|
122
|
-
type="range"
|
|
123
|
-
min={220}
|
|
124
|
-
max={480}
|
|
125
|
-
value={layout.detailsWidth}
|
|
126
|
-
onChange={(event) =>
|
|
127
|
-
setLayout((value) => ({ ...value, detailsWidth: Number(event.target.value) }))
|
|
128
|
-
}
|
|
129
|
-
/>
|
|
130
|
-
</label>
|
|
131
|
-
</aside>
|
|
132
|
-
)}
|
|
133
|
-
<footer
|
|
134
|
-
className="vgai-asset-editor__status"
|
|
135
|
-
style={{ borderColor: border[1], color: text[2] }}
|
|
136
|
-
>
|
|
137
|
-
{status}
|
|
138
|
-
</footer>
|
|
139
69
|
</section>
|
|
140
70
|
);
|
|
141
71
|
}
|
|
@@ -15,20 +15,37 @@ export function AssetImportDetails({ assetPath }: { assetPath: string }) {
|
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
let cancelled = false;
|
|
17
17
|
const path = importRecipePath(assetPath);
|
|
18
|
-
getStorageBackend()
|
|
19
|
-
|
|
18
|
+
const backend = getStorageBackend();
|
|
19
|
+
backend
|
|
20
|
+
.exists(path)
|
|
21
|
+
.then((exists) => (exists ? backend.read(path) : null))
|
|
20
22
|
.then(
|
|
21
23
|
(content) => {
|
|
22
24
|
if (cancelled) return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
if (content === null) {
|
|
26
|
+
setSaved(null);
|
|
27
|
+
setDraft(null);
|
|
28
|
+
setError(null);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const plan = assetImportPlanSchema.parse(JSON.parse(content));
|
|
33
|
+
setSaved(plan);
|
|
34
|
+
setDraft(plan);
|
|
35
|
+
setError(null);
|
|
36
|
+
} catch (cause) {
|
|
37
|
+
setSaved(null);
|
|
38
|
+
setDraft(null);
|
|
39
|
+
setError(
|
|
40
|
+
`Invalid import recipe: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
27
43
|
},
|
|
28
44
|
() => {
|
|
29
45
|
if (!cancelled) {
|
|
30
46
|
setSaved(null);
|
|
31
47
|
setDraft(null);
|
|
48
|
+
setError(null);
|
|
32
49
|
}
|
|
33
50
|
},
|
|
34
51
|
);
|
|
@@ -50,6 +67,7 @@ export function AssetImportDetails({ assetPath }: { assetPath: string }) {
|
|
|
50
67
|
return (
|
|
51
68
|
<SectionHeader label="Import">
|
|
52
69
|
<div>No saved import recipe. Native project asset.</div>
|
|
70
|
+
{error && <div style={{ color: danger }}>{error}</div>}
|
|
53
71
|
</SectionHeader>
|
|
54
72
|
);
|
|
55
73
|
const setSetting = <K extends keyof AssetImportPlan['settings']>(
|
|
@@ -8,7 +8,7 @@ import { OnlineAssetBrowser } from './OnlineAssetBrowser';
|
|
|
8
8
|
export function AssetLibraryPanel() {
|
|
9
9
|
return (
|
|
10
10
|
<div className="vgai-asset-library-panel" data-testid="asset-library-panel">
|
|
11
|
-
<OnlineAssetBrowser
|
|
11
|
+
<OnlineAssetBrowser />
|
|
12
12
|
</div>
|
|
13
13
|
);
|
|
14
14
|
}
|
|
@@ -83,6 +83,11 @@ import {
|
|
|
83
83
|
faTriangleExclamation,
|
|
84
84
|
} from '@fortawesome/free-solid-svg-icons';
|
|
85
85
|
import { useCallback, useEffect, useReducer, useRef, useState, useSyncExternalStore } from 'react';
|
|
86
|
+
import {
|
|
87
|
+
assetEditorContextVersion,
|
|
88
|
+
getActiveAssetEditorContext,
|
|
89
|
+
subscribeAssetEditorContext,
|
|
90
|
+
} from '../asset-editor-context';
|
|
86
91
|
import { getActiveAuthoring, hasAuthoringOverride } from '../authoring/active-adapter';
|
|
87
92
|
import { CompositeAuthoringAdapter } from '../authoring/composite-authoring-adapter';
|
|
88
93
|
import { requestWorldFocus, worldFocusScenePath } from '../authoring/edit-mode-authoring';
|
|
@@ -1342,7 +1347,7 @@ function Row({
|
|
|
1342
1347
|
|
|
1343
1348
|
// --- Main panel ---
|
|
1344
1349
|
|
|
1345
|
-
|
|
1350
|
+
function SceneHierarchyContent() {
|
|
1346
1351
|
const store = useEditorStore();
|
|
1347
1352
|
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
1348
1353
|
const adapter = getActiveAuthoring(store);
|
|
@@ -1930,3 +1935,23 @@ export function GameHierarchy() {
|
|
|
1930
1935
|
</Panel>
|
|
1931
1936
|
);
|
|
1932
1937
|
}
|
|
1938
|
+
|
|
1939
|
+
/** The navigator slot follows the active center document. Scene/world
|
|
1940
|
+
* documents use the normal authoring hierarchy; a structured Asset Editor
|
|
1941
|
+
* contributes its model/bone/material outline into this same outer panel. */
|
|
1942
|
+
export function GameHierarchy() {
|
|
1943
|
+
useSyncExternalStore(subscribeAssetEditorContext, assetEditorContextVersion);
|
|
1944
|
+
const context = getActiveAssetEditorContext();
|
|
1945
|
+
if (!context?.structure) return <SceneHierarchyContent />;
|
|
1946
|
+
return (
|
|
1947
|
+
<Panel
|
|
1948
|
+
name={`${context.title} Structure`}
|
|
1949
|
+
hideHeader
|
|
1950
|
+
data-testid="asset-editor-structure"
|
|
1951
|
+
onPointerDown={() => setActiveScope('hierarchy')}
|
|
1952
|
+
style={{ flex: 1, minWidth: 0, minHeight: 0, overflow: 'auto' }}
|
|
1953
|
+
>
|
|
1954
|
+
{context.structure}
|
|
1955
|
+
</Panel>
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
@@ -27,7 +27,16 @@ import type { AuthoringAdapter, EditorNode, PropertyDescriptor, Transform } from
|
|
|
27
27
|
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
|
28
28
|
import { useSyncExternalStore } from 'react';
|
|
29
29
|
import * as THREE from 'three';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
assetEditorContextVersion,
|
|
32
|
+
getActiveAssetEditorContext,
|
|
33
|
+
subscribeAssetEditorContext,
|
|
34
|
+
} from '../asset-editor-context';
|
|
35
|
+
import {
|
|
36
|
+
assetSelectionVersion,
|
|
37
|
+
getSelectedAsset,
|
|
38
|
+
subscribeAssetSelection,
|
|
39
|
+
} from '../asset-selection';
|
|
31
40
|
import { getActiveAuthoring } from '../authoring/active-adapter';
|
|
32
41
|
import { CompositeAuthoringAdapter } from '../authoring/composite-authoring-adapter';
|
|
33
42
|
import { ReactWorldAuthoringAdapter } from '../authoring/react-world-authoring-adapter';
|
|
@@ -57,6 +66,7 @@ import {
|
|
|
57
66
|
workspaceDocumentRegistryVersion,
|
|
58
67
|
} from '../workspace-document-registry';
|
|
59
68
|
import { openEntityAssetDocument } from './asset-documents';
|
|
69
|
+
import { AssetSelectionSection } from './asset-selection-section';
|
|
60
70
|
import { groupProperties } from './inspector-property-grouping';
|
|
61
71
|
import { groupLabelStyle, LABEL_COL_WIDTH } from './inspector-widgets/shared';
|
|
62
72
|
|
|
@@ -179,7 +189,7 @@ function AxisRow({
|
|
|
179
189
|
);
|
|
180
190
|
}
|
|
181
191
|
|
|
182
|
-
|
|
192
|
+
function SceneInspector() {
|
|
183
193
|
const store = useEditorStore();
|
|
184
194
|
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
185
195
|
// Re-render when section registrations change (W3): project inspector
|
|
@@ -723,3 +733,29 @@ export function Inspector() {
|
|
|
723
733
|
</Panel>
|
|
724
734
|
);
|
|
725
735
|
}
|
|
736
|
+
|
|
737
|
+
/** Asset documents reuse the ONE outer Inspector rather than rendering a
|
|
738
|
+
* second details rail inside the center document. */
|
|
739
|
+
export function Inspector() {
|
|
740
|
+
useSyncExternalStore(subscribeAssetEditorContext, assetEditorContextVersion);
|
|
741
|
+
useSyncExternalStore(subscribeAssetSelection, assetSelectionVersion);
|
|
742
|
+
const context = getActiveAssetEditorContext();
|
|
743
|
+
const selectedAsset = getSelectedAsset();
|
|
744
|
+
if (!context?.details && !selectedAsset) return <SceneInspector />;
|
|
745
|
+
return (
|
|
746
|
+
<Panel
|
|
747
|
+
name="Inspector"
|
|
748
|
+
hideHeader
|
|
749
|
+
data-testid="inspector-panel"
|
|
750
|
+
onPointerDown={() => setActiveScope('inspector')}
|
|
751
|
+
style={{ overflowY: 'auto' }}
|
|
752
|
+
>
|
|
753
|
+
<div
|
|
754
|
+
data-testid={context ? 'asset-editor-details' : 'asset-selection-details'}
|
|
755
|
+
style={{ pointerEvents: 'auto', padding: context ? 8 : 0 }}
|
|
756
|
+
>
|
|
757
|
+
{context?.details ?? <AssetSelectionSection />}
|
|
758
|
+
</div>
|
|
759
|
+
</Panel>
|
|
760
|
+
);
|
|
761
|
+
}
|