@ives_xxz/framework 1.4.2 → 1.4.4

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/FW.d.ts CHANGED
@@ -34,6 +34,7 @@ declare namespace FW {
34
34
  engineMgr: EngineManager;
35
35
  taskMgr: TaskManager;
36
36
  hotUpdateMgr: HotUpdateManager;
37
+ promiseMgr: PromiseManager;
37
38
  scene: Scene;
38
39
  bundleName: string;
39
40
  getComponent<T>(serviceIdentifier: FW.ServiceIdentifier<T>): T;
@@ -48,6 +49,17 @@ declare namespace FW {
48
49
  onDestroy(): void;
49
50
  };
50
51
 
52
+ type PromiseManager = {
53
+ public execute<T = any>(
54
+ executor: (
55
+ resolve: (value: T | PromiseLike<T>) => void,
56
+ reject: (reason?: any) => void,
57
+ signal: AbortSignal,
58
+ ) => void = PromiseExcutor,
59
+ options: PromiseExecuteOptions = {},
60
+ ): PromiseProxy<T>;
61
+ };
62
+
51
63
  type Registry = {
52
64
  /** 注册 */
53
65
  register(): void;
@@ -1899,6 +1911,52 @@ declare namespace FW {
1899
1911
  message?: string;
1900
1912
  };
1901
1913
 
1914
+ // 颜色分段配置接口
1915
+ type AssemblerLabelColorSegment = {
1916
+ color: cc.Color;
1917
+ position: number;
1918
+ };
1919
+
1920
+ type AssemblerLabelColorConfig = {
1921
+ color: cc.Color;
1922
+ percentages: number;
1923
+ };
1924
+
1925
+ type PromiseProxy<T = Promise> = {
1926
+ id: number;
1927
+ promise: Promise<T>;
1928
+ status: any;
1929
+ abortController: AbortController;
1930
+ abort?: (reason?: any) => void;
1931
+ addAbortEventListener?: (
1932
+ listener: (this: AbortSignal, ev: Event) => any,
1933
+ options?: boolean | AddEventListenerOptions,
1934
+ ) => void;
1935
+ };
1936
+
1937
+ type PromiseExcutor<T> = (
1938
+ resolve: (value: T | PromiseLike<T>) => void,
1939
+ reject: (reason?: any) => void,
1940
+ signal: AbortSignal,
1941
+ ) => void;
1942
+
1943
+ type PromiseExecuteOptions = {
1944
+ timeout?: number;
1945
+ retryCount?: number;
1946
+ retryInterval?: number;
1947
+ retryCondition?: (error: any, retryCount: number) => boolean;
1948
+ };
1949
+
1950
+ type Promise = (resolve: (value: any) => void, reject: (reason?: any) => void) => void;
1951
+
1952
+ type PromiseResult<T = any> = {
1953
+ success: T[];
1954
+ failed: { id: number; reason: any }[];
1955
+ cancelled: number[];
1956
+ };
1957
+
1958
+ type PromiseStatus = 'pending' | 'fulfilled' | 'rejected' | 'cancelled';
1959
+
1902
1960
  declare function timeScale(scale: number);
1903
1961
  declare let Entry: Entry;
1904
1962
  }
@@ -130,4 +130,11 @@ export namespace FWSystemDefine {
130
130
  SPRITE = 'sprite',
131
131
  SKELETON = 'skeleton',
132
132
  }
133
+
134
+ export enum FWPromiseStatus {
135
+ PENDING = 'pending',
136
+ FULFILLED = 'fulfilled',
137
+ REJECTED = 'rejected',
138
+ CANCELLED = 'cancelled',
139
+ }
133
140
  }
package/entry/FWEntry.ts CHANGED
@@ -18,6 +18,7 @@ import { FWEventDefine } from '../define/FWEventDefine';
18
18
  import FWTaskManager from '../manager/FWTaskManager';
19
19
  import FWEngineManager from '../manager/FWEngineManager';
20
20
  import FWHotUpdateManager from '../manager/FWHotUpdateManager';
21
+ import FWPromiseManager from '../manager/FWPromiseManager';
21
22
 
22
23
  /**
23
24
  * 入口脚本
@@ -84,6 +85,10 @@ export default class FWEntry implements FW.Entry {
84
85
  * 热更新管理器
85
86
  */
86
87
  hotUpdateMgr: FW.HotUpdateManager;
88
+ /**
89
+ * promise管理器
90
+ */
91
+ promiseMgr: FW.PromiseManager;
87
92
  /**
88
93
  * 当前Scene
89
94
  */
