@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,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"
@@ -12,14 +12,14 @@
12
12
  * open-asset`) keeps its meaning unchanged:
13
13
  *
14
14
  * - project asset → the serving path itself (`/textures/crate.png`)
15
- * - Asset Lab → `asset-lab:entity:<entityId>`
15
+ * - legacy entity editor key → `asset-lab:entity:<entityId>`
16
16
  * - online asset → `online:<source>:<id>`
17
17
  *
18
18
  * Titles are source-owned file names as authored (§8 — `crate.png`, never a
19
19
  * technology bucket); the online document keeps the catalog's own asset name.
20
20
  *
21
21
  * Per-document RUNTIME state (an online asset resolving to a local path after
22
- * download) lives in a module-scope spec map in the house
22
+ * download) lives in an HMR-stable module spec map in the house
23
23
  * `useSyncExternalStore` shape — the registry's descriptors stay immutable,
24
24
  * and `AssetDocumentContent` re-renders from the spec map. Specs are dropped
25
25
  * on document dispose (close / project teardown via
@@ -60,7 +60,7 @@ import './asset-workflow.css';
60
60
  export interface AssetDocumentSpec {
61
61
  readonly assetPath: string;
62
62
  readonly kind: AssetKind;
63
- /** Entity source for a session-only Asset Lab document. Never serialized. */
63
+ /** Entity source for a session-only live-object Asset Editor. Never serialized. */
64
64
  readonly entityId?: string;
65
65
  /** Present while this document shows an online (not-yet-downloaded) asset. */
66
66
  readonly online?: OnlineAssetInfo;
@@ -74,8 +74,20 @@ export interface AssetDocumentMutation {
74
74
  paths?: readonly string[];
75
75
  }
76
76
 
77
- const _specs = new Map<string, AssetDocumentSpec>();
78
- const _pathAliases = new Map<string, string>();
77
+ const hotData = import.meta.hot?.data;
78
+ const _specs =
79
+ (hotData?.['vgai:asset-document-specs'] as Map<string, AssetDocumentSpec> | undefined) ??
80
+ new Map<string, AssetDocumentSpec>();
81
+ const _pathAliases =
82
+ (hotData?.['vgai:asset-document-path-aliases'] as Map<string, string> | undefined) ??
83
+ new Map<string, string>();
84
+ if (hotData) {
85
+ // Workspace descriptors intentionally survive compatible Fast Refreshes.
86
+ // Their specs must survive with them or the next persistence snapshot drops
87
+ // the open document and leaves a titled-but-empty Dockview tab on reload.
88
+ hotData['vgai:asset-document-specs'] = _specs;
89
+ hotData['vgai:asset-document-path-aliases'] = _pathAliases;
90
+ }
79
91
  let _version = 0;
80
92
  const _listeners = new Set<() => void>();
81
93
 
@@ -113,7 +125,7 @@ export interface AssetDocumentStore {
113
125
  setActiveViewportTab(tab: 'scene' | 'game'): void;
114
126
  }
115
127
 
116
- /** The Asset Lab additionally needs the live object map for its §8 title. */
128
+ /** A live-entity Asset Editor additionally needs the object map for its §8 title. */
117
129
  export interface EntityAssetDocumentStore extends AssetDocumentStore {
118
130
  readonly objectMap: Map<string, THREE.Object3D>;
119
131
  }
@@ -159,7 +171,7 @@ export function openAssetDocument(
159
171
  return openSpecDocument(store, id, title, { assetPath, kind }, { sourcePath: assetPath });
160
172
  }
161
173
 
162
- /** Open the Asset Lab document for a live entity (`Open in Asset Lab`). */
174
+ /** Open the typed Asset Editor for a live entity (legacy id retained for API compatibility). */
163
175
  export function openEntityAssetDocument(store: EntityAssetDocumentStore, entityId: string): void {
164
176
  const object = store.objectMap.get(entityId);
165
177
  if (!object) return;
@@ -277,6 +289,8 @@ function AssetViewerBody({
277
289
  if (spec.unavailableReason) {
278
290
  return (
279
291
  <AssetEditorShell
292
+ documentId={id}
293
+ active={active}
280
294
  type={spec.kind}
281
295
  title={spec.assetPath.split('/').pop() || 'Deleted asset'}
282
296
  status="Source unavailable"
@@ -288,16 +302,29 @@ function AssetViewerBody({
288
302
  const route = assetDocumentViewerRoute(spec);
289
303
  if (route === 'online' && spec.online) {
290
304
  return (
291
- <OnlineAssetDetail
292
- online={spec.online}
293
- onResolved={(localPath) => resolveOnlineAssetDocument(id, localPath)}
294
- />
305
+ <AssetEditorShell
306
+ documentId={id}
307
+ active={active}
308
+ type="online"
309
+ title={spec.online.name}
310
+ details={
311
+ <OnlineAssetDetail
312
+ online={spec.online}
313
+ mode="inspector"
314
+ onResolved={(localPath) => resolveOnlineAssetDocument(id, localPath)}
315
+ />
316
+ }
317
+ status={`${spec.online.type} · ${spec.online.source}`}
318
+ >
319
+ <OnlineAssetDetail online={spec.online} mode="preview" />
320
+ </AssetEditorShell>
295
321
  );
296
322
  }
297
323
  const title = spec.assetPath.split('/').pop() || spec.entityId || 'Asset';
298
324
  if (route === 'model')
299
325
  return (
300
326
  <ModelViewer
327
+ documentId={id}
301
328
  assetPath={spec.assetPath || undefined}
302
329
  entityId={spec.entityId}
303
330
  active={active}
@@ -330,6 +357,8 @@ function AssetViewerBody({
330
357
  }
331
358
  return (
332
359
  <AssetEditorShell
360
+ documentId={id}
361
+ active={active}
333
362
  type={route}
334
363
  title={title}
335
364
  details={
@@ -369,7 +398,7 @@ export function AssetDocumentContent({ documentId, active }: WorkspaceDocumentCo
369
398
  style={{
370
399
  flex: 1,
371
400
  minHeight: 0,
372
- overflowY: 'auto',
401
+ overflow: 'hidden',
373
402
  pointerEvents: 'auto',
374
403
  background: bg[1],
375
404
  }}
@@ -22,13 +22,14 @@
22
22
  import { resolveUrl } from '@engine/loader';
23
23
  import { MaterialFileSchema } from '@engine/scene/schema/material';
24
24
  import { useEffect, useState } from 'react';
25
- import { getSelectedAsset, type SelectedAsset } from '../asset-selection';
25
+ import { getSelectedAsset, type SelectedAsset, setSelectedAsset } from '../asset-selection';
26
26
  import { assetDocumentKind } from '../asset-workflow/asset-capabilities';
27
27
  import { PROJECT_ASSET_COMMANDS } from '../asset-workflow/project-asset-commands';
28
28
  import { Button, bg, border, radius, text } from '../design-system';
29
29
  import { registerInspectorSections } from '../inspector-section-registry';
30
30
  import { getModelThumbnailRenderer } from '../model-thumbnail';
31
31
  import { validateInputMapForViewer } from './asset-viewers/InputMapViewer';
32
+ import { OnlineAssetDetail } from './asset-viewers/OnlineAssetDetail';
32
33
 
33
34
  const labelStyle: React.CSSProperties = { fontSize: 11, color: text[3], marginBottom: 2 };
34
35
  const valueStyle: React.CSSProperties = { fontSize: 11, color: text[1], wordBreak: 'break-all' };
@@ -227,6 +228,38 @@ export function AssetSelectionSection() {
227
228
  const asset = getSelectedAsset();
228
229
  if (!asset) return null;
229
230
  const ext = asset.name.includes('.') ? (asset.name.split('.').pop() ?? '') : '';
231
+ if (asset.online) {
232
+ const online = asset.online;
233
+ return (
234
+ <div
235
+ data-testid="inspector-asset-selection"
236
+ style={{
237
+ pointerEvents: 'auto',
238
+ borderBottom: `1px solid ${border[1]}`,
239
+ display: 'flex',
240
+ flexDirection: 'column',
241
+ gap: 6,
242
+ }}
243
+ >
244
+ <OnlineAssetDetail
245
+ online={online}
246
+ mode="inspector"
247
+ onResolved={(localPath) => setSelectedAsset({ ...asset, path: localPath })}
248
+ />
249
+ <div style={{ padding: '0 8px 8px' }}>
250
+ <Button
251
+ type="button"
252
+ style={{ width: '100%' }}
253
+ onClick={() =>
254
+ window.dispatchEvent(new CustomEvent('editor:open-online-asset', { detail: online }))
255
+ }
256
+ >
257
+ Open in Asset Editor
258
+ </Button>
259
+ </div>
260
+ </div>
261
+ );
262
+ }
230
263
  return (
231
264
  <div
232
265
  data-testid="inspector-asset-selection"
@@ -50,6 +50,7 @@ import { AssetImportDetails } from '../AssetImportDetails';
50
50
  import { writeProjectAsset } from '../asset-editor-persistence';
51
51
 
52
52
  interface ModelViewerProps {
53
+ documentId?: string | undefined;
53
54
  assetPath: string | undefined;
54
55
  entityId: string | undefined;
55
56
  materialPath?: string | undefined;
@@ -71,6 +72,7 @@ interface ModelViewerRuntime {
71
72
 
72
73
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one viewer coordinates embedded/full chrome and both live-entity and file-backed Asset Editor sources.
73
74
  export function ModelViewer({
75
+ documentId,
74
76
  assetPath,
75
77
  entityId,
76
78
  materialPath,
@@ -493,6 +495,8 @@ export function ModelViewer({
493
495
 
494
496
  return (
495
497
  <AssetEditorShell
498
+ documentId={documentId ?? `asset-editor:${assetPath ?? entityId ?? displayName}`}
499
+ active={active}
496
500
  type="model"
497
501
  title={displayName}
498
502
  toolbar={
@@ -663,7 +667,8 @@ export function ModelViewer({
663
667
  )}
664
668
  </div>
665
669
  }
666
- status={
670
+ status={`${loading ? `${loadStage}…` : error ? `${issue?.stage ?? 'rendering'} failed · ${issue?.code}` : 'Ready'} · ${inspection?.triangles.toLocaleString() ?? 0} triangles`}
671
+ controls={
667
672
  inspection?.clips ? (
668
673
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
669
674
  <Select
@@ -712,9 +717,7 @@ export function ModelViewer({
712
717
  </Select>
713
718
  </label>
714
719
  </div>
715
- ) : (
716
- `${loading ? `${loadStage}…` : error ? `${issue?.stage ?? 'rendering'} failed · ${issue?.code}` : 'Ready'} · ${inspection?.triangles.toLocaleString() ?? 0} triangles`
717
- )
720
+ ) : undefined
718
721
  }
719
722
  >
720
723
  {viewport}