# Sketch MCP 最佳实践

> 使用 Sketch MCP 工具准确测量设计稿的规范指南
> 版本: v2.0 | 最后更新: 2026-02-11

## 📋 概述

Sketch MCP 是用于从 Sketch 设计文件中提取 UI 参数的工具。本文档记录了使用过程中发现的问题和最佳实践。

## 🔴 设计还原前置约束

当请求是“还原设计稿”“按设计稿实现”“像素级还原”时：

1. 在任何代码修改前，先调用 Sketch MCP 获取测量结果或紧凑 restoration contract
2. 如果用户还没有在 Sketch 中选中目标画板或图层，先要求用户选中，不能跳过
3. Icon / bitmap 先在项目现有资源目录中按设计稿图层名或编组名搜索完全一致的导出资源
4. `@2x`、`@3x`、`@4x` 仅表示倍率，不表示语义不同
5. 找不到可确认的导出资源时，必须明确说明未确认，不能猜一个“差不多”的图标顶上

### Icon 资源判定顺序

```text
1. 设计稿图层名 / 编组名完全匹配的导出资源
2. 同名 stem + 不同倍率后缀的导出资源
3. Sketch 已测得为矢量且拿到了目标纯色，才允许复用同形单色 SVG
4. 仍无法确认时，停止并报告不确定性
```

### Icon 问题修复完整流程

当图标已经缩小到最后的视觉偏差时，按这个顺序处理：

1. 先判定问题类型：形状不对、颜色不对、视觉大小不对，或三者同时存在
2. 回到 Sketch 重新测量目标图层，至少记录：图层名、外层 frame、fill 颜色、内部 path frame 或 content bounds
3. 在项目资源目录里按设计稿图层名 / 编组名 stem 搜现有导出资源，再做视觉比对，不要只看文件名
4. 如果旧位图形状接近但颜色发灰、发浅或需要反复 `ColorFilter` / tint，优先直接导出当前 Sketch 图层，避免 alpha 漂移
5. 如果两个图标需要视觉对齐但外层 Group 尺寸不同，代码里必须分离 slot size 与 render size：
  - slot size = 设计稿外层 frame
  - render size = content bounds 或精确导出后的真实图形尺寸
  - render 在 slot 内居中
6. 如果最终采用的是新导出的 bitmap，必须检查落盘文件像素尺寸是否足以覆盖目标设备像素比，避免把 1x 临时导出当成正式资源导致发糊
7. 输出结论时必须说明：测得图层名、最终 assetPath、slot size、render size

---

## 🔴 核心原则：测量 + 计算（最高优先级）

**⚠️ 关键规则：绝不直接使用测量数据，必须分析计算后再使用！**

### 为什么需要计算？

| 场景 | 测量得到的原始数据 | 需要计算的目标值 |
|------|-------------------|-----------------|
| **行高** | 文本 Y=8, 高度=20, 字号=14 | `height: 20/14 = 1.43` |
| **居中边距** | 上边距=8, 下边距=8 | 容器用 `alignment: center` |
| **渐变方向** | from={x:0,y:0}, to={x:1,y:1} | Flutter `Alignment(-1,-1) → (1,1)` |
| **透明度** | `#1C2B4580` | `0x80 = 128/255 = 0.5` |
| **Text topOffset** | 字号=24, 帧高=33 | `topOffset=4.5`, `StrutStyle.height=33/24=1.375` |

### 标准流程

```
1. 测量 - 获取原始像素值和坐标
2. 分析 - 理解设计师意图（居中？等间距？）
3. 计算 - 转换为目标框架需要的参数
4. 验证 - 与设计稿对比确认
```

### 典型计算公式

```javascript
// 行高计算（Flutter/CSS 通用）
const lineHeight = 文本高度 / 字号;  // 如 20/14 = 1.43

// 居中验证
const isVerticalCentered = Math.abs(上边距 - 下边距) < 2;

// 渐变角度（CSS）
const angle = Math.atan2(to.y - from.y, to.x - from.x) * 180 / Math.PI + 90;

// 透明度（从 8 位十六进制）
const alpha = parseInt(colorHex.slice(7, 9), 16) / 255;

// Sketch "适应布局" Text 帧高留白（topOffset）
// Sketch Text 图层在「适应布局」模式下：frameHeight ≈ fontSize × 1.375
// 文字视觉上沿不在 frame.y，而是 frame.y + topOffset
const topOffset = (frameHeight - fontSize) / 2;  // 如 (33-24)/2 = 4.5px

// Flutter StrutStyle.height（消除 CJK 字体 ascent/descent 不对称导致的视觉偏下）
const strutHeight = frameHeight / fontSize;  // 如 33/24 = 1.375
// 使用方式：StrutStyle(fontSize: fs, height: strutHeight, forceStrutHeight: true, leading: 0)
```

> 📖 **详细案例**：
> - [Text 普通文字垂直居中偏下（StrutStyle）](../troubleshooting-cases/flutter/text-vertical-centering-strutstyle.md)
> - [TextField 垂直居中问题](../troubleshooting-cases/flutter/textfield-vertical-centering.md)

---

## 🔴 v4.0 测量数据解读规范（必读）

> 测量插件 v4.0 新增三类关键数据，正确解读可避免 80% 的还原偏差

### 1. iconContentBounds — Icon 双尺寸数据

**输出结构**：
```json
{
  "iconContentBounds": {
    "containerSize": { "width": 16, "height": 16 },
    "contentSize": { "width": 12, "height": 12 },
    "renderHint": "占位用 containerSize(16×16)，渲染用 contentSize(12×12)"
  }
}
```

**解读规则**：
| 字段 | 含义 | 代码用途 |
|------|------|---------|
| `containerSize` | Group 外层框架尺寸（含 padding） | 外层 `SizedBox` 占位 |
| `contentSize` | 内部所有路径的 union 包围盒 | `SvgPicture` 的 `width`/`height` |

**错误模式**（导致需要 2-3 轮修正）：
```
❌ 直接用 containerSize 渲染 → Icon 偏大 2-4px
❌ 忽略 containerSize 直接用 contentSize → 同行文字错位
```

### 2. tagStyle — Tag 容器完整样式

**输出结构**：
```json
{
  "tagStyle": {
    "background": "#1676fe99",
    "padding": { "horizontal": 3, "vertical": 1 },
    "cornerRadius": 3,
    "textContent": "支付宝",
    "textStyle": { "fontSize": 8, "color": "#ffffffff" }
  }
}
```

**解读规则**：
- 直接使用 `padding`、`background`、`cornerRadius`，不要猜测
- `background` 包含透明度（如 `#1676fe99` = 60% 不透明度），转换时保留
- 注意 `cornerRadius` 通常很小（2-4px），不要用默认的大圆角

### 3. siblingIconAlignment — 同行 Icon 对齐

**输出结构**：
```json
{
  "siblingIconAlignment": {
    "detected": true,
    "maxSlotSize": { "width": 16, "height": 16 },
    "icons": [
      { "name": "Fee Icon", "containerSize": [16,16], "contentSize": [14,14] },
      { "name": "Coupon Icon", "containerSize": [12,12], "contentSize": [12,12] }
    ]
  }
}
```

