@volter/editor-sdk 0.5.57
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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Browser fetch owns connections and has no Node header timeout. */
|
|
2
|
+
import type { Dispatcher } from 'undici';
|
|
3
|
+
|
|
4
|
+
export function createDispatcher(_timeoutMs: number): undefined {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function dispatchFetch(
|
|
9
|
+
url: string,
|
|
10
|
+
init?: RequestInit,
|
|
11
|
+
_dispatcher?: Dispatcher,
|
|
12
|
+
): Promise<Response> {
|
|
13
|
+
return fetch(url, init);
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Node session clients need explicit header deadlines for long modeling calls.
|
|
2
|
+
* Some runtimes supply an undici-compatible Agent without destroy(); callers
|
|
3
|
+
* close explicitly when that lifecycle method exists. */
|
|
4
|
+
import { Agent, fetch as nodeFetch, type Dispatcher } from 'undici';
|
|
5
|
+
|
|
6
|
+
export function createDispatcher(timeoutMs: number): Dispatcher {
|
|
7
|
+
return new Agent({ headersTimeout: timeoutMs, bodyTimeout: timeoutMs });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function dispatchFetch(
|
|
11
|
+
url: string,
|
|
12
|
+
init?: RequestInit,
|
|
13
|
+
dispatcher?: Dispatcher,
|
|
14
|
+
): Promise<Response> {
|
|
15
|
+
if (!dispatcher) return fetch(url, init);
|
|
16
|
+
return await nodeFetch(url, {
|
|
17
|
+
...(init as object), dispatcher,
|
|
18
|
+
}) as unknown as Response;
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
GenerationBilling,
|
|
3
|
+
GenerationJob,
|
|
4
|
+
GenerationJobStatus,
|
|
5
|
+
GenerationJobsDocument,
|
|
6
|
+
} from '@volter/editor-sdk/generations';
|
|
7
|
+
export type {
|
|
8
|
+
EditorEnvelopeObservation,
|
|
9
|
+
GameDebugDoor,
|
|
10
|
+
HistoryStep,
|
|
11
|
+
OpenedDocument,
|
|
12
|
+
} from './client.js';
|
|
13
|
+
export { EditorClient, EditorCommandError } from './client.js';
|
|
14
|
+
export type {
|
|
15
|
+
ToolAssetInspectorContributionMatch,
|
|
16
|
+
ToolAssetInspectorContributionProps,
|
|
17
|
+
ToolContributionAsset,
|
|
18
|
+
ToolContributionNode,
|
|
19
|
+
ToolContributionProps,
|
|
20
|
+
ToolGenerationResultContributionProps,
|
|
21
|
+
ToolInspectorContributionMatch,
|
|
22
|
+
ToolInspectorContributionProps,
|
|
23
|
+
} from './contributions.js';
|
|
24
|
+
export type {
|
|
25
|
+
DocumentClickStep,
|
|
26
|
+
DocumentKeyStep,
|
|
27
|
+
DocumentPasteStep,
|
|
28
|
+
DocumentProbeResult,
|
|
29
|
+
DocumentProbeScope,
|
|
30
|
+
DocumentProbeStep,
|
|
31
|
+
DocumentQueryStep,
|
|
32
|
+
DocumentSelectStep,
|
|
33
|
+
DocumentTypeStep,
|
|
34
|
+
ProbedElement,
|
|
35
|
+
} from './document-probe.js';
|
|
36
|
+
export { editorViewFromUrl, editorViewUrl } from './editor-view.js';
|
|
37
|
+
export type { ExtensionContributionState } from './extension.js';
|
|
38
|
+
export type {
|
|
39
|
+
ActiveDocumentCapture,
|
|
40
|
+
AssetCompareCapture,
|
|
41
|
+
AssetCompareOptions,
|
|
42
|
+
AssetCompareView,
|
|
43
|
+
AssetKind,
|
|
44
|
+
AssetPreviewBackground,
|
|
45
|
+
AssetPreviewCameraChoice,
|
|
46
|
+
AssetPreviewCapture,
|
|
47
|
+
AssetPreviewOptions,
|
|
48
|
+
AssetPreviewOrientation,
|
|
49
|
+
AssetPreviewPose,
|
|
50
|
+
AssetPreviewShotSetDefinition,
|
|
51
|
+
AssetPreviewShotWarning,
|
|
52
|
+
AssetPreviewSource,
|
|
53
|
+
AssetPreviewStage,
|
|
54
|
+
AssetPreviewView,
|
|
55
|
+
CaptureDimensions,
|
|
56
|
+
DocumentCameraPose,
|
|
57
|
+
DocumentLookOutcome,
|
|
58
|
+
DocumentTableEntryProjection,
|
|
59
|
+
DocumentTableProjection,
|
|
60
|
+
EditorCameraState,
|
|
61
|
+
EditorChromeCapture,
|
|
62
|
+
EditorChromeCaptureOptions,
|
|
63
|
+
EditorEntitySummary,
|
|
64
|
+
EditorState,
|
|
65
|
+
EditorView,
|
|
66
|
+
EditorViewBuiltInUtilityId,
|
|
67
|
+
EditorViewDocument,
|
|
68
|
+
EditorViewUtility,
|
|
69
|
+
EditorWorkspaceName,
|
|
70
|
+
GameCapture,
|
|
71
|
+
GameplayRecordingCapture,
|
|
72
|
+
GameplayRecordingOptions,
|
|
73
|
+
GameplayRecordingStarted,
|
|
74
|
+
GameplayRecordingTimeline,
|
|
75
|
+
GameplayReplayCapture,
|
|
76
|
+
HelperVisibility,
|
|
77
|
+
InspectedAction,
|
|
78
|
+
InspectedField,
|
|
79
|
+
InspectedFieldWrite,
|
|
80
|
+
InspectedHierarchy,
|
|
81
|
+
InspectedHierarchyRow,
|
|
82
|
+
InspectedInspection,
|
|
83
|
+
InspectedNothing,
|
|
84
|
+
InspectedSection,
|
|
85
|
+
InspectedSectionBody,
|
|
86
|
+
InspectedSubject,
|
|
87
|
+
InspectedWriteDestination,
|
|
88
|
+
InspectionPresentationKind,
|
|
89
|
+
InspectionSurface,
|
|
90
|
+
LabeledShotSetCapture,
|
|
91
|
+
PlayRecordingStatus,
|
|
92
|
+
PlayStarted,
|
|
93
|
+
PresentedEditorView,
|
|
94
|
+
ProjectInfo,
|
|
95
|
+
ProjectTemplate,
|
|
96
|
+
ProjectToolCatalog,
|
|
97
|
+
ProjectToolCatalogEntry,
|
|
98
|
+
ProjectToolContribution,
|
|
99
|
+
ProjectToolOutcome,
|
|
100
|
+
RecentProject,
|
|
101
|
+
ShadingMode,
|
|
102
|
+
ShotSetPoseMorph,
|
|
103
|
+
ShotSetPoseRotation,
|
|
104
|
+
ShotSetPoseStep,
|
|
105
|
+
ShotSetPoseTranslation,
|
|
106
|
+
ShotSetShot,
|
|
107
|
+
StoryCaptureOptions,
|
|
108
|
+
StoryVariantCapture,
|
|
109
|
+
StoryVariantImage,
|
|
110
|
+
StructureOp,
|
|
111
|
+
StructureOpOptions,
|
|
112
|
+
StructureOpResult,
|
|
113
|
+
TransformMode,
|
|
114
|
+
TransformSpace,
|
|
115
|
+
Vec3Value,
|
|
116
|
+
ViewPreset,
|
|
117
|
+
ViewportCapture,
|
|
118
|
+
ViewportTab,
|
|
119
|
+
} from './types.js';
|
|
120
|
+
export {
|
|
121
|
+
EDITOR_VIEW_BUILT_IN_UTILITY_IDS,
|
|
122
|
+
EDITOR_VIEW_DOCUMENT_KINDS,
|
|
123
|
+
EDITOR_VIEW_KEYS,
|
|
124
|
+
EDITOR_VIEW_TOOL_UTILITY_PREFIX,
|
|
125
|
+
EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS,
|
|
126
|
+
isEditorViewDocumentKind,
|
|
127
|
+
isEditorViewUtility,
|
|
128
|
+
} from './types.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { WorkspaceArrangement } from '@volter/editor-project/adapter/workspace-arrangement';
|
|
2
|
+
export type { WorkspaceArrangement, WorkspaceAreaContribution } from '@volter/editor-project/adapter/workspace-arrangement';
|
|
3
|
+
|
|
4
|
+
export const GameArrangement: WorkspaceArrangement = { id: 'game', title: 'Game' };
|
|
5
|
+
export const DesignArrangement: WorkspaceArrangement = { id: 'design', title: 'Design' };
|
package/src/layouts.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Programmable editor composition. Layouts are React components, not registry keys. */
|
|
2
|
+
import { type ComponentType, type CSSProperties, createElement, type ReactNode } from 'react';
|
|
3
|
+
import { DesignArrangement, type WorkspaceArrangement } from './layout-arrangements';
|
|
4
|
+
import type { EditorView } from './types';
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
DesignArrangement,
|
|
8
|
+
GameArrangement,
|
|
9
|
+
type WorkspaceArrangement,
|
|
10
|
+
} from './layout-arrangements';
|
|
11
|
+
|
|
12
|
+
export interface EditorLayoutProps {
|
|
13
|
+
readonly playing: boolean;
|
|
14
|
+
readonly paused: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type EditorLayout = ComponentType<EditorLayoutProps>;
|
|
17
|
+
export interface LayoutFrameProps {
|
|
18
|
+
readonly children?: ReactNode;
|
|
19
|
+
readonly style?: CSSProperties;
|
|
20
|
+
/** Let the enclosing web page scroll when the pointer is over a viewport. */
|
|
21
|
+
readonly pageScroll?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface WorkspaceProps {
|
|
24
|
+
readonly arrangement?: WorkspaceArrangement;
|
|
25
|
+
readonly immersivePlay?: boolean;
|
|
26
|
+
readonly playUtilities?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
export interface DocumentHandle {
|
|
29
|
+
/** Select exactly one named native object; ambiguous/missing names refuse. */
|
|
30
|
+
select(name: string): void;
|
|
31
|
+
transform(mode: 'combined' | 'translate' | 'rotate' | 'scale'): void;
|
|
32
|
+
frame(): void;
|
|
33
|
+
}
|
|
34
|
+
export interface DocumentViewProps {
|
|
35
|
+
readonly document:
|
|
36
|
+
| Extract<NonNullable<EditorView['document']>, { kind: 'story' | 'workspace' }>
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: 'asset';
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly assetKind?: 'source' | 'model' | 'image' | 'audio' | 'json';
|
|
41
|
+
};
|
|
42
|
+
readonly chrome?: boolean;
|
|
43
|
+
readonly active?: boolean;
|
|
44
|
+
readonly onReady?: (document: DocumentHandle) => void;
|
|
45
|
+
}
|
|
46
|
+
export interface LayoutHost {
|
|
47
|
+
readonly Frame: ComponentType<LayoutFrameProps>;
|
|
48
|
+
readonly Header: ComponentType;
|
|
49
|
+
readonly Footer: ComponentType;
|
|
50
|
+
readonly Workspace: ComponentType<WorkspaceProps>;
|
|
51
|
+
readonly Document: ComponentType<DocumentViewProps>;
|
|
52
|
+
}
|
|
53
|
+
const HOST_KEY = Symbol.for('vgai.editor.layout-host');
|
|
54
|
+
const hosts = globalThis as typeof globalThis & { [HOST_KEY]?: LayoutHost };
|
|
55
|
+
/** Installed once by the editor runtime; project modules share this SDK instance. */
|
|
56
|
+
export function registerLayoutHost(value: LayoutHost): void {
|
|
57
|
+
hosts[HOST_KEY] = value;
|
|
58
|
+
}
|
|
59
|
+
function currentHost(): LayoutHost {
|
|
60
|
+
const host = hosts[HOST_KEY];
|
|
61
|
+
if (!host) throw new Error('Editor layouts require an editor host.');
|
|
62
|
+
return host;
|
|
63
|
+
}
|
|
64
|
+
export function EditorFrame(props: LayoutFrameProps) {
|
|
65
|
+
return createElement(currentHost().Frame, props);
|
|
66
|
+
}
|
|
67
|
+
export function EditorHeader() {
|
|
68
|
+
return createElement(currentHost().Header);
|
|
69
|
+
}
|
|
70
|
+
export function EditorFooter() {
|
|
71
|
+
return createElement(currentHost().Footer);
|
|
72
|
+
}
|
|
73
|
+
export function Workspace(props: WorkspaceProps) {
|
|
74
|
+
return createElement(currentHost().Workspace, props);
|
|
75
|
+
}
|
|
76
|
+
export function DocumentView(props: DocumentViewProps) {
|
|
77
|
+
return createElement(currentHost().Document, props);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Full game authoring, with the running game taking over the workspace during Play. */
|
|
81
|
+
export function GameLayout() {
|
|
82
|
+
return (
|
|
83
|
+
<EditorFrame>
|
|
84
|
+
<EditorHeader />
|
|
85
|
+
<Workspace immersivePlay />
|
|
86
|
+
<EditorFooter />
|
|
87
|
+
</EditorFrame>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
/** Studio keeps its authoring panels and opens analytics while the game runs. */
|
|
91
|
+
export function StudioLayout() {
|
|
92
|
+
return (
|
|
93
|
+
<EditorFrame>
|
|
94
|
+
<EditorHeader />
|
|
95
|
+
<Workspace immersivePlay={false} playUtilities={['tool:analytics.analytics']} />
|
|
96
|
+
<EditorFooter />
|
|
97
|
+
</EditorFrame>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
export function DesignLayout() {
|
|
101
|
+
return (
|
|
102
|
+
<EditorFrame>
|
|
103
|
+
<EditorHeader />
|
|
104
|
+
<Workspace arrangement={DesignArrangement} immersivePlay={false} />
|
|
105
|
+
<EditorFooter />
|
|
106
|
+
</EditorFrame>
|
|
107
|
+
);
|
|
108
|
+
}
|
package/src/looks.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Contribution data types are owned by the project adapter contract. */
|
|
2
|
+
export type {
|
|
3
|
+
EditorKeyActionId,
|
|
4
|
+
KeyChord,
|
|
5
|
+
KeymapBindings,
|
|
6
|
+
KeymapContribution,
|
|
7
|
+
WorkspaceLayoutRegions,
|
|
8
|
+
WorkspaceLayoutContribution,
|
|
9
|
+
MaterialContribution,
|
|
10
|
+
DensityContribution,
|
|
11
|
+
IconCategoryTone,
|
|
12
|
+
IconSetContribution,
|
|
13
|
+
StyleContribution,
|
|
14
|
+
} from '@volter/editor-project/adapter/editor-looks';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project directories a tool may commit generated bytes into, and the one
|
|
3
|
+
* normalizer every writer validates a path with.
|
|
4
|
+
*
|
|
5
|
+
* A project has two of them, and the difference is what SHIPS:
|
|
6
|
+
*
|
|
7
|
+
* - `public/` — game assets. Exported, served at a root-relative URL,
|
|
8
|
+
* loaded by the game's own source.
|
|
9
|
+
* - `references/` — reference material: moodboards, style plates, generated
|
|
10
|
+
* stills and clips an author or an agent looks at while
|
|
11
|
+
* building. Ordinary files, indexed by the editor's Content
|
|
12
|
+
* panel like any other media, and NOT exported — the
|
|
13
|
+
* project's own build copies `publicDir` and nothing else.
|
|
14
|
+
*
|
|
15
|
+
* This lives in the SDK rather than beside one writer because the rule has more
|
|
16
|
+
* than one enforcer: the editor host's atomic project-output writer, and every
|
|
17
|
+
* registered provider boundary that takes an `outputDirectory` from a caller.
|
|
18
|
+
* Two copies of "must be under public/" is how a reference output becomes
|
|
19
|
+
* writable through one door and refused at the next.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The writable output roots, in the order a chooser should offer them. */
|
|
23
|
+
export const PROJECT_OUTPUT_ROOTS = ['public', 'references'] as const;
|
|
24
|
+
|
|
25
|
+
export type ProjectOutputRoot = (typeof PROJECT_OUTPUT_ROOTS)[number];
|
|
26
|
+
|
|
27
|
+
/** Human-readable list for an error message: `public/` or `references/`. */
|
|
28
|
+
function rootList(): string {
|
|
29
|
+
return PROJECT_OUTPUT_ROOTS.map((root) => `${root}/`).join(' or ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** POSIX-normalized, `./`-stripped, trailing-slash-stripped. */
|
|
33
|
+
function normalizeSlashes(path: string): string {
|
|
34
|
+
const slashed = path.replaceAll('\\', '/');
|
|
35
|
+
const segments: string[] = [];
|
|
36
|
+
for (const segment of slashed.split('/')) {
|
|
37
|
+
if (segment === '' || segment === '.') continue;
|
|
38
|
+
segments.push(segment);
|
|
39
|
+
}
|
|
40
|
+
return segments.join('/');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The root `path` sits under, or `undefined` when it sits under none. */
|
|
44
|
+
export function projectOutputRootOf(path: string): ProjectOutputRoot | undefined {
|
|
45
|
+
const normalized = normalizeSlashes(path);
|
|
46
|
+
return PROJECT_OUTPUT_ROOTS.find(
|
|
47
|
+
(root) => normalized === root || normalized.startsWith(`${root}/`),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate and normalize a project-relative output path.
|
|
53
|
+
*
|
|
54
|
+
* Throws — with a message naming BOTH roots, because the common mistake is
|
|
55
|
+
* knowing about one of them — for an absolute path, an escape, a bare root
|
|
56
|
+
* with no file under it, or anything outside the roots above. `subject` names
|
|
57
|
+
* what is being validated in that message (`Generated output path`, `Fal
|
|
58
|
+
* outputDirectory`, …).
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeProjectOutputPath(
|
|
61
|
+
path: string,
|
|
62
|
+
subject = 'Generated output path',
|
|
63
|
+
): string {
|
|
64
|
+
const normalized = normalizeSlashes(path);
|
|
65
|
+
const escapes = normalized.split('/').includes('..');
|
|
66
|
+
const root = projectOutputRootOf(normalized);
|
|
67
|
+
if (path.startsWith('/') || escapes || root === undefined || normalized === root) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`${subject} ${JSON.stringify(path)} is invalid; it must be a project-relative path under ${rootList()}.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE TAB RESOURCE CENSUS — one declaration of the profile a browser tab
|
|
3
|
+
* reports about itself, for every compilation unit that speaks it.
|
|
4
|
+
*
|
|
5
|
+
* WHAT IT IS. What a game tab is holding: JS heap, canvas backing stores,
|
|
6
|
+
* renderer resource counts, and how many project mount generations the
|
|
7
|
+
* document has accumulated. Sampled by the page every five seconds and carried
|
|
8
|
+
* on the heartbeat, so that a browser-level renderer death — which the presence
|
|
9
|
+
* system recovers from perfectly and would otherwise leave unexplained — has a
|
|
10
|
+
* number beside it. MEASUREMENT ONLY: nothing anywhere enforces a budget from
|
|
11
|
+
* these. The page-side sampler and its `why` are
|
|
12
|
+
* `packages/editor/src/tab-census.ts`.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT LIVES HERE. This one shape crosses four compilation units — the page
|
|
15
|
+
* that samples it (`@vgai/editor`'s browser bundle), the editor server that
|
|
16
|
+
* files it (`@vgai/editor`'s node half), the editor-extension surface that
|
|
17
|
+
* describes the status response (`@volter/editor-sdk`), and the session journal
|
|
18
|
+
* plus the CLI row that print it (`@vgai/sdk`, `@vgai/cli`). Each of them used
|
|
19
|
+
* to spell the seven fields out again. `@vgai/sdk` is the only package all four
|
|
20
|
+
* already depend on: `@volter/editor-sdk` depends on `@vgai/sdk` and `@vgai/editor`
|
|
21
|
+
* depends on both, so this cannot live in `@volter/editor-sdk` without a cycle —
|
|
22
|
+
* and it does not belong in `@vgai/game-runtime`, whose subject is a running game,
|
|
23
|
+
* not an editor session's tabs.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately import-free so a browser bundle can take it: the census's other
|
|
26
|
+
* `@vgai/sdk` home, `project/session-journal.ts`, reads `node:fs`.
|
|
27
|
+
*
|
|
28
|
+
* WHAT ELSE RIDES IT. The beat is the one channel that still moves when the
|
|
29
|
+
* page's main thread or the Blender worker is blocked, so `blender` — how long
|
|
30
|
+
* the worker's calls are taking and how long the main thread has been stalled —
|
|
31
|
+
* is carried here too (absent unless a Blender session exists in the tab). Same
|
|
32
|
+
* rule as the rest of this file: measurement only, no budget anywhere.
|
|
33
|
+
*
|
|
34
|
+
* ABSENT IS NOT ZERO. `heapUsedMB`/`heapLimitMB` are null off Chromium
|
|
35
|
+
* (`performance.memory` is non-standard). The renderer counts are ABSENT rather
|
|
36
|
+
* than zero when no game has registered a render-debug adapter, and `programs`
|
|
37
|
+
* is absent until a first render: a zero would read as "no textures", which is
|
|
38
|
+
* a different claim from "nobody measured".
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* THE BLENDER WORKER'S STALLS, as numbers.
|
|
43
|
+
*
|
|
44
|
+
* WHY (measured 2026-09-16): one `blender-execute` held the Blender worker for
|
|
45
|
+
* over 1,800s and wedged the tab, and a 0.86s-per-call scene stretched into
|
|
46
|
+
* heartbeat timeouts that read as "tab present, did not respond". The product
|
|
47
|
+
* had no number for either. Owner ruling: a tab that stops answering is the
|
|
48
|
+
* product's defect regardless of what the machine is doing, and the product has
|
|
49
|
+
* to surface it — so these ride the census to `vgai status`.
|
|
50
|
+
*
|
|
51
|
+
* MEASURED BY THE PAGE, because neither blocked party can report on itself: the
|
|
52
|
+
* worker's own loop is what is stuck, and a stalled main thread cannot send.
|
|
53
|
+
* The call half comes from `BlenderRuntime.metrics()`
|
|
54
|
+
* (`packages/blender-engine/browser/runtime.ts`), the stall half from a `longtask`
|
|
55
|
+
* PerformanceObserver (`packages/editor/src/blender-tab-metrics.ts`).
|
|
56
|
+
*
|
|
57
|
+
* MEASUREMENT ONLY. No threshold here cancels, kills or budgets a call.
|
|
58
|
+
*/
|
|
59
|
+
export interface BlenderTabMetrics {
|
|
60
|
+
/** Age of the oldest OUTSTANDING worker call, or null when the worker is idle.
|
|
61
|
+
* The only field with a number during a wedge. */
|
|
62
|
+
readonly inFlightMs: number | null;
|
|
63
|
+
/** Duration of the newest completed call; null before the first one. */
|
|
64
|
+
readonly lastCallMs: number | null;
|
|
65
|
+
/** The longest call this page has seen, counting an outstanding one. */
|
|
66
|
+
readonly maxCallMs: number | null;
|
|
67
|
+
/** Calls past 5s, and past 30s, since this page loaded. */
|
|
68
|
+
readonly callsOver5s: number;
|
|
69
|
+
readonly callsOver30s: number;
|
|
70
|
+
/** Longest `longtask` entry since page load, in ms; null off Chromium (the
|
|
71
|
+
* Long Tasks API is not implemented everywhere) — never 0, which would read
|
|
72
|
+
* as "the main thread never stalled". */
|
|
73
|
+
readonly longestTaskMs: number | null;
|
|
74
|
+
/** How many long tasks ran past 100ms since page load. */
|
|
75
|
+
readonly tasksOver100ms: number;
|
|
76
|
+
/** The longest long task that overlapped the newest call's window — what the
|
|
77
|
+
* MAIN thread was doing while the worker was busy. Null when there has been
|
|
78
|
+
* no call yet, or no long task during it. */
|
|
79
|
+
readonly lastCallLongestTaskMs: number | null;
|
|
80
|
+
/**
|
|
81
|
+
* THE ENGINE'S OWN MEMORY: the Blender module's linear memory in MB, read in
|
|
82
|
+
* the worker (`packages/blender-engine/browser/worker.ts`) and posted after every
|
|
83
|
+
* call. Null before the session's first call, and absent against a page that
|
|
84
|
+
* predates the field.
|
|
85
|
+
*
|
|
86
|
+
* It is here because no other number on this census answers the question.
|
|
87
|
+
* `heapUsedMB` is the PAGE's JS heap — a few tens of MB — while the engine's
|
|
88
|
+
* memory is the larger half of the tab by far, and a reader looking at the
|
|
89
|
+
* heap line alone concludes the tab is cheap. wasm32 memory never shrinks, so
|
|
90
|
+
* this is simultaneously the current size and the session's high-water mark.
|
|
91
|
+
*
|
|
92
|
+
* MEASURED 2026-09-18 on this bundle: 512 MB reserved at boot of which ~166
|
|
93
|
+
* MB is ever touched, reaching 1036 MB across `17-workshop-interior`. The
|
|
94
|
+
* reservation costs no resident pages; the growth does, permanently.
|
|
95
|
+
*/
|
|
96
|
+
readonly wasmMemoryMB?: number | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A census as a CURRENT page produces it and the server files it.
|
|
101
|
+
*
|
|
102
|
+
* Every field here is guaranteed by a producer that always writes it, and
|
|
103
|
+
* `packages/editor/server/tab-heartbeat.ts`'s `parseCensus` is where that is
|
|
104
|
+
* checked: a beat frame missing `canvases`, `canvasMB` or `mountEpochs` is
|
|
105
|
+
* rejected outright rather than filed as a partial profile.
|
|
106
|
+
*/
|
|
107
|
+
export interface TabCensus {
|
|
108
|
+
/** `performance.memory.usedJSHeapSize` in MB; null off Chromium. */
|
|
109
|
+
readonly heapUsedMB: number | null;
|
|
110
|
+
/** `performance.memory.jsHeapSizeLimit` in MB; null off Chromium. */
|
|
111
|
+
readonly heapLimitMB: number | null;
|
|
112
|
+
/** Distinct project mount generations fetched by this document. */
|
|
113
|
+
readonly mountEpochs: number;
|
|
114
|
+
/** How many `<canvas>` elements the document holds. */
|
|
115
|
+
readonly canvases: number;
|
|
116
|
+
/** Their total pixel-buffer footprint: Σ width×height×4 bytes, in MB. */
|
|
117
|
+
readonly canvasMB: number;
|
|
118
|
+
/** `renderer.info.memory.textures` — absent with no mounted adapter. */
|
|
119
|
+
readonly textures?: number;
|
|
120
|
+
/** `renderer.info.memory.geometries` — absent with no mounted adapter. */
|
|
121
|
+
readonly geometries?: number;
|
|
122
|
+
/** Compiled programs — absent with no adapter, or before the first render. */
|
|
123
|
+
readonly programs?: number;
|
|
124
|
+
/** {@link BlenderTabMetrics} — absent unless this tab has a Blender session. */
|
|
125
|
+
readonly blender?: BlenderTabMetrics;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A census read back out of a RECORD, where the writer may predate a field.
|
|
130
|
+
*
|
|
131
|
+
* Exactly one field varies, and this is the only place that is said: a session
|
|
132
|
+
* journal line written before the mount census existed has no `mountEpochs`,
|
|
133
|
+
* and neither does a status response from an older editor server. The
|
|
134
|
+
* difference is a property of the READER's input, so it is named on the
|
|
135
|
+
* reader's type rather than by softening {@link TabCensus} — which is what the
|
|
136
|
+
* four hand-copied declarations did, and how the producers' own guarantee got
|
|
137
|
+
* lost on the way to the two SDKs.
|
|
138
|
+
*
|
|
139
|
+
* There is no runtime narrowing to point at because neither reader has a parse
|
|
140
|
+
* step to put one in: the journal is `JSON.parse` per line by recorded design
|
|
141
|
+
* (`session-journal.ts`: "nothing reads a journal back through a validator"),
|
|
142
|
+
* and the CLI reads the status body as JSON. So the absence is carried in the
|
|
143
|
+
* type and handled where it is printed — `formatTabCensus` omits the words
|
|
144
|
+
* rather than printing a fabricated count. Every {@link TabCensus} is a valid
|
|
145
|
+
* value of this type; the reverse is not.
|
|
146
|
+
*/
|
|
147
|
+
export type RecordedTabCensus = Omit<TabCensus, 'mountEpochs'> & {
|
|
148
|
+
readonly mountEpochs?: number;
|
|
149
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type ToolContributionPoint =
|
|
2
|
+
| 'workspace.document'
|
|
3
|
+
| 'selection.inspector'
|
|
4
|
+
| 'asset.inspector'
|
|
5
|
+
| 'generation.result'
|
|
6
|
+
| 'workspace.utility'
|
|
7
|
+
| 'workspace.analytics'
|
|
8
|
+
| 'workspace.status';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One contribution module, FOUND BY SCANNING — never listed anywhere.
|
|
12
|
+
*
|
|
13
|
+
* Nothing enumerates these. `src/tools/` in project source and `src/` in every
|
|
14
|
+
* `vgai`-declaring dependency are walked for the naming convention
|
|
15
|
+
* (`*.document.tsx`, `*.inspector.tsx`, `*.asset-inspector.tsx`,
|
|
16
|
+
* `*.result.tsx`, `*.utility.tsx`, `*.analytics.tsx`), and the module itself declares everything
|
|
17
|
+
* else: `point`, `title` (or `presentations`), and the `tool` it drives, by
|
|
18
|
+
* name. Its id is derived from this path. A manifest cannot disagree with a
|
|
19
|
+
* module it does not mention.
|
|
20
|
+
*/
|
|
21
|
+
export interface ProjectToolContribution {
|
|
22
|
+
/** Project-relative or package-absolute browser module path. */
|
|
23
|
+
entryPath: string;
|
|
24
|
+
/** The dependency that declared it (`package.json#vgai.contributions`),
|
|
25
|
+
* absent for the project's own `src/contributions/` modules. */
|
|
26
|
+
package?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProjectToolErrorSummary {
|
|
30
|
+
code: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
dataSchema?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProjectToolCatalogEntry {
|
|
36
|
+
name: string;
|
|
37
|
+
summary: string;
|
|
38
|
+
description: string;
|
|
39
|
+
sourcePath: string;
|
|
40
|
+
inputSchema: unknown;
|
|
41
|
+
resultSchema: unknown;
|
|
42
|
+
errors: ProjectToolErrorSummary[];
|
|
43
|
+
requires: Record<string, boolean | undefined>;
|
|
44
|
+
host: 'node' | 'editor-browser' | 'runtime-page';
|
|
45
|
+
mutates: boolean;
|
|
46
|
+
supportsDryRun: boolean;
|
|
47
|
+
longRunning: boolean;
|
|
48
|
+
permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
|
|
49
|
+
generation?: { provider: string; role: 'submit' | 'poll' | 'cancel' | 'accept' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ProjectToolLoadError {
|
|
53
|
+
sourcePath: string;
|
|
54
|
+
message: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What a host says when it cannot load the project's TypeScript at all.
|
|
59
|
+
*
|
|
60
|
+
* ONE owner for the sentence, because two realms reach this state through
|
|
61
|
+
* different doors and both must say the same true thing rather than fall back
|
|
62
|
+
* to a fabricated empty catalog:
|
|
63
|
+
*
|
|
64
|
+
* - the NODE side (`packages/editor/server/project-tools.ts`) when a host
|
|
65
|
+
* builds the catalog with no `loadModule`; and
|
|
66
|
+
* - the PAGE side (`packages/editor/src/project-tools.ts`), whose fetch of
|
|
67
|
+
* `/__editor/project-tools` can be answered by an SPA FALLBACK — `200` and
|
|
68
|
+
* HTML — when no editor server is in front of the origin, so an unguarded
|
|
69
|
+
* fetch that merely checks `response.ok` reads a successful page load as a
|
|
70
|
+
* successful catalog, then dies in
|
|
71
|
+
* `res.json()`. The panel's empty state ("No project tools registered")
|
|
72
|
+
* is a FACT about the project; a host that cannot ask has not learned it.
|
|
73
|
+
*/
|
|
74
|
+
export const NO_PROJECT_MODULE_HOST_MESSAGE =
|
|
75
|
+
'This editor host cannot RUN registered project tools (package.json#vgai.tools): callables ' +
|
|
76
|
+
'execute on the Node side. Contribution panels still load from project source; to run the ' +
|
|
77
|
+
'tools themselves, open the project in the Vite-backed dev or packaged editor.';
|
|
78
|
+
|
|
79
|
+
/** The catalog row that states {@link NO_PROJECT_MODULE_HOST_MESSAGE}. */
|
|
80
|
+
export function noProjectModuleHostError(): ProjectToolLoadError {
|
|
81
|
+
return { sourcePath: 'package.json#vgai.tools', message: NO_PROJECT_MODULE_HOST_MESSAGE };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Callables and contribution modules are SIBLINGS, not parent and child.
|
|
86
|
+
*
|
|
87
|
+
* They used to be nested — `tools[].contributions[]` — purely so the browser
|
|
88
|
+
* loader had a tool to hand the component. A document is not owned by one
|
|
89
|
+
* callable, and the nesting made project source structurally unable to present
|
|
90
|
+
* a dependency-provided tool. The module names the tool it drives instead.
|
|
91
|
+
*/
|
|
92
|
+
export interface ProjectToolCatalog {
|
|
93
|
+
tools: ProjectToolCatalogEntry[];
|
|
94
|
+
contributions: ProjectToolContribution[];
|
|
95
|
+
loadErrors: ProjectToolLoadError[];
|
|
96
|
+
}
|