@typeset/figma-plugin-typeset

@typeset/figma-plugin-typeset 是 core engine 与 Figma text pipeline 之间的 adapter,同时保留一个画布内可见的技术验证界面。Core 决定断行和 spacing;本包只负责 Figma measurement、受支持效果的呈现和验证,不重新求解。

运行流程

  1. 选择一个非空、未旋转、固定宽度或 auto-height、左对齐、uniform-style 且 letterSpacing = 0TextNode
  2. UI iframe 使用 Browser Intl.Segmenter 完成 story、grapheme、token、boundary、spacing、solver 和 Stage 8 layout。Figma main sandbox 不运行这些依赖 Browser API 的步骤。
  3. Main sandbox 测量 token,把 fractional em advances 通过 postMessage 返回 UI。
  4. UI 将完整 LayoutResult[] 返回 main sandbox。
  5. Rendering adapter 为每个 paragraph 创建独立的普通 Frame。没有基线偏移的 solved line 使用一个 mixed-style TextNode;包含基线偏移的 line 将相邻、非空格且 baseline shift 相同的 tokens 合并为 visual runs,再手动设置 run 的横纵坐标。

Figma 的 Shift+Enter 产生 U+2028 LINE SEPARATOR。它保持在同一个 core paragraph 内,measurement request 会跳过该控制 token,solver 在对应 mandatory boundary 强制换行,renderer 则为换行前后分别创建可见 TextNode。普通 Enter 所形成的 paragraph break 仍由 Story 层拆分。

UI 与 main sandbox 之间显式交换 measurement 和 layout 数据,不提供隐藏所有 pipeline stages 的 convenience orchestration。

Create Typeset Paragraph Frames

Plugin UI 的 Create Typeset paragraph frames 是当前正式输出入口:

  1. 对选中的 source TextNode 创建一组 guarded measurement probes,并在正式输出成功后删除。
  2. UI iframe 显式执行 core pipeline,生成 LayoutResult[]
  3. 在 source 右侧为每个 paragraph 创建一个同级的 positioned line Frame,依次命名为 paragraph n,默认以零段间距纵向排列;其中的 TextNode 按整个输出连续命名为 line n
  4. 完成后同时选中所有 paragraph Frames,并在 UI 中显示 paragraph、line、spacing range 和 core diagnostic 数量。

正式输出完成后只锁定内部的每个 line TextNode,paragraph Frame 保持可移动,用户可以直接调整段落位置和间距。Validation demo 保持可编辑,不应用这项锁定与命名策略。

正式操作位于 src/operations/create-typeset-frame.ts,负责连接 measurement adapter 与 rendering adapter,但不替 core 隐藏或重新实现任何 pipeline stage。正式输出在成功、失败或 UI 求解取消时都会清理本次 probes;只有独立的 validation demo 会保留可见 probes 和实验数据。

Composition Settings

Plugin UI 按 FactsRulesStylePolicyDebug 分组,使用浏览器原生控件,不包含文本预览。用户点击 Create Typeset paragraph framesCreate validation demo 时,当前配置会冻结到本次请求中;后续 measurement / rendering 过程不会读取 UI 的新状态。

Mojikumi 的 profile 选择同时出现在 StylePolicy,两处操作同一份配置。Default Inline Mojikumi spacingSimpleChinesePrettySpacing 两个 core profile 只读;custom profile 从默认规则表复制,在插件本次运行期间编辑并直接参与求解。Character classes 固定使用 defaultCharacterClassProfile。Line-end hanging 使用 core 的默认中文 profile,并可选择 noneallowforce

Measurement Adapter

正式入口位于 src/adapter/measurement/measure-token-advances.ts。调用方必须提供:

Adapter 按候选顺序选择 Figma 中第一个可用的 { family, style } face,并在 token range 上应用字体与 baseFontSize * relativeSize。随后创建 guard + guard baseline 和 guard + token + guard probes,以 absoluteRenderBounds.width 差值返回 advancePxadvanceEm 始终除以 source 的基准字号,而不是相对字号后的 local size。它不创建 solver、renderer 或隐藏 probe。