**解读规则**：
- 所有同行 Icon 用 `maxSlotSize` 做统一 SizedBox 占位
- 各 Icon 用自己的 `contentSize` 渲染
- 内部用 `Center` 确保视觉居中

### 4. fontName — PostScript 字体名（字重真值）

**输出结构**：
```json
{
  "fontName": "PingFangSC-Semibold",
  "fontWeight": 600,
  "platformCap": "PingFang SC 最大真实字重 = Semibold(w600)，超过会触发合成加粗"
}
```

**解读规则**：
- 始终用 `fontName` 映射的 `fontWeight`，忽略 Sketch API 的 `style.fontWeight` 数值
- 如果 `fontWeight` 超过平台可用范围，自动降级（详见 `platformCap` 提示）

---

## 🎯 深度还原模式（像素级精确还原）

**触发词**：
- "精确还原 XX 页面"
- "深度还原 XX 页面"
- "像素级还原 XX 页面"
- "完美还原 XX 页面"

### 架构说明

深度还原采用 **双层架构**，确保通用性的同时不牺牲各框架的专业性：

```
┌─────────────────────────────────────────────────────────┐
│                    通用分析层（本文档）                    │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
│  │ 深度测量脚本 │→ │ 布局意图分析 │→ │ Layout Intent  │  │
│  │ (Sketch API)│  │ (算法识别)  │  │ (标准化输出)   │  │
│  └─────────────┘  └─────────────┘  └────────┬────────┘  │
└─────────────────────────────────────────────┼───────────┘
                                              ↓
┌─────────────────────────────────────────────┴───────────┐
│                   框架专属转换层                          │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────┐ │
│  │  Flutter  │  │   Vue 3   │  │  小程序    │  │ React │ │
│  │ (详见下方) │  │ (详见下方) │  │ (详见下方) │  │ Native│ │
│  └───────────┘  └───────────┘  └───────────┘  └───────┘ │
└─────────────────────────────────────────────────────────┘
```

### 强制前置检查

```
⚠️ 开始前必须确认：
1. 用户已在 Sketch 中选中目标画板/Frame
2. 若未选中 → 提示用户："请先在 Sketch 中选中要还原的画板"
3. 确认选中后再执行深度测量脚本
```

---

### 第一层：深度测量脚本（通用）

此脚本提取设计稿数据并输出标准化的 **Layout Intent**，供各框架转换层使用。

