@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/app/toolbar.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { GizmoMode } from './gizmo.js';
|
|
2
|
-
|
|
3
|
-
export interface ToolbarHandlers {
|
|
4
|
-
// transport
|
|
5
|
-
play(): void;
|
|
6
|
-
pause(): void;
|
|
7
|
-
step(): void;
|
|
8
|
-
stop(): void;
|
|
9
|
-
// gizmo + edit
|
|
10
|
-
getMode(): GizmoMode;
|
|
11
|
-
setMode(mode: GizmoMode): void;
|
|
12
|
-
undo(): void;
|
|
13
|
-
redo(): void;
|
|
14
|
-
save(): void;
|
|
15
|
-
/** Current state, polled on each `refresh`. */
|
|
16
|
-
state(): {
|
|
17
|
-
mode: 'edit' | 'play';
|
|
18
|
-
playing: boolean;
|
|
19
|
-
canUndo: boolean;
|
|
20
|
-
canRedo: boolean;
|
|
21
|
-
dirty: boolean;
|
|
22
|
-
saving: boolean;
|
|
23
|
-
enabled: boolean;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface Toolbar {
|
|
28
|
-
/** Re-read handler state into the button styles (transport, active mode, enabled, dirty dot). */
|
|
29
|
-
refresh(): void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Material-style 24×24 icon paths (filled, `currentColor`).
|
|
33
|
-
const ICON = {
|
|
34
|
-
play: 'M8 5v14l11-7z',
|
|
35
|
-
pause: 'M6 5h4v14H6zm8 0h4v14h-4z',
|
|
36
|
-
step: 'M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z',
|
|
37
|
-
stop: 'M6 6h12v12H6z',
|
|
38
|
-
move: 'M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H8l4 4 4-4h-3v-3z',
|
|
39
|
-
rotate:
|
|
40
|
-
'M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z',
|
|
41
|
-
scale:
|
|
42
|
-
'M15 3l2.3 2.3-2.89 2.87 1.42 1.42L18.7 6.7 21 9V3h-6zM3 9l2.3-2.3 2.87 2.89 1.42-1.42L6.7 5.3 9 3H3v6zm6 12l-2.3-2.3 2.89-2.87-1.42-1.42L5.3 17.3 3 15v6h6zm12-6l-2.3 2.3-2.87-2.89-1.42 1.42 2.89 2.87L15 21h6v-6z',
|
|
43
|
-
undo: 'M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z',
|
|
44
|
-
redo: 'M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z',
|
|
45
|
-
save: 'M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z',
|
|
46
|
-
} as const;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Two floating, touch-reachable toolbars: a **play** panel (play/pause/step/stop)
|
|
50
|
-
* and a **movement** panel (gizmo mode, undo/redo, save). The movement panel is
|
|
51
|
-
* hidden in play mode — there's no editing while the sim runs. All actions defer
|
|
52
|
-
* to `handlers`, which read the live session/document/gizmo.
|
|
53
|
-
*/
|
|
54
|
-
export function buildToolbar(handlers: ToolbarHandlers): Toolbar {
|
|
55
|
-
const bars = el('div', 'sc-toolbars');
|
|
56
|
-
const playBar = el('div', 'sc-toolbar');
|
|
57
|
-
const editBar = el('div', 'sc-toolbar');
|
|
58
|
-
|
|
59
|
-
const play = btn('Play', ICON.play, () => handlers.play());
|
|
60
|
-
const pause = btn('Pause', ICON.pause, () => handlers.pause());
|
|
61
|
-
const step = btn('Step one frame', ICON.step, () => handlers.step());
|
|
62
|
-
const stop = btn('Stop', ICON.stop, () => handlers.stop());
|
|
63
|
-
playBar.append(play, pause, step, stop);
|
|
64
|
-
|
|
65
|
-
const move = modeBtn('translate', 'Move', ICON.move);
|
|
66
|
-
const rotate = modeBtn('rotate', 'Rotate', ICON.rotate);
|
|
67
|
-
const scale = modeBtn('scale', 'Scale', ICON.scale);
|
|
68
|
-
const undo = btn('Undo', ICON.undo, () => handlers.undo());
|
|
69
|
-
const redo = btn('Redo', ICON.redo, () => handlers.redo());
|
|
70
|
-
const save = btn('Save', ICON.save, () => handlers.save());
|
|
71
|
-
save.classList.add('sc-save');
|
|
72
|
-
editBar.append(move, rotate, scale, sep(), undo, redo, sep(), save);
|
|
73
|
-
|
|
74
|
-
bars.append(playBar, editBar);
|
|
75
|
-
document.body.append(bars);
|
|
76
|
-
|
|
77
|
-
function modeBtn(mode: GizmoMode, title: string, path: string): HTMLButtonElement {
|
|
78
|
-
return btn(title, path, () => handlers.setMode(mode), mode);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function refresh(): void {
|
|
82
|
-
const s = handlers.state();
|
|
83
|
-
const editing = s.mode === 'edit';
|
|
84
|
-
const paused = s.mode === 'play' && !s.playing;
|
|
85
|
-
|
|
86
|
-
// Transport.
|
|
87
|
-
play.disabled = s.playing; // edit → start; paused → resume; playing → disabled
|
|
88
|
-
play.classList.toggle('active', s.playing);
|
|
89
|
-
pause.disabled = !s.playing;
|
|
90
|
-
step.disabled = !paused;
|
|
91
|
-
stop.disabled = editing;
|
|
92
|
-
|
|
93
|
-
// Movement panel: editing only happens in edit mode — hide it during play.
|
|
94
|
-
editBar.style.display = editing ? '' : 'none';
|
|
95
|
-
|
|
96
|
-
const mode = handlers.getMode();
|
|
97
|
-
for (const b of [move, rotate, scale]) {
|
|
98
|
-
b.classList.toggle('active', b.dataset.mode === mode);
|
|
99
|
-
b.disabled = !s.enabled;
|
|
100
|
-
}
|
|
101
|
-
undo.disabled = !s.canUndo;
|
|
102
|
-
redo.disabled = !s.canRedo;
|
|
103
|
-
save.disabled = !s.dirty || s.saving;
|
|
104
|
-
save.classList.toggle('dirty', s.dirty);
|
|
105
|
-
save.classList.toggle('saving', s.saving);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
refresh();
|
|
109
|
-
return { refresh };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── DOM helpers ──────────────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
function el(tag: string, className: string): HTMLElement {
|
|
115
|
-
const node = document.createElement(tag);
|
|
116
|
-
node.className = className;
|
|
117
|
-
return node;
|
|
118
|
-
}
|
|
119
|
-
function btn(title: string, iconPath: string, onClick: () => void, mode?: GizmoMode): HTMLButtonElement {
|
|
120
|
-
const b = document.createElement('button');
|
|
121
|
-
b.className = 'sc-tbtn';
|
|
122
|
-
b.title = title;
|
|
123
|
-
b.setAttribute('aria-label', title);
|
|
124
|
-
b.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="${iconPath}"/></svg>`;
|
|
125
|
-
if (mode) b.dataset.mode = mode;
|
|
126
|
-
b.onclick = onClick;
|
|
127
|
-
return b;
|
|
128
|
-
}
|
|
129
|
-
function sep(): HTMLElement {
|
|
130
|
-
return el('div', 'sc-tsep');
|
|
131
|
-
}
|
package/app/wsClient.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { WS_PATH, type EditorCommand, type EditorPush, type EditorResponse, type EditorResult } from '../dist/protocol.js';
|
|
2
|
-
import type { EditorSession } from './session.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Connect the editor page to the dev server's WebSocket bridge and execute the
|
|
6
|
-
* commands the MCP server relays, replying with the result (id-correlated).
|
|
7
|
-
* Auto-reconnects (e.g. across a dev-server restart).
|
|
8
|
-
*/
|
|
9
|
-
export function connectEditor(session: EditorSession): void {
|
|
10
|
-
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
11
|
-
const url = `${proto}://${location.host}${WS_PATH}`;
|
|
12
|
-
|
|
13
|
-
let stopped = false; // set on a shutdown push — don't reconnect to a dead server
|
|
14
|
-
|
|
15
|
-
const open = (): void => {
|
|
16
|
-
const ws = new WebSocket(url);
|
|
17
|
-
ws.onmessage = async (ev) => {
|
|
18
|
-
let msg: EditorCommand | EditorPush;
|
|
19
|
-
try {
|
|
20
|
-
msg = JSON.parse(ev.data as string) as EditorCommand | EditorPush;
|
|
21
|
-
} catch {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
// Unsolicited server pushes — not id-correlated commands.
|
|
25
|
-
if ('push' in msg) {
|
|
26
|
-
if (msg.push === 'shutdown') {
|
|
27
|
-
stopped = true;
|
|
28
|
-
closeEditorTab();
|
|
29
|
-
} else {
|
|
30
|
-
session.onExternalChange?.(msg.file, msg.role);
|
|
31
|
-
}
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const cmd = msg;
|
|
35
|
-
let response: EditorResponse;
|
|
36
|
-
try {
|
|
37
|
-
response = { id: cmd.id, ok: true, result: await runCommand(session, cmd) };
|
|
38
|
-
} catch (e) {
|
|
39
|
-
response = { id: cmd.id, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
40
|
-
}
|
|
41
|
-
ws.send(JSON.stringify(response));
|
|
42
|
-
};
|
|
43
|
-
ws.onclose = () => {
|
|
44
|
-
if (!stopped) setTimeout(open, 1000);
|
|
45
|
-
};
|
|
46
|
-
ws.onerror = () => ws.close();
|
|
47
|
-
};
|
|
48
|
-
open();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The dev server stopped: close this tab. `window.close()` only succeeds for a
|
|
53
|
-
* tab the script itself opened (or one Chrome deems closable) — if the browser
|
|
54
|
-
* blocks it, fall back to a "server stopped" placeholder so the stale page is
|
|
55
|
-
* obvious and inert rather than silently retrying a dead connection.
|
|
56
|
-
*/
|
|
57
|
-
function closeEditorTab(): void {
|
|
58
|
-
window.close();
|
|
59
|
-
setTimeout(() => {
|
|
60
|
-
document.title = 'Editor stopped';
|
|
61
|
-
document.body.innerHTML =
|
|
62
|
-
'<div style="position:fixed;inset:0;display:flex;align-items:center;justify-content:center;' +
|
|
63
|
-
'font:500 15px system-ui,sans-serif;color:#9aa4b2;background:#0a0e16">' +
|
|
64
|
-
'Editor server stopped — you can close this tab.</div>';
|
|
65
|
-
}, 150);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Execute one relayed command against the session. Exported so the command
|
|
70
|
-
* dispatch (not just the WS plumbing) can be driven directly in tests.
|
|
71
|
-
*/
|
|
72
|
-
export async function runCommand(session: EditorSession, cmd: EditorCommand): Promise<EditorResult> {
|
|
73
|
-
switch (cmd.type) {
|
|
74
|
-
case 'open':
|
|
75
|
-
return { kind: 'json', data: await session.open(cmd.path) };
|
|
76
|
-
case 'preview':
|
|
77
|
-
return { kind: 'image', dataUrl: session.preview({ camera: cmd.camera, lookAt: cmd.lookAt }) };
|
|
78
|
-
case 'run':
|
|
79
|
-
session.run();
|
|
80
|
-
return { kind: 'ok', info: 'playing' };
|
|
81
|
-
case 'pause':
|
|
82
|
-
session.pause();
|
|
83
|
-
return { kind: 'ok', info: 'paused' };
|
|
84
|
-
case 'stop':
|
|
85
|
-
session.stop();
|
|
86
|
-
return { kind: 'ok', info: 'edit mode' };
|
|
87
|
-
case 'telemetry':
|
|
88
|
-
return { kind: 'json', data: session.telemetry(cmd.targetId) };
|
|
89
|
-
case 'simulate':
|
|
90
|
-
return { kind: 'multi', results: session.simulate(cmd.steps) };
|
|
91
|
-
}
|
|
92
|
-
}
|
package/dist/args.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** Split a comma-separated list, trimming blanks. */
|
|
2
|
-
export declare function parseList(v: string | undefined): string[] | undefined;
|
|
3
|
-
/** Flags that take a value; other `--flags` are booleans (`''`), the rest positionals. */
|
|
4
|
-
export declare const VALUE_FLAGS: Record<string, string>;
|
|
5
|
-
export declare function parseArgs(args: string[]): {
|
|
6
|
-
positionals: string[];
|
|
7
|
-
options: Record<string, string>;
|
|
8
|
-
};
|
package/dist/args.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Pure CLI-arg helpers (no vite imports), so they're unit-testable.
|
|
2
|
-
/** Split a comma-separated list, trimming blanks. */
|
|
3
|
-
export function parseList(v) {
|
|
4
|
-
return v ? v.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
|
|
5
|
-
}
|
|
6
|
-
/** Flags that take a value; other `--flags` are booleans (`''`), the rest positionals. */
|
|
7
|
-
export const VALUE_FLAGS = {
|
|
8
|
-
'--components': 'components',
|
|
9
|
-
'--packages': 'packages',
|
|
10
|
-
'--port': 'port',
|
|
11
|
-
};
|
|
12
|
-
export function parseArgs(args) {
|
|
13
|
-
const positionals = [];
|
|
14
|
-
const options = {};
|
|
15
|
-
for (let i = 0; i < args.length; i++) {
|
|
16
|
-
const arg = args[i];
|
|
17
|
-
if (arg in VALUE_FLAGS) {
|
|
18
|
-
options[VALUE_FLAGS[arg]] = args[++i] ?? '';
|
|
19
|
-
}
|
|
20
|
-
else if (arg.startsWith('--')) {
|
|
21
|
-
options[arg.slice(2)] = ''; // boolean flag, e.g. --no-open
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
positionals.push(arg);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return { positionals, options };
|
|
28
|
-
}
|