完整测量实验、原始数据和已知 Figma 差异见 docs/measurement.md

Rendering Adapter

正式入口位于 src/adapter/rendering/apply-text-layout.tsapplyFigmaTextLayout 接收 source TextNode、原始 sourceText、与 Stage 8 LayoutResult[] 等量且顺序一致的空 paragraphTargets

Line-start offset 使用 Figma px 坐标,因此转换为 edgeSpacingEm * baseFontSizePx。同一字体 run 内的 grapheme-pair 与 line-end spacing 使用经过 relativeSize 校正的百分比,复合字体边缘使用 base-em pixel value。空行如果带非零 line-end spacing 会明确报错,因为不存在可承载 trailing spacing 的 grapheme。

Line-end hanging 依靠既有输出结构呈现:solver 用 protrusion 从 occupiedWidth 中扣除末尾标点的部分 advance,但 LayoutLine 仍保留完整文字与 advanceWidth;Figma 的 line TextNode 使用 auto-width,paragraph Frame 保持固定宽度且 clipsContent = false,所以行尾字形会自然越过 Frame。Adapter 不额外施加负 letter spacing,避免把 hanging 与 Mojikumi line-end compression 重复计算。

Renderer 要求 source 未旋转、左对齐且宽度与所有 LayoutResult.availableWidth 一致。每个 line node 都把继承自 source 的 paragraphIndentparagraphSpacing 归零,实际位置完全由 renderer 写入的 x / y 坐标决定。每个 paragraph Frame 的高度只覆盖本段 lines;段间距不固化进 Frame。创建任一 line node 失败时,adapter 会清理本次已经创建的 clones,不留下部分渲染结果。

Validation

运行时 validation 位于 src/validation/create-measurement-validation.ts。点击 Create validation demo 后,画布会保留:

  1. Guard baseline、isolated/guarded token probes、bounds 和 SVG diagnostics。
  2. 每条 solved line 的 token measurement 与 Figma auto-width line 对照。
  3. Figma native wrapping TextNode 与手动定位的 Typeset line Frame 并排结果。
  4. Magenta native / cyan Typeset overlay。

Validation 使用与正式输出相同的当前 composition settings,包括 alignment、Kinsoku、Inline Mojikumi、两条 last-line policies 与 line-end hanging。Validation 会显示生成的 line node、非零 line-start position、letter-spacing range、line-end spacing 与 hanging line-end 数量。默认 Mojikumi profile 对 line-start 遇到前括号、后括号或句读标点遇到前括号,以及后括号、句读标点或中间标点遇到 line-end 使用压缩 spacing;其他 spacing 仍取决于 solver 是否实际采用相应 adjustment。

连续换行产生的 empty paragraph 已用 第一段\n\n第三段 验证:Figma native pipeline 与 Typeset positioned line Frame 完全重合。当前空行 TextNode 的实际高度可以正确参与手动 y 累加。

段落内的 U+2028 支持普通、连续与末尾 forced line break;连续或末尾 separator 会产生对应的 empty line TextNode。该路径不把 U+2028 发送给 guarded measurement,因此不会再触发空 token advance 错误。

能力边界

Development

从 workspace root 运行:

pnpm --filter @typeset/figma-plugin-typeset build
pnpm --filter @typeset/figma-plugin-typeset typecheck
pnpm --filter @typeset/figma-plugin-typeset lint
pnpm --filter @typeset/figma-plugin-typeset watch:build

Figma 开发插件加载 dist/manifest.jsondist/ 是完整的插件目录,包含生成的 manifest、code.js 与内联所有 UI 资源的 ui.html。插件版本由本包 package.jsonversion 唯一控制,manifest 由 manifest.ts 生成。本包保留独立 TypeScript config,因为构建脚本、main sandbox 与 Browser iframe 使用不同 runtime APIs。