Skip to main content
Glama
1yhy
by 1yhy
grid-layout-research.md57 kB
# Grid 布局检测算法研究 ## 研究摘要 本文档综合多个来源的研究成果,设计将 Figma 设计转换为 CSS Grid 的布局检测算法。 ## 分析的资料来源 ### 1. 学术研究 **"GUI 布局推断算法" (ScienceDirect)** - 使用 **Allen 区间代数** 进行空间关系建模 - 两阶段算法: 1. 使用有向图将绝对坐标转换为相对定位 2. 应用模式匹配和图重写进行布局推断 - 在保持原始布局忠实度方面达到 97% 的准确率 - 84% 的视图在调整大小时保持比例 **Allen 的 13 种区间关系** (适用于一维空间分析): - `before/after`: 元素 A 完全在 B 之前/之后 - `meets/met-by`: 元素 A 恰好在 B 开始处结束 - `overlaps/overlapped-by`: 元素 A 部分重叠 B - `starts/started-by`: 元素 A 与 B 在同一位置开始 - `finishes/finished-by`: 元素 A 与 B 在同一位置结束 - `during/contains`: 元素 A 完全在 B 内部/外部 - `equals`: 元素 A 与 B 位置相同 ### 2. 开源实现 **FigmaToCode (bernaferrari/FigmaToCode)** - 使用中间 "AltNodes" 表示 - 分析自动布局、响应式约束、颜色变量 - 处理混合定位(绝对定位 + 自动布局) - 智能决策代码结构 **imgcook (阿里巴巴)** - 布局还原是"整个 D2C 流程的核心" - 使用包含绝对位置、大小、样式的扁平 JSON - 专家规则系统 + 机器学习混合方法 - 组件:页面拆分、分组、循环检测、多状态处理 - 规则系统优先用于可控性,接近 100% 可用性 **GRIDS (阿尔托大学)** - 基于 MILP(混合整数线性规划)的方法 - 数学优化用于网格生成 - Python 实现,使用 Gurobi 优化器 ### 3. 行业实现 **Figma 原生 Grid 自动布局 (2025年5月)** - 除水平/垂直外新增 Grid 流选项 - 支持跨度、行/列数 - 相比完整 CSS Grid 规范有限制 - 缺失:`fr` 单位、命名网格区域、子网格 **Screen Parsing (CMU - UIST 2021)** - 基于 ML 的 UI 结构推断 - 检测 7 种容器类型,包括网格 - 在 13 万 iOS + 8 万 Android 屏幕上训练 ## 当前实现分析 ### 现有 Flex 布局检测 当前代码库在 `src/algorithms/layout/detector.ts` 中有健壮的 Flex 检测: ``` 流程:元素 → 边界框 → 行/列分组 → 间距分析 → 对齐检测 → CSS 生成 ``` **关键算法:** 1. **Y 轴重叠** → 行分组(同一行的元素) 2. **X 轴重叠** → 列分组(同一列的元素) 3. **间距分析** → 一致间距检测(20% 标准差容差) 4. **对齐检测** → left/center/right/stretch 5. **布局评分** → 基于置信度的方向选择 ### 当前实现的不足 `LayoutInfo` 接口已定义 `type: "flex" | "absolute" | "grid"`,但 Grid 检测**尚未实现**。 ## Grid 布局检测算法设计 ### 核心概念:何时使用 Grid vs Flex | 标准 | Flexbox | CSS Grid | | ---------- | -------------- | --------------------- | | 维度 | 一维(行或列) | 二维(行和列) | | 行一致性 | 单行跨越全宽 | 多行且列对齐 | | 列数 | 每行可变 | 跨行一致 | | 单元格对齐 | 仅在行内 | 行和列都对齐 | | 间距 | 单一间距值 | row-gap 和 column-gap | ### 算法:Grid 检测 ```typescript interface GridAnalysisResult { isGrid: boolean; confidence: number; rows: number; columns: number; rowGap: number; columnGap: number; trackWidths: number[]; // 用于 grid-template-columns trackHeights: number[]; // 用于 grid-template-rows cellMap: (number | null)[][]; // 网格位置中的元素索引 } function detectGridLayout(rects: ElementRect[]): GridAnalysisResult { // 步骤 1:按行分组(Y 轴重叠) const rows = groupIntoRows(rects); // 步骤 2:检查跨行列数是否一致 const columnCounts = rows.map((row) => row.length); const isConsistentColumns = areValuesAligned(columnCounts, 0); // 步骤 3:检查跨行列对齐 const columnPositions = extractColumnPositions(rows); const areColumnsAligned = checkColumnAlignment(columnPositions); // 步骤 4:计算置信度 // Grid 置信度在以下情况下更高: // - 存在多行(> 1) // - 列跨行对齐 // - 行间距和列间距都一致 // 步骤 5:提取轨道尺寸 const trackWidths = calculateTrackWidths(rows, columnPositions); const trackHeights = calculateTrackHeights(rows); return result; } ``` ### 分步算法 #### 步骤 1:行检测(已有) ```typescript // 已实现:groupIntoRows() const rows = groupIntoRows(rects, tolerance); ``` #### 步骤 2:列对齐检测(新增) ```typescript function extractColumnPositions(rows: ElementRect[][]): number[][] { // 对每一行,提取元素的 X 位置 return rows.map((row) => row.map((el) => el.x).sort((a, b) => a - b)); } function checkColumnAlignment(columnPositions: number[][]): { isAligned: boolean; alignedPositions: number[]; tolerance: number; } { if (columnPositions.length < 2) { return { isAligned: false, alignedPositions: [], tolerance: 0 }; } // 合并所有 X 位置并聚类 const allPositions = columnPositions.flat(); const clusters = clusterValues(allPositions, tolerance); // 检查每行是否在聚类位置有元素 const alignedPositions = clusters.map((c) => c.center); // 验证跨行对齐 let alignedRows = 0; for (const row of columnPositions) { const rowAligned = row.every((x) => alignedPositions.some((ap) => Math.abs(x - ap) <= tolerance), ); if (rowAligned) alignedRows++; } return { isAligned: alignedRows / columnPositions.length >= 0.8, alignedPositions, tolerance, }; } ``` #### 步骤 3:Grid 置信度评分(新增) ```typescript function calculateGridConfidence( rows: ElementRect[][], columnAlignment: ColumnAlignmentResult, rowGapAnalysis: GapAnalysis, columnGapAnalysis: GapAnalysis, ): number { let score = 0; // 1. 多行(grid 必需) if (rows.length >= 2) score += 0.2; if (rows.length >= 3) score += 0.1; // 2. 一致的列数 const columnCounts = rows.map((r) => r.length); if (areValuesEqual(columnCounts)) score += 0.2; // 3. 跨行列对齐 if (columnAlignment.isAligned) score += 0.25; // 4. 一致的行间距 if (rowGapAnalysis.isConsistent) score += 0.1; // 5. 一致的列间距 if (columnGapAnalysis.isConsistent) score += 0.1; // 6. 二维规则性(元素形成规则矩阵) const expectedCells = rows.length * Math.max(...columnCounts); const actualCells = rows.reduce((sum, r) => sum + r.length, 0); const fillRatio = actualCells / expectedCells; if (fillRatio >= 0.75) score += 0.05; return Math.min(1, score); } ``` #### 步骤 4:轨道尺寸提取(新增) ```typescript function calculateTrackWidths( rows: ElementRect[][], alignedPositions: number[], ): (number | "auto" | "fr")[] { // 按列位置分组元素 const columns: ElementRect[][] = []; for (let i = 0; i < alignedPositions.length; i++) { columns[i] = []; } for (const row of rows) { for (const el of row) { const colIndex = alignedPositions.findIndex((pos) => Math.abs(el.x - pos) <= tolerance); if (colIndex >= 0) { columns[colIndex].push(el); } } } // 计算每列的宽度 return columns.map((col) => { const widths = col.map((el) => el.width); const avgWidth = widths.reduce((a, b) => a + b, 0) / widths.length; return roundToCommonValue(avgWidth); }); } function calculateTrackHeights(rows: ElementRect[][]): number[] { return rows.map((row) => { const heights = row.map((el) => el.height); const maxHeight = Math.max(...heights); return roundToCommonValue(maxHeight); }); } ``` #### 步骤 5:CSS Grid 生成(新增) ```typescript function generateGridCSS(analysis: GridAnalysisResult): CSSStyle { const css: CSSStyle = { display: "grid", }; // grid-template-columns const columns = analysis.trackWidths.map((w) => (typeof w === "number" ? `${w}px` : w)).join(" "); css["gridTemplateColumns"] = columns; // grid-template-rows(可选,通常为 auto) const rows = analysis.trackHeights .map((h) => (typeof h === "number" ? `${h}px` : "auto")) .join(" "); if (!rows.split(" ").every((r) => r === "auto")) { css["gridTemplateRows"] = rows; } // Gap if (analysis.rowGap > 0 || analysis.columnGap > 0) { if (analysis.rowGap === analysis.columnGap) { css.gap = `${analysis.rowGap}px`; } else { css.gap = `${analysis.rowGap}px ${analysis.columnGap}px`; } } return css; } ``` ### 决策树:Grid vs Flex vs Absolute ``` 开始 │ ▼ ┌─────────────────┐ │ 元素 ≥ 2? │──否──▶ position: absolute └────────┬────────┘ │是 ▼ ┌─────────────────┐ │ 存在重叠元素? │──是──▶ position: absolute(重叠部分) └────────┬────────┘ │否 ▼ ┌─────────────────┐ │ 检测到多行? │──否──▶ display: flex (row) └────────┬────────┘ │是 ▼ ┌─────────────────┐ │ 列跨行对齐? │──否──▶ display: flex (column) │ │ 嵌套 flex rows └────────┬────────┘ │是 ▼ ┌─────────────────┐ │ Grid 置信度 │──否──▶ display: flex (column) │ ≥ 0.6? │ └────────┬────────┘ │是 ▼ display: grid ``` ## 需要支持的额外 CSS 属性 ### Grid 特有属性 | 属性 | 优先级 | 描述 | | -------------------------------- | ------ | ---------------------- | | `display: grid` | P0 | 启用网格布局 | | `grid-template-columns` | P0 | 定义列轨道 | | `grid-template-rows` | P1 | 定义行轨道 | | `gap` / `row-gap` / `column-gap` | P0 | 轨道之间的间距 | | `grid-auto-flow` | P2 | 自动放置算法 | | `justify-items` | P1 | 在单元格中水平对齐项目 | | `align-items` | P1 | 在单元格中垂直对齐项目 | | `place-items` | P2 | align + justify 的简写 | ### 子元素属性 | 属性 | 优先级 | 描述 | | -------------- | ------ | ------------ | | `grid-column` | P1 | 列跨度/位置 | | `grid-row` | P1 | 行跨度/位置 | | `grid-area` | P2 | 命名网格区域 | | `justify-self` | P2 | 自身水平对齐 | | `align-self` | P2 | 自身垂直对齐 | ### 增强的 Flex 属性(缺失) | 属性 | 优先级 | 描述 | | ------------- | ------ | ------------------------- | | `flex-grow` | P1 | 元素增长因子 | | `flex-shrink` | P2 | 元素收缩因子 | | `flex-basis` | P2 | 增长/收缩前的初始大小 | | `flex` | P1 | 简写(grow shrink basis) | | `order` | P2 | 元素排序 | ## 实现计划 ### 阶段 1:修复现有 TODO (layout.ts) 将 `convertAlign` 拆分为两个函数: - `convertJustifyContent()` - 用于主轴对齐 - `convertAlignItems()` - 用于交叉轴对齐 ### 阶段 2:添加 Grid 检测 (detector.ts) 1. 添加 `GridAnalysisResult` 接口 2. 实现 `detectGridLayout()` 函数 3. 添加列对齐检测 4. 实现网格置信度评分 5. 添加轨道尺寸提取 ### 阶段 3:CSS 生成 (optimizer.ts) 1. 更新 `LayoutInfo` 使用以包含网格类型 2. 添加 `generateGridCSS()` 函数 3. 集成到 `optimizeDesign()` 管道 4. 添加 grid vs flex 决策树 ### 阶段 4:类型更新 (simplified.ts) 1. 向 `CSSStyle` 添加 Grid CSS 属性 2. 扩展 `LayoutInfo` 以包含 grid 特定数据 ### 阶段 5:测试 1. 为 grid 检测添加单元测试 2. 使用真实 Figma 数据添加集成测试 3. 测试边缘情况(不规则网格、混合布局) ## 当前实现问题(2024-12 分析) ### 问题:混合布局误判 使用真实 Figma 数据测试发现,Grid 检测算法错误地将混合布局识别为网格。 **示例案例:关键词管理面板 (node-402-34955)** ``` 容器:1580px × 340px 子元素: 1. 标签页 (320×41) left: 630px top: 0px ← 居中的标签页 2. 分隔线 (1580×1) left: 0px top: 41px ← 全宽线条 3. 信息栏 (1528×88) left: 26px top: 62px ← 几乎全宽 4. 卡片 1 (500×78) left: 26px top: 170px ┐ 5. 卡片 2 (500×78) left: 540px top: 170px ├─ 实际的网格候选元素 6. 卡片 3 (500×78) left: 1054px top: 170px ┘ 7. 卡片 4 (500×78) left: 26px top: 262px ← 第二行 ``` **检测结果(错误):** ```css display: grid; grid-template-columns: 1580px 1528px 500px 320px 500px; /* ❌ 总和 = 4428px > 1580px */ ``` **期望结果:** - 整体容器:`flex-direction: column` 或 `position: absolute` - 仅卡片部分(项目 4-7):`display: grid; grid-template-columns: repeat(3, 500px);` ### 根本原因分析 | 问题 | 代码位置 | 描述 | | ----------------------- | --------------------- | ----------------------------------------- | | **缺少元素类型过滤** | `detector.ts:1073` | 所有子元素被统一分析,不考虑大小/类型差异 | | **仅通过 Y 轴重叠分组** | `detector.ts:154-185` | 2px 容差忽略了视觉/功能上的差异 | | **缺少同质性检查** | N/A | 缺少验证元素是否"看起来相似"的逻辑 | | **严格的列对齐要求** | `detector.ts:908-911` | 80% 阈值在有意混合的布局中失效 | ### 研究:业界方法 #### 1. UI 语义分组检测(2024) > 来源:[arxiv.org/html/2403.04984v1](https://arxiv.org/html/2403.04984v1) - **核心洞察**:"在布局检测之前,将具有相似语义的相邻元素分组" - **方法**:基于 Transformer 的检测器,使用格式塔原则 - **相关性**:对同质元素进行预聚类 #### 2. UIHASH - 基于网格的 UI 相似性 > 来源:[jun-zeng.github.io](https://jun-zeng.github.io/file/uihash_paper.pdf) - **核心洞察**:"邻近原则 - 用户将相邻元素视为统一实体" - **方法**:将屏幕划分为区域,按组成控件编码 - **相关性**:网格分析前基于大小的聚类 #### 3. GUI 布局推断算法 > 来源:[ScienceDirect](https://www.sciencedirect.com/science/article/abs/pii/S0950584915001718) - **核心洞察**:"两阶段:相对定位 → 模式匹配" - **方法**:Allen 关系 + 探索式算法用于布局组合 - **相关性**:层次化布局检测 #### 4. 多级同质性结构 > 来源:[ScienceDirect](https://www.sciencedirect.com/science/article/abs/pii/S0957417417303469) - **核心洞察**:"自底向上聚合为同质区域" - **方法**:连通组件 → 单词 → 文本行 → 区域 - **相关性**:渐进式同质分组 ## 优化方案 ### 解决方案:同质元素预过滤 在 Grid 检测之前添加预过滤步骤,识别"看起来相似"且应该一起考虑的元素。 #### 同质性判断标准 ```typescript interface HomogeneityCheck { sizeVariance: number; // 宽度/高度方差(阈值:20%) typeConsistency: boolean; // 相同的节点类型 stylesSimilar: boolean; // 相似的 CSS 属性 } function isHomogeneousGroup(nodes: SimplifiedNode[]): boolean { if (nodes.length < 4) return false; // 1. 大小聚类 - 宽度/高度在 20% 方差范围内 const widths = nodes.map((n) => parseFloat(n.cssStyles?.width || "0")); const heights = nodes.map((n) => parseFloat(n.cssStyles?.height || "0")); const widthCV = coefficientOfVariation(widths); const heightCV = coefficientOfVariation(heights); if (widthCV > 0.2 || heightCV > 0.2) return false; // 2. 类型一致性 - 允许 FRAME + INSTANCE + COMPONENT const types = new Set(nodes.map((n) => n.type)); const allowedTypes = new Set(["FRAME", "INSTANCE", "COMPONENT", "GROUP"]); const hasDisallowedType = [...types].some((t) => !allowedTypes.has(t)); if (hasDisallowedType || types.size > 3) return false; // 3. 样式相似性(可选)- 背景、边框等 // ... return true; } ``` #### 更新的检测流程 ``` 之前: detectGridLayout(allChildren) → 错误的网格 之后: 1. clusterBySimilarSize(allChildren) → 大小分组 2. 对每个包含 4+ 元素的分组: a. isHomogeneousGroup(group) → true/false b. 如果为 true:detectGridLayout(group) 3. 剩余元素:使用 flex/absolute ``` #### 实现步骤 1. **添加 `isHomogeneousGroup()` 函数** - `detector.ts` 2. **添加 `clusterBySimilarSize()` 函数** - `detector.ts` 3. **更新 `LayoutOptimizer.optimizeContainer()`** - `optimizer.ts` - 在网格检测前调用同质性检查 - 只将同质元素传递给 `detectGridLayout()` 4. **添加测试** - `layout.test.ts` ### 预期结果 | 场景 | 之前 | 之后 | | ------------------------- | ---------------- | ----------------- | | 混合布局(标签页 + 卡片) | 所有元素错误网格 | 仅卡片部分 → 网格 | | 纯卡片网格(4+ 相同大小) | 正确的网格 | 正确的网格 | | 单行项目 | Flex 行 | Flex 行(无变化) | | 不规则大小 | 错误的网格 | Flex/absolute | ## 参考资料 - [Allen's Interval Algebra - Wikipedia](https://en.wikipedia.org/wiki/Allen's_interval_algebra) - [FigmaToCode - GitHub](https://github.com/bernaferrari/FigmaToCode) - [GRIDS Layout Engine - GitHub](https://github.com/aalto-ui/GRIDS) - [CSS Grid Layout Module Level 1 - W3C](https://www.w3.org/TR/css-grid-1/) - [Figma Grid Auto-Layout Help](https://help.figma.com/hc/en-us/articles/31289469907863-Use-the-grid-auto-layout-flow) - [Screen Parsing - CMU ML Blog](https://blog.ml.cmu.edu/2021/12/10/understanding-user-interfaces-with-screen-parsing/) --- ## 完整实现链路分析 本节深入分析 Grid 布局检测算法的代码实现,包括完整的调用链路和核心函数。 ### 系统架构:Grid vs Flex 决策 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Layout Detection Decision Tree │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────┐ │ │ │ optimizeContainer() │ │ │ │ [optimizer.ts:89] │ │ │ └───────────┬─────────────┘ │ │ │ │ │ ┌───────────────────┼───────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ STEP 1: 重叠检测 │ │ STEP 1.5: 背景 │ │ STEP 2: Grid │ │ │ │ IoU > 0.1 ? │ │ 检测 │ │ 检测 (优先) │ │ │ └────────┬────────┘ └────────────────┘ └────────┬────────┘ │ │ │ │ │ │ │ ┌────────────────────┤ │ │ │ │ │ │ │ │ Grid 检测成功? Grid 检测失败 │ │ │ confidence >= 0.6 │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ display: grid │ │ STEP 3: Flex │ │ │ │ │ grid-template- │ │ 检测 (后备) │ │ │ │ │ columns/rows │ │ │ │ │ │ └─────────────────┘ └────────┬────────┘ │ │ │ │ │ │ │ │ │ Flex 检测成功? │ │ │ │ score > 0.4 │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ 重叠元素保持 │ 生成 Grid CSS │ │ display: flex │ │ │ position: absolute │ + padding │ │ + gap + align │ │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ │ └───────────┴─────────────────────────────────────────────────────────────┘ ``` ### Grid 检测入口:detectGridIfApplicable ``` detectGridIfApplicable(nodes) - [optimizer.ts:918-961] ═══════════════════════════════════════════════════════════════════════════ ┌─────────────────────────────────┐ │ detectGridIfApplicable(nodes) │ │ [optimizer.ts:918] │ └───────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 前置检查 │ │ • nodes.length < 4 → return null│ │ • 最少需要 2×2 网格 │ └───────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ nodesToElementRects(nodes) │ │ 转换为 ElementRect[] │ │ [optimizer.ts:856-870] │ └───────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ filterHomogeneousForGrid(elementRects, nodeTypes) │ │ [detector.ts:1216-1235] │ │ │ │ ★ 关键步骤: 同质性过滤 │ │ • 筛选大小相似的元素 │ │ • 防止混合布局被错误识别为 Grid │ └───────────────────────────┬─────────────────────────┘ │ ┌─────────────────┴─────────────────┐ │ │ homogeneousElements.length < 4 homogeneousElements >= 4 │ │ ▼ ▼ return null ┌─────────────────────────┐ │ detectGridLayout() │ │ [detector.ts:1490] │ │ 在同质元素上运行 Grid 检测│ └───────────────┬─────────┘ │ ┌───────────────────────────────────────┤ │ │ isGrid && confidence >= 0.6 ? confidence < 0.6 rowCount >= 2 && columnCount >= 2 │ │ │ ▼ ▼ ┌─────────────────────────┐ return null │ return { │ │ gridResult, │ │ gridIndices │ │ } │ └─────────────────────────┘ ``` ### 同质性分析:analyzeHomogeneity ``` analyzeHomogeneity(rects, nodeTypes) - [detector.ts:1127-1196] ═══════════════════════════════════════════════════════════════════════════ 目的: 确保只有"相似"的元素参与 Grid 检测,避免混合布局误判 混合布局示例 (应该被过滤): ┌──────────────────────────────────────────────────────────────────────────┐ │ 容器 1580px × 340px │ │ │ │ ┌─────────────────────┐ ← 标签页 320×41 (异质) │ │ │ Tabs │ │ │ └─────────────────────┘ │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← 分隔线 1580×1 (异质) │ │ ┌──────────────────────────────────────────────────────────────────────┐│ │ │ Info Bar 1528×88 ││ │ └──────────────────────────────────────────────────────────────────────┘│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ← 同质元素 (Grid) │ │ │ Card 500×78│ │ Card 500×78│ │ Card 500×78│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ┌─────────────┐ │ │ │ Card 500×78│ │ │ └─────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘ 算法流程: ┌────────────────────────────────────────────────────────────────┐ │ 1. 大小聚类 (clusterBySimilarSize) │ │ │ │ 输入: 所有子元素的 ElementRect[] │ │ 容差: 20% (widthDiff <= 0.2 && heightDiff <= 0.2) │ │ │ │ 处理: │ │ for each rect: │ │ 找到大小相近的已有 cluster │ │ 如果找到 → 加入该 cluster │ │ 如果未找到 → 创建新 cluster │ │ │ │ 输出: SizeCluster[] (按元素数量降序排列) │ │ │ │ 示例: │ │ Cluster 1: [Card1, Card2, Card3, Card4] ← 500×78 │ │ Cluster 2: [InfoBar] ← 1528×88 │ │ Cluster 3: [Tabs] ← 320×41 │ │ Cluster 4: [Divider] ← 1580×1 │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 2. 取最大 Cluster (largestCluster) │ │ │ │ 如果 largestCluster.length < 4 → 非同质 │ │ │ │ 示例: [Card1, Card2, Card3, Card4] = 4 个元素 ✓ │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 3. 计算变异系数 (Coefficient of Variation) │ │ │ │ widthCV = stddev(widths) / mean(widths) │ │ heightCV = stddev(heights) / mean(heights) │ │ │ │ 如果 widthCV > 0.2 || heightCV > 0.2 → 非同质 │ │ │ │ 示例: │ │ widths = [500, 500, 500, 500] → CV = 0 ✓ │ │ heights = [78, 78, 78, 78] → CV = 0 ✓ │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 4. 类型一致性检查 (可选) │ │ │ │ 允许的类型: FRAME, INSTANCE, COMPONENT, GROUP, RECTANGLE │ │ 如果存在其他类型 → 非同质 │ │ │ │ 示例: 所有 Card 都是 FRAME → 通过 ✓ │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 输出: HomogeneityResult │ │ { │ │ isHomogeneous: true, │ │ widthCV: 0, │ │ heightCV: 0, │ │ types: ['FRAME'], │ │ homogeneousElements: [Card1, Card2, Card3, Card4], │ │ outlierElements: [Tabs, Divider, InfoBar] │ │ } │ └────────────────────────────────────────────────────────────────┘ ``` ### Grid 检测核心:detectGridLayout ``` detectGridLayout(rects) - [detector.ts:1490-1573] ═══════════════════════════════════════════════════════════════════════════ ┌─────────────────────────────────┐ │ detectGridLayout(rects) │ │ 输入: 已过滤的同质元素 │ └───────────────┬─────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ STEP 1: 行分组 (groupIntoRows) │ │ [detector.ts:1513] │ │ │ │ 使用 Y 轴重叠检测将元素分组为"行" │ │ │ │ 输入 (4 个卡片): │ │ ┌────┐ ┌────┐ ┌────┐ │ │ │ 1 │ │ 2 │ │ 3 │ y: 170, height: 78 │ │ └────┘ └────┘ └────┘ │ │ ┌────┐ │ │ │ 4 │ y: 262, height: 78 │ │ └────┘ │ │ │ │ 输出: rows = [[1, 2, 3], [4]] │ │ rowCount = 2 │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ STEP 2: 列对齐检测 (checkColumnAlignment) │ │ [detector.ts:1305-1339] │ │ │ │ 检查各行元素的 X 位置是否对齐 │ │ │ │ 1. 提取所有 X 位置: │ │ Row 1: [26, 540, 1054] │ │ Row 2: [26] │ │ │ │ 2. 聚类所有 X 位置 (tolerance=3px): │ │ Cluster 1: center=26 │ │ Cluster 2: center=540 │ │ Cluster 3: center=1054 │ │ │ │ 3. 验证各行元素是否在聚类位置: │ │ Row 1: [26 ≈ 26 ✓, 540 ≈ 540 ✓, 1054 ≈ 1054 ✓] │ │ Row 2: [26 ≈ 26 ✓] │ │ │ │ 输出: { isAligned: true, alignedPositions: [26, 540, 1054] } │ │ columnCount = 3 │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ STEP 3: 间距分析 │ │ [detector.ts:1524-1529] │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 行间距 (Row Gap): │ │ │ │ │ │ │ │ Row 1 bottom = max(170+78) = 248 │ │ │ │ Row 2 top = min(262) = 262 │ │ │ │ rowGap = 262 - 248 = 14px │ │ │ │ │ │ │ │ analyzeGaps([14]) → { isConsistent: true, rounded: 16 }│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 列间距 (Column Gap): │ │ │ │ │ │ │ │ Row 1: gap1 = 540 - (26+500) = 14px │ │ │ │ gap2 = 1054 - (540+500) = 14px │ │ │ │ │ │ │ │ analyzeGaps([14,14]) → { isConsistent: true, rounded: 16 }│ │ │ └─────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ STEP 4: Grid 置信度计算 (calculateGridConfidence) │ │ [detector.ts:1446-1479] │ │ │ │ 6 项评分因子: │ │ │ │ ┌────────────────────────────────────────────────────────────┐│ │ │ 因子 │ 条件 │ 得分 ││ │ ├────────────────────────────────────────────────────────────┤│ │ │ 1. 多行 (rows >= 2) │ 2 行 │ +0.2 ││ │ │ 2. 多行 (rows >= 3) │ 2 行 < 3 │ +0.0 ││ │ │ 3. 列数一致 │ [3, 1] 不一致 │ +0.0 ││ │ │ 4. 列对齐 │ isAligned = true │ +0.25 ││ │ │ 5. 行间距一致 │ isConsistent = true│ +0.1 ││ │ │ 6. 列间距一致 │ isConsistent = true│ +0.1 ││ │ │ 7. 填充率 >= 75% │ 4/6 = 67% < 75% │ +0.0 ││ │ └────────────────────────────────────────────────────────────┘│ │ │ │ 总分: 0.2 + 0.25 + 0.1 + 0.1 = 0.65 │ │ 阈值: 0.6 │ │ 结果: 0.65 >= 0.6 → isGrid = true ✓ │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ STEP 5-6: 轨道尺寸计算 │ │ [detector.ts:1552-1557] │ │ │ │ trackWidths = calculateTrackWidths(rows, alignedPositions) │ │ = [500, 500, 500] │ │ │ │ trackHeights = calculateTrackHeights(rows) │ │ = [78, 78] │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ STEP 7: 单元格映射 (buildCellMap) │ │ [detector.ts:1556] │ │ │ │ cellMap: │ │ ┌─────────────────────────────────────┐ │ │ │ Col 0 │ Col 1 │ Col 2 │ │ │ ├───────────┼───────────┼─────────────┤ │ │ │ Card 0 │ Card 1 │ Card 2 │ Row 0 │ │ │ Card 3 │ null │ null │ Row 1 │ │ └───────────┴───────────┴─────────────┘ │ └────────────────────────────────────────────────────────────────┘ ``` ### CSS Grid 生成 ``` generateGridCSS(gridResult) - [optimizer.ts:875-913] ═══════════════════════════════════════════════════════════════════════════ 输入: GridAnalysisResult 输出: CSS Grid 样式对象 ┌─────────────────────────────────┐ │ GridAnalysisResult │ │ { │ │ isGrid: true, │ │ rowCount: 2, │ │ columnCount: 3, │ │ rowGap: 16, │ │ columnGap: 16, │ │ trackWidths: [500, 500, 500],│ │ trackHeights: [78, 78] │ │ } │ └───────────────┬─────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 生成 grid-template-columns │ │ │ │ trackWidths = [500, 500, 500] │ │ → "500px 500px 500px" │ │ │ │ (如果所有宽度相同,可优化为 repeat(3, 500px)) │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 生成 grid-template-rows (仅当行高不同时) │ │ │ │ trackHeights = [78, 78] │ │ 所有行高相同 → 不设置 (使用 auto) │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 生成 gap │ │ │ │ rowGap = 16, columnGap = 16 │ │ rowGap === columnGap → gap: "16px" │ │ │ │ 如果不同 → gap: "${rowGap}px ${columnGap}px" │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ 输出 CSS: │ │ { │ │ display: "grid", │ │ gridTemplateColumns: "500px 500px 500px", │ │ gap: "16px" │ │ } │ └────────────────────────────────────────────────────────────────┘ ``` ### 核心数据结构 ```typescript // detector.ts 中的 Grid 相关数据结构 ═══════════════════════════════════════════════════════════════════════════ // Grid 分析结果 interface GridAnalysisResult { isGrid: boolean; // 是否检测到有效 Grid confidence: number; // 置信度 (0-1) rowCount: number; // 行数 columnCount: number; // 列数 rowGap: number; // 行间距 (px) columnGap: number; // 列间距 (px) isRowGapConsistent: boolean; // 行间距是否一致 isColumnGapConsistent: boolean; // 列间距是否一致 trackWidths: number[]; // 各列宽度 [col0Width, col1Width, ...] trackHeights: number[]; // 各行高度 [row0Height, row1Height, ...] alignedColumnPositions: number[]; // 对齐的列 X 坐标 rows: ElementRect[][]; // 分组后的行数据 cellMap: (number | null)[][]; // 单元格到元素索引的映射 } // 同质性分析结果 interface HomogeneityResult { isHomogeneous: boolean; // 是否同质 widthCV: number; // 宽度变异系数 heightCV: number; // 高度变异系数 types: string[]; // 元素类型列表 homogeneousElements: ElementRect[]; // 同质元素 outlierElements: ElementRect[]; // 异质元素 (不参与 Grid) } // 大小聚类 interface SizeCluster { width: number; // 聚类代表宽度 height: number; // 聚类代表高度 elements: ElementRect[]; // 属于该聚类的元素 types?: string[]; // 元素类型 } ``` ### 文件路径映射 | 功能模块 | 文件路径 | 行号范围 | | ------------- | ------------------------------------ | --------- | | Grid 入口 | `src/algorithms/layout/optimizer.ts` | 918-961 | | Grid CSS 生成 | `src/algorithms/layout/optimizer.ts` | 875-913 | | 同质性过滤 | `src/algorithms/layout/detector.ts` | 1216-1235 | | 同质性分析 | `src/algorithms/layout/detector.ts` | 1127-1196 | | 大小聚类 | `src/algorithms/layout/detector.ts` | 1077-1116 | | 变异系数计算 | `src/algorithms/layout/detector.ts` | 1057-1067 | | Grid 检测核心 | `src/algorithms/layout/detector.ts` | 1490-1573 | | 列对齐检测 | `src/algorithms/layout/detector.ts` | 1305-1339 | | 行间距计算 | `src/algorithms/layout/detector.ts` | 1344-1356 | | 列间距计算 | `src/algorithms/layout/detector.ts` | 1361-1376 | | 置信度计算 | `src/algorithms/layout/detector.ts` | 1446-1479 | | 轨道宽度计算 | `src/algorithms/layout/detector.ts` | 1381-1404 | | 轨道高度计算 | `src/algorithms/layout/detector.ts` | 1409-1415 | | 单元格映射 | `src/algorithms/layout/detector.ts` | 1420-1441 | --- _最后更新: 2025-12-06_

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1yhy/Figma-Context-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server