@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
package/src/tool-loader.ts
CHANGED
|
@@ -1,218 +1,144 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* path (the same loader pattern, cache-busted with `?t=` on every refresh so
|
|
6
|
-
* a re-discover always gets fresh code).
|
|
7
|
-
*
|
|
8
|
-
* The registry here is LIVE project state derived from the folder scan
|
|
9
|
-
* (`GET /__editor/tool-files`, editor-server.ts) — never a cached manifest
|
|
10
|
-
* (§3.5 rule 2). Project-global tools (`placement: 'document' | 'utility'`,
|
|
11
|
-
* plus the deprecated `'dock'` alias — widened in W0 of
|
|
12
|
-
* docs/EDITOR-WORKSPACE-SHELL-TODO.md §7.2) go to their SEMANTIC homes since
|
|
13
|
-
* W3 of that program (inventory rows T3/B12):
|
|
14
|
-
* - `document` tools open as CENTER workspace documents —
|
|
15
|
-
* `components/tool-documents.tsx` consumes the subscribe/get
|
|
16
|
-
* store below (`useSyncExternalStore`-shaped, mirroring `editorConsole`'s
|
|
17
|
-
* contract) and the palette lists one `Open Tool: <title>` action each;
|
|
18
|
-
* - `utility` tools and legacy `dock` tools are registered into the
|
|
19
|
-
* workspace-UTILITY registry by `publishGlobalTools` below.
|
|
20
|
-
* Discovery LIFECYCLE (when the pass runs) lives in
|
|
21
|
-
* `project-tool-discovery.ts`, started at editor init — not in any panel.
|
|
22
|
-
* Inspector tools (`placement: 'inspector'`, W3) are threaded into the
|
|
23
|
-
* PERMANENT section seam instead: each one becomes an
|
|
24
|
-
* `inspector-section-registry` registration (the same contract the
|
|
25
|
-
* first-party adapters use), wrapped in `InspectorToolSection`'s
|
|
26
|
-
* editor-styled chrome + error boundary. The registry's own change
|
|
27
|
-
* notification re-renders the inspector shell when tools (re)load.
|
|
28
|
-
*
|
|
29
|
-
* HMR: `dev.ts`'s `vgai-script-hmr` plugin already broadcasts
|
|
30
|
-
* `vgai:script-update` for every project `.tsx` change (the same event
|
|
31
|
-
* `play-mode.ts` reloads scripts from). The module-level listener at the
|
|
32
|
-
* bottom re-runs the whole discover+import pass whenever the changed file
|
|
33
|
-
* lives under `src/tools/` — Vite has already invalidated the changed
|
|
34
|
-
* module(s) in its graph by then, so the cache-busted re-import picks up
|
|
35
|
-
* edits to a tool AND to any helper module it imports, no editor reload.
|
|
36
|
-
*
|
|
37
|
-
* Failure physics (§6.5 "errors teach", §3's "never crashes the editor"):
|
|
38
|
-
* a tool file that fails to import, or that lacks the `tool`/default-component
|
|
39
|
-
* export pair, gets ONE console teaching error naming the file and citing the
|
|
40
|
-
* template example, and is skipped — every other tool still loads. A throwing
|
|
41
|
-
* inspector `match` is caught per call (logged once per load) and treated as
|
|
42
|
-
* "no match". Render-time crashes are the error boundary's job
|
|
43
|
-
* (`ToolHost.tsx` / `InspectorToolSection.tsx`), not ours.
|
|
2
|
+
* Browser loader for explicitly registered editor contributions attached to
|
|
3
|
+
* project tools. Callable discovery/execution stays Node-side; this module
|
|
4
|
+
* imports only contribution modules named by package.json metadata.
|
|
44
5
|
*/
|
|
45
6
|
|
|
46
7
|
import type { AuthoringAdapter, EditorNode } from '@engine/adapter';
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} from '@
|
|
8
|
+
import { EditorClient } from '@vgai/editor-sdk';
|
|
9
|
+
import type {
|
|
10
|
+
ToolContributionProps,
|
|
11
|
+
ToolInspectorContributionMatch,
|
|
12
|
+
ToolInspectorContributionProps,
|
|
13
|
+
} from '@vgai/editor-sdk/contributions';
|
|
53
14
|
import { type ComponentType, createElement } from 'react';
|
|
54
15
|
import { InspectorToolSection } from './components/InspectorToolSection';
|
|
55
16
|
import { ToolHost } from './components/ToolHost';
|
|
56
17
|
import { registerInspectorSections } from './inspector-section-registry';
|
|
57
18
|
import { getCurrentProject } from './project-manager';
|
|
19
|
+
import type {
|
|
20
|
+
ProjectToolCatalog,
|
|
21
|
+
ProjectToolCatalogEntry,
|
|
22
|
+
ProjectToolContribution,
|
|
23
|
+
} from './project-tools';
|
|
58
24
|
import { registerWorkspaceUtility } from './workspace-utility-registry';
|
|
59
25
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/** `ToolConfig.id` — the mount's `data-testid` suffix. */
|
|
26
|
+
interface LoadedToolContributionBase {
|
|
27
|
+
/** Tool-qualified contribution id; registration ids are local to one tool. */
|
|
63
28
|
id: string;
|
|
64
|
-
|
|
29
|
+
registrationId: string;
|
|
65
30
|
title: string;
|
|
66
|
-
/** Project-relative source file, e.g. `src/tools/example.tool.tsx`. */
|
|
67
31
|
file: string;
|
|
68
|
-
/** Bumped on every (re)load — keys the host so an HMR reload remounts the
|
|
69
|
-
* component fresh and clears any tripped error boundary. */
|
|
70
32
|
version: number;
|
|
33
|
+
tool: ProjectToolCatalogEntry;
|
|
71
34
|
}
|
|
72
35
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* workspace-shell TODO §7.2); `normalizeToolPlacement` decides the physical
|
|
77
|
-
* home: `document` → center document, `utility`/`dock` → workspace utility.
|
|
78
|
-
*/
|
|
79
|
-
export interface GlobalTool extends LoadedToolBase {
|
|
80
|
-
placement: GlobalToolPlacement;
|
|
81
|
-
/** The module's default export — plain React, rendered inside `ToolHost`. */
|
|
82
|
-
Component: ComponentType;
|
|
36
|
+
export interface GlobalToolContribution extends LoadedToolContributionBase {
|
|
37
|
+
point: 'workspace.document' | 'workspace.utility';
|
|
38
|
+
Component: ComponentType<ToolContributionProps>;
|
|
83
39
|
}
|
|
84
40
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Component: ComponentType<InspectorToolProps>;
|
|
90
|
-
/** The tool's own selection predicate (§3.1 — registry contract). */
|
|
91
|
-
match: (node: EditorNode | null, adapter: AuthoringAdapter) => boolean;
|
|
41
|
+
export interface InspectorToolContribution extends LoadedToolContributionBase {
|
|
42
|
+
point: 'selection.inspector';
|
|
43
|
+
Component: ComponentType<ToolInspectorContributionProps>;
|
|
44
|
+
match: ToolInspectorContributionMatch;
|
|
92
45
|
}
|
|
93
46
|
|
|
94
|
-
export type
|
|
47
|
+
export type LoadedToolContribution = GlobalToolContribution | InspectorToolContribution;
|
|
95
48
|
|
|
96
|
-
const TEMPLATE_EXAMPLE = '
|
|
49
|
+
const TEMPLATE_EXAMPLE = 'src/tools/tuning.document.tsx';
|
|
97
50
|
|
|
98
|
-
|
|
99
|
-
* Validate a dynamically imported tool module against the §3.1 contract:
|
|
100
|
-
* `export const tool = defineTool(…)` plus a default-exported React
|
|
101
|
-
* component (and, for `placement: 'inspector'`, a callable `match`). Pure
|
|
102
|
-
* and export-tested; returns either the loaded tool or the teaching error to
|
|
103
|
-
* print — the caller owns the console (`refreshProjectTools`) so tests don't
|
|
104
|
-
* have to spy on it.
|
|
105
|
-
*/
|
|
106
|
-
export function extractProjectTool(
|
|
51
|
+
export function extractProjectToolContribution(
|
|
107
52
|
mod: unknown,
|
|
108
53
|
file: string,
|
|
109
54
|
version: number,
|
|
110
|
-
|
|
55
|
+
tool: ProjectToolCatalogEntry,
|
|
56
|
+
registration: ProjectToolContribution,
|
|
57
|
+
): { contribution: LoadedToolContribution } | { error: string } {
|
|
111
58
|
const record = (mod ?? {}) as Record<string, unknown>;
|
|
112
|
-
const config = record['tool'] as ToolConfig | undefined;
|
|
113
|
-
if (
|
|
114
|
-
config === null ||
|
|
115
|
-
typeof config !== 'object' ||
|
|
116
|
-
typeof config.id !== 'string' ||
|
|
117
|
-
typeof config.title !== 'string'
|
|
118
|
-
) {
|
|
119
|
-
return {
|
|
120
|
-
error:
|
|
121
|
-
`[tools] ${file} does not export \`tool\` — every project tool exports ` +
|
|
122
|
-
`\`export const tool = defineTool({ id, title, placement: 'document' | 'inspector' | 'utility' })\` ` +
|
|
123
|
-
`(docs/DATA-TOOLS-DESIGN.md §3.1; see ${TEMPLATE_EXAMPLE}). Skipped.`,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
if (
|
|
127
|
-
config.placement !== 'document' &&
|
|
128
|
-
config.placement !== 'utility' &&
|
|
129
|
-
config.placement !== 'dock' &&
|
|
130
|
-
config.placement !== 'inspector'
|
|
131
|
-
) {
|
|
132
|
-
return {
|
|
133
|
-
error:
|
|
134
|
-
`[tools] ${file} has placement ${JSON.stringify((config as { placement?: unknown }).placement)} — ` +
|
|
135
|
-
`the placements are 'document', 'inspector' and 'utility' ('dock' is a deprecated ` +
|
|
136
|
-
`bottom-panel compatibility alias — docs/EDITOR-WORKSPACE-SHELL-TODO.md §7.2). Skipped.`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
59
|
const Component = record['default'];
|
|
140
60
|
if (typeof Component !== 'function') {
|
|
141
61
|
return {
|
|
142
62
|
error:
|
|
143
|
-
`[
|
|
144
|
-
`
|
|
145
|
-
`${TEMPLATE_EXAMPLE}). Skipped.`,
|
|
63
|
+
`[tool contributions] ${file} has no default-exported React component ` +
|
|
64
|
+
`(see ${TEMPLATE_EXAMPLE}). Skipped.`,
|
|
146
65
|
};
|
|
147
66
|
}
|
|
148
|
-
const base = {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
67
|
+
const base = {
|
|
68
|
+
id: `${tool.name}:${registration.id}`,
|
|
69
|
+
registrationId: registration.id,
|
|
70
|
+
title: registration.title,
|
|
71
|
+
file,
|
|
72
|
+
version,
|
|
73
|
+
tool,
|
|
74
|
+
};
|
|
75
|
+
if (registration.point === 'selection.inspector') {
|
|
76
|
+
const match = record['match'];
|
|
77
|
+
if (typeof match !== 'function') {
|
|
153
78
|
return {
|
|
154
79
|
error:
|
|
155
|
-
`[
|
|
156
|
-
`match
|
|
157
|
-
`${TEMPLATE_EXAMPLE}). Skipped.`,
|
|
80
|
+
`[tool contributions] ${file} targets 'selection.inspector' but does not export ` +
|
|
81
|
+
'`match(node, adapter)`. Skipped.',
|
|
158
82
|
};
|
|
159
83
|
}
|
|
160
84
|
return {
|
|
161
|
-
|
|
85
|
+
contribution: {
|
|
162
86
|
...base,
|
|
163
|
-
|
|
164
|
-
Component: Component as ComponentType<
|
|
165
|
-
match:
|
|
87
|
+
point: registration.point,
|
|
88
|
+
Component: Component as ComponentType<ToolInspectorContributionProps>,
|
|
89
|
+
match: match as ToolInspectorContributionMatch,
|
|
166
90
|
},
|
|
167
91
|
};
|
|
168
92
|
}
|
|
169
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
contribution: {
|
|
95
|
+
...base,
|
|
96
|
+
point: registration.point,
|
|
97
|
+
Component: Component as ComponentType<ToolContributionProps>,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
170
100
|
}
|
|
171
101
|
|
|
172
|
-
|
|
102
|
+
let globalContributions: GlobalToolContribution[] = [];
|
|
103
|
+
const listeners = new Set<() => void>();
|
|
173
104
|
|
|
174
|
-
|
|
175
|
-
|
|
105
|
+
export function getGlobalToolContributions(): GlobalToolContribution[] {
|
|
106
|
+
return globalContributions;
|
|
107
|
+
}
|
|
176
108
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return _globalTools;
|
|
109
|
+
export function getDocumentToolContributions(): GlobalToolContribution[] {
|
|
110
|
+
return globalContributions.filter((item) => item.point === 'workspace.document');
|
|
180
111
|
}
|
|
181
112
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return _globalTools.filter((t) => normalizeToolPlacement(t.placement) === 'document');
|
|
113
|
+
export function subscribeToolContributions(fn: () => void): () => void {
|
|
114
|
+
listeners.add(fn);
|
|
115
|
+
return () => listeners.delete(fn);
|
|
186
116
|
}
|
|
187
117
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
118
|
+
let editorClient: EditorClient | null = null;
|
|
119
|
+
|
|
120
|
+
export function getToolContributionClient(): EditorClient {
|
|
121
|
+
if (!editorClient) {
|
|
122
|
+
const origin = globalThis.location?.origin;
|
|
123
|
+
editorClient = origin ? new EditorClient({ url: origin }) : new EditorClient();
|
|
124
|
+
}
|
|
125
|
+
return editorClient;
|
|
192
126
|
}
|
|
193
127
|
|
|
194
|
-
|
|
195
|
-
// loaded tool holds one `registerInspectorSections` registration, replaced
|
|
196
|
-
// wholesale on every refresh (fresh Section identity per load = remount =
|
|
197
|
-
// tripped error boundaries clear on HMR, same physics as the dock's
|
|
198
|
-
// version-keyed `ToolHost`).
|
|
199
|
-
let _unregisterInspectorTools: Array<() => void> = [];
|
|
128
|
+
let unregisterInspectorContributions: Array<() => void> = [];
|
|
200
129
|
|
|
201
|
-
function
|
|
202
|
-
// A project `match` that throws must not take the inspector shell down —
|
|
203
|
-
// it runs inside `Inspector.tsx`'s render. Teach once per load, then
|
|
204
|
-
// treat as "no match" (the §6.5 failure physics, at match granularity).
|
|
130
|
+
function registrationForInspectorContribution(item: InspectorToolContribution): () => void {
|
|
205
131
|
let warned = false;
|
|
206
132
|
return registerInspectorSections({
|
|
207
|
-
match: (node, adapter) => {
|
|
133
|
+
match: (node: EditorNode | null, adapter: AuthoringAdapter) => {
|
|
208
134
|
try {
|
|
209
|
-
return
|
|
210
|
-
} catch (
|
|
135
|
+
return item.match(node, adapter);
|
|
136
|
+
} catch (error) {
|
|
211
137
|
if (!warned) {
|
|
212
138
|
warned = true;
|
|
213
139
|
teachingError(
|
|
214
|
-
`[
|
|
215
|
-
`is fixed and saved
|
|
140
|
+
`[tool contributions] ${item.file}'s match() threw; treating it as no match until ` +
|
|
141
|
+
`the file is fixed and saved.\n${String(error)}`,
|
|
216
142
|
);
|
|
217
143
|
}
|
|
218
144
|
return false;
|
|
@@ -220,128 +146,113 @@ function registrationForInspectorTool(tool: InspectorTool): () => void {
|
|
|
220
146
|
},
|
|
221
147
|
Section: ({ adapter, nodeId }) =>
|
|
222
148
|
createElement(InspectorToolSection, {
|
|
223
|
-
id:
|
|
224
|
-
title:
|
|
225
|
-
file:
|
|
226
|
-
|
|
149
|
+
id: item.id,
|
|
150
|
+
title: item.title,
|
|
151
|
+
file: item.file,
|
|
152
|
+
tool: item.tool,
|
|
153
|
+
Component: item.Component,
|
|
227
154
|
adapter,
|
|
228
155
|
nodeId,
|
|
229
156
|
}),
|
|
230
157
|
});
|
|
231
158
|
}
|
|
232
159
|
|
|
233
|
-
function
|
|
234
|
-
for (const unregister of
|
|
235
|
-
|
|
160
|
+
function publishInspectorContributions(items: InspectorToolContribution[]): void {
|
|
161
|
+
for (const unregister of unregisterInspectorContributions) unregister();
|
|
162
|
+
unregisterInspectorContributions = items.map(registrationForInspectorContribution);
|
|
236
163
|
}
|
|
237
164
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
* each module fresh (`/@fs/` + `?t=` cache-bust) → validate → publish (dock
|
|
241
|
-
* store + inspector-section registrations). Cheap by design (a project has
|
|
242
|
-
* a handful of tools), so it doubles as the v1 HMR path — see the module
|
|
243
|
-
* doc comment. Never throws: per-file failures are console teaching errors
|
|
244
|
-
* and the file is skipped.
|
|
245
|
-
*/
|
|
246
|
-
export async function refreshProjectTools(): Promise<void> {
|
|
165
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one bounded load pass preserves the catalog order while routing three explicit contribution points.
|
|
166
|
+
export async function refreshProjectToolContributions(): Promise<void> {
|
|
247
167
|
const project = getCurrentProject();
|
|
248
168
|
if (!project) {
|
|
249
|
-
|
|
250
|
-
|
|
169
|
+
publishGlobalContributions([]);
|
|
170
|
+
publishInspectorContributions([]);
|
|
251
171
|
return;
|
|
252
172
|
}
|
|
253
173
|
|
|
254
|
-
let
|
|
174
|
+
let catalog: ProjectToolCatalog;
|
|
255
175
|
try {
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
258
|
-
|
|
259
|
-
paths = files.map((f) => f.path);
|
|
176
|
+
const response = await fetch('/__editor/project-tools');
|
|
177
|
+
if (!response.ok) return;
|
|
178
|
+
catalog = (await response.json()) as ProjectToolCatalog;
|
|
260
179
|
} catch {
|
|
261
|
-
return;
|
|
180
|
+
return;
|
|
262
181
|
}
|
|
263
182
|
|
|
264
183
|
const version = Date.now();
|
|
265
|
-
const nextGlobal:
|
|
266
|
-
const nextInspector:
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (seen.has(result.tool.id)) {
|
|
291
|
-
teachingError(
|
|
292
|
-
`[tools] ${file} reuses tool id "${result.tool.id}" — ids must be unique per project ` +
|
|
293
|
-
`(they key the dock tabs and inspector sections). Skipped.`,
|
|
184
|
+
const nextGlobal: GlobalToolContribution[] = [];
|
|
185
|
+
const nextInspector: InspectorToolContribution[] = [];
|
|
186
|
+
for (const tool of catalog.tools) {
|
|
187
|
+
for (const registration of tool.contributions) {
|
|
188
|
+
const modulePath = registration.entryPath.startsWith('/')
|
|
189
|
+
? registration.entryPath
|
|
190
|
+
: `${project.rootPath}/${registration.entryPath}`;
|
|
191
|
+
let mod: unknown;
|
|
192
|
+
try {
|
|
193
|
+
mod = await import(/* @vite-ignore */ `/@fs/${modulePath}?t=${version}`);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
teachingError(
|
|
196
|
+
`[tool contributions] failed to load ${registration.entryPath}; fix the module or ` +
|
|
197
|
+
`remove its contribution registration.\n${String(error)}`,
|
|
198
|
+
);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const result = extractProjectToolContribution(
|
|
202
|
+
mod,
|
|
203
|
+
registration.entryPath,
|
|
204
|
+
version,
|
|
205
|
+
tool,
|
|
206
|
+
registration,
|
|
294
207
|
);
|
|
295
|
-
|
|
208
|
+
if ('error' in result) {
|
|
209
|
+
teachingError(result.error);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (result.contribution.point === 'selection.inspector') {
|
|
213
|
+
nextInspector.push(result.contribution);
|
|
214
|
+
} else {
|
|
215
|
+
nextGlobal.push(result.contribution);
|
|
216
|
+
}
|
|
296
217
|
}
|
|
297
|
-
seen.add(result.tool.id);
|
|
298
|
-
if (result.tool.placement === 'inspector') nextInspector.push(result.tool);
|
|
299
|
-
else nextGlobal.push(result.tool);
|
|
300
218
|
}
|
|
301
|
-
|
|
302
|
-
|
|
219
|
+
publishGlobalContributions(nextGlobal);
|
|
220
|
+
publishInspectorContributions(nextInspector);
|
|
303
221
|
}
|
|
304
222
|
|
|
305
|
-
|
|
306
|
-
// registrations, replaced wholesale on every refresh — unregister-first is
|
|
307
|
-
// the registry's own duplicate-id discipline, and a fresh Content per load
|
|
308
|
-
// re-keys ToolHost so HMR clears a tripped error boundary (same physics as
|
|
309
|
-
// the inspector-tool registrations above).
|
|
310
|
-
let _unregisterUtilityTools: Array<() => void> = [];
|
|
223
|
+
let unregisterUtilityContributions: Array<() => void> = [];
|
|
311
224
|
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
for (const fn of
|
|
315
|
-
for (const unregister of
|
|
316
|
-
|
|
317
|
-
.filter((
|
|
318
|
-
.map((
|
|
225
|
+
function publishGlobalContributions(next: GlobalToolContribution[]): void {
|
|
226
|
+
globalContributions = next;
|
|
227
|
+
for (const fn of listeners) fn();
|
|
228
|
+
for (const unregister of unregisterUtilityContributions) unregister();
|
|
229
|
+
unregisterUtilityContributions = next
|
|
230
|
+
.filter((item) => item.point === 'workspace.utility')
|
|
231
|
+
.map((item) =>
|
|
319
232
|
registerWorkspaceUtility({
|
|
320
|
-
id: `tool:${
|
|
321
|
-
title:
|
|
322
|
-
Content: () =>
|
|
233
|
+
id: `tool:${item.id}`,
|
|
234
|
+
title: item.title,
|
|
235
|
+
Content: () =>
|
|
236
|
+
createElement(ToolHost, {
|
|
237
|
+
contribution: item,
|
|
238
|
+
key: `${item.id}:${item.version}`,
|
|
239
|
+
}),
|
|
323
240
|
}),
|
|
324
241
|
);
|
|
325
242
|
}
|
|
326
243
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
export function __publishGlobalToolsForTest(next: GlobalTool[]): void {
|
|
330
|
-
publishGlobalTools(next);
|
|
244
|
+
export function __publishGlobalToolContributionsForTest(next: GlobalToolContribution[]): void {
|
|
245
|
+
publishGlobalContributions(next);
|
|
331
246
|
}
|
|
332
247
|
|
|
333
248
|
function teachingError(message: string): void {
|
|
334
|
-
// biome-ignore lint/suspicious/noConsole:
|
|
249
|
+
// biome-ignore lint/suspicious/noConsole: broken project contributions must be loud without crashing the editor
|
|
335
250
|
console.error(message);
|
|
336
251
|
}
|
|
337
252
|
|
|
338
|
-
// --- HMR: re-discover on any src/tools/ change (see module doc comment) ---
|
|
339
253
|
if (import.meta.hot) {
|
|
340
254
|
import.meta.hot.on('vgai:script-update', (data: { file: string }) => {
|
|
341
|
-
// `data.file` is absolute and forward-slash normalized (dev.ts). Any
|
|
342
|
-
// change under src/tools/ — the tool itself OR a helper it imports —
|
|
343
|
-
// triggers a full cheap refresh; Vite already invalidated the module.
|
|
344
255
|
if (!data.file.includes('/src/tools/')) return;
|
|
345
|
-
void
|
|
256
|
+
void refreshProjectToolContributions();
|
|
346
257
|
});
|
|
347
258
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ViewportShadingRenderer } from '@engine/render/viewport-shading';
|
|
2
|
+
import type * as THREE from 'three';
|
|
3
|
+
import { EDITOR_LAYER } from './editor-layers';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scene diagnostic shading targets authored/runtime meshes only. Editor
|
|
7
|
+
* infrastructure shares the native scene for authoring, but its custom
|
|
8
|
+
* gizmo materials are part of the editor implementation and must stay native.
|
|
9
|
+
*/
|
|
10
|
+
export const isEditorViewportShadingTarget: Parameters<ViewportShadingRenderer['render']>[3] = (
|
|
11
|
+
mesh: THREE.Mesh,
|
|
12
|
+
) =>
|
|
13
|
+
!mesh.layers.isEnabled(EDITOR_LAYER) &&
|
|
14
|
+
!mesh.userData['editorHelper'] &&
|
|
15
|
+
!mesh.userData['engineInternal'];
|
package/src/widgets/index.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* the exact components its inspector renders — so a project tool built from
|
|
7
7
|
* them looks native to the editor and re-skins with it.
|
|
8
8
|
*
|
|
9
|
-
* Import it from
|
|
9
|
+
* Import it from an editor contribution such as
|
|
10
|
+
* `src/tools/tuning.document.tsx`:
|
|
10
11
|
*
|
|
11
12
|
* ```tsx
|
|
12
13
|
* import { Inline, NumberInput, SectionHeader, themeVars } from '@editor/widgets';
|
|
@@ -11,6 +11,10 @@ export interface WorkspaceDockCommands {
|
|
|
11
11
|
showNavigator(id: 'hierarchy' | 'assets' | 'asset-library'): void;
|
|
12
12
|
showUtility(id: string): void;
|
|
13
13
|
toggleUtility(id: string): void;
|
|
14
|
+
/** Remove a transient utility from its current Dockview location. Hosts
|
|
15
|
+
* restore the utility that was active before it was revealed, or collapse
|
|
16
|
+
* an edge drawer that was opened only for this utility. */
|
|
17
|
+
closeUtility?(id: string): void;
|
|
14
18
|
showAuxiliary(id: 'undo-history'): void;
|
|
15
19
|
toggleFocus(): void;
|
|
16
20
|
resetLayout(): void;
|
|
@@ -46,6 +50,10 @@ export function toggleWorkspaceUtility(id: string): void {
|
|
|
46
50
|
invoke((value) => value.toggleUtility(id));
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
export function closeWorkspaceUtility(id: string): void {
|
|
54
|
+
invoke((value) => value.closeUtility?.(id));
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
export function showWorkspaceAuxiliary(id: 'undo-history'): void {
|
|
50
58
|
invoke((value) => value.showAuxiliary(id));
|
|
51
59
|
}
|
|
@@ -32,8 +32,8 @@ import type { ComponentType } from 'react';
|
|
|
32
32
|
/**
|
|
33
33
|
* Semantic document kinds — §7.1 "title, semantic kind, and optional
|
|
34
34
|
* source/provenance metadata". One entry per real first-party implementer
|
|
35
|
-
* §7.1 enumerates, plus
|
|
36
|
-
*
|
|
35
|
+
* §7.1 enumerates, plus distinct generic project-tool runners and optional
|
|
36
|
+
* project-authored tool surfaces. Extend the union when a new document kind genuinely ships;
|
|
37
37
|
* the registry itself never branches on it.
|
|
38
38
|
*/
|
|
39
39
|
export type WorkspaceDocumentKind =
|
|
@@ -46,8 +46,8 @@ export type WorkspaceDocumentKind =
|
|
|
46
46
|
| 'data' // data asset form/table document (§5)
|
|
47
47
|
| 'content-browser' // optional thumbnail-heavy browser (§5)
|
|
48
48
|
| 'build-profiles' // build configuration (§8: 'Build Profiles')
|
|
49
|
-
| '
|
|
50
|
-
| 'tool'; // project
|
|
49
|
+
| 'project-tool' // registered callable using the generic runner
|
|
50
|
+
| 'tool-contribution'; // optional React presentation for a registered project tool
|
|
51
51
|
|
|
52
52
|
/** Where a document came from — §7.1 "optional source/provenance metadata".
|
|
53
53
|
* All fields optional: a Game preview has none; a scene has a `sourcePath`;
|