@kurot/core 2.0.0 → 2.0.1

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,232 @@
1
+ # GPU filters
2
+
3
+ Kurot executes `displayObject.filters` in array order. Each filter reads the
4
+ previous filter's image. WebGL 1 and WebGL 2 are supported; shader sources are
5
+ compiled verbatim, with no automatic GLSL translation.
6
+
7
+ ## Built-in effects
8
+
9
+ - `ColorMatrixFilter`: color-matrix transform with premultiplied-alpha handling.
10
+ - `BlurFilter(x, y, quality)`: non-negative logical-pixel radii; `quality` is an
11
+ integer from 1 to 16. Each quality step adds one horizontal/vertical pair,
12
+ using radius / sqrt(quality) to approximately preserve variance. The kernel
13
+ remains the existing triangular kernel. Large radii use a reduced-resolution
14
+ image so the per-axis shader radius stays within 32 physical pixels, then
15
+ resample back. This trades fine detail for wider blur; it is not bokeh DoF.
16
+ - `GlowFilter` / `DropShadowFilter`: the existing radial alpha-sampling shader.
17
+ `knockout=false` preserves the original. `hideObject` hides the original for
18
+ shadows. Their `quality` field remains serialized metadata; it does not
19
+ change this shader's fixed sample count.
20
+ - `BloomFilter`: LDR brightness extraction, blur, and original-image composition.
21
+
22
+ ```ts
23
+ import { BloomFilter, BlurFilter } from '@kurot/core';
24
+
25
+ const blur = new BlurFilter(12, 12, 2);
26
+ blur.resolution = 0.5;
27
+ background.filters = [blur];
28
+
29
+ const bloom = new BloomFilter({
30
+ threshold: 0.7,
31
+ intensity: 1,
32
+ blur: 12,
33
+ blurScale: 0.5,
34
+ });
35
+ world.filters = [bloom];
36
+ bloom.intensity = 0.8;
37
+ ```
38
+
39
+ `Filter.resolution` specifies physical pixels per logical unit. `undefined`
40
+ inherits the parent. A filter chain uses the minimum of the parent resolution
41
+ and its explicit filter resolutions, additionally limited by GPU texture size.
42
+ `BloomFilter.blurScale` reduces only extraction/blur; the original is retained
43
+ at the effect's full resolution.
44
+
45
+ Filtering includes the rendered descendants. Padding from consecutive filters
46
+ is added; descendant effect extents are included without changing layout bounds.
47
+ A mask/scroll clip on the filtered object clips its final result. Layered
48
+ containers provide control over whether a mask clips before or after another
49
+ effect. A lone ColorMatrix on an unmasked leaf with inherited resolution retains
50
+ its inline fast path; groups and combinations use offscreen composition.
51
+
52
+ ## CustomFilter
53
+
54
+ Use `CustomFilter.from()` to provide explicit backend variants. Omit a vertex
55
+ source to use Kurot's standard filter quad. A missing backend throws a descriptive
56
+ error rather than silently drawing the unfiltered image.
57
+
58
+ ```ts
59
+ import { CustomFilter } from '@kurot/core';
60
+
61
+ const dissolve = CustomFilter.from({
62
+ webgl1: {
63
+ fragment: `precision mediump float;
64
+ varying vec2 vTextureCoord;
65
+ uniform sampler2D uSampler;
66
+ uniform float threshold;
67
+ void main() {
68
+ gl_FragColor = texture2D(uSampler, vTextureCoord)
69
+ * step(threshold, vTextureCoord.x);
70
+ }`,
71
+ },
72
+ webgl2: {
73
+ fragment: `#version 300 es
74
+ precision mediump float;
75
+ in vec2 vTextureCoord;
76
+ uniform sampler2D uSampler;
77
+ uniform float threshold;
78
+ out vec4 result;
79
+ void main() {
80
+ result = texture(uSampler, vTextureCoord)
81
+ * step(threshold, vTextureCoord.x);
82
+ }`,
83
+ },
84
+ uniforms: { threshold: 0 },
85
+ padding: 0,
86
+ });
87
+ actor.filters = [dissolve];
88
+ dissolve.setUniform('threshold', 0.5);
89
+ ```
90
+
91
+ The existing `new CustomFilter(vertex, fragment, uniforms)` constructor submits
92
+ that exact source pair on either backend; the caller is responsible for source
93
+ compatibility. An empty vertex source selects the backend's standard vertex.
94
+ The source-pair key distinguishes vertex/fragment boundaries. Compiled programs
95
+ are cached by context and actual source, so uniforms are never part of the key.
96
+ Do not generate new shader text every frame; update uniforms instead.
97
+
98
+ Supported uniforms: `float`, `int`, `bool`, floating/integer/boolean vectors,
99
+ `mat2`, `mat3`, `mat4`, and their numeric arrays. Values are numbers, booleans,
100
+ numeric arrays, `Float32Array`, or `Int32Array`. Matrices use column-major order.
101
+ Active uniforms must be supplied with the correct component count; missing,
102
+ non-finite, or incompatible values throw before drawing. Unsigned uniforms,
103
+ non-square matrices, sampler arrays and uniform blocks are not part of this API.
104
+
105
+ `setUniform()` / `setTexture()` invalidate attached objects. After direct edits
106
+ to an array, `filter.uniforms`, or binding descriptor, call `filter.invalidate()`.
107
+ Built-in effect setters invalidate their users as well.
108
+
109
+ ### Coordinates and automatic uniforms
110
+
111
+ The default vertex accepts `aVertexPosition` (vec2), `aTextureCoord` (vec2), and
112
+ `aColor` (vec4), with `projectionVector` (vec2). Custom vertex programs must use
113
+ this attribute layout. Vertex displacement needs sufficient `padding` and does
114
+ not change scene geometry, layout or hit testing.
115
+
116
+ `vTextureCoord` uses **bottom-left framebuffer UVs**: the top is y=1. It spans
117
+ the entire padded input, not just the unpadded object. Input/output colors are
118
+ premultiplied RGBA. `vColor` is neutral in custom intermediate passes because
119
+ subtree vertices have already received world alpha/tint. For effects that need
120
+ straight RGB, divide by alpha only when alpha is positive, then premultiply the
121
+ output again.
122
+
123
+ | Reserved uniform | GLSL type | Value |
124
+ | --- | --- | --- |
125
+ | `uSampler` | sampler2D | Previous pass image, texture unit 0 |
126
+ | `projectionVector` | vec2 | Quad projection, managed by the renderer |
127
+ | `uTextureSize` | vec2 | Input width/height in physical pixels |
128
+ | `uInputSize` | vec4 | Input width, height, inverse width, inverse height |
129
+ | `uInputClamp` | vec4 | Input texel-center UV min/max |
130
+ | `uOutputSize` | vec4 | Output width, height, inverse width, inverse height |
131
+ | `uResolution` | float | Requested output pixels per logical unit for this pass |
132
+
133
+ Only declare the uniforms needed by the shader. They cannot be overridden in
134
+ `uniforms` or auxiliary bindings. Small textures may round to one physical pixel.
135
+ Use actual `uInputSize` / `uOutputSize` when exact texel spacing matters.
136
+
137
+ UV distortion should clamp to `uInputClamp`. A sample outside [0,1] otherwise
138
+ uses clamp-to-edge sampling, not automatic transparent border behavior.
139
+
140
+ ## Auxiliary images
141
+
142
+ `textures` maps sampler names to full-image `BitmapData` resources. The caller
143
+ retains ownership. Cropped/rotated atlas `Texture` objects are not accepted.
144
+ Auxiliary uploads have their own nearest/linear sampling state, so they do not
145
+ change the same image's sprite sampling.
146
+
147
+ ```ts
148
+ filter.setTexture('uMap', { source: mapBitmapData, smoothing: false });
149
+ ```
150
+
151
+ Declare `uniform sampler2D uMap`. Optionally declare `uMapMatrix` (mat3) and
152
+ `uMapClamp` (vec4). The default matrix converts bottom-left filter UV to top-left
153
+ image UV. For example, in GLSL 1.00:
154
+
155
+ ```glsl
156
+ vec2 mapUV = (uMapMatrix * vec3(vTextureCoord, 1.0)).xy;
157
+ vec2 displacement = texture2D(uMap, clamp(mapUV, uMapClamp.xy, uMapClamp.zw)).rg * 2.0 - 1.0;
158
+ vec2 uv = vTextureCoord + displacement * amplitude / uTextureSize;
159
+ gl_FragColor = texture2D(uSampler, clamp(uv, uInputClamp.xy, uInputClamp.zw));
160
+ ```
161
+
162
+ This maps the image across the padded input. `transform: [a,b,c,d,tx,ty]` can
163
+ replace the affine mapping; it is expressed directly in normalized UV units.
164
+ The default is `[1,0,0,-1,0,1]`.
165
+
166
+ After editing canvas/video content, call `BitmapData.invalidate(source)` so the
167
+ next pass uploads the new pixels. Call `filter.invalidate()` when application
168
+ scheduling also needs an explicit invalidation. Disposed or unavailable sources
169
+ throw instead of sampling stale pixels. Texture units are checked against the
170
+ context's fragment-stage limit, including the primary input.
171
+
172
+ ## Multi-pass effects
173
+
174
+ `MultiPassFilter` accepts 1–32 immutable pass descriptions. A pass reads the
175
+ previous output by default. `input: 'original'` reads the input to this effect;
176
+ `input: 0` reads the first pass output. Additional `textures` references use the
177
+ same notation and are supported by CustomFilter passes only. Forward references,
178
+ cycles and nested MultiPassFilter instances are rejected.
179
+
180
+ ```ts
181
+ import { MultiPassFilter } from '@kurot/core';
182
+
183
+ const effect = new MultiPassFilter([
184
+ { filter: extractHighlights, scale: 0.5 },
185
+ { filter: blurHighlights, scale: 0.5 },
186
+ { filter: combine, textures: { uOriginal: 'original' } },
187
+ ]);
188
+ world.filters = [effect];
189
+ ```
190
+
191
+ Here `combine` declares `uniform sampler2D uOriginal`. Borrowed framebuffer
192
+ inputs share the primary input's bottom-left UV convention; their automatic
193
+ `<name>Matrix` is identity. Primary and additional inputs may differ in pixel
194
+ size but cover the same logical rectangle. `scale` is relative to the enclosing
195
+ effect resolution, not the previous pass. Per-pass filter `resolution` can
196
+ further reduce its output resolution. Update child filter uniforms to animate;
197
+ replace the MultiPassFilter to change its pass graph.
198
+
199
+ GPU targets are never exposed to application code. The renderer preserves inputs
200
+ until their consumers finish, prohibits framebuffer feedback and recycles owned
201
+ targets even after errors. The idle intermediate pool is limited to 16 targets
202
+ and 64 MiB. Active pass images, subtree capture buffers and auxiliary images are
203
+ not included in that idle limit. Player's framebuffer pool counters report this
204
+ intermediate pool. `Player.destroy()` releases idle effect targets and auxiliary
205
+ uploads; caller image objects remain intact. Context restoration recreates
206
+ programs, intermediate targets and auxiliary uploads.
207
+
208
+ ## Backend and caching boundaries
209
+
210
+ CustomFilter, MultiPassFilter and BloomFilter are GPU effects. Canvas 2D skips
211
+ them; it continues to approximate supported built-in effects through its existing
212
+ CSS/pixel paths. Its filter combinations and Blur quality are not pixel-equivalent
213
+ to WebGL.
214
+
215
+ `cacheAsTexture` and `RenderTexture` still rasterize through Canvas 2D. Do not use
216
+ them to capture or cache these GPU effects. For post-processing use filter chains
217
+ or MultiPassFilter, which retain GPU images.
218
+
219
+ ## Validation
220
+
221
+ Run from the repository root:
222
+
223
+ ```sh
224
+ pnpm --dir packages/core build
225
+ pnpm --dir packages/core test
226
+ pnpm --dir packages/core test:visual
227
+ ```
228
+
229
+ The browser suite includes deterministic scene screenshots and explicit pixel
230
+ assertions on both WebGL versions, resource restoration, error recovery, filter
231
+ ordering, masks, high-resolution inputs, large-radius blur and Bloom. It is a
232
+ Chromium baseline, not a guarantee for every physical GPU or mobile browser.
@@ -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,59 @@
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
+ - CRLF, CR, LF, VT, FF, NEL, line separator and paragraph separator force line
25
+ breaks. A trailing hard separator retains a final empty line.
26
+
27
+ `wordWrap = false` retains character-based wrapping. Single-line INPUT fields
28
+ do not wrap to available width; multiline INPUT and dynamic text do. An
29
+ unconstrained field measures its hard-separated lines without automatic wrapping.
30
+
31
+ `WordWrap.tokenize()` remains a public word-segmentation utility; it is not the
32
+ TextField line-break algorithm. `splitGraphemes()` is likewise independent.
33
+
34
+ ## Validation
35
+
36
+ `test/LineBreaks.test.ts` verifies all 19,338 official Unicode 17.0 line-break
37
+ cases against the untailored profile. The compressed fixture includes its
38
+ source URL, SHA-256 and Unicode license. Dictionary and emergency tailoring,
39
+ multilingual width fitting, rich text, input offsets and rendering are tested
40
+ separately in `test/TextWrapping.test.ts`.
41
+
42
+ The dependency bundles its Unicode property tables and uses no network or Node
43
+ runtime APIs. Both WebGL text rasterization and Canvas rendering share this
44
+ layout. Core 2.0.1 is sufficient for existing UI 3.x / ui-runtime 0.7 consumers;
45
+ no CLI XML-format change is involved.
46
+
47
+ ## Consumer adoption for 2.0.1
48
+
49
+ UI 3.0.0, game 2.0.0 and ui-runtime 0.7.0 already declare Core `^2.0.0` in their
50
+ peer/development ranges. Repository examples and `kurot-project-kui` use the
51
+ same compatible range; their lockfiles need an update to pick up this fix.
52
+ CLI templates use `latest` and do not need a range change. Editor currently
53
+ pins the published Core 2.0.0 and builds its 0.4.6 trial with local 2.0.1 through
54
+ `KUROT_CORE_PATH`; update that pin and lockfile after Core 2.0.1 is published.
55
+ No dependant package is automatically bumped by this Core release preparation.
56
+
57
+ References: [UAX #14 revision 55](https://www.unicode.org/reports/tr14/tr14-55.html),
58
+ [official corpus](https://www.unicode.org/Public/17.0.0/ucd/auxiliary/LineBreakTest.txt),
59
+ [@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.0.1",
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
  }