☰
Open-Pencil 文本节点缺失字体检测:useNodeFontStatus Composable 深度解析与实战
2026/9/27 21:38:12 网站建设 项目流程
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

AI-native design editor. Open-source Figma alternative.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载

useNodeFontStatus是 Open-Pencil Vue SDK 提供的一个轻量级响应式 composable,用于检查文本节点(text node)当前引用的字体家族在运行时是否已经加载,并返回缺失字体的家族列表。它主要服务于字体属性面板(Typography 面板)与“缺失字体”警告类 UI,是 Open-Pencil 作为 AI 原生设计编辑器在「字体异步加载 + 属性面板实时反馈」场景下的关键工具。读完本文,你将掌握该 composable 的签名、返回值、底层实现原理、与useTypography/TypographyControlsRoot的集成方式,以及如何基于FontManager自己实现缺失字体检测与加载触发。

1. 为什么需要“缺失字体检测”

在 Open-Pencil 这类浏览器内渲染的设计编辑器中,字体(尤其是用户导入、远端注册的字体)是异步加载的:节点可以引用某个字体家族,但该家族的字形数据可能尚未到达运行时。此时:

  • 文本渲染可能使用替代字体(substitute)兜底;
  • 用户在字体面板里看到的家族并不代表它真正可用;
  • 导出、协作预览等流程需要感知字体状态,避免结果与设计稿不符。

useNodeFontStatus(node)正是为这一需求设计的:接收一个文本节点 getter,响应式地返回该节点所引用、但尚未加载完成的字体家族集合。官方文档明确指出,它应被用于排版面板(typography panels)以及需要暴露不可用字体家族的警告场景(见 use-node-font-status.md)。

2. 接口签名与返回值

依据源码 packages/vue/src/shared/font-status/use.ts,其类型签名可归纳为:

export function useNodeFontStatus( node: () => SceneNode | null | undefined ): { missingFonts: ComputedRef<string[]> hasMissingFonts: ComputedRef<boolean> }

关键点:

成员类型含义
node(参数)() => SceneNode \| null \| undefined一个返回目标节点的getter 函数,而不是节点本身。这是响应式设计的关键:composable 内部基于该 getter 建立computed,当选中节点变化时,缺失字体状态自动重新求值
missingFontsComputedRef<string[]>缺失的字体家族名数组(已去重);非 TEXT 节点时为[]
hasMissingFontsComputedRef<boolean>missingFonts.length > 0的布尔快捷值,便于模板中直接驱动警告条显隐

返回值是两个computed引用,意味着你可以直接在 Vue 模板中绑定,无需手动订阅更新。

3. 底层实现逐行解析

useNodeFontStatus的实现非常精简,完整源码仅 30 行(packages/vue/src/shared/font-status/use.ts),但背后涉及三条核心链路:节点类型判定 → 字体家族收集 → 加载状态查询。

3.1 类型守卫:只关心 TEXT 节点

