FigmaのノードツリーはリッチですがノイジーEDEKです。Frame、Group、Component、Instance、Text、Rectangle、Ellipse、Vector—それぞれが何十もの任意フィールドを持ち、一部は互いに競合します。生のFigma APIレスポンスから直接作業するエージェントはスキーマを理解するためにコグニティブバジェットを費やし、コードを書く代わりに理解に時間を取られます。
figmascopeはツリーを4つのノード種類に正規化します:stack、overlay、absolute、leaf。すべてのscreens/*.jsonファイルのすべてのノードはこれら4つのいずれかです。それ以外はありません。
4つの種類
stack
スタックはFigmaオートレイアウトを持つノードです—layoutMode: "VERTICAL"またはlayoutMode: "HORIZONTAL"。その子はEDEK軸に沿ってフロー配置されます。ギャップとパディングは明示的です。
{
"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
オーバーレイはその子がその内部でアブソリュート位置を持つノードです。FigmaはこれをlayoutMode: "NONE"で子がabsoluteBoundingBox座標を持つFrameとして表現します。オーバーレイ種類はエージェントが構造を理解するために生の座標を推論せずに済むようにこれを捉えます。
{
"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
アブソリュートノードは親オーバーレイ内の特定の(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
リーフは末端ノードです—子なし。スタイリング(フィル、ストローク、コーナー半径)と任意でテキストコンテンツを持ちます。テキストリーフにはEDEKtextフィールド(リテラル文字列)と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
}
テキストコンテンツにはText()コンポーザブルとして、非テキストリーフにはフィルタイプによってSurface、Box、またはImageとしてComposeにマッピングされます。
4つの種類はどんなUIフレームワークの合成モデルとも1:1でマッピングされます。スタックはフローレイアウト。オーバーレイはz位置コンテナ。アブソリュートは空間メタデータを持ちます。リーフはコンテンツです。以上です。FigmaのノードタクソノミーはEDEK12以上のノードタイプを持ちます—IRはそれを4つに削減します。
種類の決定方法
分類ロジック:
layoutModeが"VERTICAL"または"HORIZONTAL"なら→stacklayoutModeが"NONE"でノードに子があれば→overlay(子はabsoluteとしてラップ)- ノードがオーバーレイの直接の子で位置を持てば→
absolute - ノードに子がなくアブソリュートラッパーでもなければ→
leaf
layout-mode-none-inferred警告は_meta.jsonで、layoutMode: "NONE"のFrameがEDEK非自明に重なり合う境界ボックスを持つ子を持つときに発火します。figmascopeはオーバーレイとして扱いますが、実際のいくつかのlayoutMode: "NONE"フレームは単一の子のコンテナ(ゼロ重複)であり単純化できるため推論に注記します。警告はエージェントが処理方法を決定できるようにします。
absoluteBoundingBox — なぜ保持されるか
IRのすべてのノードはFigmaからのabsoluteBoundingBoxを保持します。親がスタックの場合(絶対位置が理論的にはレイアウトに無関係)でも:
{
"kind": "stack",
"absoluteBoundingBox": { "x": 0, "y": 88, "width": 390, "height": 756 },
...
}
これは直接の親子関係を共有しないノード間の空間関係を推論する必要があるエージェントのためにあります。ツリーの異なるブランチからの2つの要素を重ねるには、相対オフセットだけでなく絶対位置を知る必要があります。座標系はFigmaのキャンバスです—左上原点、y軸は下向きに増加。
PNGクロスリファレンスにも役立ちます。_meta.jsonにはpngCountフィールドが含まれ、エクスポートされたPNGは画面スラグで名前付けされます。IRをスクリーンショットと比較する場合、absoluteBoundingBoxを使って画像内の特定ノードを見つけられます。
コンテナ種類のフィルとストローク
よくある混乱点:スタックとオーバーレイノードはリーフだけでなくフィルを持てます。背景色を持つColumnはまだスタックです—ただしfills配列も持ちます。figmascopeはこれを保持します:
{
"kind": "stack",
"axis": "vertical",
"fills": [{ "type": "SOLID", "color": "#f6f2ea" }],
"cornerRadius": 12,
...
}
Composeでは通常SurfaceまたはCard内のColumnになり、フィルカラーとコーナー半径がコンテナに適用されます。IRはフィルを子に折り畳まず—Figmaが設定したノードに保持します。
コンテナ種類のグラデーションフィルはbackground-gradient-not-supported:<name>警告を発生させます。ノードはIRに他のフィールドと共に存在し続けます。エージェントはフィルをTODOとして扱い、CONTEXT.mdのスコープ注記に従ってソリッドカラーで近似するかカスタム描画コールを生成するかを選択すべきです。
stringRef + textの関係
テキストリーフノードは両方を持ちます:
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ノードには2つの追加フィールドがあります:
{
"kind": "stack",
"componentId": "789:012",
"componentName": "PrimaryButton",
...
}
これはcomponents/inventory.jsonにリンクします。エージェントはこのノードがEDEK独自レイアウトではなく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" }]
}
}
]
}
}
これからエージェントはコンテンツ用のアブソリュート配置のColumnと下部のアブソリュート配置のPrimaryButtonコンポーネントを持つComposeBox画面を正しく生成できます。すべてのレイアウト決定はIRから推測なしに導出可能です。
この構造のスペーシングとカラー値にトークンがどう適用されるかはtokens.json 解説で。CursorまたはClaude Codeを使ったこのIRの完全なエージェントワークフローはFigmaからJetpack Composeで。figmascopeにFigma URLを貼り付けて自分で試してみてください。