完善UI编辑器字体预览与BestFit

绑定字体加载成功后应用私有 font-family,并回退系统字体

新增字形映射和浏览器真实文本测量的 BestFit 算法

补充文本渲染回归测试及字体跨运行时一致性待评估文档

更新UI编辑器字体预览权威实施合同
This commit is contained in:
2026-08-19 13:56:08 +08:00
parent b5351f03fa
commit 17b09804fa
9 changed files with 358 additions and 31 deletions
@@ -0,0 +1,11 @@
# UI 编辑器字体跨运行时像素一致性评估
## 背景
UI Editor 预览已使用浏览器真实排版测量 `BestFit`:同样式的不可见文本节点按容器实际尺寸,在整数 `[min,max]` 中选择最大可完整容纳字号。绑定字体只在 `FontFace` 加载成功后生效,加载中、失败或悬空引用回退系统字体。
## 待评估范围
- 比较目标浏览器 WebView 与后续正式运行时的字体回退、字形替换、换行和行高是否需要像素级一致。
- 如需一致,先确认权威运行时、目标平台与可接受误差,再设计可复现的字体夹具和视觉回归门禁。
- 不得把此评估误解为 `BestFit` 尚未实现,或重新引入仅取 `BestFit.max` 的近似逻辑。
@@ -24,11 +24,11 @@ UI Editor Inspector 的全局只读状态唯一来源是 `controller.editor.isLo
## 2026-08-17 UI Editor 项目字体导入与预览
UI Editor 的 `State.font_assets` 正式承载项目字体面资源;`FontAsset` 包含项目资产 ID、受控项目相对路径、内容 SHA-256,以及由 Rust 解析的 family、face、weight、italic、格式和源文件名。`TextComponent.font` 直接绑定一个具体字体面;原 `font_style` 是与字体面元数据重复且可能矛盾的状态,直接从 Rust 权威类型和生成 TypeScript 类型中删除,不保留会话态兼容层。UI Editor 整体 State 仍只属于当前桌面会话,不新增持久化合同。
UI Editor 的 `State.font_assets` 正式承载项目字体面资源;`FontAsset` 包含项目资产 ID、受控项目相对路径、内容 SHA-256,以及由 Rust 解析的 family、face、weight、italic、格式和源文件名。`TextComponent.font` 直接绑定一个具体字体面,`font_style` 继续表达组件要求的 Normal / Bold / Italic / BoldItalic 浏览器字形;预览将其稳定映射为 CSS `fontWeight` 与 `fontStyle`。UI Editor 整体 State 仍只属于当前桌面会话,不新增持久化合同。
字体资源发现与 Sprite 保持同一项目资产语义:两种 importer 在 FileManager Home 下都从唯一的 `本地项目` 根进入,再按各自 `typeFilter` 展示已登记资源;不扫描或接纳未登记文件,字体不展示云端素材库。从电脑导入使用 Tauri 系统文件选择器,Rust 整批读取普通文件,拒绝符号链接、集合字体、超过 `8 MiB` 的单文件、超过 `64` 个字体面或 `32 MiB` 项目总量,完成真实签名、字体表、名称与 weight/style 解析后复制到 `assets/fonts/` 并登记 manifest。内容相同的字体复用已登记资源;Sprite 与 Font 的 State 批量加入都采用幂等合并:相同 ID 且完整资源相等时跳过,同 ID 数据冲突时整批失败。删除只移除会话 State 资源并清空相应 `Image.target_graphic` 或 `Text.font` 引用,不删除项目文件或 manifest 条目。
候选格式为 TTF、OTF、WOFF 和 WOFF2,但 Rust 安全解析是导入硬门;当前解析依赖不能完整解析的压缩 Web Font 必须拒绝,不能把浏览器可能加载当作验证成功。已登记字体字节只能经字体专用 Tauri 命令读取;命令重新核对 manifest 的 asset ID / 相对路径、普通文件、大小、字体结构与摘要。Importer 预览也只读取同一受 manifest 约束的字体字节,再临时加载 `FontFace` 显示黑色的中英文、数字和标点多字号样张;切换选择或关闭时立即卸载。前端以 `FontAssetId` 派生私有 CSS family,创建 Blob URL 和 `FontFace`,加载成功后加入当前 `document.fonts`,资源变更或卸载时删除 FontFace 并回收 Blob URL。同名 family 不共享浏览器注册名。WebView 加载失败或字体缺少当前文本字形时不阻塞后续阶段,Inspector 显示非阻断提示并回退系统字体;悬空字体 ID 继续由 prerequisite 阻止。`BestFit` 保持现有取最大字号的近似,不在本次字体闭环中扩展为测量算法。
候选格式为 TTF、OTF、WOFF 和 WOFF2,但 Rust 安全解析是导入硬门;当前解析依赖不能完整解析的压缩 Web Font 必须拒绝,不能把浏览器可能加载当作验证成功。已登记字体字节只能经字体专用 Tauri 命令读取;命令重新核对 manifest 的 asset ID / 相对路径、普通文件、大小、字体结构与摘要。Importer 预览也只读取同一受 manifest 约束的字体字节,再临时加载 `FontFace` 显示黑色的中英文、数字和标点多字号样张;切换选择或关闭时立即卸载。前端以 `FontAssetId` 派生私有 CSS family,创建 Blob URL 和 `FontFace`,加载成功后加入当前 `document.fonts`,资源变更或卸载时删除 FontFace 并回收 Blob URL。同名 family 不共享浏览器注册名。预览仅在绑定字体已加载时使用其私有 family;加载中、失败或悬空引用均回退系统字体。`BestFit` 使用同样式的不可见浏览器文本节点和容器实际尺寸,在整数 `[min,max]` 中二分取得最大可完整容纳字号;没有字号能完整容纳时使用 `min`,容器、文本、字体族、字形、行高或溢出规则变化后重新测量。WebView 加载失败或字体缺少当前文本字形时不阻塞后续阶段,Inspector 显示非阻断提示并回退系统字体;悬空字体 ID 继续由 prerequisite 阻止。
## 2026-08-18 UI Editor 图片 / 字体通用 AssetImporter 契约