```javascript
const sketch = require('sketch');

/**
 * 深度还原模式 - 完整测量脚本
 * 输出标准化的 Layout Intent，供各框架转换使用
 */
function deepMeasureForRestoration() {
  const doc = sketch.getSelectedDocument();
  const selection = doc.selectedLayers.layers;
  
  if (selection.length === 0) {
    console.log('❌ 错误：请先选中要还原的画板或 Frame');
    console.log('💡 提示：在 Sketch 左侧图层面板中点击目标画板');
    return null;
  }
  
  const artboard = selection[0];
  console.log(`\n🎯 深度还原模式 - 目标: ${artboard.name}`);
  console.log(`📐 画板尺寸: ${artboard.frame.width} x ${artboard.frame.height}`);
  console.log('━'.repeat(50));
  
  const result = {
    artboard: {
      name: artboard.name,
      width: artboard.frame.width,
      height: artboard.frame.height,
    },
    elements: [],
    // Layout Intent 汇总（框架转换时使用）
    layoutIntents: [],
  };
  
  // 递归测量所有元素
  function measureRecursively(layer, depth = 0, parentFrame = null) {
    if (layer.hidden) return null;
    
    const element = {
      id: layer.id,
      name: layer.name,
      type: layer.type,
      depth: depth,
      frame: {
        x: Math.round(layer.frame.x),
        y: Math.round(layer.frame.y),
        width: Math.round(layer.frame.width),
        height: Math.round(layer.frame.height),
      },
      // 相对于父容器的位置（关键！）
      relativePosition: null,
      // Layout Intent（标准化布局意图）
      layoutIntent: {},
      // Icon 检测标记
      isIcon: false,
      iconExportPath: null,
      children: [],
    };
    
    // ========== Icon 智能检测 ==========
    element.isIcon = detectIfIcon(layer);
    if (element.isIcon) {
      element.iconExportPath = `icons/${sanitizeFileName(layer.name)}.svg`;
      // v4.0: 提取 Icon 内部路径尺寸
      element.iconContentBounds = extractIconContentBounds(layer);
      console.log(`🎨 检测到 Icon: ${layer.name}`);
      console.log(`   容器: ${element.iconContentBounds.containerSize.width}×${element.iconContentBounds.containerSize.height}`);
      console.log(`   路径: ${element.iconContentBounds.contentSize.width}×${element.iconContentBounds.contentSize.height}`);
    }
    
    // 计算相对位置并生成 Layout Intent
    if (parentFrame) {
      const relLeft = layer.frame.x;
      const relTop = layer.frame.y;
      const relRight = parentFrame.width - layer.frame.x - layer.frame.width;
      const relBottom = parentFrame.height - layer.frame.y - layer.frame.height;
      
      element.relativePosition = {
        left: Math.round(relLeft),
        top: Math.round(relTop),
        right: Math.round(relRight),
        bottom: Math.round(relBottom),
      };
      
      // ========== 生成 Layout Intent ==========
      const tolerance = 2; // 允许 2px 误差
      const intent = {
        alignment: [],
        sizing: {},
        spacing: {},
      };
      
      // 对齐意图
      const isHCenter = Math.abs(relLeft - relRight) <= tolerance;
      const isVCenter = Math.abs(relTop - relBottom) <= tolerance;
      
      if (isHCenter && isVCenter) {
        intent.alignment = ['CENTER'];
      } else {
        if (isHCenter) intent.alignment.push('HORIZONTAL_CENTER');
        if (isVCenter) intent.alignment.push('VERTICAL_CENTER');
        if (relLeft < parentFrame.width * 0.05 && relRight > relLeft * 3) {
          intent.alignment.push('ALIGN_START');  // 通用术语，非 left
        }
        if (relRight < parentFrame.width * 0.05 && relLeft > relRight * 3) {
          intent.alignment.push('ALIGN_END');
        }
      }
      
      // 尺寸意图
      const widthRatio = layer.frame.width / parentFrame.width;
      const heightRatio = layer.frame.height / parentFrame.height;
      
      if (isHCenter && relLeft < 30) {
        intent.sizing.width = { type: 'FILL', margin: relLeft };
      } else if (widthRatio > 0.9) {
        intent.sizing.width = { type: 'MATCH_PARENT' };
      } else {
        intent.sizing.width = { type: 'FIXED', value: layer.frame.width };
      }
      
      if (heightRatio > 0.9) {
        intent.sizing.height = { type: 'MATCH_PARENT' };
      } else {
        intent.sizing.height = { type: 'WRAP_CONTENT', minHeight: layer.frame.height };
      }
      
      // 边距意图（用于 padding 计算）
      intent.spacing = {
        paddingStart: relLeft,
        paddingEnd: relRight,
        paddingTop: relTop,
        paddingBottom: relBottom,
      };
      
      element.layoutIntent = intent;
    }
    
    // 提取样式
    if (layer.style) {
      element.style = extractCompleteStyle(layer);
    }
    
    // 文本特殊处理
    if (layer.type === 'Text') {
      element.text = extractTextInfo(layer);
    }
    
    // 递归处理子元素
    const childLayers = layer.layers || [];
    const expandedLayers = layer.expandedLayers || [];
    const allChildren = [...childLayers, ...expandedLayers];
    
    allChildren.forEach(child => {
      const childData = measureRecursively(child, depth + 1, layer.frame);
      if (childData) {
        element.children.push(childData);
      }
    });
    
    // 分析子元素排列规律 → 生成容器 Layout Intent
    if (element.children.length > 1) {
      element.containerIntent = analyzeChildrenLayout(element.children);
      // v4.0: 检测同行 Icon 对齐
      element.siblingIconAlignment = detectSiblingIconAlignment(element.children);
    }
    
    // v4.0: 检测 Tag 容器
    element.tagStyle = detectTagContainer(layer);
    
    return element;
  }
  
  // 提取完整样式
  function extractCompleteStyle(layer) {
    const style = {};
    
    // 填充色（正确处理渐变）
    const fills = layer.style.fills?.filter(f => f.enabled) || [];
    if (fills.length > 0) {
      style.fills = fills.map(f => {
        if (f.fillType === 'Gradient' && f.gradient) {
          return {
            type: 'gradient',
            gradientType: f.gradient.gradientType,
            from: f.gradient.from,
            to: f.gradient.to,
            stops: f.gradient.stops.map(s => ({
              position: s.position,
              color: s.color
            }))
          };
        }
        return { type: 'solid', color: f.color };
      });
    }
    
    // 边框
    const borders = layer.style.borders?.filter(b => b.enabled) || [];
    if (borders.length > 0) {
      style.borders = borders.map(b => ({
        color: b.color,
        thickness: b.thickness,
        position: b.position
      }));
    }
    
    // 阴影
    const shadows = layer.style.shadows?.filter(s => s.enabled) || [];
    if (shadows.length > 0) {
      style.shadows = shadows.map(s => ({
        color: s.color,
        blur: s.blur,
        x: s.x,
        y: s.y,
        spread: s.spread || 0
      }));
    }
    
    // 圆角
    if (layer.style.corners) {
      const radii = layer.style.corners.radii;
      style.borderRadius = radii.every(r => r === radii[0]) 
        ? radii[0] 
        : radii;
    }
    
    // 透明度
    if (layer.style.opacity !== undefined && layer.style.opacity !== 1) {
      style.opacity = layer.style.opacity;
    }
    
    return style;
  }
  
  // 提取文本信息
  function extractTextInfo(layer) {
    const info = {
      content: layer.text,
      fontSize: layer.style.fontSize,
      fontFamily: layer.style.fontFamily,
      textColor: layer.style.textColor,
      alignment: layer.style.alignment,
    };
    
    // 获取真实字体名（解决 fontWeight 不可靠问题）
    if (layer.sketchObject?.font) {
      const font = layer.sketchObject.font();
      if (font) {
        info.fontName = String(font.fontName());
        info.fontWeight = parseFontWeight(info.fontName);
      }
    }
    
    // 计算行高比例（通用，各框架按需转换）
    if (layer.style.lineHeight) {
      info.lineHeight = layer.style.lineHeight;
      info.lineHeightRatio = (layer.style.lineHeight / layer.style.fontSize).toFixed(2);
    } else {
      // 从实际高度推算
      info.lineHeightRatio = (layer.frame.height / layer.style.fontSize).toFixed(2);
    }
    
    return info;
  }
  
  // 解析字重（通用映射）
  function parseFontWeight(fontName) {
    const weightMap = {
      'Thin': 100, 'Ultralight': 200, 'Light': 300,
      'Regular': 400, 'Medium': 500, 'Semibold': 600,
      'Bold': 700, 'Heavy': 800, 'Black': 900
    };
    for (const [key, value] of Object.entries(weightMap)) {
      if (fontName.includes(key)) return value;
    }
    return 400;
  }
  
  // ========== Icon 智能检测 ==========
  // 根据元素特征判断是否为 icon
  function detectIfIcon(layer) {
    const name = layer.name.toLowerCase();
    const size = Math.max(layer.frame.width, layer.frame.height);
    
    // 规则 1: 名称特征
    const iconKeywords = ['icon', 'ico', 'logo', 'symbol', 'arrow', 'close', 'menu', 'search', 'star', 'check'];
    const hasIconKeyword = iconKeywords.some(keyword => name.includes(keyword));
    
    // 规则 2: 尺寸特征（v4.0 放宽: 8-64px，覆盖小箭头 5×8 等场景）
    const isIconSize = size >= 8 && size <= 64;
    
    // 规则 3: 形状特征（v4.0 放宽: 宽高比 0.5-2.0，覆盖非正方形图标如箭头 5×8）
    const aspectRatio = layer.frame.width / layer.frame.height;
    const isSquarish = aspectRatio >= 0.5 && aspectRatio <= 2.0;
    
    // 规则 4: 类型特征（Shape Path / Symbol Instance / Group 且子元素少）
    const isVectorType = layer.type === 'ShapePath' || layer.type === 'SymbolInstance' || 
                         (layer.type === 'Group' && (layer.layers?.length || 0) <= 5);
    
    // 规则 5: 无文本内容
    const hasNoText = layer.type !== 'Text';
    
    // 综合判定（满足 3 个条件即认为是 icon）
    const matchCount = [hasIconKeyword, isIconSize, isSquarish, isVectorType, hasNoText]
      .filter(Boolean).length;
    
    return matchCount >= 3;
  }
  
  // ========== v4.0 新增：Icon 内容边界提取 ==========
  function extractIconContentBounds(layer) {
    const containerSize = { width: Math.round(layer.frame.width), height: Math.round(layer.frame.height) };
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    let foundPaths = false;
    
    function findPaths(l) {
      if (l.type === 'ShapePath' || l.type === 'Shape') {
        foundPaths = true;
        const x = l.frame.x, y = l.frame.y;
        const w = l.frame.width, h = l.frame.height;
        minX = Math.min(minX, x);
        minY = Math.min(minY, y);
        maxX = Math.max(maxX, x + w);
        maxY = Math.max(maxY, y + h);
      }
      (l.layers || []).forEach(findPaths);
    }
    findPaths(layer);
    
    const contentSize = foundPaths
      ? { width: Math.round(maxX - minX), height: Math.round(maxY - minY) }
      : containerSize;
    
    return {
      containerSize,
      contentSize,
      renderHint: `占位用 containerSize(${containerSize.width}×${containerSize.height})，渲染用 contentSize(${contentSize.width}×${contentSize.height})`
    };
  }
  
  // 文件名清理（用于 SVG 导出）
  function sanitizeFileName(name) {
    return name
      .toLowerCase()
      .replace(/\s+/g, '-')
      .replace(/[^a-z0-9-_]/g, '')
      .replace(/-+/g, '-')
      .replace(/^-|-$/g, '');
  }
  
  // ========== v4.0 新增：Tag 容器检测 ==========
  function detectTagContainer(layer) {
    if (layer.type !== 'Group') return null;
    const w = layer.frame.width, h = layer.frame.height;
    if (w > 150 || h > 40) return null;  // Tag 通常很小
    
    const children = layer.layers || [];
    let shapeBg = null, textLayer = null;
    for (const child of children) {
      if ((child.type === 'ShapePath' || child.type === 'Shape') && !shapeBg) shapeBg = child;
      if (child.type === 'Text' && !textLayer) textLayer = child;
    }
    if (!shapeBg || !textLayer) return null;
    
    // 提取样式
    const fills = shapeBg.style?.fills?.filter(f => f.enabled) || [];
    const bgColor = fills.length > 0 ? fills[0].color : null;
    const radii = shapeBg.style?.corners?.radii;
    const cornerRadius = radii ? (radii.every(r => r === radii[0]) ? radii[0] : radii) : 0;
    
    const paddingH = Math.round(textLayer.frame.x);
    const paddingV = Math.round(textLayer.frame.y);
    
    return {
      background: bgColor,
      padding: { horizontal: paddingH, vertical: paddingV },
      cornerRadius,
      textContent: textLayer.text,
      textStyle: {
        fontSize: textLayer.style?.fontSize,
        color: textLayer.style?.textColor
      }
    };
  }
  
  // ========== v4.0 新增：同行 Icon 对齐检测 ==========
  function detectSiblingIconAlignment(children) {
    const iconChildren = children.filter(c => c.isIcon && c.iconContentBounds);
    if (iconChildren.length < 2) return null;
    
    // 检查 Y 坐标是否相近（同行判定）
    const ys = iconChildren.map(c => c.frame.y);
    const yRange = Math.max(...ys) - Math.min(...ys);
    if (yRange > 20) return null;  // 不在同行
    
    const maxW = Math.max(...iconChildren.map(c => c.iconContentBounds.containerSize.width));
    const maxH = Math.max(...iconChildren.map(c => c.iconContentBounds.containerSize.height));
    
    return {
      detected: true,
      maxSlotSize: { width: maxW, height: maxH },
      icons: iconChildren.map(c => ({
        name: c.name,
        containerSize: [c.iconContentBounds.containerSize.width, c.iconContentBounds.containerSize.height],
        contentSize: [c.iconContentBounds.contentSize.width, c.iconContentBounds.contentSize.height]
      }))
    };
  }
  
  // ========== 子元素对齐检测（优化布局分析）==========
  function detectChildrenAlignment(children) {
    if (children.length < 2) return null;
    
    const tolerance = 2; // 允许 2px 误差
    const leftEdges = children.map(c => c.frame.x);
    const rightEdges = children.map(c => c.frame.x + c.frame.width);
    const centerXs = children.map(c => c.frame.x + c.frame.width / 2);
    
    // 检测左对齐
    const leftAligned = leftEdges.every(x => Math.abs(x - leftEdges[0]) <= tolerance);
    // 检测右对齐
    const rightAligned = rightEdges.every(x => Math.abs(x - rightEdges[0]) <= tolerance);
    // 检测水平居中对齐
    const centerAligned = centerXs.every(x => Math.abs(x - centerXs[0]) <= tolerance);
    
    if (leftAligned) return 'START';
    if (rightAligned) return 'END';
    if (centerAligned) return 'CENTER';
    return 'NONE';
  }
  
  // ========== 响应式建议（增强交互准确性）==========
  function generateResponsiveHint(children) {
    if (children.length === 0) return null;
    
    const hint = {
      wrapNeeded: false,
      scrollNeeded: false,
      expandableChildren: [],
    };
    
    // 检测是否需要换行（子元素宽度总和超过容器）
    const totalChildWidth = children.reduce((sum, c) => sum + c.frame.width, 0);
    const parentWidth = children[0].frame.x; // 假设父容器信息存在
    if (totalChildWidth > parentWidth * 0.9) {
      hint.wrapNeeded = true;
    }
    
    // 检测是否需要滚动（子元素高度总和超过容器）
    const totalChildHeight = children.reduce((sum, c) => sum + c.frame.height, 0);
    const parentHeight = children[0].frame.y;
    if (totalChildHeight > parentHeight * 0.9) {
      hint.scrollNeeded = true;
    }
    
    // 检测哪些子元素应该可扩展（占据剩余空间）
    children.forEach((child, index) => {
      // 如果是容器且包含大量内容
      if ((child.children?.length || 0) > 3) {
        hint.expandableChildren.push(index);
      }
    });
    
    return hint;
  }
  
  // 分析子元素排列规律 → 生成容器级 Layout Intent
  function analyzeChildrenLayout(children) {
    // 按 Y 坐标排序检测垂直排列
    const sortedByY = [...children].sort((a, b) => a.frame.y - b.frame.y);
    const verticalGaps = [];
    for (let i = 0; i < sortedByY.length - 1; i++) {
      const gap = sortedByY[i + 1].frame.y - 
                  (sortedByY[i].frame.y + sortedByY[i].frame.height);
      verticalGaps.push(Math.round(gap));
    }
    
    // 按 X 坐标排序检测水平排列
    const sortedByX = [...children].sort((a, b) => a.frame.x - b.frame.x);
    const horizontalGaps = [];
    for (let i = 0; i < sortedByX.length - 1; i++) {
      const gap = sortedByX[i + 1].frame.x - 
                  (sortedByX[i].frame.x + sortedByX[i].frame.width);
      horizontalGaps.push(Math.round(gap));
    }
    
    // 生成标准化的容器 Layout Intent
    const avgVGap = verticalGaps.length > 0 
      ? verticalGaps.reduce((a, b) => a + b, 0) / verticalGaps.length : 0;
    const avgHGap = horizontalGaps.length > 0
      ? horizontalGaps.reduce((a, b) => a + b, 0) / horizontalGaps.length : 0;
    
    const containerIntent = {
      direction: null,
      gap: null,
      isGapUniform: true,
      gaps: [],
      // 新增：对齐检测
      alignment: detectChildrenAlignment(children),
      // 新增：响应式建议
      responsiveHint: generateResponsiveHint(children),
    };
    
    if (avgVGap > 0 && avgHGap <= 0) {
      containerIntent.direction = 'COLUMN';
      containerIntent.gap = Math.round(avgVGap);
      containerIntent.gaps = verticalGaps;
      containerIntent.isGapUniform = verticalGaps.every(g => Math.abs(g - avgVGap) <= 2);
    } else if (avgHGap > 0 && avgVGap <= 0) {
      containerIntent.direction = 'ROW';
      containerIntent.gap = Math.round(avgHGap);
      containerIntent.gaps = horizontalGaps;
      containerIntent.isGapUniform = horizontalGaps.every(g => Math.abs(g - avgHGap) <= 2);
    } else if (avgVGap > 0 && avgHGap > 0) {
      // 网格布局
      containerIntent.direction = 'GRID';
      containerIntent.rowGap = Math.round(avgVGap);
      containerIntent.columnGap = Math.round(avgHGap);
    }
    
    return containerIntent;
  }
  
  // 执行测量
  const rootElement = measureRecursively(artboard, 0, null);
  result.elements.push(rootElement);
  
  // 收集所有 icon 元素
  const icons = [];
  function collectIcons(element) {
    if (element.isIcon) {
      icons.push({
        name: element.name,
        id: element.id,
        exportPath: element.iconExportPath,
        size: { width: element.frame.width, height: element.frame.height }
      });
    }
    element.children.forEach(collectIcons);
  }
  collectIcons(rootElement);
  
  // 输出结构化结果
  console.log('\n📊 测量完成，Layout Intent 数据：');
  console.log(JSON.stringify(result, null, 2));
  
  // 输出 icon 导出指引
  if (icons.length > 0) {
    console.log('\n🎨 检测到 Icon，建议导出步骤：');
    console.log('1️⃣  在 Sketch 中选中以下图层：');
    icons.forEach(icon => {
      console.log(`   - ${icon.name} (建议尺寸: ${icon.size.width}x${icon.size.height})`);
    });
    console.log('\n2️⃣  点击右下角"Make Exportable"');
    console.log('3️⃣  选择格式: SVG');
    console.log('4️⃣  设置高度: 200h（保持高宽比，高度200px）');
    console.log('5️⃣  导出位置建议: assets/icons/ 或 src/assets/icons/');
    console.log('\n📝 导出后的使用方式：');
    icons.forEach(icon => {
      console.log(`   ${icon.exportPath} → 在代码中引用`);
    });
  }
  
  return result;
}

// 执行
deepMeasureForRestoration();
```

