figma-preview-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -0
- package/dist/ast-enricher.d.ts +80 -0
- package/dist/ast-enricher.d.ts.map +1 -0
- package/dist/ast-enricher.js +318 -0
- package/dist/ast-enricher.js.map +1 -0
- package/dist/ast-module-splitter.d.ts +106 -0
- package/dist/ast-module-splitter.d.ts.map +1 -0
- package/dist/ast-module-splitter.js +325 -0
- package/dist/ast-module-splitter.js.map +1 -0
- package/dist/bounds-cache.d.ts +48 -0
- package/dist/bounds-cache.d.ts.map +1 -0
- package/dist/bounds-cache.js +71 -0
- package/dist/bounds-cache.js.map +1 -0
- package/dist/image-downloader.d.ts +20 -0
- package/dist/image-downloader.d.ts.map +1 -0
- package/dist/image-downloader.js +248 -0
- package/dist/image-downloader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1554 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-inference.d.ts +98 -0
- package/dist/layout-inference.d.ts.map +1 -0
- package/dist/layout-inference.js +486 -0
- package/dist/layout-inference.js.map +1 -0
- package/dist/layout-verifier.d.ts +91 -0
- package/dist/layout-verifier.d.ts.map +1 -0
- package/dist/layout-verifier.js +318 -0
- package/dist/layout-verifier.js.map +1 -0
- package/dist/parser.d.ts +120 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +310 -0
- package/dist/parser.js.map +1 -0
- package/dist/renderer.d.ts +8 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +639 -0
- package/dist/renderer.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# figma-preview-mcp
|
|
2
|
+
|
|
3
|
+
一个 Model Context Protocol (MCP) 服务器,可将 Figma 设计稿渲染为**高保真交互式预览**——带有节点检查器,可直接点击复制节点信息到 AI 对话中。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ✨ 功能特性
|
|
8
|
+
|
|
9
|
+
- 📸 **高保真预览** — 在浏览器中预览 Figma 设计稿,100% 还原视觉效果
|
|
10
|
+
- 🖱️ **节点检查器** — 悬停查看节点名称,点击复制节点信息到剪贴板
|
|
11
|
+
- 🔍 **节点详情查询** — 获取完整节点数据,包括被压缩的内联样式
|
|
12
|
+
- 📷 **截图验证** — 截取预览页面图片,在 AI 对话中展示
|
|
13
|
+
- 🔬 **像素级 Diff 对比** — 使用 pixelmatch 进行程序化像素级对比,生成差异高亮图 + 差异百分比统计
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🚀 快速开始
|
|
18
|
+
|
|
19
|
+
### 1. 获取 Figma Access Token
|
|
20
|
+
|
|
21
|
+
1. 打开 [Figma](https://www.figma.com),进入 **Account Settings**
|
|
22
|
+
2. 滚动到 **Personal access tokens**
|
|
23
|
+
3. 点击 **Generate new token**,命名并复制 token
|
|
24
|
+
|
|
25
|
+
### 2. 安装配置
|
|
26
|
+
|
|
27
|
+
在 MCP 配置文件中添加(如 `mcp.json` 或 Claude Desktop 的 `claude_desktop_config.json`):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"figma-preview-mcp": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "figma-preview-mcp"],
|
|
35
|
+
"env": {
|
|
36
|
+
"FIGMA_TOKEN": "figd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
37
|
+
"PIXELMATCH_THRESHOLD": "0.2"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> **注意:** 将 `FIGMA_TOKEN` 替换为你的 Figma 个人访问令牌。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🎮 使用方法
|
|
49
|
+
|
|
50
|
+
配置完成后,在 AI 对话中粘贴 Figma 链接:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
预览 https://www.figma.com/design/YOUR_FILE_KEY/MyApp?node-id=123-456
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
AI 会自动:
|
|
57
|
+
1. 解析 URL 中的 `fileKey` 和 `nodeId`
|
|
58
|
+
2. 直接从 Figma API 获取完整节点树(包含所有子节点)
|
|
59
|
+
3. 下载目标节点的高清截图
|
|
60
|
+
4. 在 **http://localhost:3456** 打开交互式预览
|
|
61
|
+
|
|
62
|
+
### 节点检查器
|
|
63
|
+
|
|
64
|
+
| 操作 | 效果 |
|
|
65
|
+
|------|------|
|
|
66
|
+
| **悬停** 任意元素 | 蓝色高亮边框 + 节点名称提示 |
|
|
67
|
+
| **点击** 任意元素 | 橙色选中边框 + 节点信息复制到剪贴板 |
|
|
68
|
+
| **再次点击** | 取消选中 |
|
|
69
|
+
|
|
70
|
+
复制的文本格式:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
节点路径: MyPage / header / navigation-bar / back-button
|
|
74
|
+
节点ID: 978:14642
|
|
75
|
+
类型: INSTANCE
|
|
76
|
+
fileKey: YOUR_FILE_KEY
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
直接粘贴到 AI 对话中即可引用该节点。
|
|
80
|
+
|
|
81
|
+
### 获取完整节点详情
|
|
82
|
+
|
|
83
|
+
当需要完整节点数据(如修复内联文字样式)时,AI 会调用 `get_node_details`:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
修复这个节点的样式
|
|
87
|
+
节点路径: MyPage / title
|
|
88
|
+
节点ID: 981:18766
|
|
89
|
+
类型: TEXT
|
|
90
|
+
fileKey: YOUR_FILE_KEY
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
AI 会自动获取完整数据,包括:
|
|
94
|
+
- `characterStyleOverrides` — 逐字符样式索引
|
|
95
|
+
- `styleOverrideTable` — 内联样式定义
|
|
96
|
+
- `fills`、`strokes`、`effects` — 完整视觉属性
|
|
97
|
+
- 更多...
|
|
98
|
+
|
|
99
|
+
### 截图与对比工具
|
|
100
|
+
|
|
101
|
+
#### 截取预览页面
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
截取当前预览页面的图片
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### 像素级 Diff 对比
|
|
108
|
+
|
|
109
|
+
使用 **pixelmatch** 进行程序化像素级对比。工作流程:
|
|
110
|
+
|
|
111
|
+
1. **渲染设计稿预览** — 调用 `render_preview` 获取 Figma 设计稿截图(自动返回 Base64 截图)
|
|
112
|
+
2. **截取开发环境截图** — 使用 Chrome DevTools MCP 的 `take_screenshot` 保存开发页面为 PNG
|
|
113
|
+
3. **执行像素级对比** — 调用 `compare_design_implementation` 对比两张截图
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
# Step 1: 渲染设计稿(figma-preview-mcp)
|
|
117
|
+
调用 render_preview:
|
|
118
|
+
fileKey: <fileKey>
|
|
119
|
+
nodeId: <nodeId>
|
|
120
|
+
# 返回结果包含 screenshot (Base64) 和 url,直接使用 screenshot 即可
|
|
121
|
+
|
|
122
|
+
# Step 2: 截取开发页面(Chrome DevTools MCP)
|
|
123
|
+
调用 resize_page: { width: 375, height: 812 }
|
|
124
|
+
调用 take_screenshot: { filePath: /tmp/dev.png }
|
|
125
|
+
|
|
126
|
+
# Step 3: 像素级对比(figma-preview-mcp)
|
|
127
|
+
调用 compare_design_implementation:
|
|
128
|
+
designImagePath: /tmp/design.png # 从 render_preview 的 screenshot 保存而来
|
|
129
|
+
devImagePath: /tmp/dev.png
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
AI 会返回:
|
|
133
|
+
|
|
134
|
+
1. **差异高亮图** — 红色区域表示像素不匹配
|
|
135
|
+
2. **差异统计** — 不匹配像素数、差异百分比
|
|
136
|
+
|
|
137
|
+
示例输出:
|
|
138
|
+
```
|
|
139
|
+
## ❌ 像素级对比结果
|
|
140
|
+
|
|
141
|
+
**差异统计:**
|
|
142
|
+
- 不匹配像素数:**12,345** / 608,400
|
|
143
|
+
- 差异率:**2.03%**
|
|
144
|
+
- 对比尺寸:750 × 812px
|
|
145
|
+
|
|
146
|
+
**结论:** 发现明显差异,请查看红色高亮区域。
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**可选参数:**
|
|
150
|
+
- `threshold` — 颜色差异灵敏度(0-1,默认 0.2,越小越敏感)
|
|
151
|
+
|
|
152
|
+
**如何解读 Diff 图:**
|
|
153
|
+
- 🔴 **红色区域** = 像素颜色不匹配(位置偏移、颜色错误、元素缺失)
|
|
154
|
+
- 🟠 **橙色区域** = 抗锯齿差异(通常可忽略)
|
|
155
|
+
- ⬛ **暗色区域** = 完全匹配
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 🔧 工作原理
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Figma URL
|
|
163
|
+
│
|
|
164
|
+
└─► figma-preview-mcp
|
|
165
|
+
│
|
|
166
|
+
├─► Figma REST API → 完整节点树(depth=100)
|
|
167
|
+
├─► 计算相对坐标 → 每个节点的 _absX / _absY
|
|
168
|
+
├─► Figma Export API → 高清 PNG 截图(2 倍分辨率)
|
|
169
|
+
│
|
|
170
|
+
└─► 本地 HTTP 服务器(端口 3456)
|
|
171
|
+
├── PNG 截图作为背景
|
|
172
|
+
└── 透明覆盖层(每个节点一个 div)
|
|
173
|
+
└── 悬停 / 点击 → 节点信息
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 为什么用截图 + 覆盖层,而不是 CSS 重建?
|
|
177
|
+
|
|
178
|
+
用 CSS 像素级还原 Figma 布局极其复杂(渐变、蒙版、混合模式、自动布局变体、组件覆盖...)。我们直接用 Figma 自身的渲染作为真实来源,在上面叠加一个透明交互层——100% 视觉保真度,零布局 bug。
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## ⚙️ 配置项
|
|
183
|
+
|
|
184
|
+
| 环境变量 | 必需 | 说明 |
|
|
185
|
+
|---------|------|------|
|
|
186
|
+
| `FIGMA_TOKEN` | ✅ 是 | Figma 个人访问令牌 |
|
|
187
|
+
| `PORT` | 否 | HTTP 服务器端口(默认 `3456`) |
|
|
188
|
+
| `PIXELMATCH_THRESHOLD` | 否 | 像素对比灵敏度阈值 0-1(默认 `0.2`,越小越敏感) |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 🛠️ 本地开发
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# 克隆仓库
|
|
196
|
+
cd figma-preview-mcp
|
|
197
|
+
|
|
198
|
+
# 安装依赖
|
|
199
|
+
npm install
|
|
200
|
+
|
|
201
|
+
# 构建
|
|
202
|
+
npm run build
|
|
203
|
+
|
|
204
|
+
# 在 MCP 配置中使用本地构建
|
|
205
|
+
# 在 mcp.json 中:
|
|
206
|
+
# "args": ["/absolute/path/to/figma-preview-mcp/dist/index.js"]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 📋 系统要求
|
|
212
|
+
|
|
213
|
+
- Node.js ≥ 18.0.0
|
|
214
|
+
- 有效的 Figma 访问令牌
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 📄 许可证
|
|
219
|
+
|
|
220
|
+
MIT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ParsedFigmaData } from './parser.js';
|
|
2
|
+
import { type InferredLayout } from './layout-inference.js';
|
|
3
|
+
export interface EnrichedASTNode {
|
|
4
|
+
nodeId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
bounds: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
layout: InferredLayout;
|
|
14
|
+
style?: {
|
|
15
|
+
backgroundColor?: string;
|
|
16
|
+
borderRadius?: number | string;
|
|
17
|
+
opacity?: number;
|
|
18
|
+
overflow?: string;
|
|
19
|
+
boxShadow?: string;
|
|
20
|
+
border?: string;
|
|
21
|
+
};
|
|
22
|
+
text?: {
|
|
23
|
+
content: string;
|
|
24
|
+
fontFamily?: string;
|
|
25
|
+
fontSize?: number;
|
|
26
|
+
fontWeight?: number | string;
|
|
27
|
+
color?: string;
|
|
28
|
+
lineHeight?: number | string;
|
|
29
|
+
letterSpacing?: number | string;
|
|
30
|
+
textAlign?: string;
|
|
31
|
+
/** Rich text segments with mixed styles (e.g. highlighted words) */
|
|
32
|
+
styledSegments?: Array<{
|
|
33
|
+
text: string;
|
|
34
|
+
color?: string;
|
|
35
|
+
fontWeight?: number | string;
|
|
36
|
+
fontFamily?: string;
|
|
37
|
+
fontSize?: number;
|
|
38
|
+
fontStyle?: string;
|
|
39
|
+
textDecoration?: string;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
image?: {
|
|
43
|
+
type: 'raster' | 'vector' | 'svg';
|
|
44
|
+
imageRef?: string;
|
|
45
|
+
suggestedFormat: 'png' | 'svg' | 'jpg';
|
|
46
|
+
};
|
|
47
|
+
flags?: {
|
|
48
|
+
isGraphicSubtree?: boolean;
|
|
49
|
+
needsDetailFetch?: boolean;
|
|
50
|
+
isInvisible?: boolean;
|
|
51
|
+
hasComplexFills?: boolean;
|
|
52
|
+
/** This node uses position:absolute to overlay on top of sibling (negative-gap pattern) */
|
|
53
|
+
isAbsoluteOverlay?: boolean;
|
|
54
|
+
};
|
|
55
|
+
children?: EnrichedASTNode[];
|
|
56
|
+
}
|
|
57
|
+
export interface EnrichedASTStats {
|
|
58
|
+
totalNodes: number;
|
|
59
|
+
figmaLayoutNodes: number;
|
|
60
|
+
inferredLayoutNodes: number;
|
|
61
|
+
absoluteNodes: number;
|
|
62
|
+
needsDetailFetch: number;
|
|
63
|
+
}
|
|
64
|
+
export interface EnrichedAST {
|
|
65
|
+
version: '1.0';
|
|
66
|
+
fileKey: string;
|
|
67
|
+
rootNodeId: string;
|
|
68
|
+
viewport: {
|
|
69
|
+
width: number;
|
|
70
|
+
height: number;
|
|
71
|
+
};
|
|
72
|
+
stats: EnrichedASTStats;
|
|
73
|
+
root: EnrichedASTNode;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build an EnrichedAST from a ParsedFigmaData node tree.
|
|
77
|
+
* The nodeTree should already have _absX/_absY injected (from injectBounds).
|
|
78
|
+
*/
|
|
79
|
+
export declare function enrichAST(nodeTree: ParsedFigmaData, fileKey: string, rootNodeId: string): EnrichedAST;
|
|
80
|
+
//# sourceMappingURL=ast-enricher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-enricher.d.ts","sourceRoot":"","sources":["../src/ast-enricher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { inferLayoutForNode } from './layout-inference.js';
|
|
2
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
3
|
+
const VECTOR_LEAF_TYPES = new Set([
|
|
4
|
+
'VECTOR', 'ELLIPSE', 'RECTANGLE', 'LINE', 'REGULAR_POLYGON', 'STAR',
|
|
5
|
+
'BOOLEAN_OPERATION', 'IMAGE-SVG', 'SVG', 'IMAGE',
|
|
6
|
+
]);
|
|
7
|
+
const IMAGE_TYPES = new Set(['IMAGE-SVG', 'SVG', 'IMAGE', 'VECTOR']);
|
|
8
|
+
// ─── Node Classification Helpers ────────────────────────────────────────────
|
|
9
|
+
function isNodeInvisible(node) {
|
|
10
|
+
if (node._visible === false)
|
|
11
|
+
return true;
|
|
12
|
+
if (node.style?.opacity === 0)
|
|
13
|
+
return true;
|
|
14
|
+
const w = node.layout?.width ?? 0;
|
|
15
|
+
const h = node.layout?.height ?? 0;
|
|
16
|
+
if (w === 0 && h === 0)
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function isGraphicOnlySubtree(node) {
|
|
21
|
+
if (VECTOR_LEAF_TYPES.has(node.type))
|
|
22
|
+
return true;
|
|
23
|
+
if (node.imageRef != null)
|
|
24
|
+
return true;
|
|
25
|
+
if (node.type === 'TEXT')
|
|
26
|
+
return false;
|
|
27
|
+
if (node.type === 'FRAME') {
|
|
28
|
+
const mode = node.layout?.mode;
|
|
29
|
+
if (mode === 'row' || mode === 'column')
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!node.children || node.children.length === 0)
|
|
33
|
+
return true;
|
|
34
|
+
return node.children.every(child => isGraphicOnlySubtree(child));
|
|
35
|
+
}
|
|
36
|
+
function isImageNode(node) {
|
|
37
|
+
return IMAGE_TYPES.has(node.type) || node.imageRef != null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detect if a node needs a get_node_details call for full style data.
|
|
41
|
+
*/
|
|
42
|
+
function detectNeedsDetailFetch(node) {
|
|
43
|
+
let needsDetailFetch = false;
|
|
44
|
+
let hasComplexFills = false;
|
|
45
|
+
// Check for gradient/complex fills
|
|
46
|
+
if (node.style?.backgroundColor) {
|
|
47
|
+
const bg = node.style.backgroundColor;
|
|
48
|
+
if (bg.includes('gradient')) {
|
|
49
|
+
// Gradient string may be incomplete from initial API
|
|
50
|
+
hasComplexFills = true;
|
|
51
|
+
needsDetailFetch = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Check fills reference for multiple fills
|
|
55
|
+
if (Array.isArray(node.fills) && node.fills.length > 1) {
|
|
56
|
+
hasComplexFills = true;
|
|
57
|
+
needsDetailFetch = true;
|
|
58
|
+
}
|
|
59
|
+
// TEXT node without textStyle (missing font data)
|
|
60
|
+
if (node.type === 'TEXT' && !node.textStyle?.fontFamily && !node.textStyle?.fontSize) {
|
|
61
|
+
needsDetailFetch = true;
|
|
62
|
+
}
|
|
63
|
+
return { needsDetailFetch, hasComplexFills };
|
|
64
|
+
}
|
|
65
|
+
// ─── Node Enrichment ────────────────────────────────────────────────────────
|
|
66
|
+
function enrichNode(node, parentBounds) {
|
|
67
|
+
// Skip invisible nodes
|
|
68
|
+
if (isNodeInvisible(node))
|
|
69
|
+
return null;
|
|
70
|
+
const bounds = {
|
|
71
|
+
x: node._absX ?? node.layout?.x ?? 0,
|
|
72
|
+
y: node._absY ?? node.layout?.y ?? 0,
|
|
73
|
+
width: node.layout?.width ?? 0,
|
|
74
|
+
height: node.layout?.height ?? 0,
|
|
75
|
+
};
|
|
76
|
+
const isText = node.type === 'TEXT';
|
|
77
|
+
const isImage = isImageNode(node);
|
|
78
|
+
const isGraphic = isGraphicOnlySubtree(node);
|
|
79
|
+
// Build child bounds for layout inference (only for non-leaf, non-graphic nodes)
|
|
80
|
+
// Use the same coordinate source as bounds (i.e. _absX ?? layout.x) for consistency.
|
|
81
|
+
const visibleChildren = (node.children ?? []).filter(c => !isNodeInvisible(c));
|
|
82
|
+
// Separate absolutely-positioned children from flex flow children.
|
|
83
|
+
// Figma `layoutPositioning: "ABSOLUTE"` means the child is positioned absolutely
|
|
84
|
+
// within an Auto Layout parent — it doesn't participate in flex flow.
|
|
85
|
+
const absoluteChildren = visibleChildren.filter(c => c.layout?.layoutPositioning === 'ABSOLUTE');
|
|
86
|
+
const flowChildren = visibleChildren.filter(c => c.layout?.layoutPositioning !== 'ABSOLUTE');
|
|
87
|
+
const childBoundsList = flowChildren.map(c => ({
|
|
88
|
+
nodeId: c.id,
|
|
89
|
+
x: c._absX ?? c.layout?.x ?? 0,
|
|
90
|
+
y: c._absY ?? c.layout?.y ?? 0,
|
|
91
|
+
width: c.layout?.width ?? 0,
|
|
92
|
+
height: c.layout?.height ?? 0,
|
|
93
|
+
}));
|
|
94
|
+
// Infer layout (use same bounds as used for this node's output bounds)
|
|
95
|
+
// Only pass flow children (not absolute ones) to the inference engine
|
|
96
|
+
const layout = (!isText && !isImage && !isGraphic)
|
|
97
|
+
? inferLayoutForNode(node, childBoundsList, bounds)
|
|
98
|
+
: {
|
|
99
|
+
source: 'figma',
|
|
100
|
+
display: 'block',
|
|
101
|
+
selfSizing: {
|
|
102
|
+
horizontal: (node.layout?.horizontalSizing ?? 'fixed'),
|
|
103
|
+
vertical: (node.layout?.verticalSizing ?? 'fixed'),
|
|
104
|
+
...((node.layout?.horizontalSizing === 'fill' || node.layout?.verticalSizing === 'fill') ? { flex: 1 } : {}),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
// Build enriched node
|
|
108
|
+
const enriched = {
|
|
109
|
+
nodeId: node.id,
|
|
110
|
+
name: node.name,
|
|
111
|
+
type: node.type,
|
|
112
|
+
bounds,
|
|
113
|
+
layout,
|
|
114
|
+
};
|
|
115
|
+
// Style — for TEXT nodes, remove backgroundColor (it's the text fill color, not a background)
|
|
116
|
+
if (node.style && Object.keys(node.style).length > 0) {
|
|
117
|
+
if (isText) {
|
|
118
|
+
const { backgroundColor, ...restStyle } = node.style;
|
|
119
|
+
if (Object.keys(restStyle).length > 0) {
|
|
120
|
+
enriched.style = restStyle;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
enriched.style = { ...node.style };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Text
|
|
128
|
+
if (isText) {
|
|
129
|
+
enriched.text = {
|
|
130
|
+
content: node.text ?? node.name,
|
|
131
|
+
fontFamily: node.textStyle?.fontFamily,
|
|
132
|
+
fontSize: node.textStyle?.fontSize,
|
|
133
|
+
fontWeight: node.textStyle?.fontWeight,
|
|
134
|
+
color: node.textStyle?.color,
|
|
135
|
+
lineHeight: node.textStyle?.lineHeight,
|
|
136
|
+
letterSpacing: node.textStyle?.letterSpacing,
|
|
137
|
+
textAlign: node.textStyle?.textAlign,
|
|
138
|
+
};
|
|
139
|
+
// Rich text: include styled segments if present
|
|
140
|
+
if (node.styledSegments && node.styledSegments.length > 0) {
|
|
141
|
+
enriched.text.styledSegments = node.styledSegments;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Image
|
|
145
|
+
if (isImage || isGraphic) {
|
|
146
|
+
const hasImageRef = node.imageRef != null;
|
|
147
|
+
enriched.image = {
|
|
148
|
+
type: hasImageRef ? 'raster' : 'vector',
|
|
149
|
+
imageRef: node.imageRef,
|
|
150
|
+
suggestedFormat: hasImageRef ? 'png' : 'svg',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Flags
|
|
154
|
+
const detailCheck = detectNeedsDetailFetch(node);
|
|
155
|
+
const flags = {};
|
|
156
|
+
if (isGraphic)
|
|
157
|
+
flags.isGraphicSubtree = true;
|
|
158
|
+
if (detailCheck.needsDetailFetch)
|
|
159
|
+
flags.needsDetailFetch = true;
|
|
160
|
+
if (detailCheck.hasComplexFills)
|
|
161
|
+
flags.hasComplexFills = true;
|
|
162
|
+
if (Object.keys(flags).length > 0) {
|
|
163
|
+
enriched.flags = flags;
|
|
164
|
+
}
|
|
165
|
+
// Recurse children (skip for graphic subtrees and leaf nodes)
|
|
166
|
+
if (!isText && !isImage && !isGraphic && visibleChildren.length > 0) {
|
|
167
|
+
const enrichedChildren = [];
|
|
168
|
+
// Check if any children are "overlapping" (negative-gap pattern)
|
|
169
|
+
const overlappingIds = new Set(layout.overlappingChildIds ?? []);
|
|
170
|
+
// Build set of Figma-absolute children (layoutPositioning: ABSOLUTE)
|
|
171
|
+
const figmaAbsoluteIds = new Set(absoluteChildren.map(c => c.id));
|
|
172
|
+
for (const child of visibleChildren) {
|
|
173
|
+
const enrichedChild = enrichNode(child, bounds);
|
|
174
|
+
if (!enrichedChild)
|
|
175
|
+
continue;
|
|
176
|
+
// Handle Figma absolute-positioned children (layoutPositioning: "ABSOLUTE")
|
|
177
|
+
if (figmaAbsoluteIds.has(child.id)) {
|
|
178
|
+
const childBounds = enrichedChild.bounds;
|
|
179
|
+
const relLeft = childBounds.x - bounds.x;
|
|
180
|
+
const relTop = childBounds.y - bounds.y;
|
|
181
|
+
const relRight = bounds.width - (relLeft + childBounds.width);
|
|
182
|
+
const relBottom = bounds.height - (relTop + childBounds.height);
|
|
183
|
+
// Use Figma constraints to decide which edges to pin to
|
|
184
|
+
const hConstraint = child.layout?.constraintHorizontal ?? 'LEFT';
|
|
185
|
+
const vConstraint = child.layout?.constraintVertical ?? 'TOP';
|
|
186
|
+
const posProps = {};
|
|
187
|
+
if (hConstraint === 'RIGHT') {
|
|
188
|
+
posProps.right = relRight;
|
|
189
|
+
}
|
|
190
|
+
else if (hConstraint === 'CENTER') {
|
|
191
|
+
posProps.left = relLeft;
|
|
192
|
+
}
|
|
193
|
+
else if (hConstraint === 'LEFT_RIGHT') {
|
|
194
|
+
posProps.left = relLeft;
|
|
195
|
+
posProps.right = relRight;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
posProps.left = relLeft;
|
|
199
|
+
} // LEFT or default
|
|
200
|
+
if (vConstraint === 'BOTTOM') {
|
|
201
|
+
posProps.bottom = relBottom;
|
|
202
|
+
}
|
|
203
|
+
else if (vConstraint === 'CENTER') {
|
|
204
|
+
posProps.top = relTop;
|
|
205
|
+
}
|
|
206
|
+
else if (vConstraint === 'TOP_BOTTOM') {
|
|
207
|
+
posProps.top = relTop;
|
|
208
|
+
posProps.bottom = relBottom;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
posProps.top = relTop;
|
|
212
|
+
} // TOP or default
|
|
213
|
+
enrichedChild.layout = {
|
|
214
|
+
...enrichedChild.layout,
|
|
215
|
+
source: 'figma',
|
|
216
|
+
display: enrichedChild.layout.display,
|
|
217
|
+
position: 'absolute',
|
|
218
|
+
...posProps,
|
|
219
|
+
};
|
|
220
|
+
if (!enrichedChild.flags)
|
|
221
|
+
enrichedChild.flags = {};
|
|
222
|
+
enrichedChild.flags.isAbsoluteOverlay = true;
|
|
223
|
+
enrichedChildren.push(enrichedChild);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Override layout for overlapping children (negative-gap heuristic, backup)
|
|
227
|
+
if (overlappingIds.has(child.id)) {
|
|
228
|
+
const childBounds = enrichedChild.bounds;
|
|
229
|
+
// Compute position relative to parent's top-left corner
|
|
230
|
+
const relLeft = childBounds.x - bounds.x;
|
|
231
|
+
const relTop = childBounds.y - bounds.y;
|
|
232
|
+
const relRight = bounds.width - (relLeft + childBounds.width);
|
|
233
|
+
const relBottom = bounds.height - (relTop + childBounds.height);
|
|
234
|
+
// Determine which edge to pin to (whichever is closer)
|
|
235
|
+
const pinRight = relRight <= relLeft;
|
|
236
|
+
const pinBottom = relBottom <= relTop;
|
|
237
|
+
enrichedChild.layout = {
|
|
238
|
+
...enrichedChild.layout,
|
|
239
|
+
source: 'figma',
|
|
240
|
+
display: 'block',
|
|
241
|
+
position: 'absolute',
|
|
242
|
+
top: relTop,
|
|
243
|
+
...(pinRight ? { right: relRight } : { left: relLeft }),
|
|
244
|
+
...(pinBottom ? { bottom: relBottom } : {}),
|
|
245
|
+
};
|
|
246
|
+
// Add a hint flag
|
|
247
|
+
if (!enrichedChild.flags)
|
|
248
|
+
enrichedChild.flags = {};
|
|
249
|
+
enrichedChild.flags.isAbsoluteOverlay = true;
|
|
250
|
+
}
|
|
251
|
+
enrichedChildren.push(enrichedChild);
|
|
252
|
+
}
|
|
253
|
+
if (enrichedChildren.length > 0) {
|
|
254
|
+
enriched.children = enrichedChildren;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return enriched;
|
|
258
|
+
}
|
|
259
|
+
// ─── Stats ──────────────────────────────────────────────────────────────────
|
|
260
|
+
function buildStats(root) {
|
|
261
|
+
const stats = {
|
|
262
|
+
totalNodes: 0,
|
|
263
|
+
figmaLayoutNodes: 0,
|
|
264
|
+
inferredLayoutNodes: 0,
|
|
265
|
+
absoluteNodes: 0,
|
|
266
|
+
needsDetailFetch: 0,
|
|
267
|
+
};
|
|
268
|
+
function walk(node) {
|
|
269
|
+
stats.totalNodes++;
|
|
270
|
+
if (node.layout.source === 'figma')
|
|
271
|
+
stats.figmaLayoutNodes++;
|
|
272
|
+
else if (node.layout.source === 'inferred')
|
|
273
|
+
stats.inferredLayoutNodes++;
|
|
274
|
+
else if (node.layout.source === 'absolute')
|
|
275
|
+
stats.absoluteNodes++;
|
|
276
|
+
if (node.flags?.needsDetailFetch)
|
|
277
|
+
stats.needsDetailFetch++;
|
|
278
|
+
if (node.children) {
|
|
279
|
+
node.children.forEach(walk);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
walk(root);
|
|
283
|
+
return stats;
|
|
284
|
+
}
|
|
285
|
+
// ─── Main Entry Point ───────────────────────────────────────────────────────
|
|
286
|
+
/**
|
|
287
|
+
* Build an EnrichedAST from a ParsedFigmaData node tree.
|
|
288
|
+
* The nodeTree should already have _absX/_absY injected (from injectBounds).
|
|
289
|
+
*/
|
|
290
|
+
export function enrichAST(nodeTree, fileKey, rootNodeId) {
|
|
291
|
+
const rootNode = nodeTree.nodes[0];
|
|
292
|
+
if (!rootNode) {
|
|
293
|
+
throw new Error('No root node found in nodeTree');
|
|
294
|
+
}
|
|
295
|
+
const rootBounds = {
|
|
296
|
+
x: 0,
|
|
297
|
+
y: 0,
|
|
298
|
+
width: rootNode.layout?.width ?? 0,
|
|
299
|
+
height: rootNode.layout?.height ?? 0,
|
|
300
|
+
};
|
|
301
|
+
const enrichedRoot = enrichNode(rootNode, rootBounds);
|
|
302
|
+
if (!enrichedRoot) {
|
|
303
|
+
throw new Error('Root node was filtered out (invisible?)');
|
|
304
|
+
}
|
|
305
|
+
const stats = buildStats(enrichedRoot);
|
|
306
|
+
return {
|
|
307
|
+
version: '1.0',
|
|
308
|
+
fileKey,
|
|
309
|
+
rootNodeId: rootNodeId.replace(/-/g, ':'),
|
|
310
|
+
viewport: {
|
|
311
|
+
width: rootBounds.width,
|
|
312
|
+
height: rootBounds.height,
|
|
313
|
+
},
|
|
314
|
+
stats,
|
|
315
|
+
root: enrichedRoot,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=ast-enricher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-enricher.js","sourceRoot":"","sources":["../src/ast-enricher.ts"],"names":[],"mappings":"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"}
|