@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.
Files changed (137) hide show
  1. package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
  2. package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
  3. package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
  4. package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
  5. package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
  6. package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
  7. package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
  8. package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
  9. package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
  10. package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
  11. package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
  12. package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
  13. package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
  14. package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
  15. package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
  16. package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
  17. package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
  18. package/dist/assets/index-DiUacMXR.css +1 -0
  19. package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
  20. package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
  21. package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
  22. package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
  23. package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/dist-server/packaged.mjs +27125 -1075
  26. package/package.json +2 -2
  27. package/server/asset-history-snapshots.ts +189 -0
  28. package/server/asset-library-routes.ts +404 -53
  29. package/server/catalog-import-recipe.ts +97 -0
  30. package/server/catalog-reimport.ts +141 -0
  31. package/server/cloud-asset-catalog.ts +71 -5
  32. package/server/dev.ts +1 -1
  33. package/server/editor-server.ts +106 -51
  34. package/server/editor-sse.ts +5 -0
  35. package/server/generative-execution-context.ts +45 -0
  36. package/server/model-import-conversion.ts +345 -0
  37. package/server/packaged.ts +1 -1
  38. package/server/project-hmr-files.ts +3 -5
  39. package/server/project-output-writer.ts +440 -36
  40. package/server/project-tools.ts +529 -0
  41. package/server/server-utils.ts +14 -11
  42. package/src/EditorContext.tsx +4 -4
  43. package/src/action-registry.ts +12 -0
  44. package/src/asset-editor-context.tsx +9 -0
  45. package/src/asset-events.ts +3 -15
  46. package/src/asset-selection.ts +3 -0
  47. package/src/asset-workflow/asset-capabilities.ts +40 -0
  48. package/src/asset-workflow/asset-import-jobs.ts +108 -0
  49. package/src/asset-workflow/asset-library-query.ts +8 -1
  50. package/src/asset-workflow/asset-workflow-quality.ts +2 -0
  51. package/src/asset-workflow/import-contract.ts +2 -0
  52. package/src/asset-workflow/model-inspection.ts +92 -1
  53. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  54. package/src/asset-workflow/project-asset-commands.ts +1 -0
  55. package/src/asset-workflow/project-asset-health.ts +3 -6
  56. package/src/asset-workflow/project-asset-operations.ts +119 -4
  57. package/src/command-listener.ts +37 -9
  58. package/src/components/ApplicationMenus.tsx +13 -15
  59. package/src/components/AssetBrowser.tsx +301 -26
  60. package/src/components/AssetImportDetails.tsx +207 -92
  61. package/src/components/AssetImportJobStatus.tsx +55 -0
  62. package/src/components/CenterDocuments.tsx +28 -1
  63. package/src/components/CommandPalette.tsx +2 -2
  64. package/src/components/DefaultEditorLayout.tsx +4 -0
  65. package/src/components/InspectorToolSection.tsx +12 -10
  66. package/src/components/OnlineAssetBrowser.tsx +197 -11
  67. package/src/components/ToolHost.tsx +8 -8
  68. package/src/components/ViewportOverlay.tsx +13 -26
  69. package/src/components/ViewportPanel.tsx +19 -14
  70. package/src/components/ViewportShadingMenu.tsx +103 -0
  71. package/src/components/WorkspaceDock.tsx +103 -17
  72. package/src/components/asset-documents.tsx +38 -11
  73. package/src/components/asset-editor-persistence.ts +300 -5
  74. package/src/components/asset-selection-section.tsx +83 -9
  75. package/src/components/asset-viewers/AudioViewer.tsx +4 -7
  76. package/src/components/asset-viewers/ModelViewer.tsx +168 -5
  77. package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
  78. package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
  79. package/src/components/asset-workflow.css +30 -3
  80. package/src/components/primitives/editor-icons.ts +1 -0
  81. package/src/components/project-tool-documents.tsx +298 -0
  82. package/src/components/status-contributions.tsx +38 -0
  83. package/src/components/tool-documents.tsx +20 -17
  84. package/src/components/tool-schema-form.tsx +258 -0
  85. package/src/components/workspace-dock.css +16 -0
  86. package/src/editor-api.ts +159 -22
  87. package/src/editor-store.ts +6 -5
  88. package/src/editor-viewport.ts +12 -7
  89. package/src/game-viewport-state.ts +28 -0
  90. package/src/model-thumbnail.ts +97 -9
  91. package/src/play-mode.ts +30 -3
  92. package/src/project-tool-discovery.ts +5 -5
  93. package/src/{project-operations.ts → project-tools.ts} +39 -26
  94. package/src/scene-sync.ts +0 -67
  95. package/src/storage/handle-storage.ts +5 -1
  96. package/src/storage/http-storage.ts +7 -2
  97. package/src/storage/mem-storage.ts +7 -1
  98. package/src/storage/types.ts +2 -0
  99. package/src/theme-css.ts +17 -7
  100. package/src/tool-loader.ts +152 -241
  101. package/src/viewport-shading-boundary.ts +15 -0
  102. package/src/widgets/index.ts +2 -1
  103. package/src/workspace-dock-controller.ts +8 -0
  104. package/src/workspace-document-registry.ts +4 -4
  105. package/src/workspace-state-persistence.ts +36 -10
  106. package/template/.claude/skills/editor/SKILL.md +13 -3
  107. package/template/AGENTS.md +100 -83
  108. package/template/package.json +16 -0
  109. package/template/public/scenes/default.vscn.json +1 -2
  110. package/template/scripts/playtest.ts +7 -37
  111. package/template/server/rooms/game-room.ts +2 -2
  112. package/template/src/data/README.md +3 -2
  113. package/template/src/data/tuning.data.json +1 -3
  114. package/template/src/data/tuning.schema.json +0 -12
  115. package/template/src/data/tuning.schema.ts +2 -9
  116. package/template/src/data/tuning.ts +1 -1
  117. package/template/src/scripts/registry.ts +0 -3
  118. package/template/src/tools/README.md +151 -16
  119. package/template/src/tools/autoplay.tool.ts +31 -0
  120. package/template/src/tools/autoplay.ts +15 -0
  121. package/template/src/tools/tuning.document.tsx +107 -0
  122. package/template/src/tools/tuning.tool.ts +24 -0
  123. package/template/src/tools/tuning.ts +9 -0
  124. package/template/src/ui/Button.stories.tsx +1 -1
  125. package/template/tests/acceptance/example.spec.ts +4 -59
  126. package/template/vgai.game.json +1 -1
  127. package/dist/assets/index-bAGhEnnO.css +0 -1
  128. package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
  129. package/dist/project-scripts.bundle.js +0 -3917
  130. package/server/project-operations.ts +0 -343
  131. package/src/components/project-operation-documents.tsx +0 -226
  132. package/template/src/operations/README.md +0 -88
  133. package/template/src/scripts/components/data-spinner.ts +0 -86
  134. package/template/src/scripts/components/spin-lap-math.ts +0 -27
  135. package/template/src/tools/example.tool.tsx +0 -97
  136. package/template/src/tools/spin.tool.tsx +0 -101
  137. package/template/tests/logic/example.test.ts +0 -31
