Figma 的节点树内容丰富但噪音较多。Frame、Group、Component、Instance、Text、Rectangle、Ellipse、Vector——每个都有数十个可选字段,其中一些相互冲突。直接使用原始 Figma API 响应工作的 Agent 会将认知预算花在理解 schema 上,而不是编写代码。

figmascope 将树规范化为四种节点类型:stackoverlayabsoluteleaf。每个 screens/*.json 文件中的每个节点都是这四种之一。没有其他。

四种类型

stack

stack 是具有 Figma 自动布局的节点——layoutMode: "VERTICAL"layoutMode: "HORIZONTAL"。其子元素沿轴向流式定位。间距和内边距是明确的。

{
  "kind": "stack",
  "id": "123:456",
  "name": "ContentColumn",
  "axis": "vertical",
  "gap": 16,
  "paddingTop": 24,
  "paddingBottom": 24,
  "paddingLeft": 20,
  "paddingRight": 20,
  "primaryAxisAlignItems": "SPACE_BETWEEN",
  "counterAxisAlignItems": "CENTER",
  "width": 390,
  "height": 844,
  "fills": [{ "type": "SOLID", "color": "#1a1a2e" }],
  "children": [ ... ]
}

在 Jetpack Compose 中映射为 Column(垂直轴)或 Row(水平轴)。primaryAxisAlignItemscounterAxisAlignItems 字段分别映射到 ArrangementAlignmentgap 变为 Arrangement.spacedBy()

overlay

overlay 是子元素在其内部具有绝对位置的节点。Figma 将其表示为 layoutMode: "NONE" 的 Frame,子元素有 absoluteBoundingBox 坐标。overlay 类型捕获这一点,而不需要 Agent 通过推理原始坐标来理解结构。

{
  "kind": "overlay",
  "id": "123:789",
  "name": "CardOverlay",
  "width": 358,
  "height": 200,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "children": [
    {
      "kind": "absolute",
      "offset": { "x": 16, "y": 16 },
      "child": { ... }
    }
  ]
}

在 Compose 中映射为 Box。子元素使用 Modifier.offset()Modifier.align() 定位,具体取决于它们的位置与父元素边界的关系。

absolute

absolute 节点是一个包装器,在其父 overlay 内的特定 (x, y) 偏移处携带单个子元素。它是一个薄薄的结构节点——它的存在是为了保留来自 Figma 的空间关系,而不将位置与内容混为一谈。

{
  "kind": "absolute",
  "offset": { "x": 24, "y": 140 },
  "child": {
    "kind": "leaf",
    "name": "BadgeLabel",
    "text": "NEW",
    "stringRef": "badge.new",
    ...
  }
}

在 Compose 中,这通常成为带有 Modifier.offset(x.dp, y.dp)Box 的子元素。如果间距令牌在范围内存在,偏移值是令牌替换的候选。

leaf

leaf 是终端节点——没有子元素。它有样式(填充、描边、圆角半径)以及可选的文本内容。文本叶节点有 text 字段(字面字符串)和 stringRef 字段(来自 strings.json 的 i18n 资源键)。

{
  "kind": "leaf",
  "id": "123:101",
  "name": "SpeedLabel",
  "text": "Speed Test",
  "stringRef": "speed.test",
  "fontSize": 18,
  "fontWeight": 600,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "width": 160,
  "height": 28
}

对于文本内容映射为 Compose 的 Text() composable,对于非文本叶节点根据填充类型映射为 SurfaceBoxImage

四种类型与任何 UI 框架的组合模型一一对应。Stack 是流式布局。Overlay 是 z 轴定位的容器。Absolute 携带空间元数据。Leaf 是内容。就是这样。Figma 节点分类有 12 种以上的节点类型——IR 将其减少为四种。

如何确定类型

分类逻辑:

  1. 如果 layoutMode"VERTICAL""HORIZONTAL"stack
  2. 如果 layoutMode"NONE" 且节点有子元素 → overlay(子元素包装为 absolute
  3. 如果节点是 overlay 的直接子元素且携带位置 → absolute
  4. 如果节点没有子元素且不是 absolute 包装器 → leaf

layoutMode: "NONE" 的 Frame 的子元素有非平凡重叠边界框时,_meta.json 中的 layout-mode-none-inferred 警告会触发。figmascope 将其视为 overlay,但注明推断,因为实际上一些 layoutMode: "NONE" 的 Frame 只是单个子元素的容器(零重叠),可以简化。警告让 Agent 决定如何处理。

absoluteBoundingBox——为什么要保留它

IR 中的每个节点都保留其来自 Figma 的 absoluteBoundingBox,即使父元素是 stack(理论上绝对位置与布局无关):

{
  "kind": "stack",
  "absoluteBoundingBox": { "x": 0, "y": 88, "width": 390, "height": 756 },
  ...
}

这是为需要推断不共享直接父子关系的节点之间空间关系的 Agent 而存在的。将来自树的不同分支的两个元素重叠需要知道它们的绝对位置,而不仅仅是相对偏移。坐标系是 Figma 的画布——左上角原点,y 向下递增。

它还有助于 PNG 交叉参考。_meta.json 包含 pngCount 字段,导出的 PNG 按屏幕 slug 命名。如果你将 IR 与截图进行比较,absoluteBoundingBox 让你在图像中定位特定节点。要在自己的设计上查看这一点,从 figmascope.dev 导出包

容器类型上的填充和描边

一个常见的混淆点:stack 和 overlay 节点可以有填充,不只是叶节点。有背景颜色的列仍然是 stack——它只是还有一个 fills 数组。figmascope 保留了这一点:

{
  "kind": "stack",
  "axis": "vertical",
  "fills": [{ "type": "SOLID", "color": "#f6f2ea" }],
  "cornerRadius": 12,
  ...
}

在 Compose 中,这通常成为 SurfaceCard 内的 Column,填充颜色和圆角半径应用于容器。IR 不会将填充折叠到子元素中——它保留在 Figma 设置它的节点上。

容器类型上的渐变填充会发出 background-gradient-not-supported:<name> 警告。节点仍然以其他字段完整存在于 IR 中。Agent 应将填充视为待办事项,要么用纯色近似,要么按照 CONTEXT.md 范围说明生成自定义绘制调用。

stringRef + text 的关系

文本叶节点同时携带:

如果文本节点的内容因碰撞或过滤(仅数字、为空、太短)而从 strings.json 中删除,叶节点仍然有 textstringRef 将缺失。在那种情况下 Agent 应回退到字面值。有关完整的碰撞处理逻辑,请参阅 strings.json

当两者都存在时,CONTEXT.md 约束说使用 stringReftext 字段是为 Agent 推理而存在的(让它知道字符串说什么而无需打开 strings.json),也作为后备。

IR 中的组件实例

当 Figma 节点是组件的 INSTANCE 时,IR 节点携带两个额外字段:

{
  "kind": "stack",
  "componentId": "789:012",
  "componentName": "PrimaryButton",
  ...
}

这链接回 components/inventory.json。Agent 知道这个节点是 PrimaryButton 的实例而非定制布局,应该引用现有组件而不是生成重复代码。完整覆盖在 组件库存中。

真实屏幕示例

一个简化但结构上准确的示例,包含标题、内容列和浮动按钮的屏幕:

{
  "screen": "home",
  "root": {
    "kind": "overlay",
    "name": "HomeScreen",
    "width": 390,
    "height": 844,
    "fills": [{ "type": "SOLID", "color": "#0d0d1a" }],
    "children": [
      {
        "kind": "absolute",
        "offset": { "x": 0, "y": 0 },
        "child": {
          "kind": "stack",
          "name": "ContentArea",
          "axis": "vertical",
          "gap": 24,
          "paddingTop": 56,
          "paddingLeft": 20,
          "paddingRight": 20,
          "children": [
            {
              "kind": "leaf",
              "name": "Title",
              "text": "Speed Test",
              "stringRef": "speed.test",
              "fontSize": 28,
              "fontWeight": 700
            }
          ]
        }
      },
      {
        "kind": "absolute",
        "offset": { "x": 111, "y": 752 },
        "child": {
          "kind": "stack",
          "name": "StartButton",
          "componentId": "321:654",
          "componentName": "PrimaryButton",
          "axis": "horizontal",
          "gap": 8,
          "paddingTop": 16,
          "paddingBottom": 16,
          "paddingLeft": 32,
          "paddingRight": 32,
          "cornerRadius": 24,
          "fills": [{ "type": "SOLID", "color": "#7f5cfe" }]
        }
      }
    ]
  }
}

从这里 Agent 可以正确生成一个 Compose Box 屏幕,内容区域有绝对定位的 Column,底部有绝对定位的 PrimaryButton 组件。每个布局决策都可以从 IR 推导出来,无需猜测。

有关令牌如何应用于此结构中的间距和颜色值,请参阅 tokens.json 详解。有关使用此 IR 与 Cursor 或 Claude Code 的完整 Agent 工作流,请参阅 从 Figma 生成 Jetpack Compose。通过将你的 Figma URL 粘贴到 figmascope 来自己尝试。