依据描述生成 DIM,再依据 DIM 同步到 Figma 的完整工程流程

这篇文档把DIM到Figma的能力串成一条完整链路:先把自然语言描述沉淀成结构化的 DIM,再把 DIM 作为唯一事实源同步到 Figma,最终做到“所有 UI 变更先改 DIM,再由 DIM 驱动 Figma”。

核心文件包括:

一句话定义这条链路

这套流程的本质是:

自然语言需求
-> DIM(结构化设计意图模型)
-> Figma 页面与组件
-> 回查校验

这里的 DIM,在当前工程里指的是 Design Intent Model,也就是“设计意图模型”。它不是设计稿图片,也不是最终代码,而是一个足够结构化、足够精确、足够可计算的中间层。

在这个工程里,DIM 被拆成 3 个文件:

  • tokens.json:全局视觉变量
  • components.json:组件结构与状态
  • screens.json:页面级布局与节点树

为什么一定要先有 DIM

如果直接从一句需求去画 Figma,通常会有 4 个问题:

  1. 描述里存在隐含值,比如字号、圆角、按钮高度、边框颜色没有写全。
  2. 同一个元素在不同页面里会被人“凭感觉”画出微小差异。
  3. 后续改版时,无法确定哪个值是规范,哪个值只是临时修补。
  4. Figma 会慢慢变成事实源,设计规范反而失效。

DIM 的作用就是把这些不确定性提前消灭掉。只有当描述被压缩成结构化、显式、无歧义的数据后,Figma 才能被稳定地“生成”出来,而不是被一次次“手画”出来。

当前工程里的 DIM 结构

1. tokens.json

tokens.json 负责描述设计中的全局常量,例如:

  • 品牌色
  • 文本色
  • 页面底色
  • 间距体系
  • 圆角体系
  • 字体体系
  • 阴影体系

例如当前 exam/tokens.json 里已经定义了:

  • color.brand.primary:微信绿 #07C160
  • spacing.s16spacing.s24 等间距 token
  • radius.r8radius.r12radius.pill
  • typography.titleMdtypography.bodyMdtypography.caption

这一层解决的是“视觉数值必须唯一”的问题。

2. components.json

components.json 负责定义组件蓝图。它并不画页面,而是描述“页面里会重复使用的那类元素”。

当前 exam/components.json 已定义了这些组件:

  • cmp.nav-bar
  • cmp.exam-card
  • cmp.progress-bar
  • cmp.button-primary
  • cmp.question-option

每个组件描述了几类信息:

  • 基本尺寸
  • padding / gap / 排布方向
  • 基础样式
  • 可承载的 slot
  • 支持的状态 states

例如 cmp.question-option 不只是一个矩形框,而是一个带:

  • 固定宽度
  • 最小高度
  • 内边距
  • 文本样式
  • 选中/禁用/正确/错误等状态

的题目选项组件。

这一层解决的是“重复 UI 必须可复用、可变体化”的问题。

3. screens.json

screens.json 负责把组件实例和纯文本节点真正拼成页面。

当前 exam/screens.json 里有 2 个页面:

  • screen.home
  • screen.question

它里面定义了:

  • 页面尺寸
  • 页面背景色
  • 每个节点的类型
  • 节点坐标
  • 节点引用的组件
  • 节点传入的 props

例如:

  • 首页里的 home.card.upcoming 是一个 cmp.exam-card
  • 做题页里的 question.option.b 是一个 cmp.question-option
  • 做题页里的 question.title 是一个纯文本节点

这一层解决的是“具体页面到底由哪些元素构成”的问题。

从自然语言到 DIM:应该怎么拆

这一步是整条链路里最关键的认知层工作。原则是:先把需求翻译成视觉事实,再把视觉事实翻译成 DIM。

例如用户说:

做一个微信风格的考试类小程序首页,上面有标题,中间是考试卡片,底部是继续答题按钮。

这句话不能直接进 Figma,因为还缺很多信息。正确做法是先补齐这些维度:

1. 页面级信息

  • 目标设备尺寸是什么
  • 是微信小程序风格,还是完全复刻微信原生页面密度
  • 顶部导航高度是多少
  • 页面背景色是什么

2. 组件级信息

  • 考试卡片宽高是多少
  • 卡片圆角和阴影是什么
  • 卡片内标题、元信息、进度条、按钮的相对位置是什么

3. 文字级信息

  • 字体是 PingFang SC
  • 字重分别是多少
  • 行高多少
  • 文本垂直居中规则是什么

4. 状态级信息

  • 选项的 default / selected / correct / wrong / disabled 怎么表达
  • 主按钮的 default / pressed / disabled 怎么表达

只有这些值都显式化,DIM 才算合格。

这个工程里对 DIM 的硬约束

当前 Skill 已经把 DIM 输入约束写得很明确,见 skills/ui-figma-pixel-lock/SKILL.md

它要求 DIM 必须显式提供:

  • Frame size and grid
  • Typography values
  • Corner radius / stroke / shadow / blur
  • Spacing and alignment rules
  • Interaction states

它还定义了一些“禁止自由发挥”的规则:

  • 不允许凭审美补值
  • 不允许随意改 token 名
  • 不允许替换字体
  • 不允许手动调整 spacing 来“看起来差不多”
  • 除非明确说明,否则文本要垂直居中
  • 默认使用 390 x 844 画板
  • 每次改动必须先改本地 DIM,再从 DIM 更新 Figma

这里最重要的一条是:

Never update Figma first.

也就是:Figma 只是渲染结果,不是源头。

DIM 写完以后,为什么还要做 preflight

DIM 不是只要存在就够了,还要确认它是“这次确实被改过、而且结构合法”的。