---

### 第二层：框架专属转换规范

**⚠️ 关键设计：各框架有独立的转换规范，确保深度还原效果不因通用化而降低。**

获取 Layout Intent 后，根据目标框架调用对应的转换规范：

#### 框架转换索引

| 框架 | 规范位置 | 调用方式 |
|------|---------|---------|
| **Flutter** | 本文档下方 + flutter.agent.md | `get_standard_by_id({ id: 'sketch-mcp' })` |
| **Vue 3 / CSS** | vue3.agent.md + design-restoration.md | `get_standard_by_id({ id: 'design-restoration' })` |
| **微信小程序** | wechat-miniprogram.agent.md | `get_standard_by_id({ id: 'wechat-miniprogram' })` |
| **React Native** | 参照 Flutter 规范 | Layout Intent 映射类似 |

---

### Flutter 深度还原转换规范

> **架构说明（v2.1 插件）**：测量插件只输出原始像素数据，不生成 Flutter 代码。
> 布局决策由 AI 根据以下规则完成。

#### relativePosition → Flutter 布局解读（AI 负责）

`relativePosition: { left, top, right, bottom, parentW, parentH }` — 图层相对父容器四边的原始距离（pt）

**水平方向解读：**

| relativePosition 条件 | AI 推导的布局意图 | Flutter 实现 |
|----------------------|----------------|-------------|
| `left ≈ right`（差值 ≤ 2）且 `left ≤ 24` | FILL：填充父宽，均匀左右边距 | `SizedBox(width: double.infinity)` / `Expanded` |
| `left ≈ right`（差值 ≤ 2）且 `left > 24` | 水平居中，含内边距 | `Padding(horizontal: left)` 内包 `double.infinity` |
| `left + width + right ≈ parentW`（误差 ≤ 2） | 确认 FILL | `width: double.infinity` |
| `left < 3` | 左对齐，无左边距 | `CrossAxisAlignment.start` 或 `Alignment.centerLeft` |
| `right < 3` | 右对齐，无右边距 | `CrossAxisAlignment.end` 或 `Alignment.centerRight` |
| `left` 与 `right` 差异 > 8 | 固定偏移 | `Padding(left: left)` 或 `Positioned(left: left)` |

