@forgeax/interface 0.5.0 → 0.6.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.
@@ -1,182 +0,0 @@
1
- // packages/interface/src/core/extensions/editor-commands.ts
2
- //
3
- // Command-bus wrappers for editor-domain actions (Play/Stop/Save/Undo/Redo/
4
- // select-all/delete/frame/toggle-display/deselect/…). These commands are the
5
- // single entry point the top menu bar (builtin-menus.ts) and the keyboard
6
- // router share so palette/menu/shortcut hit the same call path.
7
- //
8
- // The interface package is editor-agnostic (lint:agnostic forbids importing
9
- // @forgeax/editor), so every command routes through the injected
10
- // KeyboardRouterDeps — the host editor (studio/main.tsx) calls
11
- // registerKeyboardRouterDeps() at boot BEFORE <App> mounts, and each execute
12
- // resolves the deps LAZILY via getKeyboardRouterDeps(). Capture-at-setup
13
- // would freeze deps to null since setup runs during bootstrapAppHost() which
14
- // happens before main.tsx has finished mounting <App>; lazy resolution keeps
15
- // this file boot-order-safe regardless of whether registration happened first.
16
- //
17
- // import + reveal: NOT registered. Content-browser import and OS file-reveal
18
- // live outside KeyboardRouterDeps and can't be reached editor-agnostically
19
- // from the interface foundation. The corresponding menu items in builtin-menus.ts carry no
20
- // commandId and stay disabled — the correct signal for a not-yet-wired
21
- // capability. Flip them on when a dep or command later lands.
22
- //
23
- // reloadPreview: registered as a best-effort event emit (`preview:reload` on
24
- // ctx.bus). No listener consumes it yet — the Play iframe self-reload lives
25
- // in @forgeax/editor/PlaySurface which the interface foundation cannot import. Emitting the event
26
- // keeps the menu item live and gives a future HMR/preview owner a stable hook
27
- // to subscribe to without touching this file.
28
- import { APPLICATION_KEYBINDING_SCOPE } from '../contextual-keybindings.js';
29
- import { getKeyboardRouterDeps } from '../../lib/global-shortcuts.js';
30
- import { executeFocusedTextEditAction } from '../../lib/text-edit-actions.js';
31
- /** Resolve the injected router deps or throw a clear error naming the missing
32
- * wiring — makes a mis-boot fail loudly at command-invoke time instead of
33
- * silently no-op'ing. */
34
- function requireDeps() {
35
- const deps = getKeyboardRouterDeps();
36
- if (!deps)
37
- throw new Error('editor-commands: editor deps not injected (host must call registerKeyboardRouterDeps before invoking editor.* commands)');
38
- return deps;
39
- }
40
- export const editorCommandsExtension = {
41
- id: 'editor-commands',
42
- version: '1.0.0',
43
- requires: ['commands', 'keybindings'],
44
- setup(ctx) {
45
- const { registerCommand, host } = ctx;
46
- const cleanups = [];
47
- cleanups.push(registerCommand({
48
- id: 'editor.play',
49
- title: '开始预览 (Play)',
50
- execute: () => {
51
- requireDeps().dispatch({ kind: 'play' }, 'human');
52
- return { status: 'completed' };
53
- },
54
- }));
55
- cleanups.push(registerCommand({
56
- id: 'editor.stop',
57
- title: '停止预览 (Stop)',
58
- execute: () => {
59
- requireDeps().dispatch({ kind: 'stop' }, 'human');
60
- return { status: 'completed' };
61
- },
62
- }));
63
- cleanups.push(registerCommand({
64
- id: 'editor.toggleDisplay',
65
- title: '切换 Scene / Game 视图',
66
- execute: () => {
67
- const deps = requireDeps();
68
- const next = deps.getDisplay() === 'scene' ? 'game' : 'scene';
69
- deps.dispatch({ kind: 'setDisplay', display: next }, 'human');
70
- return { status: 'completed' };
71
- },
72
- }));
73
- cleanups.push(registerCommand({
74
- id: 'editor.undo',
75
- title: '撤销',
76
- execute: () => {
77
- requireDeps().undo();
78
- return { status: 'completed' };
79
- },
80
- }));
81
- cleanups.push(registerCommand({
82
- id: 'editor.redo',
83
- title: '重做',
84
- execute: () => {
85
- requireDeps().redo();
86
- return { status: 'completed' };
87
- },
88
- }));
89
- cleanups.push(registerCommand({
90
- id: 'editor.save',
91
- title: '保存',
92
- execute: () => {
93
- requireDeps().save();
94
- return { status: 'completed' };
95
- },
96
- }));
97
- // Application-scoped Mod+S runs before legacy edit shortcuts and is not
98
- // gated on the viewport surface anchor. Material / Texture / other asset
99
- // document pages replace the viewport dock layout, so isEditorSurfaceActive()
100
- // is false there — without this binding Ctrl+S falls through to the browser
101
- // "save page" dialog instead of editor.save → material-first save diversion.
102
- cleanups.push(host.keybindings.register({
103
- commandId: 'editor.save',
104
- keys: 'Mod+S',
105
- scope: APPLICATION_KEYBINDING_SCOPE,
106
- allowInEditable: true,
107
- priority: 100,
108
- }));
109
- cleanups.push(registerCommand({
110
- id: 'editor.selectAll',
111
- title: '全选实体',
112
- execute: async () => {
113
- if (await executeFocusedTextEditAction('selectAll')) {
114
- return { status: 'completed' };
115
- }
116
- requireDeps().selectAllEntities();
117
- return { status: 'completed' };
118
- },
119
- }));
120
- cleanups.push(registerCommand({
121
- id: 'editor.deselect',
122
- title: '清除选择',
123
- execute: () => {
124
- requireDeps().dispatch({ kind: 'setSelection', id: null }, 'human');
125
- return { status: 'completed' };
126
- },
127
- }));
128
- cleanups.push(registerCommand({
129
- id: 'editor.frameSelected',
130
- title: '聚焦所选',
131
- execute: () => {
132
- requireDeps().dispatch({ kind: 'requestFrame' }, 'human');
133
- return { status: 'completed' };
134
- },
135
- }));
136
- cleanups.push(registerCommand({
137
- id: 'editor.delete',
138
- title: '删除所选实体',
139
- execute: () => {
140
- const deps = requireDeps();
141
- const ids = deps.getEntitySelection();
142
- if (ids.length > 0)
143
- deps.deleteEntities(ids);
144
- return { status: 'completed' };
145
- },
146
- }));
147
- // Partial: bus event only, no listener wired yet (see file header). A
148
- // future preview/HMR owner in a standalone application subscribes to 'preview:reload'
149
- // and forwards to the Play iframe self-reload. `ctx.bus.emit` is typed
150
- // against AppBusEventMap which extends Record<string, unknown>, so an
151
- // ad-hoc topic name is accepted with an `unknown` payload.
152
- cleanups.push(registerCommand({
153
- id: 'editor.reloadPreview',
154
- title: '重载预览 (partial: 事件已发,尚无消费者)',
155
- execute: () => {
156
- // Not calling requireDeps() — reloadPreview is decoupled from the
157
- // editor gateway; the current path is a bus-event handoff, and a
158
- // future consumer may not even need the router deps.
159
- ctx.bus.emit('preview:reload', {});
160
- return { status: 'completed' };
161
- },
162
- }));
163
- cleanups.push(registerCommand({
164
- id: 'editor.restartPreview',
165
- title: '重建预览运行时',
166
- execute: () => {
167
- const restart = requireDeps().restartPreview;
168
- if (!restart)
169
- throw new Error('editor.restartPreview: host did not provide restartPreview');
170
- restart();
171
- return { status: 'completed' };
172
- },
173
- }));
174
- return () => {
175
- // Reverse order so first-registered is torn down last — same pattern
176
- // as builtin-commands.ts; `.slice()` clones the array so a repeat
177
- // unload doesn't mutate the closed-over `cleanups`.
178
- for (const c of cleanups.slice().reverse())
179
- c();
180
- };
181
- },
182
- };