当前工程里的 scripts/preflight_dim.js 就是这一步的前置检查脚本。它主要做三件事:

  1. 检查 exam/tokens.jsonexam/components.jsonexam/screens.json 是否存在。
  2. 检查这三个文件是不是合法 JSON。
  3. 检查当前 Git diff 里,这次是否真的改到了 exam/*.json

它的目的不是“验证美观”,而是防止出现这类错误流程:

  • 人在 Figma 里改了 UI,但忘了回写 DIM
  • 人准备执行同步,但本次根本没有 DIM 变化
  • DIM 文件被手改坏了,JSON 已不合法

也就是说,preflight_dim.js 是把“流程纪律”程序化。

从 DIM 到 Figma:这个工程是怎么画的

真正把 DIM 变成 Figma 节点的入口是 tmp_figma_draw_dim.js

虽然它现在文件名里有 tmp,但逻辑上它已经是一个完整的 DIM 渲染器,主要承担 6 层职责。

1. 读取 DIM

脚本启动后先读取:

exam/tokens.json
exam/components.json
exam/screens.json

并把它们解析成内存对象。

2. 解析 token

脚本内部有一套 resolver,会把 token 路径解析成具体值,例如:

  • color.brand.primary -> RGBA
  • radius.r8 -> 数值
  • typography.bodyMd -> 字体对象

这一步的价值是:页面绘制代码不直接写死颜色和字号,而是统一通过 token 引用,保证 Figma 和 DIM 的视觉数值始终一致。

3. 连接 Figma MCP

脚本会通过 cursor-talk-to-figma-mcp 连接 Figma,并先 join_channel 到指定通道。

这一层的作用是把“结构化 DIM”翻译成一连串可执行的 Figma 操作,例如:

  • create_frame
  • create_rectangle
  • create_text
  • set_fill_color
  • set_stroke_color
  • set_corner_radius
  • move_node
  • resize_node

也就是说,DIM 本身不懂 Figma API,但渲染器懂。

4. 读取当前 Figma 页面,决定写入模式

这是当前 Skill 里一个很重要的安全设计。

写入模式分两种:

  • append
  • replace

默认是 append,不是 replace

含义是:

  • append:即便 Figma 里已有同名页面,也要新建一个不重叠的页面放进去
  • replace:只有用户明确要求覆盖、刷新、同步替换时,才允许在原位置清空并重画

当前 Skill 对这件事要求非常严格,因为它要避免把已有设计稿误删掉。

5. 为页面寻找不重叠位置

脚本会先读取当前 Figma 文档顶层 frame,然后计算一个不会与现有 frame 重叠的位置。

这一步对应 Skill 中的规则:

  • 新页面必须放到可见区域
  • 不能压在已有页面上面
  • 同名 frame 不是覆盖许可

这个机制解决的是“同步一次把旧稿盖掉”的事故风险。

6. 按 screen 节点逐个绘制

drawScreen() 会遍历 screens.json 中的节点,并根据不同类型分发给不同绘制函数。

例如当前工程里已经覆盖了:

  • drawNavBar()
  • drawExamCard()
  • drawQuestionOption()
  • drawPrimaryButton()
  • drawTextNode()
  • drawRectNode()

也就是说,screens.json 只是告诉系统“这里有一个 cmp.question-option 实例”,真正决定怎么画的是渲染器里的实现。

当前工程中的页面是怎么落到 Figma 的

以当前 exam 目录里的数据为例:

首页 screen.home

这个页面由:

  • NavBar
  • 两张 ExamCard

构成。

渲染时流程大致是:

  1. 创建页面 frame
  2. 用页面背景 token 填充底色
  3. 在顶部绘制导航栏
  4. 在指定坐标绘制第一张考试卡片
  5. 再绘制第二张考试卡片

考试卡片内部又会继续细分为:

  • 卡片背景
  • 标题
  • 元信息
  • 进度条轨道
  • 进度条实心条
  • 按钮底
  • 按钮文字

做题页 screen.question

这个页面由:

  • NavBar
  • 题干文本
  • 4 个选项组件
  • 一个主按钮

构成。

drawQuestionOption() 里,脚本会根据 state 决定:

  • 选中态是否使用 color.brand.primarySurface
  • 边框是否切换到 color.brand.primary

这就说明:组件状态并不是 Figma 里事后人工改出来的,而是从 DIM 里明确定义后被程序渲染出来的。

真正稳定的流程应该怎么跑

如果把这件事变成工程规范,那么一次标准的 UI 变更应该按这个顺序执行:

第 1 步:接收自然语言需求

输入可能是:

  • 做一个新页面
  • 修改某个组件状态
  • 调整页面尺寸
  • 改按钮圆角或色值

这一步不要直接动 Figma。

第 2 步:把需求翻译为 DIM 变更

按变化范围修改对应文件:

  • 全局视觉变化改 tokens.json
  • 组件蓝图变化改 components.json
  • 页面内容和实例排布变化改 screens.json

如果新需求涉及多个层级,允许同时改多个 DIM 文件。

第 3 步:跑 preflight

执行:

node scripts/preflight_dim.js

如果只是验证结构而不要求本次一定有 DIM diff,可加:

node scripts/preflight_dim.js --allow-no-dim-change

通过这一步,确保:

  • 文件存在
  • JSON 合法
  • 本次修改确实已经落到 DIM

第 4 步:连接 Figma,并选择同步模式

如果需求是“把新页面加到 Figma 里看看”,就用 append

如果需求是“我确认要覆盖当前同名页面”,才用 replace

这个决策非常重要,因为它决定了同步器是“保守新增”还是“原地刷新”。

第 5 步:执行 DIM -> Figma 同步

tmp_figma_draw_dim.js 执行实际绘制。

逻辑上它会:

读 DIM
-> 解析 token
-> 连接 Figma MCP
-> 读取现有页面
-> 计算放置位置
-> 创建 frame / rect / text
-> 应用颜色 / 描边 / 圆角 / 尺寸 / 位置

第 6 步:做零漂移校验

当前 Skill 里定义的校验目标包括:

  • token 值一致
  • 组件属性集合一致
  • 页面节点树一致
  • frame 尺寸一致
  • 文字样式元组一致
  • 填充、描边、效果一致
  • 文本默认垂直居中一致
  • 新增页面不得覆盖旧页面

只要有一项不一致,结果就应该是 FAIL,而不是“差不多可以”。

为什么这套流程比“先画 Figma,再整理规范”更稳

因为它把 UI 生产过程拆成了两个职责清晰的层:

  • DIM 负责描述事实
  • 同步器负责渲染事实

一旦职责分清,后续好处非常明显:

  1. 改 UI 时,不需要猜哪个 Figma 值才是准的。
  2. 相同组件在多个页面里天然保持一致。
  3. 可以对 DIM 做版本控制、代码评审、差异对比。
  4. 可以把“设计变更”纳入工程流程,而不是留在手工软件里。
  5. 可以持续扩展同步器,让更多组件和布局规则自动化。

当前工程还可以继续补强的地方

虽然主链路已经有了,但如果要把这套方案真正做成稳定生产流,还建议补 5 类能力。

1. 把 tmp_figma_draw_dim.js 正式化

当前它已经是核心执行器,建议后续改成更正式的命名,例如:

scripts/sync_dim_to_figma.js

并补:

  • 参数说明
  • 日志输出格式
  • 错误码
  • 成功/失败摘要

2. 增加 DIM schema 校验

目前 preflight_dim.js 只检查“是不是合法 JSON”,还没检查“字段结构是否完整”。

后续建议增加 schema 规则,例如:

  • tokens.typography.* 必须含 fontFamily/fontWeight/fontSize/lineHeight
  • components[].id 必须唯一
  • screens[].nodes[].id 必须唯一
  • instance 节点必须有 ref

3. 增加回读校验

同步完成后,可以再通过 Figma MCP 读取已创建节点,把关键属性反查回来,与 DIM 做比对,形成真正的“闭环校验”。

4. 把组件绘制逻辑继续抽象

当前部分组件是手写绘制函数。后面可以继续往“通用渲染器”方向演进,让更多组件由 schema 自动生成,减少每加一种组件都要写新函数。

5. 增加从文本描述自动生成 DIM 的规则层

现在工程里已经有了 DIM 和同步器,但“自然语言 -> DIM”这一段还主要依赖人工整理。

下一步最值得做的是建立一层明确的生成规范,例如:

  • 哪些描述进入 tokens
  • 哪些描述进入 components
  • 哪些描述进入 screens
  • 遇到缺失值时必须中断,而不是猜测

这样才能把整条链路真正变成:

描述 -> 结构化设计意图 -> Figma

而不是:

描述 -> 人工脑补 -> Figma

推荐的团队协作规则

如果这个仓库以后要多人协作,我建议把下面几条写成硬规范:

  1. 所有 UI 修改先改 exam/*.json,后改 Figma。
  2. 禁止只改 Figma 不回写 DIM。
  3. 所有同步前先跑 preflight_dim.js
  4. 默认 append,禁止无说明覆盖。
  5. 只要同步结果和 DIM 有差异,就视为失败。

这 5 条实际上就是把“设计工具操作”转成“工程化流程”。

总结

当前工程已经具备一条很清晰的主链路:

  1. tokens/components/screens 描述 UI。
  2. 用 Skill 限制 DIM 必须显式、可计算、不可脑补。
  3. preflight_dim.js 保证同步前输入安全。
  4. tmp_figma_draw_dim.js 把 DIM 翻译成 Figma 操作。
  5. 用 append/replace、非重叠放置、零漂移校验保护现有设计。

如果把这套思路再往前推进一步,把“自然语言 -> DIM”的生成规则也工程化,那么这个项目就不只是“把设计稿画到 Figma”,而是在做一条真正可复用的 UI 生产流水线。

它的目标不是辅助画图,而是把“画图”本身降级为一个可靠的渲染步骤。