**垂直方向解读：**

| relativePosition 条件 | AI 推导的布局意图 | Flutter 实现 |
|----------------------|----------------|-------------|
| `top ≈ bottom`（差值 ≤ 2）| 垂直居中 | `crossAxisAlignment: CrossAxisAlignment.center` |
| `top < 3` | 顶部对齐 | `crossAxisAlignment: CrossAxisAlignment.start` |
| `bottom < 3` | 底部对齐 | `crossAxisAlignment: CrossAxisAlignment.end` |
| `top ≠ bottom`（差值 > 4）| 垂直非居中 | `Align(alignment: Alignment(0, (top/(top+bottom))*2-1))` |

> **计算示例**：`left=16, right=16, parentW=375` → left+right=32 ≠ parentW，FILL 成立；
> `left=47, right=48` → left≈right，left=47>24 → 居中+内边距47。

#### layoutHint → Flutter 容器解读（AI 负责）

`layoutHint` 由插件对含 2+ 个子元素的 Group 输出：
`{ direction, gaps, uniformGap, edgeStart, edgeEnd, suggestedMainAxisAlignment, suggestedGap }`

| layoutHint 字段 | AI 推导 | Flutter 实现 |
|----------------|---------|-------------|
| `direction: 'Column'` | 子元素纵向排列 | `Column(children: [...])` |
| `direction: 'Row'` | 子元素横向排列 | `Row(children: [...])` |
| `uniformGap: true` + `suggestedGap: N` | 均匀间距 | 子元素间插 `Gap(N)` 或 `SizedBox(height: N)` |
| `uniformGap: false` + `gaps: [a, b, c]` | 非均匀间距 | 按 `gaps[]` 数组逐个设置 `Gap` |
| `suggestedMainAxisAlignment` | 主轴对齐建议 | 直接采用（已由测量数据推算） |
| `edgeStart ≈ edgeEnd`（差值 ≤ 3） | 起止边距均匀 | `padding: EdgeInsets.symmetric(...)` |
| `edgeStart < 3` | 起始无边距 | 无需 padding，子元素从 0 开始 |

> **注意**：`suggestedMainAxisAlignment` 是建议值，AI 应结合 `edgeStart`/`edgeEnd` 的绝对值确认。
> 若 `edgeStart = edgeEnd = 0` 且 `uniformGap = false`，考虑 `MainAxisAlignment.spaceBetween`。

#### Flutter 响应式禁止规则

| ❌ 禁止 | 原因 | ✅ 正确做法 |
|--------|------|-----------|
| `width: 343` | 仅适配设计稿尺寸 | `MediaQuery.of(context).size.width - 32` |
| `height: 812` | 仅适配特定设备 | `Expanded` / `Flexible` |
| `left: 47` | 无法适配其他尺寸 | 分析 Layout Intent，用 `Alignment` |
| `fontSize: 14` | 无响应式 | Token 如 `$t.bodyMd` |
| `Color(0xFF...)` | 硬编码颜色 | Token 如 `$c.primary` |

#### Flutter v4.0 新增映射规则

| 测量数据 | Flutter 转换 | 说明 |
|----------|-------------|------|
| `iconContentBounds.containerSize` | `SizedBox(width: W, height: H)` 外层占位 | Group 框架尺寸 |
| `iconContentBounds.contentSize` | `SvgPicture.asset(width: W, height: H)` 内层渲染 | 路径 union 尺寸 |
| `siblingIconAlignment.maxSlotSize` | 所有同行 Icon 的 SizedBox 统一此值 | 确保文字对齐 |
| `tagStyle.background` | `Color(转换后)` | 注意保留透明度 |
| `tagStyle.padding` | `EdgeInsets.symmetric(h: X, v: Y)` | 直接使用，不猜测 |
| `tagStyle.cornerRadius` | `BorderRadius.circular(X)` | 通常 2-4px |
| `fontName: PingFangSC-Semibold` | `FontWeight.w600` | 忽略 fontWeight 数值 |

#### Flutter 强制禁止规则（v4.0 新增）

