@fenglimg/cocos-state-controller 0.1.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.
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/assets/script/controller/Capability.ts +100 -0
- package/assets/script/controller/Capability.ts.meta +10 -0
- package/assets/script/controller/CapabilityRegistry.ts +116 -0
- package/assets/script/controller/CapabilityRegistry.ts.meta +10 -0
- package/assets/script/controller/EnumPropRefMap.ts +232 -0
- package/assets/script/controller/EnumPropRefMap.ts.meta +10 -0
- package/assets/script/controller/NestedCtrlData.ts +199 -0
- package/assets/script/controller/NestedCtrlData.ts.meta +10 -0
- package/assets/script/controller/PrefabIntrospection.ts +151 -0
- package/assets/script/controller/PrefabIntrospection.ts.meta +10 -0
- package/assets/script/controller/Props.meta +13 -0
- package/assets/script/controller/StateControllerV2.ts +1957 -0
- package/assets/script/controller/StateControllerV2.ts.meta +10 -0
- package/assets/script/controller/StateEnumV2.ts +165 -0
- package/assets/script/controller/StateEnumV2.ts.meta +10 -0
- package/assets/script/controller/StateErrorManagerV2.ts +217 -0
- package/assets/script/controller/StateErrorManagerV2.ts.meta +10 -0
- package/assets/script/controller/StatePropHandlerV2.ts +316 -0
- package/assets/script/controller/StatePropHandlerV2.ts.meta +10 -0
- package/assets/script/controller/StatePropertyControlService.ts +148 -0
- package/assets/script/controller/StatePropertyControlService.ts.meta +10 -0
- package/assets/script/controller/StateSelectV2.ts +4542 -0
- package/assets/script/controller/StateSelectV2.ts.meta +10 -0
- package/assets/script/controller/capabilities/AutoSyncCapability.ts +30 -0
- package/assets/script/controller/capabilities/AutoSyncCapability.ts.meta +10 -0
- package/assets/script/controller/capabilities/EventCapability.ts +144 -0
- package/assets/script/controller/capabilities/EventCapability.ts.meta +10 -0
- package/assets/script/controller/capabilities/MigrationCapability.ts +94 -0
- package/assets/script/controller/capabilities/MigrationCapability.ts.meta +10 -0
- package/assets/script/controller/capabilities/MultiCtrlBindingCapability.ts +157 -0
- package/assets/script/controller/capabilities/MultiCtrlBindingCapability.ts.meta +10 -0
- package/assets/script/controller/capabilities/PropertyControlCapability.ts +124 -0
- package/assets/script/controller/capabilities/PropertyControlCapability.ts.meta +10 -0
- package/assets/script/controller/capabilities/RecordingCapability.ts +69 -0
- package/assets/script/controller/capabilities/RecordingCapability.ts.meta +10 -0
- package/assets/script/controller/capabilities/SelectedPageIdCapability.ts +88 -0
- package/assets/script/controller/capabilities/SelectedPageIdCapability.ts.meta +10 -0
- package/assets/script/controller/capabilities.meta +13 -0
- package/assets/script/controller/props/CtrlInspectorGroups.ts +138 -0
- package/assets/script/controller/props/CtrlInspectorGroups.ts.meta +10 -0
- package/assets/script/controller/props/SelectInspectorGroups.ts +104 -0
- package/assets/script/controller/props/SelectInspectorGroups.ts.meta +10 -0
- package/bin/csc.js +286 -0
- package/package.json +60 -0
- package/packages/state-controller-v2-panel/README.md +80 -0
- package/packages/state-controller-v2-panel/inspector-inject.js +917 -0
- package/packages/state-controller-v2-panel/inspector-probe.json +3767 -0
- package/packages/state-controller-v2-panel/lib/handlers.js +534 -0
- package/packages/state-controller-v2-panel/main.js +149 -0
- package/packages/state-controller-v2-panel/package.json +32 -0
- package/packages/state-controller-v2-panel/panel/build.js +23 -0
- package/packages/state-controller-v2-panel/panel/logic.js +1207 -0
- package/packages/state-controller-v2-panel/panel/styles.css +454 -0
- package/packages/state-controller-v2-panel/panel/template.html +296 -0
- package/packages/state-controller-v2-panel/scene-accessor.js +657 -0
- package/skills/cocos-state-controller/SKILL.md +28 -0
- package/skills/cocos-state-controller/refs/cli-usage.md +78 -0
- package/skills/cocos-state-controller/refs/editor-guide.md +127 -0
- package/skills/cocos-state-controller/refs/migrate.md +106 -0
- package/skills/cocos-state-controller/refs/upstream-pr.md +66 -0
- package/tools/migration/migrate-prefab-v1-to-v2.js +608 -0
- package/tools/state-controller-sync-manifest.json +33 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* State Controller Panel — Scene Script (Wave 3 scaffold).
|
|
5
|
+
*
|
|
6
|
+
* 跑在 Cocos 编辑器 scene 上下文 (有 cc / Editor 全局), 把 panel IPC 消息路由到
|
|
7
|
+
* scene 里的 StateControllerV2 实例上, 调 lib/handlers.js 的纯函数完成业务.
|
|
8
|
+
*
|
|
9
|
+
* 消息名规则 (与 panel/build.js 对应):
|
|
10
|
+
* state-controller-v2-panel:get-ctrl-snapshot
|
|
11
|
+
* state-controller-v2-panel:set-selected-index
|
|
12
|
+
* state-controller-v2-panel:set-state-by-id
|
|
13
|
+
* state-controller-v2-panel:set-recording
|
|
14
|
+
* state-controller-v2-panel:add-state
|
|
15
|
+
* state-controller-v2-panel:remove-state
|
|
16
|
+
* state-controller-v2-panel:add-property
|
|
17
|
+
* state-controller-v2-panel:list-ctrls (扫场景列所有 StateControllerV2)
|
|
18
|
+
*
|
|
19
|
+
* 广播事件名 (Editor.Ipc.sendToPanel):
|
|
20
|
+
* state-controller-v2-panel:on-state-changed
|
|
21
|
+
* state-controller-v2-panel:on-recording-changed
|
|
22
|
+
* state-controller-v2-panel:on-data-changed
|
|
23
|
+
*
|
|
24
|
+
* TODO Gemini 接入 panel UI 时验证:
|
|
25
|
+
* - require('./lib/handlers') 在 scene-script 上下文是否可达
|
|
26
|
+
* (handlers.js 进一步 require 项目内 assets/script/controller/*).
|
|
27
|
+
* 如不可达, 把 handlers 改成 inline 或通过 Editor.require 解析.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// 加载阶段日志: 帮诊断 cocos 2.x scene-script 静默吞错. 若控制台看不到
|
|
31
|
+
// "[state-controller-v2-panel] scene-script loaded" 说明 require 链失败.
|
|
32
|
+
try {
|
|
33
|
+
if (typeof Editor !== 'undefined' && Editor.log) {
|
|
34
|
+
Editor.log('[state-controller-v2-panel] scene-accessor.js loading...');
|
|
35
|
+
}
|
|
36
|
+
} catch (_) { /* noop */ }
|
|
37
|
+
|
|
38
|
+
let handlers;
|
|
39
|
+
try {
|
|
40
|
+
handlers = require('./lib/handlers');
|
|
41
|
+
if (typeof Editor !== 'undefined' && Editor.log) {
|
|
42
|
+
Editor.log('[state-controller-v2-panel] scene-script loaded, handlers keys:', Object.keys(handlers).join(','));
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (typeof Editor !== 'undefined' && Editor.error) {
|
|
46
|
+
Editor.error('[state-controller-v2-panel] handlers.js load failed:', e && (e.stack || e.message || String(e)));
|
|
47
|
+
}
|
|
48
|
+
handlers = {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 临时持有: 当前 panel 关注的 ctrl uuid → unsubscribe 函数 */
|
|
52
|
+
const broadcastBridges = new Map();
|
|
53
|
+
|
|
54
|
+
function getNodeByUuid(uuid) {
|
|
55
|
+
// cocos 2.x scene 上下文 API; 编辑器外的 Jest 测试不走这里 (handlers 单元已覆盖)
|
|
56
|
+
if (typeof cc === 'undefined' || !cc.engine) return null;
|
|
57
|
+
return cc.engine.getInstanceById(uuid) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getCtrlByUuid(uuid) {
|
|
61
|
+
const node = getNodeByUuid(uuid);
|
|
62
|
+
if (!node) return null;
|
|
63
|
+
return node.getComponent('StateControllerV2') || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSelectByUuid(uuid) {
|
|
67
|
+
const node = getNodeByUuid(uuid);
|
|
68
|
+
if (!node) return null;
|
|
69
|
+
return node.getComponent('StateSelectV2') || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function broadcast(eventSuffix, payload) {
|
|
73
|
+
if (typeof Editor === 'undefined' || !Editor.Ipc) return;
|
|
74
|
+
Editor.Ipc.sendToPanel('state-controller-v2-panel', 'state-controller-v2-panel:' + eventSuffix, payload);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ensureBridge(ctrl) {
|
|
78
|
+
if (!ctrl) return;
|
|
79
|
+
if (broadcastBridges.has(ctrl.ctrlId)) return;
|
|
80
|
+
const unsub = handlers.installBroadcastBridge(ctrl, function (eventName, payload) {
|
|
81
|
+
// eventName 形如 onStateChanged → on-state-changed
|
|
82
|
+
const suffix = eventName.replace(/[A-Z]/g, function (m) { return '-' + m.toLowerCase(); });
|
|
83
|
+
broadcast(suffix, payload);
|
|
84
|
+
});
|
|
85
|
+
broadcastBridges.set(ctrl.ctrlId, unsub);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function disposeBridge(ctrlId) {
|
|
89
|
+
const unsub = broadcastBridges.get(ctrlId);
|
|
90
|
+
if (unsub) {
|
|
91
|
+
try { unsub(); } catch (_) { /* noop */ }
|
|
92
|
+
broadcastBridges.delete(ctrlId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ===== inspector 行枚举 (P2b 排除 + M1 状态行为可视化 共用) =====
|
|
97
|
+
// 复刻 PrefabIntrospection.enumPropsForCtor 的过滤 (跳 _前缀 / SYSTEM_EXCLUDE / visible:false / readonly),
|
|
98
|
+
// 按 inspector 显示行结构产出 [{ scope, display, refs }]:
|
|
99
|
+
// scope = 组件序号 (node._components 下标, 对齐 inspector __comps__.<idx>) | 'node'
|
|
100
|
+
// refs = 该行的 leaf propRef 列表 (cc.Node 段按显示名聚合 x/y 等)
|
|
101
|
+
const SCI_SYSTEM_EXCLUDE = {
|
|
102
|
+
'cc.Widget.target': 1, 'cc.Widget.alignFlags': 1, 'cc.Animation.defaultClip': 1,
|
|
103
|
+
'cc.Animation.currentClip': 1, 'cc.ParticleSystem.file': 1, 'cc.AudioSource.clip': 1,
|
|
104
|
+
'cc.Node.rotation': 1, 'cc.Node.rotationX': 1, 'cc.Node.rotationY': 1,
|
|
105
|
+
};
|
|
106
|
+
const SCI_CONTROLLER_COMPS = { StateSelectV2: 1, StateControllerV2: 1, StateValue: 1, stateValue: 1 };
|
|
107
|
+
const SCI_NODE_AGG = {
|
|
108
|
+
Position: ['x', 'y'], Scale: ['scaleX', 'scaleY'], Anchor: ['anchorX', 'anchorY'],
|
|
109
|
+
Size: ['width', 'height'], Rotation: ['angle'], Color: ['color'], Opacity: ['opacity'],
|
|
110
|
+
Skew: ['skewX', 'skewY'],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function sciHumanize(key) {
|
|
114
|
+
return key.replace(/([A-Z])/g, ' $1').replace(/^./, function (c) { return c.toUpperCase(); }).replace(/\s+/g, ' ').trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sciEnumCompProps(ctor, compName, Attr) {
|
|
118
|
+
const out = [];
|
|
119
|
+
const props = ctor && ctor.__props__;
|
|
120
|
+
if (!Array.isArray(props)) return out;
|
|
121
|
+
const attrs = (Attr && Attr.getClassAttrs) ? Attr.getClassAttrs(ctor) : {};
|
|
122
|
+
for (let i = 0; i < props.length; i++) {
|
|
123
|
+
const key = props[i];
|
|
124
|
+
if (key.charAt(0) === '_') continue;
|
|
125
|
+
const propRef = compName + '.' + key;
|
|
126
|
+
if (SCI_SYSTEM_EXCLUDE[propRef]) continue;
|
|
127
|
+
if (attrs[key + '$_$visible'] === false) continue;
|
|
128
|
+
if (attrs[key + '$_$hasGetter'] === true && attrs[key + '$_$hasSetter'] !== true) continue; // readonly
|
|
129
|
+
out.push({ propRef: propRef, display: attrs[key + '$_$displayName'] || sciHumanize(key) });
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 插件侧按 propRef 读节点当前值 (mirror StateSelectV2.readNodeValueByPropRef, 不 require 项目源). */
|
|
135
|
+
function readNodeValByRef(node, propRef) {
|
|
136
|
+
if (!node || typeof propRef !== 'string') return undefined;
|
|
137
|
+
const lastDot = propRef.lastIndexOf('.');
|
|
138
|
+
if (lastDot <= 0 || lastDot >= propRef.length - 1) return undefined;
|
|
139
|
+
const compName = propRef.substring(0, lastDot);
|
|
140
|
+
const key = propRef.substring(lastDot + 1);
|
|
141
|
+
if (compName === 'cc.Node') return node[key];
|
|
142
|
+
const comp = node.getComponent(compName);
|
|
143
|
+
if (!comp) return undefined;
|
|
144
|
+
return comp[key];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 枚举选中节点的所有 inspector 显示行 → [{ scope, display, refs }] (无过滤, 由各调用方加语义). */
|
|
148
|
+
function enumInspectorRows(node) {
|
|
149
|
+
const cc_ = (typeof cc !== 'undefined') ? cc : null;
|
|
150
|
+
const Attr = cc_ && cc_.Class && cc_.Class.Attr;
|
|
151
|
+
const rows = [];
|
|
152
|
+
const comps = node._components || (node.getComponents ? node.getComponents(cc_ ? cc_.Component : Object) : []);
|
|
153
|
+
for (let ci = 0; ci < comps.length; ci++) {
|
|
154
|
+
const comp = comps[ci];
|
|
155
|
+
if (!comp) continue;
|
|
156
|
+
const cn = comp.__classname__ || (comp.constructor && comp.constructor.name) || '';
|
|
157
|
+
if (SCI_CONTROLLER_COMPS[cn]) continue;
|
|
158
|
+
const plist = sciEnumCompProps(comp.constructor, cn, Attr);
|
|
159
|
+
for (let pi = 0; pi < plist.length; pi++) {
|
|
160
|
+
rows.push({ scope: ci, display: plist[pi].display, refs: [plist[pi].propRef] });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// cc.Node 内置段: 按 inspector 显示名聚合 (2D inspector Position 只显 X/Y, 不含 z)
|
|
164
|
+
for (const disp in SCI_NODE_AGG) {
|
|
165
|
+
const subs = SCI_NODE_AGG[disp];
|
|
166
|
+
const refs = [];
|
|
167
|
+
for (let si = 0; si < subs.length; si++) {
|
|
168
|
+
const pr = 'cc.Node.' + subs[si];
|
|
169
|
+
if (!SCI_SYSTEM_EXCLUDE[pr]) refs.push(pr);
|
|
170
|
+
}
|
|
171
|
+
if (refs.length) rows.push({ scope: 'node', display: disp, refs: refs });
|
|
172
|
+
}
|
|
173
|
+
return rows;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
|
|
178
|
+
'get-ctrl-snapshot'(event, payload) {
|
|
179
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
180
|
+
if (!ctrl) return event.reply('ctrl not found', null);
|
|
181
|
+
ensureBridge(ctrl);
|
|
182
|
+
event.reply(null, handlers.getCtrlSnapshot(ctrl));
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
'set-selected-index'(event, payload) {
|
|
186
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
187
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
188
|
+
event.reply(null, handlers.setSelectedIndex(ctrl, payload.index));
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
'set-state-by-id'(event, payload) {
|
|
192
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
193
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
194
|
+
event.reply(null, handlers.setStateById(ctrl, payload.stateId));
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
'set-recording'(event, payload) {
|
|
198
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
199
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
200
|
+
event.reply(null, handlers.setRecording(ctrl, !!payload.isRecording));
|
|
201
|
+
// setRecording 是局部状态变, 顺手广播 data-changed 给 panel 刷新
|
|
202
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* TASK-002: 撤销本次录制. 调 ctrl.cancelRecording, ctrlData 回滚 + 视觉同步回滚.
|
|
207
|
+
* 同时广播 data-changed 让 panel 刷新; onRecordingCancelled 由 broadcast bridge 自动转发.
|
|
208
|
+
*/
|
|
209
|
+
'cancel-recording'(event, payload) {
|
|
210
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
211
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
212
|
+
event.reply(null, handlers.cancelRecording(ctrl));
|
|
213
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
214
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
'add-state'(event, payload) {
|
|
218
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
219
|
+
if (!ctrl) return event.reply('ctrl not found', -1);
|
|
220
|
+
const newId = handlers.addState(ctrl, payload.name || '');
|
|
221
|
+
event.reply(null, newId);
|
|
222
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
223
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
'remove-state'(event, payload) {
|
|
227
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
228
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
229
|
+
const ok = handlers.removeState(ctrl, payload.index);
|
|
230
|
+
event.reply(null, ok);
|
|
231
|
+
if (ok) {
|
|
232
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
233
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
'restore-last-deleted-state'(event, payload) {
|
|
238
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
239
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
240
|
+
const ok = handlers.restoreLastDeletedState(ctrl);
|
|
241
|
+
event.reply(null, ok);
|
|
242
|
+
if (ok) {
|
|
243
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
244
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
/** 回收站: 恢复指定 stateId 的暂存 state (追加到尾部). payload = { uuid, stateId } */
|
|
249
|
+
'restore-deleted-state'(event, payload) {
|
|
250
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
251
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
252
|
+
const ok = handlers.restoreDeletedState(ctrl, payload && payload.stateId);
|
|
253
|
+
event.reply(null, ok);
|
|
254
|
+
if (ok) {
|
|
255
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
256
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/** 回收站硬删: 彻底删除指定 stateId 的页数据 (不可恢复). payload = { uuid, stateId } */
|
|
261
|
+
'purge-deleted-state'(event, payload) {
|
|
262
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
263
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
264
|
+
const ok = handlers.purgeDeletedState(ctrl, payload && payload.stateId);
|
|
265
|
+
event.reply(null, ok);
|
|
266
|
+
if (ok) {
|
|
267
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
268
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
/** 回收站: 清空 (对所有暂存项硬删, 不可恢复). payload = { uuid } */
|
|
273
|
+
'purge-all-deleted-states'(event, payload) {
|
|
274
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
275
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
276
|
+
const ok = handlers.purgeAllDeletedStates(ctrl);
|
|
277
|
+
event.reply(null, ok);
|
|
278
|
+
if (ok) {
|
|
279
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
280
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 回收站: 进入某 stateId 的只读预览 (叠加到节点, 不改选中). payload = { uuid, stateId }
|
|
286
|
+
* 预览只动节点显示、不写 ctrlData, 故不标 scene-dirty; 广播 data-changed 让面板刷新预览态。
|
|
287
|
+
*/
|
|
288
|
+
'preview-deleted-state'(event, payload) {
|
|
289
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
290
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
291
|
+
const ok = handlers.previewDeletedState(ctrl, payload && payload.stateId);
|
|
292
|
+
event.reply(null, ok);
|
|
293
|
+
if (ok) broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/** 回收站: 退出预览 (按快照还原节点). payload = { uuid } */
|
|
297
|
+
'exit-preview'(event, payload) {
|
|
298
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
299
|
+
if (!ctrl) return event.reply('ctrl not found', false);
|
|
300
|
+
const ok = handlers.exitPreview(ctrl);
|
|
301
|
+
event.reply(null, ok);
|
|
302
|
+
if (ok) broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
'add-property'(event, payload) {
|
|
306
|
+
const ctrl = getCtrlByUuid(payload && payload.ctrlUuid);
|
|
307
|
+
const select = getSelectByUuid(payload && payload.selectUuid);
|
|
308
|
+
if (!ctrl || !select) return event.reply('ctrl or select not found', false);
|
|
309
|
+
const ok = handlers.addProperty(ctrl, select, payload.propType);
|
|
310
|
+
event.reply(null, ok);
|
|
311
|
+
if (ok) {
|
|
312
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
313
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
/** TASK-003: 移除 prop 跟随. 跟 'add-property' 同模板, 调 handlers.removeProperty. */
|
|
318
|
+
'remove-property'(event, payload) {
|
|
319
|
+
const ctrl = getCtrlByUuid(payload && payload.ctrlUuid);
|
|
320
|
+
const select = getSelectByUuid(payload && payload.selectUuid);
|
|
321
|
+
if (!ctrl || !select) return event.reply('ctrl or select not found', false);
|
|
322
|
+
const ok = handlers.removeProperty(ctrl, select, payload.propType);
|
|
323
|
+
event.reply(null, ok);
|
|
324
|
+
if (ok) {
|
|
325
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
326
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 列场景里所有 StateControllerV2. Panel 打开时调用, 拿 controller 树.
|
|
332
|
+
* 返回 [{ uuid, ctrlId, ctrlName }].
|
|
333
|
+
*/
|
|
334
|
+
'list-ctrls'(event) {
|
|
335
|
+
const out = [];
|
|
336
|
+
if (typeof cc === 'undefined' || !cc.director || !cc.director.getScene) {
|
|
337
|
+
return event.reply(null, out);
|
|
338
|
+
}
|
|
339
|
+
const scene = cc.director.getScene();
|
|
340
|
+
if (!scene) return event.reply(null, out);
|
|
341
|
+
|
|
342
|
+
function walk(node) {
|
|
343
|
+
if (!node) return;
|
|
344
|
+
const ctrl = node.getComponent('StateControllerV2');
|
|
345
|
+
if (ctrl) {
|
|
346
|
+
out.push({ uuid: node.uuid, ctrlId: ctrl.ctrlId, ctrlName: ctrl.ctrlName || '' });
|
|
347
|
+
ensureBridge(ctrl);
|
|
348
|
+
}
|
|
349
|
+
if (node.children) {
|
|
350
|
+
for (let i = 0; i < node.children.length; i++) walk(node.children[i]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
walk(scene);
|
|
354
|
+
event.reply(null, out);
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 新增观测树聚合查询. 返回整个场景的控制器-成员-属性拓扑结构.
|
|
359
|
+
*/
|
|
360
|
+
'list-scene-topology'(event) {
|
|
361
|
+
if (typeof cc === 'undefined' || !cc.director || !cc.director.getScene) {
|
|
362
|
+
return event.reply(null, { controllers: [] });
|
|
363
|
+
}
|
|
364
|
+
const scene = cc.director.getScene();
|
|
365
|
+
if (!scene) return event.reply(null, { controllers: [] });
|
|
366
|
+
|
|
367
|
+
const ctrlsInfo = [];
|
|
368
|
+
const selectsInfo = [];
|
|
369
|
+
|
|
370
|
+
function getNodePath(node) {
|
|
371
|
+
let path = node.name;
|
|
372
|
+
let curr = node.parent;
|
|
373
|
+
while (curr && curr.parent) {
|
|
374
|
+
path = curr.name + '/' + path;
|
|
375
|
+
curr = curr.parent;
|
|
376
|
+
}
|
|
377
|
+
return path;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function walk(node) {
|
|
381
|
+
if (!node) return;
|
|
382
|
+
const ctrl = node.getComponent('StateControllerV2');
|
|
383
|
+
if (ctrl) {
|
|
384
|
+
ctrlsInfo.push({ uuid: node.uuid, ctrl: ctrl });
|
|
385
|
+
ensureBridge(ctrl);
|
|
386
|
+
}
|
|
387
|
+
const select = node.getComponent('StateSelectV2');
|
|
388
|
+
if (select) {
|
|
389
|
+
selectsInfo.push({
|
|
390
|
+
nodeUuid: node.uuid,
|
|
391
|
+
nodeName: node.name,
|
|
392
|
+
nodePath: getNodePath(node),
|
|
393
|
+
select: select,
|
|
394
|
+
getRowsInfo: function() {
|
|
395
|
+
const userExcluded = {};
|
|
396
|
+
const ue = select._userExcludedProps || [];
|
|
397
|
+
for (let i = 0; i < ue.length; i++) userExcluded[ue[i]] = 1;
|
|
398
|
+
const canCtrl = typeof select.isPropertyControlledByPropRef === 'function';
|
|
399
|
+
|
|
400
|
+
function classify(propRef) {
|
|
401
|
+
if (userExcluded[propRef]) return 'excluded';
|
|
402
|
+
let ctrled = false;
|
|
403
|
+
if (canCtrl) { try { ctrled = !!select.isPropertyControlledByPropRef(propRef); } catch (e) { ctrled = false; } }
|
|
404
|
+
return ctrled ? 'tracked' : 'loose';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function rowKind(refs) {
|
|
408
|
+
let t = 0, e = 0, l = 0;
|
|
409
|
+
for (let i = 0; i < refs.length; i++) {
|
|
410
|
+
const k = classify(refs[i]);
|
|
411
|
+
if (k === 'tracked') t++; else if (k === 'excluded') e++; else l++;
|
|
412
|
+
}
|
|
413
|
+
if (l > 0) return (t > 0 || e > 0) ? 'mixed' : 'loose';
|
|
414
|
+
if (e > 0) return (t > 0) ? 'mixed' : 'excluded';
|
|
415
|
+
return 'tracked';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const rows = enumInspectorRows(node);
|
|
419
|
+
const result = [];
|
|
420
|
+
for (let i = 0; i < rows.length; i++) {
|
|
421
|
+
const kind = rowKind(rows[i].refs);
|
|
422
|
+
let compName = rows[i].scope === 'node' ? 'cc.Node' : '';
|
|
423
|
+
if (rows[i].scope !== 'node' && node._components && node._components[rows[i].scope]) {
|
|
424
|
+
const comp = node._components[rows[i].scope];
|
|
425
|
+
compName = comp.__classname__ || (comp.constructor && comp.constructor.name) || '';
|
|
426
|
+
}
|
|
427
|
+
result.push({
|
|
428
|
+
display: rows[i].display,
|
|
429
|
+
refs: rows[i].refs,
|
|
430
|
+
kind: kind,
|
|
431
|
+
compName: compName
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (node.children) {
|
|
439
|
+
for (let i = 0; i < node.children.length; i++) walk(node.children[i]);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
walk(scene);
|
|
443
|
+
|
|
444
|
+
// 支柱 B: 编辑器期也把序列化 binding 接上运行时监听, 让面板切状态能即时预览联动
|
|
445
|
+
// (rehydrate 幂等; 此刻全场景控制器都已 __preload 登记进 _byId, 目标按 id 可解析).
|
|
446
|
+
for (let i = 0; i < ctrlsInfo.length; i++) {
|
|
447
|
+
const c = ctrlsInfo[i].ctrl;
|
|
448
|
+
if (c && typeof c.rehydrateBindings === 'function') {
|
|
449
|
+
try { c.rehydrateBindings(); } catch (e) { /* 静默 */ }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const topology = handlers.buildTopology(ctrlsInfo, selectsInfo);
|
|
454
|
+
event.reply(null, topology);
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 支柱 B: 新增一条跨控制器联动声明 (写序列化 _bindingsData + 标脏).
|
|
459
|
+
* payload = { uuid(源ctrl节点), sourceStateId, targetCtrlId, targetStateId }
|
|
460
|
+
*/
|
|
461
|
+
'add-binding'(event, payload) {
|
|
462
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
463
|
+
if (!ctrl || typeof ctrl.addBinding !== 'function') return event.reply('ctrl not found', false);
|
|
464
|
+
const ok = ctrl.addBinding(payload.sourceStateId, payload.targetCtrlId, payload.targetStateId);
|
|
465
|
+
if (ok && typeof ctrl.rehydrateBindings === 'function') { try { ctrl.rehydrateBindings(); } catch (e) {} }
|
|
466
|
+
event.reply(null, ok);
|
|
467
|
+
if (ok) {
|
|
468
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
469
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/** 支柱 B: 删除一条联动. payload = { uuid, sourceStateId, targetCtrlId } */
|
|
474
|
+
'remove-binding'(event, payload) {
|
|
475
|
+
const ctrl = getCtrlByUuid(payload && payload.uuid);
|
|
476
|
+
if (!ctrl || typeof ctrl.removeBinding !== 'function') return event.reply('ctrl not found', false);
|
|
477
|
+
const ok = ctrl.removeBinding(payload.sourceStateId, payload.targetCtrlId);
|
|
478
|
+
if (ok && typeof ctrl.rehydrateBindings === 'function') { try { ctrl.rehydrateBindings(); } catch (e) {} }
|
|
479
|
+
event.reply(null, ok);
|
|
480
|
+
if (ok) {
|
|
481
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
482
|
+
broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* P2b: inspector 注入层查询 "选中节点上**非全受控**的属性行".
|
|
488
|
+
*
|
|
489
|
+
* 语义反转 (用户决策): auto-opt-in 让绝大多数属性受控 → 标记受控是噪音; 真正有价值的是**例外**:
|
|
490
|
+
* - excluded: 用户主动排除 (在 select._userExcludedProps)
|
|
491
|
+
* - loose: 可跟踪但没受控 (掉出控制 / 未接入) ← prefab-diff 漏更新的 bug 信号
|
|
492
|
+
* - mixed: 聚合行 (Vec) 子项里既有受控又有未受控/排除
|
|
493
|
+
* 全部受控的行不返回 → 注入侧不打标 (干净).
|
|
494
|
+
*
|
|
495
|
+
* 实现: 不 require 项目 ts 源, 在 plugin 侧用 cc 反射枚举 trackable props (复刻
|
|
496
|
+
* PrefabIntrospection.enumPropsForCtor 的过滤: 跳 _前缀 / SYSTEM_EXCLUDE / visible:false / readonly),
|
|
497
|
+
* 受控/排除判定走活 select 实例公开 API. cc.Node 按 inspector 显示名聚合 (Position=x/y 等).
|
|
498
|
+
*
|
|
499
|
+
* payload = { uuid }, reply = { ok, hasSelect, items: [{ scope, display, kind, refs }] }
|
|
500
|
+
* scope = 组件序号 (node._components 下标, 与 inspector __comps__.<idx> 对齐) | 'node'; display = 显示名; kind = excluded|loose|mixed
|
|
501
|
+
*/
|
|
502
|
+
'inspector-prop-status'(event, payload) {
|
|
503
|
+
const reply = function (r) { if (event && event.reply) event.reply(null, r); };
|
|
504
|
+
const node = getNodeByUuid(payload && payload.uuid);
|
|
505
|
+
if (!node) return reply({ ok: false, reason: 'no node' });
|
|
506
|
+
const select = node.getComponent('StateSelectV2');
|
|
507
|
+
if (!select) return reply({ ok: true, hasSelect: false, items: [] });
|
|
508
|
+
|
|
509
|
+
const userExcluded = {};
|
|
510
|
+
const ue = select._userExcludedProps || [];
|
|
511
|
+
for (let i = 0; i < ue.length; i++) userExcluded[ue[i]] = 1;
|
|
512
|
+
const canCtrl = typeof select.isPropertyControlledByPropRef === 'function';
|
|
513
|
+
|
|
514
|
+
function classify(propRef) {
|
|
515
|
+
if (userExcluded[propRef]) return 'excluded';
|
|
516
|
+
let ctrled = false;
|
|
517
|
+
if (canCtrl) { try { ctrled = !!select.isPropertyControlledByPropRef(propRef); } catch (e) { ctrled = false; } }
|
|
518
|
+
return ctrled ? 'tracked' : 'loose';
|
|
519
|
+
}
|
|
520
|
+
// 一行 (单 prop 或聚合多子项) → kind. 全 tracked 返回 null (不标)
|
|
521
|
+
function rowKind(refs) {
|
|
522
|
+
let t = 0, e = 0, l = 0;
|
|
523
|
+
for (let i = 0; i < refs.length; i++) {
|
|
524
|
+
const k = classify(refs[i]);
|
|
525
|
+
if (k === 'tracked') t++; else if (k === 'excluded') e++; else l++;
|
|
526
|
+
}
|
|
527
|
+
if (l > 0) return (t > 0 || e > 0) ? 'mixed' : 'loose';
|
|
528
|
+
if (e > 0) return (t > 0) ? 'mixed' : 'excluded';
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const items = [];
|
|
533
|
+
const rows = enumInspectorRows(node);
|
|
534
|
+
for (let i = 0; i < rows.length; i++) {
|
|
535
|
+
const kind = rowKind(rows[i].refs);
|
|
536
|
+
if (kind) items.push({ scope: rows[i].scope, display: rows[i].display, kind: kind, refs: rows[i].refs });
|
|
537
|
+
}
|
|
538
|
+
reply({ ok: true, hasSelect: true, items: items });
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* M1-1/M1-2: inspector 状态行为可视化查询. 返回:
|
|
543
|
+
* - props: 各受控 propRef 的 { variesAcrossStates, valueByState:{[idx]:serialized}, defaultValue } (M1-1 primitive)
|
|
544
|
+
* - states: [{index, stateId, name}] (hover 标题用)
|
|
545
|
+
* - rows: inspector 行视图 [{ scope, display, refs, variesAcrossStates }] — 注入侧据此对
|
|
546
|
+
* 「跨状态有差异」的行标 ● (varies = 行内任一 ref 在 props 里 variesAcrossStates).
|
|
547
|
+
* 纯读, 不改 ctrlData. payload = { uuid }.
|
|
548
|
+
*/
|
|
549
|
+
'inspector-prop-state-values'(event, payload) {
|
|
550
|
+
const reply = function (r) { if (event && event.reply) event.reply(null, r); };
|
|
551
|
+
const node = getNodeByUuid(payload && payload.uuid);
|
|
552
|
+
if (!node) return reply({ ok: false, reason: 'no node' });
|
|
553
|
+
const select = node.getComponent('StateSelectV2');
|
|
554
|
+
if (!select) return reply({ ok: true, hasSelect: false, states: [], props: {}, rows: [] });
|
|
555
|
+
// ctrl 从 select 推导 (可能在祖先节点上); handlers 内部兜底再推一次
|
|
556
|
+
const ctrl = (select._ctrlsMap && select._ctrlsMap[select.currCtrlId]) || node.getComponent('StateControllerV2') || null;
|
|
557
|
+
const sv = handlers.getPropStateValues(select, ctrl);
|
|
558
|
+
// M2a-1: 录制态 → 标"改过未提交"的脏行 (节点当前值 ≠ 该 state 存储值, default 兜底).
|
|
559
|
+
const isRecording = !!(ctrl && ctrl.isRecording);
|
|
560
|
+
sv.isRecording = isRecording;
|
|
561
|
+
const selIdx = sv.selectedIndex;
|
|
562
|
+
function isRowDirty(refs) {
|
|
563
|
+
if (!isRecording || selIdx < 0) return false;
|
|
564
|
+
for (let r = 0; r < refs.length; r++) {
|
|
565
|
+
const p = sv.props && sv.props[refs[r]];
|
|
566
|
+
if (!p) continue;
|
|
567
|
+
const curSer = handlers.serializeStateValue(readNodeValByRef(node, refs[r]));
|
|
568
|
+
let base = p.valueByState ? p.valueByState[selIdx] : undefined;
|
|
569
|
+
if (base === null || base === undefined) base = p.defaultValue;
|
|
570
|
+
if (base === undefined) base = null;
|
|
571
|
+
if (JSON.stringify(curSer) !== JSON.stringify(base)) return true;
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
// 行视图: 把 props (per-ref varies/override/dirty) join 到 inspector 显示行
|
|
576
|
+
const rows = enumInspectorRows(node);
|
|
577
|
+
sv.rows = [];
|
|
578
|
+
for (let i = 0; i < rows.length; i++) {
|
|
579
|
+
const refs = rows[i].refs;
|
|
580
|
+
let varies = false, override = false, hasData = false;
|
|
581
|
+
for (let r = 0; r < refs.length; r++) {
|
|
582
|
+
const p = sv.props && sv.props[refs[r]];
|
|
583
|
+
if (p) {
|
|
584
|
+
hasData = true;
|
|
585
|
+
if (p.variesAcrossStates) varies = true;
|
|
586
|
+
if (p.overriddenAtCurrent) override = true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (hasData) sv.rows.push({
|
|
590
|
+
scope: rows[i].scope, display: rows[i].display, refs: refs,
|
|
591
|
+
variesAcrossStates: varies, overriddenAtCurrent: override,
|
|
592
|
+
dirty: isRowDirty(refs),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
reply(sv);
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* P2b 写半边: 点击 inspector 标记 → 切换某些 propRef 的排除状态.
|
|
600
|
+
*
|
|
601
|
+
* M2b-1 后: 走干净的 select.setPropExcluded(ref, bool) mutation API (替代旧的
|
|
602
|
+
* "改 _userExcludedProps 数组 + 读 excludedPropsDisplay getter 副作用" hack). 该方法内部
|
|
603
|
+
* 改数组 + togglePropertyControl 同步跟踪态 + 同步 _lastSeenExcluded 快照.
|
|
604
|
+
*
|
|
605
|
+
* Undo: best-effort 包 _Scene.Undo.recordObject/commit (项目无先例, API 不对也不影响写入).
|
|
606
|
+
* payload = { uuid, refs: [propRef...], action: 'exclude'|'unexclude' }
|
|
607
|
+
*/
|
|
608
|
+
'inspector-toggle-exclude'(event, payload) {
|
|
609
|
+
const reply = function (r) { if (event && event.reply) event.reply(null, r); };
|
|
610
|
+
const node = getNodeByUuid(payload && payload.uuid);
|
|
611
|
+
if (!node) return reply({ ok: false, reason: 'no node' });
|
|
612
|
+
const select = node.getComponent('StateSelectV2');
|
|
613
|
+
if (!select) return reply({ ok: false, reason: 'no select' });
|
|
614
|
+
const refs = (payload && payload.refs) || [];
|
|
615
|
+
const action = payload && payload.action;
|
|
616
|
+
if (!refs.length || (action !== 'exclude' && action !== 'unexclude')) {
|
|
617
|
+
return reply({ ok: false, reason: 'bad payload' });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Undo best-effort: 记录变更前 (项目内无 _Scene.Undo 先例, 容错处理)
|
|
621
|
+
const Undo = (typeof _Scene !== 'undefined' && _Scene) ? _Scene.Undo : null;
|
|
622
|
+
try { if (Undo && Undo.recordObject) Undo.recordObject(node.uuid, 'state-controller: toggle exclude'); } catch (e) { /* noop */ }
|
|
623
|
+
|
|
624
|
+
const excluded = (action === 'exclude');
|
|
625
|
+
const useApi = typeof select.setPropExcluded === 'function';
|
|
626
|
+
for (let i = 0; i < refs.length; i++) {
|
|
627
|
+
if (useApi) {
|
|
628
|
+
try { select.setPropExcluded(refs[i], excluded); } catch (e) { /* noop */ }
|
|
629
|
+
} else {
|
|
630
|
+
// 兜底 (旧组件无 setPropExcluded): 退回数组 + getter 副作用路径
|
|
631
|
+
if (!select._userExcludedProps) select._userExcludedProps = [];
|
|
632
|
+
const idx = select._userExcludedProps.indexOf(refs[i]);
|
|
633
|
+
if (excluded) { if (idx === -1) select._userExcludedProps.push(refs[i]); }
|
|
634
|
+
else { if (idx >= 0) select._userExcludedProps.splice(idx, 1); }
|
|
635
|
+
try { void select.excludedPropsDisplay; } catch (e) { /* noop */ }
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
try { if (Undo && Undo.commit) Undo.commit(); } catch (e) { /* noop */ }
|
|
640
|
+
// 标脏 → 可 Ctrl+S 存盘
|
|
641
|
+
if (typeof Editor !== 'undefined' && Editor.Ipc) Editor.Ipc.sendToMain('scene:set-dirty');
|
|
642
|
+
// 广播 data-changed (其它面板 / 监听方刷新)
|
|
643
|
+
const ctrl = node.getComponent('StateControllerV2');
|
|
644
|
+
if (ctrl) broadcast('on-data-changed', { ctrlId: ctrl.ctrlId });
|
|
645
|
+
reply({ ok: true });
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* panel 关闭时调用, 解除所有广播桥, 避免 leak.
|
|
650
|
+
*/
|
|
651
|
+
'dispose-all-bridges'(event) {
|
|
652
|
+
const ids = [];
|
|
653
|
+
broadcastBridges.forEach(function (_, id) { ids.push(id); });
|
|
654
|
+
ids.forEach(disposeBridge);
|
|
655
|
+
event.reply(null, true);
|
|
656
|
+
},
|
|
657
|
+
};
|