@kurot/core 2.0.0 → 2.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.
@@ -0,0 +1,333 @@
1
+ # 资源管理器 (Resource Manager)
2
+
3
+ ## 概述
4
+
5
+ Kurot 资源管理器是从 Egret RES 模块现代化迁移而来的异步资源加载与缓存系统。它提供:
6
+
7
+ - **async/await API** — 所有加载操作返回 Promise
8
+ - **类型安全** — `ResourceType` 枚举 + 泛型 `get<T>()`
9
+ - **配置驱动** — 兼容 Egret `resource.json` 格式
10
+ - **按组加载** — 并发控制(默认 2 线程)、自动重试(默认 3 次)
11
+ - **可扩展** — 自定义 Analyzer 支持新资源类型
12
+ - **零外部依赖** — 仅依赖 core 内部的 `net/` 和 `events/`
13
+
14
+ ## 目录结构
15
+
16
+ ```
17
+ packages/core/src/kurot/resource/
18
+ ├── index.ts — 统一导出
19
+ ├── Resource.ts — 资源管理器主类(单例 + 便捷实例)
20
+ ├── ResourceConfig.ts — 配置文件解析
21
+ ├── ResourceLoader.ts — 队列加载器(并发 + 重试)
22
+ ├── ResourceItem.ts — 资源项定义 + ResourceType 枚举
23
+ ├── ResourceEvent.ts — 事件类型定义
24
+ └── analyzers/
25
+ ├── index.ts
26
+ ├── AnalyzerBase.ts — 解析器抽象基类
27
+ ├── ImageAnalyzer.ts — 图片 → Texture
28
+ ├── JsonAnalyzer.ts — JSON 文件 → object
29
+ ├── TextAnalyzer.ts — 纯文本 → string
30
+ ├── SoundAnalyzer.ts — 音频 → HTMLAudioElement
31
+ └── SheetAnalyzer.ts — 精灵表 JSON + 图片 → SpriteSheet
32
+ ```
33
+
34
+ ## 快速开始
35
+
36
+ ### 1. 准备配置文件
37
+
38
+ ```json
39
+ // resource.json
40
+ {
41
+ "resources": [
42
+ { "name": "bg", "type": "image", "url": "assets/bg.png" },
43
+ { "name": "config", "type": "json", "url": "config.json" },
44
+ { "name": "hero", "type": "sheet", "url": "hero.json" }
45
+ ],
46
+ "groups": [
47
+ { "name": "preload", "keys": "bg,config" },
48
+ { "name": "game", "keys": "hero" }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ ### 2. 加载与使用
54
+
55
+ ```typescript
56
+ import { resource, ResourceType } from '@kurot/core';
57
+
58
+ // 加载配置
59
+ await resource.loadConfig('resource.json', 'assets/');
60
+
61
+ // 按组加载
62
+ await resource.loadGroup('preload');
63
+
64
+ // 获取缓存的资源(泛型)
65
+ const texture = resource.get<Texture>('bg');
66
+
67
+ // 异步加载单个资源
68
+ const data = await resource.load<object>('config');
69
+
70
+ // 监听加载进度
71
+ await resource.loadGroup('game', 0, (loaded, total) => {
72
+ console.log(`进度: ${loaded}/${total}`);
73
+ });
74
+ ```
75
+
76
+ ## API 参考
77
+
78
+ ### Resource 主类
79
+
80
+ #### 配置
81
+
82
+ | 方法 | 说明 |
83
+ | -------------------------- | -------------------------------------------------- |
84
+ | `loadConfig(url, folder?)` | 加载并解析 resource.json,`folder` 为资源 URL 前缀 |
85
+ | `addResource(def)` | 动态添加资源定义 `{ name, url, type }` |
86
+
87
+ #### 加载
88
+
89
+ | 方法 | 说明 |
90
+ | ----------------------------------------- | -------------------------------------------- |
91
+ | `loadGroup(name, priority?, onProgress?)` | 按组加载,返回 Promise。已加载的资源自动跳过 |
92
+ | `load<T>(name)` | 异步加载单个资源,返回缓存数据 |
93
+
94
+ #### 缓存
95
+
96
+ | 方法 | 说明 |
97
+ | ----------------- | ---------------------------------------- |
98
+ | `get<T>(name)` | 同步获取缓存资源,未加载返回 `undefined` |
99
+ | `hasRes(name)` | 检查资源是否已缓存 |
100
+ | `hasGroup(name)` | 检查组是否存在 |
101
+ | `getGroupNames()` | 获取所有组名 |
102
+
103
+ #### 销毁
104
+
105
+ | 方法 | 说明 |
106
+ | -------------------- | ---------------------- |
107
+ | `destroy(name)` | 销毁单个资源,释放缓存 |
108
+ | `destroyGroup(name)` | 销毁整组资源 |
109
+ | `destroyAll()` | 销毁所有已加载资源 |
110
+
111
+ #### 事件
112
+
113
+ | 方法 | 说明 |
114
+ | ---------------------- | -------------------------------------- |
115
+ | `on(type, listener)` | 监听资源事件 |
116
+ | `off(type, listener)` | 移除事件监听 |
117
+ | `onProgress(callback)` | 便捷进度监听 `(loaded, total) => void` |
118
+
119
+ #### 扩展
120
+
121
+ | 方法 | 说明 |
122
+ | ---------------------------------- | ------------------------ |
123
+ | `registerAnalyzer(type, analyzer)` | 注册自定义资源类型解析器 |
124
+
125
+ ### ResourceType 枚举
126
+
127
+ | 值 | 说明 |
128
+ | --------- | --------------------------------- |
129
+ | `'image'` | 图片资源,生成 `Texture` |
130
+ | `'json'` | JSON 文件,解析为 `object` |
131
+ | `'text'` | 纯文本文件,返回 `string` |
132
+ | `'sound'` | 音频文件,返回 `HTMLAudioElement` |
133
+ | `'sheet'` | 精灵表,返回 `SpriteSheet` |
134
+
135
+ ### ResourceEventType 事件类型
136
+
137
+ | 事件 | 触发时机 |
138
+ | ------------------- | ------------------------ |
139
+ | `CONFIG_COMPLETE` | 配置文件加载完成 |
140
+ | `CONFIG_LOAD_ERROR` | 配置文件加载失败 |
141
+ | `GROUP_COMPLETE` | 资源组全部加载完成 |
142
+ | `GROUP_PROGRESS` | 资源组中单个资源加载完成 |
143
+ | `GROUP_LOAD_ERROR` | 资源组中有资源加载失败 |
144
+ | `ITEM_LOAD_ERROR` | 单个资源加载失败 |
145
+
146
+ ### ResourceEvent 接口
147
+
148
+ ```typescript
149
+ interface ResourceEvent {
150
+ type: ResourceEventType;
151
+ groupName: string;
152
+ item?: ResourceItem;
153
+ itemsLoaded: number;
154
+ itemsTotal: number;
155
+ }
156
+ ```
157
+
158
+ ## 内置解析器
159
+
160
+ ### ImageAnalyzer
161
+
162
+ 加载图片并创建 `Texture` 对象。销毁时自动调用 `texture.dispose()`。
163
+
164
+ ```typescript
165
+ const texture = resource.get<Texture>('bg');
166
+ // texture 可直接用于 Bitmap 显示对象
167
+ ```
168
+
169
+ ### JsonAnalyzer
170
+
171
+ 通过 `HttpRequest` 加载 JSON 文件并解析为对象。
172
+
173
+ ```typescript
174
+ const config = resource.get<Record<string, unknown>>('config');
175
+ ```
176
+
177
+ ### TextAnalyzer
178
+
179
+ 通过 `HttpRequest` 加载纯文本文件。
180
+
181
+ ```typescript
182
+ const text = resource.get<string>('readme');
183
+ ```
184
+
185
+ ### SoundAnalyzer
186
+
187
+ 加载音频文件,返回 `HTMLAudioElement`。销毁时自动 `pause()` 并释放 `src`。
188
+
189
+ ```typescript
190
+ const audio = resource.get<HTMLAudioElement>('bgm');
191
+ audio.play();
192
+ ```
193
+
194
+ ### SheetAnalyzer
195
+
196
+ 加载精灵表(TexturePacker JSON 格式),两步加载:
197
+
198
+ 1. 加载 JSON 配置获取帧信息和图片路径
199
+ 2. 加载关联图片,切割为子纹理
200
+
201
+ 支持三种获取方式:
202
+
203
+ ```typescript
204
+ // 获取整个 SpriteSheet
205
+ const sheet = resource.get<SpriteSheet>('hero');
206
+
207
+ // 通过子键名直接获取子纹理
208
+ const frame = resource.get<Texture>('hero_idle_01');
209
+
210
+ // 通过点号语法获取
211
+ const frame2 = resource.get<Texture>('hero.idle_01');
212
+ ```
213
+
214
+ ## 自定义解析器
215
+
216
+ 继承 `AnalyzerBase` 实现自定义资源类型:
217
+
218
+ ```typescript
219
+ import { AnalyzerBase } from '@kurot/core';
220
+ import { ResourceItem } from '@kurot/core';
221
+
222
+ class XmlAnalyzer extends AnalyzerBase {
223
+ public loadFile(item: ResourceItem): Promise<ResourceItem> {
224
+ if (this.fileDic.has(item.name)) {
225
+ item.loaded = true;
226
+ return Promise.resolve(item);
227
+ }
228
+
229
+ return fetch(item.url)
230
+ .then(res => res.text())
231
+ .then(text => {
232
+ const parser = new DOMParser();
233
+ const doc = parser.parseFromString(text, 'text/xml');
234
+ this.fileDic.set(item.name, doc);
235
+ item.loaded = true;
236
+ return item;
237
+ })
238
+ .catch(() => {
239
+ item.loaded = false;
240
+ return item;
241
+ });
242
+ }
243
+ }
244
+
245
+ // 注册
246
+ resource.registerAnalyzer('xml', new XmlAnalyzer());
247
+ ```
248
+
249
+ ## 队列加载器 (ResourceLoader)
250
+
251
+ `ResourceLoader` 是内部使用的队列加载器,支持:
252
+
253
+ - **并发控制** — `threadCount`(默认 2)限制同时加载数
254
+ - **自动重试** — `retryCount`(默认 3)次重试后报告失败
255
+ - **回调通知** — `onComplete` / `onError` / `onProgress`
256
+
257
+ 通常不需要直接使用 `ResourceLoader`,通过 `Resource.loadGroup()` 即可间接使用。
258
+
259
+ ## 配置文件格式
260
+
261
+ ### 标准格式(2.0.0)
262
+
263
+ ```json
264
+ {
265
+ "resources": [
266
+ {
267
+ "name": "资源唯一标识",
268
+ "type": "image|json|text|sound|sheet",
269
+ "url": "相对或绝对路径",
270
+ "subkeys": { "frameName": { "scale9grid": "1,1,2,2" }, "ordinaryFrame": {} }
271
+ }
272
+ ],
273
+ "groups": [
274
+ {
275
+ "name": "组名",
276
+ "keys": "逗号分隔的资源名称"
277
+ }
278
+ ]
279
+ }
280
+ ```
281
+
282
+ 当前源码的 `subkeys` 为对象,旧字符串需要在 Editor 中显式刷新图集后再使用。
283
+ `scale9grid` 是 Image 编译默认值,Core 不会自动把它应用到任意 Bitmap。
284
+ 参见 [资源默认九宫格契约](../../ui-document/docs/resource-nine-slice.md)。
285
+
286
+ ### URL 解析规则
287
+
288
+ - 包含 `://` 的 URL 视为绝对路径,不做处理
289
+ - 其他 URL 会在前面拼接 `loadConfig()` 的 `folder` 参数
290
+
291
+ ### 动态创建组
292
+
293
+ ```typescript
294
+ import { resource, ResourceConfig } from '@kurot/core';
295
+
296
+ // 通过 ResourceConfig 的 createGroup 方法
297
+ // 注意:这是底层 API,通常通过配置文件定义组
298
+ ```
299
+
300
+ ## 与 Egret RES 的对比
301
+
302
+ | 维度 | Egret RES | Kurot Resource |
303
+ | ---------- | ----------------------- | ----------------------------------- |
304
+ | API 风格 | 回调 `compFunc(data)` | `async/await` + Promise |
305
+ | 模块化 | 全局 `RES.xxx` | ES Module 导入 |
306
+ | 事件系统 | `egret.EventDispatcher` | 轻量 `on/off` 模式 |
307
+ | 类型安全 | 大量 `any` | 泛型 `get<T>()` |
308
+ | 版本控制 | `VersionController` | 剔除(构建工具处理) |
309
+ | XML 解析 | 内置 | 剔除(KUI XML compiler 处理) |
310
+ | 国际化 | 内置 | 剔除(不属于资源加载器职责) |
311
+ | 解析器类型 | 8 种 | 5 种(Image/Json/Text/Sound/Sheet) |
312
+ | 代码量 | ~3,000 行 | ~1,400 行 |
313
+
314
+ ### 剔除的功能
315
+
316
+ 以下旧版功能被有意剔除:
317
+
318
+ - **VersionController** — CDN 版本控制由现代构建工具(Vite/Webpack)处理
319
+ - **i18n 国际化** — 不属于资源加载器的职责
320
+ - **XML/Bin 分析器** — KUI XML 由 compiler 预处理,Binary 格式初期不需要
321
+ - **AnimationAnalyzer** — 初期不需要,后续按需添加
322
+ - **FontAnalyzer** — 初期不需要,后续按需添加
323
+ - **同步 getRes() 的复杂 sheet 子键查找** — 简化为点号语法
324
+
325
+ ## 注意事项
326
+
327
+ 1. **并发 `loadGroup` 调用会排队串行执行** — 内部共享一个 `ResourceLoader` 实例,同时调用多个 `loadGroup`(同组或不同组)不会互相覆盖回调或丢失进度;每次调用仍各自独立 resolve/reject,只是底层批次按调用顺序依次执行,不会真正并行加载。
328
+
329
+ 2. **`onProgress` 监听器不会自动移除** — 每次 `onProgress()` 调用会添加一个永久监听器。如需一次性监听,请使用 `loadGroup` 的 `onProgress` 参数。
330
+
331
+ 3. **Sheet 子键冲突** — 不同精灵表的子纹理名称不应重复,后加载的不会覆盖先加载的同名子键。
332
+
333
+ 4. **资源 URL 需要同源或 CORS** — 图片和音频加载受浏览器同源策略限制。
@@ -0,0 +1,77 @@
1
+ # Text layout
2
+
3
+ `TextField.wordWrap = true` uses Unicode 17.0 UAX #14 line-break opportunities
4
+ from `@cto.af/linebreak`. Available width determines which opportunity is used.
5
+ Font family, size, bold and italic are measured through the same Canvas font
6
+ configuration used for rendering. Rich-text style boundaries do not create
7
+ additional wrap opportunities.
8
+
9
+ - Latin and Cyrillic text prefer word boundaries. CJK text observes opening
10
+ and closing punctuation rules rather than treating dictionary words as units.
11
+ - Thai, Lao and Khmer add dictionary boundaries through the browser's
12
+ `Intl.Segmenter`. The default Unicode rules remain independently testable.
13
+ - When an alphabetic word is wider than the entire line, emergency wrapping
14
+ relaxes UAX #14's alphabetic rule LB28. Non-breaking spaces, narrow
15
+ non-breaking spaces, word joiners and punctuation protection remain in force.
16
+ Protected sequences can therefore exceed the field width.
17
+ - Ordinary spaces and tabs at a wrapped line's end are omitted from drawing
18
+ and width measurement. They are consumed on that line, so they never become
19
+ indentation on the next automatic line. Explicit indentation after a hard
20
+ separator is retained. Other space characters keep their Unicode behavior.
21
+ - The source string is unchanged. Line `charNum` counts consumed UTF-16 code
22
+ units, including unpainted spaces and both characters of CRLF. Painting,
23
+ input caret and hit testing use these source offsets.
24
+ - In multiline mode, CRLF, CR, LF, VT, FF, NEL, line separator and paragraph
25
+ separator force line breaks. A trailing hard separator retains a final empty line.
26
+
27
+ `wordWrap = false` retains character-based wrapping in multiline mode. Since
28
+ Core 2.1.0, explicit `multiline = false` renders only the first
29
+ hard-separated line without width wrapping. Dynamic text defaults to multiline;
30
+ INPUT defaults to single-line until the flag is assigned. An unconstrained
31
+ multiline field measures its hard-separated lines without automatic wrapping.
32
+ Fixed height clips drawing rather than changing those measured lines.
33
+
34
+ ## Font readiness
35
+
36
+ Load fonts before constructing or measuring text where possible. After a font
37
+ becomes ready late, call `textField.invalidateTextMetrics()`; reassigning an
38
+ unchanged family name does not invalidate cached line measurements. This method
39
+ clears text layout and marks the text for rendering with the newly available
40
+ font. UI Labels additionally need layout invalidation through their documented
41
+ `invalidateSize()` contract. Core does not install a global font-loading listener.
42
+
43
+ ## Migration from 2.0.1
44
+
45
+ Dynamic text previously wrapped even when `multiline` was explicitly false.
46
+ Remove that flag or set it to true where multiple lines are intended. The source
47
+ string is retained, including later hard-separated lines hidden in single-line
48
+ mode. Existing files are not rewritten automatically.
49
+
50
+ `WordWrap.tokenize()` remains a public word-segmentation utility; it is not the
51
+ TextField line-break algorithm. `splitGraphemes()` is likewise independent.
52
+
53
+ ## Validation
54
+
55
+ `test/LineBreaks.test.ts` verifies all 19,338 official Unicode 17.0 line-break
56
+ cases against the untailored profile. The compressed fixture includes its
57
+ source URL, SHA-256 and Unicode license. Dictionary and emergency tailoring,
58
+ multilingual width fitting, rich text, input offsets and rendering are tested
59
+ separately in `test/TextWrapping.test.ts`.
60
+
61
+ The dependency bundles its Unicode property tables and uses no network or Node
62
+ runtime APIs. Both WebGL text rasterization and Canvas rendering share this
63
+ layout. Core 2.1.0 also adds explicit single-line and font-invalidation
64
+ regressions. No resource-manifest or CLI XML-format change is involved.
65
+
66
+ ## Package dependencies
67
+
68
+ UI 3.0.0, game 2.0.0 and ui-runtime 0.7.0 already declare Core `^2.0.0` in their
69
+ peer/development ranges, which include Core 2.1.0. Repository examples require
70
+ Core `^2.0.1`, which also permits 2.1.0; installed lockfiles still need updating.
71
+ CLI templates use `latest`. The upcoming Label font-shrinking capability calls
72
+ `invalidateTextMetrics()` and therefore needs a Core minimum of 2.1.0 in UI and
73
+ the matching ui-runtime release. Game does not require a release for this change.
74
+
75
+ References: [UAX #14 revision 55](https://www.unicode.org/reports/tr14/tr14-55.html),
76
+ [official corpus](https://www.unicode.org/Public/17.0.0/ucd/auxiliary/LineBreakTest.txt),
77
+ [@cto.af/linebreak](https://github.com/cto-af/linebreak).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurot/core",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Modern rewrite of the Egret game engine — WebGL multi-texture batching, instruction-driven render pipeline, strict TypeScript, Egret-compatible API",
5
5
  "author": "Kelvin <kyia.x52@gmail.com>",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist/**/*",
19
+ "docs/**/*",
19
20
  "CHANGELOG.md",
20
21
  "LICENSE"
21
22
  ],
@@ -65,5 +66,8 @@
65
66
  "packageManager": "pnpm@10.33.0",
66
67
  "publishConfig": {
67
68
  "access": "public"
69
+ },
70
+ "dependencies": {
71
+ "@cto.af/linebreak": "4.0.3"
68
72
  }
69
73
  }