@scenoco-three/editor 0.4.0 → 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 (49) 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 -304
  12. package/app/meta.ts +24 -0
  13. package/app/picking.ts +47 -0
  14. package/app/session.ts +476 -311
  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/dist/cli/index.js +93 -70
  35. package/dist/index.d.ts +10 -3
  36. package/dist/index.js +9 -5
  37. package/dist/mcp.d.ts +28 -12
  38. package/dist/mcp.js +172 -131
  39. package/dist/protocol.d.ts +74 -65
  40. package/dist/protocol.js +17 -4
  41. package/dist/server.d.ts +106 -39
  42. package/dist/server.js +386 -361
  43. package/package.json +25 -10
  44. package/app/filePicker.ts +0 -64
  45. package/app/panels.ts +0 -553
  46. package/app/toolbar.ts +0 -131
  47. package/app/wsClient.ts +0 -92
  48. package/dist/args.d.ts +0 -8
  49. 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
- }