@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,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoSyncCapability (Wave 2 T23).
|
|
3
|
+
*
|
|
4
|
+
* 把现有的 StateSelectV2.autoSyncEnabled (硬编码 true) 抽成 capability:
|
|
5
|
+
* - 切 state 时, 是否保持 inspector 上当前选中的 propKey (autoSync ON, 当前默认行为)
|
|
6
|
+
* - 还是改用切到的 state 的 lastProp (autoSync OFF)
|
|
7
|
+
*
|
|
8
|
+
* 当前 Wave 2 不主动改 StateSelectV2 内的引用 (autoSyncEnabled 仍是 true), 仅暴露 capability
|
|
9
|
+
* 接口 + isEnabled() 静态查询, 为 Panel / Wave 3 接管时切换做准备.
|
|
10
|
+
*
|
|
11
|
+
* 命名空间: 配置存放在静态字段, 不写入 ctrlData 任何 namespace (这是全局开关, 不分 state).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CapabilityRegistry } from "../CapabilityRegistry";
|
|
15
|
+
import { ICapability } from "../Capability";
|
|
16
|
+
|
|
17
|
+
let _enabled = true;
|
|
18
|
+
|
|
19
|
+
export const AutoSyncCapability: ICapability & {
|
|
20
|
+
isEnabled: () => boolean
|
|
21
|
+
setEnabled: (v: boolean) => void
|
|
22
|
+
} = {
|
|
23
|
+
name: "autoSync",
|
|
24
|
+
isEnabled: () => _enabled,
|
|
25
|
+
setEnabled: (v: boolean) => {
|
|
26
|
+
_enabled = !!v;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
CapabilityRegistry.register(AutoSyncCapability);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventCapability (Wave 3 T02).
|
|
3
|
+
*
|
|
4
|
+
* runtime 事件订阅 capability. 让代码通过 capability 接口订阅 controller 的 state 切换:
|
|
5
|
+
*
|
|
6
|
+
* EventCapability.on(ctrl, "stateChanged", cb)
|
|
7
|
+
* EventCapability.off(ctrl, "stateChanged", cb)
|
|
8
|
+
* EventCapability.once(ctrl, "stateChanged", cb)
|
|
9
|
+
*
|
|
10
|
+
* payload: { ctrl, fromState, toState, fromName, toName }
|
|
11
|
+
*
|
|
12
|
+
* 实装路径:
|
|
13
|
+
* - StateControllerV2.selectedIndex.setter 已 dispatch("onStateChanged", ...)
|
|
14
|
+
* - EventCapability.onStateChanged hook 在被 dispatch 时, 查 ctrl-local listener Map 并 fanout
|
|
15
|
+
* - listener 存在 WeakMap<ctrl, Map<eventName, Set<cb>>>
|
|
16
|
+
*
|
|
17
|
+
* 命名空间: 不写 ctrlData (运行期 listener 不持久化), 仅内存里维持订阅表.
|
|
18
|
+
*
|
|
19
|
+
* 内存安全: 提供 clear(ctrl) 供 ctrl.onDestroy 调用; 用 WeakMap 保证 ctrl GC 后自动释放.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { CapabilityRegistry } from "../CapabilityRegistry";
|
|
23
|
+
import { CapabilityContext, ICapability } from "../Capability";
|
|
24
|
+
import { StateErrorManager } from "../StateErrorManagerV2";
|
|
25
|
+
|
|
26
|
+
export type EventName = "stateChanged";
|
|
27
|
+
|
|
28
|
+
export interface StateChangedPayload {
|
|
29
|
+
ctrl: any
|
|
30
|
+
fromState: number
|
|
31
|
+
toState: number
|
|
32
|
+
fromName: string | null
|
|
33
|
+
toName: string | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Listener = (payload: StateChangedPayload) => void;
|
|
37
|
+
|
|
38
|
+
const listenerMap: WeakMap<object, Map<EventName, Listener[]>> = new WeakMap();
|
|
39
|
+
const onceFlag: WeakSet<Listener> = new WeakSet();
|
|
40
|
+
|
|
41
|
+
function getOrInitListeners(ctrl: object, event: EventName): Listener[] {
|
|
42
|
+
let perCtrl = listenerMap.get(ctrl);
|
|
43
|
+
if (!perCtrl) {
|
|
44
|
+
perCtrl = new Map();
|
|
45
|
+
listenerMap.set(ctrl, perCtrl);
|
|
46
|
+
}
|
|
47
|
+
let arr = perCtrl.get(event);
|
|
48
|
+
if (!arr) {
|
|
49
|
+
arr = [];
|
|
50
|
+
perCtrl.set(event, arr);
|
|
51
|
+
}
|
|
52
|
+
return arr;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stateNameAt(ctrl: any, idx: number): string | null {
|
|
56
|
+
const states = ctrl && ctrl._states;
|
|
57
|
+
if (!states || idx < 0 || idx >= states.length) return null;
|
|
58
|
+
const s = states[idx];
|
|
59
|
+
return (s && s.name) ? s.name : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const EventCapability: ICapability & {
|
|
63
|
+
on: (ctrl: any, event: EventName, cb: Listener) => void
|
|
64
|
+
off: (ctrl: any, event: EventName, cb: Listener) => void
|
|
65
|
+
once: (ctrl: any, event: EventName, cb: Listener) => void
|
|
66
|
+
clear: (ctrl: any) => void
|
|
67
|
+
listenerCount: (ctrl: any, event: EventName) => number
|
|
68
|
+
} = {
|
|
69
|
+
name: "event",
|
|
70
|
+
|
|
71
|
+
on(ctrl: any, event: EventName, cb: Listener): void {
|
|
72
|
+
if (!ctrl || typeof cb !== "function") return;
|
|
73
|
+
getOrInitListeners(ctrl, event).push(cb);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
off(ctrl: any, event: EventName, cb: Listener): void {
|
|
77
|
+
if (!ctrl) return;
|
|
78
|
+
const perCtrl = listenerMap.get(ctrl);
|
|
79
|
+
if (!perCtrl) return;
|
|
80
|
+
const arr = perCtrl.get(event);
|
|
81
|
+
if (!arr) return;
|
|
82
|
+
const i = arr.indexOf(cb);
|
|
83
|
+
if (i >= 0) arr.splice(i, 1);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
once(ctrl: any, event: EventName, cb: Listener): void {
|
|
87
|
+
if (!ctrl || typeof cb !== "function") return;
|
|
88
|
+
const wrapper: Listener = (payload) => {
|
|
89
|
+
EventCapability.off(ctrl, event, wrapper);
|
|
90
|
+
cb(payload);
|
|
91
|
+
};
|
|
92
|
+
onceFlag.add(wrapper);
|
|
93
|
+
getOrInitListeners(ctrl, event).push(wrapper);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
clear(ctrl: any): void {
|
|
97
|
+
if (!ctrl) return;
|
|
98
|
+
listenerMap.delete(ctrl);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
listenerCount(ctrl: any, event: EventName): number {
|
|
102
|
+
const perCtrl = listenerMap.get(ctrl);
|
|
103
|
+
if (!perCtrl) return 0;
|
|
104
|
+
const arr = perCtrl.get(event);
|
|
105
|
+
return arr ? arr.length : 0;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
onStateChanged(ctx: CapabilityContext): void {
|
|
109
|
+
const ctrl = ctx.ctrl;
|
|
110
|
+
if (!ctrl) return;
|
|
111
|
+
const perCtrl = listenerMap.get(ctrl);
|
|
112
|
+
if (!perCtrl) return;
|
|
113
|
+
const arr = perCtrl.get("stateChanged");
|
|
114
|
+
if (!arr || arr.length === 0) return;
|
|
115
|
+
|
|
116
|
+
const fromState = (ctx.fromState === undefined || ctx.fromState === null) ? -1 : ctx.fromState;
|
|
117
|
+
const toState = (ctx.toState === undefined || ctx.toState === null) ? ctrl.selectedIndex : ctx.toState;
|
|
118
|
+
const payload: StateChangedPayload = {
|
|
119
|
+
ctrl,
|
|
120
|
+
fromState,
|
|
121
|
+
toState,
|
|
122
|
+
fromName: stateNameAt(ctrl, fromState),
|
|
123
|
+
toName: stateNameAt(ctrl, toState),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// 拷贝快照 避免 listener 内部 off 改动遍历中数组
|
|
127
|
+
const snapshot = arr.slice();
|
|
128
|
+
for (let i = 0; i < snapshot.length; i++) {
|
|
129
|
+
const cb = snapshot[i];
|
|
130
|
+
try {
|
|
131
|
+
cb(payload);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
StateErrorManager.warn("EventCapability listener 抛异常", {
|
|
135
|
+
component: "EventCapability",
|
|
136
|
+
method: "onStateChanged",
|
|
137
|
+
params: { error: (e as Error).message },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
CapabilityRegistry.register(EventCapability);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MigrationCapability (Wave 4 T01 实装).
|
|
3
|
+
*
|
|
4
|
+
* 数据版本迁移框架. 用途:
|
|
5
|
+
* - prefab 反序列化时, 老格式 _ctrlData → 当前格式
|
|
6
|
+
* - 未来字段重命名 / 结构调整, 不破坏老 scene
|
|
7
|
+
*
|
|
8
|
+
* 使用方式:
|
|
9
|
+
* MigrationCapability.registerStep(1, function(data){
|
|
10
|
+
* data.newField = computeFromOld(data);
|
|
11
|
+
* delete data.oldField;
|
|
12
|
+
* return data;
|
|
13
|
+
* });
|
|
14
|
+
* // 反序列化路径自动调:
|
|
15
|
+
* const upgraded = MigrationCapability.migrate(oldData, oldVersion, MigrationCapability.CURRENT_VERSION);
|
|
16
|
+
*
|
|
17
|
+
* step(fromVersion) 表示 "把版本 N 的数据升到版本 N+1". migrate() 按 from 升序依次跑.
|
|
18
|
+
*
|
|
19
|
+
* 错误兜底: step 抛异常 → 停止后续, 返回最后一个成功的中间结果 (尽力升级而非崩盘).
|
|
20
|
+
*
|
|
21
|
+
* 当前 CURRENT_VERSION = 1 (基线). 未来真要改数据格式时, ++ 并注册对应 step.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { CapabilityRegistry } from "../CapabilityRegistry";
|
|
25
|
+
import { ICapability } from "../Capability";
|
|
26
|
+
import { StateErrorManager } from "../StateErrorManagerV2";
|
|
27
|
+
|
|
28
|
+
type MigrationStep = (data: unknown) => unknown;
|
|
29
|
+
|
|
30
|
+
const steps: Map<number, MigrationStep> = new Map();
|
|
31
|
+
|
|
32
|
+
export const MigrationCapability: ICapability & {
|
|
33
|
+
CURRENT_VERSION: number
|
|
34
|
+
registerStep: (fromVersion: number, fn: MigrationStep) => void
|
|
35
|
+
migrate: (data: unknown, fromVersion: number, toVersion: number) => unknown
|
|
36
|
+
clearSteps: () => void
|
|
37
|
+
} = {
|
|
38
|
+
name: "migration",
|
|
39
|
+
CURRENT_VERSION: 1,
|
|
40
|
+
|
|
41
|
+
registerStep(fromVersion: number, fn: MigrationStep): void {
|
|
42
|
+
if (typeof fn !== "function") return;
|
|
43
|
+
steps.set(fromVersion, fn);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
clearSteps(): void {
|
|
47
|
+
steps.clear();
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
migrate(data: unknown, fromVersion: number, toVersion: number): unknown {
|
|
51
|
+
if (fromVersion === toVersion) return data;
|
|
52
|
+
if (fromVersion > toVersion) {
|
|
53
|
+
StateErrorManager.warn("MigrationCapability.migrate: fromVersion > toVersion, 不支持降级", {
|
|
54
|
+
component: "MigrationCapability",
|
|
55
|
+
method: "migrate",
|
|
56
|
+
params: { fromVersion, toVersion },
|
|
57
|
+
});
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sortedKeys: number[] = [];
|
|
62
|
+
steps.forEach((_, k) => {
|
|
63
|
+
sortedKeys.push(k);
|
|
64
|
+
});
|
|
65
|
+
sortedKeys.sort((a, b) => a - b);
|
|
66
|
+
|
|
67
|
+
let current: unknown = data;
|
|
68
|
+
for (let i = 0; i < sortedKeys.length; i++) {
|
|
69
|
+
const v = sortedKeys[i];
|
|
70
|
+
if (v < fromVersion) continue;
|
|
71
|
+
if (v >= toVersion) break;
|
|
72
|
+
const step = steps.get(v);
|
|
73
|
+
if (!step) continue;
|
|
74
|
+
try {
|
|
75
|
+
current = step(current);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
StateErrorManager.warn(`MigrationCapability step(v=${v}) 抛异常, 停止后续`, {
|
|
79
|
+
component: "MigrationCapability",
|
|
80
|
+
method: "migrate",
|
|
81
|
+
params: { fromVersion, toVersion, failedStepFrom: v, error: (e as Error).message },
|
|
82
|
+
});
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
onCtrlDataMigrate(data: unknown, version: number): unknown {
|
|
90
|
+
return MigrationCapability.migrate(data, version, MigrationCapability.CURRENT_VERSION);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
CapabilityRegistry.register(MigrationCapability);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultiCtrlBindingCapability (Wave 5 T02).
|
|
3
|
+
*
|
|
4
|
+
* 声明式跨 ctrl 状态联动: ctrlA 切到 stateId X → ctrlB 自动切到 stateId Y.
|
|
5
|
+
*
|
|
6
|
+
* addBinding(sourceCtrl, sourceStateId, targetCtrl, targetStateId) → boolean
|
|
7
|
+
* removeBinding(sourceCtrl, sourceStateId, targetCtrl) → boolean
|
|
8
|
+
* listBindings(sourceCtrl) → [{sourceStateId, targetCtrl, targetStateId}]
|
|
9
|
+
* clearAllBindings(sourceCtrl)
|
|
10
|
+
*
|
|
11
|
+
* 内部用 EventCapability.on 监听 source 切换, 命中 sourceStateId 后走
|
|
12
|
+
* SelectedPageIdCapability.setStateById 切 target.
|
|
13
|
+
*
|
|
14
|
+
* 重要约束:
|
|
15
|
+
* - 同 (source, sourceStateId, target) 重复 add 覆盖
|
|
16
|
+
* - 循环防护: 当 binding 正在 dispatch, 嵌套触发的子 binding 跳过 (一帧只允许 1 跳传播深度).
|
|
17
|
+
* A → B → A 不会死循环.
|
|
18
|
+
* - clearAllBindings 解 EventCapability listener, 避免 listener 泄漏
|
|
19
|
+
*
|
|
20
|
+
* 数据存储: 进程级 Map<sourceCtrl, Map<sourceStateId, Map<targetCtrl, {targetStateId, listenerHandle}>>>.
|
|
21
|
+
* sourceCtrl / targetCtrl 用 WeakMap 持有避免泄漏. (内层 Map 用普通 Map 因 key 是 number/object 混合.)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { CapabilityRegistry } from "../CapabilityRegistry";
|
|
25
|
+
import { ICapability } from "../Capability";
|
|
26
|
+
import { StateErrorManager } from "../StateErrorManagerV2";
|
|
27
|
+
import { EventCapability, StateChangedPayload } from "./EventCapability";
|
|
28
|
+
import { SelectedPageIdCapability } from "./SelectedPageIdCapability";
|
|
29
|
+
|
|
30
|
+
interface BindingEntry {
|
|
31
|
+
targetStateId: number
|
|
32
|
+
target: object
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SourceListener {
|
|
36
|
+
/** sourceStateId → list of bindings */
|
|
37
|
+
byState: Map<number, BindingEntry[]>
|
|
38
|
+
/** EventCapability 注册的回调引用 (用于卸载) */
|
|
39
|
+
listener: (payload: StateChangedPayload) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sourceMap: WeakMap<object, SourceListener> = new WeakMap();
|
|
43
|
+
|
|
44
|
+
/** 循环防护: 当正在 dispatch binding, 后续切换不再触发 binding. 单帧 boolean 即可. */
|
|
45
|
+
let dispatching = false;
|
|
46
|
+
|
|
47
|
+
function ensureSourceListener(sourceCtrl: any): SourceListener {
|
|
48
|
+
let entry = sourceMap.get(sourceCtrl);
|
|
49
|
+
if (entry) return entry;
|
|
50
|
+
|
|
51
|
+
const byState = new Map<number, BindingEntry[]>();
|
|
52
|
+
|
|
53
|
+
const listener: (payload: StateChangedPayload) => void = function (payload) {
|
|
54
|
+
if (dispatching) return;
|
|
55
|
+
const sId = SelectedPageIdCapability.getSelectedStateId(payload.ctrl);
|
|
56
|
+
const bindings = byState.get(sId);
|
|
57
|
+
if (!bindings || bindings.length === 0) return;
|
|
58
|
+
|
|
59
|
+
dispatching = true;
|
|
60
|
+
try {
|
|
61
|
+
for (let i = 0; i < bindings.length; i++) {
|
|
62
|
+
const b = bindings[i];
|
|
63
|
+
try {
|
|
64
|
+
SelectedPageIdCapability.setStateById(b.target, b.targetStateId);
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
StateErrorManager.warn("MultiCtrlBinding setStateById 异常", {
|
|
68
|
+
component: "MultiCtrlBindingCapability",
|
|
69
|
+
method: "dispatch",
|
|
70
|
+
params: { error: (e as Error).message, targetStateId: b.targetStateId },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
dispatching = false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
entry = { byState, listener };
|
|
81
|
+
sourceMap.set(sourceCtrl, entry);
|
|
82
|
+
EventCapability.on(sourceCtrl, "stateChanged", listener);
|
|
83
|
+
return entry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BindingDescriptor {
|
|
87
|
+
sourceStateId: number
|
|
88
|
+
targetCtrl: any
|
|
89
|
+
targetStateId: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const MultiCtrlBindingCapability: ICapability & {
|
|
93
|
+
addBinding: (source: any, sourceStateId: number, target: any, targetStateId: number) => boolean
|
|
94
|
+
removeBinding: (source: any, sourceStateId: number, target: any) => boolean
|
|
95
|
+
listBindings: (source: any) => BindingDescriptor[]
|
|
96
|
+
clearAllBindings: (source: any) => void
|
|
97
|
+
} = {
|
|
98
|
+
name: "multiCtrlBinding",
|
|
99
|
+
dependsOn: ["event", "selectedPageId"],
|
|
100
|
+
|
|
101
|
+
addBinding(source: any, sourceStateId: number, target: any, targetStateId: number): boolean {
|
|
102
|
+
if (!source || !target) return false;
|
|
103
|
+
if (typeof sourceStateId !== "number" || typeof targetStateId !== "number") return false;
|
|
104
|
+
|
|
105
|
+
const entry = ensureSourceListener(source);
|
|
106
|
+
let bindings = entry.byState.get(sourceStateId);
|
|
107
|
+
if (!bindings) {
|
|
108
|
+
bindings = [];
|
|
109
|
+
entry.byState.set(sourceStateId, bindings);
|
|
110
|
+
}
|
|
111
|
+
// 覆盖同 target 的旧 binding
|
|
112
|
+
const existIdx = bindings.findIndex(b => b.target === target);
|
|
113
|
+
if (existIdx >= 0) {
|
|
114
|
+
bindings[existIdx] = { targetStateId, target };
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
bindings.push({ targetStateId, target });
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
removeBinding(source: any, sourceStateId: number, target: any): boolean {
|
|
123
|
+
if (!source || !target) return false;
|
|
124
|
+
const entry = sourceMap.get(source);
|
|
125
|
+
if (!entry) return false;
|
|
126
|
+
const bindings = entry.byState.get(sourceStateId);
|
|
127
|
+
if (!bindings) return false;
|
|
128
|
+
const idx = bindings.findIndex(b => b.target === target);
|
|
129
|
+
if (idx < 0) return false;
|
|
130
|
+
bindings.splice(idx, 1);
|
|
131
|
+
if (bindings.length === 0) entry.byState.delete(sourceStateId);
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
listBindings(source: any): BindingDescriptor[] {
|
|
136
|
+
if (!source) return [];
|
|
137
|
+
const entry = sourceMap.get(source);
|
|
138
|
+
if (!entry) return [];
|
|
139
|
+
const out: BindingDescriptor[] = [];
|
|
140
|
+
entry.byState.forEach((bindings, sId) => {
|
|
141
|
+
bindings.forEach((b) => {
|
|
142
|
+
out.push({ sourceStateId: sId, targetCtrl: b.target, targetStateId: b.targetStateId });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
return out;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
clearAllBindings(source: any): void {
|
|
149
|
+
if (!source) return;
|
|
150
|
+
const entry = sourceMap.get(source);
|
|
151
|
+
if (!entry) return;
|
|
152
|
+
EventCapability.off(source, "stateChanged", entry.listener);
|
|
153
|
+
sourceMap.delete(source);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
CapabilityRegistry.register(MultiCtrlBindingCapability);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PropertyControlCapability (Wave 2 T21).
|
|
3
|
+
*
|
|
4
|
+
* 把现有的 StatePropertyControlService 包装成一个 capability, 让"prop 可用性 / 受控状态判断"
|
|
5
|
+
* 成为可独立挂载/卸载的能力, 而非 StateSelectV2 紧耦合.
|
|
6
|
+
*
|
|
7
|
+
* 现状: StateSelectV2 的 isPropertyAvailable / isPropertyControlled 调 PropertyControlService
|
|
8
|
+
* 静态方法. 本 capability 暴露同样的静态 API + 自注册到 Registry. 既不破现有调用,
|
|
9
|
+
* 又让 panel / 第三方能力可以通过 CapabilityRegistry.get("propertyControl") 拿到.
|
|
10
|
+
*
|
|
11
|
+
* 命名空间: 使用 `$$controlledProps$$` (沿用历史 key, 向后兼容). 不改名为 `$$propertyControl$$`
|
|
12
|
+
* 避免老 scene 数据反序列化丢失 — 这是 PLN-002 R3 的应对策略.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { CapabilityRegistry } from "../CapabilityRegistry";
|
|
16
|
+
import { CapabilityContext, ICapability } from "../Capability";
|
|
17
|
+
import { EnumPropName } from "../StateEnumV2";
|
|
18
|
+
import { PropertyControlService } from "../StatePropertyControlService";
|
|
19
|
+
import { ENUM_TO_PROPREF, PROPREF_TO_ENUM } from "../EnumPropRefMap";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* PropertyControlCapability — 静态工具风格 (不持实例状态), 同时实现 ICapability 接口.
|
|
23
|
+
* 暴露三个核心查询方法 (同 PropertyControlService); StateSelectV2 现有调用保持不变.
|
|
24
|
+
*
|
|
25
|
+
* W6-2b: 增加 propRef 解析工具 + onPropertyControlled / onPropertyReleased hook stub.
|
|
26
|
+
* hook 当前仅做日志, 不写 ctrlData (实际数据写入仍在 StateSelectV2.addPropertyControl 路径).
|
|
27
|
+
* propRef 优先, fallback propType + ENUM_TO_PROPREF 派生.
|
|
28
|
+
*/
|
|
29
|
+
class PropertyControlCapabilityImpl implements ICapability {
|
|
30
|
+
public readonly name = "propertyControl";
|
|
31
|
+
|
|
32
|
+
/** 节点上是否可以使用某 prop 类型. (代理到 PropertyControlService) */
|
|
33
|
+
public static isPropertyAvailable(node: cc.Node, propType: EnumPropName): boolean {
|
|
34
|
+
return PropertyControlService.isPropertyAvailable(node, propType);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** propData 中某 prop 是否标记为受控. */
|
|
38
|
+
public static isPropertyControlled(propData: any, propType: EnumPropName): boolean {
|
|
39
|
+
return PropertyControlService.isPropertyControlled(propData, propType);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 列出当前节点上所有可用的 prop. */
|
|
43
|
+
public static scanAvailableProperties(node: cc.Node): EnumPropName[] {
|
|
44
|
+
return PropertyControlService.scanAvailableProperties(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 注册第三方组件 prop 类型 (PropertyControlService.registerComponentProp 的代理) */
|
|
48
|
+
public static registerComponentProp(propType: EnumPropName, check: (node: cc.Node) => boolean): void {
|
|
49
|
+
PropertyControlService.registerComponentProp(propType, check);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* W6-2b: 从 ctx 解析 propRef. 优先用 ctx.propRef, fallback ctx.propType + ENUM_TO_PROPREF.
|
|
54
|
+
* 返回 undefined 表示既无 propRef 又无可派生 propType (e.g. AMBIGUOUS Position/Anchor/Size/GrayScale).
|
|
55
|
+
*/
|
|
56
|
+
public static resolvePropRef(ctx: CapabilityContext): string | undefined {
|
|
57
|
+
if (typeof ctx.propRef === "string" && ctx.propRef.length > 0) return ctx.propRef;
|
|
58
|
+
if (typeof ctx.propType === "number" && ctx.propType > 0) {
|
|
59
|
+
return ENUM_TO_PROPREF[ctx.propType];
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* W6-2b: 从 ctx 解析 propType (反向). 优先用 ctx.propType, fallback ctx.propRef + PROPREF_TO_ENUM.
|
|
66
|
+
* 返回 undefined 表示自定义 prop (无 EnumPropName 映射).
|
|
67
|
+
*/
|
|
68
|
+
public static resolvePropType(ctx: CapabilityContext): EnumPropName | undefined {
|
|
69
|
+
if (typeof ctx.propType === "number" && ctx.propType > 0) return ctx.propType;
|
|
70
|
+
if (typeof ctx.propRef === "string") {
|
|
71
|
+
const mapped = PROPREF_TO_ENUM[ctx.propRef];
|
|
72
|
+
if (mapped !== undefined) return mapped as EnumPropName;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 导出对象 (不是类) — 让 PropertyControlCapability.name === "propertyControl",
|
|
80
|
+
* 避免 JS function.name 默认返回类名 "PropertyControlCapabilityImpl" 的陷阱.
|
|
81
|
+
*
|
|
82
|
+
* 该对象同时是 ICapability 单例 (注册到 Registry) 和静态工具命名空间.
|
|
83
|
+
*
|
|
84
|
+
* W6-2b: 加 resolvePropRef / resolvePropType 静态工具 + onPropertyControlled / onPropertyReleased
|
|
85
|
+
* hook 占位 (优先用 ctx.propRef, fallback propType).
|
|
86
|
+
*/
|
|
87
|
+
export const PropertyControlCapability: ICapability & {
|
|
88
|
+
isPropertyAvailable: typeof PropertyControlCapabilityImpl.isPropertyAvailable
|
|
89
|
+
isPropertyControlled: typeof PropertyControlCapabilityImpl.isPropertyControlled
|
|
90
|
+
scanAvailableProperties: typeof PropertyControlCapabilityImpl.scanAvailableProperties
|
|
91
|
+
registerComponentProp: typeof PropertyControlCapabilityImpl.registerComponentProp
|
|
92
|
+
resolvePropRef: typeof PropertyControlCapabilityImpl.resolvePropRef
|
|
93
|
+
resolvePropType: typeof PropertyControlCapabilityImpl.resolvePropType
|
|
94
|
+
} = {
|
|
95
|
+
name: "propertyControl",
|
|
96
|
+
isPropertyAvailable: PropertyControlCapabilityImpl.isPropertyAvailable,
|
|
97
|
+
isPropertyControlled: PropertyControlCapabilityImpl.isPropertyControlled,
|
|
98
|
+
scanAvailableProperties: PropertyControlCapabilityImpl.scanAvailableProperties,
|
|
99
|
+
registerComponentProp: PropertyControlCapabilityImpl.registerComponentProp,
|
|
100
|
+
resolvePropRef: PropertyControlCapabilityImpl.resolvePropRef,
|
|
101
|
+
resolvePropType: PropertyControlCapabilityImpl.resolvePropType,
|
|
102
|
+
|
|
103
|
+
// W6-2b: onPropertyControlled hook — 优先 propRef, fallback propType + ENUM_TO_PROPREF 派生
|
|
104
|
+
onPropertyControlled(ctx: CapabilityContext): void {
|
|
105
|
+
// hook 不做实际数据写入 (那是 StateSelectV2.addPropertyControl 的职责),
|
|
106
|
+
// 仅消费 ctx 验证 propRef + propType 双字段并存的契约 — 让下游 capability
|
|
107
|
+
// (e.g. Recording / Tween) 能用 propRef 做 propRef-aware 逻辑.
|
|
108
|
+
const propRef = PropertyControlCapabilityImpl.resolvePropRef(ctx);
|
|
109
|
+
const propType = PropertyControlCapabilityImpl.resolvePropType(ctx);
|
|
110
|
+
// no-op: 仅契约消费. 留出 propRef + propType 给 downstream capability 用.
|
|
111
|
+
void propRef;
|
|
112
|
+
void propType;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
onPropertyReleased(ctx: CapabilityContext): void {
|
|
116
|
+
const propRef = PropertyControlCapabilityImpl.resolvePropRef(ctx);
|
|
117
|
+
const propType = PropertyControlCapabilityImpl.resolvePropType(ctx);
|
|
118
|
+
void propRef;
|
|
119
|
+
void propType;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// 自注册 — 模块被 require 即生效
|
|
124
|
+
CapabilityRegistry.register(PropertyControlCapability);
|