Дерево узлов Figma богато, но зашумлено. Frame, Group, Component, Instance, Text, Rectangle, Ellipse, Vector — у каждого десятки опциональных полей, некоторые из которых конфликтуют друг с другом. Агент, работающий напрямую с сырым ответом Figma API, тратит контекстный бюджет на понимание схемы вместо написания кода.

figmascope нормализует дерево в четыре вида узлов: stack, overlay, absolute и leaf. Каждый узел в каждом файле screens/*.json является одним из этих четырёх. Ничего больше.

Четыре вида

stack

Stack — это узел с Auto Layout Figma — layoutMode: "VERTICAL" или layoutMode: "HORIZONTAL". Его дочерние элементы располагаются по оси в потоке. Отступы и padding явные.

{
  "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 (горизонтальная ось). Поля primaryAxisAlignItems и counterAxisAlignItems отображаются на Arrangement и Alignment соответственно. gap становится Arrangement.spacedBy().

overlay

Overlay — это узел, дочерние элементы которого имеют абсолютные позиции внутри него. Figma представляет это как Frame с layoutMode: "NONE", где дочерние элементы имеют координаты absoluteBoundingBox. Вид overlay захватывает это, не требуя от агента рассуждений о сырых координатах для понимания структуры.

{
  "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-узел — это обёртка, несущая единственный дочерний элемент со смещением (x, y) внутри родительского overlay. Это тонкий структурный узел — он существует для сохранения пространственного отношения из Figma без смешения позиции с содержимым.

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

В Compose это обычно становится дочерним элементом Box с Modifier.offset(x.dp, y.dp). Значения смещения — кандидаты для замены токенами, если существуют токены отступов в нужном диапазоне.

leaf

Leaf — это терминальный узел — без дочерних элементов. Он имеет стилизацию (заливки, обводки, радиус скруглений) и опционально текстовое содержимое. Текстовые листья имеют поле text (строковый литерал) и поле stringRef (ключ i18n-ресурса из strings.json).

{
  "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 как Composable Text() для текстового содержимого, или как Surface, Box или Image для нетекстовых листьев в зависимости от типа заливки.

Четыре вида отображаются один к одному на композиционную модель любого UI-фреймворка. Stacks — это flow-вёрстка. Overlays — контейнеры с z-позиционированием. Absolutes несут пространственные метаданные. Leafs — это содержимое. Вот и всё. Таксономия узлов Figma насчитывает более 12 типов узлов — IR сводит их к четырём.

Как определяется вид

Логика классификации:

  1. Если layoutMode равен "VERTICAL" или "HORIZONTAL"stack
  2. Если layoutMode равен "NONE" и узел имеет дочерние элементы → overlay (дочерние элементы обёрнуты как absolute)
  3. Если узел является прямым дочерним элементом overlay и несёт позицию → absolute
  4. Если узел не имеет дочерних элементов и не является абсолютной обёрткой → leaf

Предупреждение layout-mode-none-inferred в _meta.json срабатывает, когда Frame с layoutMode: "NONE" имеет дочерние элементы с нетривиально перекрывающимися ограничивающими прямоугольниками. figmascope считает это overlay, но фиксирует вывод, потому что некоторые фреймы с layoutMode: "NONE" на практике просто являются контейнерами для одного дочернего элемента (нулевое перекрытие) и могут быть упрощены. Предупреждение позволяет агенту самому решить, как с этим обращаться.

absoluteBoundingBox — почему он сохраняется

Каждый узел в IR сохраняет свой absoluteBoundingBox из Figma, даже когда родитель является stack (где абсолютные позиции теоретически не важны для вёрстки):

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

Это необходимо для агентов, которым нужно рассуждать о пространственных отношениях между узлами, не имеющими прямой связи родитель-дочерний. Наложение двух элементов из разных ветвей дерева требует знания их абсолютных позиций, а не только относительных смещений. Система координат — холст Figma: начало в верхнем левом углу, y возрастает вниз.

Это также помогает с кросс-референцией PNG. _meta.json включает поле pngCount, а экспортированные PNG именуются по слагу экрана. При сравнении IR со скриншотом absoluteBoundingBox позволяет найти конкретные узлы на изображении. Чтобы увидеть это в вашем собственном дизайне, экспортируйте бандл из figmascope.dev.

Заливки и обводки на видах-контейнерах

Часто возникающая путаница: узлы stack и overlay могут иметь заливки, а не только листья. Колонка с фоновым цветом остаётся stack — просто у неё есть массив fills. figmascope это сохраняет:

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

В Compose это обычно становится Column внутри Surface или Card с цветом заливки и радиусом скруглений, применёнными к контейнеру. IR не сворачивает заливку в дочерние элементы — он сохраняет её на узле, где Figma её задала.

Градиентные заливки на видах-контейнерах генерируют предупреждение background-gradient-not-supported:<name>. Узел всё равно присутствует в IR с остальными полями нетронутыми. Агент должен трактовать заливку как TODO и либо приблизить её сплошным цветом, либо сгенерировать кастомный вызов рисования согласно примечаниям об области применения в CONTEXT.md.

Отношение stringRef + text

Текстовые leaf-узлы несут оба поля:

Если содержимое текстового узла было исключено из strings.json из-за коллизии или фильтра (только цифры, пустое, слишком короткое), лист всё равно имеет text, но stringRef будет отсутствовать. Агент должен откатиться к литералу в этом случае. Полная логика обработки коллизий описана в strings.json.

Когда присутствуют оба, ограничение CONTEXT.md говорит использовать stringRef. Поле text существует для рассуждений агента (чтобы он знал, что говорит строка, без открытия strings.json) и как запасной вариант.

Экземпляры компонентов в IR

Когда узел Figma является INSTANCE компонента, IR-узел несёт два дополнительных поля:

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

Это ссылается на components/inventory.json. Агент знает, что этот узел является экземпляром 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" }]
        }
      }
    ]
  }
}

Из этого агент может корректно сгенерировать Compose-экран Box с абсолютно-позиционированной Column для контента и абсолютно-позиционированным компонентом PrimaryButton внизу. Каждое решение по вёрстке выводится из IR без угадывания.

Для того, как токены применяются к значениям отступов и цветов в этой структуре, см. tokens.json: объяснение. Для полного агентного рабочего процесса с этим IR в Cursor или Claude Code, см. Jetpack Compose из Figma. Попробуйте сами, вставив URL Figma в figmascope.