@volter/project 0.0.0-stage → 0.5.203
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 +6 -0
- package/README.md +18 -3
- package/package.json +64 -4
- package/schemas/engine-api.json +57 -0
- package/schemas/engine-api.md +22 -0
- package/schemas/volter-project.schema.json +468 -0
- package/schemas/volter-settings.schema.json +149 -0
- package/src/adapter/adapter-module.ts +997 -0
- package/src/adapter/adapter-surface.ts +34 -0
- package/src/adapter/authoring-seam-contract.ts +224 -0
- package/src/adapter/authoring.ts +1336 -0
- package/src/adapter/binding.ts +445 -0
- package/src/adapter/debug-registry-ref.ts +19 -0
- package/src/adapter/editor-looks.ts +519 -0
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/registry.ts +116 -0
- package/src/adapter/frame-capture.ts +80 -0
- package/src/adapter/host-context.ts +114 -0
- package/src/adapter/index.ts +209 -0
- package/src/adapter/ingest/contract-system-slots.ts +28 -0
- package/src/adapter/ingest/game-contract-seams.ts +86 -0
- package/src/adapter/ingest/game-contract.ts +506 -0
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/loop-gate-report.ts +53 -0
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-entry-surface.ts +100 -0
- package/src/adapter/physics-adapter-2d.ts +30 -0
- package/src/adapter/render-audio.ts +20 -0
- package/src/adapter/render-memory.ts +34 -0
- package/src/adapter/renderer-config.ts +84 -0
- package/src/adapter/root-adapter.ts +243 -0
- package/src/adapter/root-seam-contract.ts +61 -0
- package/src/adapter/root-stacking.ts +67 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/system-adapter.ts +865 -0
- package/src/adapter/system-seam-contract.ts +138 -0
- package/src/adapter/system-slot.ts +301 -0
- package/src/adapter/transform.ts +35 -0
- package/src/adapter/workspace-arrangement.ts +92 -0
- package/src/core/system-phase.ts +52 -0
- package/src/inspection.ts +281 -0
- package/src/manifest/configuration-kinds.ts +241 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +45 -0
- package/src/manifest/kind-modules.ts +58 -0
- package/src/manifest/load-file.ts +33 -0
- package/src/manifest/load.ts +380 -0
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/runtime-environment.ts +48 -0
- package/src/manifest/schema.ts +690 -0
- package/src/settings/keys.ts +152 -0
- package/src/settings/schema.ts +151 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SETTINGS KEY TABLE — every setting there is, as ONE dotted `volter.*` name
|
|
3
|
+
* per leaf, with the type and the sentence a person reads beside it.
|
|
4
|
+
*
|
|
5
|
+
* ## Why it exists, and why it is DERIVED
|
|
6
|
+
*
|
|
7
|
+
* Under the Code-OSS frame (ARCHITECTURE-CORE §The core is Code-OSS, U7) the
|
|
8
|
+
* settings layers are the configuration service's, and a configuration service
|
|
9
|
+
* speaks FLAT DOTTED KEYS — `volter.appearance.palette` — while
|
|
10
|
+
* `schema.ts`'s document is a nested object. Something has to be the one place
|
|
11
|
+
* those two spellings meet, and the same table is what the fork's
|
|
12
|
+
* `contributes.configuration` is generated from, so VS Code's Settings editor
|
|
13
|
+
* shows every volter setting with the description its `.describe()` call already
|
|
14
|
+
* carries.
|
|
15
|
+
*
|
|
16
|
+
* It is DERIVED from {@link EditorSettingsSchema} rather than written, for the
|
|
17
|
+
* reason `PROVIDER_GAP` in `adapter-reach.ts` is a `Record` over a pinned key
|
|
18
|
+
* union: a hand-kept second list of the same seventeen keys drifts the first
|
|
19
|
+
* time somebody adds a field, and a settings key with no declaration is
|
|
20
|
+
* invisible in the one settings UI rather than loud. The walk is over the JSON
|
|
21
|
+
* Schema Zod itself produces (`z.toJSONSchema`) — the SAME derivation
|
|
22
|
+
* `scripts/generate-schema.ts` commits as
|
|
23
|
+
* `packages/project/schemas/volter-settings.schema.json`, which is what the
|
|
24
|
+
* fork's generator reads. One derivation, three consumers: this table, the
|
|
25
|
+
* committed schema a person's editor autocompletes against, and the frame's
|
|
26
|
+
* configuration contribution.
|
|
27
|
+
*
|
|
28
|
+
* ## The `volter.` prefix is part of the key, everywhere
|
|
29
|
+
*
|
|
30
|
+
* Not a thing the frame adds on the way in. One spelling in the door
|
|
31
|
+
* (`EditorHost.settings`), in `.vscode/settings.json`, in the Settings editor,
|
|
32
|
+
* and in what the editor's `eval` command prints — because the moment there are two, a reader
|
|
33
|
+
* has to know which side of which seam they are on to know which to type.
|
|
34
|
+
*/
|
|
35
|
+
import { z } from 'zod';
|
|
36
|
+
import { type EditorSettings, EditorSettingsSchema } from './schema';
|
|
37
|
+
|
|
38
|
+
/** The namespace every settings key carries. */
|
|
39
|
+
export const SETTINGS_KEY_PREFIX = 'volter';
|
|
40
|
+
|
|
41
|
+
/** What a key is, and what the Settings editor shows for it. */
|
|
42
|
+
export interface SettingsKeyDescriptor {
|
|
43
|
+
/** The dotted name, prefix included: `volter.appearance.palette`. */
|
|
44
|
+
readonly key: string;
|
|
45
|
+
/** The path into the settings document: `['appearance', 'palette']`. */
|
|
46
|
+
readonly path: readonly string[];
|
|
47
|
+
/** JSON Schema `type`, when the field has exactly one. A nullable field
|
|
48
|
+
* (`devicePreview.touch`) has none, and the frame declares no type for it
|
|
49
|
+
* rather than picking one of the two. */
|
|
50
|
+
readonly type?: string | undefined;
|
|
51
|
+
/** The closed set of values, when the field is an enum. */
|
|
52
|
+
readonly values?: readonly string[] | undefined;
|
|
53
|
+
/** The field's own `.describe()` sentence — the one a person reads. */
|
|
54
|
+
readonly description: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type JsonSchemaNode = {
|
|
58
|
+
type?: string | string[];
|
|
59
|
+
enum?: readonly string[];
|
|
60
|
+
description?: string;
|
|
61
|
+
properties?: Record<string, JsonSchemaNode>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function walk(node: JsonSchemaNode, path: readonly string[], out: SettingsKeyDescriptor[]): void {
|
|
65
|
+
if (node.properties) {
|
|
66
|
+
for (const [name, child] of Object.entries(node.properties)) walk(child, [...path, name], out);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// `$schema` is the autocomplete pointer the document carries for a person
|
|
70
|
+
// editing it by hand; it describes the file, not the editor, and there is
|
|
71
|
+
// nothing for a settings UI to show.
|
|
72
|
+
if (path.length === 1 && path[0] === '$schema') return;
|
|
73
|
+
out.push({
|
|
74
|
+
key: [SETTINGS_KEY_PREFIX, ...path].join('.'),
|
|
75
|
+
path,
|
|
76
|
+
type: typeof node.type === 'string' ? node.type : undefined,
|
|
77
|
+
values: node.enum,
|
|
78
|
+
description: node.description ?? '',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let table: readonly SettingsKeyDescriptor[] | null = null;
|
|
83
|
+
let byKey: ReadonlyMap<string, SettingsKeyDescriptor> | null = null;
|
|
84
|
+
|
|
85
|
+
/** Every settings key, in the schema's own declaration order. */
|
|
86
|
+
export function settingsKeys(): readonly SettingsKeyDescriptor[] {
|
|
87
|
+
if (!table) {
|
|
88
|
+
const out: SettingsKeyDescriptor[] = [];
|
|
89
|
+
walk(z.toJSONSchema(EditorSettingsSchema, { io: 'input' }) as JsonSchemaNode, [], out);
|
|
90
|
+
table = out;
|
|
91
|
+
byKey = new Map(out.map((entry) => [entry.key, entry]));
|
|
92
|
+
}
|
|
93
|
+
return table;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One key's declaration, or `null` for a name no schema field owns. A caller
|
|
97
|
+
* that gets `null` is holding a key nobody can read — never a silent miss. */
|
|
98
|
+
export function settingsKey(key: string): SettingsKeyDescriptor | null {
|
|
99
|
+
settingsKeys();
|
|
100
|
+
return byKey?.get(key) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
104
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Read one key out of a settings document. `undefined` means the document
|
|
108
|
+
* does not carry it — which is what "this layer is silent about it" means. */
|
|
109
|
+
export function readSettingsValue(document: EditorSettings, key: string): unknown {
|
|
110
|
+
const descriptor = settingsKey(key);
|
|
111
|
+
if (!descriptor) return undefined;
|
|
112
|
+
let cursor: unknown = document;
|
|
113
|
+
for (const segment of descriptor.path) {
|
|
114
|
+
if (!isPlainObject(cursor)) return undefined;
|
|
115
|
+
cursor = cursor[segment];
|
|
116
|
+
}
|
|
117
|
+
return cursor;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A settings document built from dotted keys — the inverse of
|
|
121
|
+
* {@link flattenSettings}, and what turns a per-key configuration service back
|
|
122
|
+
* into the nested document every reader in the editor already speaks. */
|
|
123
|
+
export function settingsFromEntries(entries: Iterable<readonly [string, unknown]>): EditorSettings {
|
|
124
|
+
const document: Record<string, unknown> = {};
|
|
125
|
+
for (const [key, value] of entries) {
|
|
126
|
+
if (value === undefined) continue;
|
|
127
|
+
const descriptor = settingsKey(key);
|
|
128
|
+
if (!descriptor) continue;
|
|
129
|
+
let cursor = document;
|
|
130
|
+
for (const segment of descriptor.path.slice(0, -1)) {
|
|
131
|
+
const next = cursor[segment];
|
|
132
|
+
if (!isPlainObject(next)) cursor[segment] = {};
|
|
133
|
+
cursor = cursor[segment] as Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
const leaf = descriptor.path[descriptor.path.length - 1];
|
|
136
|
+
if (leaf !== undefined) cursor[leaf] = value;
|
|
137
|
+
}
|
|
138
|
+
return document as EditorSettings;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A settings document as dotted key/value pairs, skipping keys it is silent
|
|
142
|
+
* about. A value at a path no schema field owns is DROPPED rather than
|
|
143
|
+
* carried: the document is `.strict()`, so such a path can only come from a
|
|
144
|
+
* layer this build does not understand. */
|
|
145
|
+
export function flattenSettings(document: EditorSettings): ReadonlyMap<string, unknown> {
|
|
146
|
+
const out = new Map<string, unknown>();
|
|
147
|
+
for (const descriptor of settingsKeys()) {
|
|
148
|
+
const value = readSettingsValue(document, descriptor.key);
|
|
149
|
+
if (value !== undefined) out.set(descriptor.key, value);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SETTINGS DOCUMENT — the Zod truth for the two shared settings layers
|
|
3
|
+
* (ARCHITECTURE-CORE §Editor chrome, "Settings have four layers with named
|
|
4
|
+
* homes"):
|
|
5
|
+
*
|
|
6
|
+
* ~/.volter/settings.json the USER layer — one person, every project
|
|
7
|
+
* <project>/.volter/settings.json the PROJECT layer — one project, committed,
|
|
8
|
+
* every person who opens it
|
|
9
|
+
*
|
|
10
|
+
* Both files carry the same shape; the project layer overrides the user layer
|
|
11
|
+
* key by key (`mergeEditorSettings`). Neither layer holds authored content —
|
|
12
|
+
* an unreadable file is reported by name and falls back to defaults, never
|
|
13
|
+
* migrated. The generated JSON Schema (`npm run generate-schema` →
|
|
14
|
+
* `packages/project/schemas/volter-settings.schema.json`) is what an editor
|
|
15
|
+
* autocompletes against when a person edits either file by hand.
|
|
16
|
+
*
|
|
17
|
+
* Every field here has a runtime reader in the editor's settings store
|
|
18
|
+
* consumers (`theme-preference.ts`, `workspace-regions.ts`,
|
|
19
|
+
* `keymap-presets.ts`, `workspace-presets.ts`, `device-preview.ts`); a field
|
|
20
|
+
* with no reader does not get declared (CLAUDE.md, "Do not author schema
|
|
21
|
+
* fields that have no runtime reader").
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
|
|
25
|
+
const RegionVisibility = z.enum(['shown', 'hidden']);
|
|
26
|
+
|
|
27
|
+
export const ChromeRegionsSettingsSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
workspaceTabs: RegionVisibility.optional().describe(
|
|
30
|
+
"The workspace tab strip in the top bar (Blender's); hidden unless a style shows it.",
|
|
31
|
+
),
|
|
32
|
+
header: RegionVisibility.optional().describe('The top header bar.'),
|
|
33
|
+
shelf: RegionVisibility.optional().describe('The utility shelf.'),
|
|
34
|
+
inspector: z
|
|
35
|
+
.enum(['column', 'properties', 'card'])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Inspector presentation: the narrow docked column or a properties panel.'),
|
|
38
|
+
drawer: RegionVisibility.optional().describe('The bottom drawer.'),
|
|
39
|
+
telemetry: RegionVisibility.optional().describe(
|
|
40
|
+
"The top bar's runtime telemetry cluster: the frame-rate readout, its sparkline and the audio meter. The Profiler utility is the same information's other door.",
|
|
41
|
+
),
|
|
42
|
+
hierarchyTypeSuffix: RegionVisibility.optional().describe(
|
|
43
|
+
"The `·TypeName` a component-instance row prints after its name in the hierarchy. Hidden leaves the row its name alone, the way Blender's Outliner does; the type stays reachable through the row's own tooltip and the inspector.",
|
|
44
|
+
),
|
|
45
|
+
hierarchyInstanceRule: RegionVisibility.optional().describe(
|
|
46
|
+
"The dotted rule a component-instance row draws under its name in the hierarchy. Hidden leaves the name unmarked, the way Blender's Outliner does; the fact stays reachable through the row's own tooltip, its source actions and the inspector.",
|
|
47
|
+
),
|
|
48
|
+
hierarchyRestrictions: z
|
|
49
|
+
.enum(['select+viewport', 'viewport+render'])
|
|
50
|
+
.optional()
|
|
51
|
+
.describe(
|
|
52
|
+
"Which restriction columns a hierarchy row carries at its right edge: this editor's selection lock beside the eye, or Blender's default filter — the eye in its own column with the render column reserved and blank, three.js having one visibility flag for viewport and render alike.",
|
|
53
|
+
),
|
|
54
|
+
})
|
|
55
|
+
.strict()
|
|
56
|
+
.describe('Which chrome regions a style bundle shows or hides.');
|
|
57
|
+
|
|
58
|
+
export const EditorSettingsSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
$schema: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('JSON Schema pointer for editor autocomplete; ignored by the editor.'),
|
|
64
|
+
appearance: z
|
|
65
|
+
.object({
|
|
66
|
+
palette: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Editor palette id — a built-in or a custom theme document.'),
|
|
70
|
+
material: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Editor material id (`classic`, `glass`, or one a style bundle carries).'),
|
|
74
|
+
icons: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('Editor icon set id (`default`, or one a style bundle carries).'),
|
|
78
|
+
regions: ChromeRegionsSettingsSchema.optional(),
|
|
79
|
+
})
|
|
80
|
+
.strict()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('How the editor chrome looks.'),
|
|
83
|
+
keymap: z.string().optional().describe('Active keymap preset id (`volter`, `blender`, …).'),
|
|
84
|
+
play: z
|
|
85
|
+
.object({
|
|
86
|
+
keepPanelsVisible: z
|
|
87
|
+
.boolean()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe(
|
|
90
|
+
'Keep editor panels visible when starting Play, instead of letting an immersive workspace hide them. Turning this on during Play restores the panels; turning it off applies on the next Play.',
|
|
91
|
+
),
|
|
92
|
+
})
|
|
93
|
+
.strict()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('How the editor presents Play mode.'),
|
|
96
|
+
devicePreview: z
|
|
97
|
+
.object({
|
|
98
|
+
preset: z.string().optional().describe('Device preview frame preset id.'),
|
|
99
|
+
touch: z
|
|
100
|
+
.boolean()
|
|
101
|
+
.nullable()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Touch emulation override; null follows the preset.'),
|
|
104
|
+
})
|
|
105
|
+
.strict()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('The device frame the Game document previews in.'),
|
|
108
|
+
})
|
|
109
|
+
.strict()
|
|
110
|
+
.describe('Editor settings — the user layer and the project layer share this shape.');
|
|
111
|
+
|
|
112
|
+
export type EditorSettings = z.infer<typeof EditorSettingsSchema>;
|
|
113
|
+
export type ChromeRegionsSettings = z.infer<typeof ChromeRegionsSettingsSchema>;
|
|
114
|
+
|
|
115
|
+
/** Which layer a file is; the file paths are the server's. */
|
|
116
|
+
export type SettingsLayer = 'user' | 'project';
|
|
117
|
+
|
|
118
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
119
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Deep-merge plain objects; anything else (primitives, arrays, null)
|
|
123
|
+
* replaces. `merge(user, project)` is the effective document. */
|
|
124
|
+
export function mergeEditorSettings(base: EditorSettings, over: EditorSettings): EditorSettings {
|
|
125
|
+
const out: Record<string, unknown> = { ...base };
|
|
126
|
+
const patch = over as Record<string, unknown>;
|
|
127
|
+
for (const key of Object.keys(patch)) {
|
|
128
|
+
const value = patch[key];
|
|
129
|
+
const current = out[key];
|
|
130
|
+
out[key] =
|
|
131
|
+
isPlainObject(current) && isPlainObject(value)
|
|
132
|
+
? mergeEditorSettings(current as EditorSettings, value as EditorSettings)
|
|
133
|
+
: value;
|
|
134
|
+
}
|
|
135
|
+
return out as EditorSettings;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Validate a raw document. The result carries the parsed settings or the
|
|
139
|
+
* issues, spelled for a person who has the file open. */
|
|
140
|
+
export function parseEditorSettings(
|
|
141
|
+
raw: unknown,
|
|
142
|
+
): { settings: EditorSettings; issues: [] } | { settings: null; issues: string[] } {
|
|
143
|
+
const result = EditorSettingsSchema.safeParse(raw);
|
|
144
|
+
if (result.success) return { settings: result.data, issues: [] };
|
|
145
|
+
return {
|
|
146
|
+
settings: null,
|
|
147
|
+
issues: result.error.issues.map((issue) =>
|
|
148
|
+
issue.path.length ? `${issue.path.join('.')}: ${issue.message}` : issue.message,
|
|
149
|
+
),
|
|
150
|
+
};
|
|
151
|
+
}
|