@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,1336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthoringAdapter — the editor's authoring contract. The editor talks to THIS,
|
|
3
|
+
* keyed by opaque string node ids, instead of to a concrete document format.
|
|
4
|
+
*
|
|
5
|
+
* An implementer's document model is its own private business; an ingested
|
|
6
|
+
* game's adapter implements the same
|
|
7
|
+
* providers directly over its live `Object3D` tree. The editor sees only the
|
|
8
|
+
* interface and the advertised `capabilities` — it never branches on which
|
|
9
|
+
* implementer it is talking to.
|
|
10
|
+
*
|
|
11
|
+
* The interface lives in the engine (not the editor) so `MountedThreeRoot.authoring`
|
|
12
|
+
* can reference it without the engine depending on the editor; implementers live
|
|
13
|
+
* in `packages/editor/src/authoring/`.
|
|
14
|
+
*
|
|
15
|
+
* **Reserved inspector paths convention:** the property paths `name`, `visible`,
|
|
16
|
+
* `locked`, `renderVisible` and `exclude` are reserved — an adapter that
|
|
17
|
+
* supports them exposes them through the ordinary `InspectorProvider.get`/`set`
|
|
18
|
+
* (no separate provider). The shell renders the hierarchy panel's rename field,
|
|
19
|
+
* eye (visibility) toggle, lock toggle, render-camera toggle and
|
|
20
|
+
* exclude checkbox against these paths, generically, for any adapter that
|
|
21
|
+
* reports them via `InspectorProvider.properties`.
|
|
22
|
+
*
|
|
23
|
+
* A COLUMN FOLLOWS WHAT THE ADAPTER ANSWERS, never a chrome knob (owner ruling,
|
|
24
|
+
* 2026-09-19). `renderVisible` and `exclude` are answered by almost nothing —
|
|
25
|
+
* three.js has ONE `Object3D.visible` governing viewport and render alike, and
|
|
26
|
+
* nothing but a view layer has an exclusion — so their columns are absent or a
|
|
27
|
+
* reserved blank cell, and appear the moment an adapter whose truth really does
|
|
28
|
+
* separate them says so. `exclude` and `renderVisible` are reported in the
|
|
29
|
+
* sense their name carries: `exclude: true` means EXCLUDED, `renderVisible:
|
|
30
|
+
* false` means hidden from renders.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { Transform } from './transform';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Booleans the editor UI gates affordances on (hide what an adapter can't do).
|
|
37
|
+
*
|
|
38
|
+
* Deliberately SMALL: a capability flag earns its place only when the shell
|
|
39
|
+
* actually branches on it. Structural affordances (create/delete/reparent) are
|
|
40
|
+
* gated by `structure` PROVIDER PRESENCE instead — the flag and the provider
|
|
41
|
+
* cannot disagree that way.
|
|
42
|
+
*/
|
|
43
|
+
export interface AuthoringCapabilities {
|
|
44
|
+
transform: boolean;
|
|
45
|
+
inspectorFields: boolean;
|
|
46
|
+
persist: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The adapter's own answer to "what is truth behind these rows, and how
|
|
51
|
+
* writable is it" (spec 29 §5). Provenance never varies WITHIN one adapter's
|
|
52
|
+
* rows — it is a property of the world boundary — so it is declared ONCE per
|
|
53
|
+
* adapter and rendered at the SEAM (the composite's `world:<id>` group row,
|
|
54
|
+
* or the panel header for a bare adapter), never per row. Only the adapter
|
|
55
|
+
* knows its truth: the shell must read this field, never guess from adapter
|
|
56
|
+
* identity (rule zero). Machine-readable operation gates stay in {@link
|
|
57
|
+
* AuthoringCapabilities}/provider presence — this type carries the HUMAN
|
|
58
|
+
* explanation those gates can point at when an affordance is present but
|
|
59
|
+
* unavailable.
|
|
60
|
+
*/
|
|
61
|
+
export interface AuthoringProvenance {
|
|
62
|
+
/**
|
|
63
|
+
* What the rows project:
|
|
64
|
+
* - `document` — an authored data file the editor owns (e.g. a scene file);
|
|
65
|
+
* - `source-code` — the game's own source is the document (e.g. JSX);
|
|
66
|
+
* - `foreign` — a live tree the editor does not own (an unmodified external
|
|
67
|
+
* game, or any world edited only through an overlay); edits go to the
|
|
68
|
+
* overlay, never the source;
|
|
69
|
+
* - `live` — a running game adopted at play time; ordinary edits are not persisted;
|
|
70
|
+
* - `boundary` — a declared world with no live editing surface here.
|
|
71
|
+
*/
|
|
72
|
+
source: 'document' | 'source-code' | 'foreign' | 'live' | 'boundary';
|
|
73
|
+
/** Short badge text the shell shows verbatim at the seam (e.g. "scene",
|
|
74
|
+
* "jsx", "overlay", "read-only", "live"). */
|
|
75
|
+
label: string;
|
|
76
|
+
/** One sentence explaining the truth/writability — shown as the seam
|
|
77
|
+
* badge's tooltip and as the reason on disabled affordances. */
|
|
78
|
+
detail: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Semantic place a node occupies in the universal authoring hierarchy.
|
|
83
|
+
*
|
|
84
|
+
* `kind` below remains the adapter's native kind (`mesh`, `button`,
|
|
85
|
+
* `pixi-container`, ...). `role` says what the row MEANS to the editor shell,
|
|
86
|
+
* so presentation and interaction never have to infer semantics from an id,
|
|
87
|
+
* label, or substrate-specific kind string.
|
|
88
|
+
*/
|
|
89
|
+
export type EditorNodeRole =
|
|
90
|
+
| 'folder'
|
|
91
|
+
| 'root'
|
|
92
|
+
| 'document'
|
|
93
|
+
| 'story'
|
|
94
|
+
| 'component'
|
|
95
|
+
| 'instance'
|
|
96
|
+
| 'element'
|
|
97
|
+
| 'entity'
|
|
98
|
+
| 'boundary';
|
|
99
|
+
|
|
100
|
+
/** A node in the authoring hierarchy — format-neutral. */
|
|
101
|
+
export interface EditorNode {
|
|
102
|
+
/** STABLE id — survives reload (see ingest structural-path ids). */
|
|
103
|
+
id: string;
|
|
104
|
+
label: string;
|
|
105
|
+
/** Adapter-declared semantic role. Optional for third-party/older adapters;
|
|
106
|
+
* the shell falls back to the native `kind` only for icon selection. */
|
|
107
|
+
role?: EditorNodeRole;
|
|
108
|
+
/** Optional source-owned context kept visually subordinate to `label`, such
|
|
109
|
+
* as `<header>` beneath a React component name or a document's file path. */
|
|
110
|
+
secondaryLabel?: string;
|
|
111
|
+
kind: 'mesh' | 'light' | 'camera' | 'audio' | 'group' | 'object' | (string & {});
|
|
112
|
+
/**
|
|
113
|
+
* What to CALL this node's type in the inspector header — the component or
|
|
114
|
+
* class an author thinks of it as, when that differs from the native `kind`
|
|
115
|
+
* the shell uses to pick an icon. An `<Enemy>` renders a `group`, but "group"
|
|
116
|
+
* is a fact about its implementation, not its identity; Godot names the class
|
|
117
|
+
* here and Unity names the prefab. Adapters that omit it keep showing `kind`.
|
|
118
|
+
*/
|
|
119
|
+
typeLabel?: string;
|
|
120
|
+
parentId: string | null;
|
|
121
|
+
childIds: string[];
|
|
122
|
+
/**
|
|
123
|
+
* THIS ADAPTER'S OWN DEFAULT OPEN STATE for this row, overriding the shell's
|
|
124
|
+
* shape heuristic (`GameHierarchy.tsx`'s `defaultExpansionByNode`, which
|
|
125
|
+
* infers openness from authored depth and branch shape).
|
|
126
|
+
*
|
|
127
|
+
* It exists because one adapter genuinely knows better than any shape rule
|
|
128
|
+
* can: Blender's Outliner has an explicit per-element flag
|
|
129
|
+
* (`outliner_tree.cc:139` makes every fresh element CLOSED; only the Scene
|
|
130
|
+
* Collection and the layer collections clear it,
|
|
131
|
+
* `tree_display_view_layer.cc:130,167`), so "collections open, objects
|
|
132
|
+
* closed" is a fact about the tree rather than a guess about it. Omitted —
|
|
133
|
+
* which is every other adapter — leaves the heuristic exactly as it was.
|
|
134
|
+
*/
|
|
135
|
+
defaultExpanded?: boolean;
|
|
136
|
+
/** Opaque project-owned identity shared by authored rows across adapter roots. */
|
|
137
|
+
crossSurfaceId?: string;
|
|
138
|
+
/** Optional authored parent identity resolved against another row's cross-surface id. */
|
|
139
|
+
crossSurfaceParentId?: string;
|
|
140
|
+
/** Source-owned sibling ordinal used only when native rows are joined across surfaces. */
|
|
141
|
+
crossSurfaceOrder?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Live source-owned label for the projected group containing this row's adapter root.
|
|
144
|
+
* Any row may carry it: a native surface can have no semantic root of its own when all of
|
|
145
|
+
* its authored rows are children of entities owned by another surface.
|
|
146
|
+
*/
|
|
147
|
+
crossSurfaceGroupLabel?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface PropertyDescriptor {
|
|
151
|
+
path: string;
|
|
152
|
+
label: string;
|
|
153
|
+
type: 'string' | 'number' | 'boolean' | 'vec3' | 'color' | 'enum' | 'asset' | 'json';
|
|
154
|
+
readonly?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Why this field cannot be written, in the adapter's own vocabulary. The
|
|
157
|
+
* Inspector, its control surface, and the write refusal all project this
|
|
158
|
+
* same sentence; omitting it uses the generic read-only explanation.
|
|
159
|
+
*/
|
|
160
|
+
readonlyReason?: string;
|
|
161
|
+
options?: unknown[];
|
|
162
|
+
/**
|
|
163
|
+
* Optional domain-shaped grouping (T3.4 slice 2): properties sharing the
|
|
164
|
+
* same `group` label render together under a titled sub-section in the
|
|
165
|
+
* generic inspector, instead of the flat property grid. Backward-compatible
|
|
166
|
+
* — omitted (or two descriptors with different/no `group`) renders exactly
|
|
167
|
+
* as before this field existed. This is the ONLY authoring-richness
|
|
168
|
+
* addition v1 makes; there is no schema and no new PropertyDescriptor
|
|
169
|
+
* `type` — a custom adapter reports its own domain concepts
|
|
170
|
+
* (health/score/an enum, …) through the SAME provider, merely labeled into
|
|
171
|
+
* a named group.
|
|
172
|
+
*/
|
|
173
|
+
group?: string;
|
|
174
|
+
/**
|
|
175
|
+
* The value shown is the DECLARED DEFAULT — the document does not carry this
|
|
176
|
+
* property, the thing that owns it does. Every mainstream inspector
|
|
177
|
+
* distinguishes these (Unity greys an unmodified field and bolds an
|
|
178
|
+
* overridden one) because otherwise you cannot tell what a document actually
|
|
179
|
+
* says from what it merely inherits. Editing a defaulted property writes it
|
|
180
|
+
* into the document for the first time.
|
|
181
|
+
*/
|
|
182
|
+
defaulted?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* This property can be REMOVED from the document, reverting it to the
|
|
185
|
+
* declared default — Unity's revert arrow, Godot's reset arrow. Only set when
|
|
186
|
+
* removal is legal: the owner declares the property optional (or gives it a
|
|
187
|
+
* default), so dropping it leaves a still-valid document. The inverse of
|
|
188
|
+
* editing a `defaulted` property.
|
|
189
|
+
*/
|
|
190
|
+
resettable?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* The SOURCE TEXT of the default a revert returns this property to (`1.55`,
|
|
193
|
+
* `'raider'`), so the revert affordance can name its destination instead of
|
|
194
|
+
* saying "default" and hoping.
|
|
195
|
+
*
|
|
196
|
+
* Set ONLY when the owner actually declares a default the adapter can read.
|
|
197
|
+
* Absence is the honest, common case: an optional property with no declared
|
|
198
|
+
* default is still removable — what takes over is then the owner's own
|
|
199
|
+
* internal fallback or nothing at all, which this side does not know. Never
|
|
200
|
+
* fill it with the property's CURRENT value: that is what the author is
|
|
201
|
+
* discarding, not what they get back.
|
|
202
|
+
*/
|
|
203
|
+
revertsTo?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface HierarchyProvider {
|
|
207
|
+
roots(): EditorNode[];
|
|
208
|
+
node(id: string): EditorNode | null;
|
|
209
|
+
/**
|
|
210
|
+
* OPTIONAL semantic-join revision. A composite hierarchy uses explicit
|
|
211
|
+
* `crossSurface*` fields to join native roots; this signature changes if and
|
|
212
|
+
* only if those fields, their semantic ancestry, or their order changed.
|
|
213
|
+
* Ordinary native-only child churn may leave it stable. `null` explicitly
|
|
214
|
+
* means this provider has no semantic participation. Omission means the
|
|
215
|
+
* composite must conservatively re-read the provider's whole hierarchy.
|
|
216
|
+
*/
|
|
217
|
+
crossSurfaceStructureSignature?(): string | null;
|
|
218
|
+
/**
|
|
219
|
+
* OPTIONAL, THREE-SURFACE — the live scene object for raycast/gizmo binding
|
|
220
|
+
* (null for a node this adapter has no object for), opaque here: its Three
|
|
221
|
+
* typing is `@volter/editor-threejs`'s `threeHierarchy`. A Pixi, DOM or React
|
|
222
|
+
* adapter has no scene object at all: it OMITS this method rather than
|
|
223
|
+
* implementing a `() => null` stub that pretends the concept applies. Callers
|
|
224
|
+
* already treat `null` as "no object here", so absence and `null` mean the
|
|
225
|
+
* same thing to them — optional-chain it (`hierarchy.object3D?.(id) ?? null`).
|
|
226
|
+
*/
|
|
227
|
+
object3D?(id: string): unknown;
|
|
228
|
+
/** OPTIONAL, THREE-SURFACE — inverse of {@link object3D}. Same rule: an
|
|
229
|
+
* adapter with no scene-object tree omits it. */
|
|
230
|
+
idForObject3D?(o: unknown): string | null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface SelectionProvider {
|
|
234
|
+
get(): string[];
|
|
235
|
+
/** Set the selected authoring subjects. `exact` is for projections that
|
|
236
|
+
* already name a real node (for example a folded-internal light inventory);
|
|
237
|
+
* the default keeps the adapter's normal semantic component resolution. */
|
|
238
|
+
set(ids: string[], options?: { intent?: 'semantic' | 'exact' }): void;
|
|
239
|
+
/** Resolve a raw native/render hit to the semantic authoring subject. The
|
|
240
|
+
* shell must not infer ownership from ancestry or substrate-specific kinds.
|
|
241
|
+
* Older adapters may omit this; raw ids then remain the selection ids. */
|
|
242
|
+
resolve?(
|
|
243
|
+
rawId: string,
|
|
244
|
+
options?: { intent?: 'normal' | 'deep'; scopeId?: string | null },
|
|
245
|
+
): SelectionResolution | null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Adapter-owned result of semantic selection resolution: the raw native/render
|
|
249
|
+
* hit mapped to the node the author actually means. ONE id, deliberately:
|
|
250
|
+
* separate inspector/transform/boundary aliases would have to agree, and
|
|
251
|
+
* nothing in the shell can enforce that. */
|
|
252
|
+
export interface SelectionResolution {
|
|
253
|
+
/** Hierarchy selection and Inspector subject. */
|
|
254
|
+
id: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type TransformChannel = 'position' | 'rotation' | 'scale';
|
|
258
|
+
|
|
259
|
+
export interface TransformEditability {
|
|
260
|
+
writable: boolean;
|
|
261
|
+
/** Human-facing explanation for a disabled gizmo/field. */
|
|
262
|
+
reason?: string;
|
|
263
|
+
/**
|
|
264
|
+
* Is there an AUTHORED override of this channel that {@link
|
|
265
|
+
* TransformProvider.remove} could drop? The per-channel analogue of a
|
|
266
|
+
* property descriptor's `resettable`, and it is answered the same way: the
|
|
267
|
+
* document actually carries this channel, and this session has a door that
|
|
268
|
+
* can take it back.
|
|
269
|
+
*
|
|
270
|
+
* It lives HERE rather than being inferred from `remove`'s presence because
|
|
271
|
+
* `remove` is a lane-wide fact while this is a per-subject one — a composite
|
|
272
|
+
* routes both, but only this one can say that THIS node's `position` is
|
|
273
|
+
* authored and THAT node's is not. Omitted ⇒ nothing to drop, and the
|
|
274
|
+
* protocol's removal door refuses by name instead of acking a revert it did
|
|
275
|
+
* not perform.
|
|
276
|
+
*
|
|
277
|
+
* EACH HALF IS ANSWERED BY WHOEVER CAN SEE IT, and a lane that cannot read
|
|
278
|
+
* its own document may not invent the first. A source lane holding the
|
|
279
|
+
* file's text answers both (the first-party R3F lane reads the callsite's
|
|
280
|
+
* attributes before saying yes). A lane whose client holds only an
|
|
281
|
+
* oid → file:line index and writes through a server that resolves the
|
|
282
|
+
* address — the vendored-ingest lane — can honestly answer only the SECOND,
|
|
283
|
+
* so that is what it answers, and `remove`'s own {@link WriteAck} reports
|
|
284
|
+
* whether an attribute was actually dropped (`persisted: false` when the
|
|
285
|
+
* callsite carried none). The inverse is what is forbidden on every lane:
|
|
286
|
+
* acking a drop that took no byte.
|
|
287
|
+
*/
|
|
288
|
+
removable?: boolean;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The one lawful Play→source write: a person explicitly attributes the
|
|
292
|
+
* selected live node's current transform to its OID-anchored source literal.
|
|
293
|
+
* This is an atomic optional group so availability and the write door cannot
|
|
294
|
+
* drift apart. Ordinary transform gestures remain governed by begin/apply/end
|
|
295
|
+
* and therefore stay ephemeral while Play is running. */
|
|
296
|
+
export interface TransformSourceCommitProvider {
|
|
297
|
+
availability(id: string): { available: boolean; reason?: string };
|
|
298
|
+
commit(id: string): Promise<WriteAck>;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface TransformProvider {
|
|
302
|
+
/** Honest inspector presentation for the substrate's transform vocabulary. */
|
|
303
|
+
dimensions?(id: string): '2d' | '3d' | null;
|
|
304
|
+
get(id: string): Transform;
|
|
305
|
+
/** Pre-gesture persistence check. Omitted means the adapter's transform
|
|
306
|
+
* capability applies uniformly. A false result must suppress manipulation
|
|
307
|
+
* before the live object is moved, never discover refusal at drag end. */
|
|
308
|
+
editability?(id: string, channel: TransformChannel): TransformEditability;
|
|
309
|
+
/** Pause whatever controller fights the gizmo (physics/script) for editing. */
|
|
310
|
+
beginEdit(id: string): void;
|
|
311
|
+
apply(id: string, t: Transform): void;
|
|
312
|
+
/**
|
|
313
|
+
* Close the gesture — and, for an adapter that persists, ANSWER FOR IT: the
|
|
314
|
+
* returned {@link WriteAck} is this edit's own outcome, produced after the
|
|
315
|
+
* write it triggered has settled. Returning nothing is the honest report of
|
|
316
|
+
* an adapter that performed no write, and the shell then reports the
|
|
317
|
+
* live-only floor rather than borrowing an answer from somewhere else.
|
|
318
|
+
*/
|
|
319
|
+
endEdit(id: string): void | WriteAck | Promise<void | WriteAck>;
|
|
320
|
+
/**
|
|
321
|
+
* Optional — DROP one channel's authored transform entirely, rather than
|
|
322
|
+
* writing a value into it.
|
|
323
|
+
*
|
|
324
|
+
* THE SAME DOOR {@link InspectorProvider.remove} IS, for the channel the
|
|
325
|
+
* Transform section owns instead of a property row. `apply`/`endEdit` write a
|
|
326
|
+
* VALUE, so reverting a channel an authoring gesture APPENDED by writing the
|
|
327
|
+
* old numbers back leaves `position={[0, 0, 0]}` standing where the source
|
|
328
|
+
* carried nothing — the file ends one attribute heavier than it started, and
|
|
329
|
+
* no byte-level edit/revert round trip can close. Only removal expresses that
|
|
330
|
+
* absence, which is why a lane without this door grades UNVERIFIABLE rather
|
|
331
|
+
* than passing.
|
|
332
|
+
*
|
|
333
|
+
* SAME ACK CONTRACT AS {@link endEdit} — the pipe's per-edit {@link WriteAck}
|
|
334
|
+
* for THIS removal, awaited. Removal is a write; it moves bytes, and the
|
|
335
|
+
* caller has the same right to be told where they went. Returning nothing
|
|
336
|
+
* means this provider performed no persisted write and the shell reports the
|
|
337
|
+
* live-only floor.
|
|
338
|
+
*
|
|
339
|
+
* What "absent" MEANS is the dialect writer's to decide (deleting the JSX
|
|
340
|
+
* attribute, for the source lanes). Nothing above this seam infers it: a lane
|
|
341
|
+
* whose transform truth IS the running object — there is no document for the
|
|
342
|
+
* channel to be absent from — simply omits the method, and the protocol door
|
|
343
|
+
* refuses by name rather than falling back to a value write.
|
|
344
|
+
*/
|
|
345
|
+
remove?(id: string, channel: TransformChannel): void | WriteAck | Promise<void | WriteAck>;
|
|
346
|
+
/** Explicit Play→source attribution. Absent outside an eligible live OID
|
|
347
|
+
* surface; never used implicitly by a transform gesture. */
|
|
348
|
+
readonly sourceCommit?: TransformSourceCommitProvider;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export interface InspectorProvider {
|
|
352
|
+
/** Schema-driven — fixed to no document format. */
|
|
353
|
+
properties(id: string): PropertyDescriptor[];
|
|
354
|
+
get(id: string, path: string): unknown;
|
|
355
|
+
/**
|
|
356
|
+
* Write one property — and, for an adapter that persists, ANSWER FOR IT: the
|
|
357
|
+
* returned {@link WriteAck} is this edit's own outcome (same contract as
|
|
358
|
+
* {@link TransformProvider.endEdit}). Returning nothing means no write was
|
|
359
|
+
* performed by this provider, and the shell reports the live-only floor.
|
|
360
|
+
*/
|
|
361
|
+
set(id: string, path: string, value: unknown): void | WriteAck | Promise<void | WriteAck>;
|
|
362
|
+
/**
|
|
363
|
+
* Optional — SHOW a value without writing it, for a control the user drags.
|
|
364
|
+
*
|
|
365
|
+
* The gizmo already has this pair ({@link TransformProvider.writeTransform}
|
|
366
|
+
* live, {@link TransformProvider.endEdit} persisting), and a colour picker
|
|
367
|
+
* needs the same shape: it streams values continuously while the pointer
|
|
368
|
+
* moves, and persisting each one is a write burst the absorb cycle cannot
|
|
369
|
+
* follow. So the control previews here on every change and calls
|
|
370
|
+
* {@link set} ONCE when the gesture ends.
|
|
371
|
+
*
|
|
372
|
+
* A preview NEVER persists and is never the truth: whatever the adapter
|
|
373
|
+
* mutates is discarded by the next remount unless a `set` follows. An
|
|
374
|
+
* adapter that cannot show a value without writing it simply omits this,
|
|
375
|
+
* and the control's preview is a no-op — the value still lands on commit.
|
|
376
|
+
*/
|
|
377
|
+
preview?(id: string, path: string, value: unknown): void;
|
|
378
|
+
/** Optional per-subject write preflight for shell affordances outside the
|
|
379
|
+
* Inspector field grid (notably hierarchy eye/lock controls). The property
|
|
380
|
+
* descriptor remains the Inspector's source of `readonly`; this gives other
|
|
381
|
+
* generic surfaces the same answer and its adapter-authored reason. */
|
|
382
|
+
editability?(id: string, path: string): { writable: boolean; reason?: string };
|
|
383
|
+
/**
|
|
384
|
+
* Optional — REMOVE a property's authored override entirely (not "set to a
|
|
385
|
+
* value"), letting whatever governs it in its absence take over.
|
|
386
|
+
*
|
|
387
|
+
* THIS IS THE ONLY WAY TO EXPRESS BYTE-ABSENCE. {@link set} writes a VALUE:
|
|
388
|
+
* reverting a prop an authoring gesture ADDED by setting it back to what the
|
|
389
|
+
* default would have been leaves `position={[0, 0, 0]}` in the source where
|
|
390
|
+
* the file previously carried nothing at all. Only removal restores the
|
|
391
|
+
* bytes, which is why a lane with no `remove` cannot close its own
|
|
392
|
+
* edit/revert round trip and grades UNVERIFIABLE rather than passing.
|
|
393
|
+
*
|
|
394
|
+
* SAME ACK CONTRACT AS {@link set} — the pipe's per-edit {@link WriteAck}
|
|
395
|
+
* (`editor/src/authoring/write-pipe.ts`), awaited. Removal is a write; it
|
|
396
|
+
* moves bytes, and the caller has the same right to be told where they went
|
|
397
|
+
* and whether they moved. Returning nothing means this provider performed no
|
|
398
|
+
* persisted write, and the shell reports the live-only floor.
|
|
399
|
+
*
|
|
400
|
+
* What "absent" MEANS is the dialect writer's to decide — deleting a JSX
|
|
401
|
+
* attribute, dropping a CSS longhand so its shorthand cascades. Nothing above
|
|
402
|
+
* this seam infers it. Adapters with no removable-override concept simply
|
|
403
|
+
* omit the method; callers optional-chain, and the protocol door refuses by
|
|
404
|
+
* name rather than falling back to a value write.
|
|
405
|
+
*/
|
|
406
|
+
remove?(id: string, path: string): void | WriteAck | Promise<void | WriteAck>;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* An authored node that is itself an asset, even when its bytes are embedded
|
|
411
|
+
* in another source document instead of living at a standalone project path.
|
|
412
|
+
*
|
|
413
|
+
* The first concrete case is inline SVG in a DOM/React root: its `<path>` and
|
|
414
|
+
* `<g>` implementation is no more part of the authoring hierarchy than the
|
|
415
|
+
* meshes inside an imported model. The adapter owns the native-to-asset
|
|
416
|
+
* conversion; the shell only opens the returned image in its ordinary Asset
|
|
417
|
+
* Editor document.
|
|
418
|
+
*/
|
|
419
|
+
export interface AuthoringAssetSubject {
|
|
420
|
+
readonly kind: 'image';
|
|
421
|
+
readonly name: string;
|
|
422
|
+
readonly mediaType: 'image/svg+xml';
|
|
423
|
+
readonly text: string;
|
|
424
|
+
/** Project-root-relative source definition, when the adapter can prove it. */
|
|
425
|
+
readonly sourcePath?: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface AssetSubjectProvider {
|
|
429
|
+
get(id: string): AuthoringAssetSubject | null;
|
|
430
|
+
/**
|
|
431
|
+
* Assets known to this authoring document even when they are not in its
|
|
432
|
+
* currently focused hierarchy projection. React story boards use this to
|
|
433
|
+
* expose SVGs mounted in inactive story frames without flattening those
|
|
434
|
+
* frames into the active hierarchy.
|
|
435
|
+
*/
|
|
436
|
+
entries?(): ReadonlyArray<{ id: string; subject: AuthoringAssetSubject }>;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** A semantic jump from one authored subject to another document/subject.
|
|
440
|
+
* The adapter owns resolution and opening; the inspector only presents the
|
|
441
|
+
* title and exposes the same verb to human and serialized projections. */
|
|
442
|
+
export interface RelatedSubjectLink {
|
|
443
|
+
readonly id: string;
|
|
444
|
+
readonly title: string;
|
|
445
|
+
open(): void;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface RelatedSubjectsProvider {
|
|
449
|
+
links(id: string): readonly RelatedSubjectLink[];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** One source-owned prop that differs from its component's declared default. */
|
|
453
|
+
export interface ComponentInstanceOverride {
|
|
454
|
+
/** The owning Inspector field path; stable and agent-addressable. */
|
|
455
|
+
readonly path: string;
|
|
456
|
+
readonly label: string;
|
|
457
|
+
readonly value: unknown;
|
|
458
|
+
/** Source text of the component default, when the declaration exposes one. */
|
|
459
|
+
readonly defaultText?: string;
|
|
460
|
+
/** Applying requires a literal callsite value and a literal declared default. */
|
|
461
|
+
readonly canApplyToComponent: boolean;
|
|
462
|
+
readonly applyUnavailableReason?: string;
|
|
463
|
+
/** Placed instances that will inherit this default after the apply. */
|
|
464
|
+
readonly affectedInstanceCount: number;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Derived prefab-instance facts. Nothing in this shape is persisted separately. */
|
|
468
|
+
export interface ComponentInstanceDescription {
|
|
469
|
+
readonly componentName: string;
|
|
470
|
+
readonly sourcePath?: string;
|
|
471
|
+
readonly overrides: readonly ComponentInstanceOverride[];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export interface ComponentInstanceApplyResult {
|
|
475
|
+
readonly changed: boolean;
|
|
476
|
+
readonly message: string;
|
|
477
|
+
/** This operation's own writer acknowledgement. Absent when the request was
|
|
478
|
+
* refused before any write; live-only is an explicit WriteAck. */
|
|
479
|
+
readonly write?: WriteAck;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Source-derived component-instance operations. A provider exists only when an
|
|
484
|
+
* adapter can prove component identity and write the component's native source.
|
|
485
|
+
* No override store is implied: describe is a diff, revert removes callsite
|
|
486
|
+
* props, and apply edits the component declaration itself.
|
|
487
|
+
*/
|
|
488
|
+
export interface ComponentInstancesProvider {
|
|
489
|
+
describe(id: string): ComponentInstanceDescription | null;
|
|
490
|
+
/** Open the native component/prefab board for this instance's surface. */
|
|
491
|
+
openComponent?(id: string): void;
|
|
492
|
+
revert(id: string, paths: readonly string[]): Promise<void | WriteAck>;
|
|
493
|
+
applyToComponent(id: string, path: string): Promise<ComponentInstanceApplyResult>;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* WHAT A STRUCTURAL VERB ANSWERS WITH — the same widening
|
|
498
|
+
* {@link TransformProvider.endEdit} and {@link InspectorProvider.set} carry,
|
|
499
|
+
* for the same reason: the ack is produced by the component that performed
|
|
500
|
+
* THIS edit, and there is nowhere else for a caller to get one.
|
|
501
|
+
*
|
|
502
|
+
* `void` is the honest answer for a lane whose verb performs no write of its
|
|
503
|
+
* own (an in-memory adapter mutating a graph it already owns); a lane that
|
|
504
|
+
* writes returns the persistence pipe's own {@link WriteAck}
|
|
505
|
+
* (`editor/src/authoring/write-pipe.ts`), awaited, so a caller that awaits the
|
|
506
|
+
* verb has awaited the byte.
|
|
507
|
+
*
|
|
508
|
+
* A structural verb resolves on ITS OWN verb: `remove` asks whether the struct
|
|
509
|
+
* writer is bound, never whether the PROP writer is — resolving on a
|
|
510
|
+
* value-write door is how a lane comes to ack `source-prop` for a door it does
|
|
511
|
+
* not have.
|
|
512
|
+
*/
|
|
513
|
+
export type StructuralWriteOutcome = void | WriteAck | Promise<void | WriteAck>;
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* WHAT AN ID-RETURNING STRUCTURAL VERB ANSWERS WITH — the id and the ack, as
|
|
517
|
+
* ONE return.
|
|
518
|
+
*
|
|
519
|
+
* The writing verbs ({@link StructureProvider.remove} and friends) answer with
|
|
520
|
+
* a bare {@link StructuralWriteOutcome}, because the write IS their whole
|
|
521
|
+
* answer. `create`/`duplicate`/`group`/`ungroup` owe their caller a second
|
|
522
|
+
* thing — the id(s) the op produced — and those two halves settle at DIFFERENT
|
|
523
|
+
* TIMES:
|
|
524
|
+
*
|
|
525
|
+
* - the id is SYNCHRONOUS where a lane can mint one. The live canvas surface
|
|
526
|
+
* adds a real display object to the running tree and re-indexes before it
|
|
527
|
+
* returns, so `create` hands back an id its caller can select in the same
|
|
528
|
+
* turn ("create then immediately select it" is the ordinary shell
|
|
529
|
+
* sequence). Making the verb `async` to carry the ack would take that
|
|
530
|
+
* away from every caller to serve a value that is already known.
|
|
531
|
+
* - the ack is ASYNCHRONOUS wherever a byte is involved, and it is the only
|
|
532
|
+
* thing that says whether the edit reached the file it claims.
|
|
533
|
+
*
|
|
534
|
+
* So the return carries both. A source lane whose new id only exists after the
|
|
535
|
+
* remount stamps it answers `id: ''` (there is no honest synchronous id) with a
|
|
536
|
+
* real `ack`; a router that could not route answers with an unchanged id and
|
|
537
|
+
* `ack: undefined` (no write was attempted, which is exactly what `void` means
|
|
538
|
+
* in {@link StructuralWriteOutcome}).
|
|
539
|
+
*
|
|
540
|
+
* Firing the write and dropping the promise on the floor — `void structOp(…)`
|
|
541
|
+
* behind a `return ''` — is the shape this type exists to make unrepresentable:
|
|
542
|
+
* it left the caller unable to await the byte, which is how N same-file
|
|
543
|
+
* duplicates went out together, each reading the file before any wrote back.
|
|
544
|
+
*/
|
|
545
|
+
export interface StructuralIdWrite<Id extends string | null = string> {
|
|
546
|
+
/** The id the op produced — `''` when the lane cannot know it until its own
|
|
547
|
+
* source write completes and the world remounts. */
|
|
548
|
+
readonly id: Id;
|
|
549
|
+
readonly ack: StructuralWriteOutcome;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** The plural half of {@link StructuralIdWrite}, for a verb that promotes a set
|
|
553
|
+
* of ids rather than minting one. */
|
|
554
|
+
export interface StructuralIdsWrite {
|
|
555
|
+
readonly ids: readonly string[];
|
|
556
|
+
readonly ack: StructuralWriteOutcome;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Clipboard-backed structural writes retain the existing `false` refusal
|
|
560
|
+
* channel while answering a successful cut/paste with the write's real
|
|
561
|
+
* destination. A bare `true` cannot prove a byte landed anywhere. */
|
|
562
|
+
export type StructuralClipboardOutcome = false | WriteAck | Promise<false | WriteAck>;
|
|
563
|
+
|
|
564
|
+
/** One kind `create` accepts: its label, and optionally what it is (a create dialog's Description)
|
|
565
|
+
* and the kind it extends (the dialog's class tree, as Godot's Create New Node shows it). */
|
|
566
|
+
export interface CreatableKind {
|
|
567
|
+
kind: string;
|
|
568
|
+
label: string;
|
|
569
|
+
description?: string;
|
|
570
|
+
extends?: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export interface StructureProvider {
|
|
574
|
+
/** Create a `kind` under `parentId`, answering with the new id AND this
|
|
575
|
+
* creation's own write ack (see {@link StructuralIdWrite}). `at` places it: a
|
|
576
|
+
* point in the frame this adapter's `rects` answer in (a 2D editor's "add node
|
|
577
|
+
* where I clicked"); absent, the kind's own default place. */
|
|
578
|
+
create(kind: string, parentId?: string, at?: { readonly x: number; readonly y: number }): StructuralIdWrite;
|
|
579
|
+
/**
|
|
580
|
+
* Remove `id`. May optionally return an awaitable when the
|
|
581
|
+
* underlying write is asynchronous (e.g. a react-world source-file edit) —
|
|
582
|
+
* `deleteSelection` (`editor-hotkeys.ts`) awaits it per id so a same-file
|
|
583
|
+
* multi-delete's writes land strictly one at a time (see that function's own
|
|
584
|
+
* doc comment for why serialization + deletion order together are what make
|
|
585
|
+
* a per-id loop sound for an adapter whose OID index isn't reindexed between
|
|
586
|
+
* writes). An adapter with a synchronous/in-memory remove (e.g.
|
|
587
|
+
* `UIAuthoringAdapter`) returns `void` — `await`ing it is a harmless no-op.
|
|
588
|
+
*
|
|
589
|
+
* A source-writing lane returns THIS removal's own {@link WriteAck} (see
|
|
590
|
+
* {@link StructuralWriteOutcome}).
|
|
591
|
+
*/
|
|
592
|
+
remove(id: string): StructuralWriteOutcome;
|
|
593
|
+
/** Copy `id`, answering with the copy's id AND this duplication's own write
|
|
594
|
+
* ack (see {@link StructuralIdWrite}). A source lane whose copy has no id
|
|
595
|
+
* until the remount stamps it answers with the SOURCE id it was given —
|
|
596
|
+
* `duplicate` has no refusal channel, so an empty string there would be
|
|
597
|
+
* indistinguishable from "refused". */
|
|
598
|
+
duplicate(id: string): StructuralIdWrite;
|
|
599
|
+
/** Duplicate a selection as one native operation and select all copies.
|
|
600
|
+
* Preserves relationships within the selection and creates one undo step.
|
|
601
|
+
* Absent means the caller falls back to individual duplicate operations. */
|
|
602
|
+
duplicateMany?(ids: readonly string[]): StructuralWriteOutcome;
|
|
603
|
+
reparent(id: string, newParentId: string | null): StructuralWriteOutcome;
|
|
604
|
+
/** Reorder `id` to sit immediately before `beforeSiblingId` among its siblings
|
|
605
|
+
* (`null` = move to the end). Absent ⇒ the shell has no sibling-reorder UI
|
|
606
|
+
* for this adapter. */
|
|
607
|
+
reorder?(id: string, beforeSiblingId: string | null): StructuralWriteOutcome;
|
|
608
|
+
/**
|
|
609
|
+
* The kinds `create` accepts for a given parent (`null` parentId = a new
|
|
610
|
+
* root), each with a display label — drives the shell's creation palette.
|
|
611
|
+
* Absent ⇒ the palette shows nothing for this adapter, even though `create`
|
|
612
|
+
* itself may still work when called directly (e.g. programmatically, or by
|
|
613
|
+
* an adapter-specific affordance outside the generic palette).
|
|
614
|
+
*/
|
|
615
|
+
creatableKinds?(parentId: string | null): CreatableKind[];
|
|
616
|
+
/** D3 (spec 27 §6) — wrap `id` in a new container element (default tag
|
|
617
|
+
* adapter-chosen, e.g. a `div`), re-parenting `id` as that container's sole
|
|
618
|
+
* child. Absent ⇒ the shell's context menu shows no Wrap item for this
|
|
619
|
+
* adapter. */
|
|
620
|
+
wrap?(id: string, wrapperTag?: string): StructuralWriteOutcome;
|
|
621
|
+
/** D3 (spec 27 §6) — replace `id` with its own children (the inverse of
|
|
622
|
+
* `wrap`). Absent ⇒ the shell's context menu shows no Unwrap item for this
|
|
623
|
+
* adapter. */
|
|
624
|
+
unwrap?(id: string): StructuralWriteOutcome;
|
|
625
|
+
/** Group sibling authoring objects beneath one new spatial parent. The
|
|
626
|
+
* adapter owns transform preservation, persistence, and undo semantics.
|
|
627
|
+
* Answers with the new group id — `null` when the selection cannot be
|
|
628
|
+
* grouped at all — AND this grouping's own write ack (see
|
|
629
|
+
* {@link StructuralIdWrite}). */
|
|
630
|
+
group?(ids: readonly string[]): StructuralIdWrite<string | null>;
|
|
631
|
+
/** Dissolve a plain spatial group, answering with the ids of its promoted
|
|
632
|
+
* children AND this dissolution's own write ack (see
|
|
633
|
+
* {@link StructuralIdsWrite}). Absent means this adapter has no lossless
|
|
634
|
+
* ungroup operation. */
|
|
635
|
+
ungroup?(id: string): StructuralIdsWrite;
|
|
636
|
+
/** Preflight for an Ungroup affordance. Omit when ungroup is never offered. */
|
|
637
|
+
canUngroup?(id: string): boolean;
|
|
638
|
+
/**
|
|
639
|
+
* D4.R2 (spec27 §6 D4 reopen) — remove every id in `ids` as ONE undoable
|
|
640
|
+
* op (a single Ctrl+Z restores the whole batch), instead of the caller
|
|
641
|
+
* looping `remove(id)` per id (which — one `remove` call = one undo push
|
|
642
|
+
* per adapter, by design — produces N separate undo entries: Ctrl+Z then
|
|
643
|
+
* restores them one at a time, asymmetric with the first-party adapter's
|
|
644
|
+
* own single batched multi-delete undo). Absent ⇒ the shell falls back to
|
|
645
|
+
* the per-id `remove` loop (today's N-entry behavior, unchanged) — an
|
|
646
|
+
* honest degrade, not a silent behavior change, for any adapter that
|
|
647
|
+
* hasn't implemented batching. May optionally return an awaitable, same
|
|
648
|
+
* widening `remove` documents above ({@link StructuralWriteOutcome}) and for
|
|
649
|
+
* the same reason (delete-order-residual fix): `deleteSelection`
|
|
650
|
+
* (`editor-hotkeys.ts`) awaits it so the caller can rely on the whole
|
|
651
|
+
* batch's write having landed before it returns. A synchronous/in-memory
|
|
652
|
+
* implementation (e.g. `UIAuthoringAdapter`'s) returns `void` — awaiting it
|
|
653
|
+
* is a harmless no-op.
|
|
654
|
+
*/
|
|
655
|
+
removeMany?(ids: readonly string[]): StructuralWriteOutcome;
|
|
656
|
+
/** Copy authored entities through the host clipboard. The adapter owns the
|
|
657
|
+
* native payload (JSX for a source-backed Three/Canvas world, native records
|
|
658
|
+
* for a data-backed adapter); the shell never serializes an engine-private
|
|
659
|
+
* shape. */
|
|
660
|
+
copy?(ids: readonly string[]): boolean | Promise<boolean>;
|
|
661
|
+
/** Honest affordance preflight. Omit when copy is never offered. */
|
|
662
|
+
canCopy?(ids: readonly string[]): boolean;
|
|
663
|
+
/** Copy, then remove the same authored entities only after the clipboard
|
|
664
|
+
* write succeeds. One adapter operation owns that ordering. */
|
|
665
|
+
cut?(ids: readonly string[]): StructuralClipboardOutcome;
|
|
666
|
+
/** Paste as children of `parentId`; `null` means the adapter document root.
|
|
667
|
+
* The shell derives the current selection's parent so ordinary Paste creates
|
|
668
|
+
* siblings, matching scene-hierarchy editors. */
|
|
669
|
+
paste?(parentId: string | null): StructuralClipboardOutcome;
|
|
670
|
+
/** Honest affordance preflight. It need only cover adapter/session state;
|
|
671
|
+
* the actual paste still validates the live system clipboard. */
|
|
672
|
+
canPaste?(parentId: string | null): boolean;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* PersistenceProvider — whether and where an adapter's edits persist (design).
|
|
677
|
+
* The ACTIVE adapter's provider is the ONE place this is decided — the host
|
|
678
|
+
* (editor) never hard-codes a destination or guards on session flags; it asks the
|
|
679
|
+
* provider.
|
|
680
|
+
*/
|
|
681
|
+
export interface PersistenceProvider {
|
|
682
|
+
isDirty(): boolean;
|
|
683
|
+
/** The last persistence failure that still requires attention. Successful
|
|
684
|
+
* subsequent persistence clears it. Auto-saving source adapters use this to
|
|
685
|
+
* report a rolled-back source write without inventing a dirty document. */
|
|
686
|
+
lastError?(): string | null;
|
|
687
|
+
/** Save to the adapter's OWN source of truth (its source file, its own
|
|
688
|
+
* data, …) — whatever that adapter defines it to be. */
|
|
689
|
+
save(): Promise<void>;
|
|
690
|
+
/**
|
|
691
|
+
* Human/agent-readable destination this provider persists to — e.g.
|
|
692
|
+
* `"src/world.tsx"`, `"live-only (not saved)"`, or `"ephemeral
|
|
693
|
+
* (discarded on stop)"`. Drives the save-status UI and makes routing
|
|
694
|
+
* inspectable (design §2). Implementers whose destination can change during
|
|
695
|
+
* the session (a provider that tracks the currently focused document, say)
|
|
696
|
+
* should expose this as a live getter rather than a value captured once.
|
|
697
|
+
*/
|
|
698
|
+
readonly destination: string;
|
|
699
|
+
/**
|
|
700
|
+
* The reload contract (design §5): apply an external change to this
|
|
701
|
+
* provider's persisted artifact into the RUNNING session (e.g. a file-watcher
|
|
702
|
+
* update to the artifact). Absent ⇒ the host must remount to
|
|
703
|
+
* pick up external changes — the honest floor, and correctly what the
|
|
704
|
+
* ephemeral provider reports.
|
|
705
|
+
*
|
|
706
|
+
* `rawContent`, when the host has it (the file-watcher SSE payload carries the
|
|
707
|
+
* artifact's exact bytes), is an OPTIONAL second parameter enabling own-echo
|
|
708
|
+
* detection — an event that is exactly what this provider itself last wrote is
|
|
709
|
+
* its own save reflected back, and should be skipped even outside the normal
|
|
710
|
+
* debounce window (see `EditorStore.applyExternalUpdate`'s doc comment). This
|
|
711
|
+
* stays optional/provider-specific — only the first-party provider currently
|
|
712
|
+
* uses it — so other implementers can ignore the parameter entirely.
|
|
713
|
+
*/
|
|
714
|
+
applyExternal?(content: unknown, rawContent?: string): void;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** D12 — per-layer viewport picking. Coordinates are client (browser) px. */
|
|
718
|
+
export interface PickProvider {
|
|
719
|
+
pick(clientX: number, clientY: number): string | null;
|
|
720
|
+
/**
|
|
721
|
+
* Every authorable subject below the point, frontmost first. The shell uses
|
|
722
|
+
* this for the ordinary scene-editor "pick from overlap" menu; adapters
|
|
723
|
+
* that cannot enumerate an overlap may omit it and still provide the
|
|
724
|
+
* single-hit floor through {@link pick}. `includeLocked` lists nodes the editor's lock keeps
|
|
725
|
+
* clicks away from too (Godot's Alt+RMB: "all nodes … including locked").
|
|
726
|
+
*/
|
|
727
|
+
candidates?(clientX: number, clientY: number, options?: { includeLocked?: boolean }): readonly string[];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** D4 — storybook stories. */
|
|
731
|
+
export interface StoryRef {
|
|
732
|
+
id: string;
|
|
733
|
+
label: string;
|
|
734
|
+
}
|
|
735
|
+
export interface StoriesProvider {
|
|
736
|
+
storiesFor(nodeId: string): StoryRef[]; // [] = none
|
|
737
|
+
active(nodeId: string): string | null;
|
|
738
|
+
apply(nodeId: string, storyId: string | null): void; // null clears
|
|
739
|
+
/** Render ONLY this node against the story (storybook canvas); null exits. */
|
|
740
|
+
isolate?(nodeId: string | null, storyId?: string): void;
|
|
741
|
+
/**
|
|
742
|
+
* Display vocabulary for this provider's section — e.g. an ingested game's
|
|
743
|
+
* contract scenes say "Scenes"; absent means the default "Stories". The
|
|
744
|
+
* states are the same seam either way; only the word the human reads
|
|
745
|
+
* changes, so a game's own screens are never labelled with a design-time
|
|
746
|
+
* term the game never uses.
|
|
747
|
+
*/
|
|
748
|
+
readonly title?: string;
|
|
749
|
+
/**
|
|
750
|
+
* Why this provider could not enumerate, or `null` when it did. An empty
|
|
751
|
+
* `storiesFor` and an UNREAD one look identical from the section that draws
|
|
752
|
+
* them, and only the provider knows which it is — so the PROVIDER answers,
|
|
753
|
+
* and the shell never reaches past it into one particular provider's own
|
|
754
|
+
* discovery to find out (a CSF registry, a game contract's screen list).
|
|
755
|
+
* Absent means the provider always ran.
|
|
756
|
+
*/
|
|
757
|
+
unavailable?(): string | null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Where a drop landed, when the shell knows. The VIEWPORT knows a world point
|
|
762
|
+
* (its ground-plane raycast under the cursor); the hierarchy panel does not,
|
|
763
|
+
* and passes nothing. Optional both ways: an adapter with no placement concept
|
|
764
|
+
* ignores it, and a caller with no point omits it — so neither side has to
|
|
765
|
+
* pretend. Coordinates are world-space, in the adapter's own units.
|
|
766
|
+
*/
|
|
767
|
+
export interface AssetDropContext {
|
|
768
|
+
readonly position?: readonly [number, number, number];
|
|
769
|
+
/** Transient drag metadata. The project's native source remains truth; this
|
|
770
|
+
* only tells an adapter whether the path names a file or a source component. */
|
|
771
|
+
readonly item?:
|
|
772
|
+
| { readonly kind: 'file'; readonly name: string }
|
|
773
|
+
| {
|
|
774
|
+
readonly kind: 'component';
|
|
775
|
+
readonly name: string;
|
|
776
|
+
readonly sourcePath: string;
|
|
777
|
+
readonly exportKind: 'default' | 'named';
|
|
778
|
+
readonly surface: 'three' | 'canvas' | 'dom' | 'unknown';
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Asset drop (hierarchy + viewport). */
|
|
783
|
+
export interface AssetDropProvider {
|
|
784
|
+
accepts(nodeId: string, assetPath: string, context?: AssetDropContext): boolean;
|
|
785
|
+
/** A drop that lands as an ELEMENT in the game's own source is a structural
|
|
786
|
+
* write, so it answers with the same {@link StructuralWriteOutcome} the rest
|
|
787
|
+
* of {@link StructureProvider}'s writing verbs do — a lane whose drop only
|
|
788
|
+
* mutates the running graph returns `void`. */
|
|
789
|
+
drop(nodeId: string, assetPath: string, context?: AssetDropContext): StructuralWriteOutcome;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Plain-object rect shape shared by {@link RectProvider} — the same fields a real
|
|
793
|
+
* `DOMRect` carries (a subset, so a real `DOMRect` satisfies this structurally too).
|
|
794
|
+
* See {@link RectProvider} for the coordinate space (host-relative, not viewport). */
|
|
795
|
+
export interface DOMRectLike {
|
|
796
|
+
x: number;
|
|
797
|
+
y: number;
|
|
798
|
+
width: number;
|
|
799
|
+
height: number;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Per-node screen geometry — the KEYSTONE omission today (nothing produces rects the
|
|
803
|
+
* ported inspect.ts math consumes). Coordinates are **host-relative** px: relative to
|
|
804
|
+
* the adapter's own mounted world surface (its `position:absolute; inset:0` layer), the
|
|
805
|
+
* same surface the A3 selection overlay is hosted over, so the overlay can draw against
|
|
806
|
+
* these rects directly. (NOT viewport-client — the JSDoc formerly said so in error.) */
|
|
807
|
+
export interface RectProvider {
|
|
808
|
+
/** selectable node id → its current bounding rect, or null if unmounted/offscreen */
|
|
809
|
+
rect(id: string): { x: number; y: number; width: number; height: number } | null;
|
|
810
|
+
/** rects of layout-relevant neighbours, for snap/measure/box-model overlays */
|
|
811
|
+
contextRects?(id: string): {
|
|
812
|
+
parent?: DOMRectLike;
|
|
813
|
+
siblings?: DOMRectLike[];
|
|
814
|
+
paddingBox?: DOMRectLike;
|
|
815
|
+
};
|
|
816
|
+
/**
|
|
817
|
+
* D3 (spec 27 §6) — every currently-empty container this adapter's tree
|
|
818
|
+
* contains right now: no visible children, no text, a collapsed dimension
|
|
819
|
+
* (the `ui-source/inspect.ts:findEmptyContainers` math) — each with a
|
|
820
|
+
* grown-to-tappable placeholder rect and a display name, driving the
|
|
821
|
+
* overlay's dashed empty-container hint. Host-relative, same coordinate
|
|
822
|
+
* space as {@link rect}. Absent ⇒ this adapter has no "empty container"
|
|
823
|
+
* concept (e.g. an adapter with no structural containers at all) — no
|
|
824
|
+
* hints are drawn for it.
|
|
825
|
+
*/
|
|
826
|
+
emptyContainers?(): { id: string; rect: DOMRectLike; displayName: string }[];
|
|
827
|
+
/**
|
|
828
|
+
* The node's own box as its transform carries it — the four corners of its local bounds, in the
|
|
829
|
+
* frame {@link rect} answers in. A turned or skewed node's corners are not an axis-aligned
|
|
830
|
+
* rectangle, and the overlay draws its selection frame on them, as Godot's 2D editor does.
|
|
831
|
+
* Absent ⇒ the overlay frames {@link rect}.
|
|
832
|
+
*/
|
|
833
|
+
frame?(id: string): FrameCorners | null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** A node box's corners: top-left, top-right, bottom-right and bottom-left of its local bounds. */
|
|
837
|
+
export interface FrameCorners {
|
|
838
|
+
readonly tl: { readonly x: number; readonly y: number };
|
|
839
|
+
readonly tr: { readonly x: number; readonly y: number };
|
|
840
|
+
readonly br: { readonly x: number; readonly y: number };
|
|
841
|
+
readonly bl: { readonly x: number; readonly y: number };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** One adapter-owned reference point rendered by the shared world overlay.
|
|
845
|
+
* Coordinates are host-relative px, the same frame as {@link RectProvider}.
|
|
846
|
+
* A provider deliberately exposes at most one point for a node so coincident
|
|
847
|
+
* native concepts (for example an anchor and pivot) never become ambiguous. */
|
|
848
|
+
export interface BoxEditReferencePoint {
|
|
849
|
+
/** True when the native source constrains this point (for example a
|
|
850
|
+
* normalized 0..1 Sprite anchor). The overlay then uses only exact native
|
|
851
|
+
* snap points, never an unreachable axis-aligned bounding-box target. */
|
|
852
|
+
bounded?: boolean;
|
|
853
|
+
id: string;
|
|
854
|
+
kind: 'anchor' | 'pivot';
|
|
855
|
+
label: string;
|
|
856
|
+
/** Exact host-space points the native reference can reach and should snap
|
|
857
|
+
* to. Useful for transformed corner/edge/center grids on rotated content. */
|
|
858
|
+
snapPoints?: ReadonlyArray<{ x: number; y: number }>;
|
|
859
|
+
x: number;
|
|
860
|
+
y: number;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** Spatial gesture → source/data write for native 2D boxes (DOM or canvas).
|
|
864
|
+
* Distinct from the 3D TransformProvider. begin/apply/end bracket a single
|
|
865
|
+
* undo step; apply is live-preview. */
|
|
866
|
+
export interface BoxEditProvider {
|
|
867
|
+
begin(id: string): void;
|
|
868
|
+
/** patch: any of x,y,width,height,marginTop… paddingLeft… — px deltas or absolutes.
|
|
869
|
+
* `referenceX`/`referenceY` are the absolute host-relative coordinates of
|
|
870
|
+
* the active {@link referencePoint}; only providers exposing that optional
|
|
871
|
+
* capability receive those keys. The provider owns conversion to native
|
|
872
|
+
* source units and any placement-preserving compensation. */
|
|
873
|
+
apply(id: string, patch: Record<string, number>): void;
|
|
874
|
+
end(id: string): void;
|
|
875
|
+
/** Native transform origin for the viewport gizmo, in the same host-space
|
|
876
|
+
* frame as {@link RectProvider}. A 2D scene uses this instead of guessing
|
|
877
|
+
* from the visual bounds: sprites can have anchors, containers can have
|
|
878
|
+
* pivots, and neither necessarily sits at the box center. */
|
|
879
|
+
gizmoOrigin?(id: string): { x: number; y: number } | null;
|
|
880
|
+
/** Optional single native anchor/pivot affordance for this node. */
|
|
881
|
+
referencePoint?(id: string): BoxEditReferencePoint | null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Text-content editing, ON the contract: the react-world adapter implements this
|
|
885
|
+
* provider (`react-world-authoring-adapter.ts`'s `readonly text: TextProvider`),
|
|
886
|
+
* bringing its own `editText(id)` write path onto the seam, so the overlay's
|
|
887
|
+
* double-click-to-edit-text (D3) is contract-driven rather than reaching for a
|
|
888
|
+
* concrete adapter's method. */
|
|
889
|
+
export interface TextProvider {
|
|
890
|
+
/** the element's editable pure-text, or null if the body is dynamic/has child elements */
|
|
891
|
+
get(id: string): string | null;
|
|
892
|
+
set(id: string, text: string): void; // dynamic-guarded by the writer
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* D3 (spec 27 §6) — background-color sampling at a viewport point, for the
|
|
897
|
+
* eyedropper's FALLBACK path (browsers with no native `EyeDropper` API). The
|
|
898
|
+
* native API, where available, samples real rendered pixels itself and needs
|
|
899
|
+
* none of this. Distinct from {@link PickProvider} (which returns a node id,
|
|
900
|
+
* not a color) and from `InspectorProvider.get('style.backgroundColor')`
|
|
901
|
+
* (which NORMALIZES to `#rrggbb` and so loses the "transparent" signal a
|
|
902
|
+
* color CHAIN walk needs — see `ui-source/inspect.ts:effectiveColorFromChain`'s
|
|
903
|
+
* doc comment: it walks raw, un-normalized CSS values looking for the first
|
|
904
|
+
* non-transparent one).
|
|
905
|
+
*/
|
|
906
|
+
export interface ColorSampleProvider {
|
|
907
|
+
/** Raw (un-normalized) CSS `background-color` values, hit-element FIRST,
|
|
908
|
+
* walking up its ancestor chain — the exact shape
|
|
909
|
+
* `effectiveColorFromChain` consumes. `null` when nothing is hit at the
|
|
910
|
+
* point. Client (viewport) px, matching {@link PickProvider}. */
|
|
911
|
+
backgroundChainAt(clientX: number, clientY: number): string[] | null;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Where a node's live object CAME FROM in the game's own truth.
|
|
916
|
+
*
|
|
917
|
+
* `display` is the string the editor shows verbatim. The unanchored shape
|
|
918
|
+
* carries a `reason` rather than a blank, because "we don't know" and "a
|
|
919
|
+
* library built it" are different facts and neither may present as silence
|
|
920
|
+
* (the honest-floor rule).
|
|
921
|
+
*
|
|
922
|
+
* TWO ANCHORED KINDS, because a game has two kinds of truth and only one of
|
|
923
|
+
* them is source.
|
|
924
|
+
*
|
|
925
|
+
* - `source` is the original: a `new` expression at a `file:line:col` in the
|
|
926
|
+
* game's own code, which is where an edit to a constructor literal has to be
|
|
927
|
+
* written.
|
|
928
|
+
* - `data` is the sibling a level-based game needs. Its placed cargo — the
|
|
929
|
+
* enemies, the pickups, the objectives — reaches no source literal at all;
|
|
930
|
+
* it is binary records inside a level file that the game parses at load. An
|
|
931
|
+
* authored move of one is therefore an edit to THE GAME'S OWN DATA, and
|
|
932
|
+
* the honest anchor is the data file plus the index of the record that
|
|
933
|
+
* addresses the object inside it.
|
|
934
|
+
*
|
|
935
|
+
* `kind` is an explicit discriminant rather than an inferred one, so a consumer
|
|
936
|
+
* that only understands source anchors fails to compile against a data anchor
|
|
937
|
+
* instead of silently reading `line` off something that has none.
|
|
938
|
+
*
|
|
939
|
+
* A data record is EXACTLY ONE object by construction — an index addresses one
|
|
940
|
+
* record — so the "how many objects did this site construct?" question that
|
|
941
|
+
* governs a source write has no data-side analogue and must not be invented
|
|
942
|
+
* for one.
|
|
943
|
+
*/
|
|
944
|
+
export type NodeCreationSite =
|
|
945
|
+
| { anchored: true; kind: 'source'; file: string; line: number; col: number; display: string }
|
|
946
|
+
| {
|
|
947
|
+
anchored: true;
|
|
948
|
+
kind: 'data';
|
|
949
|
+
/** Project-relative path of the game's own data file, POSIX separators. */
|
|
950
|
+
file: string;
|
|
951
|
+
/** Index of the record inside that file which addresses this object. */
|
|
952
|
+
record: number;
|
|
953
|
+
display: string;
|
|
954
|
+
}
|
|
955
|
+
| { anchored: false; reason: string };
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* WHERE AN AUTHORED WRITE LANDS — the vocabulary of anchor KINDS, and the
|
|
959
|
+
* closed set of them.
|
|
960
|
+
*
|
|
961
|
+
* {@link NodeCreationSite} answers "where did this object come from"; a kind
|
|
962
|
+
* answers the write-side question beside it: WHICH LANE would carry an edit to
|
|
963
|
+
* this node, and therefore WHAT has to be true for that edit to be correct. The
|
|
964
|
+
* two are not the same fact — two nodes can both be `{ kind: 'source' }` at a
|
|
965
|
+
* `file:line` and still be written by different lanes with different gates.
|
|
966
|
+
*
|
|
967
|
+
* Each kind's CORRECTNESS CONTRACT, and the named test that proves it:
|
|
968
|
+
*
|
|
969
|
+
* - `source-prop` — the value is a literal PROP on an element of the game's own
|
|
970
|
+
* source (a JSX attribute the serve-time authoring stamp addresses). Lands at
|
|
971
|
+
* the anchor; survives a cold remount, because the file is truth and the next
|
|
972
|
+
* mount re-derives from it.
|
|
973
|
+
* Proof: `packages/editor/test/oid-source-persistence.test.ts`.
|
|
974
|
+
* - `source-structure` — the edit is not a VALUE at all: it adds, removes,
|
|
975
|
+
* moves or re-nests an ELEMENT of the game's own source (`delete`,
|
|
976
|
+
* `duplicate`, `wrap`/`unwrap`, `create`, `reorder`, `reparent`, `group`).
|
|
977
|
+
* It is a kind rather than a flavour of `source-prop` because it resolves on
|
|
978
|
+
* a DIFFERENT DOOR — the dialect's structural writer, not its attribute
|
|
979
|
+
* writer — and a lane can have one without the other. Claiming `source-prop`
|
|
980
|
+
* for a structural edit is exactly the classifier/writer split the
|
|
981
|
+
* persistence pipe exists to make impossible: it names a value lane that
|
|
982
|
+
* never carried the bytes. Its landing contract adds one gate the prop lane
|
|
983
|
+
* does not have: the write is WHOLE-FILE and checksum-guarded, so an op
|
|
984
|
+
* resolved against a stale element index must fail rather than land at the
|
|
985
|
+
* wrong offset.
|
|
986
|
+
* Proof: `packages/editor/test/structural-write-pipe.test.ts`.
|
|
987
|
+
* - `construction-literal` — the value is a literal in the CONSTRUCTION
|
|
988
|
+
* EXPRESSION that built the object (`new THREE.Mesh(…)` in the game's own
|
|
989
|
+
* served module). Same landing/remount contract, plus one gate the prop lane
|
|
990
|
+
* does not have: a site that constructed more than one object is refused,
|
|
991
|
+
* because rewriting its literal would move all of them.
|
|
992
|
+
* Proof: `packages/editor/test/ingest-source-persistence.test.ts`.
|
|
993
|
+
* - `physics-binding` — the node's own transform props are DEAD: a simulated
|
|
994
|
+
* body writes its matrix every frame from a spawn it reads elsewhere, so the
|
|
995
|
+
* write lands at the literal that binding reads (the component callsite whose
|
|
996
|
+
* props the `useBox(…)`/`<RigidBody>` hook takes). It carries the two
|
|
997
|
+
* contracts above AND ONE MORE: the authored value must SURVIVE RE-SETTLE —
|
|
998
|
+
* after the world re-derives from source and the design-time settle runs, the
|
|
999
|
+
* body must have re-spawned at the AUTHORED pose, not at the old one. Being
|
|
1000
|
+
* silently re-posed by the owner is this kind's whole failure mode, and the
|
|
1001
|
+
* reason it is a kind rather than a flavour of `source-prop`.
|
|
1002
|
+
* Proof: `packages/editor/test/r3f-physics-binding.test.ts`.
|
|
1003
|
+
* - `data-record` — the value is a field of a RECORD in the game's own data
|
|
1004
|
+
* file, addressed by index; no source literal mentions it. Lands in that
|
|
1005
|
+
* file through the game's own declared writer; survives a cold remount for
|
|
1006
|
+
* the same reason source does.
|
|
1007
|
+
* Proof: `packages/editor/test/ingest-source-persistence.test.ts`
|
|
1008
|
+
* ("a level-data record is an anchor too").
|
|
1009
|
+
* - `live-only` — there is no anchor. The edit lands on the running object for
|
|
1010
|
+
* the session and NOTHING else, and the contract is that the surface SAYS SO:
|
|
1011
|
+
* a write ack that claims a destination it did not reach is the lie this kind
|
|
1012
|
+
* exists to keep nameable.
|
|
1013
|
+
* Proof: `packages/editor/test/oid-source-persistence.test.ts`
|
|
1014
|
+
* ("an object with no source stamp is refused by name")
|
|
1015
|
+
* + `packages/editor/test/inspection-write-destination.test.ts`.
|
|
1016
|
+
*/
|
|
1017
|
+
export type WriteAnchorKind =
|
|
1018
|
+
| 'source-prop'
|
|
1019
|
+
| 'source-structure'
|
|
1020
|
+
| 'construction-literal'
|
|
1021
|
+
| 'physics-binding'
|
|
1022
|
+
| 'data-record'
|
|
1023
|
+
| 'live-only';
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* WHAT ONE EDIT DID — the ack the persistence pipe returns for THAT edit, from
|
|
1027
|
+
* the component that actually performed the write.
|
|
1028
|
+
*
|
|
1029
|
+
* It is deliberately NOT a property of an adapter, a session or a surface. The
|
|
1030
|
+
* fact it carries is per-edit, because that is the only granularity at which it
|
|
1031
|
+
* can be true: one composite may hold a source-backed three root beside a
|
|
1032
|
+
* live-only DOM root, and one three root holds body-placed nodes beside
|
|
1033
|
+
* ordinary JSX props. An answer computed from the SURFACE rather than from the
|
|
1034
|
+
* edit reports whichever child happened to be persist-capable — measured on the
|
|
1035
|
+
* vendored racing game, where a three-root edit acked the other root's
|
|
1036
|
+
* destination with `persisted: true`.
|
|
1037
|
+
*
|
|
1038
|
+
* `persisted: false` is an ANSWER, never a failure: `live-only` is a real
|
|
1039
|
+
* anchor kind and the contract of that kind is that the surface SAYS SO. What
|
|
1040
|
+
* is forbidden is a `destination` no byte reached being reported with
|
|
1041
|
+
* `persisted: true`.
|
|
1042
|
+
*
|
|
1043
|
+
* A provider that performs a write returns this; one that has no write to
|
|
1044
|
+
* perform returns nothing and the shell reports the live-only floor. There is
|
|
1045
|
+
* no third answer, and in particular there is no adapter-wide blanket
|
|
1046
|
+
* destination standing in for one — {@link PersistenceProvider.destination}
|
|
1047
|
+
* answers the different question "where do this surface's SAVES go", for the
|
|
1048
|
+
* save-status chrome.
|
|
1049
|
+
*/
|
|
1050
|
+
export interface WriteAck {
|
|
1051
|
+
/** Where THIS edit's bytes landed, in the writer's own words — or the
|
|
1052
|
+
* live-only/ephemeral floor when none did. */
|
|
1053
|
+
readonly destination: string;
|
|
1054
|
+
/** Whether a byte actually moved for THIS edit. */
|
|
1055
|
+
readonly persisted: boolean;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* The exhaustiveness pin, the same shape and for the same reason as
|
|
1060
|
+
* {@link AUTHORING_PROVIDER_PRESENCE}: a `Record` over the union, so a lane that
|
|
1061
|
+
* starts producing a new kind of anchor cannot ship without joining the
|
|
1062
|
+
* vocabulary (the `Record` would be missing a key) and cannot invent a key the
|
|
1063
|
+
* union does not have (excess-property error). The values are `true` and carry
|
|
1064
|
+
* no meaning — the KEYS are the payload.
|
|
1065
|
+
*
|
|
1066
|
+
* What this buys downstream: the per-kind write-reach counts and a probe's
|
|
1067
|
+
* edit-write walk both enumerate {@link WRITE_ANCHOR_KINDS} rather than a
|
|
1068
|
+
* hand-written list, so a new kind arrives already measured and already walked
|
|
1069
|
+
* instead of silently untested.
|
|
1070
|
+
*/
|
|
1071
|
+
const WRITE_ANCHOR_KIND_PRESENCE: Readonly<Record<WriteAnchorKind, true>> = {
|
|
1072
|
+
'source-prop': true,
|
|
1073
|
+
'source-structure': true,
|
|
1074
|
+
'construction-literal': true,
|
|
1075
|
+
'physics-binding': true,
|
|
1076
|
+
'data-record': true,
|
|
1077
|
+
'live-only': true,
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* The vocabulary as an ordered list — most-addressable first, `live-only` last,
|
|
1082
|
+
* so two reports of the same world are diffable line for line.
|
|
1083
|
+
*
|
|
1084
|
+
* Derived from {@link WRITE_ANCHOR_KIND_PRESENCE} rather than spelled a second
|
|
1085
|
+
* time: one list the compiler pins to the union, and nothing downstream can
|
|
1086
|
+
* enumerate a different set.
|
|
1087
|
+
*/
|
|
1088
|
+
export const WRITE_ANCHOR_KINDS: readonly WriteAnchorKind[] = Object.keys(
|
|
1089
|
+
WRITE_ANCHOR_KIND_PRESENCE,
|
|
1090
|
+
) as WriteAnchorKind[];
|
|
1091
|
+
|
|
1092
|
+
/** Zero counts for every kind — the starting point of any per-kind tally, so a
|
|
1093
|
+
* tally can never omit a kind by forgetting to initialize it. */
|
|
1094
|
+
export function emptyWriteAnchorKindCounts(): Record<WriteAnchorKind, number> {
|
|
1095
|
+
const counts = {} as Record<WriteAnchorKind, number>;
|
|
1096
|
+
for (const kind of WRITE_ANCHOR_KINDS) counts[kind] = 0;
|
|
1097
|
+
return counts;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Absent ⇒ this adapter's substrate has no creation-site index at all (a
|
|
1102
|
+
* document-backed world's truth is the document, not a construction site).
|
|
1103
|
+
* Present ⇒ every id gets an answer, anchored or reasoned.
|
|
1104
|
+
*/
|
|
1105
|
+
export interface AuthoringTruth {
|
|
1106
|
+
/** Where the runtime object came from in the game's own source or data. */
|
|
1107
|
+
readonly site: NodeCreationSite;
|
|
1108
|
+
/** The lane THIS property edit would actually take. `undefined` means the
|
|
1109
|
+
* subject is synthetic or otherwise has no write plan; `live-only` is a real
|
|
1110
|
+
* plan whose destination is the running object. */
|
|
1111
|
+
readonly writeAnchorKind: WriteAnchorKind | undefined;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* The TRUTH-family binding. One resolution answers both where a subject came
|
|
1116
|
+
* from and where an edit to one of its properties lands. A caller may not read
|
|
1117
|
+
* those facts through separate providers: that was the classifier/writer split
|
|
1118
|
+
* the persistence pipe exists to remove.
|
|
1119
|
+
*/
|
|
1120
|
+
export interface TruthProvider {
|
|
1121
|
+
resolve(id: string, property: string): AuthoringTruth;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** A plain world-space point. Component handles cross the authoring seam as
|
|
1125
|
+
* data; a substrate object or renderer handle never does. */
|
|
1126
|
+
export type SpatialPoint3 = readonly [x: number, y: number, z: number];
|
|
1127
|
+
|
|
1128
|
+
/** Non-interactive geometry an adapter asks the viewport to draw for one
|
|
1129
|
+
* spatial component. The vocabulary is intentionally geometric rather than
|
|
1130
|
+
* domain-shaped: audio attenuation, light range and a collider radius can all
|
|
1131
|
+
* use the same sphere without teaching the shell any of those concepts. */
|
|
1132
|
+
export type SpatialHandleGuide =
|
|
1133
|
+
| {
|
|
1134
|
+
readonly kind: 'line';
|
|
1135
|
+
readonly points: readonly SpatialPoint3[];
|
|
1136
|
+
readonly color: string;
|
|
1137
|
+
readonly opacity?: number;
|
|
1138
|
+
readonly dashed?: boolean;
|
|
1139
|
+
}
|
|
1140
|
+
| {
|
|
1141
|
+
readonly kind: 'sphere';
|
|
1142
|
+
readonly center: SpatialPoint3;
|
|
1143
|
+
readonly radius: number;
|
|
1144
|
+
readonly color: string;
|
|
1145
|
+
readonly opacity?: number;
|
|
1146
|
+
readonly dashed?: boolean;
|
|
1147
|
+
}
|
|
1148
|
+
| {
|
|
1149
|
+
readonly kind: 'cone';
|
|
1150
|
+
readonly origin: SpatialPoint3;
|
|
1151
|
+
readonly direction: SpatialPoint3;
|
|
1152
|
+
readonly length: number;
|
|
1153
|
+
/** Full opening angle in degrees, matching Web Audio and authoring UIs. */
|
|
1154
|
+
readonly angle: number;
|
|
1155
|
+
readonly color: string;
|
|
1156
|
+
readonly opacity?: number;
|
|
1157
|
+
readonly dashed?: boolean;
|
|
1158
|
+
}
|
|
1159
|
+
| {
|
|
1160
|
+
readonly kind: 'box';
|
|
1161
|
+
readonly center: SpatialPoint3;
|
|
1162
|
+
readonly rotation: readonly [x: number, y: number, z: number, w: number];
|
|
1163
|
+
readonly halfExtents: SpatialPoint3;
|
|
1164
|
+
readonly color: string;
|
|
1165
|
+
readonly opacity?: number;
|
|
1166
|
+
readonly dashed?: boolean;
|
|
1167
|
+
}
|
|
1168
|
+
| {
|
|
1169
|
+
readonly kind: 'capsule';
|
|
1170
|
+
readonly center: SpatialPoint3;
|
|
1171
|
+
readonly rotation: readonly [x: number, y: number, z: number, w: number];
|
|
1172
|
+
readonly radius: number;
|
|
1173
|
+
readonly halfHeight: number;
|
|
1174
|
+
readonly color: string;
|
|
1175
|
+
readonly opacity?: number;
|
|
1176
|
+
readonly dashed?: boolean;
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
/** One draggable point. `id` is stable only within its owning node; `label`
|
|
1180
|
+
* feeds accessible/editor hints rather than becoming a persisted name. */
|
|
1181
|
+
export interface SpatialDragHandle {
|
|
1182
|
+
readonly id: string;
|
|
1183
|
+
readonly label: string;
|
|
1184
|
+
readonly position: SpatialPoint3;
|
|
1185
|
+
readonly color: string;
|
|
1186
|
+
readonly writable: boolean;
|
|
1187
|
+
readonly reason?: string;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** One adapter-owned component projection. `category` is a viewport overlay
|
|
1191
|
+
* channel (for example `audio`), not a game-object kind; unknown categories
|
|
1192
|
+
* obey the master Helpers toggle instead of being rejected. */
|
|
1193
|
+
export interface SpatialHandleLayer {
|
|
1194
|
+
readonly id: string;
|
|
1195
|
+
readonly category: string;
|
|
1196
|
+
readonly guides: readonly SpatialHandleGuide[];
|
|
1197
|
+
readonly handles: readonly SpatialDragHandle[];
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Format-neutral direct-manipulation seam for component-owned spatial values.
|
|
1202
|
+
*
|
|
1203
|
+
* The viewport draws only the returned geometry. During a drag it returns a
|
|
1204
|
+
* world-space point to the adapter; the adapter alone knows whether that means
|
|
1205
|
+
* an AudioSource distance, a light cone, a collider extent, or something else,
|
|
1206
|
+
* and the adapter alone owns live preview + source persistence. `commit` runs
|
|
1207
|
+
* once at gesture end, so source/HMR/history are never hammered per pointer
|
|
1208
|
+
* move. Absent means this adapter has no spatial component handles.
|
|
1209
|
+
*/
|
|
1210
|
+
export interface SpatialHandlesProvider {
|
|
1211
|
+
layers(id: string): readonly SpatialHandleLayer[];
|
|
1212
|
+
preview(id: string, handleId: string, worldPosition: SpatialPoint3): void;
|
|
1213
|
+
/** Commit the preview and pass through the owning writer's acknowledgement.
|
|
1214
|
+
* `void` remains the honest live-only/no-write floor; callers must not grade
|
|
1215
|
+
* it as a persisted round trip. */
|
|
1216
|
+
commit(id: string, handleId: string, worldPosition: SpatialPoint3): StructuralWriteOutcome;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
export interface AuthoringAdapter {
|
|
1220
|
+
readonly capabilities: AuthoringCapabilities;
|
|
1221
|
+
/** Spec 29 §5 — seam-level provenance ("what is truth behind these rows").
|
|
1222
|
+
* Absent ⇒ the shell shows no provenance badge (degrade silently — never
|
|
1223
|
+
* fabricate a claim the adapter didn't make). */
|
|
1224
|
+
readonly provenance?: AuthoringProvenance;
|
|
1225
|
+
readonly hierarchy: HierarchyProvider; // required — minimum is "read the tree"
|
|
1226
|
+
readonly selection?: SelectionProvider;
|
|
1227
|
+
readonly transforms?: TransformProvider;
|
|
1228
|
+
readonly inspector?: InspectorProvider;
|
|
1229
|
+
/** The selected node is an atomic asset that can open in the Asset Editor. */
|
|
1230
|
+
readonly assetSubject?: AssetSubjectProvider;
|
|
1231
|
+
readonly related?: RelatedSubjectsProvider;
|
|
1232
|
+
/** Source-derived prefab/component instance overrides and native writes. */
|
|
1233
|
+
readonly instances?: ComponentInstancesProvider;
|
|
1234
|
+
readonly structure?: StructureProvider;
|
|
1235
|
+
readonly persistence?: PersistenceProvider;
|
|
1236
|
+
/** D12 — per-layer viewport picking. Absent ⇒ this adapter is not pickable. */
|
|
1237
|
+
readonly pickable?: PickProvider;
|
|
1238
|
+
/** T0 (spec 27 §2) — per-node screen geometry for a DOM visual editor's overlay/
|
|
1239
|
+
* snap/measure math. Absent ⇒ this adapter produces no rects (no overlay). */
|
|
1240
|
+
readonly rects?: RectProvider;
|
|
1241
|
+
/** T0 (spec 27 §2) — spatial drag-resize/move → source/data write for non-Object3D
|
|
1242
|
+
* (DOM) nodes. Absent ⇒ no box-edit gesture for this adapter. */
|
|
1243
|
+
readonly boxEdit?: BoxEditProvider;
|
|
1244
|
+
/** T0 (spec 27 §2) — double-click-to-edit-text on the contract (wires the react
|
|
1245
|
+
* adapter's existing off-contract `editText`). Absent ⇒ no in-place text edit. */
|
|
1246
|
+
readonly text?: TextProvider;
|
|
1247
|
+
/** D3 (spec 27 §6) — eyedropper fallback color sampling. Absent ⇒ the
|
|
1248
|
+
* canvas eyedropper swatch has no non-native path for this adapter. */
|
|
1249
|
+
readonly colorSample?: ColorSampleProvider;
|
|
1250
|
+
/** D4 — storybook stories. Absent ⇒ no stories for any node in this adapter. */
|
|
1251
|
+
readonly stories?: StoriesProvider;
|
|
1252
|
+
/** Projection subject → source/data anchor and write lane, resolved together. */
|
|
1253
|
+
readonly truth?: TruthProvider;
|
|
1254
|
+
/** Adapter-owned component guides and direct-manipulation points. */
|
|
1255
|
+
readonly spatialHandles?: SpatialHandlesProvider;
|
|
1256
|
+
/** Asset drop (hierarchy + viewport). Absent ⇒ this adapter accepts no drops. */
|
|
1257
|
+
readonly assetDrop?: AssetDropProvider;
|
|
1258
|
+
/** Change notification → UI refresh. */
|
|
1259
|
+
subscribe?(listener: () => void): () => void;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* THE PROVIDER VOCABULARY — every member of {@link AuthoringAdapter} beyond the
|
|
1264
|
+
* two an adapter cannot exist without.
|
|
1265
|
+
*
|
|
1266
|
+
* Why this exists: the editor's coverage warnings are supposed to make stopping
|
|
1267
|
+
* short of a native root IMPOSSIBLE, and they cannot do that while the row
|
|
1268
|
+
* vocabulary is a hand-written list frozen at what its author happened to know.
|
|
1269
|
+
* A list like that goes quiet at "all warnings closed" while whole capabilities
|
|
1270
|
+
* — stories, asset drop, related subjects, structure — were never enumerated at
|
|
1271
|
+
* all, so nothing ever warned about them. So the vocabulary IS the type: adding
|
|
1272
|
+
* a provider to `AuthoringAdapter` and forgetting it here is a COMPILE ERROR
|
|
1273
|
+
* (the `Record` below is missing a key), and inventing a key the type does not
|
|
1274
|
+
* have is an excess-property error. There is no third way to add a provider.
|
|
1275
|
+
*
|
|
1276
|
+
* `capabilities` and `hierarchy` are excluded because they are required members
|
|
1277
|
+
* — every adapter has them by construction, so "absent" is not a state they can
|
|
1278
|
+
* be in and a row about them could never say anything.
|
|
1279
|
+
*/
|
|
1280
|
+
export type AuthoringProviderKey = Exclude<keyof AuthoringAdapter, 'capabilities' | 'hierarchy'>;
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* The exhaustiveness pin. A `Record` over the key union, so the compiler — not
|
|
1284
|
+
* a scan, not a reviewer — is what refuses a vocabulary that has drifted from
|
|
1285
|
+
* the interface above. The values are `true` and carry no meaning: the KEYS are
|
|
1286
|
+
* the payload.
|
|
1287
|
+
*/
|
|
1288
|
+
const AUTHORING_PROVIDER_PRESENCE: Readonly<Record<AuthoringProviderKey, true>> = {
|
|
1289
|
+
provenance: true,
|
|
1290
|
+
selection: true,
|
|
1291
|
+
transforms: true,
|
|
1292
|
+
inspector: true,
|
|
1293
|
+
assetSubject: true,
|
|
1294
|
+
related: true,
|
|
1295
|
+
instances: true,
|
|
1296
|
+
structure: true,
|
|
1297
|
+
persistence: true,
|
|
1298
|
+
pickable: true,
|
|
1299
|
+
rects: true,
|
|
1300
|
+
boxEdit: true,
|
|
1301
|
+
text: true,
|
|
1302
|
+
colorSample: true,
|
|
1303
|
+
stories: true,
|
|
1304
|
+
truth: true,
|
|
1305
|
+
spatialHandles: true,
|
|
1306
|
+
assetDrop: true,
|
|
1307
|
+
subscribe: true,
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* The vocabulary as an ordered list — declaration order of the interface, so
|
|
1312
|
+
* two reports of the same root are diffable line for line.
|
|
1313
|
+
*
|
|
1314
|
+
* Derived from {@link AUTHORING_PROVIDER_PRESENCE} rather than spelled a second
|
|
1315
|
+
* time: one list that the compiler pins to the type, and nothing downstream can
|
|
1316
|
+
* enumerate a different set.
|
|
1317
|
+
*/
|
|
1318
|
+
export const AUTHORING_PROVIDER_KEYS: readonly AuthoringProviderKey[] = Object.keys(
|
|
1319
|
+
AUTHORING_PROVIDER_PRESENCE,
|
|
1320
|
+
) as AuthoringProviderKey[];
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Which providers this adapter actually exposes — PRESENCE, measured off the
|
|
1324
|
+
* object itself. Nothing here consults `capabilities`, the adapter's class, the
|
|
1325
|
+
* route that built it, or the game's id: an adapter this function has never
|
|
1326
|
+
* heard of gets the same answer as one it has.
|
|
1327
|
+
*/
|
|
1328
|
+
export function measureAuthoringProviders(
|
|
1329
|
+
adapter: AuthoringAdapter,
|
|
1330
|
+
): Readonly<Record<AuthoringProviderKey, boolean>> {
|
|
1331
|
+
const measured = {} as Record<AuthoringProviderKey, boolean>;
|
|
1332
|
+
for (const key of AUTHORING_PROVIDER_KEYS) {
|
|
1333
|
+
measured[key] = (adapter as unknown as Record<string, unknown>)[key] !== undefined;
|
|
1334
|
+
}
|
|
1335
|
+
return measured;
|
|
1336
|
+
}
|