@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,1957 @@
|
|
|
1
|
+
const { ccclass, menu, property, executeInEditMode } = cc._decorator;
|
|
2
|
+
import { CapabilityRegistry } from "./CapabilityRegistry";
|
|
3
|
+
import { cloneValueByType } from "./NestedCtrlData";
|
|
4
|
+
// Wave 3 T07: 让所有 L0 内置 capability 跟着 StateControllerV2 一起被打入产出 (side-effect 自注册).
|
|
5
|
+
// 显式 /index: cocos 2.x ts 编译路径不做 folder→index 解析, 写 "./capabilities" 会报
|
|
6
|
+
// "Cannot find module './capabilities'" (jest 用 node resolver 能解出, 编辑器不行).
|
|
7
|
+
import { EnumPropName, EnumStateName, EnumUpdateType } from "./StateEnumV2";
|
|
8
|
+
import { StateErrorManager } from "./StateErrorManagerV2";
|
|
9
|
+
import { StateSelectV2 } from "./StateSelectV2";
|
|
10
|
+
// 仅取 CtrlStateOpsGroup; CtrlRecordGroup 不再在 controller inspector 暴露(录制改由 StateSelect 承载).
|
|
11
|
+
// 该 import 仍会加载整个模块, 触发 CtrlRecordGroup 的 @ccclass 注册 — 旧 prefab 序列化里残留的
|
|
12
|
+
// 录制组按 cid 反序列化不报错(重存即清).
|
|
13
|
+
import { CtrlRecycleBinGroup, CtrlStateOpsGroup } from "./props/CtrlInspectorGroups";
|
|
14
|
+
// 支柱 B: 可序列化跨控制器联动 — 复用运行时 binding capability 接线 (无循环依赖: 该 capability 不 import 本类).
|
|
15
|
+
import { MultiCtrlBindingCapability } from "./capabilities/MultiCtrlBindingCapability";
|
|
16
|
+
|
|
17
|
+
cc.Enum(EnumStateName);
|
|
18
|
+
|
|
19
|
+
@ccclass("StateValue")
|
|
20
|
+
export class StateValue {
|
|
21
|
+
@property(cc.String)
|
|
22
|
+
public name: string = "";
|
|
23
|
+
|
|
24
|
+
@property({ type: cc.Integer, readonly: true })
|
|
25
|
+
public stateId: number = 0;
|
|
26
|
+
|
|
27
|
+
// cc 反序列化要求 @ccclass 必须可以无参构造。
|
|
28
|
+
// 通过工厂方法构造业务实例,避免反序列化路径崩溃。
|
|
29
|
+
public static create(name: string, stateId: number): StateValue {
|
|
30
|
+
const value = new StateValue();
|
|
31
|
+
value.name = name;
|
|
32
|
+
value.stateId = stateId;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 项目内 Component 脚本不传类名: 引擎按 frame.script(文件名 "StateControllerV2")自动注册,
|
|
38
|
+
// 避免 editor 告警 3616 "Should not specify class name ... for Component which defines in project".
|
|
39
|
+
// getComponent('StateControllerV2') 与 .fire cid 序列化均不受影响 (cid 由 _RF.uuid 注册).
|
|
40
|
+
@ccclass
|
|
41
|
+
@menu("State/StateControllerV2")
|
|
42
|
+
@executeInEditMode()
|
|
43
|
+
export class StateControllerV2 extends cc.Component {
|
|
44
|
+
/** 状态id自增 */
|
|
45
|
+
@property({ visible: false })
|
|
46
|
+
private stateIdAuto = 0;
|
|
47
|
+
|
|
48
|
+
/** 控制器唯一id,如果使用uuid每次打开编辑器就会变 */
|
|
49
|
+
@property({ visible: false })
|
|
50
|
+
public ctrlId = Date.now();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 支柱 B: 序列化的跨控制器联动声明. JSON 串 [{sourceStateId,targetCtrlId,targetStateId}].
|
|
54
|
+
* 用 targetCtrlId(数字)代替对象引用以便进 .fire/.prefab 序列化; 运行时 start() 时
|
|
55
|
+
* rehydrateBindings 解析 id→ctrl 对象, 复用 MultiCtrlBindingCapability 接线.
|
|
56
|
+
*/
|
|
57
|
+
@property({ visible: false })
|
|
58
|
+
private _bindingsData: string = "";
|
|
59
|
+
|
|
60
|
+
/** 支柱 B: ctrlId → 实例 全局注册表, 供 binding 按 id 解析目标控制器. */
|
|
61
|
+
private static _byId: { [id: number]: StateControllerV2 } = {};
|
|
62
|
+
public static getById(id: number): StateControllerV2 | null {
|
|
63
|
+
return (typeof id === "number" && StateControllerV2._byId[id]) || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static _register(ctrl: StateControllerV2): void {
|
|
67
|
+
if (ctrl && typeof ctrl.ctrlId === "number") StateControllerV2._byId[ctrl.ctrlId] = ctrl;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static _unregister(ctrl: StateControllerV2): void {
|
|
71
|
+
if (ctrl && typeof ctrl.ctrlId === "number" && StateControllerV2._byId[ctrl.ctrlId] === ctrl) {
|
|
72
|
+
delete StateControllerV2._byId[ctrl.ctrlId];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 历史状态名字 */
|
|
77
|
+
@property({ visible: false })
|
|
78
|
+
private _historyStateName: { [key: number]: string } = {};
|
|
79
|
+
|
|
80
|
+
/** 是否正在改变 */
|
|
81
|
+
private isChanging?: boolean;
|
|
82
|
+
/** 是否初始 ,假设编辑器默认状态是2,代码里面正好第一次状态也是2,会导致selecteindex那里不刷新状态。 */
|
|
83
|
+
private isInit: boolean = true;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 是否正在录制 (Wave 2 Topic 3 prefab diff 录制路径).
|
|
87
|
+
* 通过普通字段, 不加 @property → 不序列化, 重启编辑器 / 反序列化后自动回 false。
|
|
88
|
+
*/
|
|
89
|
+
private _recording: boolean = false;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 录制开始时的 selectedIndex (TASK-002 cancelRecording 用).
|
|
93
|
+
* 在 _doStartRecording 中赋值; cancelRecording 用它定位需要回滚的 ctrlData state.
|
|
94
|
+
* 非 @property, 不序列化.
|
|
95
|
+
*/
|
|
96
|
+
private _recordingStartState: number = -1;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 标记当前 stopRecording 的触发来源 (模型 Z, 切 state 时自动停).
|
|
100
|
+
* "manual": 用户点录制按钮关 (默认), StateSelectV2.onRecordingStop 走完整路径 + 弹窗
|
|
101
|
+
* "auto": selectedIndex setter 自动触发, StateSelectV2.onRecordingStop 走静默 + Editor.log
|
|
102
|
+
* 字段非 @property, 不序列化. 仅在 stopRecording 调用前后短暂有效.
|
|
103
|
+
*/
|
|
104
|
+
public stopRecordingMode: "manual" | "auto" = "manual";
|
|
105
|
+
|
|
106
|
+
// ================== 🔧 IMPL-001: BFS缓存优化 ==================
|
|
107
|
+
/**
|
|
108
|
+
* 🎯 缓存优化说明:
|
|
109
|
+
* - _stateSelectCache: 缓存当前控制器直接控制的所有StateSelectV2组件
|
|
110
|
+
* - _cacheDirty: 缓存脏标记,当节点结构变化时设为true
|
|
111
|
+
* - 使用缓存后,状态切换从O(n)遍历优化为O(1)查找
|
|
112
|
+
*/
|
|
113
|
+
/** 🔧 缓存:存储直接控制的StateSelectV2组件 */
|
|
114
|
+
private _stateSelectCache: StateSelectV2[] = null;
|
|
115
|
+
|
|
116
|
+
/** 🔧 缓存脏标记:true表示需要重建缓存 */
|
|
117
|
+
private _cacheDirty: boolean = true;
|
|
118
|
+
|
|
119
|
+
/** 控制器名字 (反序列化存储字段, inspector 通过 ctrlName getter 显示) */
|
|
120
|
+
@property({ visible: false })
|
|
121
|
+
private _ctrlName: string = "";
|
|
122
|
+
|
|
123
|
+
@property({ displayName: "控制器 id", tooltip: "控制器唯一名称" })
|
|
124
|
+
public get ctrlName() {
|
|
125
|
+
return this._ctrlName;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public set ctrlName(value: string) {
|
|
129
|
+
if (!CC_EDITOR) {
|
|
130
|
+
StateErrorManager.error("非编辑器环境,不更新名称", {
|
|
131
|
+
component: "StateControllerV2",
|
|
132
|
+
method: "ctrlName.setter",
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this._ctrlName = value;
|
|
137
|
+
this.updateState(EnumUpdateType.Name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private _previousIndex: number = -1;
|
|
141
|
+
/** 上一次的选中下标 */
|
|
142
|
+
public get previousIndex(): number {
|
|
143
|
+
return this._previousIndex;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 选中的状态下标 (反序列化存储, inspector 通过 selectedIndex getter 显示下拉) */
|
|
147
|
+
@property({ type: EnumStateName, visible: false })
|
|
148
|
+
private _selectedIndex: EnumStateName = 0;
|
|
149
|
+
|
|
150
|
+
/** 状态顺序上移触发 (普通访问器, inspector 可见性由 stateOps 折叠组代理) */
|
|
151
|
+
public get moveStateUp() {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public set moveStateUp(value: boolean) {
|
|
156
|
+
if (value) {
|
|
157
|
+
this.adjustSelectedStateOrder(-1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 状态顺序下移触发 (普通访问器, inspector 可见性由 stateOps 折叠组代理) */
|
|
162
|
+
public get moveStateDown() {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public set moveStateDown(value: boolean) {
|
|
167
|
+
if (value) {
|
|
168
|
+
this.adjustSelectedStateOrder(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 复制当前状态触发 (普通访问器, inspector 可见性由 stateOps 折叠组代理) */
|
|
173
|
+
public get duplicateCurrentState() {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public set duplicateCurrentState(value: boolean) {
|
|
178
|
+
if (value) {
|
|
179
|
+
this.copySelectedState();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 删除当前状态触发 (普通访问器, inspector 可见性由 stateOps 折叠组代理) */
|
|
184
|
+
public get deleteCurrentState() {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public set deleteCurrentState(value: boolean) {
|
|
189
|
+
if (value) {
|
|
190
|
+
this.removeSelectedState();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 状态名字列表 (反序列化字段). inspector 通过 states getter/setter 暴露. */
|
|
195
|
+
@property({ type: StateValue, visible: false })
|
|
196
|
+
private _states: StateValue[] = [];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 上一次稳定的 state 快照。
|
|
200
|
+
* Cocos Inspector 数组 UI 会先原地改 this._states 再调 setter, 不能依赖 this._states
|
|
201
|
+
* 推导旧长度/旧 id, 否则新增默认 StateValue(name="", stateId=0) 和删除迁移都会漏判。
|
|
202
|
+
*/
|
|
203
|
+
private _stateSnapshot: StateValue[] = [];
|
|
204
|
+
|
|
205
|
+
/** 软删除 state 暂存。缩短 states 只移出活跃列表, 不立刻清对应 stateId 数据。 */
|
|
206
|
+
@property({ type: StateValue, visible: false })
|
|
207
|
+
private _deletedStates: StateValue[] = [];
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 回收站下拉 (restoreTarget / purgeTarget) 的选项→stateId 反查表 (非序列化)。
|
|
211
|
+
* refreshRecycleBinEnums 注入 enumList 时同步刷新; 选项 value=v 对应 _recycleBinOptionIds[v-1]。
|
|
212
|
+
*/
|
|
213
|
+
private _recycleBinOptionIds: number[] = [];
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 回收站只读预览中的 stateId (非 @property, 不序列化, 重载/反序列化后自动回 -1)。
|
|
217
|
+
* -1 = 未预览。预览不改 selectedIndex (激活态高亮不变), 仅把回收态数据只读叠加到节点;
|
|
218
|
+
* 任何退出路径 (退出/恢复/切state/录制/销毁) 都先 exitPreview 按快照精确还原。
|
|
219
|
+
*/
|
|
220
|
+
private _previewingStateId: number = -1;
|
|
221
|
+
|
|
222
|
+
/** 状态列表 inspector 入口. cocos 数组 UI 的 + / × / 拖动会调 setter, 走完整 invariants. */
|
|
223
|
+
@property({ type: StateValue, displayName: "states", tooltip: "状态列表 — 用 cocos 数组 UI 直接添加/删除/重排/改名" })
|
|
224
|
+
public get states() {
|
|
225
|
+
return this._states;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private set states(value: StateValue[]) {
|
|
229
|
+
if (!CC_EDITOR) {
|
|
230
|
+
StateErrorManager.error("非编辑器环境,不更新状态", {
|
|
231
|
+
component: "StateControllerV2",
|
|
232
|
+
method: "states.setter",
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// TASK-002: 录制中不能修改状态列表 (避免 ctrlData 索引错位).
|
|
238
|
+
if (this._recording) {
|
|
239
|
+
StateErrorManager.warn("录制中不能修改状态列表, 请先停止/撤销录制", {
|
|
240
|
+
component: "StateControllerV2",
|
|
241
|
+
method: "states.setter",
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 🔧 输入验证:确保数组有效
|
|
247
|
+
if (!value || !Array.isArray(value)) {
|
|
248
|
+
StateErrorManager.warn("states必须是有效的数组", {
|
|
249
|
+
component: "StateControllerV2",
|
|
250
|
+
method: "states.setter",
|
|
251
|
+
params: { valueType: typeof value, isArray: Array.isArray(value) },
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const oldStates = this._stateSnapshot.length > 0 ? this._stateSnapshot : this.cloneStateSnapshot(this._states);
|
|
257
|
+
const oldLen = oldStates.length;
|
|
258
|
+
const newLen = value.length;
|
|
259
|
+
|
|
260
|
+
let applyIndex: number = this._selectedIndex;
|
|
261
|
+
|
|
262
|
+
// 处理状态数量不足的情况
|
|
263
|
+
if (newLen < 2) {
|
|
264
|
+
applyIndex = 0;
|
|
265
|
+
StateErrorManager.warn("建议至少添加两个状态", {
|
|
266
|
+
component: "StateControllerV2",
|
|
267
|
+
method: "states.setter",
|
|
268
|
+
params: { currentStateCount: newLen },
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 🔧 处理状态变化的核心逻辑
|
|
273
|
+
let deletedIndices: number[] = [];
|
|
274
|
+
|
|
275
|
+
// 🔧 首先检查并初始化所有未正确初始化的状态对象
|
|
276
|
+
for (let index = 0; index < newLen; index++) {
|
|
277
|
+
// 🔧 新增的状态由编辑器默认构造,name为""且stateId为0(未分配),需要正确初始化
|
|
278
|
+
const isUninitialized
|
|
279
|
+
= !value[index]
|
|
280
|
+
|| value[index].name === undefined
|
|
281
|
+
|| value[index].stateId === undefined
|
|
282
|
+
|| (value[index].name === "" && value[index].stateId === 0 && index >= oldLen);
|
|
283
|
+
if (isUninitialized) {
|
|
284
|
+
// 🔧 使用智能命名方法生成状态名字
|
|
285
|
+
const smartStateName = this.getSmartStateName(index);
|
|
286
|
+
const newStateId = this.stateIdAuto++;
|
|
287
|
+
value[index] = StateValue.create(smartStateName, newStateId);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// 🔧 检测现有状态的手动更改
|
|
291
|
+
const defaultName = (index + 1).toString();
|
|
292
|
+
const currentName = value[index].name;
|
|
293
|
+
|
|
294
|
+
// 只有当名字不是默认名字时,才可能是手动修改的
|
|
295
|
+
if (currentName !== defaultName) {
|
|
296
|
+
// 检查是否已经在历史记录中
|
|
297
|
+
if (!this._historyStateName[index] || this._historyStateName[index] !== currentName) {
|
|
298
|
+
// 记录或更新历史记录
|
|
299
|
+
if (!this._historyStateName) {
|
|
300
|
+
this._historyStateName = {};
|
|
301
|
+
}
|
|
302
|
+
this._historyStateName[index] = currentName;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// 如果改回了默认名字,删除历史记录
|
|
307
|
+
if (this._historyStateName && this._historyStateName[index]) {
|
|
308
|
+
delete this._historyStateName[index];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (oldLen > newLen) {
|
|
315
|
+
// 🔧 处理状态删除:找到所有被删除的状态索引
|
|
316
|
+
deletedIndices = this.findDeletedIndices(oldStates, value);
|
|
317
|
+
this.stashDeletedStates(oldStates, deletedIndices, value);
|
|
318
|
+
let adjustment = 0;
|
|
319
|
+
// #S5: 仅"删在选中**之前**(严格 <)"的 state 才下移选中, 让选中跟随; 删选中**自身**
|
|
320
|
+
// (deletedIndex == applyIndex) 不下移 → 选中保持原 index = 补位进来的下一个 state(用户裁定"选下一个")。
|
|
321
|
+
// 旧 <= 会在删选中自身时多减一位 → 落到前一个; 且与 removeSelectedState 预设值双重调整。
|
|
322
|
+
for (const deletedIndex of deletedIndices) {
|
|
323
|
+
if (deletedIndex < applyIndex) {
|
|
324
|
+
adjustment++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// 应用调整 (clamp 到合法范围: 删末位选中时 applyIndex 可能越界, Math.min 兜底)
|
|
328
|
+
let newIndex = Math.max(0, applyIndex - adjustment);
|
|
329
|
+
newIndex = Math.min(newIndex, newLen - 1);
|
|
330
|
+
applyIndex = newIndex;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 🔧 更新内部状态数组
|
|
334
|
+
StateErrorManager.debug("开始更新状态数组", {
|
|
335
|
+
component: "StateControllerV2",
|
|
336
|
+
method: "states.setter",
|
|
337
|
+
params: { oldLength: oldLen, newLength: newLen, deletedCount: deletedIndices.length },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
this._states = value;
|
|
341
|
+
this.ensureUniqueStateIds();
|
|
342
|
+
|
|
343
|
+
const stateMap: { [key: string]: boolean } = {};
|
|
344
|
+
const array = value.map((val, i) => {
|
|
345
|
+
if (!val) {
|
|
346
|
+
StateErrorManager.error("状态对象不能为空", {
|
|
347
|
+
component: "StateControllerV2",
|
|
348
|
+
method: "states.setter",
|
|
349
|
+
params: { stateIndex: i },
|
|
350
|
+
});
|
|
351
|
+
return { name: "error", value: i };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 🔧 处理重复状态名
|
|
355
|
+
if (stateMap[val.name]) {
|
|
356
|
+
const newName = val.name + "_" + i;
|
|
357
|
+
StateErrorManager.warn("检测到重复的状态名,自动重命名", {
|
|
358
|
+
component: "StateControllerV2",
|
|
359
|
+
method: "states.setter",
|
|
360
|
+
params: { originalName: val.name, newName: newName },
|
|
361
|
+
});
|
|
362
|
+
val.name = newName;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
stateMap[val.name] = true;
|
|
366
|
+
return { name: val.name, value: i };
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// @ts-expect-error 允许使用该方法
|
|
370
|
+
cc.Class.Attr.setClassAttr(this, "selectedIndex", "enumList", array);
|
|
371
|
+
this._selectedIndex = applyIndex;
|
|
372
|
+
|
|
373
|
+
// 刻意不调 forceRefreshInspector: 自动强刷会打断当前操作 (焦点丢失 / 抖动),
|
|
374
|
+
// selectedPage 等 getter @property 的陈旧显示由用户点 "刷新检查器" 按钮解决.
|
|
375
|
+
|
|
376
|
+
this.refreshStateSnapshot();
|
|
377
|
+
|
|
378
|
+
// 🔧 通知相关组件状态列表已更新
|
|
379
|
+
if (deletedIndices.length > 0) {
|
|
380
|
+
StateErrorManager.info("状态列表更新完成(包含删除)", {
|
|
381
|
+
component: "StateControllerV2",
|
|
382
|
+
method: "states.setter",
|
|
383
|
+
params: { finalStateCount: newLen, deletedIndices: deletedIndices, currentIndex: applyIndex },
|
|
384
|
+
});
|
|
385
|
+
// state 数据以 stateId 为身份保留。缩短 states 只刷新枚举, 不做 index 数据迁移/清理。
|
|
386
|
+
this.updateState(EnumUpdateType.SelPage);
|
|
387
|
+
// 删除后选中态可能补位/clamp 到新 index (这里 _selectedIndex 是直接赋值, 没走 setter 的
|
|
388
|
+
// 节点 apply 流程), 故主动重绘节点到新选中 state — 否则节点残留被删 state 的视觉,
|
|
389
|
+
// 表现为"删除后当前状态没切换"(尤其删末位/只剩一个状态时)。
|
|
390
|
+
// 注意: 只在删除分支补 apply。新增分支若也 apply 会污染新 state 的 propData。
|
|
391
|
+
this.updateState(EnumUpdateType.State);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
StateErrorManager.info("状态列表更新完成", {
|
|
395
|
+
component: "StateControllerV2",
|
|
396
|
+
method: "states.setter",
|
|
397
|
+
params: { finalStateCount: newLen, currentIndex: applyIndex },
|
|
398
|
+
});
|
|
399
|
+
this.updateState(EnumUpdateType.SelPage);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 选择的状态下标 (index 级低层 API)。
|
|
405
|
+
*
|
|
406
|
+
* @deprecated 业务代码请改用 `SelectedPageIdCapability.getSelectedStateId(ctrl)` 读取。
|
|
407
|
+
* index 会随 state reorder/delete 漂移; SelectedPageIdCapability 用稳定 stateId 定位。
|
|
408
|
+
* 此 getter/setter 仅供 inspector 下拉与 capability 内部使用, 不要在业务层直接读写。
|
|
409
|
+
*/
|
|
410
|
+
@property({ type: EnumStateName, displayName: "state", tooltip: "当前选中的状态" })
|
|
411
|
+
public get selectedIndex() {
|
|
412
|
+
return this._selectedIndex;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @deprecated 业务代码请改用 `SelectedPageIdCapability.setStateById(ctrl, stateId)` 切换 (按稳定 stateId, 抗 reorder/delete 漂移)。详见 getter 说明。
|
|
417
|
+
*/
|
|
418
|
+
public set selectedIndex(value: EnumStateName) {
|
|
419
|
+
if (this.isInit || this._selectedIndex != value) {
|
|
420
|
+
this.isInit = false;
|
|
421
|
+
|
|
422
|
+
// 切换激活态前先退出回收态预览 (按快照还原, 避免预览值残留/与新激活态混淆)
|
|
423
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
424
|
+
|
|
425
|
+
// 模型 Z: 录制中切 state → 自动 stopRecording, 把改动 commit 到 fromState 后再切.
|
|
426
|
+
// 标记 stopRecordingMode="auto" 让 StateSelectV2.onRecordingStop 走静默 + log 路径,
|
|
427
|
+
// 不弹"未跟随 prop"窗 (高频操作不打扰). stopRecording 后 _recording=false,
|
|
428
|
+
// 后续 StateWillChange / onStateWillChange 看到 !isRecording 自动跳过, 不重复 commit.
|
|
429
|
+
if (this._recording) {
|
|
430
|
+
this.stopRecordingMode = "auto";
|
|
431
|
+
try {
|
|
432
|
+
this.stopRecording();
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
this.stopRecordingMode = "manual";
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const originalValue = value;
|
|
440
|
+
// 🔧 边界检查:确保状态索引在有效范围内
|
|
441
|
+
value = Math.max(0, Math.min(this._states.length - 1, value));
|
|
442
|
+
|
|
443
|
+
if (originalValue !== value) {
|
|
444
|
+
StateErrorManager.warn("状态索引超出范围,已自动调整", {
|
|
445
|
+
component: "StateControllerV2",
|
|
446
|
+
method: "selectedIndex.setter",
|
|
447
|
+
params: { requestedIndex: originalValue, adjustedIndex: value, maxIndex: this._states.length - 1 },
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
StateErrorManager.debug("开始状态切换", {
|
|
452
|
+
component: "StateControllerV2",
|
|
453
|
+
method: "selectedIndex.setter",
|
|
454
|
+
params: { fromState: this._selectedIndex, toState: value, isInit: this.isInit },
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// 🔧 状态切换流程:标记正在变化 → 保存上一状态 → 更新当前状态 → 触发更新
|
|
458
|
+
this.isChanging = true;
|
|
459
|
+
|
|
460
|
+
// Wave 2: 切 state 前通知 (录制中需 commit diff 到 fromState)
|
|
461
|
+
this.updateState(EnumUpdateType.StateWillChange, this._selectedIndex);
|
|
462
|
+
// Wave 2 T25: capability 层广播 state 切换
|
|
463
|
+
// W6-2b: 留 propType / propRef 字段位 (state-change 事件本身不针对单 prop, 占位让下游 capability
|
|
464
|
+
// 读 ctx 不会 undefined 报错; per-prop 事件在 onPropertyControlled / onPropertyReleased 派发).
|
|
465
|
+
CapabilityRegistry.dispatch("onStateWillChange", {
|
|
466
|
+
ctrl: this, fromState: this._selectedIndex, toState: value, propType: undefined, propRef: undefined,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
this._previousIndex = this._selectedIndex;
|
|
470
|
+
this._selectedIndex = value;
|
|
471
|
+
|
|
472
|
+
// 🔧 通知所有相关组件状态已改变
|
|
473
|
+
this.updateState(EnumUpdateType.State);
|
|
474
|
+
// Wave 2 T25: capability 层广播 state 已切
|
|
475
|
+
// W6-2b: 同 onStateWillChange, 留 propType / propRef 字段位
|
|
476
|
+
CapabilityRegistry.dispatch("onStateChanged", {
|
|
477
|
+
ctrl: this, fromState: this._previousIndex, toState: value, propType: undefined, propRef: undefined,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// 🔧 编辑器环境下同步属性更新
|
|
481
|
+
if (CC_EDITOR) {
|
|
482
|
+
this.updateState(EnumUpdateType.Prop);
|
|
483
|
+
// 🔧 IMPL-002.1: 触发selectedPage变更通知
|
|
484
|
+
this._emitSelectedPageChanged();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
this.isChanging = false;
|
|
488
|
+
|
|
489
|
+
StateErrorManager.info("状态切换完成", {
|
|
490
|
+
component: "StateControllerV2",
|
|
491
|
+
method: "selectedIndex.setter",
|
|
492
|
+
params: { newState: value, stateName: this.selectedPage },
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** 状态操作折叠组 (上移/下移/复制/删除) — inspector 可折叠区域, 代理到本类同名访问器. */
|
|
498
|
+
@property({ type: CtrlStateOpsGroup, displayName: "状态操作", tooltip: "对当前选中状态的结构操作 (上移/下移/复制/删除)" })
|
|
499
|
+
public stateOps = new CtrlStateOpsGroup();
|
|
500
|
+
|
|
501
|
+
/** 回收站折叠组 (恢复/彻底删除已移除状态) — inspector 可折叠区域, 代理到本类回收站访问器. */
|
|
502
|
+
@property({ type: CtrlRecycleBinGroup, displayName: "回收站", tooltip: "已移除状态的暂存区 — 可恢复, 或彻底删除 (硬删数据, 不可恢复)" })
|
|
503
|
+
public recycleBin = new CtrlRecycleBinGroup();
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 一键刷新 inspector (对齐 StateSelectV2.refreshInspectorTrigger).
|
|
507
|
+
* 在 panel/外部改了状态后 inspector 偶尔不自动刷新时手动触发, 重建 state 枚举 + 强制 cocos refreshSelectedInspector.
|
|
508
|
+
*/
|
|
509
|
+
@property({ displayName: "🔄 刷新 inspector", tooltip: "手动刷新 inspector: 重建 state 枚举显示 + 强制 cocos refreshSelectedInspector" })
|
|
510
|
+
public get refreshInspectorTrigger(): boolean {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
public set refreshInspectorTrigger(_value: boolean) {
|
|
515
|
+
if (!CC_EDITOR) return;
|
|
516
|
+
this.refreshSelectedPage();
|
|
517
|
+
this.forceRefreshInspector();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** 🔧 调整当前选中状态的顺序 */
|
|
521
|
+
private adjustSelectedStateOrder(offset: number) {
|
|
522
|
+
if (!CC_EDITOR) {
|
|
523
|
+
StateErrorManager.error("仅在编辑器中调整状态顺序", {
|
|
524
|
+
component: "StateControllerV2",
|
|
525
|
+
method: "adjustSelectedStateOrder",
|
|
526
|
+
});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// TASK-002: 录制中不能调整状态顺序.
|
|
531
|
+
if (this._recording) {
|
|
532
|
+
StateErrorManager.warn("录制中不能调整状态顺序, 请先停止/撤销录制", {
|
|
533
|
+
component: "StateControllerV2",
|
|
534
|
+
method: "adjustSelectedStateOrder",
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!this._states || this._states.length === 0) {
|
|
540
|
+
StateErrorManager.warn("当前没有可调整的状态", {
|
|
541
|
+
component: "StateControllerV2",
|
|
542
|
+
method: "adjustSelectedStateOrder",
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const fromIndex = this._selectedIndex;
|
|
548
|
+
if (fromIndex < 0 || fromIndex >= this._states.length) {
|
|
549
|
+
StateErrorManager.warn("选中的状态索引无效,无法调整顺序", {
|
|
550
|
+
component: "StateControllerV2",
|
|
551
|
+
method: "adjustSelectedStateOrder",
|
|
552
|
+
params: { selectedIndex: fromIndex, stateCount: this._states.length },
|
|
553
|
+
});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const targetIndex = fromIndex + offset;
|
|
558
|
+
if (targetIndex < 0 || targetIndex >= this._states.length) {
|
|
559
|
+
StateErrorManager.warn("已到达边界,无法继续移动", {
|
|
560
|
+
component: "StateControllerV2",
|
|
561
|
+
method: "adjustSelectedStateOrder",
|
|
562
|
+
params: { fromIndex: fromIndex, targetIndex: targetIndex, stateCount: this._states.length },
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const newStates = [...this._states];
|
|
568
|
+
const [moved] = newStates.splice(fromIndex, 1);
|
|
569
|
+
newStates.splice(targetIndex, 0, moved);
|
|
570
|
+
|
|
571
|
+
// 🔧 同步历史命名记录的顺序,避免新增状态时名称错位
|
|
572
|
+
this.reorderHistoryNames(fromIndex, targetIndex);
|
|
573
|
+
|
|
574
|
+
// 先更新选中索引,再触发 setter 以同步 inspector
|
|
575
|
+
this._selectedIndex = targetIndex;
|
|
576
|
+
this.states = newStates;
|
|
577
|
+
|
|
578
|
+
// 🔧 通知 StateSelectV2 携带数据一起移动
|
|
579
|
+
this.updateState(EnumUpdateType.Move, { fromIndex: fromIndex, toIndex: targetIndex });
|
|
580
|
+
|
|
581
|
+
StateErrorManager.info("状态顺序已调整", {
|
|
582
|
+
component: "StateControllerV2",
|
|
583
|
+
method: "adjustSelectedStateOrder",
|
|
584
|
+
params: { fromIndex: fromIndex, toIndex: targetIndex, stateName: moved?.name },
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** 🔧 辅助:同步_historyStateName 顺序 */
|
|
589
|
+
private reorderHistoryNames(fromIndex: number, toIndex: number) {
|
|
590
|
+
if (!this._historyStateName) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const newHistory: { [key: number]: string } = {};
|
|
595
|
+
|
|
596
|
+
Object.keys(this._historyStateName).forEach((key) => {
|
|
597
|
+
const idx = parseInt(key, 10);
|
|
598
|
+
if (isNaN(idx)) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const name = this._historyStateName[idx];
|
|
603
|
+
|
|
604
|
+
if (idx === fromIndex) {
|
|
605
|
+
newHistory[toIndex] = name;
|
|
606
|
+
}
|
|
607
|
+
else if (fromIndex < toIndex && idx > fromIndex && idx <= toIndex) {
|
|
608
|
+
// 向下移动:中间元素整体上移一位
|
|
609
|
+
newHistory[idx - 1] = name;
|
|
610
|
+
}
|
|
611
|
+
else if (fromIndex > toIndex && idx >= toIndex && idx < fromIndex) {
|
|
612
|
+
// 向上移动:中间元素整体下移一位
|
|
613
|
+
newHistory[idx + 1] = name;
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
newHistory[idx] = name;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
this._historyStateName = newHistory;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** 🔧 复制当前选中的状态并插入到下一位 */
|
|
624
|
+
private copySelectedState() {
|
|
625
|
+
if (!CC_EDITOR) {
|
|
626
|
+
StateErrorManager.error("仅在编辑器中复制状态", {
|
|
627
|
+
component: "StateControllerV2",
|
|
628
|
+
method: "copySelectedState",
|
|
629
|
+
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// TASK-002: 录制中不能复制状态.
|
|
634
|
+
if (this._recording) {
|
|
635
|
+
StateErrorManager.warn("录制中不能复制状态, 请先停止/撤销录制", {
|
|
636
|
+
component: "StateControllerV2",
|
|
637
|
+
method: "copySelectedState",
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!this._states || this._states.length === 0) {
|
|
643
|
+
StateErrorManager.warn("当前没有可复制的状态", {
|
|
644
|
+
component: "StateControllerV2",
|
|
645
|
+
method: "copySelectedState",
|
|
646
|
+
});
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const index = this._selectedIndex;
|
|
651
|
+
if (index < 0 || index >= this._states.length) {
|
|
652
|
+
StateErrorManager.warn("选中的状态索引无效,无法复制", {
|
|
653
|
+
component: "StateControllerV2",
|
|
654
|
+
method: "copySelectedState",
|
|
655
|
+
params: { selectedIndex: index, stateCount: this._states.length },
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const origin = this._states[index];
|
|
661
|
+
const baseName = origin && origin.name ? origin.name : this.getSmartStateName(this._states.length);
|
|
662
|
+
const copyName = `${baseName}_copy`;
|
|
663
|
+
const newState = StateValue.create(copyName, this.stateIdAuto++);
|
|
664
|
+
|
|
665
|
+
const newStates = [...this._states];
|
|
666
|
+
const insertIndex = index + 1;
|
|
667
|
+
newStates.splice(insertIndex, 0, newState);
|
|
668
|
+
|
|
669
|
+
this._selectedIndex = insertIndex;
|
|
670
|
+
this.states = newStates;
|
|
671
|
+
// 先派发 Copy 让各 StateSelectV2 深拷贝 pageData, 再发 State 让所有 select apply 当前 state
|
|
672
|
+
this.updateState(EnumUpdateType.Copy, { fromIndex: index, toIndex: insertIndex });
|
|
673
|
+
this.updateState(EnumUpdateType.State);
|
|
674
|
+
|
|
675
|
+
StateErrorManager.info("已复制当前状态", {
|
|
676
|
+
component: "StateControllerV2",
|
|
677
|
+
method: "copySelectedState",
|
|
678
|
+
params: { fromIndex: index, insertIndex: insertIndex, originName: baseName, newName: copyName },
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** 🔧 删除当前选中的状态,至少保留一个 */
|
|
683
|
+
private removeSelectedState() {
|
|
684
|
+
if (!CC_EDITOR) {
|
|
685
|
+
StateErrorManager.error("仅在编辑器中删除状态", {
|
|
686
|
+
component: "StateControllerV2",
|
|
687
|
+
method: "removeSelectedState",
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// TASK-002: 录制中不能删除状态.
|
|
693
|
+
if (this._recording) {
|
|
694
|
+
StateErrorManager.warn("录制中不能删除状态, 请先停止/撤销录制", {
|
|
695
|
+
component: "StateControllerV2",
|
|
696
|
+
method: "removeSelectedState",
|
|
697
|
+
});
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!this._states || this._states.length === 0) {
|
|
702
|
+
StateErrorManager.warn("当前没有可删除的状态", {
|
|
703
|
+
component: "StateControllerV2",
|
|
704
|
+
method: "removeSelectedState",
|
|
705
|
+
});
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (this._states.length <= 1) {
|
|
710
|
+
StateErrorManager.warn("至少保留一个状态,已取消删除", {
|
|
711
|
+
component: "StateControllerV2",
|
|
712
|
+
method: "removeSelectedState",
|
|
713
|
+
params: { stateCount: this._states.length },
|
|
714
|
+
});
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const index = this._selectedIndex;
|
|
719
|
+
if (index < 0 || index >= this._states.length) {
|
|
720
|
+
StateErrorManager.warn("选中的状态索引无效,无法删除", {
|
|
721
|
+
component: "StateControllerV2",
|
|
722
|
+
method: "removeSelectedState",
|
|
723
|
+
params: { selectedIndex: index, stateCount: this._states.length },
|
|
724
|
+
});
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const removed = this._states[index];
|
|
729
|
+
const newStates = [...this._states];
|
|
730
|
+
newStates.splice(index, 1);
|
|
731
|
+
|
|
732
|
+
// 🔧 同步历史命名,保持索引与状态对齐
|
|
733
|
+
if (this._historyStateName) {
|
|
734
|
+
const newHistory: { [key: number]: string } = {};
|
|
735
|
+
Object.keys(this._historyStateName).forEach((key) => {
|
|
736
|
+
const oldIdx = parseInt(key, 10);
|
|
737
|
+
if (isNaN(oldIdx)) return;
|
|
738
|
+
if (oldIdx < index) {
|
|
739
|
+
newHistory[oldIdx] = this._historyStateName[oldIdx];
|
|
740
|
+
}
|
|
741
|
+
else if (oldIdx > index) {
|
|
742
|
+
newHistory[oldIdx - 1] = this._historyStateName[oldIdx];
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
this._historyStateName = newHistory;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// 🔧 预设新的选中索引,避免 setter 收到越界值
|
|
749
|
+
const newIndex = Math.min(index, newStates.length - 1);
|
|
750
|
+
this._selectedIndex = newIndex;
|
|
751
|
+
|
|
752
|
+
this.states = newStates;
|
|
753
|
+
|
|
754
|
+
StateErrorManager.info("已删除当前状态", {
|
|
755
|
+
component: "StateControllerV2",
|
|
756
|
+
method: "removeSelectedState",
|
|
757
|
+
params: {
|
|
758
|
+
removedIndex: index,
|
|
759
|
+
removedName: removed?.name,
|
|
760
|
+
newSelectedIndex: newIndex,
|
|
761
|
+
remainingCount: newStates.length,
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
767
|
+
protected __preload() {
|
|
768
|
+
StateControllerV2._register(this); // 支柱 B: 编辑器期也登记, 供 binding 解析 + 面板观测
|
|
769
|
+
if (!CC_EDITOR) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// 初始化 inspector 折叠组的 owner 回引 (facade 代理到本类访问器)
|
|
774
|
+
this.stateOps.owner = this;
|
|
775
|
+
this.recycleBin.owner = this;
|
|
776
|
+
|
|
777
|
+
StateErrorManager.debug("开始控制器预加载", {
|
|
778
|
+
component: "StateControllerV2",
|
|
779
|
+
method: "__preload",
|
|
780
|
+
params: { hasStates: !!this._states.length, ctrlName: this._ctrlName },
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
if (!this._states.length) {
|
|
784
|
+
// 🔧 从1开始命名状态
|
|
785
|
+
this._states = [StateValue.create("1", this.stateIdAuto++), StateValue.create("2", this.stateIdAuto++)];
|
|
786
|
+
StateErrorManager.info("创建默认状态", {
|
|
787
|
+
component: "StateControllerV2",
|
|
788
|
+
method: "__preload",
|
|
789
|
+
params: { defaultStates: ["1", "2"] },
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// 🔧 修复历史数据: 部分老 prefab 的 state 全是 stateId=0(未分配)或存在重复,
|
|
794
|
+
// 会导致"按 id 切换/联动定位"落到首个匹配 state(无法切换), 这里保证 stateId 唯一.
|
|
795
|
+
this.ensureUniqueStateIds();
|
|
796
|
+
this.refreshStateSnapshot();
|
|
797
|
+
|
|
798
|
+
const array = this.states.map((val, i) => {
|
|
799
|
+
return { name: val.name, value: i };
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// @ts-expect-error 允许使用该方法
|
|
803
|
+
cc.Class.Attr.setClassAttr(this, "selectedIndex", "enumList", array);
|
|
804
|
+
|
|
805
|
+
// 回收站下拉初始 enumList 注入 (反序列化后 _deletedStates 可能已有暂存项)
|
|
806
|
+
this.refreshRecycleBinEnums();
|
|
807
|
+
|
|
808
|
+
// 🔧 确保selectedIndex在有效范围内,默认选择第一个状态
|
|
809
|
+
if (this._states.length > 0 && (this._selectedIndex < 0 || this._selectedIndex >= this._states.length)) {
|
|
810
|
+
this._selectedIndex = 0;
|
|
811
|
+
StateErrorManager.info("初始化时自动设置selectedIndex为第一个状态", {
|
|
812
|
+
component: "StateControllerV2",
|
|
813
|
+
method: "__preload",
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (!this._ctrlName) {
|
|
818
|
+
this.ctrlName = `ctrl_${Date.now().toString()}`;
|
|
819
|
+
StateErrorManager.debug("生成默认控制器名称", {
|
|
820
|
+
component: "StateControllerV2",
|
|
821
|
+
method: "__preload",
|
|
822
|
+
params: { generatedName: this._ctrlName },
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
StateErrorManager.info("控制器预加载完成", {
|
|
827
|
+
component: "StateControllerV2",
|
|
828
|
+
method: "__preload",
|
|
829
|
+
params: { ctrlId: this.ctrlId, ctrlName: this._ctrlName, stateCount: this._states.length },
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// Wave 2 T16: 兜底 commit - 场景切换时自动 stopRecording
|
|
833
|
+
// 即使用户没主动停录, 切场景前也会 commit 当前 state 的 diff, 避免数据丢失
|
|
834
|
+
if (cc.director && typeof cc.director.on === "function") {
|
|
835
|
+
cc.director.on(cc.Director.EVENT_BEFORE_SCENE_LAUNCH, this._onSceneBeforeLaunch, this);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
this.updateState(EnumUpdateType.Init);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* 保证每个 state 的 stateId 唯一(修复历史数据)。
|
|
843
|
+
* 重复 / 非法(undefined/非数字)的 stateId 会重新分配自增 id, 并把 stateIdAuto seed 到最大 id 之后,
|
|
844
|
+
* 避免后续新增 state 再次撞 id。首个出现的合法 id 保留(含 0), 故对已正常的数据是幂等空操作。
|
|
845
|
+
*/
|
|
846
|
+
private ensureUniqueStateIds(): void {
|
|
847
|
+
const states = this._states || [];
|
|
848
|
+
let maxId = -1;
|
|
849
|
+
for (const s of states) {
|
|
850
|
+
if (s && typeof s.stateId === "number" && s.stateId > maxId) {
|
|
851
|
+
maxId = s.stateId;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// stateIdAuto 必须领先于现存最大 id, 否则自增分配会与既有 id 撞车
|
|
855
|
+
if (this.stateIdAuto <= maxId) {
|
|
856
|
+
this.stateIdAuto = maxId + 1;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const seen: { [id: number]: boolean } = {};
|
|
860
|
+
let repaired = 0;
|
|
861
|
+
for (const s of states) {
|
|
862
|
+
if (!s) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const invalid = typeof s.stateId !== "number" || seen[s.stateId];
|
|
866
|
+
if (invalid) {
|
|
867
|
+
s.stateId = this.stateIdAuto++;
|
|
868
|
+
repaired++;
|
|
869
|
+
}
|
|
870
|
+
seen[s.stateId] = true;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (repaired > 0) {
|
|
874
|
+
StateErrorManager.info("修复重复/未分配的 stateId", {
|
|
875
|
+
component: "StateControllerV2",
|
|
876
|
+
method: "ensureUniqueStateIds",
|
|
877
|
+
params: { ctrlName: this._ctrlName, repairedCount: repaired, stateCount: states.length },
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private cloneStateSnapshot(states: StateValue[]): StateValue[] {
|
|
883
|
+
const snapshot: StateValue[] = [];
|
|
884
|
+
for (const state of states || []) {
|
|
885
|
+
if (!state) continue;
|
|
886
|
+
snapshot.push(StateValue.create(state.name, state.stateId));
|
|
887
|
+
}
|
|
888
|
+
return snapshot;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private refreshStateSnapshot(): void {
|
|
892
|
+
this._stateSnapshot = this.cloneStateSnapshot(this._states);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private stashDeletedStates(oldStates: StateValue[], deletedIndices: number[], activeStates: StateValue[]): void {
|
|
896
|
+
if (!deletedIndices.length) return;
|
|
897
|
+
if (!this._deletedStates) this._deletedStates = [];
|
|
898
|
+
const activeIds = new Set<number>();
|
|
899
|
+
for (const state of activeStates || []) {
|
|
900
|
+
if (state && typeof state.stateId === "number") {
|
|
901
|
+
activeIds.add(state.stateId);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const stashedIds = new Set<number>();
|
|
905
|
+
for (const state of this._deletedStates) {
|
|
906
|
+
if (state && typeof state.stateId === "number") {
|
|
907
|
+
stashedIds.add(state.stateId);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
for (const index of deletedIndices) {
|
|
911
|
+
const state = oldStates[index];
|
|
912
|
+
if (!state || typeof state.stateId !== "number") continue;
|
|
913
|
+
if (activeIds.has(state.stateId) || stashedIds.has(state.stateId)) continue;
|
|
914
|
+
this._deletedStates.push(StateValue.create(state.name, state.stateId));
|
|
915
|
+
stashedIds.add(state.stateId);
|
|
916
|
+
}
|
|
917
|
+
this.refreshRecycleBinEnums();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** 回收站: 列出所有暂存的已删除 state (不可变副本, panel 渲染回收站列表用). */
|
|
921
|
+
public listDeletedStates(): { name: string, stateId: number }[] {
|
|
922
|
+
const out: { name: string, stateId: number }[] = [];
|
|
923
|
+
for (const s of this._deletedStates || []) {
|
|
924
|
+
if (s && typeof s.stateId === "number") out.push({ name: s.name, stateId: s.stateId });
|
|
925
|
+
}
|
|
926
|
+
return out;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
public restoreLastDeletedState(): boolean {
|
|
930
|
+
if (!this._deletedStates || this._deletedStates.length === 0) {
|
|
931
|
+
StateErrorManager.warn("没有可恢复的 state", {
|
|
932
|
+
component: "StateControllerV2",
|
|
933
|
+
method: "restoreLastDeletedState",
|
|
934
|
+
});
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
const last = this._deletedStates[this._deletedStates.length - 1];
|
|
938
|
+
return this.restoreDeletedState(last.stateId);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* 回收站: 恢复指定 stateId 的暂存 state, 追加到尾部并选中.
|
|
943
|
+
* 具体属性数据自动接回 —— _ctrlData 以 stateId 寻址, 软删时从未清理过该页数据。
|
|
944
|
+
*/
|
|
945
|
+
public restoreDeletedState(stateId: number): boolean {
|
|
946
|
+
if (!CC_EDITOR) {
|
|
947
|
+
StateErrorManager.error("仅在编辑器中恢复状态", {
|
|
948
|
+
component: "StateControllerV2",
|
|
949
|
+
method: "restoreDeletedState",
|
|
950
|
+
});
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
if (!this._deletedStates || this._deletedStates.length === 0) return false;
|
|
954
|
+
const i = this._deletedStates.findIndex(s => s && s.stateId === stateId);
|
|
955
|
+
if (i < 0) {
|
|
956
|
+
StateErrorManager.warn("回收站中找不到该 state, 无法恢复", {
|
|
957
|
+
component: "StateControllerV2",
|
|
958
|
+
method: "restoreDeletedState",
|
|
959
|
+
params: { stateId },
|
|
960
|
+
});
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
// 恢复前退出预览: 恢复后会以激活态重新 apply, 不需要预览叠加
|
|
964
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
965
|
+
const restored = this._deletedStates.splice(i, 1)[0];
|
|
966
|
+
if (!restored) return false;
|
|
967
|
+
this.states = [...this._states, StateValue.create(restored.name, restored.stateId)];
|
|
968
|
+
this.selectedIndex = this._states.length - 1;
|
|
969
|
+
this.refreshRecycleBinEnums();
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* 回收站硬删: 把指定 stateId 从回收站移除, 并广播 PurgeStateId 让所有受控
|
|
975
|
+
* StateSelectV2 清掉 _ctrlData[stateId] 的页数据 —— 不可恢复。
|
|
976
|
+
*/
|
|
977
|
+
public purgeDeletedState(stateId: number): boolean {
|
|
978
|
+
if (!CC_EDITOR) {
|
|
979
|
+
StateErrorManager.error("仅在编辑器中彻底删除状态", {
|
|
980
|
+
component: "StateControllerV2",
|
|
981
|
+
method: "purgeDeletedState",
|
|
982
|
+
});
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
if (!this._deletedStates || this._deletedStates.length === 0) return false;
|
|
986
|
+
const i = this._deletedStates.findIndex(s => s && s.stateId === stateId);
|
|
987
|
+
if (i < 0) {
|
|
988
|
+
StateErrorManager.warn("回收站中找不到该 state, 无法彻底删除", {
|
|
989
|
+
component: "StateControllerV2",
|
|
990
|
+
method: "purgeDeletedState",
|
|
991
|
+
params: { stateId },
|
|
992
|
+
});
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
// 硬删前退出预览 (尤其当正预览的就是它 — 数据即将被清, 先按快照还原节点)
|
|
996
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
997
|
+
this._deletedStates.splice(i, 1);
|
|
998
|
+
// 数据实体散在各受控 StateSelectV2 上, 广播让它们各自 delete pageData[stateId]
|
|
999
|
+
this.updateState(EnumUpdateType.PurgeStateId, stateId);
|
|
1000
|
+
this.refreshRecycleBinEnums();
|
|
1001
|
+
StateErrorManager.info("已彻底删除 state 数据 (不可恢复)", {
|
|
1002
|
+
component: "StateControllerV2",
|
|
1003
|
+
method: "purgeDeletedState",
|
|
1004
|
+
params: { stateId },
|
|
1005
|
+
});
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/** 回收站: 清空 —— 对所有暂存项执行硬删. */
|
|
1010
|
+
public purgeAllDeletedStates(): boolean {
|
|
1011
|
+
if (!CC_EDITOR) {
|
|
1012
|
+
StateErrorManager.error("仅在编辑器中清空回收站", {
|
|
1013
|
+
component: "StateControllerV2",
|
|
1014
|
+
method: "purgeAllDeletedStates",
|
|
1015
|
+
});
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
if (!this._deletedStates || this._deletedStates.length === 0) return false;
|
|
1019
|
+
// 清空前退出预览 (数据即将被清, 先按快照还原节点)
|
|
1020
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
1021
|
+
const ids: number[] = [];
|
|
1022
|
+
for (const s of this._deletedStates) {
|
|
1023
|
+
if (s && typeof s.stateId === "number") ids.push(s.stateId);
|
|
1024
|
+
}
|
|
1025
|
+
this._deletedStates = [];
|
|
1026
|
+
for (const id of ids) {
|
|
1027
|
+
this.updateState(EnumUpdateType.PurgeStateId, id);
|
|
1028
|
+
}
|
|
1029
|
+
this.refreshRecycleBinEnums();
|
|
1030
|
+
StateErrorManager.info("已清空回收站 (不可恢复)", {
|
|
1031
|
+
component: "StateControllerV2",
|
|
1032
|
+
method: "purgeAllDeletedStates",
|
|
1033
|
+
params: { count: ids.length },
|
|
1034
|
+
});
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// ===== 回收站 inspector 折叠组 (CtrlRecycleBinGroup) 代理 =====
|
|
1039
|
+
|
|
1040
|
+
/** 回收站只读展示: ["name (id N)", ...]. */
|
|
1041
|
+
public getDeletedStatesDisplay(): string[] {
|
|
1042
|
+
return (this._deletedStates || []).map(s => `${s.name} (id ${s.stateId})`);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* 注入回收站两个下拉 (restoreTarget / purgeTarget) 的 enumList 到 CtrlRecycleBinGroup 类上,
|
|
1047
|
+
* 并刷新 value→stateId 反查表. 在回收站内容变化处调 (stash / restore / purge / __preload)。
|
|
1048
|
+
* 注入到类而非实例: 编辑器读嵌套 facade 枚举走类 __attrs__ (同 SelectExcludeGroup.addExcludeTrigger)。
|
|
1049
|
+
*/
|
|
1050
|
+
public refreshRecycleBinEnums(): void {
|
|
1051
|
+
if (!CC_EDITOR) return;
|
|
1052
|
+
const items = this._deletedStates || [];
|
|
1053
|
+
this._recycleBinOptionIds = items.map(s => s.stateId);
|
|
1054
|
+
const options = items.map((s, i) => ({ name: `${s.name} (id ${s.stateId})`, value: i + 1 }));
|
|
1055
|
+
const restoreEnum = [
|
|
1056
|
+
{ name: items.length ? "(选择要恢复的状态…)" : "(回收站为空)", value: 0 },
|
|
1057
|
+
...options,
|
|
1058
|
+
];
|
|
1059
|
+
const purgeEnum = [
|
|
1060
|
+
{ name: items.length ? "(选择要彻底删除的状态…)" : "(回收站为空)", value: 0 },
|
|
1061
|
+
...options,
|
|
1062
|
+
];
|
|
1063
|
+
const previewEnum = [
|
|
1064
|
+
{ name: items.length ? "(选择要预览的状态…)" : "(回收站为空)", value: 0 },
|
|
1065
|
+
...options,
|
|
1066
|
+
];
|
|
1067
|
+
// @ts-expect-error setClassAttr 在 cocos 2.x d.ts 中未声明
|
|
1068
|
+
cc.Class.Attr.setClassAttr(CtrlRecycleBinGroup, "restoreTarget", "enumList", restoreEnum);
|
|
1069
|
+
// @ts-expect-error setClassAttr 在 cocos 2.x d.ts 中未声明
|
|
1070
|
+
cc.Class.Attr.setClassAttr(CtrlRecycleBinGroup, "purgeTarget", "enumList", purgeEnum);
|
|
1071
|
+
// @ts-expect-error setClassAttr 在 cocos 2.x d.ts 中未声明
|
|
1072
|
+
cc.Class.Attr.setClassAttr(CtrlRecycleBinGroup, "previewTarget", "enumList", previewEnum);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** 把下拉 value 反查成 stateId (value>=1, 选项 index=value-1). */
|
|
1076
|
+
private _recycleStateIdOfOption(value: number): number {
|
|
1077
|
+
if (typeof value !== "number" || value <= 0) return -1;
|
|
1078
|
+
const stateId = this._recycleBinOptionIds[value - 1];
|
|
1079
|
+
return typeof stateId === "number" ? stateId : -1;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** 回收站下拉「↩ 恢复状态」: 选中即恢复. */
|
|
1083
|
+
public recycleRestorePick(value: number): void {
|
|
1084
|
+
if (!CC_EDITOR) return;
|
|
1085
|
+
const stateId = this._recycleStateIdOfOption(value);
|
|
1086
|
+
if (stateId < 0) return;
|
|
1087
|
+
this.restoreDeletedState(stateId);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** 回收站下拉「🗑 彻底删除」: 选中 → 弹窗确认 → 硬删 (不可恢复). */
|
|
1091
|
+
public recyclePurgePick(value: number): void {
|
|
1092
|
+
if (!CC_EDITOR) return;
|
|
1093
|
+
const stateId = this._recycleStateIdOfOption(value);
|
|
1094
|
+
if (stateId < 0) return;
|
|
1095
|
+
const item = (this._deletedStates || []).find(s => s.stateId === stateId);
|
|
1096
|
+
const label = item ? `${item.name} (id ${stateId})` : `id ${stateId}`;
|
|
1097
|
+
this.showDialog({
|
|
1098
|
+
type: "warning",
|
|
1099
|
+
title: "彻底删除状态数据",
|
|
1100
|
+
message: `将彻底删除「${label}」的数据, 不可恢复。确定?`,
|
|
1101
|
+
buttons: ["彻底删除", "取消"],
|
|
1102
|
+
defaultId: 1,
|
|
1103
|
+
cancelId: 1,
|
|
1104
|
+
}, (idx) => {
|
|
1105
|
+
if (idx === 0) this.purgeDeletedState(stateId);
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** 回收站「清空回收站」: 弹窗确认 → 全部硬删 (不可恢复). */
|
|
1110
|
+
public recyclePurgeAll(): void {
|
|
1111
|
+
if (!CC_EDITOR) return;
|
|
1112
|
+
const n = (this._deletedStates || []).length;
|
|
1113
|
+
if (!n) return;
|
|
1114
|
+
this.showDialog({
|
|
1115
|
+
type: "warning",
|
|
1116
|
+
title: "清空回收站",
|
|
1117
|
+
message: `将彻底删除回收站内全部 ${n} 个状态的数据, 不可恢复。确定?`,
|
|
1118
|
+
buttons: ["清空回收站", "取消"],
|
|
1119
|
+
defaultId: 1,
|
|
1120
|
+
cancelId: 1,
|
|
1121
|
+
}, (idx) => {
|
|
1122
|
+
if (idx === 0) this.purgeAllDeletedStates();
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/** 回收站下拉「👁 预览」: 选中即进入只读预览 (inspector 折叠组代理). */
|
|
1127
|
+
public recyclePreviewPick(value: number): void {
|
|
1128
|
+
if (!CC_EDITOR) return;
|
|
1129
|
+
const stateId = this._recycleStateIdOfOption(value);
|
|
1130
|
+
if (stateId < 0) return;
|
|
1131
|
+
this.previewDeletedState(stateId);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** 回收站「退出预览」按钮 (inspector 折叠组代理). */
|
|
1135
|
+
public recycleExitPreview(): void {
|
|
1136
|
+
if (!CC_EDITOR) return;
|
|
1137
|
+
this.exitPreview();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// ===== 回收站只读预览 =====
|
|
1141
|
+
|
|
1142
|
+
/** 是否正在预览某个回收态. */
|
|
1143
|
+
public get isPreviewing(): boolean {
|
|
1144
|
+
return this._previewingStateId >= 0;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** 当前预览的 stateId (-1 = 未预览). */
|
|
1148
|
+
public get previewingStateId(): number {
|
|
1149
|
+
return this._previewingStateId;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* 进入某个回收态的只读预览: 把该 stateId 的数据叠加到受控节点 (不改 selectedIndex)。
|
|
1154
|
+
* 单实例: 预览另一个前先退出当前预览。仅回收站内的 stateId 可预览; 录制中先停预览不允许进入。
|
|
1155
|
+
*/
|
|
1156
|
+
public previewDeletedState(stateId: number): boolean {
|
|
1157
|
+
if (!CC_EDITOR) {
|
|
1158
|
+
StateErrorManager.error("仅在编辑器中预览状态", {
|
|
1159
|
+
component: "StateControllerV2",
|
|
1160
|
+
method: "previewDeletedState",
|
|
1161
|
+
});
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
// 录制中不预览 (避免把预览值 commit 进激活态)
|
|
1165
|
+
if (this._recording) {
|
|
1166
|
+
StateErrorManager.warn("录制中不能预览回收态, 请先停止录制", {
|
|
1167
|
+
component: "StateControllerV2",
|
|
1168
|
+
method: "previewDeletedState",
|
|
1169
|
+
});
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
const exists = (this._deletedStates || []).some(s => s && s.stateId === stateId);
|
|
1173
|
+
if (!exists) {
|
|
1174
|
+
StateErrorManager.warn("回收站中找不到该 state, 无法预览", {
|
|
1175
|
+
component: "StateControllerV2",
|
|
1176
|
+
method: "previewDeletedState",
|
|
1177
|
+
params: { stateId },
|
|
1178
|
+
});
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
// 已在预览同一个 → no-op; 预览别的 → 先退出当前 (按快照还原) 再进
|
|
1182
|
+
if (this._previewingStateId === stateId) return true;
|
|
1183
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
1184
|
+
this._previewingStateId = stateId;
|
|
1185
|
+
this.updateState(EnumUpdateType.PreviewEnter, stateId);
|
|
1186
|
+
StateErrorManager.info("进入回收态只读预览", {
|
|
1187
|
+
component: "StateControllerV2",
|
|
1188
|
+
method: "previewDeletedState",
|
|
1189
|
+
params: { stateId },
|
|
1190
|
+
});
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/** 退出预览: 通知所有受控 StateSelectV2 按快照精确还原节点. 幂等, 未预览时 no-op. */
|
|
1195
|
+
public exitPreview(): boolean {
|
|
1196
|
+
if (this._previewingStateId < 0) return false;
|
|
1197
|
+
const wasPreviewing = this._previewingStateId;
|
|
1198
|
+
this._previewingStateId = -1;
|
|
1199
|
+
this.updateState(EnumUpdateType.PreviewExit);
|
|
1200
|
+
StateErrorManager.info("退出回收态预览", {
|
|
1201
|
+
component: "StateControllerV2",
|
|
1202
|
+
method: "exitPreview",
|
|
1203
|
+
params: { stateId: wasPreviewing },
|
|
1204
|
+
});
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/** Wave 2 T16: 场景切换前的兜底 commit hook. */
|
|
1209
|
+
private _onSceneBeforeLaunch(): void {
|
|
1210
|
+
// 场景切换前退出预览, 按快照还原节点 (预览态不应被序列化进场景)
|
|
1211
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
1212
|
+
if (this._recording) {
|
|
1213
|
+
StateErrorManager.info("场景切换前自动停止录制", {
|
|
1214
|
+
component: "StateControllerV2",
|
|
1215
|
+
method: "_onSceneBeforeLaunch",
|
|
1216
|
+
params: { ctrlName: this._ctrlName },
|
|
1217
|
+
});
|
|
1218
|
+
this.stopRecording();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
protected onLoad() {
|
|
1223
|
+
StateControllerV2._register(this); // 支柱 B: 运行时也登记 (start() rehydrate 需按 id 解析目标)
|
|
1224
|
+
if (!CC_EDITOR) {
|
|
1225
|
+
// Wave 3: runtime 启动 capability hook (HomePage 等用此跳到指定 state)
|
|
1226
|
+
CapabilityRegistry.dispatch("onRuntimeInit", { ctrl: this });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
this.updateState(EnumUpdateType.State);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* 支柱 B: 运行时把序列化 binding 解析并接线. 放在 start() 而非 onLoad —
|
|
1234
|
+
* Cocos 保证所有组件 onLoad 先于任意 start, 故此刻全场景控制器都已登记进 _byId,
|
|
1235
|
+
* 跨控制器目标按 id 解析不受加载顺序影响.
|
|
1236
|
+
*/
|
|
1237
|
+
protected start() {
|
|
1238
|
+
if (CC_EDITOR) return;
|
|
1239
|
+
this.rehydrateBindings();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
protected onDestroy() {
|
|
1243
|
+
StateControllerV2._unregister(this); // 支柱 B
|
|
1244
|
+
if (!CC_EDITOR) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
// 销毁前退出预览, 按快照还原节点
|
|
1248
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
1249
|
+
// Wave 2 T16: onDestroy 兜底 - 若仍在录制, stopRecording 触发 final commit
|
|
1250
|
+
if (this._recording) {
|
|
1251
|
+
StateErrorManager.info("控制器销毁前自动停止录制 (commit final diff)", {
|
|
1252
|
+
component: "StateControllerV2",
|
|
1253
|
+
method: "onDestroy",
|
|
1254
|
+
});
|
|
1255
|
+
this.stopRecording();
|
|
1256
|
+
}
|
|
1257
|
+
if (cc.director && typeof cc.director.off === "function") {
|
|
1258
|
+
cc.director.off(cc.Director.EVENT_BEFORE_SCENE_LAUNCH, this._onSceneBeforeLaunch, this);
|
|
1259
|
+
}
|
|
1260
|
+
this.updateState(EnumUpdateType.Delete);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// ===== 支柱 B: 可序列化跨控制器联动 API =====
|
|
1264
|
+
|
|
1265
|
+
/** 读出序列化的联动声明 (容错: 坏数据返回空). */
|
|
1266
|
+
public getBindings(): { sourceStateId: number, targetCtrlId: number, targetStateId: number }[] {
|
|
1267
|
+
if (!this._bindingsData) return [];
|
|
1268
|
+
try {
|
|
1269
|
+
const arr = JSON.parse(this._bindingsData);
|
|
1270
|
+
if (!Array.isArray(arr)) return [];
|
|
1271
|
+
return arr
|
|
1272
|
+
.filter(b => b && typeof b.sourceStateId === "number" && typeof b.targetCtrlId === "number" && typeof b.targetStateId === "number")
|
|
1273
|
+
.map(b => ({ sourceStateId: b.sourceStateId, targetCtrlId: b.targetCtrlId, targetStateId: b.targetStateId }));
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
return [];
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
private _saveBindings(list: { sourceStateId: number, targetCtrlId: number, targetStateId: number }[]): void {
|
|
1281
|
+
this._bindingsData = JSON.stringify(list);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** 新增/更新一条联动: 本控制器切到 sourceStateId → 目标控制器(targetCtrlId)切到 targetStateId. 同 (源态,目标) 覆盖. */
|
|
1285
|
+
public addBinding(sourceStateId: number, targetCtrlId: number, targetStateId: number): boolean {
|
|
1286
|
+
if (typeof sourceStateId !== "number" || typeof targetCtrlId !== "number" || typeof targetStateId !== "number") return false;
|
|
1287
|
+
const list = this.getBindings();
|
|
1288
|
+
const i = list.findIndex(b => b.sourceStateId === sourceStateId && b.targetCtrlId === targetCtrlId);
|
|
1289
|
+
if (i >= 0) list[i].targetStateId = targetStateId;
|
|
1290
|
+
else list.push({ sourceStateId, targetCtrlId, targetStateId });
|
|
1291
|
+
this._saveBindings(list);
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** 删除一条联动. */
|
|
1296
|
+
public removeBinding(sourceStateId: number, targetCtrlId: number): boolean {
|
|
1297
|
+
const list = this.getBindings();
|
|
1298
|
+
const i = list.findIndex(b => b.sourceStateId === sourceStateId && b.targetCtrlId === targetCtrlId);
|
|
1299
|
+
if (i < 0) return false;
|
|
1300
|
+
list.splice(i, 1);
|
|
1301
|
+
this._saveBindings(list);
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** 清空本控制器全部联动 (序列化 + 运行时接线). */
|
|
1306
|
+
public clearBindings(): void {
|
|
1307
|
+
this._bindingsData = "";
|
|
1308
|
+
MultiCtrlBindingCapability.clearAllBindings(this);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* 把序列化 binding 解析成运行时接线 (复用 MultiCtrlBindingCapability). 幂等: 先清后接, 重复调用不叠加.
|
|
1313
|
+
* 目标 ctrlId 未登记 (未加载) 时跳过该条, 不抛.
|
|
1314
|
+
*/
|
|
1315
|
+
public rehydrateBindings(): void {
|
|
1316
|
+
MultiCtrlBindingCapability.clearAllBindings(this);
|
|
1317
|
+
const list = this.getBindings();
|
|
1318
|
+
for (let i = 0; i < list.length; i++) {
|
|
1319
|
+
const b = list[i];
|
|
1320
|
+
const target = StateControllerV2.getById(b.targetCtrlId);
|
|
1321
|
+
if (target) MultiCtrlBindingCapability.addBinding(this, b.sourceStateId, target, b.targetStateId);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/** 选择的状态名字 */
|
|
1326
|
+
public get selectedPage(): string {
|
|
1327
|
+
// 🔧 IMPL-002.4: 添加调试日志
|
|
1328
|
+
StateErrorManager.debug("获取selectedPage", {
|
|
1329
|
+
component: "StateControllerV2",
|
|
1330
|
+
method: "selectedPage.getter",
|
|
1331
|
+
params: { selectedIndex: this._selectedIndex, statesCount: this._states.length },
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
if (this._selectedIndex == -1 || this._selectedIndex >= this._states.length)
|
|
1335
|
+
return null;
|
|
1336
|
+
else {
|
|
1337
|
+
const currentState = this._states[this._selectedIndex];
|
|
1338
|
+
|
|
1339
|
+
// 🔧 确保状态对象有效且name不为空
|
|
1340
|
+
if (currentState && currentState.name !== undefined && currentState.name !== "") {
|
|
1341
|
+
return currentState.name;
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
StateErrorManager.warn("当前状态对象无效或名称为空", {
|
|
1345
|
+
component: "StateControllerV2",
|
|
1346
|
+
method: "selectedPage.getter",
|
|
1347
|
+
params: { currentState: currentState, selectedIndex: this._selectedIndex },
|
|
1348
|
+
});
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// ================== 🔧 IMPL-002: selectedPage修复 ==================
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* 触发 selectedPage 变更通知 (仅日志, 不再自动刷新 inspector).
|
|
1358
|
+
*
|
|
1359
|
+
* 历史: 这里曾在切状态后 setTimeout 强刷 inspector, 体验上会打断当前操作
|
|
1360
|
+
* (焦点丢失 / 滚动跳动). 现在去掉, selectedPage 的陈旧显示由 panel 主动接管.
|
|
1361
|
+
*/
|
|
1362
|
+
private _emitSelectedPageChanged(): void {
|
|
1363
|
+
if (!CC_EDITOR) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
StateErrorManager.debug("selectedPage 变更", {
|
|
1367
|
+
component: "StateControllerV2",
|
|
1368
|
+
method: "_emitSelectedPageChanged",
|
|
1369
|
+
params: { selectedPage: this.selectedPage, selectedIndex: this._selectedIndex },
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* 🔧 IMPL-002.3: 公共方法 - 手动刷新selectedPage显示
|
|
1375
|
+
* 供外部在需要时手动调用
|
|
1376
|
+
*/
|
|
1377
|
+
public refreshSelectedPage(): void {
|
|
1378
|
+
if (!CC_EDITOR) {
|
|
1379
|
+
StateErrorManager.warn("refreshSelectedPage仅在编辑器环境可用", {
|
|
1380
|
+
component: "StateControllerV2",
|
|
1381
|
+
method: "refreshSelectedPage",
|
|
1382
|
+
});
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
StateErrorManager.info("手动刷新selectedPage", {
|
|
1387
|
+
component: "StateControllerV2",
|
|
1388
|
+
method: "refreshSelectedPage",
|
|
1389
|
+
params: { selectedPage: this.selectedPage, selectedIndex: this._selectedIndex },
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
this._emitSelectedPageChanged();
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
public set selectedPage(val: string) {
|
|
1396
|
+
for (let index = 0, len = this._states.length; index < len; index++) {
|
|
1397
|
+
if (this._states[index].name == val) {
|
|
1398
|
+
this.selectedIndex = index;
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** 找到所有被删除的状态索引 */
|
|
1405
|
+
private findDeletedIndices(oldStates: StateValue[], newStates: StateValue[]): number[] {
|
|
1406
|
+
StateErrorManager.debug("开始检测删除的状态", {
|
|
1407
|
+
component: "StateControllerV2",
|
|
1408
|
+
method: "findDeletedIndices",
|
|
1409
|
+
params: { oldCount: oldStates.length, newCount: newStates.length },
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
const deletedIndices: number[] = [];
|
|
1413
|
+
|
|
1414
|
+
const newStateIds = new Set<number>();
|
|
1415
|
+
|
|
1416
|
+
for (let i = 0; i < newStates.length; i++) {
|
|
1417
|
+
if (newStates[i] && newStates[i].stateId !== undefined) {
|
|
1418
|
+
newStateIds.add(newStates[i].stateId);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
for (let i = 0; i < oldStates.length; i++) {
|
|
1423
|
+
const oldState = oldStates[i];
|
|
1424
|
+
|
|
1425
|
+
if (!oldState || oldState.stateId === undefined) {
|
|
1426
|
+
StateErrorManager.warn("发现无效的旧状态对象", {
|
|
1427
|
+
component: "StateControllerV2",
|
|
1428
|
+
method: "findDeletedIndices",
|
|
1429
|
+
params: { stateIndex: i },
|
|
1430
|
+
});
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (!newStateIds.has(oldState.stateId)) {
|
|
1435
|
+
deletedIndices.push(i);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (deletedIndices.length > 0) {
|
|
1440
|
+
StateErrorManager.info("检测到删除的状态", {
|
|
1441
|
+
component: "StateControllerV2",
|
|
1442
|
+
method: "findDeletedIndices",
|
|
1443
|
+
params: { deletedIndices: deletedIndices, deletedCount: deletedIndices.length },
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
return deletedIndices;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/** 🔧 辅助方法:生成智能状态名字,优先使用历史记录 */
|
|
1451
|
+
private getSmartStateName(index: number): string {
|
|
1452
|
+
// 检查历史记录中是否有该索引的自定义名字
|
|
1453
|
+
if (this._historyStateName && this._historyStateName[index]) {
|
|
1454
|
+
return this._historyStateName[index];
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// 默认从1开始命名
|
|
1458
|
+
const defaultName = (index + 1).toString();
|
|
1459
|
+
StateErrorManager.debug("使用默认状态名字", {
|
|
1460
|
+
component: "StateControllerV2",
|
|
1461
|
+
method: "getSmartStateName",
|
|
1462
|
+
params: { index: index, defaultName: defaultName },
|
|
1463
|
+
});
|
|
1464
|
+
return defaultName;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// ================== 🔧 IMPL-001: BFS缓存优化方法 ==================
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* 🔧 重建StateSelectV2缓存
|
|
1471
|
+
* 使用getComponentsInChildren一次性获取所有StateSelectV2,然后过滤出直接控制的组件
|
|
1472
|
+
*/
|
|
1473
|
+
private rebuildStateSelectCache(): void {
|
|
1474
|
+
if (!this._cacheDirty && this._stateSelectCache !== null) {
|
|
1475
|
+
return; // 缓存有效,无需重建
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
StateErrorManager.debug("开始重建StateSelectV2缓存", {
|
|
1479
|
+
component: "StateControllerV2",
|
|
1480
|
+
method: "rebuildStateSelectCache",
|
|
1481
|
+
params: { ctrlName: this._ctrlName },
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
const allStateSelects = this.node.getComponentsInChildren(StateSelectV2);
|
|
1485
|
+
this._stateSelectCache = allStateSelects.filter(ss => this.isDirectlyControlled(ss.node));
|
|
1486
|
+
|
|
1487
|
+
this._cacheDirty = false;
|
|
1488
|
+
|
|
1489
|
+
StateErrorManager.info("StateSelectV2缓存重建完成", {
|
|
1490
|
+
component: "StateControllerV2",
|
|
1491
|
+
method: "rebuildStateSelectCache",
|
|
1492
|
+
params: { cachedCount: this._stateSelectCache.length },
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* 🔧 检查节点是否被当前控制器直接控制
|
|
1498
|
+
* 直接控制 = 节点与控制器之间没有其他StateControllerV2
|
|
1499
|
+
*/
|
|
1500
|
+
private isDirectlyControlled(targetNode: cc.Node): boolean {
|
|
1501
|
+
// #T1: targetNode 自身带其它 StateControllerV2 → 归它(及其子树)管, 不是本(祖先)控制器直接控制。
|
|
1502
|
+
// 原实现只查父链中间 controller, 漏了 targetNode 自身 → 自带 controller 的节点被祖先双 claim。
|
|
1503
|
+
if (targetNode && targetNode !== this.node) {
|
|
1504
|
+
const ownController = targetNode.getComponent(StateControllerV2);
|
|
1505
|
+
if (ownController && ownController !== this) {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
let current: cc.Node = targetNode;
|
|
1511
|
+
|
|
1512
|
+
while (current && current !== this.node) {
|
|
1513
|
+
const parent = current.parent;
|
|
1514
|
+
if (!parent) break;
|
|
1515
|
+
|
|
1516
|
+
// 如果父节点不是当前控制器节点,检查父节点上是否有其他StateControllerV2
|
|
1517
|
+
if (parent !== this.node) {
|
|
1518
|
+
const parentController = parent.getComponent(StateControllerV2);
|
|
1519
|
+
if (parentController && parentController !== this) {
|
|
1520
|
+
// 发现了中间控制器,该节点不是直接控制的
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
current = parent;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
return current === this.node;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* 🔧 公共方法:标记缓存为脏,需要重建
|
|
1533
|
+
* 当节点增删或StateSelectV2组件增删时调用
|
|
1534
|
+
*/
|
|
1535
|
+
public markCacheDirty(): void {
|
|
1536
|
+
this._cacheDirty = true;
|
|
1537
|
+
StateErrorManager.debug("缓存已标记为脏", {
|
|
1538
|
+
component: "StateControllerV2",
|
|
1539
|
+
method: "markCacheDirty",
|
|
1540
|
+
params: { ctrlName: this._ctrlName },
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/** 🔧 核心方法:状态更新通知机制 - 使用缓存优化 (IMPL-001) */
|
|
1545
|
+
private updateState(type: EnumUpdateType, value?: unknown) {
|
|
1546
|
+
// 🔧 IMPL-001: 使用缓存替代BFS遍历
|
|
1547
|
+
this.rebuildStateSelectCache();
|
|
1548
|
+
|
|
1549
|
+
// 🔧 直接遍历缓存的StateSelectV2组件
|
|
1550
|
+
for (const stateSelect of this._stateSelectCache) {
|
|
1551
|
+
// 注意:不能用 `!stateSelect.node.active` 做过滤。
|
|
1552
|
+
// 那会让"上一个 state 把 node 关掉、新 state 应该重新开"的场景失效 —
|
|
1553
|
+
// 下一次 updateState 因为 node.active=false 而被 skip,永远拿不到 active=true 的 apply。
|
|
1554
|
+
// 这里只过滤真正失效的组件/节点。
|
|
1555
|
+
if (!stateSelect || !stateSelect.node || !stateSelect.node.isValid) {
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (type == EnumUpdateType.State) {
|
|
1560
|
+
// 🔧 状态切换:通知StateSelectV2组件状态已改变
|
|
1561
|
+
stateSelect.updateState(this);
|
|
1562
|
+
// Wave 2: 录制中切 state, apply 完新 state 后通知 select 重拍 snapshot
|
|
1563
|
+
if (this._recording && typeof (stateSelect as any).onStateChanged === "function") {
|
|
1564
|
+
(stateSelect as any).onStateChanged(this);
|
|
1565
|
+
}
|
|
1566
|
+
// 刻意不调 stateSelect.forceRefreshInspector(): 全量刷新 inspector
|
|
1567
|
+
// 会丢焦点 / 抖动. inspector 陈旧显示由 panel 主动接管 (无插件闭环).
|
|
1568
|
+
}
|
|
1569
|
+
else if (type == EnumUpdateType.Name) {
|
|
1570
|
+
// 🔧 名称更新:通知StateSelectV2组件控制器名称已更改
|
|
1571
|
+
stateSelect.updateCtrlName(this.node);
|
|
1572
|
+
}
|
|
1573
|
+
else if (type == EnumUpdateType.SelPage) {
|
|
1574
|
+
// 🔧 状态页面更新:通知StateSelectV2组件状态列表已更改
|
|
1575
|
+
stateSelect.updateCtrlPage(this, value as number);
|
|
1576
|
+
}
|
|
1577
|
+
else if (type == EnumUpdateType.Delete) {
|
|
1578
|
+
// 🔧 删除通知:通知StateSelectV2组件控制器即将被删除
|
|
1579
|
+
stateSelect.updateDelete(this);
|
|
1580
|
+
}
|
|
1581
|
+
else if (type == EnumUpdateType.Init) {
|
|
1582
|
+
// 🔧 初始化通知:通知StateSelectV2组件控制器已完成初始化
|
|
1583
|
+
stateSelect.updatePreLoad(this);
|
|
1584
|
+
}
|
|
1585
|
+
else if (type == EnumUpdateType.Prop) {
|
|
1586
|
+
// 🔧 属性更新:通知StateSelectV2组件属性已更改
|
|
1587
|
+
stateSelect.updateProp(this);
|
|
1588
|
+
}
|
|
1589
|
+
else if (type == EnumUpdateType.Move) {
|
|
1590
|
+
// 🔧 状态顺序变更:通知StateSelectV2同步状态数据顺序
|
|
1591
|
+
// @ts-expect-error 允许使用该方法
|
|
1592
|
+
stateSelect.updateStateMove(this, value);
|
|
1593
|
+
}
|
|
1594
|
+
else if (type == EnumUpdateType.Copy) {
|
|
1595
|
+
// 🔧 状态复制:通知 StateSelectV2 深拷贝 pageData[fromIndex] → pageData[toIndex]
|
|
1596
|
+
// @ts-expect-error 允许使用该方法
|
|
1597
|
+
stateSelect.updateStateCopy(this, value);
|
|
1598
|
+
}
|
|
1599
|
+
else if (type == EnumUpdateType.RecordingStart) {
|
|
1600
|
+
// Wave 2: 录制开始, StateSelectV2 拍 snapshot
|
|
1601
|
+
if (typeof (stateSelect as any).onRecordingStart === "function") {
|
|
1602
|
+
(stateSelect as any).onRecordingStart(this);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
else if (type == EnumUpdateType.RecordingStop) {
|
|
1606
|
+
// Wave 2: 录制结束, StateSelectV2 final commit + 清 snapshot
|
|
1607
|
+
if (typeof (stateSelect as any).onRecordingStop === "function") {
|
|
1608
|
+
(stateSelect as any).onRecordingStop(this);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
else if (type == EnumUpdateType.StateWillChange) {
|
|
1612
|
+
// Wave 2: 切 state 前通知 (录制中触发 diff commit), value = fromState
|
|
1613
|
+
if (typeof (stateSelect as any).onStateWillChange === "function") {
|
|
1614
|
+
(stateSelect as any).onStateWillChange(this, value as number);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
else if (type == EnumUpdateType.PurgeStateId) {
|
|
1618
|
+
// 回收站硬删: 清掉该 stateId 在本 select 上的页数据, value = stateId
|
|
1619
|
+
if (typeof (stateSelect as any).purgeStateData === "function") {
|
|
1620
|
+
(stateSelect as any).purgeStateData(this, value as number);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
else if (type == EnumUpdateType.PreviewEnter) {
|
|
1624
|
+
// 回收站预览: 快照 + apply 该 stateId 数据到节点, value = stateId
|
|
1625
|
+
if (typeof (stateSelect as any).enterPreview === "function") {
|
|
1626
|
+
(stateSelect as any).enterPreview(this, value as number);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
else if (type == EnumUpdateType.PreviewExit) {
|
|
1630
|
+
// 回收站预览退出: 按快照精确还原节点
|
|
1631
|
+
if (typeof (stateSelect as any).exitPreview === "function") {
|
|
1632
|
+
(stateSelect as any).exitPreview();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* 当前是否正在录制 (Wave 2 Topic 3). readonly, 通过 startRecording / stopRecording 修改。
|
|
1640
|
+
*/
|
|
1641
|
+
public get isRecording(): boolean {
|
|
1642
|
+
return this._recording;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* 进入录制态: 通知所有 StateSelectV2.onRecordingStart 拍 snapshot.
|
|
1647
|
+
* 幂等: 已经在录制时 no-op。
|
|
1648
|
+
*
|
|
1649
|
+
* 模型 Z dirty 检测: 进入录制前若发现节点已勾跟随的 prop 跟 ctrlData[currentState]
|
|
1650
|
+
* 不一致 (用户没录就改了节点), 弹窗 3 选 1: 保存到当前 state / 丢弃恢复 / 取消.
|
|
1651
|
+
* 弹窗异步, 用户选完才真正进入录制态. 编辑器外 (jest / 运行时) 无 Editor.Dialog,
|
|
1652
|
+
* 走默认行为 = "保存到当前 state" (defaultId=0).
|
|
1653
|
+
*/
|
|
1654
|
+
public startRecording(): void {
|
|
1655
|
+
if (this._recording) {
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
// 录制前先退出回收态预览 (避免把预览值当作节点改动 commit 进激活态)
|
|
1659
|
+
if (this._previewingStateId >= 0) this.exitPreview();
|
|
1660
|
+
const dirty = this.collectControlledDirty();
|
|
1661
|
+
if (dirty.length === 0) {
|
|
1662
|
+
this._doStartRecording();
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
this.promptDirtyAndStart(dirty);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/** 真正进入录制态 (无 dirty 检查). dirty 弹窗 / 直接 startRecording 都走这一条. */
|
|
1669
|
+
private _doStartRecording(): void {
|
|
1670
|
+
this._recording = true;
|
|
1671
|
+
// TASK-002: 记录录制开始时的 state, 供 cancelRecording 回滚定位.
|
|
1672
|
+
this._recordingStartState = this._selectedIndex;
|
|
1673
|
+
StateErrorManager.info("开始录制", {
|
|
1674
|
+
component: "StateControllerV2",
|
|
1675
|
+
method: "_doStartRecording",
|
|
1676
|
+
params: { ctrlName: this._ctrlName },
|
|
1677
|
+
});
|
|
1678
|
+
this.updateState(EnumUpdateType.RecordingStart);
|
|
1679
|
+
// Wave 2 T25: capability 层广播 (let 其它 capability 如 timeline/undo 监听)
|
|
1680
|
+
CapabilityRegistry.dispatch("onRecordingStart", { ctrl: this });
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* 扫所有受控 StateSelectV2 上的 controlled prop, 节点当前值 vs ctrlData[currentState] 不一致
|
|
1685
|
+
* 即 dirty. 返回 [{ select, propType?, propRef?, current, stored }, ...].
|
|
1686
|
+
*
|
|
1687
|
+
* W6-2a-fixup: schema 升级 - dirty entry 含双 key (内置 propType / 自定义 propRef).
|
|
1688
|
+
* 调用方 promptDirtyAndStart 显示与写回路径同步兼容.
|
|
1689
|
+
*/
|
|
1690
|
+
private collectControlledDirty(): Array<{ select: StateSelectV2, propType?: EnumPropName, propRef?: string, current: unknown, stored: unknown }> {
|
|
1691
|
+
const out: Array<{ select: StateSelectV2, propType?: EnumPropName, propRef?: string, current: unknown, stored: unknown }> = [];
|
|
1692
|
+
this.rebuildStateSelectCache();
|
|
1693
|
+
if (!this._stateSelectCache) return out;
|
|
1694
|
+
for (const select of this._stateSelectCache) {
|
|
1695
|
+
try {
|
|
1696
|
+
const list = (select as any).collectDirtyControlled
|
|
1697
|
+
? (select as any).collectDirtyControlled(this)
|
|
1698
|
+
: [];
|
|
1699
|
+
for (const entry of list) out.push({ select, ...entry });
|
|
1700
|
+
}
|
|
1701
|
+
catch (e) {
|
|
1702
|
+
StateErrorManager.warn("collectControlledDirty: StateSelectV2 收集 dirty 失败", {
|
|
1703
|
+
component: "StateControllerV2",
|
|
1704
|
+
method: "collectControlledDirty",
|
|
1705
|
+
params: { error: (e as Error).message },
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return out;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* dirty 弹窗: 节点上跟当前 state 不一致的 controlled prop, 3 选 1.
|
|
1714
|
+
* 0 = 保存到当前 state (默认) → commit 节点当前值到 ctrlData + _doStartRecording
|
|
1715
|
+
* 1 = 丢弃恢复存储值 → 应用 ctrlData 回节点 (updateState) + _doStartRecording
|
|
1716
|
+
* 2 = 取消 → 不进入录制态
|
|
1717
|
+
*/
|
|
1718
|
+
private promptDirtyAndStart(dirty: Array<{ select: StateSelectV2, propType?: EnumPropName, propRef?: string, current: unknown, stored: unknown }>): void {
|
|
1719
|
+
const lines = dirty.map((d) => {
|
|
1720
|
+
const nodeName = (d.select.node && d.select.node.name) || "?";
|
|
1721
|
+
// W6-2a-fixup: 显示兼容 - 自定义走 propRef, 内置走 EnumPropName[propType]
|
|
1722
|
+
const label = d.propRef !== undefined
|
|
1723
|
+
? d.propRef
|
|
1724
|
+
: (d.propType !== undefined ? EnumPropName[d.propType] : "(unknown)");
|
|
1725
|
+
return ` [${nodeName}] ${label}`;
|
|
1726
|
+
});
|
|
1727
|
+
const message = `节点上以下已跟随的 prop 与 state[${this._selectedIndex}] 存储不一致:\n${lines.join("\n")}\n\n如何处理后再进入录制态?`;
|
|
1728
|
+
const onSave = () => {
|
|
1729
|
+
// 把节点当前值写进 ctrlData (类似 commit 路径)
|
|
1730
|
+
for (const d of dirty) {
|
|
1731
|
+
try {
|
|
1732
|
+
const propData = (d.select as any).getPropData(this._selectedIndex, this.ctrlId);
|
|
1733
|
+
if (!propData) continue;
|
|
1734
|
+
if (d.propRef !== undefined) {
|
|
1735
|
+
// W6-2a-fixup: 自定义 propRef 走直写 propData[propRef].
|
|
1736
|
+
// 注: dirty 来源已用 cloneValueByType 拍快照 (snapshot 已 clone),
|
|
1737
|
+
// 这里再 clone 一次保证 ctrlData 不与节点共享引用.
|
|
1738
|
+
const tp = (d.select as any).resolveTrackableProp
|
|
1739
|
+
? (d.select as any).resolveTrackableProp(d.propRef)
|
|
1740
|
+
: undefined;
|
|
1741
|
+
(propData as any)[d.propRef] = tp
|
|
1742
|
+
? cloneValueByType(d.current, tp.cocosType)
|
|
1743
|
+
: d.current;
|
|
1744
|
+
}
|
|
1745
|
+
else if (d.propType !== undefined) {
|
|
1746
|
+
// W6-2c2: 走 StateSelectV2.writePropByEnum 保证写 string propRef key (跟 production 一致)
|
|
1747
|
+
(d.select as any).writePropByEnum(propData, d.propType, d.current);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
catch (_) { /* noop */ }
|
|
1751
|
+
}
|
|
1752
|
+
this._doStartRecording();
|
|
1753
|
+
};
|
|
1754
|
+
const onDiscard = () => {
|
|
1755
|
+
// 应用 ctrlData 回节点
|
|
1756
|
+
this.rebuildStateSelectCache();
|
|
1757
|
+
if (this._stateSelectCache) {
|
|
1758
|
+
for (const select of this._stateSelectCache) {
|
|
1759
|
+
try {
|
|
1760
|
+
select.updateState(this);
|
|
1761
|
+
}
|
|
1762
|
+
catch (_) { /* noop */ }
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
this._doStartRecording();
|
|
1766
|
+
};
|
|
1767
|
+
this.showDialog({
|
|
1768
|
+
type: "info",
|
|
1769
|
+
title: "进入录制前: 节点有未保存改动",
|
|
1770
|
+
message,
|
|
1771
|
+
buttons: ["保存到当前 state", "丢弃恢复存储值", "取消"],
|
|
1772
|
+
defaultId: 0,
|
|
1773
|
+
cancelId: 2,
|
|
1774
|
+
}, (idx) => {
|
|
1775
|
+
if (idx === 0) onSave();
|
|
1776
|
+
else if (idx === 1) onDiscard();
|
|
1777
|
+
// idx === 2: 取消, 什么都不做
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* 弹窗封装. cocos 2.x 文档明确 Editor.Dialog 仅 main process 可达, component 在
|
|
1783
|
+
* scene renderer 进程跑不通. 用 Electron renderer 原生 window.confirm (同步) 兜底:
|
|
1784
|
+
* - 2 按钮: 一次 confirm, OK=0 / Cancel=1
|
|
1785
|
+
* - 3 按钮: 串联两次 confirm 拼出
|
|
1786
|
+
* - jest 环境: 走 Editor.Dialog (mock 注入), 真编辑器走 window.confirm
|
|
1787
|
+
*/
|
|
1788
|
+
private showDialog(opts: { title: string, message: string, buttons: string[], defaultId?: number, cancelId?: number, type?: string }, cb: (idx: number) => void): void {
|
|
1789
|
+
// 优先 Editor.Dialog (jest mock 走这条; 真编辑器 main process 才有)
|
|
1790
|
+
try {
|
|
1791
|
+
const Ed = (globalThis as any).Editor;
|
|
1792
|
+
if (Ed && Ed.Dialog && typeof Ed.Dialog.messageBox === "function") {
|
|
1793
|
+
let resolved = false;
|
|
1794
|
+
const sync = Ed.Dialog.messageBox(opts, (idx: number) => {
|
|
1795
|
+
if (!resolved) {
|
|
1796
|
+
resolved = true;
|
|
1797
|
+
cb(typeof idx === "number" ? idx : (opts.defaultId || 0));
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
if (!resolved && typeof sync === "number") {
|
|
1801
|
+
resolved = true;
|
|
1802
|
+
cb(sync);
|
|
1803
|
+
}
|
|
1804
|
+
if (resolved) return;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
catch (_) { /* fall through to window.confirm */ }
|
|
1808
|
+
|
|
1809
|
+
// renderer 同步 fallback (跳过 jsdom: 它的 confirm 是 noop 永远返 false, 测试场景走默认)
|
|
1810
|
+
const nav = (globalThis as any).navigator;
|
|
1811
|
+
const isJsdom = !!(nav && nav.userAgent && nav.userAgent.indexOf("jsdom") >= 0);
|
|
1812
|
+
if (!isJsdom) {
|
|
1813
|
+
try {
|
|
1814
|
+
const w = (globalThis as any).window;
|
|
1815
|
+
if (w && typeof w.confirm === "function") {
|
|
1816
|
+
const head = `${opts.title}\n\n${opts.message}`;
|
|
1817
|
+
if (opts.buttons.length === 2) {
|
|
1818
|
+
const ok = w.confirm(`${head}\n\n确定 = ${opts.buttons[0]}\n取消 = ${opts.buttons[1]}`);
|
|
1819
|
+
cb(ok ? 0 : 1);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
if (opts.buttons.length === 3) {
|
|
1823
|
+
const first = w.confirm(`${head}\n\n确定 = ${opts.buttons[0]}\n取消 = (进入下一选项)`);
|
|
1824
|
+
if (first) {
|
|
1825
|
+
cb(0);
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const second = w.confirm(`继续选择:\n\n确定 = ${opts.buttons[1]}\n取消 = ${opts.buttons[2]}`);
|
|
1829
|
+
cb(second ? 1 : 2);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
catch (_) { /* fall through to defaultId */ }
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
cb(typeof opts.defaultId === "number" ? opts.defaultId : 0);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* 退出录制态: 通知所有 StateSelectV2.onRecordingStop final commit + 清 snapshot.
|
|
1842
|
+
* 幂等: 未在录制时 no-op。
|
|
1843
|
+
*/
|
|
1844
|
+
public stopRecording(): void {
|
|
1845
|
+
if (!this._recording) {
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
this._recording = false;
|
|
1849
|
+
StateErrorManager.info("停止录制", {
|
|
1850
|
+
component: "StateControllerV2",
|
|
1851
|
+
method: "stopRecording",
|
|
1852
|
+
params: { ctrlName: this._ctrlName },
|
|
1853
|
+
});
|
|
1854
|
+
this.updateState(EnumUpdateType.RecordingStop);
|
|
1855
|
+
// Wave 2 T25: capability 层广播
|
|
1856
|
+
CapabilityRegistry.dispatch("onRecordingStop", { ctrl: this });
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* 录制按钮: 切换 isRecording (普通访问器, inspector 可见性由 recording 折叠组代理).
|
|
1861
|
+
*/
|
|
1862
|
+
public get recordTrigger() {
|
|
1863
|
+
return this._recording;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
public set recordTrigger(_value: boolean) {
|
|
1867
|
+
if (!CC_EDITOR) return;
|
|
1868
|
+
if (this._recording) {
|
|
1869
|
+
this.stopRecording();
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
this.startRecording();
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* 撤销本次录制 (TASK-002, 模型 Z inspector 闭环).
|
|
1878
|
+
*
|
|
1879
|
+
* 把 ctrlData[_recordingStartState] 回滚到录制开始前的值 (复用 StateSelectV2.onRecordingStart
|
|
1880
|
+
* 已拍的 _snapshot), 置 _recording=false, 不调 stopRecording (避免触发 commit / RecordingStop).
|
|
1881
|
+
*
|
|
1882
|
+
* 设计:
|
|
1883
|
+
* - 与 stopRecording 平行, 不复用 stopRecording 路径 (后者会 commit + dispatch RecordingStop, 是 cancel 要避免的)
|
|
1884
|
+
* - 录制是事务, cancel 必须完全回滚, 不留 commit 痕迹
|
|
1885
|
+
* - dispatch onRecordingCancel 让其它 capability (如 timeline / undo) 监听
|
|
1886
|
+
*/
|
|
1887
|
+
public cancelRecording(): void {
|
|
1888
|
+
if (!this._recording) {
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
const fromState = this._recordingStartState;
|
|
1892
|
+
this.rebuildStateSelectCache();
|
|
1893
|
+
if (this._stateSelectCache) {
|
|
1894
|
+
for (const select of this._stateSelectCache) {
|
|
1895
|
+
if (!select || !select.node || !select.node.isValid) continue;
|
|
1896
|
+
if (typeof (select as any).applyRecordingSnapshot === "function") {
|
|
1897
|
+
try {
|
|
1898
|
+
(select as any).applyRecordingSnapshot(this, fromState);
|
|
1899
|
+
}
|
|
1900
|
+
catch (e) {
|
|
1901
|
+
StateErrorManager.warn("cancelRecording: applyRecordingSnapshot 失败", {
|
|
1902
|
+
component: "StateControllerV2",
|
|
1903
|
+
method: "cancelRecording",
|
|
1904
|
+
params: { error: (e as Error).message },
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
this._recording = false;
|
|
1911
|
+
StateErrorManager.info("撤销录制", {
|
|
1912
|
+
component: "StateControllerV2",
|
|
1913
|
+
method: "cancelRecording",
|
|
1914
|
+
params: { ctrlName: this._ctrlName, fromState },
|
|
1915
|
+
});
|
|
1916
|
+
// 重新应用 state[fromState] 回节点, 让视觉与回滚后的 ctrlData 一致
|
|
1917
|
+
this.updateState(EnumUpdateType.State);
|
|
1918
|
+
// TASK-002: capability 层广播 cancel 事件 (与 stop 区分, 不发 RecordingStop)
|
|
1919
|
+
CapabilityRegistry.dispatch("onRecordingCancel", { ctrl: this, fromState });
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/**
|
|
1923
|
+
* 撤销录制按钮 (TASK-002): 仅录制态下点击有效, 调 cancelRecording.
|
|
1924
|
+
* 普通访问器, inspector 可见性由 recording 折叠组代理.
|
|
1925
|
+
*/
|
|
1926
|
+
public get cancelRecordTrigger() {
|
|
1927
|
+
return false;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
public set cancelRecordTrigger(_value: boolean) {
|
|
1931
|
+
if (!CC_EDITOR) return;
|
|
1932
|
+
if (this._recording) {
|
|
1933
|
+
this.cancelRecording();
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/** 强制刷新属性检查器 (states 变化后由内部直接调用, 不再走 strategy 分支) */
|
|
1938
|
+
private forceRefreshInspector() {
|
|
1939
|
+
if (!CC_EDITOR) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
try {
|
|
1943
|
+
Editor.Utils.refreshSelectedInspector("node", this.node.uuid);
|
|
1944
|
+
}
|
|
1945
|
+
catch (error) {
|
|
1946
|
+
StateErrorManager.warn("刷新属性检查器失败", {
|
|
1947
|
+
component: "StateControllerV2",
|
|
1948
|
+
method: "forceRefreshInspector",
|
|
1949
|
+
params: { error: (error as Error).message },
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// back-compat 导出别名: 仅 JS 导出名, 不触发 @ccclass (引擎/panel 按 cid "StateControllerV2" 识别).
|
|
1956
|
+
// 供既有测试用 `{ StateController }` / `Mod.StateController` 沿用, 不影响 V2 与旧版共存.
|
|
1957
|
+
export { StateControllerV2 as StateController };
|