Files
mindfulness/spec_kit/Text Wrap/modules/width-measurement/plan.md
2026-02-10 11:39:33 +08:00

139 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# width-measurement技术计划
## 1. 计划目标
基于 `spec.md``设计说明文档/文档换行算法.md v1.2.1`,落地 Text Wrap 的“宽度测量与降级approx mode”能力保证
- APP 场景可进行**可靠的文本宽度测量**(成熟方案,结果可缓存)
- WIDGET 场景允许不测量/测量失败时**确定性降级**EN=wordCountTC=charCount/graphemeCount
- 缓存 key、错误处理与降级路径固定保证**同输入同输出**(确定性)
- `fontSpec` 缺失字段直接报错(强制调用方补齐,避免隐式默认导致跨端漂移)
- `availableWidth` 由“本机设备侧/上层”传入,本模块不内置 widgetProfiles 常量
## 2. 默认技术决策(本计划采用)
### 2.1 测量方案(成熟方法)
- **APPRN/Expo**:采用成熟的“离屏文本测量”库实现 `measureWidth(text, fontSpec)`
- 建议选型:`react-native-text-size`(或团队已有的等价成熟方案)
- 理由:可直接测量给定字体参数下的文本宽度,避免用 UI 渲染 onLayout 造成异步/不确定性
> 说明:本计划把“测量实现”作为本子模块交付的一部分,而不是仅定义接口;但仍保留注入点,便于替换实现或做平台差异适配。
### 2.2 缓存策略(由我统一定义)
采用“两级缓存 + 有上限”的策略:
1. **字符串测量缓存**`(text, fontSpecKey, contextProfile) -> width`
2. **切片测量缓存**`(start, end, fontSpecKey, contextProfile) -> width`
并且:
- 缓存采用**模块级常驻缓存**(跨 `wrapText()` 多次调用复用),提升性能
- 缓存容量必须有上限(建议 LRU 或“超限清空 + 打点”),避免内存无限增长
### 2.3 fontSpec 严格性(缺失就报错)
`fontSpec` 在 APP 测量路径下必须包含:
- `fontSize`
- `fontFamily`
- `fontWeight`
缺失任意字段时:
- **直接抛错**(错误信息必须为简体中文,并指出缺失字段与调用方应补齐的位置)
- 不允许在测量层做“隐式默认值”(避免跨端不一致与线上难定位)
### 2.4 contextProfile 的口径
为保证缓存与确定性,本模块定义 `contextProfile`
- APP`APP|<platform>|<scale?>`(按需扩展,但必须固定字段顺序与拼接方式)
- WIDGET`WIDGET|<widgetSize?>`(如果上层传入 widgetSize可写入否则仅 WIDGET
> 注意:你已确认 `availableWidth` 由设备侧传入,本模块不内置 widgetProfiles但缓存仍需要一个 profile 字段区分 APP/WIDGET。
## 3. API 设计(实现阶段稳定契约)
### 3.1 对外接口(建议)
- `buildFontSpecKey(fontSpec) -> string`
- `measureWidthCached({ text, context, fontSpec, contextProfile }) -> { width: number | null; meta: { isApprox: boolean; reason?: 'WIDTH_UNKNOWN' | 'MEASURE_FAILED' } }`
- `measureSliceWidthCached({ tokens, start, end, joinTokens, ... }) -> { width: number | null; meta: ... }`
### 3.2 approx mode 口径(必须确定性)
当出现任一情况时进入 approx mode返回 `width=null`,并由上层按 approx 口径计算 lineLen
- `measureWidth` 不可用
- `measureWidth` 抛错
- `measureWidth` 返回 NaN/Infinity/负数
- context=WIDGET 且上层选择不测量
approx 的“长度单位”口径固定为:
- EN`wordCount`
- TC`graphemeCount`(优先;若上层仅有 charCount则使用 charCount但必须在实现中写死选择
并必须打点 reason
- `WIDTH_UNKNOWN`:没有测量能力/不启用测量
- `MEASURE_FAILED`:测量抛错或返回非法值
## 4. 实现步骤(按落地顺序)
### 4.1 定义类型与错误
- 定义 `FontSpec``ContextProfile``MeasureResult` 类型
- 定义 `MissingFontSpecError`(或统一错误码),错误信息简体中文
### 4.2 实现 fontSpecKey
实现 `fontSpecKey = fontFamily|fontWeight|fontSize`
- 字段顺序固定
- `fontSize` 转为字符串(禁止浮点格式漂移:建议 `String(fontSize)`,并要求输入是 number 且有限)
### 4.3 实现缓存容器
- 实现一个带上限的缓存LRU 优先;若不引入依赖,先用 Map + 超限清空)
- Key 生成必须确定性:
- `textKey = <contextProfile>|<fontSpecKey>|<text>`
- `sliceKey = <contextProfile>|<fontSpecKey>|<start>|<end>`
### 4.4 接入成熟测量库APP
- 封装 `measureWidthImpl(text, fontSpec) -> number`
- 对返回值做校验(有限、非负)
- 失败捕获并走 approx mode并打 `MEASURE_FAILED`
### 4.5 WIDGET 策略
- 默认允许调用方传入 `measureWidthImpl`(如果 Widget 侧实现了测量)
- 若不传/不启用:直接 approx mode并 reason=`WIDTH_UNKNOWN`
## 5. 测试计划Vitest
### 5.1 纯函数与缓存测试(必须)
- `fontSpecKey`:同输入同输出;缺字段抛错
- 缓存命中:同 key 不重复调用底层 `measureWidthImpl`
- 缓存隔离:不同 `contextProfile/fontSpecKey` 不互相污染
### 5.2 降级路径测试(必须)
- 缺少 `measureWidthImpl` -> approx mode + reason=`WIDTH_UNKNOWN`
- `measureWidthImpl` 抛错/返回 NaN -> approx mode + reason=`MEASURE_FAILED`
> 说明:测量库本身的准确性不在单测中做像素级断言;单测只验证“缓存与降级语义确定性”。
## 6. 完成定义DoD
- APP/WIDGET 下 `width` 返回语义清晰:可测量返回 number不可测量返回 null
- `fontSpec` 缺字段必定报错(简体中文错误信息)
- 缓存 key 与容量策略确定性,且有上限
- approx mode 触发条件、reason 标记、EN/TC 近似口径写死
- 单测覆盖缓存命中/隔离与降级路径