@vgai/editor 0.4.0 → 0.4.1
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-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
- package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
- package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
- package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
- package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
- package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
- package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
- package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
- package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
- package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
- package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
- package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
- package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
- package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
- package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
- package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
- package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
- package/dist/assets/index-DiUacMXR.css +1 -0
- package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
- package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
- package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
- package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
- package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +27125 -1075
- package/package.json +2 -2
- package/server/asset-history-snapshots.ts +189 -0
- package/server/asset-library-routes.ts +404 -53
- package/server/catalog-import-recipe.ts +97 -0
- package/server/catalog-reimport.ts +141 -0
- package/server/cloud-asset-catalog.ts +71 -5
- package/server/dev.ts +1 -1
- package/server/editor-server.ts +106 -51
- package/server/editor-sse.ts +5 -0
- package/server/generative-execution-context.ts +45 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/packaged.ts +1 -1
- package/server/project-hmr-files.ts +3 -5
- package/server/project-output-writer.ts +440 -36
- package/server/project-tools.ts +529 -0
- package/server/server-utils.ts +14 -11
- package/src/EditorContext.tsx +4 -4
- package/src/action-registry.ts +12 -0
- package/src/asset-editor-context.tsx +9 -0
- package/src/asset-events.ts +3 -15
- package/src/asset-selection.ts +3 -0
- package/src/asset-workflow/asset-capabilities.ts +40 -0
- package/src/asset-workflow/asset-import-jobs.ts +108 -0
- package/src/asset-workflow/asset-library-query.ts +8 -1
- package/src/asset-workflow/asset-workflow-quality.ts +2 -0
- package/src/asset-workflow/import-contract.ts +2 -0
- package/src/asset-workflow/model-inspection.ts +92 -1
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +1 -0
- package/src/asset-workflow/project-asset-health.ts +3 -6
- package/src/asset-workflow/project-asset-operations.ts +119 -4
- package/src/command-listener.ts +37 -9
- package/src/components/ApplicationMenus.tsx +13 -15
- package/src/components/AssetBrowser.tsx +301 -26
- package/src/components/AssetImportDetails.tsx +207 -92
- package/src/components/AssetImportJobStatus.tsx +55 -0
- package/src/components/CenterDocuments.tsx +28 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DefaultEditorLayout.tsx +4 -0
- package/src/components/InspectorToolSection.tsx +12 -10
- package/src/components/OnlineAssetBrowser.tsx +197 -11
- package/src/components/ToolHost.tsx +8 -8
- package/src/components/ViewportOverlay.tsx +13 -26
- package/src/components/ViewportPanel.tsx +19 -14
- package/src/components/ViewportShadingMenu.tsx +103 -0
- package/src/components/WorkspaceDock.tsx +103 -17
- package/src/components/asset-documents.tsx +38 -11
- package/src/components/asset-editor-persistence.ts +300 -5
- package/src/components/asset-selection-section.tsx +83 -9
- package/src/components/asset-viewers/AudioViewer.tsx +4 -7
- package/src/components/asset-viewers/ModelViewer.tsx +168 -5
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
- package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
- package/src/components/asset-workflow.css +30 -3
- package/src/components/primitives/editor-icons.ts +1 -0
- package/src/components/project-tool-documents.tsx +298 -0
- package/src/components/status-contributions.tsx +38 -0
- package/src/components/tool-documents.tsx +20 -17
- package/src/components/tool-schema-form.tsx +258 -0
- package/src/components/workspace-dock.css +16 -0
- package/src/editor-api.ts +159 -22
- package/src/editor-store.ts +6 -5
- package/src/editor-viewport.ts +12 -7
- package/src/game-viewport-state.ts +28 -0
- package/src/model-thumbnail.ts +97 -9
- package/src/play-mode.ts +30 -3
- package/src/project-tool-discovery.ts +5 -5
- package/src/{project-operations.ts → project-tools.ts} +39 -26
- package/src/scene-sync.ts +0 -67
- package/src/storage/handle-storage.ts +5 -1
- package/src/storage/http-storage.ts +7 -2
- package/src/storage/mem-storage.ts +7 -1
- package/src/storage/types.ts +2 -0
- package/src/theme-css.ts +17 -7
- package/src/tool-loader.ts +152 -241
- package/src/viewport-shading-boundary.ts +15 -0
- package/src/widgets/index.ts +2 -1
- package/src/workspace-dock-controller.ts +8 -0
- package/src/workspace-document-registry.ts +4 -4
- package/src/workspace-state-persistence.ts +36 -10
- package/template/.claude/skills/editor/SKILL.md +13 -3
- package/template/AGENTS.md +100 -83
- package/template/package.json +16 -0
- package/template/public/scenes/default.vscn.json +1 -2
- package/template/scripts/playtest.ts +7 -37
- package/template/server/rooms/game-room.ts +2 -2
- package/template/src/data/README.md +3 -2
- package/template/src/data/tuning.data.json +1 -3
- package/template/src/data/tuning.schema.json +0 -12
- package/template/src/data/tuning.schema.ts +2 -9
- package/template/src/data/tuning.ts +1 -1
- package/template/src/scripts/registry.ts +0 -3
- package/template/src/tools/README.md +151 -16
- package/template/src/tools/autoplay.tool.ts +31 -0
- package/template/src/tools/autoplay.ts +15 -0
- package/template/src/tools/tuning.document.tsx +107 -0
- package/template/src/tools/tuning.tool.ts +24 -0
- package/template/src/tools/tuning.ts +9 -0
- package/template/src/ui/Button.stories.tsx +1 -1
- package/template/tests/acceptance/example.spec.ts +4 -59
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bAGhEnnO.css +0 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
- package/dist/project-scripts.bundle.js +0 -3917
- package/server/project-operations.ts +0 -343
- package/src/components/project-operation-documents.tsx +0 -226
- package/template/src/operations/README.md +0 -88
- package/template/src/scripts/components/data-spinner.ts +0 -86
- package/template/src/scripts/components/spin-lap-math.ts +0 -27
- package/template/src/tools/example.tool.tsx +0 -97
- package/template/src/tools/spin.tool.tsx +0 -101
- package/template/tests/logic/example.test.ts +0 -31
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from '../harness-chat-layout';
|
|
27
27
|
import type { HotkeyScope } from '../hotkeys';
|
|
28
28
|
import {
|
|
29
|
+
closeWorkspaceUtility,
|
|
29
30
|
installWorkspaceDockCommands,
|
|
30
31
|
type WorkspaceDockCommands,
|
|
31
32
|
} from '../workspace-dock-controller';
|
|
@@ -80,6 +81,20 @@ type WorkspacePanelParams =
|
|
|
80
81
|
| { kind: 'document'; documentId: string; closeable: boolean }
|
|
81
82
|
| { kind: 'utility'; utilityId: string; closeable: boolean };
|
|
82
83
|
|
|
84
|
+
type UtilityRevealOrigin = { previousPanelId: string | null; wasCollapsed: boolean };
|
|
85
|
+
|
|
86
|
+
function rememberUtilityReveal(
|
|
87
|
+
panel: NonNullable<ReturnType<DockviewApi['getPanel']>>,
|
|
88
|
+
origins: Map<string, UtilityRevealOrigin>,
|
|
89
|
+
utilityId: string,
|
|
90
|
+
): void {
|
|
91
|
+
if (panel.api.isActive) return;
|
|
92
|
+
origins.set(utilityId, {
|
|
93
|
+
previousPanelId: panel.api.group.activePanel?.id ?? null,
|
|
94
|
+
wasCollapsed: panel.api.location.type === 'edge' && panel.api.group.api.isCollapsed(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
function hotkeyScopeForPanel(params: WorkspacePanelParams): HotkeyScope {
|
|
84
99
|
if (params.kind === 'utility') return 'workspace';
|
|
85
100
|
if (params.kind === 'document') {
|
|
@@ -144,6 +159,7 @@ function WorkspacePanel({ api, params }: IDockviewPanelProps<WorkspacePanelParam
|
|
|
144
159
|
workspaceUtilityRegistryVersion,
|
|
145
160
|
workspaceUtilityRegistryVersion,
|
|
146
161
|
);
|
|
162
|
+
useSyncExternalStore(subscribeAvailabilityTick, availabilityTickVersion, availabilityTickVersion);
|
|
147
163
|
|
|
148
164
|
if (params.kind === 'static') {
|
|
149
165
|
return (
|
|
@@ -160,7 +176,7 @@ function WorkspacePanel({ api, params }: IDockviewPanelProps<WorkspacePanelParam
|
|
|
160
176
|
if (params.kind === 'utility') {
|
|
161
177
|
const utility = availableWorkspaceUtilities().find((item) => item.id === params.utilityId);
|
|
162
178
|
if (!utility) {
|
|
163
|
-
return <
|
|
179
|
+
return <UnavailableUtilityPanel utilityId={params.utilityId} />;
|
|
164
180
|
}
|
|
165
181
|
const Content = utility.Content;
|
|
166
182
|
return (
|
|
@@ -204,6 +220,13 @@ function WorkspacePanel({ api, params }: IDockviewPanelProps<WorkspacePanelParam
|
|
|
204
220
|
);
|
|
205
221
|
}
|
|
206
222
|
|
|
223
|
+
function UnavailableUtilityPanel({ utilityId }: { utilityId: string }) {
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
closeWorkspaceUtility(utilityId);
|
|
226
|
+
}, [utilityId]);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
207
230
|
function workspaceTabTestId(params: WorkspacePanelParams, panelId: string): string {
|
|
208
231
|
if (params.kind === 'document') return `workspace-tab:${params.documentId}`;
|
|
209
232
|
if (params.kind === 'utility') return `bottom-tab-utility-${params.utilityId}`;
|
|
@@ -278,6 +301,7 @@ function closeDockviewDocumentTab(
|
|
|
278
301
|
return true;
|
|
279
302
|
}
|
|
280
303
|
|
|
304
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one tab renderer owns Dockview activation, accessible close, dirty-save confirmation, badges, and resource-family metadata.
|
|
281
305
|
function WorkspaceTab(props: IDockviewPanelHeaderProps<WorkspacePanelParams>) {
|
|
282
306
|
const { api, params } = props;
|
|
283
307
|
const [confirmingClose, setConfirmingClose] = useState(false);
|
|
@@ -300,16 +324,18 @@ function WorkspaceTab(props: IDockviewPanelHeaderProps<WorkspacePanelParams>) {
|
|
|
300
324
|
const badge = useWorkspaceUtilityBadge(params.kind === 'utility' ? params.utilityId : null);
|
|
301
325
|
const testId = workspaceTabTestId(params, api.id);
|
|
302
326
|
const tabFamily = documentTabFamily(params);
|
|
327
|
+
const requestDocumentClose = () => {
|
|
328
|
+
if (params.kind !== 'document') return;
|
|
329
|
+
if (document?.dirty) {
|
|
330
|
+
setConfirmingClose(true);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
closeDockviewDocumentTab(props, params.documentId);
|
|
334
|
+
};
|
|
303
335
|
const closeProps =
|
|
304
336
|
params.kind === 'document' && params.closeable
|
|
305
337
|
? {
|
|
306
|
-
closeActionOverride:
|
|
307
|
-
if (document?.dirty) {
|
|
308
|
-
setConfirmingClose(true);
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
closeDockviewDocumentTab(props, params.documentId);
|
|
312
|
-
},
|
|
338
|
+
closeActionOverride: requestDocumentClose,
|
|
313
339
|
}
|
|
314
340
|
: params.kind === 'static' && params.staticKind === 'agent'
|
|
315
341
|
? {
|
|
@@ -364,8 +390,35 @@ function WorkspaceTab(props: IDockviewPanelHeaderProps<WorkspacePanelParams>) {
|
|
|
364
390
|
data-dirty={document?.dirty || undefined}
|
|
365
391
|
data-document-family={tabFamily}
|
|
366
392
|
data-editor-hotkey-scope={hotkeyScopeForPanel(params)}
|
|
393
|
+
onPointerDownCapture={() => {
|
|
394
|
+
// A bottom edge panel can remain Dockview's globally active panel
|
|
395
|
+
// while a center-group tab visibly changes. Select the semantic
|
|
396
|
+
// document at the tab boundary so its contributed context always
|
|
397
|
+
// follows the tab the user actually chose.
|
|
398
|
+
if (params.kind === 'document') activateWorkspaceDocument(params.documentId);
|
|
399
|
+
}}
|
|
367
400
|
>
|
|
368
|
-
<DockviewDefaultTab
|
|
401
|
+
<DockviewDefaultTab
|
|
402
|
+
{...props}
|
|
403
|
+
hideClose={params.kind === 'document' || !params.closeable}
|
|
404
|
+
{...closeProps}
|
|
405
|
+
/>
|
|
406
|
+
{params.kind === 'document' && params.closeable && (
|
|
407
|
+
<Button
|
|
408
|
+
type="button"
|
|
409
|
+
variant="ghost"
|
|
410
|
+
size="compact"
|
|
411
|
+
className="vgai-dock-tab-close"
|
|
412
|
+
aria-label={`Close ${document?.title ?? params.documentId}`}
|
|
413
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
414
|
+
onClick={(event) => {
|
|
415
|
+
event.stopPropagation();
|
|
416
|
+
requestDocumentClose();
|
|
417
|
+
}}
|
|
418
|
+
>
|
|
419
|
+
<span aria-hidden="true">×</span>
|
|
420
|
+
</Button>
|
|
421
|
+
)}
|
|
369
422
|
{document?.dirty && (
|
|
370
423
|
<span className="vgai-dock-tab-dirty" title="Unsaved" aria-hidden="true" />
|
|
371
424
|
)}
|
|
@@ -395,7 +448,6 @@ function addStaticPanel(
|
|
|
395
448
|
title: options.title,
|
|
396
449
|
component: COMPONENT_ID,
|
|
397
450
|
tabComponent: TAB_COMPONENT_ID,
|
|
398
|
-
renderer: 'always',
|
|
399
451
|
inactive: options.inactive ?? false,
|
|
400
452
|
position: { referenceGroup: options.groupId },
|
|
401
453
|
params: {
|
|
@@ -680,11 +732,10 @@ function ensureUtilityPanel(
|
|
|
680
732
|
|
|
681
733
|
function reconcileUtilityPanels(api: DockviewApi): void {
|
|
682
734
|
const utilities = availableWorkspaceUtilities();
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}
|
|
735
|
+
// Every panel uses renderer:'always', so an unavailable registration's
|
|
736
|
+
// `UnavailableUtilityPanel` closes through the semantic controller. That
|
|
737
|
+
// path can restore the previous tab/collapsed state; deleting it here would
|
|
738
|
+
// discard that lifecycle information and leave an expanded empty drawer.
|
|
688
739
|
for (const utility of utilities) {
|
|
689
740
|
if (utility.visibleByDefault !== false) ensureUtilityPanel(api, utility);
|
|
690
741
|
}
|
|
@@ -701,9 +752,11 @@ function reconcileWorkspace(api: DockviewApi): void {
|
|
|
701
752
|
|
|
702
753
|
export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string) => void }) {
|
|
703
754
|
const store = useEditorStore();
|
|
755
|
+
const dockRootRef = useRef<HTMLElement | null>(null);
|
|
704
756
|
const apiRef = useRef<DockviewApi | null>(null);
|
|
705
757
|
const readyCleanupRef = useRef<(() => void) | null>(null);
|
|
706
758
|
const lastSyncedDocumentRef = useRef<string | null>(null);
|
|
759
|
+
const utilityRevealOriginsRef = useRef(new Map<string, UtilityRevealOrigin>());
|
|
707
760
|
const documentVersion = useSyncExternalStore(
|
|
708
761
|
subscribeWorkspaceDocuments,
|
|
709
762
|
workspaceDocumentRegistryVersion,
|
|
@@ -766,6 +819,15 @@ export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string
|
|
|
766
819
|
({ api }: { api: DockviewApi }) => {
|
|
767
820
|
apiRef.current = api;
|
|
768
821
|
reconcileWorkspace(api);
|
|
822
|
+
const dockRoot = dockRootRef.current;
|
|
823
|
+
const resizeObserver =
|
|
824
|
+
dockRoot && typeof ResizeObserver !== 'undefined'
|
|
825
|
+
? new ResizeObserver(([entry]) => {
|
|
826
|
+
if (!entry) return;
|
|
827
|
+
api.layout(entry.contentRect.width, entry.contentRect.height, true);
|
|
828
|
+
})
|
|
829
|
+
: null;
|
|
830
|
+
if (dockRoot) resizeObserver?.observe(dockRoot);
|
|
769
831
|
const uninstallPersistence = installWorkspaceStatePersistence(store, api, () =>
|
|
770
832
|
reconcileWorkspace(api),
|
|
771
833
|
);
|
|
@@ -774,8 +836,12 @@ export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string
|
|
|
774
836
|
if (initialDocumentId) api.getPanel(initialDocumentId)?.api.setActive();
|
|
775
837
|
|
|
776
838
|
const activeDisposable = api.onDidActivePanelChange((event) => {
|
|
777
|
-
if (
|
|
839
|
+
if (!event.panel) return;
|
|
778
840
|
const params = event.panel.api.getParameters<WorkspacePanelParams>();
|
|
841
|
+
// Dockview does not report every tab click with `origin: 'user'`
|
|
842
|
+
// (notably after an edge utility owned focus). The panel kind is the
|
|
843
|
+
// semantic boundary we actually need: document panels select the
|
|
844
|
+
// active document, while utilities/navigators leave it untouched.
|
|
779
845
|
if (params.kind === 'document') activateWorkspaceDocument(params.documentId);
|
|
780
846
|
});
|
|
781
847
|
|
|
@@ -797,6 +863,7 @@ export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string
|
|
|
797
863
|
ensureUtilityPanel(api, utility, false);
|
|
798
864
|
const panel = api.getPanel(`utility:${id}`);
|
|
799
865
|
if (!panel) return;
|
|
866
|
+
rememberUtilityReveal(panel, utilityRevealOriginsRef.current, id);
|
|
800
867
|
panel.api.setActive();
|
|
801
868
|
if (panel.api.location.type === 'edge') panel.api.group.api.expand();
|
|
802
869
|
},
|
|
@@ -811,10 +878,27 @@ export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string
|
|
|
811
878
|
if (panel.api.isActive && panel.api.location.type === 'edge' && !group.isCollapsed()) {
|
|
812
879
|
group.collapse();
|
|
813
880
|
} else {
|
|
881
|
+
rememberUtilityReveal(panel, utilityRevealOriginsRef.current, id);
|
|
814
882
|
panel.api.setActive();
|
|
815
883
|
if (panel.api.location.type === 'edge') group.expand();
|
|
816
884
|
}
|
|
817
885
|
},
|
|
886
|
+
closeUtility(id) {
|
|
887
|
+
const panel = api.getPanel(`utility:${id}`);
|
|
888
|
+
const origin = utilityRevealOriginsRef.current.get(id);
|
|
889
|
+
utilityRevealOriginsRef.current.delete(id);
|
|
890
|
+
if (!panel) return;
|
|
891
|
+
const wasActive = panel.api.isActive;
|
|
892
|
+
const group = panel.api.group;
|
|
893
|
+
const wasEdgePanel = panel.api.location.type === 'edge';
|
|
894
|
+
panel.api.close();
|
|
895
|
+
if (!wasActive) return;
|
|
896
|
+
const previous = origin?.previousPanelId
|
|
897
|
+
? api.getPanel(origin.previousPanelId)
|
|
898
|
+
: undefined;
|
|
899
|
+
previous?.api.setActive();
|
|
900
|
+
if (wasEdgePanel && (!previous || origin?.wasCollapsed !== false)) group.api.collapse();
|
|
901
|
+
},
|
|
818
902
|
showAuxiliary(id) {
|
|
819
903
|
if (id !== 'undo-history') return;
|
|
820
904
|
if (!api.getPanel(HISTORY_PANEL_ID)) {
|
|
@@ -847,6 +931,7 @@ export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string
|
|
|
847
931
|
const uninstallCommands = installWorkspaceDockCommands(commands);
|
|
848
932
|
readyCleanupRef.current?.();
|
|
849
933
|
readyCleanupRef.current = () => {
|
|
934
|
+
resizeObserver?.disconnect();
|
|
850
935
|
activeDisposable.dispose();
|
|
851
936
|
uninstallPersistence();
|
|
852
937
|
uninstallCommands();
|
|
@@ -861,6 +946,7 @@ export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string
|
|
|
861
946
|
|
|
862
947
|
return (
|
|
863
948
|
<main
|
|
949
|
+
ref={dockRootRef}
|
|
864
950
|
className="vgai-workspace-dock"
|
|
865
951
|
aria-label="Editor workspace"
|
|
866
952
|
data-testid="workspace-dock"
|
|
@@ -873,7 +959,7 @@ export function WorkspaceDock({ onOpenScene }: { onOpenScene: (assetPath: string
|
|
|
873
959
|
tabComponents={tabs}
|
|
874
960
|
onReady={onReady}
|
|
875
961
|
theme={vgaiDockviewTheme}
|
|
876
|
-
defaultRenderer="
|
|
962
|
+
defaultRenderer="onlyWhenVisible"
|
|
877
963
|
keyboardNavigation
|
|
878
964
|
disableFloatingGroups={false}
|
|
879
965
|
singleTabMode="fullwidth"
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* open-asset`) keeps its meaning unchanged:
|
|
13
13
|
*
|
|
14
14
|
* - project asset → the serving path itself (`/textures/crate.png`)
|
|
15
|
-
* -
|
|
15
|
+
* - Asset Editor → `asset-editor:entity:<entityId>`; the legacy entity editor key
|
|
16
|
+
* `asset-lab:entity:<entityId>` remains an input compatibility alias
|
|
16
17
|
* - online asset → `online:<source>:<id>`
|
|
17
18
|
*
|
|
18
19
|
* Titles are source-owned file names as authored (§8 — `crate.png`, never a
|
|
@@ -60,11 +61,13 @@ import './asset-workflow.css';
|
|
|
60
61
|
export interface AssetDocumentSpec {
|
|
61
62
|
readonly assetPath: string;
|
|
62
63
|
readonly kind: AssetKind;
|
|
63
|
-
/** Entity source for a session-only
|
|
64
|
+
/** Entity source for a session-only Asset Editor document. Never serialized. */
|
|
64
65
|
readonly entityId?: string;
|
|
65
66
|
/** Present while this document shows an online (not-yet-downloaded) asset. */
|
|
66
67
|
readonly online?: OnlineAssetInfo;
|
|
67
68
|
readonly unavailableReason?: string;
|
|
69
|
+
/** Incremented when the file bytes change at the same path. */
|
|
70
|
+
readonly revision?: number;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export interface AssetDocumentMutation {
|
|
@@ -81,6 +84,7 @@ const _specs =
|
|
|
81
84
|
const _pathAliases =
|
|
82
85
|
(hotData?.['vgai:asset-document-path-aliases'] as Map<string, string> | undefined) ??
|
|
83
86
|
new Map<string, string>();
|
|
87
|
+
const _focusReturnTargets = new Map<string, HTMLElement>();
|
|
84
88
|
if (hotData) {
|
|
85
89
|
// Workspace descriptors intentionally survive compatible Fast Refreshes.
|
|
86
90
|
// Their specs must survive with them or the next persistence snapshot drops
|
|
@@ -111,13 +115,20 @@ export function assetDocumentsVersion(): number {
|
|
|
111
115
|
|
|
112
116
|
/** The spec behind an open asset document id, or `undefined`. */
|
|
113
117
|
export function assetDocumentSpec(id: string): AssetDocumentSpec | undefined {
|
|
114
|
-
return _specs.get(id);
|
|
118
|
+
return _specs.get(migrateAssetEditorDocumentId(id));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Compatibility window: old session/control ids resolve to the canonical Asset Editor id. */
|
|
122
|
+
export function migrateAssetEditorDocumentId(id: string): string {
|
|
123
|
+
return id.startsWith('asset-lab:entity:')
|
|
124
|
+
? `asset-editor:entity:${id.slice('asset-lab:entity:'.length)}`
|
|
125
|
+
: id;
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
/** Whether `id` is one of this module's asset documents (used by
|
|
118
129
|
* `command-listener.ts` to report the legacy `activeTabKey` facet). */
|
|
119
130
|
export function isAssetDocumentId(id: string): boolean {
|
|
120
|
-
return _specs.has(id);
|
|
131
|
+
return _specs.has(migrateAssetEditorDocumentId(id));
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
/** The narrow store surface asset documents need (T6.3 gate hand-off). */
|
|
@@ -125,7 +136,7 @@ export interface AssetDocumentStore {
|
|
|
125
136
|
setActiveViewportTab(tab: 'scene' | 'game'): void;
|
|
126
137
|
}
|
|
127
138
|
|
|
128
|
-
/**
|
|
139
|
+
/** The Asset Editor additionally needs the live object map for its §8 title. */
|
|
129
140
|
export interface EntityAssetDocumentStore extends AssetDocumentStore {
|
|
130
141
|
readonly objectMap: Map<string, THREE.Object3D>;
|
|
131
142
|
}
|
|
@@ -137,6 +148,9 @@ function openSpecDocument(
|
|
|
137
148
|
spec: AssetDocumentSpec,
|
|
138
149
|
provenance?: WorkspaceDocumentDescriptor['provenance'],
|
|
139
150
|
): string {
|
|
151
|
+
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
|
|
152
|
+
_focusReturnTargets.set(id, document.activeElement);
|
|
153
|
+
}
|
|
140
154
|
if (!_specs.has(id)) {
|
|
141
155
|
_specs.set(id, spec);
|
|
142
156
|
notifyChanged();
|
|
@@ -151,6 +165,9 @@ function openSpecDocument(
|
|
|
151
165
|
onActivate: () => store.setActiveViewportTab('scene'),
|
|
152
166
|
onDispose: (documentId) => {
|
|
153
167
|
if (_specs.delete(documentId)) notifyChanged();
|
|
168
|
+
const focusTarget = _focusReturnTargets.get(documentId);
|
|
169
|
+
_focusReturnTargets.delete(documentId);
|
|
170
|
+
if (focusTarget?.isConnected) setTimeout(() => focusTarget.focus(), 0);
|
|
154
171
|
},
|
|
155
172
|
};
|
|
156
173
|
return openWorkspaceDocument(descriptor);
|
|
@@ -171,7 +188,7 @@ export function openAssetDocument(
|
|
|
171
188
|
return openSpecDocument(store, id, title, { assetPath, kind }, { sourcePath: assetPath });
|
|
172
189
|
}
|
|
173
190
|
|
|
174
|
-
/** Open the
|
|
191
|
+
/** Open the Asset Editor document for a live entity. */
|
|
175
192
|
export function openEntityAssetDocument(store: EntityAssetDocumentStore, entityId: string): void {
|
|
176
193
|
const object = store.objectMap.get(entityId);
|
|
177
194
|
if (!object) return;
|
|
@@ -182,7 +199,7 @@ export function openEntityAssetDocument(store: EntityAssetDocumentStore, entityI
|
|
|
182
199
|
const title = entity?.name ?? object.name ?? entityId;
|
|
183
200
|
openSpecDocument(
|
|
184
201
|
store,
|
|
185
|
-
`asset-
|
|
202
|
+
`asset-editor:entity:${entityId}`,
|
|
186
203
|
title,
|
|
187
204
|
{ assetPath: '', kind: 'model', entityId },
|
|
188
205
|
{ origin: `entity:${entityId}` },
|
|
@@ -221,6 +238,7 @@ export function resolveOnlineAssetDocument(id: string, localPath: string): void
|
|
|
221
238
|
export function __resetAssetDocumentsForTest(): void {
|
|
222
239
|
_specs.clear();
|
|
223
240
|
_pathAliases.clear();
|
|
241
|
+
_focusReturnTargets.clear();
|
|
224
242
|
notifyChanged();
|
|
225
243
|
}
|
|
226
244
|
|
|
@@ -252,6 +270,12 @@ export function applyAssetDocumentMutation(
|
|
|
252
270
|
unavailableReason: `The source ${spec.assetPath} was deleted. Undo the deletion or close this editor.`,
|
|
253
271
|
});
|
|
254
272
|
notifyChanged();
|
|
273
|
+
} else if (
|
|
274
|
+
detail.type === 'change' &&
|
|
275
|
+
detail.paths?.some((path) => normalizeAssetPath(path) === current)
|
|
276
|
+
) {
|
|
277
|
+
_specs.set(documentId, { ...spec, revision: (spec.revision ?? 0) + 1 });
|
|
278
|
+
notifyChanged();
|
|
255
279
|
}
|
|
256
280
|
}
|
|
257
281
|
|
|
@@ -321,26 +345,29 @@ function AssetViewerBody({
|
|
|
321
345
|
);
|
|
322
346
|
}
|
|
323
347
|
const title = spec.assetPath.split('/').pop() || spec.entityId || 'Asset';
|
|
348
|
+
const viewerKey = `${id}:${spec.revision ?? 0}`;
|
|
324
349
|
if (route === 'model')
|
|
325
350
|
return (
|
|
326
351
|
<ModelViewer
|
|
327
352
|
documentId={id}
|
|
353
|
+
key={viewerKey}
|
|
328
354
|
assetPath={spec.assetPath || undefined}
|
|
329
355
|
entityId={spec.entityId}
|
|
330
356
|
active={active}
|
|
331
357
|
/>
|
|
332
358
|
);
|
|
333
|
-
if (route === 'source')
|
|
359
|
+
if (route === 'source')
|
|
360
|
+
return <SourceAssetViewer key={viewerKey} assetPath={spec.assetPath} active={active} />;
|
|
334
361
|
let viewer: ReactNode;
|
|
335
362
|
switch (route) {
|
|
336
363
|
case 'image':
|
|
337
|
-
viewer = <ImageViewer assetPath={spec.assetPath} />;
|
|
364
|
+
viewer = <ImageViewer key={viewerKey} assetPath={spec.assetPath} />;
|
|
338
365
|
break;
|
|
339
366
|
case 'audio':
|
|
340
|
-
viewer = <AudioViewer assetPath={spec.assetPath} />;
|
|
367
|
+
viewer = <AudioViewer key={viewerKey} assetPath={spec.assetPath} />;
|
|
341
368
|
break;
|
|
342
369
|
case 'material':
|
|
343
|
-
viewer = <MaterialAssetEditor assetPath={spec.assetPath} documentId={id} />;
|
|
370
|
+
viewer = <MaterialAssetEditor key={viewerKey} assetPath={spec.assetPath} documentId={id} />;
|
|
344
371
|
break;
|
|
345
372
|
case 'input-map':
|
|
346
373
|
viewer = <InputMapViewer assetPath={spec.assetPath} documentId={id} />;
|