@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.
Files changed (47) hide show
  1. package/dist/assets/{axe-C7YBtOuM.js → axe-BJjvBeOZ.js} +1 -1
  2. package/dist/assets/{browserAll-DDtGGE48.js → browserAll-Dn0pgD87.js} +1 -1
  3. package/dist/assets/{create-runtime-DbTlBlKG.js → create-runtime-BF9OD-U8.js} +1 -1
  4. package/dist/assets/{game-Dch9sAPc.js → game-B346oj_f.js} +1 -1
  5. package/dist/assets/{game-BE0r7DMI.js → game-BCNN63uV.js} +1 -1
  6. package/dist/assets/{game-SAFn90pA.js → game-BR0GBNgF.js} +1 -1
  7. package/dist/assets/{game-Cqzj_lpD.js → game-CLNm2g0y.js} +1 -1
  8. package/dist/assets/{game-CcHfYxjP.js → game-DQxhCYsZ.js} +1 -1
  9. package/dist/assets/{game-BdTHR2_X.js → game-DeTGdgYt.js} +1 -1
  10. package/dist/assets/{game-loop-DMIgSMV-.js → game-loop-t5W_oqxz.js} +1 -1
  11. package/dist/assets/{game-source-Dk83w26t.js → game-source-DzE9taML.js} +1 -1
  12. package/dist/assets/{game-DSZdeCHu.js → game-u6c2Patu.js} +1 -1
  13. package/dist/assets/{game-C3iQGSqo.js → game-umrHIqFB.js} +1 -1
  14. package/dist/assets/{index-C-LhOTBN.js → index-CgcH1RpF.js} +1 -1
  15. package/dist/assets/{index-CDkxceqE.js → index-D3tcNB2T.js} +1 -1
  16. package/dist/assets/{index-CQEfi217.js → index-DbQzGLxv.js} +771 -771
  17. package/dist/assets/{index-4idIsHHJ.js → index-Fc8Ezr20.js} +1 -1
  18. package/dist/assets/index-bAGhEnnO.css +1 -0
  19. package/dist/assets/{ingest-siblings-s0P5I1Ub.js → ingest-siblings-CW_xbvrF.js} +1 -1
  20. package/dist/assets/{mount-manifest-DXMOlF79.js → mount-manifest-DgUscyaD.js} +1 -1
  21. package/dist/assets/{react-18-C9eHn8x1.js → react-18-C4tVCii_.js} +1 -1
  22. package/dist/assets/webworkerAll-BA4aq7xq.js +1 -0
  23. package/dist/assets/{world-documents-GM0a9Kv7.js → world-documents-BSaA5YIF.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/dist-server/packaged.mjs +21 -1
  26. package/package.json +3 -3
  27. package/server/packaged.ts +20 -0
  28. package/src/asset-editor-context.tsx +130 -0
  29. package/src/asset-preview.ts +4 -0
  30. package/src/asset-selection.ts +9 -4
  31. package/src/asset-workflow/thumbnail-system.ts +16 -7
  32. package/src/components/AssetBrowser.tsx +330 -539
  33. package/src/components/AssetEditorShell.tsx +42 -112
  34. package/src/components/AssetImportDetails.tsx +24 -6
  35. package/src/components/GameHierarchy.tsx +26 -1
  36. package/src/components/Inspector.tsx +38 -2
  37. package/src/components/OnlineAssetBrowser.tsx +295 -343
  38. package/src/components/WorkspaceDock.tsx +6 -12
  39. package/src/components/asset-documents.tsx +41 -12
  40. package/src/components/asset-selection-section.tsx +34 -1
  41. package/src/components/asset-viewers/ModelViewer.tsx +7 -4
  42. package/src/components/asset-viewers/OnlineAssetDetail.tsx +233 -205
  43. package/src/components/asset-workflow.css +197 -264
  44. package/src/editor-api.ts +1 -1
  45. package/template/vgai.game.json +1 -1
  46. package/dist/assets/index-Dvi-V0NE.css +0 -1
  47. package/dist/assets/webworkerAll-Fn0e-ouF.js +0 -1
@@ -1,141 +1,71 @@
1
1
  import type { ReactNode } from 'react';
2
- import { useEffect, useState } from 'react';
3
- import { Button, border, TextInput, text } from '../design-system';
4
- import { getCurrentProject } from '../project-manager';
5
-
6
- export interface AssetEditorLayout {
7
- structureCollapsed: boolean;
8
- detailsCollapsed: boolean;
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?: ReactNode;
29
+ status?: string;
30
+ controls?: ReactNode;
51
31
  children: ReactNode;
52
32
  }) {
53
- const [layout, setLayout] = useState(() => readAssetEditorLayout(type));
54
33
  useEffect(() => {
55
- localStorage.setItem(assetEditorLayoutKey(type), JSON.stringify(layout));
56
- }, [layout, type]);
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
- data-structure-collapsed={layout.structureCollapsed}
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
- <header className="vgai-asset-editor__toolbar">
71
- <strong title={title}>{title}</strong>
72
- <div className="vgai-asset-editor__commands">{toolbar}</div>
73
- {structure && (
74
- <Button
75
- type="button"
76
- aria-pressed={!layout.structureCollapsed}
77
- onClick={() =>
78
- setLayout((value) => ({ ...value, structureCollapsed: !value.structureCollapsed }))
79
- }
80
- >
81
- Structure
82
- </Button>
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
- .read(path)
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
- const plan = assetImportPlanSchema.parse(JSON.parse(content));
24
- setSaved(plan);
25
- setDraft(plan);
26
- setError(null);
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']>(
@@ -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
- export function GameHierarchy() {
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 { assetSelectionVersion, subscribeAssetSelection } from '../asset-selection';
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
- export function Inspector() {
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
+ }