Дерево узлов 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 сводит их к четырём.
Как определяется вид
Логика классификации:
- Если
layoutModeравен"VERTICAL"или"HORIZONTAL"→stack - Если
layoutModeравен"NONE"и узел имеет дочерние элементы →overlay(дочерние элементы обёрнуты какabsolute) - Если узел является прямым дочерним элементом overlay и несёт позицию →
absolute - Если узел не имеет дочерних элементов и не является абсолютной обёрткой →
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-узлы несут оба поля:
text: строковый литерал из Figma (например,"Speed Test")stringRef: ключ ресурса изstrings.json(например,"speed.test")
Если содержимое текстового узла было исключено из 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.