| ❌ 禁止 | 后果 | ✅ 正确做法 |
|--------|------|-----------|
| 用 Icon Group 尺寸渲染 SVG | Icon 偏大 2-4px | containerSize 占位 + contentSize 渲染 |
| 同行 Icon 各用各的宽度 | 文字起始位置不一致 | maxSlotSize 统一占位 |
| 凭感觉写 Tag padding/bg | 需要 2-3 轮修正 | 直接用 tagStyle 数据 |
| fontWeight 直接用 Sketch 数值 | 合成加粗，文字过粗 | 用 fontName 后缀映射 |
| 根据语义推测添加 UI 元素 | UI 与设计稿不符 | 只还原测量数据中的元素 |

#### Flutter 验证清单

```
布局推导（relativePosition）:
□ left ≈ right (≤2pt) → 是否正确推导为 FILL / double.infinity？
□ left/right 不对称 → 是否避免了 CrossAxisAlignment.center？
□ top ≠ bottom (>4pt) → 是否使用了 Align 而非 Center？
□ layoutHint.uniformGap → 是否用 Gap(suggestedGap) 替代硬编码间距？

样式与规范:
□ 所有颜色使用项目 Token（$c.primary）或来自测量数据的原始值
□ 文字样式使用 Token（$t.bodyMd）或来自 getTextStyle 的实测值
□ 阴影使用 Token（$shadow.md）或来自 shadows 数组的实测值
□ 间距使用 Gap() 或 SizedBox，值来自 layoutHint.suggestedGap 或 Token
□ 圆角使用 flutterRadius 字段（如 BorderRadius.circular(12)）

精度验证:
□ Icon 使用 contentSize 渲染、containerSize 占位（非 Group.frame）
□ 同行 Icon 统一 SizedBox 占位尺寸（siblingIconAlignment.maxSlotSize）
□ Tag 容器 padding/background/cornerRadius 来自 tagStyle（非猜测）
□ fontWeight 按 fontName PostScript 名映射（PingFang ≤ w600）
□ 只还原测量数据中存在的元素（不根据语义推测添加）

设备测试:
□ 在 iPhone SE（320pt 宽）验证 FILL 布局不溢出
□ 在 iPhone 15 Pro Max（430pt 宽）验证边距等比例
□ 与设计稿截图叠加对比差异 < 2pt
```

---

### Vue 3 / CSS 深度还原转换规范

#### Layout Intent → Vue/CSS 代码映射

| Layout Intent | CSS / Vue 实现 | 说明 |
|---------------|---------------|------|
| `alignment: ['CENTER']` | `display: flex; justify-content: center; align-items: center;` | 完全居中 |
| `alignment: ['HORIZONTAL_CENTER']` | `margin: 0 auto;` 或 `justify-content: center;` | 水平居中 |
| `alignment: ['VERTICAL_CENTER']` | `align-items: center;` | 垂直居中 |
| `sizing.width: { type: 'FILL', margin: 16 }` | `width: calc(100% - 32px); margin: 0 16px;` | 填充宽度 |
| `sizing.width: { type: 'MATCH_PARENT' }` | `width: 100%;` | 匹配父宽 |
| `containerIntent.direction: 'COLUMN'` | `display: flex; flex-direction: column;` | 垂直排列 |
| `containerIntent.direction: 'ROW'` | `display: flex; flex-direction: row;` | 水平排列 |
| `containerIntent.gap: 16` | `gap: 16px;` | 间距 |
| `containerIntent.direction: 'GRID'` | `display: grid; grid-template-columns: repeat(...); gap: 16px;` | 网格布局 |

#### Vue 响应式禁止规则

| ❌ 禁止 | 原因 | ✅ 正确做法 |
|--------|------|-----------|
| `width: 343px` | 仅适配设计稿尺寸 | `width: calc(100% - 32px)` |
| `height: 812px` | 仅适配特定设备 | `min-height: 100vh` / `flex: 1` |
| `left: 47px` | 无法适配其他尺寸 | 分析 Layout Intent，用 flex |
| `font-size: 14px` | 无响应式 | CSS 变量 `var(--font-body-md)` |
| `color: #3B82F6` | 硬编码颜色 | CSS 变量 `var(--color-primary)` |

#### Vue 验证清单

```
□ 使用 CSS 变量（--spacing-md, --color-primary）
□ 使用 flex/grid 布局，避免绝对定位
□ 移动端使用 vw/vh 或 calc()
□ 在 375px / 1920px 视口测试
□ 与设计稿截图叠加对比
```

---

### 微信小程序深度还原转换规范

#### Layout Intent → 小程序代码映射

| Layout Intent | WXML / WXSS 实现 | 说明 |
|---------------|-----------------|------|
| `alignment: ['CENTER']` | `display: flex; justify-content: center; align-items: center;` | 完全居中 |
| `sizing.width: { type: 'FILL', margin: 16 }` | `width: calc(100% - 64rpx); margin: 0 32rpx;` | 填充宽度（注意 rpx） |
| `containerIntent.direction: 'COLUMN'` | `display: flex; flex-direction: column;` | 垂直排列 |
| `containerIntent.gap: 16` | 小程序不支持 gap，用 `margin-bottom: 32rpx;` | 间距（最后一个用 :last-child 移除） |

#### 小程序特殊规则

| 设计稿值 (px) | 小程序值 (rpx) | 转换公式 |
|--------------|---------------|---------|
| 16px | 32rpx | `rpx = px * 2`（基于 375 设计稿） |
| 343px | 686rpx 或 `calc(100% - 64rpx)` | 宽度优先用 calc |
| 14px (字号) | 28rpx | 字号也需转换 |

#### 小程序验证清单

```
□ 所有 px 值已转换为 rpx（×2）
□ 宽度使用 calc(100% - Xrpx) 而非固定 rpx
□ 间距用 margin 模拟 gap
□ 在微信开发者工具多机型预览
□ 与设计稿截图叠加对比
```

---

## 🛠️ 配合测量模板使用

调用 Sketch MCP 时，应结合 `templates/design-measurement/` 目录下的测量脚本：

| 场景 | 使用的模板 | 用途 |
|------|-----------|------|
| 单组件测量 | `component-measurement.js` | 获取尺寸、**自动计算相对边距** |
| 元素间距 | `gap-measurement.js` | 计算垂直/水平间距，检测是否统一 |
| 样式提取 | `style-extraction.js` | 渐变、阴影、圆角等完整参数 |

### 使用示例

```javascript
// 1. 先用模板获取完整信息
// （在 Sketch 中选中元素，运行 component-measurement.js）

// 2. 模板输出示例：
// === 相对边距（可直接使用）===
// 距父容器左边: 16
// 距父容器顶部: 8
// 距父容器右边: 16
// 距父容器底部: 8
// 父容器: Input (200x36)

// 3. 分析：上下边距相等 → 垂直居中设计
// 4. 转换为 Flutter：Container(alignment: Alignment.center, ...)
```

---

## ⚠️ 已知问题与解决方案

### 问题 1：渐变色被读取为单色

**严重程度**: 🔴 高

**现象**：
```javascript
layer.style.fills[0].color  // 返回 "#4a60b2ff"
layer.style.fills[0].fillType  // 返回 "Gradient"
```

