# width-measurement（技术计划）

## 1. 计划目标

基于 `spec.md` 与 `设计说明文档/文档换行算法.md v1.2.1`，落地 Text Wrap 的“宽度测量与降级（approx mode）”能力，保证：

- APP 场景可进行**可靠的文本宽度测量**（成熟方案，结果可缓存）
- WIDGET 场景允许不测量/测量失败时**确定性降级**（EN=wordCount，TC=charCount/graphemeCount）
- 缓存 key、错误处理与降级路径固定，保证**同输入同输出**（确定性）
- `fontSpec` 缺失字段直接报错（强制调用方补齐，避免隐式默认导致跨端漂移）
- `availableWidth` 由“本机设备侧/上层”传入，本模块不内置 widgetProfiles 常量

## 2. 默认技术决策（本计划采用）

### 2.1 测量方案（成熟方法）

- **APP（RN/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 近似口径写死
- 单测覆盖缓存命中/隔离与降级路径