@@ -113,6 +118,7 @@ export default class FWEntry implements FW.Entry {
113
118
  this.componentMgr = new FWComponentManager();
114
119
  this.languageMgr = new FWLanguageManager();
115
120
  this.hotUpdateMgr = new FWHotUpdateManager();
121
+ this.promiseMgr = new FWPromiseManager();
116
122
 
117
123
  this.resMgr.initialize();
118
124
  this.layerMgr.initialize();
@@ -77,6 +77,9 @@ export class FWAssetManager extends FWManager implements FW.AssetManager {
77
77
  ): Promise<T> {
78
78
  return new Promise((resolve, reject) => {
79
79
  cc.assetManager.loadRemote(url, { cacheEnabled: true, maxRetryCount: 0 }, (err, asset) => {
80
+ if (err || !asset) {
81
+ reject(err);
82
+ }
80
83
  cb?.(asset as T);
81
84
  resolve(asset as T);
82
85
  });
@@ -128,11 +131,8 @@ export class FWAssetManager extends FWManager implements FW.AssetManager {
128
131
 
129
132
  assetData.loaded = false;
130
133
 
131
- return new Promise(
132
- (
133
- resolve: (value: FW.AssetData | PromiseLike<FW.AssetData>) => void,
134
- reject: (reason?: any) => void,
135
- ) => {
134
+ return FW.Entry.promiseMgr.execute(
135
+ (resolve, reject, signal) => {
136
136
  const self = this;
137
137
  bundle.load(
138
138
  path,
@@ -142,8 +142,11 @@ export class FWAssetManager extends FWManager implements FW.AssetManager {
142
142
  },
143
143
  (err: Error, asset: cc.Asset) => {
144
144
  if (err || !asset) {
145
- FWLog.error(`加载资源失败:${asset},请检查!`);
146
- reject(err);
145
+ signal.addEventListener('abort', () => {
146
+ FWLog.error(`加载资源失败:${asset},请检查!`);
147
+ reject(err);
148
+ });
149
+ return;
147
150
  }
148
151
 
149
152
  assetData.loaded = true;
@@ -162,7 +165,11 @@ export class FWAssetManager extends FWManager implements FW.AssetManager {
162
165
  },
163
166
  );
164
167
  },
165
- );
168
+ {
169
+ timeout: 10,
170
+ retryCount: 3,
171
+ },
172
+ ).promise;
166
173
  }
167
174
  /**
168
175
  * 加载文件夹
@@ -0,0 +1,244 @@
1
+ import { FWSystemDefine } from '../define/FWSystemDefine';
2
+ import FWLog from '../log/FWLog';
3
+ import { FWManager } from './FWManager';
4
+
5
+ export default class FWPromiseManager extends FWManager implements FW.PromiseManager {
6
+ private promiseRegistry: Map<number, FW.PromiseProxy>;
7
+ private uniqueId: number = 0;
8
+ private timerSchedule: FW.TimerSchedule;
9
+
10
+ public initialize(): void {
11
+ this.promiseRegistry = new Map<number, FW.PromiseProxy>();
12
+ }
13
+
14
+ public onDestroy(): void {
15
+ this.cancelAll('Manager destroyed');
16
+ this.promiseRegistry.clear();
17
+ this.promiseRegistry = null;
18
+ }
19
+
20
+ /** 创建Promise执行器 */
21
+ public execute<T = any>(
22
+ executor: FW.PromiseExcutor<T>,
23
+ options: FW.PromiseExecuteOptions = {},
24
+ ): FW.PromiseProxy<T> {
25
+ const id = this.uniqueId++;
26
+ const abortController = new AbortController();
27
+ let retryCount = 0;
28
+ let promise: Promise<T>;
29
+ const maxRetryTimes = options.retryCount || 0;
30
+ const retryInterval = options.retryInterval || 0;
31
+ const promiseProxy: FW.PromiseProxy<T> = {
32
+ id,
33
+ promise,
34
+ status: FWSystemDefine.FWPromiseStatus.PENDING,
35
+ abortController,
36
+ };
37
+
38
+ const createPromise = (): Promise<T> => {
39
+ return new Promise<T>((resolve, reject) => {
40
+ if (options.timeout && options.timeout > 0) {
41
+ this.timerSchedule?.unSchedule();
42
+ this.timerSchedule = FW.Entry.timeMgr.scheduleOnce(() => {
43
+ const timeoutError = new Error(`Promise ${id} timeout after ${options.timeout} s`);
44
+ if (
45
+ retryCount < maxRetryTimes &&
46
+ (!options.retryCondition || options.retryCondition(timeoutError, retryCount))
47
+ ) {
48
+ retryCount++;
49
+ FWLog.debug(`Promise ${id} timeout, retrying (${retryCount}/${maxRetryTimes})`);
50
+ if (retryInterval > 0) {
51
+ FW.Entry.timeMgr.scheduleOnce(() => {
52
+ createPromise().then(resolve, reject);
53
+ }, retryInterval);
54
+ } else {
55
+ createPromise().then(resolve, reject);
56
+ }
57
+ } else {
58
+ abortController.abort(timeoutError.message);
59
+ this.timerSchedule?.unSchedule();
60
+ }
61
+ }, options.timeout);
62
+ }
63
+
64
+ const onAbort = () => {
65
+ this.timerSchedule?.unSchedule();
66
+ if (promiseProxy.status === FWSystemDefine.FWPromiseStatus.PENDING) {
67
+ promiseProxy.status = FWSystemDefine.FWPromiseStatus.CANCELLED;
68
+ this.removePromise(id);
69
+ }
70
+ };
71
+
72
+ if (abortController.signal.aborted) {
73
+ onAbort();
74
+ return;
75
+ }
76
+
77
+ abortController.signal.addEventListener('abort', onAbort);
78
+
79
+ const wrappedResolve = (value: T | PromiseLike<T>) => {
80
+ this.timerSchedule?.unSchedule();
81
+ if (promiseProxy.status === FWSystemDefine.FWPromiseStatus.PENDING) {
82
+ promiseProxy.status = FWSystemDefine.FWPromiseStatus.FULFILLED;
83
+ abortController.signal.removeEventListener('abort', onAbort);
84
+ this.removePromise(id);
85
+ resolve(value);
86
+ }
87
+ };
88
+
89
+ const wrappedReject = (reason?: any) => {
90
+ this.timerSchedule?.unSchedule();
91
+ if (
92
+ retryCount < maxRetryTimes &&
93
+ (!options.retryCondition || options.retryCondition(reason, retryCount))
94
+ ) {
95
+ retryCount++;
96
+ FWLog.debug(`Promise ${id} failed, retrying (${retryCount}/${maxRetryTimes}):`, reason);
97
+ if (retryInterval > 0) {
98
+ FW.Entry.timeMgr.scheduleOnce(() => {
99
+ createPromise().then(resolve, reject);
100
+ }, retryInterval);
101
+ } else {
102
+ createPromise().then(resolve, reject);
103
+ }
104
+ } else {
105
+ if (promiseProxy.status === FWSystemDefine.FWPromiseStatus.PENDING) {
106
+ promiseProxy.status = FWSystemDefine.FWPromiseStatus.REJECTED;
107
+ abortController.signal.removeEventListener('abort', onAbort);
108
+ this.removePromise(id);
109
+ reject(reason);
110
+ }
111
+ }
112
+ };
113
+ try {
114
+ executor(wrappedResolve, wrappedReject, abortController.signal);
115
+ } catch (error) {
116
+ wrappedReject(error);
117
+ }
118
+ });
119
+ };
120
+
121
+ promise = createPromise();
122
+ promiseProxy.abort = (reason?: any) => {
123
+ if (promiseProxy.status === FWSystemDefine.FWPromiseStatus.PENDING) {
124
+ FWLog.debug(reason || 'promise cancelled');
125
+ abortController.abort(reason);
126
+ }
127
+ };
128
+ promiseProxy.addAbortEventListener = (
129
+ listener: (this: AbortSignal, ev: Event) => any,
130
+ options?: boolean | AddEventListenerOptions,
131
+ ) => {
132
+ abortController.signal.addEventListener('abort', listener, options);
133
+ };
134
+ this.promiseRegistry.set(id, promiseProxy);
135
+ return promiseProxy;
136
+ }
137
+
138
+ /** 取消指定Promise */
139
+ public cancel(id: number, reason?: any): boolean {
140
+ const promiseProxy = this.promiseRegistry.get(id);
141
+ if (promiseProxy && promiseProxy.status === FWSystemDefine.FWPromiseStatus.PENDING) {
142
+ promiseProxy.abort(reason);
143
+ return true;
144
+ }
145
+ return false;
146
+ }
147
+
148
+ /** 批量取消Promise */
149
+ public cancelMultiple(ids: number[], reason?: any): number[] {
150
+ const cancelled: number[] = [];
151
+ ids.forEach((id) => {
152
+ if (this.cancel(id, reason)) {
153
+ cancelled.push(id);
154
+ }
155
+ });
156
+ return cancelled;
157
+ }
158
+
159
+ /** 取消所有Promise */
160
+ public cancelAll(reason?: any): number[] {
161
+ const cancelled: number[] = [];
162
+ this.promiseRegistry.forEach((promiseProxy, id) => {
163
+ if (promiseProxy.status === FWSystemDefine.FWPromiseStatus.PENDING) {
164
+ promiseProxy.abort(reason);
165
+ cancelled.push(id);
166
+ }
167
+ });
168
+ return cancelled;
169
+ }
170
+
171
+ /** 批量执行Promise并等待所有完成 */
172
+ public async all<T = any>(
173
+ promises: FW.PromiseProxy<T>[],
174
+ options: FW.PromiseExecuteOptions = {},
175
+ ): Promise<FW.PromiseResult<T>> {
176
+ const result: FW.PromiseResult<T> = {
177
+ success: [],
178
+ failed: [],
179
+ cancelled: [],
180
+ };
181
+
182
+ for (const promiseProxy of promises) {
183
+ try {
184
+ const value = await promiseProxy.promise;
185
+ result.success.push(value);
186
+ } catch (error) {
187
+ if (promiseProxy.status === FWSystemDefine.FWPromiseStatus.CANCELLED) {
188
+ result.cancelled.push(promiseProxy.id);
189
+ } else {
190
+ result.failed.push({
191
+ id: promiseProxy.id,
192
+ reason: error,
193
+ });
194
+ }
195
+ }
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ /** 获取Promise状态 */
202
+ public getStatus(id: number): FWSystemDefine.FWPromiseStatus | null {
203
+ const promiseProxy = this.promiseRegistry.get(id);
204
+ return promiseProxy ? promiseProxy.status : null;
205
+ }
206
+
207
+ /** 获取所有Promise状态 */
208
+ public getAllStatus(): Map<number, FWSystemDefine.FWPromiseStatus> {
209
+ const statusMap = new Map<number, FWSystemDefine.FWPromiseStatus>();
210
+ this.promiseRegistry.forEach((promiseProxy, id) => {
211
+ statusMap.set(id, promiseProxy.status);
212
+ });
213
+ return statusMap;
214
+ }
215
+
216
+ /** 移除Promise */
217
+ private removePromise(id: number): void {
218
+ this.promiseRegistry.delete(id);
219
+ }
220
+
221
+ /** 获取正在执行的Promise数量 */
222
+ public getActiveCount(): number {
223
+ return Array.from(this.promiseRegistry.values()).filter(
224
+ (p) => p.status === FWSystemDefine.FWPromiseStatus.PENDING,
225
+ ).length;
226
+ }
227
+
228
+ /** 清理已完成的Promise */
229
+ public clearCompletedPromise(): number {
230
+ const completedIds: number[] = [];
231
+
232
+ this.promiseRegistry.forEach((promiseProxy, id) => {
233
+ if (promiseProxy.status !== FWSystemDefine.FWPromiseStatus.PENDING) {
234
+ completedIds.push(id);
235
+ }
236
+ });
237
+
238
+ completedIds.forEach((id) => {
239
+ this.promiseRegistry.delete(id);
240
+ });
241
+
242
+ return completedIds.length;
243
+ }
244
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "ver": "1.1.0",
3
+ "uuid": "efe0f191-ae87-4773-9dbe-5c66f49670db",
4
+ "importer": "typescript",
5
+ "isPlugin": false,
6
+ "loadPluginInWeb": true,
7
+ "loadPluginInNative": true,
8
+ "loadPluginInEditor": false,
9
+ "subMetas": {}
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ives_xxz/framework",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "cocoscreator 2.x mvc framework",
5
5
  "main": "index.js",
6
6
  "keywords": ["123456"],
@@ -0,0 +1,11 @@
1
+ // Learn TypeScript:
2
+ // - https://docs.cocos.com/creator/manual/en/scripting/typescript.html
3
+ // Learn Attribute:
4
+ // - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html
5
+ // Learn life-cycle callbacks:
6
+ // - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html
7
+
8
+ const { ccclass, property } = cc._decorator;
9
+
10
+ @ccclass
11
+ export default class FWAssembler extends cc['Assembler'] {}
@@ -0,0 +1,10 @@
1
+ {
2
+ "ver": "1.1.0",
3
+ "uuid": "3b9ad8cf-0317-4163-bda0-17c6f4e86028",
4
+ "importer": "typescript",
5
+ "isPlugin": false,
6
+ "loadPluginInWeb": true,
7
+ "loadPluginInNative": true,
8
+ "loadPluginInEditor": false,
9
+ "subMetas": {}
10
+ }
@@ -0,0 +1,620 @@
1
+ import { FWLodash } from '../utils/FWLodash';
2
+
3
+ const { ccclass, property, executeInEditMode, menu } = cc._decorator;
4
+
5
+ @ccclass('AssemblerLabelColorConfig')
6
+ class AssemblerLabelColorConfig implements FW.AssemblerLabelColorConfig {
7
+ @property({ displayName: '颜色' })
8
+ color: cc.Color = cc.Color.WHITE;
9
+
10
+ @property({ type: cc.Float, min: 0, max: 1, displayName: '占比', slide: true })
11
+ percentages: number = 0;
12
+ }
13
+
14
+ @ccclass
15
+ @executeInEditMode
16
+ @menu('CustomComponent/FWRenderAssembler')
17
+ export default class FWRenderAssembler extends cc.Component {
18
+ @property({ displayName: '文本自定义渐变色' })
19
+ enableLabelCustomColors: boolean = false;
20
+
21
+ @property
22
+ _gradientDirection: number = 0;
23
+ @property({
24
+ type: cc.Enum({ 从左到右: 0, 从上到下: 1 }),
25
+ displayName: '渐变方向',
26
+ visible() {
27
+ return this.enableLabelCustomColors;
28
+ },
29
+ })
30
+ get gradientDirection() {
31
+ return this._gradientDirection;
32
+ }
33
+
34
+ set gradientDirection(gradientDirection: number) {
35
+ this._gradientDirection = gradientDirection;
36
+ if (CC_EDITOR) {
37
+ this.adjustLabel();
38
+ this.onPropertyChanged();
39
+ }
40
+ }
41
+
42
+ @property
43
+ _config: AssemblerLabelColorConfig[] = [];
44
+
45
+ @property({
46
+ type: [AssemblerLabelColorConfig],
47
+ visible() {
48
+ return this.enableLabelCustomColors;
49
+ },
50
+ displayName: '颜色配置',
51
+ })
52
+ get config() {
53
+ return this._config;
54
+ }
55
+ set config(config: AssemblerLabelColorConfig[]) {
56
+ this._config = config;
57
+ if (CC_EDITOR) {
58
+ this.adjustLabel();
59
+ this.onPropertyChanged();
60
+ }
61
+ }
62
+
63
+ private _horizontalAlign: number;
64
+ private _verticalAlign: number;
65
+ private _originalRenderData: any = null;
66
+ private _originalUpdateRenderData: Function = null;
67
+ private _originalFillBuffers: Function = null;
68
+ private _isPatched: boolean = false;
69
+ private _colors: cc.Color[] = [];
70
+ private _percentages: number[] = [];
71
+ private _isOriginalDataSaved: boolean = false;
72
+ private _cachedProperties: any;
73
+
74
+ protected start(): void {
75
+ this.adjustLabel();
76
+ this.config.forEach((v) => {
77
+ this._colors.push(v.color);
78
+ this._percentages.push(v.percentages);
79
+ });
80
+
81
+ if (!CC_EDITOR) {
82
+ cc.director.on(cc.Director.EVENT_AFTER_DRAW, this.onUpdateAssembler, this);
83
+ } else {
84
+ this.node.on(cc.Node.EventType.SIZE_CHANGED, this.onRefreshEditor, this);
85
+ this.node.on(cc.Node.EventType.COLOR_CHANGED, this.onRefreshEditor, this);
86
+ this.node.on(cc.Node.EventType.ANCHOR_CHANGED, this.onRefreshEditor, this);
87
+ setTimeout(() => {
88
+ this.cacheLabelProperties();
89
+ this.onPropertyChanged();
90
+ });
91
+ }
92
+ }
93
+
94
+ onRefreshEditor() {
95
+ if (CC_EDITOR && this.enableLabelCustomColors) {
96
+ this._originalRenderData = null;
97
+ this._isOriginalDataSaved = false;
98
+ this.node.label['_assembler'] = null;
99
+ this.node.active = false;
100
+ this.node.active = true;
101
+ setTimeout(() => {
102
+ this.onPropertyChanged();
103
+ }, 30);
104
+ }
105
+ }
106
+
107
+ private refreshGradient(): void {
108
+ this._colors = [];
109
+ this._percentages = [];
110
+ this.config.forEach((v) => {
111
+ this._colors.push(v.color);
112
+ this._percentages.push(v.percentages);
113
+ });
114
+
115
+ this._isPatched = false;
116
+ this.onUpdateAssembler();
117
+ }
118
+
119
+ private cacheLabelProperties(): void {
120
+ const label = this.node.label;
121
+ if (!label) {
122
+ this._cachedProperties = null;
123
+ return;
124
+ }
125
+
126
+ this._cachedProperties = {
127
+ horizontalAlign: label.horizontalAlign,
128
+ verticalAlign: label.verticalAlign,
129
+ overflow: label.overflow,
130
+ enableWrap: label.enableWrapText,
131
+ fontFamily: label.fontFamily,
132
+ font: label.font ? label.font['_uuid'] : null,
133
+ underlineHeight: label.underlineHeight,
134
+ cacheMode: label.cacheMode,
135
+ };
136
+ }
137
+
138
+ private checkLabelPropertiesChanged(): boolean {
139
+ const label = this.node.label;
140
+ if (!label || !this._cachedProperties) {
141
+ return false;
142
+ }
143
+
144
+ const current = {
145
+ horizontalAlign: label.horizontalAlign,
146
+ verticalAlign: label.verticalAlign,
147
+ overflow: label.overflow,
148
+ enableWrap: label.enableWrapText,
149
+ underlineHeight: label.underlineHeight,
150
+ fontFamily: label.fontFamily,
151
+ cacheMode: label.cacheMode,
152
+ font: label.font ? label.font['_uuid'] : null,
153
+ };
154
+
155
+ const keys = Object.keys(current);
156
+ for (let key of keys) {
157
+ if (current[key] !== this._cachedProperties[key]) {
158
+ this._cachedProperties = current;
159
+ return true;
160
+ }
161
+ }
162
+
163
+ return false;
164
+ }
165
+
166
+ protected onPropertyChanged(): void {
167
+ if (CC_EDITOR && this.enableLabelCustomColors) {
168
+ this.restoreOriginalAssembler();
169
+ this.refreshGradient();
170
+ }
171
+ }
172
+
173
+ protected onDisable(): void {
174
+ this.restoreOriginalAssembler();
175
+ }
176
+
177
+ private adjustLabel(): void {
178
+ if (CC_EDITOR && !this.node.label) {
179
+ this.node.label = this.node.getComponent(cc.Label);
180
+ }
181
+ if (this.node.label) {
182
+ if (this.gradientDirection === 1) {
183
+ this.node.label.lineHeight = this.node.label.fontSize * 0.8;
184
+ }
185
+
186
+ if (!this._horizontalAlign) {
187
+ this._horizontalAlign = this.node.label.horizontalAlign;
188
+ }
189
+
190
+ if (!this._verticalAlign) {
191
+ this._verticalAlign = this.node.label.verticalAlign;
192
+ }
193
+ }
194
+ }
195
+
196
+ protected update(dt: number): void {
197
+ if (!CC_EDITOR) return;
198
+ if (this.checkLabelPropertiesChanged()) {
199
+ setTimeout(() => {
200
+ this.adjustLabel();
201
+ this.onRefreshEditor();
202
+ }, 100);
203
+ }
204
+ }
205
+
206
+ private onUpdateAssembler(): void {
207
+ if (this.enableLabelCustomColors && this.node.label) {
208
+ this.patchAssembler();
209
+ } else {
210
+ this.restoreOriginalAssembler();
211
+ }
212
+ }
213
+
214
+ private patchAssembler(): void {
215
+ const label = this.node.label;
216
+ if (!label || !label['_assembler'] || this._isPatched) return;
217
+
218
+ const assembler = label['_assembler'];
219
+
220
+ if (!this._isOriginalDataSaved) {
221
+ this._originalRenderData = FWLodash.cloneDeep(assembler);
222
+ this._isOriginalDataSaved = true;
223
+ }
224
+
225
+ const colorSegments = this.createColorSegments();
226
+ if (!colorSegments) return;
227
+
228
+ if (!this._originalUpdateRenderData) {
229
+ this._originalUpdateRenderData = assembler.updateRenderData;
230
+ }
231
+ if (!this._originalFillBuffers) {
232
+ this._originalFillBuffers = assembler.fillBuffers;
233
+ }
234
+
235
+ assembler.updateRenderData = () => {
236
+ if (this._originalUpdateRenderData) {
237
+ this._originalUpdateRenderData.call(assembler, label);
238
+ }
239
+ this.applyMultiColorGradient(assembler, colorSegments);
240
+ };
241
+
242
+ assembler.fillBuffers = (renderer: any, node: any) => {
243
+ this.fillGradientBuffers(renderer, node, assembler);
244
+ };
245
+ if (colorSegments.length > 2) {
246
+ }
247
+
248
+ this._isPatched = true;
249
+ assembler.updateRenderData();
250
+ }
251
+
252
+ private restoreOriginalAssembler(): void {
253
+ if (!this.node.label || !this._isOriginalDataSaved) return;
254
+
255
+ const label = this.node.label;
256
+ if (label['_assembler']) {
257
+ const assembler = label['_assembler'];
258
+
259
+ if (this._originalUpdateRenderData) {
260
+ assembler.updateRenderData = this._originalUpdateRenderData;
261
+ }
262
+ if (this._originalFillBuffers) {
263
+ assembler.fillBuffers = this._originalFillBuffers;
264
+ }
265
+
266
+ if (this._originalRenderData) {
267
+ label['_assembler'] = FWLodash.cloneDeep(this._originalRenderData);
268
+ }
269
+ }
270
+
271
+ this._isPatched = false;
272
+ }
273
+
274
+ private applyMultiColorGradient(
275
+ assembler: any,
276
+ colorSegments: FW.AssemblerLabelColorSegment[],
277
+ ): void {
278
+ const renderData = assembler._renderData;
279
+ const originalUintVData = renderData.uintVDatas[0];
280
+ const charsCount = originalUintVData.length / 20;
281
+ const segmentsCount = colorSegments.length - 1;
282
+ const useVertical = this.gradientDirection === 1;
283
+
284
+ const verticesPerChar = segmentsCount * 4;
285
+ const newVertexCount = charsCount * verticesPerChar;
286
+ const newUintVData = new Uint32Array(newVertexCount * 5);
287
+
288
+ let writeIndex = 0;
289
+
290
+ for (let c = 0; c < charsCount; c++) {
291
+ const base = c * 20;
292
+ const vertices = this.getCharacterVertices(originalUintVData, base);
293
+ const bounds = this.calculateBounds(vertices);
294
+
295
+ for (let s = 0; s < segmentsCount; s++) {
296
+ const segment = colorSegments[s];
297
+ const nextSegment = colorSegments[s + 1];
298
+
299
+ if (useVertical) {
300
+ this.createVerticalSegment(
301
+ newUintVData,
302
+ writeIndex,
303
+ vertices,
304
+ bounds,
305
+ segment,
306
+ nextSegment,
307
+ );
308
+ } else {
309
+ this.createHorizontalSegment(
310
+ newUintVData,
311
+ writeIndex,
312
+ vertices,
313
+ bounds,
314
+ segment,
315
+ nextSegment,
316
+ );
317
+ }
318
+ writeIndex += 4;
319
+ }
320
+ }
321
+
322
+ this.updateRenderData(renderData, newUintVData, charsCount, segmentsCount);
323
+ }
324
+
325
+ private getCharacterVertices(uintVData: Uint32Array, baseIndex: number): any[] {
326
+ const vertices = [];
327
+ for (let i = 0; i < 4; i++) {
328
+ const idx = baseIndex + i * 5;
329
+ vertices.push({
330
+ x: this.uintToFloat(uintVData[idx]),
331
+ y: this.uintToFloat(uintVData[idx + 1]),
332
+ u: this.uintToFloat(uintVData[idx + 2]),
333
+ v: this.uintToFloat(uintVData[idx + 3]),
334
+ });
335
+ }
336
+ return vertices;
337
+ }
338
+
339
+ private calculateBounds(vertices: any[]): any {
340
+ const xs = vertices.map((v) => v.x);
341
+ const ys = vertices.map((v) => v.y);
342
+ return {
343
+ minX: Math.min(...xs),
344
+ maxX: Math.max(...xs),
345
+ minY: Math.min(...ys),
346
+ maxY: Math.max(...ys),
347
+ width: Math.max(...xs) - Math.min(...xs),
348
+ height: Math.max(...ys) - Math.min(...ys),
349
+ };
350
+ }
351
+
352
+ private createHorizontalSegment(
353
+ newUintVData: Uint32Array,
354
+ writeIndex: number,
355
+ vertices: any[],
356
+ bounds: any,
357
+ segment: any,
358
+ nextSegment: any,
359
+ ): void {
360
+ const startX = bounds.minX + bounds.width * segment.position;
361
+ const endX = bounds.minX + bounds.width * nextSegment.position;
362
+ const startU = vertices[0].u + (vertices[1].u - vertices[0].u) * segment.position;
363
+ const endU = vertices[0].u + (vertices[1].u - vertices[0].u) * nextSegment.position;
364
+
365
+ this.setVertexData(
366
+ newUintVData,
367
+ writeIndex,
368
+ startX,
369
+ bounds.minY,
370
+ startU,
371
+ vertices[0].v,
372
+ segment.color._val,
373
+ );
374
+ this.setVertexData(
375
+ newUintVData,
376
+ writeIndex + 1,
377
+ endX,
378
+ bounds.minY,
379
+ endU,
380
+ vertices[1].v,
381
+ nextSegment.color._val,
382
+ );
383
+ this.setVertexData(
384
+ newUintVData,
385
+ writeIndex + 2,
386
+ startX,
387
+ bounds.maxY,
388
+ startU,
389
+ vertices[2].v,
390
+ segment.color._val,
391
+ );
392
+ this.setVertexData(
393
+ newUintVData,
394
+ writeIndex + 3,
395
+ endX,
396
+ bounds.maxY,
397
+ endU,
398
+ vertices[3].v,
399
+ nextSegment.color._val,
400
+ );
401
+ }
402
+
403
+ private createVerticalSegment(
404
+ newUintVData: Uint32Array,
405
+ writeIndex: number,
406
+ vertices: any[],
407
+ bounds: any,
408
+ segment: any,
409
+ nextSegment: any,
410
+ ): void {
411
+ const startY = bounds.minY + bounds.height * (1 - segment.position);
412
+ const endY = bounds.minY + bounds.height * (1 - nextSegment.position);
413
+
414
+ const startVL = vertices[0].v + (vertices[2].v - vertices[0].v) * (1 - segment.position);
415
+ const endVL = vertices[0].v + (vertices[2].v - vertices[0].v) * (1 - nextSegment.position);
416
+ const startVR = vertices[1].v + (vertices[3].v - vertices[1].v) * (1 - segment.position);
417
+ const endVR = vertices[1].v + (vertices[3].v - vertices[1].v) * (1 - nextSegment.position);
418
+
419
+ this.setVertexData(
420
+ newUintVData,
421
+ writeIndex,
422
+ bounds.minX,
423
+ startY,
424
+ vertices[0].u,
425
+ startVL,
426
+ segment.color._val,
427
+ );
428
+ this.setVertexData(
429
+ newUintVData,
430
+ writeIndex + 1,
431
+ bounds.maxX,
432
+ startY,
433
+ vertices[1].u,
434
+ startVR,
435
+ segment.color._val,
436
+ );
437
+ this.setVertexData(
438
+ newUintVData,
439
+ writeIndex + 2,
440
+ bounds.minX,
441
+ endY,
442
+ vertices[2].u,
443
+ endVL,
444
+ nextSegment.color._val,
445
+ );
446
+ this.setVertexData(
447
+ newUintVData,
448
+ writeIndex + 3,
449
+ bounds.maxX,
450
+ endY,
451
+ vertices[3].u,
452
+ endVR,
453
+ nextSegment.color._val,
454
+ );
455
+ }
456
+
457
+ private setVertexData(
458
+ data: Uint32Array,
459
+ index: number,
460
+ x: number,
461
+ y: number,
462
+ u: number,
463
+ v: number,
464
+ color: number,
465
+ ): void {
466
+ const base = index * 5;
467
+ data[base] = this.floatToUint(x);
468
+ data[base + 1] = this.floatToUint(y);
469
+ data[base + 2] = this.floatToUint(u);
470
+ data[base + 3] = this.floatToUint(v);
471
+ data[base + 4] = color;
472
+ }
473
+
474
+ private updateRenderData(
475
+ renderData: any,
476
+ newUintVData: Uint32Array,
477
+ charsCount: number,
478
+ segmentsCount: number,
479
+ ): void {
480
+ renderData.uintVDatas[0] = newUintVData;
481
+ renderData.iDatas[0] = this.createQuadMeshIndices(charsCount, segmentsCount);
482
+ renderData.vertexCount = newUintVData.length / 5;
483
+
484
+ const newVData = new Float32Array(renderData.vertexCount * 5);
485
+ for (let i = 0; i < newVData.length; i++) {
486
+ newVData[i] = this.uintToFloat(newUintVData[i]);
487
+ }
488
+ renderData.vDatas[0] = newVData;
489
+ }
490
+
491
+ private createQuadMeshIndices(charsCount: number, segmentsCount: number): Uint16Array {
492
+ const indicesPerChar = segmentsCount * 6;
493
+ const totalIndices = charsCount * indicesPerChar;
494
+ const indices = new Uint16Array(totalIndices);
495
+ let idx = 0;
496
+
497
+ for (let charIndex = 0; charIndex < charsCount; charIndex++) {
498
+ const vertexOffset = charIndex * segmentsCount * 4;
499
+
500
+ for (let segIndex = 0; segIndex < segmentsCount; segIndex++) {
501
+ const base = vertexOffset + segIndex * 4;
502
+ indices[idx++] = base;
503
+ indices[idx++] = base + 1;
504
+ indices[idx++] = base + 2;
505
+ indices[idx++] = base + 1;
506
+ indices[idx++] = base + 3;
507
+ indices[idx++] = base + 2;
508
+ }
509
+ }
510
+
511
+ return indices;
512
+ }
513
+
514
+ private getLabelBounds(uintVData: Uint32Array): any {
515
+ let minX = Number.MAX_VALUE,
516
+ maxX = -Number.MAX_VALUE;
517
+ let minY = Number.MAX_VALUE,
518
+ maxY = -Number.MAX_VALUE;
519
+
520
+ for (let i = 0; i < uintVData.length; i += 5) {
521
+ const x = this.uintToFloat(uintVData[i]);
522
+ const y = this.uintToFloat(uintVData[i + 1]);
523
+ minX = Math.min(minX, x);
524
+ maxX = Math.max(maxX, x);
525
+ minY = Math.min(minY, y);
526
+ maxY = Math.max(maxY, y);
527
+ }
528
+
529
+ return {
530
+ minX,
531
+ maxX,
532
+ minY,
533
+ maxY,
534
+ width: Math.max(maxX - minX, 1),
535
+ height: Math.max(maxY - minY, 1),
536
+ };
537
+ }
538
+
539
+ private fillGradientBuffers(renderer: any, node: any, assembler: any): void {
540
+ const renderData = assembler._renderData;
541
+ if (!renderData?.vDatas?.[0] || !renderData?.iDatas?.[0]) return;
542
+
543
+ const buffer = node._buffer;
544
+ const offsetInfo = buffer.request(renderData.vertexCount, renderData.iDatas[0].length);
545
+
546
+ const vertexOffset = offsetInfo.byteOffset >> 2;
547
+ buffer._vData.set(renderData.vDatas[0], vertexOffset);
548
+
549
+ const ibuf = buffer._iData;
550
+ const vertexId = offsetInfo.vertexOffset;
551
+
552
+ renderData.iDatas[0].forEach((index, i) => {
553
+ ibuf[offsetInfo.indiceOffset + i] = vertexId + index;
554
+ });
555
+ }
556
+
557
+ private createColorSegments(): FW.AssemblerLabelColorSegment[] | null {
558
+ if (!this._colors || this._colors.length < 2) return null;
559
+
560
+ const percentages = this.calculatePercentages();
561
+ const segments: FW.AssemblerLabelColorSegment[] = [];
562
+
563
+ for (let i = 0; i < this._colors.length; i++) {
564
+ segments.push({
565
+ color: this.cloneColor(this._colors[i]),
566
+ position: percentages[i],
567
+ });
568
+ }
569
+
570
+ return segments;
571
+ }
572
+
573
+ private calculatePercentages(): number[] {
574
+ if (this._percentages?.length === this._colors.length) {
575
+ const percentages = [...this._percentages];
576
+ percentages[0] = 0;
577
+ percentages[percentages.length - 1] = 1;
578
+
579
+ for (let i = 1; i < percentages.length - 1; i++) {
580
+ if (percentages[i] <= percentages[i - 1]) {
581
+ percentages[i] = percentages[i - 1] + 0.001;
582
+ }
583
+ if (percentages[i] >= percentages[i + 1]) {
584
+ percentages[i] = (percentages[i - 1] + percentages[i + 1]) / 2;
585
+ }
586
+ }
587
+ return percentages;
588
+ }
589
+
590
+ return Array.from({ length: this._colors.length }, (_, i) => i / (this._colors.length - 1));
591
+ }
592
+
593
+ private cloneColor(color: cc.Color): cc.Color {
594
+ return color.clone ? color.clone() : cc.color(color.r, color.g, color.b, color.a);
595
+ }
596
+
597
+ private interpolateColor(color1: cc.Color, color2: cc.Color, progress: number): cc.Color {
598
+ const r = Math.round(color1.r + (color2.r - color1.r) * progress);
599
+ const g = Math.round(color1.g + (color2.g - color1.g) * progress);
600
+ const b = Math.round(color1.b + (color2.b - color1.b) * progress);
601
+ const a = Math.round(color1.a + (color2.a - color1.a) * progress);
602
+ return cc.color(r, g, b, a);
603
+ }
604
+
605
+ private uintToFloat(uintValue: number): number {
606
+ const buffer = new ArrayBuffer(4);
607
+ const uintView = new Uint32Array(buffer);
608
+ const floatView = new Float32Array(buffer);
609
+ uintView[0] = uintValue;
610
+ return floatView[0];
611
+ }
612
+
613
+ private floatToUint(floatValue: number): number {
614
+ const buffer = new ArrayBuffer(4);
615
+ const floatView = new Float32Array(buffer);
616
+ const uintView = new Uint32Array(buffer);
617
+ floatView[0] = floatValue;
618
+ return uintView[0];
619
+ }
620
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "ver": "1.1.0",
3
+ "uuid": "040c0953-9ad0-4720-8e60-e3e15bd539d4",
4
+ "importer": "typescript",
5
+ "isPlugin": false,
6
+ "loadPluginInWeb": true,
7
+ "loadPluginInNative": true,
8
+ "loadPluginInEditor": false,
9
+ "subMetas": {}
10
+ }
package/render.meta ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "ver": "1.1.3",
3
+ "uuid": "d49ee3cc-a25e-4c14-8a04-5f22d03b8164",
4
+ "importer": "folder",
5
+ "isBundle": false,
6
+ "bundleName": "",
7
+ "priority": 1,
8
+ "compressionType": {},
9
+ "optimizeHotUpdate": {},
10
+ "inlineSpriteFrames": {},
11
+ "isRemoteBundle": {},
12
+ "subMetas": {}
13
+ }