当 `fillType` 是 `Gradient` 时，`color` 属性返回的是一个"代表色"，而非实际的渐变颜色。

**解决方案**：

```javascript
function getLayerColor(layer) {
  const fills = layer.style?.fills?.filter(f => f.enabled);
  if (!fills || fills.length === 0) return null;
  
  const fill = fills[0];
  
  // 关键：先判断 fillType
  if (fill.fillType === 'Gradient' && fill.gradient) {
    const g = fill.gradient;
    return {
      type: 'gradient',
      gradientType: g.gradientType,  // Linear | Radial | Angular
      direction: { from: g.from, to: g.to },
      colors: g.stops.map(s => ({
        position: s.position,
        color: s.color
      }))
    };
  }
  
  return {
    type: 'solid',
    color: fill.color
  };
}
```

### 问题 2：fontWeight 数值不准确（重要）

**严重程度**: 🔴 高

**现象**：`layer.style.fontWeight` 返回的数值（如 8）不能直接映射到 Flutter/CSS 字重。

```javascript
// ❌ 错误方式 - fontWeight 值不可靠
layer.style.fontWeight  // 返回 8，但实际可能是 Semibold(w600) 而非 ExtraBold(w800)
```

**原因**：Sketch API 的 `fontWeight` 属性返回的是内部权重值，不是标准的 100-900 字重值。

**解决方案**：通过 `sketchObject.font()` 获取真实的 PostScript 字体名称：

```javascript
function getTextFontInfo(textLayer) {
  const result = {
    fontSize: textLayer.style.fontSize,
    fontFamily: textLayer.style.fontFamily,
    fontWeight: textLayer.style.fontWeight,  // 仅供参考
    fontName: null,  // 关键：真实字体名称
    displayName: null
  };
  
  // 获取真实字体名称
  if (textLayer.sketchObject && textLayer.sketchObject.font) {
    const font = textLayer.sketchObject.font();
    if (font) {
      result.fontName = String(font.fontName());      // 如 "PingFangSC-Semibold"
      result.displayName = String(font.displayName()); // 如 "苹方-简 中粗体"
      result.familyName = String(font.familyName());   // 如 "PingFang SC"
    }
  }
  
  return result;
}
```

**PostScript 字体名到 Flutter FontWeight 映射**：

| PostScript 后缀 | 中文名 | Flutter | CSS |
|----------------|--------|---------|-----|
| `-Thin` | 极细 | w100 | 100 |
| `-Ultralight` | 纤细 | w200 | 200 |
| `-Light` | 细体 | w300 | 300 |
| `-Regular` | 常规 | w400 | 400 |
| `-Medium` | 中等 | w500 | 500 |
| `-Semibold` | 中粗体 | w600 | 600 |
| `-Bold` | 粗体 | w700 | 700 |
| `-Heavy` | 特粗 | w800 | 800 |
| `-Black` | 黑体 | w900 | 900 |

### 问题 3：图标必须从设计稿导出

**严重程度**: 🔴 高

**现象**：AI 可能会自己生成图标 SVG，而不是从设计稿中提取。

**正确做法**：使用 `sketch.export` API 导出设计稿中的图标：

```javascript
const sketch = require('sketch');

function exportIconAsSVG(layer) {
  // 导出为 Buffer
  const buffer = sketch.export(layer, {
    formats: 'svg',
    output: false,  // 关键：返回 Buffer 而非写文件
    scales: '1'
  });
  
  // Buffer 转字符串
  const svgString = buffer.toString('utf-8');
  
  // 清理 SVG（移除 Sketch 生成的多余属性）
  return svgString
    .replace(/id="[^"]*"/g, '')
    .replace(/sketch:type="[^"]*"/g, '')
    .replace(/xmlns:sketch="[^"]*"/g, '')
    .replace(/\s+/g, ' ')
    .trim();
}
```

**在 Flutter 中使用导出的 SVG**：

```dart
// 方式1：内联 SVG（推荐小图标）
SvgPicture.string(
  '''<svg>...</svg>''',
  width: 24,
  height: 24,
)

// 方式2：保存为文件（推荐大图标）
// 将导出的 SVG 保存到 assets/icons/ 目录
SvgPicture.asset('assets/icons/notification.svg')
```

### 问题 4：列表项间距测量

**严重程度**: 🔴 高

**现象**：列表中多个同类元素的间距可能不一致，需要逐个测量。

**测量脚本**：

```javascript
function measureListSpacing(container) {
  const children = container.layers;
  const spacings = [];
  
  for (let i = 0; i < children.length - 1; i++) {
    const current = children[i];
    const next = children[i + 1];
    
    // 垂直间距 = 下一个元素的 y - (当前元素的 y + 高度)
    const spacing = next.frame.y - (current.frame.y + current.frame.height);
    
    spacings.push({
      between: `${current.name} → ${next.name}`,
      spacing: Math.round(spacing)
    });
  }
  
  // 检查是否一致
  const uniqueSpacings = [...new Set(spacings.map(s => s.spacing))];
  
  return {
    spacings,
    isConsistent: uniqueSpacings.length === 1,
    recommendedSpacing: uniqueSpacings.length === 1 
      ? uniqueSpacings[0] 
      : Math.round(spacings.reduce((a, b) => a + b.spacing, 0) / spacings.length)
  };
}
```

**注意**：如果设计稿中间距不一致，应与设计师确认是故意还是误差。代码中应使用统一间距。

### 问题 5：Symbol 内部样式读取

**严重程度**: 🟡 中

**现象**：Symbol 实例的内部图层样式可能需要通过 `expandedLayers` 访问。

**解决方案**：

```javascript
if (layer.type === 'SymbolInstance') {
  layer.expandedLayers.forEach(nestedLayer => {
    // 递归处理嵌套图层
    measureLayer(nestedLayer);
  });
}
```

### 问题 6：阴影数组顺序

**严重程度**: 🟢 低

**现象**：多重阴影的顺序可能与视觉效果不一致。

**解决方案**：记录所有阴影，并按 blur 值或 enabled 状态排序。

---

## 📐 标准测量脚本

### 完整测量函数