@@ -1,218 +1,144 @@
1
1
  /**
2
- * Project-tool loader W4/W3 of `docs/DATA-TOOLS-DESIGN.md` (§3.1 contract,
3
- * §3.5 discovery). The tool sibling of `script-loader.ts`: project
4
- * `src/tools/*.tool.tsx` modules are dynamically imported via Vite's `/@fs/`
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
- type GlobalToolPlacement,
49
- type InspectorToolProps,
50
- normalizeToolPlacement,
51
- type ToolConfig,
52
- } from '@engine/tools/define-tool';
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
- /** Fields shared by every successfully loaded project tool. */
61
- interface LoadedToolBase {
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
- /** `ToolConfig.title` — the tab label / section header (after ⚙, §3.5). */
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
- * One successfully loaded project-global tool. `placement` is kept AS
75
- * DECLARED (`'document'`, `'utility'`, or the deprecated `'dock'` alias —
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
- /** One successfully loaded inspector tool (W3), ready to register. */
86
- export interface InspectorTool extends LoadedToolBase {
87
- placement: 'inspector';
88
- /** The module's default export, rendered inside `InspectorToolSection`. */
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 ProjectTool = GlobalTool | InspectorTool;
47
+ export type LoadedToolContribution = GlobalToolContribution | InspectorToolContribution;
95
48
 
96
- const TEMPLATE_EXAMPLE = 'the template worked example: src/tools/example.tool.tsx';
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
- ): { tool: ProjectTool } | { error: string } {
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
- `[tools] ${file} has no default-exported React component — export default a plain ` +
144
- `function component next to \`tool\` (docs/DATA-TOOLS-DESIGN.md §3.1; see ` +
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 = { id: config.id, title: config.title, file, version };
149
- if (config.placement === 'inspector') {
150
- // Re-checked here (defineTool already validates) because a hand-rolled
151
- // `tool` object can bypass defineTool entirely.
152
- if (typeof config.match !== 'function') {
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
- `[tools] ${file} has placement 'inspector' but no \`match\` function — pass ` +
156
- `match: (node) => boolean in defineTool (docs/DATA-TOOLS-DESIGN.md §3.1; see ` +
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
- tool: {
85
+ contribution: {
162
86
  ...base,
163
- placement: 'inspector',
164
- Component: Component as ComponentType<InspectorToolProps>,
165
- match: config.match,
87
+ point: registration.point,
88
+ Component: Component as ComponentType<ToolInspectorContributionProps>,
89
+ match: match as ToolInspectorContributionMatch,
166
90
  },
167
91
  };
168
92
  }
169
- return { tool: { ...base, placement: config.placement, Component: Component as ComponentType } };
93
+ return {
94
+ contribution: {
95
+ ...base,
96
+ point: registration.point,
97
+ Component: Component as ComponentType<ToolContributionProps>,
98
+ },
99
+ };
170
100
  }
171
101
 
172
- // --- The live registry (subscribe/get store, `useSyncExternalStore`-shaped) ---
102
+ let globalContributions: GlobalToolContribution[] = [];
103
+ const listeners = new Set<() => void>();
173
104
 
174
- let _globalTools: GlobalTool[] = [];
175
- const _listeners = new Set<() => void>();
105
+ export function getGlobalToolContributions(): GlobalToolContribution[] {
106
+ return globalContributions;
107
+ }
176
108
 
177
- /** Every current project-global tool (stable reference between refreshes). */
178
- export function getGlobalTools(): GlobalTool[] {
179
- return _globalTools;
109
+ export function getDocumentToolContributions(): GlobalToolContribution[] {
110
+ return globalContributions.filter((item) => item.point === 'workspace.document');
180
111
  }
181
112
 
182
- /** Explicit document-placement tools: what opens as center documents.
183
- * Legacy `'dock'` preserves its original bottom-panel behavior. */
184
- export function getDocumentTools(): GlobalTool[] {
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
- /** Subscribe to global-tool registry changes. Returns an unsubscribe function. */
189
- export function subscribeGlobalTools(fn: () => void): () => void {
190
- _listeners.add(fn);
191
- return () => _listeners.delete(fn);
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
- // Inspector tools live in the SECTION registry, not a parallel store: each
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 registrationForInspectorTool(tool: InspectorTool): () => void {
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 tool.match(node, adapter);
210
- } catch (err) {
135
+ return item.match(node, adapter);
136
+ } catch (error) {
211
137
  if (!warned) {
212
138
  warned = true;
213
139
  teachingError(
214
- `[tools] ${tool.file}'s match() threw treating it as "no match" until the file ` +
215
- `is fixed and saved (docs/DATA-TOOLS-DESIGN.md §3.1).\n${String(err)}`,
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: tool.id,
224
- title: tool.title,
225
- file: tool.file,
226
- Component: tool.Component,
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 publishInspectorTools(tools: InspectorTool[]): void {
234
- for (const unregister of _unregisterInspectorTools) unregister();
235
- _unregisterInspectorTools = tools.map(registrationForInspectorTool);
160
+ function publishInspectorContributions(items: InspectorToolContribution[]): void {
161
+ for (const unregister of unregisterInspectorContributions) unregister();
162
+ unregisterInspectorContributions = items.map(registrationForInspectorContribution);
236
163
  }
237
164
 
238
- /**
239
- * Re-run the whole discovery pass: scan (`/__editor/tool-files`) import
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
- publishGlobalTools([]);
250
- publishInspectorTools([]);
169
+ publishGlobalContributions([]);
170
+ publishInspectorContributions([]);
251
171
  return;
252
172
  }
253
173
 
254
- let paths: string[];
174
+ let catalog: ProjectToolCatalog;
255
175
  try {
256
- const res = await fetch('/__editor/tool-files');
257
- if (!res.ok) return; // no dev server route (e.g. browser mode) — keep whatever we had
258
- const { files } = (await res.json()) as { files: Array<{ path: string }> };
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; // server unreachable — keep whatever we had
180
+ return;
262
181
  }
263
182
 
264
183
  const version = Date.now();
265
- const nextGlobal: GlobalTool[] = [];
266
- const nextInspector: InspectorTool[] = [];
267
- const seen = new Set<string>();
268
- for (const file of paths) {
269
- let mod: unknown;
270
- try {
271
- // Same `/@fs/` dynamic-import path as script-loader.ts — the module
272
- // arrives through Vite (transpiled, aliases resolved), and the `?t=`
273
- // bust guarantees a fresh evaluation on every refresh.
274
- mod = await import(/* @vite-ignore */ `/@fs/${project.rootPath}/${file}?t=${version}`);
275
- } catch (err) {
276
- // Import-time failure: a syntax error, or defineTool itself throwing
277
- // its teaching error surface it verbatim, name the file, keep the
278
- // editor alive.
279
- teachingError(
280
- `[tools] failed to load ${file} — fix the error below or delete the file ` +
281
- `(see ${TEMPLATE_EXAMPLE}). Skipped.\n${String(err)}`,
282
- );
283
- continue;
284
- }
285
- const result = extractProjectTool(mod, file, version);
286
- if ('error' in result) {
287
- teachingError(result.error);
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
- continue;
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
- publishGlobalTools(nextGlobal);
302
- publishInspectorTools(nextInspector);
219
+ publishGlobalContributions(nextGlobal);
220
+ publishInspectorContributions(nextInspector);
303
221
  }
304
222
 
305
- // Utility-placement and legacy dock tools hold live workspace-utility
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 publishGlobalTools(next: GlobalTool[]): void {
313
- _globalTools = next;
314
- for (const fn of _listeners) fn();
315
- for (const unregister of _unregisterUtilityTools) unregister();
316
- _unregisterUtilityTools = next
317
- .filter((t) => normalizeToolPlacement(t.placement) === 'utility')
318
- .map((t) =>
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:${t.id}`,
321
- title: t.title,
322
- Content: () => createElement(ToolHost, { tool: t, key: `${t.id}:${t.version}` }),
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
- /** Test-only publish seam exercises the same store/utility sync the real
328
- * discovery pass runs, without a dev server. */
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: deliberate, greppable — a broken project tool must be loud in the editor console without crashing the editor (§6.5 "errors teach"; mirrors data-asset.ts's hotSwap rejection)
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 refreshProjectTools();
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'];
@@ -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 a `src/tools/*.tool.tsx` file:
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 `'tool'` for project-authored document-placement
36
- * tools (§7.2). Extend the union when a new document kind genuinely ships;
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
- | 'operation' // project-local validated tooling operation
50
- | 'tool'; // project-authored document-placement tool (§7.2)
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`;