const missingFonts = computed(() => { const n = node() if (n?.type !== 'TEXT') return [] // ... })

SceneNode场景图中存在多种节点类型(矩形、椭圆、路径等),只有TEXT节点才携带字体信息。因此当 getter 返回null、undefined或非文本节点时,直接返回空数组,避免对其他节点产生无效计算。

3.2 家族收集:节点级 + 富文本 run 级

const families = new Set<string>() families.add(n.fontFamily || DEFAULT_FONT_FAMILY) for (const run of n.styleRuns) { if (run.style.fontFamily) families.add(run.style.fontFamily) }

字体家族来自两个层级:

  1. 节点级fontFamily:TEXT 节点的默认字体属性;若为空则回退到DEFAULT_FONT_FAMILY,其值为'Inter'(见 packages/core/src/constants.ts),这也是 Open-Pencil 内置的默认界面字体之一;
  2. run 级styleRuns:文本节点支持富文本分段(style run),每一段可以覆盖不同的style.fontFamily。遍历所有 run 即可把“一段文字混排多种字体”的情况全部覆盖。

使用Set<string>去重,避免同一个家族被节点级与 run 级重复报告。

3.3 加载状态查询:FontManager.isLoaded

return [...families].filter((f) => !fontManager.isLoaded(f))

最终过滤掉所有“已加载”的家族,剩下的即为missingFonts。这里的fontManager是来自@open-pencil/core/text的全局单例FontManager(packages/core/src/text/fonts.ts),其判定逻辑为:

isLoaded(family: string): boolean { return [...this.loadedFamilies.keys()].some((k) => k.startsWith(`${family}|`)) } isStyleLoaded(family: string, style: string): boolean { return this.loadedFamilies.has(`${family}|${style}`) }

(packages/core/src/text/fonts.ts)

loadedFamilies以`${family}|${style}`为键缓存已注册字体的字形数据,因此isLoaded(family)只要发现该家族任意样式已注册即视为“已加载”。与之配套的还有markLoaded(family, style, data, source)(packages/core/src/text/fonts.ts)用于在字体加载完成后登记缓存,以及loadedFontSource/isStyleLoaded用于更细粒度的样式级查询。

值得注意的边界:useNodeFontStatus返回的是去重后的家族名列表,粒度到“家族”而非“家族+样式”。若需要样式级明细(例如区分「家族已加载但某字重缺失」),可以基于FontManager.isStyleLoaded(family, style)自行扩展。

4. 在 Typography 面板体系中的集成

useNodeFontStatus并非孤立存在,它是 Open-Pencil 字体属性控制体系的数据源头之一。

4.1 与 useTypography 的关系

useTypography()是文本属性面板的核心 composable(见 use-typography.md),在其状态构造器createTypographyState内部直接调用了useNodeFontStatus:

const node = useSceneComputed<SceneNode | null>(() => editor.getSelectedNode() ?? null) const { missingFonts, hasMissingFonts } = useNodeFontStatus(() => node.value)

(packages/vue/src/controls/typography/actions.ts)

也就是说,useTypography()返回的missingFonts/hasMissingFonts正是由useNodeFontStatus提供的;选中节点变化时,useSceneComputed驱动 getter 重新求值,缺失字体列表随之实时刷新(完整返回值见 actions.ts)。

4.2 与 TypographyControlsRoot 的关系

TypographyControlsRoot是无头(headless)根组件,将useTypography()的状态与操作以 slot props 的形式暴露给自定义 UI(见 typography-controls-root.md),其中就包含字体状态:

<slot :node="ctx.node" :weights="ctx.weights" :missing-fonts="ctx.missingFonts" :has-missing-fonts="ctx.hasMissingFonts" :active-formatting="ctx.activeFormatting" :actions="actions" />

(packages/vue/src/primitives/TypographyControls/TypographyControlsRoot.vue)

因此,一个基于TypographyControlsRoot的自定义字体面板可以直接从 slot 中拿到missingFonts与hasMissingFonts,在模板里渲染“缺失字体”警告,并在用户选择缺失家族时触发加载。

4.3 公共导出

useNodeFontStatus已作为公共 API 从@open-pencil/vue导出(packages/vue/src/index.ts),并在 packages/vue/README.md 的 composable 清单中列出,可直接import { useNodeFontStatus } from '@open-pencil/vue'。

5. 实战示例

5.1 直接使用:自定义缺失字体警告条

不经过useTypography,直接对任意文本节点 getter 使用:

<script setup lang="ts"> import { computed } from 'vue' import { useNodeFontStatus } from '@open-pencil/vue' import type { SceneNode } from '@open-pencil/scene-graph' const props = defineProps<{ node: SceneNode | null }>() const { missingFonts, hasMissingFonts } = useNodeFontStatus( computed(() => props.node) ) </script> <template> <div v-if="hasMissingFonts" class="font-warning" role="status"> 以下字体尚未加载:{{ missingFonts.join(', ') }} </div> </template>

注意:这里传给useNodeFontStatus的是一个返回节点引用的函数;computed(() => props.node)恰好满足 getter 签名,且保证props.node变化时缺失列表自动重算。

5.2 基于 TypographyControlsRoot:字体面板 + 缺失提示 + 按需加载

<script setup lang="ts"> import { TypographyControlsRoot } from '@open-pencil/vue' async function loadFont(family: string, style: string) { // 由你的应用实现:请求远端字体并调用 fontManager.markLoaded(family, style, data) } </script> <template> <TypographyControlsRoot :font-loader="{ load: loadFont }" v-slot="{ missingFonts, hasMissingFonts, actions }"> <select @change="actions.setFamily(($event.target as HTMLSelectElement).value)"> <option v-for="f in fonts" :key="f" :value="f">{{ f }}</option> </select> <div v-if="hasMissingFonts" class="warning"> 缺失字体:{{ missingFonts.join(', ') }} — 切换家族时将自动加载 </div> </TypographyControlsRoot> </template>

这里的fontLoader会被useTypography的createTypographyActions在setFamily/setWeight之前调用(见 packages/vue/src/controls/typography/actions.ts),实现“先加载、再改属性”的防白屏体验:

async function setFamily(family: string) { if (!node.value) return await doLoadFont(family, currentWeightLabel.value) // 先加载 editor.updateNodeWithUndo(node.value.id, { fontFamily: family }, 'Change font') }

5.3 与文档级字体状态 API 对照

如果需要对整个文档而非单个节点做字体体检,Open-Pencil 还在核心层提供了documentFontStatus(graph, rootIds, manager)(packages/core/src/text/font/status.ts),它扫描场景图并给出每个字体字形的状态:

  • available:精确的「家族 + 样式」已加载;
  • substituted:家族已加载但指定样式缺失,或整体回退到DEFAULT_FONT_FAMILY(Inter);
  • unresolved:连默认字体都不可用。

两者定位不同:useNodeFontStatus是节点级、响应式、面向面板 UI的轻量方案;documentFontStatus是文档级、扫描式、面向导出/诊断的批处理方案。在面板场景优先使用前者,在导出前校验等场景可以使用后者。

6. 使用建议与注意事项

  1. 入参必须是 getter 而非节点值:这是响应式更新的前提;传普通值会导致选中节点切换后状态不刷新。
  2. 粒度是字体家族:missingFonts是去重后的家族名数组;样式级(字重/斜体)细分需结合FontManager.isStyleLoaded。
  3. 非文本节点返回空数组:无需在调用方额外做类型判断。
  4. 默认回退 Inter:节点未显式设置fontFamily时按DEFAULT_FONT_FAMILY(Inter)判定,Inter 属于内置字体,正常情况下不会被报告为缺失。
  5. 配合fontLoader使用:在面板中把“检测缺失 → 用户选择 → 自动加载”串成闭环,能显著改善多字体文档的编辑体验。

7. 相关 API 与源码索引

  • 本 API 文档:use-node-font-status.md
  • 实现源码:packages/vue/src/shared/font-status/use.ts
  • 上层 composable:useTypography、packages/vue/src/controls/typography/use.ts
  • 无头组件:TypographyControlsRoot、packages/vue/src/primitives/TypographyControls/TypographyControlsRoot.vue
  • 字体加载与判定:packages/core/src/text/fonts.ts
  • 文档级字体状态:packages/core/src/text/font/status.ts
  • 默认字体常量:packages/core/src/constants.ts
  • 公共导出:packages/vue/src/index.ts
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

AI-native design editor. Open-source Figma alternative.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载
上一篇:Godot卡牌游戏框架终极指南:如何在1小时内构建专业级卡牌游戏
下一篇:Legacy iOS Kit终极指南:让旧iPhone/iPad重获新生的完整工具

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询