```javascript
const sketch = require('sketch');

/**
 * 完整的图层测量函数
 * 正确处理渐变色、阴影、圆角等
 */
function measureLayerComplete(layer) {
  const result = {
    name: layer.name,
    type: layer.type,
    frame: {
      x: Math.round(layer.frame.x),
      y: Math.round(layer.frame.y),
      width: Math.round(layer.frame.width),
      height: Math.round(layer.frame.height),
    },
  };
  
  if (!layer.style) return result;
  
  // ========== 填充色 ==========
  const enabledFills = layer.style.fills?.filter(f => f.enabled) || [];
  if (enabledFills.length > 0) {
    result.fills = enabledFills.map(fill => {
      if (fill.fillType === 'Gradient' && fill.gradient) {
        const g = fill.gradient;
        return {
          type: 'gradient',
          gradientType: g.gradientType,
          from: g.from,
          to: g.to,
          stops: g.stops.map(s => ({
            position: s.position,
            color: s.color
          }))
        };
      }
      return {
        type: 'solid',
        color: fill.color
      };
    });
  }
  
  // ========== 边框 ==========
  const enabledBorders = layer.style.borders?.filter(b => b.enabled) || [];
  if (enabledBorders.length > 0) {
    result.borders = enabledBorders.map(b => ({
      color: b.color,
      thickness: b.thickness,
      position: b.position  // inside | outside | center
    }));
  }
  
  // ========== 阴影 ==========
  const enabledShadows = layer.style.shadows?.filter(s => s.enabled) || [];
  if (enabledShadows.length > 0) {
    result.shadows = enabledShadows.map(s => ({
      color: s.color,
      blur: s.blur,
      x: s.x,
      y: s.y,
      spread: s.spread || 0,
      isInner: s.isInnerShadow || false
    }));
  }
  
  // ========== 内阴影 ==========
  const innerShadows = layer.style.innerShadows?.filter(s => s.enabled) || [];
  if (innerShadows.length > 0) {
    result.innerShadows = innerShadows.map(s => ({
      color: s.color,
      blur: s.blur,
      x: s.x,
      y: s.y,
      spread: s.spread || 0
    }));
  }
  
  // ========== 圆角 ==========
  if (layer.style.corners) {
    const radii = layer.style.corners.radii;
    // 检查是否四角相同
    if (radii.every(r => r === radii[0])) {
      result.borderRadius = radii[0];
    } else {
      result.borderRadius = radii;  // [topLeft, topRight, bottomRight, bottomLeft]
    }
  }
  
  // ========== 文字属性 ==========
  if (layer.type === 'Text') {
    result.text = {
      content: layer.text,
      fontSize: layer.style.fontSize,
      fontFamily: layer.style.fontFamily,
      fontWeight: layer.style.fontWeight,
      textColor: layer.style.textColor,
      alignment: layer.style.alignment,
      lineHeight: layer.style.lineHeight,
      letterSpacing: layer.style.kerning
    };
  }
  
  // ========== 透明度 ==========
  if (layer.style.opacity !== undefined && layer.style.opacity !== 1) {
    result.opacity = layer.style.opacity;
  }
  
  // ========== 模糊效果 ==========
  if (layer.style.blur?.enabled) {
    result.blur = {
      type: layer.style.blur.blurType,
      radius: layer.style.blur.radius
    };
  }
  
  return result;
}

/**
 * 递归测量整个画板
 */
function measureArtboard(artboard) {
  const results = [];
  
  function traverse(layer, depth = 0) {
    if (layer.hidden) return;
    
    const data = measureLayerComplete(layer);
    data.depth = depth;
    results.push(data);
    
    // 递归子图层
    if (layer.layers) {
      layer.layers.forEach(child => traverse(child, depth + 1));
    }
    
    // Symbol 展开图层
    if (layer.expandedLayers) {
      layer.expandedLayers.forEach(child => traverse(child, depth + 1));
    }
  }
  
  traverse(artboard);
  return results;
}

// 使用示例
const doc = sketch.getSelectedDocument();
const artboard = doc.selectedLayers.layers[0];
const measurements = measureArtboard(artboard);
console.log(JSON.stringify(measurements, null, 2));
```

---

## 🎨 颜色转换

### Sketch 颜色到各框架

```javascript
/**
 * 将 Sketch 颜色转换为各框架格式
 * @param {string} sketchColor - Sketch 颜色格式 "#rrggbbaa"
 * @param {string} framework - 目标框架
 */
function convertColor(sketchColor, framework) {
  // Sketch 格式: #rrggbbaa
  const r = parseInt(sketchColor.slice(1, 3), 16);
  const g = parseInt(sketchColor.slice(3, 5), 16);
  const b = parseInt(sketchColor.slice(5, 7), 16);
  const a = parseInt(sketchColor.slice(7, 9), 16) / 255;
  
  switch (framework) {
    case 'flutter':
      // Flutter: Color(0xAARRGGBB)
      const flutterHex = sketchColor.slice(7, 9) + sketchColor.slice(1, 7);
      return `Color(0x${flutterHex.toUpperCase()})`;
    
    case 'css':
      // CSS: rgba(r, g, b, a)
      if (a === 1) {
        return sketchColor.slice(0, 7);
      }
      return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
    
    case 'react-native':
      // React Native: '#RRGGBBAA' 或 'rgba(...)'
      if (a === 1) {
        return `'${sketchColor.slice(0, 7)}'`;
      }
      return `'rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})'`;
    
    default:
      return sketchColor;
  }
}

// 渐变转换
function convertGradient(gradient, framework) {
  const colors = gradient.stops.map(s => 
    convertColor(s.color, framework)
  );
  
  switch (framework) {
    case 'flutter':
      return `LinearGradient(
  begin: Alignment(${gradient.from.x * 2 - 1}, ${gradient.from.y * 2 - 1}),
  end: Alignment(${gradient.to.x * 2 - 1}, ${gradient.to.y * 2 - 1}),
  colors: [${colors.join(', ')}],
)`;
    
    case 'css':
      const angle = Math.atan2(
        gradient.to.y - gradient.from.y,
        gradient.to.x - gradient.from.x
      ) * 180 / Math.PI + 90;
      return `linear-gradient(${angle}deg, ${colors.join(', ')})`;
    
    default:
      return colors;
  }
}
```

---

## ✅ 使用检查清单

### 开始测量前

- [ ] 确认选中正确的画板/Frame
- [ ] 检查是否有隐藏图层需要测量
- [ ] 确认设计稿是最新版本

### 测量过程中

- [ ] 对每个颜色检查 `fillType` 是否为 Gradient
- [ ] 阴影检查是否有多重阴影
- [ ] 圆角检查四角是否相同
- [ ] 文字检查行高是否有定义

### 测量完成后

- [ ] 与设计稿视觉对比
- [ ] 渐变色验证（最容易出错）
- [ ] 透明度验证
- [ ] 响应式适配验证

---

## 📚 相关资源

- [Sketch JavaScript API 文档](https://developer.sketch.com/reference/api/)
- [设计稿还原规范](../workflows/design-restoration.md)
- [设计稿还原指南](../workflows/design-restoration-guide.md)
- [测量模板](../../templates/design-measurement/README.md)
- [TextField 居中案例](../troubleshooting-cases/flutter/textfield-vertical-centering.md)
- [Text 普通文字垂直居中偏下（StrutStyle）](../troubleshooting-cases/flutter/text-vertical-centering-strutstyle.md)

---

**维护者**: MTA工作室  
**创建日期**: 2026-01-20  
**最后更新**: 2026-01-23

### 更新日志

| 版本 | 日期 | 更新内容 |
|------|------|---------|
| v1.0 | 2026-01-20 | 初始版本 |
| v1.1 | 2026-01-21 | 新增：fontWeight不可靠问题及fontName获取方案、图标导出规范、列表间距测量脚本 |
| v1.2 | 2026-01-23 | 新增：核心原则章节（测量+计算），强调配合测量模板使用，增加计算公式参考 |
| v1.3 | 2026-01-23 | 新增：深度还原模式（像素级精确还原），含完整测量脚本、布局分析、响应式转换流程 |
| v2.0 | 2026-02-11 | 重大更新：v4.0 测量数据解读规范（iconContentBounds/tagStyle/siblingIconAlignment），Icon检测放宽（8-64px, 0.5-2.0宽高比），新增 extractIconContentBounds/detectTagContainer/detectSiblingIconAlignment，Flutter v4.0 映射规则 |
