figma-local-context-mcp/FIG_DATA_STRUCTURE_ANALYSIS.md

638 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# .fig 数据结构与组装逻辑分析
本文档基于当前项目源码,说明本地 `.fig` 文件如何被解析成可查询的设计上下文,以及如何进一步组装成 SVG/PNG 导出结果。
## 1. 总体链路
```mermaid
flowchart TD
A["本地 .fig / .fig.json"] --> B["loadFigFile"]
B --> C["FigJson"]
C --> D["nodeChanges 扁平节点数组"]
C --> E["blobs 几何路径数据"]
A --> F["images/<hash> 图片资源"]
D --> G["getChildrenByParent 重建父子关系"]
G --> H["设计上下文: serializeNode / tokens / codeHints"]
G --> I["renderNodeToSvg 递归渲染"]
E --> I
F --> I
I --> J["SVG"]
J --> K["resvg 栅格化 PNG"]
```
核心结论:
- `.fig` 解码后的节点不是树形结构,而是 `nodeChanges` 扁平数组。
- 节点树通过 `guid``parentIndex.guid` 在运行时重建。
- 子节点层级顺序通过 `parentIndex.position` 排序。
- 矢量路径不直接保存在节点字段里,而是通过 `commandsBlob` 引用 `blobs[index]`
- 图片资源来自 `.fig` 外层 zip 的 `images/<hash>`,不是来自 `FigJson.blobs`
## 2. 文件读取与解码
入口:`src/services/fig-file.ts`
```ts
export function loadFigFile(filePath: string): FigJson {
const absolutePath = path.resolve(filePath)
if (absolutePath.toLowerCase().endsWith(".json")) {
return JSON.parse(fs.readFileSync(absolutePath, "utf8")) as FigJson
}
return figToJson(fs.readFileSync(absolutePath))
}
```
读取策略:
- `.fig.json`:直接作为已经解码过的 JSON 读取。
- `.fig`:调用 `figToJson` 解析 Figma 本地文件。
`.fig` 解码入口:`src/services/fig2json.ts`
主要步骤:
1. 判断输入是否已经是 `fig-kiwi` 格式。
2. 如果不是 `fig-kiwi`,按 zip 解析并取出 `canvas.fig`
3.`canvas.fig` 里切分二进制 parts。
4. 对 part 做解压:
- zstd magic 开头:使用 `fzstd` 解压。
- PNG magic 开头:保留。
- 其他内容:使用 `UZIP.inflateRaw` 解压。
5. 第一个 part 是 schema第二个 part 是 data。
6. 使用 `kiwi-schema` 解码 schema再 decode data。
7.`blobs` 转成 base64得到项目内部使用的 `FigJson`
输出的 `FigJson` 还会附带 `__figmaToJson` 元信息:
```ts
{
__figmaToJson: {
schema: string,
delimiter: number
}
}
```
## 3. 核心数据结构
类型定义入口:`src/services/fig-types.ts`
### 3.1 FigJson
```ts
export type FigJson = {
type?: string
sessionID?: number
ackID?: number
nodeChanges?: FigNode[]
blobs?: string[]
__figmaToJson?: {
schema: string
delimiter: number
}
}
```
字段含义:
- `nodeChanges`:所有节点的扁平列表。
- `blobs`:几何路径 blob已被转为 base64 字符串。
- `sessionID` / `ackID`:文件级元信息。
- `__figmaToJson`:当前项目解码时保存的 schema 和 delimiter 信息。
### 3.2 FigNode
```ts
export type FigNode = {
guid: Guid
parentIndex?: { guid: Guid; position?: string }
type?: string
name?: string
visible?: boolean
opacity?: number
size?: { x: number; y: number }
transform?: FigmaMatrix
fillPaints?: FigPaint[]
strokePaints?: FigPaint[]
fillGeometry?: FigGeometry[]
strokeGeometry?: FigGeometry[]
effects?: FigEffect[]
textData?: FigTextData
derivedTextData?: FigDerivedTextData
}
```
重要字段:
- `guid`:节点唯一标识。
- `parentIndex.guid`:父节点标识。
- `parentIndex.position`:同级节点排序信息,也是 z-order 的关键。
- `type`:节点类型,例如 `DOCUMENT`、`FRAME`、`TEXT`、`VECTOR`、`ELLIPSE`。
- `size`:节点本地尺寸。
- `transform`:节点相对父级的 2D 变换矩阵。
- `fillPaints` / `strokePaints`:填充和描边 paint。
- `fillGeometry` / `strokeGeometry`:几何数据引用。
- `effects`:阴影、模糊等效果。
- `textData` / `derivedTextData`文本内容、样式覆盖、glyph 路径等。
### 3.3 Guid 与 node-id
```ts
export type Guid = {
sessionID: number
localID: number
}
```
项目内部将它标准化为:
```ts
`${sessionID}:${localID}`
```
相关工具:`src/utils/node-id.ts`
- `keyForGuid(guid)`:把 `{ sessionID, localID }` 转成 `sessionID:localID`
- `normalizeNodeId(value)`:支持 `123:456`、`123-456`、`node-id=123-456` 和完整 Figma 链接。
### 3.4 Geometry 与 Blob
```ts
export type FigGeometry = {
commandsBlob: number
windingRule?: string
styleID?: number
}
```
`commandsBlob``FigJson.blobs` 的索引。例如:
```ts
const blob = figJson.blobs?.[geometry.commandsBlob]
```
blob 内容是 base64 编码的紧凑路径命令。渲染时会被解析为 SVG path。
当前支持的路径命令:
| opcode | SVG 命令 | 含义 |
| --- | --- | --- |
| `1` | `M` | moveTo |
| `2` | `L` | lineTo |
| `3` | `Q` | quadraticCurveTo |
| `4` | `C` | cubicCurveTo |
| `0` | `Z` | closePath |
解析入口:`src/services/fig-node-svg.ts` 的 `parsePathBlob`
### 3.5 Paint
```ts
export type FigPaint = {
type: "SOLID" | "GRADIENT_LINEAR" | string
color?: FigColor
opacity?: number
visible?: boolean
stops?: Array<{ color: FigColor; position: number }>
transform?: FigmaMatrix
image?: { hash?: Uint8Array | number[] | string; name?: string }
imageThumbnail?: { hash?: Uint8Array | number[] | string; name?: string }
}
```
当前 SVG 渲染重点支持:
- `SOLID`
- `GRADIENT_LINEAR`
- `GRADIENT_RADIAL`
- `IMAGE`
`IMAGE` paint 会通过 `image.hash``imageThumbnail.hash` 去匹配 `.fig` zip 外层的 `images/<hash>`
## 4. 节点树组装逻辑
组装入口:`src/services/fig-node-svg.ts`
```ts
export function getChildrenByParent(figJson: FigJson): Map<string, FigNode[]> {
const childrenByParent = new Map<string, FigNode[]>()
for (const node of figJson.nodeChanges ?? []) {
if (!node.parentIndex?.guid) continue
const parentKey = keyForGuid(node.parentIndex.guid)
const children = childrenByParent.get(parentKey) ?? []
children.push(node)
childrenByParent.set(parentKey, children)
}
return childrenByParent
}
```
组装结果是:
```ts
Map<parentNodeId, childNodes[]>
```
节点自身 id
```ts
keyForGuid(node.guid)
```
父节点 id
```ts
keyForGuid(node.parentIndex.guid)
```
子节点排序:
```ts
function getSortedChildren(context: RenderContext, node: FigNode): FigNode[] {
return [...(context.childrenByParent.get(keyForGuid(node.guid)) ?? [])].sort((a, b) =>
comparePosition(a.parentIndex?.position ?? "", b.parentIndex?.position ?? "")
)
}
```
排序使用字符串原始比较:
```ts
if (left === right) return 0
return left < right ? -1 : 1
```
原因是 `parentIndex.position` 是 fractional-index 字符串,使用 locale-aware 排序可能会重排标点,从而改变 Figma 原始 z-order。
## 5. 目标节点查找
入口:`findTargetNode`
查找逻辑:
1. 先尝试从 `nodeId``nodeQuery` 中解析标准 node-id。
2. 如果能解析成 `sessionID:localID`,按 `guid` 查找。
3. 如果不能解析,则按节点名称精确匹配。
```ts
const target = normalizedNodeId
? nodes.find((node) => keyForGuid(node.guid) === normalizedNodeId)
: nodes.find((node) => node.name === nodeName)
```
因此当前项目的节点查询不是模糊搜索。模糊搜索只存在于 `listFigNodes` 的查询列表逻辑里。
## 6. 设计上下文组装
入口:`src/services/design-context.ts`
### 6.1 getDesignContext
流程:
1. `loadFigFile` 读取文件。
2. `getChildrenByParent` 组装父子映射。
3. 根据 `nodeQuery` 找目标节点;没有传则找根节点。
4. `serializeNode` 输出节点摘要和子树。
5. 可选输出 tokens。
6. 可选输出 codeHints。
输出结构:
```ts
{
kind: "figma-local-design-context",
filePath,
query,
node,
tokens,
codeHints
}
```
### 6.2 serializeNode
`serializeNode` 会输出:
- 基础摘要id、name、type、size、visible、opacity。
- 父节点 id。
- 子节点数量。
- paint 和 effect 是否存在。
- transform、stroke、arcData。
- 简化 fills / strokes。
- geometry 数量。
- 指定深度内的 children。
这部分适合给模型或上层工具提供可读的设计上下文。
### 6.3 tokens 推导
`collectDesignTokens` 并不读取 Figma 变量或全局样式注册表,而是从当前子树中推导:
- 颜色
- 线性渐变
- 投影
- 描边宽度
源码里也明确说明:本地 `.fig` 解码目前还没有解析 Figma 的变量和样式 registry。
## 7. SVG 组装逻辑
入口:`renderNodeToSvg`
流程:
1. 找目标节点。
2. 重建 `childrenByParent`
3. 初始化 `RenderContext`
4. 调用 `renderNodeSubtree` 递归生成 SVG body。
5. 根据节点尺寸、实际渲染 bounds 和 effect bounds 计算导出 viewBox。
6. 拼接 `<svg>`、`<defs>`、背景和 body。
### 7.1 RenderContext
```ts
type RenderContext = {
figJson: FigJson
childrenByParent: Map<string, FigNode[]>
defs: string[]
rasterHints: FigmaLikeRasterHint[]
imageAssets: FigImageAssets
bounds: Bounds | null
effectBounds: Bounds | null
idSeed: number
pngFigmaLike: boolean
}
```
关键字段:
- `defs`渐变、clipPath、filter 等 SVG 定义。
- `rasterHints`PNG 阶段需要额外像素补偿的信息。
- `imageAssets`:图片 hash 到 data URL 的映射。
- `bounds`:普通可见内容范围。
- `effectBounds`:阴影、模糊等效果扩展范围。
### 7.2 renderNodeSubtree
递归渲染顺序:
1. 跳过 `visible === false` 的节点。
2. 合并父级矩阵和当前节点 transform。
3. 渲染 fill geometry。
4. 渲染文本 glyph。
5. 收集 Figma-like PNG raster hint。
6. 渲染 stroke。
7. 递归渲染 children。
8. 给当前节点包一层 `<g>`,附加 opacity 和 filter。
特殊处理:
- `BOOLEAN_OPERATION` 如果已经有计算后的几何路径,就不再渲染源 children。
- mask 节点会转成 `clipPath`,后续 sibling 会被 clip。
- effect 作为节点级 filter 包在 `<g>` 上,而不是只包单个 path。
### 7.3 transform 组装
Figma 矩阵:
```ts
{
m00, m01, m02,
m10, m11, m12
}
```
SVG matrix
```ts
[m00, m10, m01, m11, m02, m12]
```
转换入口:
```ts
function toSvgMatrix(matrix?: FigmaMatrix): SvgMatrix
```
递归时通过矩阵乘法累积:
```ts
const matrix = multiply(parentMatrix, localMatrix)
```
这样子节点最终会进入根节点坐标空间。
## 8. 几何路径渲染
几何渲染入口:`renderGeometry`
流程:
1. 读取 `fillGeometry``strokeGeometry`
2.`commandsBlob` 找到 `figJson.blobs[index]`
3. `parsePathBlob` 转成 SVG path `d`
4. 计算 bounds。
5. 根据 paint 类型输出对应 SVG。
普通填充:
```svg
<path d="..." fill="..." />
```
奇偶填充规则:
```svg
fill-rule="evenodd"
```
描边优先使用 SVG stroke 属性,而不是直接填充 decoded `strokeGeometry`。原因是 Figma 的 decoded `strokeGeometry` 更像内部栅格使用的扩展轮廓,直接填充可能让细描边变粗。
## 9. 图片资源组装
图片读取入口:`src/services/fig-images.ts`
流程:
1. 只对 `.fig` 生效,`.json` 返回空 Map。
2. 判断文件是否是 zip。
3. 解析 zip。
4. 遍历 `images/` 下的资源。
5. 通过文件头识别 mime
- PNG
- JPEG
- GIF
- WEBP
6. 存成:
```ts
Map<hash, dataUrl>
```
图片渲染入口:`renderImageFill`
流程:
1.`paint.image.hash``paint.imageThumbnail.hash` 得到 hash。
2.`imageAssets` 中查找 data URL。
3. 用当前矢量路径创建 `clipPath`
4. 输出一个单位尺寸 `<image width="1" height="1">`
5.`paint.transform` 和节点尺寸计算 image matrix。
6. 用 wrapper `<g clip-path="...">` 裁切图片。
这样可以把 Figma 的图片裁切、填充和矢量路径边界转成 SVG 近似表达。
## 10. 文本渲染
文本节点不直接输出 `<text>`,而是使用 glyph path。
入口:`renderTextNode`
流程:
1. 判断 `node.type === "TEXT"`
2. 读取 `derivedTextData.glyphs`
3. 每个 glyph 通过 `commandsBlob` 解析出路径。
4. 根据 glyph 的 position、fontSize、rotation 计算矩阵。
5. 根据 `textData.characterStyleIDs``styleOverrideTable` 找到 glyph 对应 paint。
6. 将相同 paint 的 glyph path 合并成一个 `<path>`
关键转换:
```ts
const scale: SvgMatrix = [fontSize, 0, 0, -fontSize, 0, 0]
```
原因是 glyph blob 使用字体轮廓坐标Y 轴向上SVG 本地空间 Y 轴向下,所以要做垂直翻转。
## 11. 渐变与效果
### 11.1 线性渐变
入口:`paintToSvgFill` 中的 `GRADIENT_LINEAR` 分支。
逻辑:
- Figma 的 paint transform 是从对象空间回到单位渐变空间。
- SVG 需要的是 user space 中的 `x1/y1/x2/y2`
- 所以代码会先对 transform 求逆,再投影到节点 box。
### 11.2 径向渐变
入口:`GRADIENT_RADIAL` 分支。
逻辑:
- 使用 `radialGradient`
- 通过 `gradientTransform` 把单位圆投影到节点空间。
### 11.3 阴影与模糊
入口:`createNodeEffectFilter` 和 `createFilter`
支持的主要 effect
- `DROP_SHADOW`
- `INNER_SHADOW`
- `FOREGROUND_BLUR`
- `LAYER_BLUR`
SVG 导出时会生成 `<filter>`,并放入 `context.defs`
PNG 的 `figma-like` 模式对椭圆内阴影有额外处理:
- SVG 阶段记录 `rasterHints`
- PNG 阶段在 `export-node.ts` 中直接操作像素。
- 目的是补偿 `resvg` 和 Figma 原生 PNG 渲染在内阴影边缘上的差异。
## 12. PNG 导出逻辑
入口:`src/services/export-node.ts`
流程:
1. `renderNodeToSvg` 先生成 SVG。
2. 使用 `new Resvg(rendered.svg).render()` 栅格化。
3. 如果是普通预览,直接 `image.asPng()`
4. 如果是 `figma-like`,先拿 RGBA pixels再应用 raster hints。
5. 使用项目内的 PNG encoder 写回 PNG。
当前导出能力说明:
- SVG本地结构化输出。
- PNG本地 SVG 经过 `@resvg/resvg-js` 栅格化。
- Figma-like PNG额外补偿部分滤镜和内阴影。
- Figma native PNG当前不支持因为没有调用 Figma 官方渲染器。
## 13. 当前实现边界
当前项目已经支持:
- 本地 `.fig``.fig.json` 读取。
- 节点列表、节点上下文、设计上下文。
- 子树序列化。
- 颜色、渐变、阴影、描边宽度 token 推导。
- SVG 导出。
- PNG 预览导出。
- 图片填充。
- 文本 glyph path 渲染。
- mask 到 clipPath 的转换。
- 线性和径向渐变。
- 虚线描边。
- 部分 filter 和 Figma-like PNG 补偿。
主要限制:
- 不解析 Figma 官方变量和样式 registry。
- 不保证 PNG 与 Figma native export 像素级一致。
- `getDesignContext` 里的节点查找是精确名称或精确 node-id。
- `.fig.json` 不包含 zip 外层图片资源时,`IMAGE` 填充无法恢复真实图片。
- 复杂 blend mode、组件实例覆盖、变量绑定、auto layout 语义等还没有完整建模。
## 14. 源码入口索引
| 功能 | 文件 | 关键函数 |
| --- | --- | --- |
| 文件读取 | `src/services/fig-file.ts` | `loadFigFile` |
| `.fig` 解码 | `src/services/fig2json.ts` | `figToJson` |
| 类型定义 | `src/services/fig-types.ts` | `FigJson`、`FigNode`、`FigPaint` |
| 节点 id 处理 | `src/utils/node-id.ts` | `keyForGuid`、`normalizeNodeId` |
| 父子关系组装 | `src/services/fig-node-svg.ts` | `getChildrenByParent` |
| 目标节点查找 | `src/services/fig-node-svg.ts` | `findTargetNode` |
| SVG 渲染 | `src/services/fig-node-svg.ts` | `renderNodeToSvg`、`renderNodeSubtree` |
| 路径解析 | `src/services/fig-node-svg.ts` | `parsePathBlob` |
| 图片资源读取 | `src/services/fig-images.ts` | `loadFigImageAssets` |
| 设计上下文 | `src/services/design-context.ts` | `getDesignContext`、`getCodeContext` |
| token 推导 | `src/services/design-context.ts` | `collectDesignTokens` |
| 导出文件 | `src/services/export-node.ts` | `exportFigNode` |
| MCP 工具注册 | `src/mcp/index.ts` | `createServer` |
## 15. 简化伪代码
```ts
const figJson = loadFigFile(filePath)
const imageAssets = loadFigImageAssets(filePath)
const childrenByParent = getChildrenByParent(figJson)
const target = findTargetNode(figJson, { nodeQuery })
const svg = renderNodeToSvg(figJson, {
nodeQuery,
imageAssets,
scale,
pngFigmaLike
})
if (format === "png") {
const image = new Resvg(svg).render()
writePng(image)
} else {
writeSvg(svg)
}
```
这就是当前项目对 `.fig` 的核心处理方式:先把 Figma 本地文件解成扁平数据,再用 `guid` 关系重建节点树,最后把节点、几何 blob、paint、图片资源和 effect 递归组合成可导出的 SVG/PNG。