@scenoco-three/editor 0.4.0 → 0.5.4
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 +21 -0
- package/README.md +23 -17
- package/app/cameraRig.ts +73 -12
- package/app/commands.ts +5 -1
- package/app/desktopControls.ts +13 -10
- package/app/document.ts +474 -427
- package/app/editLog.ts +127 -0
- package/app/editor.css +51 -0
- package/app/gizmo.ts +100 -6
- package/app/globals.d.ts +9 -0
- package/app/index.html +6 -122
- package/app/main.ts +491 -304
- package/app/meta.ts +24 -0
- package/app/picking.ts +47 -0
- package/app/session.ts +513 -308
- package/app/touchControls.ts +85 -62
- package/app/ui-react/AddMemberPopup.tsx +114 -0
- package/app/ui-react/BottomLauncher.tsx +15 -0
- package/app/ui-react/EditorUI.tsx +22 -0
- package/app/ui-react/FilePicker.tsx +80 -0
- package/app/ui-react/HeadBtn.tsx +21 -0
- package/app/ui-react/InspectorSheet.tsx +475 -0
- package/app/ui-react/NodeListSheet.tsx +56 -0
- package/app/ui-react/TopBar.tsx +137 -0
- package/app/ui-react/Tree.tsx +106 -0
- package/app/ui-react/cx.ts +4 -0
- package/app/ui-react/fields.tsx +407 -0
- package/app/ui-react/format.ts +30 -0
- package/app/ui-react/mount.tsx +31 -0
- package/app/ui-react/store.ts +80 -0
- package/app/ui-react/types.ts +88 -0
- package/app/ui-react/ui.module.css +250 -0
- package/app/ui-react/useSize.ts +20 -0
- package/app/values.ts +137 -0
- package/dist/cli/index.js +93 -70
- package/dist/index.d.ts +10 -3
- package/dist/index.js +9 -5
- package/dist/mcp.d.ts +28 -12
- package/dist/mcp.js +172 -131
- package/dist/protocol.d.ts +74 -65
- package/dist/protocol.js +17 -4
- package/dist/server.d.ts +106 -39
- package/dist/server.js +389 -361
- package/package.json +24 -9
- package/app/filePicker.ts +0 -64
- package/app/panels.ts +0 -553
- package/app/toolbar.ts +0 -131
- package/app/wsClient.ts +0 -92
- package/dist/args.d.ts +0 -8
- package/dist/args.js +0 -28
package/dist/protocol.d.ts
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
45
|
+
camera: string;
|
|
12
46
|
lookAt?: string;
|
|
13
|
-
|
|
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
|
-
*
|
|
35
|
-
* game to a precise condition
|
|
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
|
|
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
|
-
/**
|
|
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: '
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
136
|
+
* An unsolicited Node → page notification.
|
|
96
137
|
*
|
|
97
|
-
* - `
|
|
98
|
-
*
|
|
99
|
-
* - `
|
|
100
|
-
*
|
|
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: '
|
|
145
|
+
push: 'documentChanged';
|
|
104
146
|
file: string;
|
|
105
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
/**
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
/** Fixed 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
|
-
/**
|
|
10
|
-
|
|
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
|
|
22
|
+
/** Relays a command to the connected editor page and awaits its reply. */
|
|
13
23
|
export interface EditorBridge {
|
|
14
24
|
connected(): boolean;
|
|
15
|
-
send(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
121
|
+
export declare function createBridgeCore(options?: BridgeOptions): {
|
|
122
|
+
bridge: EditorBridge;
|
|
123
|
+
register: (ws: BridgeSocket) => void;
|
|
124
|
+
};
|