@scenoco-three/editor 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -17
  3. package/app/cameraRig.ts +73 -12
  4. package/app/commands.ts +5 -1
  5. package/app/desktopControls.ts +13 -10
  6. package/app/document.ts +474 -427
  7. package/app/editLog.ts +127 -0
  8. package/app/editor.css +51 -0
  9. package/app/gizmo.ts +100 -6
  10. package/app/index.html +6 -122
  11. package/app/main.ts +488 -293
  12. package/app/meta.ts +24 -0
  13. package/app/picking.ts +47 -0
  14. package/app/session.ts +487 -210
  15. package/app/touchControls.ts +85 -62
  16. package/app/ui-react/AddMemberPopup.tsx +114 -0
  17. package/app/ui-react/BottomLauncher.tsx +15 -0
  18. package/app/ui-react/EditorUI.tsx +22 -0
  19. package/app/ui-react/FilePicker.tsx +80 -0
  20. package/app/ui-react/HeadBtn.tsx +21 -0
  21. package/app/ui-react/InspectorSheet.tsx +475 -0
  22. package/app/ui-react/NodeListSheet.tsx +56 -0
  23. package/app/ui-react/TopBar.tsx +137 -0
  24. package/app/ui-react/Tree.tsx +106 -0
  25. package/app/ui-react/cx.ts +4 -0
  26. package/app/ui-react/fields.tsx +407 -0
  27. package/app/ui-react/format.ts +30 -0
  28. package/app/ui-react/mount.tsx +31 -0
  29. package/app/ui-react/store.ts +80 -0
  30. package/app/ui-react/types.ts +88 -0
  31. package/app/ui-react/ui.module.css +250 -0
  32. package/app/ui-react/useSize.ts +20 -0
  33. package/app/values.ts +137 -0
  34. package/app/vite-env.d.ts +4 -2
  35. package/dist/cli/index.js +93 -70
  36. package/dist/index.d.ts +10 -3
  37. package/dist/index.js +9 -5
  38. package/dist/mcp.d.ts +28 -12
  39. package/dist/mcp.js +172 -131
  40. package/dist/protocol.d.ts +74 -65
  41. package/dist/protocol.js +17 -4
  42. package/dist/server.d.ts +106 -39
  43. package/dist/server.js +387 -324
  44. package/package.json +26 -11
  45. package/app/filePicker.ts +0 -64
  46. package/app/panels.ts +0 -553
  47. package/app/toolbar.ts +0 -131
  48. package/app/wsClient.ts +0 -92
  49. package/dist/args.d.ts +0 -8
  50. package/dist/args.js +0 -28
@@ -1,6 +1,40 @@
1
- /** WebSocket path the editor connects to on the dev server. */
1
+ /**
2
+ * The contract between the Node side and the editor page.
3
+ *
4
+ * Pure types plus one correlation helper, so both ends agree and the relay is testable on its
5
+ * own. Nothing here reaches the compiler: the browser holds no compiler in v2 either.
6
+ *
7
+ * ### What is deliberately absent
8
+ *
9
+ * v1 carried an editing half — `EditTarget`, `EditOp`, `StructuralOp`, and `CompiledScene`
10
+ * fragments travelling over the wire so a remote caller could reshape a document. All of it is
11
+ * gone. An agent has file tools and the watcher rebuilds either way, so a second write path into
12
+ * the user's source bought nothing and cost the guarantee that every change to their scene went
13
+ * through their editor, their diff and their undo.
14
+ *
15
+ * What remains is observation: open, render, drive, sample.
16
+ */
17
+ /** WebSocket path the editor page connects to. */
2
18
  export declare const WS_PATH = "/scenoco-ws";
3
- /** A command sent MCP-server → editor (id-correlated with its response). */
19
+ /**
20
+ * How a capture is rendered — everything except *where the camera is*.
21
+ *
22
+ * Framing belongs to the individual shot and changes between them; this is the output format,
23
+ * and it is the same for every capture in one command. So it sits on the command rather than on
24
+ * each step: one simulation renders one way throughout, and two shots of the same run stay
25
+ * comparable because nothing but the camera moved between them.
26
+ */
27
+ export interface PreviewFrame {
28
+ width: number;
29
+ height: number;
30
+ /**
31
+ * Include the screen-space UI layer — a `<Canvas>` HUD and anything else parented to a camera.
32
+ * The shot is then framed *through* that camera, which is what puts the HUD where the player
33
+ * sees it rather than floating in world space in front of a camera nobody is looking through.
34
+ */
35
+ ui: boolean;
36
+ }
37
+ /** A command sent Node → editor page, correlated with its reply by `id`. */
4
38
  export type EditorCommand = {
5
39
  id: string;
6
40
  type: 'open';
@@ -8,9 +42,9 @@ export type EditorCommand = {
8
42
  } | {
9
43
  id: string;
10
44
  type: 'preview';
11
- camera?: string;
45
+ camera: string;
12
46
  lookAt?: string;
13
- size?: string;
47
+ frame: PreviewFrame;
14
48
  } | {
15
49
  id: string;
16
50
  type: 'run';
@@ -28,24 +62,30 @@ export type EditorCommand = {
28
62
  id: string;
29
63
  type: 'simulate';
30
64
  steps: SimStep[];
65
+ frame: PreviewFrame;
31
66
  };
32
67
  /**
33
- * One step in a `simulate` batch — transport, frame stepping, synthetic input, or
34
- * a capture. Executed in order against the live engine, so an agent can drive the
35
- * game to a precise condition and sample preview/telemetry there.
68
+ * One step in a `simulate` batch — transport, frame stepping, synthetic input, or a capture.
69
+ *
70
+ * Run in order against the live engine, so a caller can drive the game to a precise condition
71
+ * and sample it there. This is the whole reason the editor is worth having for an agent: a
72
+ * screenshot of frame zero says almost nothing about whether a game works.
36
73
  */
37
74
  export type SimStep = {
38
75
  do: 'play' | 'pause' | 'stop';
39
76
  }
40
- /** Advance the simulation by `frames` ticks (default 1) of `dt` seconds (default 1/60). */
77
+ /** Advance by `frames` ticks (default 1) of `dt` seconds (default 1/60). */
41
78
  | {
42
79
  do: 'step';
43
80
  frames?: number;
44
81
  dt?: number;
45
82
  }
46
- /** Keyboard — `key` is a Unity KeyCode name (e.g. 'Space', 'W'). Each action is its own verb. */
83
+ /**
84
+ * Keyboard. `key` is a `KeyCode` member name — `Space`, `W`, `LeftArrow`; an unknown one fails
85
+ * the batch with the valid list rather than doing nothing. Each action is its own verb.
86
+ */
47
87
  | {
48
- do: 'tap';
88
+ do: 'press';
49
89
  key: string;
50
90
  } | {
51
91
  do: 'keyDown';
@@ -54,7 +94,7 @@ export type SimStep = {
54
94
  do: 'keyUp';
55
95
  key: string;
56
96
  }
57
- /** Mouse — `x,y` canvas-relative px (top-left). `click` = down+up; button defaults to left. */
97
+ /** Mouse. `x,y` are canvas-relative pixels from the top left; `click` is down plus up. */
58
98
  | {
59
99
  do: 'click';
60
100
  x?: number;
@@ -67,21 +107,22 @@ export type SimStep = {
67
107
  dx?: number;
68
108
  dy?: number;
69
109
  }
70
- /** Gamepad: synthetic pad `index` (default 0) with pressed `buttons` + `axes`. */
110
+ /**
111
+ * Capture a PNG. `camera` is a position in the document's own vector grammar
112
+ * (`"y: 6; z: 12"`), or `"fit"` to frame the whole scene. Size and UI come from the batch's
113
+ * `frame` — see {@link PreviewFrame} for why they are not repeated per step.
114
+ */
71
115
  | {
72
- do: 'gamepad';
73
- index?: number;
74
- buttons?: boolean[];
75
- axes?: number[];
76
- } | {
77
116
  do: 'preview';
78
- camera?: string;
117
+ camera: string;
79
118
  lookAt?: string;
80
- } | {
119
+ }
120
+ /** Sample state at an `id` the document declares — a node and its components. */
121
+ | {
81
122
  do: 'telemetry';
82
123
  id: string;
83
124
  };
84
- /** The editor's reply, keyed by the command id. */
125
+ /** The page's reply, keyed by the command id. */
85
126
  export type EditorResponse = {
86
127
  id: string;
87
128
  ok: true;
@@ -92,17 +133,20 @@ export type EditorResponse = {
92
133
  error: string;
93
134
  };
94
135
  /**
95
- * An unsolicited server→editor notification (not id-correlated).
136
+ * An unsolicited Node → page notification.
96
137
  *
97
- * - `fileChanged`: a watched file changed on disk — the open scene/prefab
98
- * (`role: 'open'`) or one of its `src=` deps, a prefab/material (`role: 'dep'`).
99
- * - `shutdown`: the dev server is stopping; the editor page closes itself so no
100
- * stale tab is left behind.
138
+ * - `documentChanged` — the open document, or something it references, changed on disk. The page
139
+ * reloads the scene.
140
+ * - `buildFailed` — a document no longer compiles. The page shows the diagnostics rather than
141
+ * reloading into a broken scene.
142
+ * - `shutdown` — the server is stopping, so the page closes itself instead of being left stale.
101
143
  */
102
144
  export type EditorPush = {
103
- push: 'fileChanged';
145
+ push: 'documentChanged';
104
146
  file: string;
105
- role: 'open' | 'dep';
147
+ } | {
148
+ push: 'buildFailed';
149
+ message: string;
106
150
  } | {
107
151
  push: 'shutdown';
108
152
  };
@@ -116,46 +160,11 @@ export type EditorResult = {
116
160
  } | {
117
161
  kind: 'json';
118
162
  data: unknown;
119
- } | {
163
+ }
164
+ /** `simulate` — one entry per capturing step, in order. */
165
+ | {
120
166
  kind: 'multi';
121
167
  results: EditorResult[];
122
168
  };
123
169
  /** Random correlation id for a command. */
124
170
  export declare function commandId(): string;
125
- /**
126
- * Which compiled element an edit targets. Resolved server-side via the source
127
- * map (compiled index → source element), so it survives prefab expansion where
128
- * structural paths don't. `id`s are carried for readability/debugging.
129
- */
130
- export type EditTarget = {
131
- kind: 'node';
132
- nodeIndex: number;
133
- id?: string;
134
- } | {
135
- kind: 'component';
136
- componentIndex: number;
137
- nodeIndex: number;
138
- nodeId?: string;
139
- /** Component tag + its ordinal among same-typed components on the node. */
140
- type: string;
141
- ordinal: number;
142
- } | {
143
- kind: 'attachment';
144
- nodeIndex: number;
145
- /** Owning node id; absent for the scene-root, which `root` then marks. */
146
- nodeId?: string;
147
- root?: boolean;
148
- /** Attachment tag + its ordinal among same-tag attachments on the node. */
149
- tag: string;
150
- ordinal: number;
151
- };
152
- /**
153
- * One attribute change in compiled form (numbers/arrays/strings/bools). The
154
- * server serializes `value` to an attribute string using the registry codecs,
155
- * or removes the attribute when it equals the registry default (minimal diff).
156
- */
157
- export interface EditOp {
158
- target: EditTarget;
159
- attr: string;
160
- value: number | boolean | string | number[];
161
- }
package/dist/protocol.js CHANGED
@@ -1,7 +1,20 @@
1
- // Shared command/response protocol for the WebSocket bridge between the MCP
2
- // server (Node) and the editor app (browser). Pure types + a tiny correlation
3
- // helper, so both ends agree and the relay logic is unit-testable.
4
- /** WebSocket path the editor connects to on the dev server. */
1
+ /**
2
+ * The contract between the Node side and the editor page.
3
+ *
4
+ * Pure types plus one correlation helper, so both ends agree and the relay is testable on its
5
+ * own. Nothing here reaches the compiler: the browser holds no compiler in v2 either.
6
+ *
7
+ * ### What is deliberately absent
8
+ *
9
+ * v1 carried an editing half — `EditTarget`, `EditOp`, `StructuralOp`, and `CompiledScene`
10
+ * fragments travelling over the wire so a remote caller could reshape a document. All of it is
11
+ * gone. An agent has file tools and the watcher rebuilds either way, so a second write path into
12
+ * the user's source bought nothing and cost the guarantee that every change to their scene went
13
+ * through their editor, their diff and their undo.
14
+ *
15
+ * What remains is observation: open, render, drive, sample.
16
+ */
17
+ /** WebSocket path the editor page connects to. */
5
18
  export const WS_PATH = '/scenoco-ws';
6
19
  /** Random correlation id for a command. */
7
20
  export function commandId() {
package/dist/server.d.ts CHANGED
@@ -1,57 +1,124 @@
1
+ import { type ElementAt, type ProjectModel } from '@scenoco-three/compiler';
1
2
  import { type EditorCommand, type EditorPush, type EditorResponse } from './protocol.js';
2
3
  export interface ServerOptions {
3
- /** Directories scanned for `@component`/`@node` tags (default `['assets']`). */
4
- componentRoots?: string[];
5
- /** Tag packages to register (default: the `@scenoco-three/*` deps in package.json). */
6
- packages?: string[];
7
- /** Fixed port (default: a free OS port). */
4
+ /** Project root — the directory holding `tsconfig.json` and `assets/`. Defaults to cwd. */
5
+ root?: string;
6
+ /** Packages whose tags the documents may use, beyond `three`. */
7
+ modules?: readonly string[];
8
+ /** Fixed port. Defaults to a free one the OS picks. */
8
9
  port?: number;
9
- /** Base directory `componentRoots` are resolved against (default: `process.cwd()`). */
10
- cwd?: string;
10
+ /**
11
+ * Called when no editor page has been connected for a while — none ever connected within
12
+ * `initialGraceMs`, or the last one disconnected and none came back within `idleMs`.
13
+ *
14
+ * Opt-in, so an embedder or a test stays long-lived. The CLI wires it to a graceful shutdown,
15
+ * which is what keeps a closed tab from leaving an editor process running for the rest of the
16
+ * day.
17
+ */
18
+ onIdle?: () => void;
19
+ idleMs?: number;
20
+ initialGraceMs?: number;
11
21
  }
12
- /** Relays MCP tool commands to the connected editor page and awaits its reply. */
22
+ /** Relays a command to the connected editor page and awaits its reply. */
13
23
  export interface EditorBridge {
14
24
  connected(): boolean;
15
- send(cmd: EditorCommand): Promise<EditorResponse>;
16
- /** Send an unsolicited notification to the editor page (e.g. a file change). */
17
- push(msg: EditorPush): void;
18
- }
19
- /** Watches the open file + its `src=` deps and pushes change notifications. */
20
- export interface FileWatcher {
21
- /** (Re)arm the watch set after a successful compile of `openFile` with `deps`. */
22
- arm(openFile: string, openContents: string, deps: {
23
- path: string;
24
- contents: string;
25
- }[]): void;
26
- /** Record content the editor itself just wrote, so its own save isn't seen as external. */
27
- note(file: string, contents: string): void;
28
- setPush(push: (msg: EditorPush) => void): void;
29
- close(): Promise<void>;
25
+ send(command: EditorCommand): Promise<EditorResponse>;
26
+ push(message: EditorPush): void;
27
+ /**
28
+ * Resolve as soon as an editor page is connected, or `false` after `ms`.
29
+ *
30
+ * The CLI waits on this after reclaiming the previous session's port: a tab stranded by an
31
+ * ungraceful exit is still retrying its socket every second, so giving it a moment to come
32
+ * back is the difference between reusing the tab and stacking up a new one.
33
+ */
34
+ waitForConnection(ms: number): Promise<boolean>;
30
35
  }
31
36
  export interface EditorServer {
32
- /** Local URL the editor app is served at. */
33
37
  url: string;
34
- /** LAN URL (open on another device on the same network). */
38
+ /**
39
+ * The same editor, reachable from another device on the network — a phone, for testing touch.
40
+ *
41
+ * Absent when the host has no LAN interface to offer.
42
+ */
35
43
  networkUrl?: string;
44
+ port: number;
45
+ /**
46
+ * True when this server bound the **previous** session's port rather than a fresh one, so any
47
+ * tab left over from that session is about to reattach on its own retry loop.
48
+ */
49
+ reusedPort: boolean;
36
50
  bridge: EditorBridge;
51
+ /** What the project's TypeScript declares — the inspector's schema. */
52
+ model(): ProjectModel;
37
53
  close(): Promise<void>;
38
54
  }
39
55
  /**
40
- * Start the editor's Vite dev server: serves the app, registers all project
41
- * components/packages, compiles any scene/prefab on demand, and bridges the page
42
- * to the MCP server over a WebSocket.
56
+ * The editor's own dev server.
57
+ *
58
+ * It serves the editor page from this package and lets it import the **project's** generated
59
+ * modules over `/@fs`, which is the arrangement decided when this was ported: riding the
60
+ * project's own Vite would put a React app inside the game's build.
61
+ *
62
+ * Its jobs are few. Compile the project **with provenance** so the page can map a mesh back to
63
+ * the element that built it. Serve the model and the document text the inspector needs. Take a
64
+ * save and splice it into the source. Watch, rebuild, and tell the page.
65
+ */
66
+ export declare function startEditorServer(options?: ServerOptions): Promise<EditorServer>;
67
+ /** One change to a document, addressed the way provenance addresses everything. */
68
+ export type Edit = {
69
+ op: 'setAttribute';
70
+ at: ElementAt;
71
+ name: string;
72
+ value: string | null;
73
+ } | {
74
+ op: 'removeElement';
75
+ at: ElementAt;
76
+ } | {
77
+ op: 'insertChild';
78
+ at: ElementAt;
79
+ xml: string;
80
+ index?: number;
81
+ } | {
82
+ op: 'moveElement';
83
+ at: ElementAt;
84
+ parent: ElementAt;
85
+ index?: number;
86
+ };
87
+ /**
88
+ * Apply a save.
89
+ *
90
+ * One at a time against the running text, because each edit's addresses are computed by the page
91
+ * from the document it is looking at — so a batch has to be built as a sequence of
92
+ * already-consistent steps, not as parallel edits into one snapshot.
43
93
  */
44
- export declare function startEditorServer(opts?: ServerOptions): Promise<EditorServer>;
45
- /** A scene/prefab source the picker can open, addressed relative to `cwd`. */
46
- export interface SceneFile {
47
- /** Path relative to `cwd` — what the editor passes to `?file=` / `open()`. */
48
- file: string;
49
- /** Whether it's a scene or a prefab, for the picker's label. */
50
- kind: 'scene' | 'prefab';
94
+ export declare function applyEdits(source: string, edits: readonly Edit[]): {
95
+ text: string;
96
+ problems: string[];
97
+ };
98
+ /**
99
+ * The half of an editor socket this file uses — `ws`'s `WebSocket`, and anything a test can fake.
100
+ */
101
+ export interface BridgeSocket {
102
+ readonly readyState: number;
103
+ readonly OPEN: number;
104
+ send(data: string): void;
105
+ on(event: 'message', listener: (data: unknown) => void): void;
106
+ on(event: 'close', listener: () => void): void;
107
+ }
108
+ /** How the bridge decides the process has nothing left to serve. */
109
+ export interface BridgeOptions {
110
+ onIdle?: () => void;
111
+ idleMs?: number;
112
+ initialGraceMs?: number;
51
113
  }
52
114
  /**
53
- * Every `*.scene.xml` / `*.prefab.xml` under the asset `roots` (default assets/),
54
- * relative to `cwd`. Isolated to the roots — the walk never escapes them, and
55
- * build/cache dirs are skipped. Sorted for a stable picker order.
115
+ * Command routing, failover and the idle self-halt, with no server attached.
116
+ *
117
+ * Separate from `attachBridge` because these are the rules worth pinning down — which tab a
118
+ * command goes to, what happens to a command when that tab closes, when the process gives up —
119
+ * and none of them need a port, a browser, or a compiled project to be true.
56
120
  */
57
- export declare function listSceneFiles(roots: string[], cwd: string): SceneFile[];
121
+ export declare function createBridgeCore(options?: BridgeOptions): {
122
+ bridge: EditorBridge;
123
+ register: (ws: BridgeSocket) => void;
124
+ };