Skip to main content
Glama
1yhy
by 1yhy
layout-detection.md56.3 kB
# 设计稿转代码 - Flex 布局检测算法研究文档 ## 目录 1. [研究背景](#1-研究背景) 2. [行业方案分析](#2-行业方案分析) 3. [核心算法原理](#3-核心算法原理) 4. [算法实现详解](#4-算法实现详解) 5. [测试验证](#5-测试验证) 6. [最佳实践与建议](#6-最佳实践与建议) 7. [参考资源](#7-参考资源) --- ## 1. 研究背景 ### 1.1 问题描述 设计稿(如 Figma)中的元素通常使用**绝对定位**(x, y, width, height),而前端代码需要使用**相对布局**(Flexbox, Grid)才能实现响应式设计。 **核心挑战**:如何从扁平的绝对定位元素列表中,准确推断出 Flex 布局结构? ### 1.2 难点分析 | 难点 | 描述 | | -------- | ---------------------------------------------- | | 行列识别 | 如何判断元素是水平排列还是垂直排列? | | 嵌套结构 | 如何将扁平列表转换为嵌套的 DOM 树? | | 间距计算 | 如何判断 gap 是否一致,是否应该使用 gap 属性? | | 对齐检测 | 如何检测 justify-content 和 align-items? | | 重叠处理 | 如何处理需要 absolute 定位的重叠元素? | | 容差处理 | 设计稿中的微小偏移如何容错? | --- ## 2. 行业方案分析 ### 2.1 主流工具对比 | 工具 | 开发者 | 布局检测方式 | 开源 | | --------------- | ------------ | --------------------------- | ---- | | **FigmaToCode** | bernaferrari | 依赖 Figma Auto Layout 数据 | ✓ | | **Grida** | gridaco | 规则 + ML 混合 | ✓ | | **imgcook** | 阿里巴巴 | 规则系统 + 机器学习 | ✗ | | **Anima** | Anima | 约束推断 | ✗ | ### 2.2 FigmaToCode 分析 **GitHub**: https://github.com/bernaferrari/FigmaToCode **特点**: - 不做布局推断,直接映射 Figma 的 Auto Layout 属性 - 使用 AltNodes 作为中间表示层 - 对于非 Auto Layout 的设计,使用 absolute 定位 **局限**: - 依赖设计师正确使用 Auto Layout - 对于旧设计稿或手动布局的设计无能为力 ### 2.3 imgcook 布局算法(阿里巴巴) **文档**: https://www.alibabacloud.com/blog/imgcook-3-0-series-layout-algorithm-design-based-code-generation_597856 **核心流程**: ``` 平面化 JSON → 行列分组 → 布局推断 → 语义化 → 代码生成 ``` **关键技术**: 1. **页面分割**:将页面拆分为不同子模块 2. **分组算法**:确定元素的包含关系 3. **循环识别**:识别列表/重复元素 4. **多状态识别**:识别同一组件的不同状态 **技术选型**: - 规则系统用于布局算法(需要接近 100% 可用性) - 机器学习用于组件识别(可容忍一定误差) ### 2.4 学术研究 **论文**: "A layout inference algorithm for Graphical User Interfaces" **核心思想**: 1. 将坐标定位转换为基于有向图和 Allen 关系的相对定位 2. 使用模式匹配和图重写的探索算法 3. 生成多个布局方案,选择最优解 --- ## 3. 核心算法原理 ### 3.1 Y轴重叠检测法(行分组) **原理**:如果两个元素在 Y 轴上有重叠,它们属于同一行。 ``` 元素 A: y=10, height=30 → Y范围 [10, 40] 元素 B: y=20, height=30 → Y范围 [20, 50] [10, 40] 和 [20, 50] 有交集 → 同一行 ``` **算法**: ```typescript function isOverlappingY(a: Rect, b: Rect, tolerance = 0): boolean { return !(a.bottom + tolerance < b.y || b.bottom + tolerance < a.y); } ``` ### 3.2 X轴重叠检测法(列分组) **原理**:如果两个元素在 X 轴上有重叠,它们属于同一列。 ```typescript function isOverlappingX(a: Rect, b: Rect, tolerance = 0): boolean { return !(a.right + tolerance < b.x || b.right + tolerance < a.x); } ``` ### 3.3 布局方向判断 **算法流程**: 1. 将元素按 Y 轴重叠分组为"行" 2. 将元素按 X 轴重叠分组为"列" 3. 比较行分组和列分组的质量分数 4. 选择分数更高的布局方向 **质量分数计算**: - 分组数量合理性(理想:单一分组) - 间距一致性 - 交叉轴对齐程度 - 元素覆盖率 ### 3.4 间距一致性分析 **原理**:使用标准差判断间距是否一致。 ```typescript function analyzeGaps(gaps: number[], tolerancePercent = 20) { const avg = sum(gaps) / gaps.length; const stdDev = sqrt(variance(gaps)); // 标准差 <= 平均值 * 20% 认为一致 const isConsistent = stdDev <= avg * (tolerancePercent / 100); return { isConsistent, average: avg }; } ``` **间距圆整**: ```typescript const COMMON_GAPS = [0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64]; function roundToCommonGap(gap: number): number { // 找最接近的常用值,如果差距 > 4px 则使用四舍五入 } ``` ### 3.5 对齐方式检测 **水平对齐**: - 左对齐:所有元素的 `left` 值相近 - 右对齐:所有元素的 `right` 值相近 - 居中对齐:所有元素的 `centerX` 值相近 **垂直对齐**: - 顶对齐:所有元素的 `top` 值相近 - 底对齐:所有元素的 `bottom` 值相近 - 居中对齐:所有元素的 `centerY` 值相近 **CSS 映射**: ```typescript // Row 布局 justifyContent = horizontal alignment // flex-start, center, flex-end alignItems = vertical alignment // flex-start, center, flex-end // Column 布局 justifyContent = vertical alignment alignItems = horizontal alignment ``` ### 3.6 重叠元素检测 **原理**:使用 IoU(Intersection over Union)检测重叠程度。 ```typescript function isFullyOverlapping(a: Rect, b: Rect, threshold = 0.5): boolean { const overlapArea = calculateOverlapArea(a, b); const minArea = Math.min(area(a), area(b)); return overlapArea / minArea > threshold; } ``` **处理策略**: - 重叠元素需要使用 `position: absolute` - 非重叠元素可以使用 Flex 布局 --- ## 4. 算法实现详解 ### 4.1 数据结构 ```typescript // 元素边界框 interface BoundingBox { x: number; y: number; width: number; height: number; } // 扩展的元素矩形(包含计算属性) interface ElementRect extends BoundingBox { index: number; right: number; // x + width bottom: number; // y + height centerX: number; // x + width/2 centerY: number; // y + height/2 } // 布局分析结果 interface LayoutAnalysisResult { direction: "row" | "column" | "none"; confidence: number; gap: number; isGapConsistent: boolean; justifyContent: string; alignItems: string; rows: ElementRect[][]; columns: ElementRect[][]; overlappingElements: ElementRect[]; } // 布局树节点 interface LayoutNode { type: "container" | "element"; direction?: "row" | "column"; gap?: number; justifyContent?: string; alignItems?: string; children?: LayoutNode[]; elementIndex?: number; needsAbsolute?: boolean; } ``` ### 4.2 行分组算法 ```typescript function groupIntoRows(rects: ElementRect[], tolerance = 2): ElementRect[][] { if (rects.length <= 1) return [rects]; // 1. 按 Y 坐标排序 const sorted = [...rects].sort((a, b) => a.y - b.y); const rows: ElementRect[][] = []; let currentRow: ElementRect[] = [sorted[0]]; // 2. 遍历检查重叠 for (let i = 1; i < sorted.length; i++) { const elem = sorted[i]; // 检查是否与当前行的任意元素在 Y 轴上重叠 const overlapsWithRow = currentRow.some((rowElem) => isOverlappingY(rowElem, elem, tolerance)); if (overlapsWithRow) { currentRow.push(elem); } else { // 当前行完成,按 X 排序后保存 rows.push(currentRow.sort((a, b) => a.x - b.x)); currentRow = [elem]; } } // 3. 保存最后一行 if (currentRow.length > 0) { rows.push(currentRow.sort((a, b) => a.x - b.x)); } return rows; } ``` ### 4.3 布局方向检测 ```typescript function detectLayoutDirection(rects: ElementRect[]) { const rows = groupIntoRows(rects); const columns = groupIntoColumns(rects); // 计算行布局分数 const rowScore = calculateLayoutScore(rows, "row", rects.length); // 计算列布局分数 const columnScore = calculateLayoutScore(columns, "column", rects.length); // 选择分数更高的布局 if (rowScore.score > columnScore.score && rowScore.score > 0.3) { return { direction: "row", confidence: rowScore.score }; } else if (columnScore.score > rowScore.score && columnScore.score > 0.3) { return { direction: "column", confidence: columnScore.score }; } return { direction: "none", confidence: 0 }; } ``` ### 4.4 布局分数计算 ```typescript function calculateLayoutScore(groups, direction, totalElements) { let score = 0; // 1. 分组数量合理性 if (groups.length === 1 && groups[0].length === totalElements) { score += 0.4; // 完美分组 } else if (groups.length <= 3) { score += 0.2; // 分组合理 } // 2. 间距一致性 for (const group of groups) { if (group.length >= 2) { const gaps = calculateGaps(group, direction); const gapAnalysis = analyzeGaps(gaps); if (gapAnalysis.isConsistent) { score += 0.3 / groups.length; } } } // 3. 交叉轴对齐 for (const group of groups) { if (group.length >= 2) { const alignment = analyzeAlignment(group); if (alignment !== "none") { score += 0.2 / groups.length; } } } // 4. 主要分布集中度 const largestGroup = groups.reduce((a, b) => (a.length > b.length ? a : b)); if (largestGroup.length >= totalElements * 0.7) { score += 0.1; } return { score: Math.min(1, score) }; } ``` ### 4.5 递归布局树构建 ```typescript function buildLayoutTree(rects: ElementRect[], depth = 0): LayoutNode { // 单个元素直接返回 if (rects.length === 1) { return { type: "element", elementIndex: rects[0].index }; } // 达到最大深度 if (depth >= 5) { return { type: "container", direction: "column", children: rects.map((r) => ({ type: "element", elementIndex: r.index })), }; } // 分析布局 const analysis = analyzeLayout(rects); // 处理重叠元素 const overlappingNodes = analysis.overlappingElements.map((r) => ({ type: "element", elementIndex: r.index, needsAbsolute: true, })); // 过滤掉重叠元素 const nonOverlapping = rects.filter( (r) => !analysis.overlappingElements.some((o) => o.index === r.index), ); if (analysis.direction === "none") { return { type: "container", direction: "column", children: [ ...nonOverlapping.map((r) => ({ type: "element", elementIndex: r.index, })), ...overlappingNodes, ], }; } // 根据布局方向分组并递归 const groups = analysis.direction === "row" ? analysis.rows : analysis.columns; const children = groups.map((group) => { if (group.length === 1) { return { type: "element", elementIndex: group[0].index }; } return buildLayoutTree(group, depth + 1); }); return { type: "container", direction: analysis.direction, gap: analysis.isGapConsistent && analysis.gap > 0 ? analysis.gap : undefined, justifyContent: analysis.justifyContent !== "flex-start" ? analysis.justifyContent : undefined, alignItems: analysis.alignItems !== "stretch" ? analysis.alignItems : undefined, children: [...children, ...overlappingNodes], }; } ``` --- ## 5. 测试验证 ### 5.1 测试数据 使用真实的 Figma 导出数据进行测试: - 数据来源:TikTok 风格的短视频卡片设计 - 节点数量:17 个 - 包含:文本、图标(SVG向量组)、容器 ### 5.2 测试结果 ``` 总容器数: 4 - Flex Row: 0 (0.0%) - Flex Column: 1 (25.0%) - Absolute (重叠): 3 (75.0%) - Mixed/Unknown: 0 (0.0%) ``` **分析**: - 主 Group 被正确识别为 **flex-column** 布局 - 3 个图标组(由重叠的 SVG 向量组成)被正确识别需要 **absolute** 定位 - 算法正确处理了重叠元素的情况 ### 5.3 单元测试结果 | 测试项 | 结果 | | -------------------------------- | ------ | | 边界框提取 | ✓ 通过 | | 行分组算法 | ✓ 通过 | | 列分组算法 | ✓ 通过 | | 一致间距检测 (16px, stdDev=0.63) | ✓ 通过 | | 不一致间距检测 | ✓ 通过 | | 左对齐检测 | ✓ 通过 | | 居中对齐检测 | ✓ 通过 | | 布局树构建 | ✓ 通过 | --- ## 6. 最佳实践与建议 ### 6.1 算法调优参数 | 参数 | 默认值 | 说明 | | -------------- | --------- | -------------------------- | | 重叠容差 | 2px | Y/X 轴重叠检测的容差 | | 间距一致性阈值 | 20% | 标准差/平均值的比例阈值 | | 对齐容差 | 3px 或 2% | 对齐检测的容差(取较大值) | | 布局置信度阈值 | 0.3 | 低于此值认为无明确布局 | | 最大递归深度 | 5 | 布局树构建的最大深度 | ### 6.2 处理特殊情况 **1. 图标/SVG 向量组** - 通常由多个重叠的向量组成 - 应识别为需要 absolute 定位 - 建议导出为单个 SVG 图片 **2. 复杂嵌套布局** - 使用递归分析 - 限制最大深度防止过度嵌套 - 对于超复杂布局,可能需要人工干预 **3. 设计师未使用 Auto Layout** - 依赖本算法推断布局 - 建议设计师学习使用 Auto Layout - 可以提供布局建议/警告 ### 6.3 与 Figma Auto Layout 的配合 当 Figma 节点包含 `layoutMode` 属性时,优先使用 Figma 的布局数据: ```typescript function processNode(node: FigmaNode) { // 优先使用 Figma 的 Auto Layout 数据 if (node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL") { return { direction: node.layoutMode === "HORIZONTAL" ? "row" : "column", gap: node.itemSpacing, // ... 映射其他属性 }; } // 否则使用推断算法 return inferLayout(node.children); } ``` ### 6.4 性能优化建议 1. **缓存计算结果**:对于相同的节点集合,缓存布局分析结果 2. **并行处理**:对于独立的子树,可以并行分析 3. **早期剪枝**:如果分组数量过多,可以提前放弃推断 4. **增量更新**:设计变更时只重新分析受影响的部分 --- ## 7. 参考资源 ### 7.1 开源项目 | 项目 | 链接 | 说明 | | ------------- | ------------------------------------------- | ---------------------- | | FigmaToCode | https://github.com/bernaferrari/FigmaToCode | Figma 转代码插件 | | Grida Code | https://github.com/gridaco/code | 设计转代码引擎 | | Facebook Yoga | https://github.com/facebook/yoga | C++ Flexbox 布局引擎 | | Taffy | https://github.com/DioxusLabs/taffy | Rust Flexbox/Grid 引擎 | ### 7.2 文档与文章 - [imgcook 布局算法](https://www.alibabacloud.com/blog/imgcook-3-0-series-layout-algorithm-design-based-code-generation_597856) - 阿里巴巴 - [GUI 布局推断算法论文](https://www.sciencedirect.com/science/article/abs/pii/S0950584915001718) - 学术研究 - [Flexbox 布局引擎实现](https://tchayen.com/how-to-write-a-flexbox-layout-engine) - 600 行代码实现 - [Figma Auto Layout 文档](https://help.figma.com/hc/en-us/articles/360040451373-Guide-to-auto-layout) - 官方指南 ### 7.3 本项目文件 | 文件 | 说明 | | -------------------------------------- | -------------------- | | `src/utils/layout-detection.ts` | 核心布局检测算法实现 | | `src/transformers/layout-optimizer.ts` | 布局优化器(集成版) | | `test/test-layout-detection.ts` | 单元测试 | | `test/test-full-layout-analysis.ts` | 集成测试 | | `test/generate-html-preview.ts` | HTML 预览生成器 | --- ## 附录A:算法流程图 ``` ┌─────────────────┐ │ 输入: 元素列表 │ │ (绝对定位坐标) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 提取边界框 │ │ (x,y,w,h) │ └────────┬────────┘ │ ┌──────────────┴──────────────┐ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Y轴重叠分组 │ │ X轴重叠分组 │ │ (检测行) │ │ (检测列) │ └────────┬────────┘ └────────┬────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ 计算行布局分数 │ │ 计算列布局分数 │ └────────┬────────┘ └────────┬────────┘ │ │ └──────────────┬──────────────┘ │ ▼ ┌─────────────────┐ │ 比较分数 │ │ 选择方向 │ └────────┬────────┘ │ ┌──────────────┴──────────────┐ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ 检测重叠元素 │ │ 分析间距/对齐 │ │ (需要absolute) │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ └──────────────┬──────────────┘ │ ▼ ┌─────────────────┐ │ 递归构建 │ │ 布局树 │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 生成 CSS │ │ flex 属性 │ └─────────────────┘ ``` --- ## 附录B:完整实现链路分析 本节深入分析布局检测算法的代码实现,包括完整的调用链路和核心函数。 ### B.1 系统架构概览 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Layout Optimization System │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ optimizer.ts (协调层) │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ optimizeDesign() → optimizeNodeTree() → optimizeContainer() │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────────┼───────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │ │ detector.ts │ │ detector.ts │ │ optimizer.ts │ │ │ │ (重叠检测) │ │ (布局检测) │ │ (样式生成) │ │ │ │ │ │ │ │ │ │ │ │ detectOverlapping() │ │ analyzeLayout() │ │ generateGridCSS() │ │ │ │ detectBackground() │ │ detectGrid() │ │ convertToRelative() │ │ │ └─────────────────────┘ └─────────────────┘ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### B.2 核心入口函数调用链 ``` src/algorithms/layout/optimizer.ts ═══════════════════════════════════════════════════════════════════════════ ┌─────────────────────────┐ │ LayoutOptimizer. │ │ optimizeDesign(design) │ │ [optimizer.ts:36] │ └───────────┬─────────────┘ │ 重置 containerIdCounter │ ▼ ┌─────────────────────────────────┐ │ design.nodes.map((node) => │ │ optimizeNodeTree(node) │ │ ) │ │ [optimizer.ts:46] │ └───────────────┬─────────────────┘ │ ▼ (递归处理每个节点) ┌─────────────────────────────────┐ │ optimizeNodeTree(node) │ │ [optimizer.ts:61] │ │ │ │ 对每个子节点递归调用自身, │ │ 然后调用 optimizeContainer() │ └───────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ optimizeContainer(node) │ │ [optimizer.ts:89] │ │ │ │ 核心优化逻辑 (见 B.3) │ └─────────────────────────────────┘ ``` ### B.3 optimizeContainer 四步算法 ``` optimizeContainer(node) - [optimizer.ts:89-356] ═══════════════════════════════════════════════════════════════════════════ 输入: SimplifiedNode (带 children 的容器节点) 输出: 优化后的 SimplifiedNode ┌─────────────────────────────────────────────────────────────────────────┐ │ 前置检查 │ │ • children.length <= 1 → 直接返回 │ │ • 检查是否为 FRAME/GROUP 容器 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ STEP 1: 重叠检测 (Overlap Detection) │ │ [optimizer.ts:98-109] │ │ │ │ elementRects = nodesToElementRects(children) │ │ │ │ │ ▼ │ │ overlapResult = detectOverlappingElements(elementRects, 0.1) │ │ │ │ │ ┌───────────────┴───────────────┐ │ │ │ │ │ │ ▼ ▼ │ │ flowElements[] stackedElements[] │ │ (参与布局) (保持 absolute) │ │ │ │ 如果 flowElements.length < 2 → 跳过布局优化 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ STEP 1.5: 背景元素检测 (Background Detection) │ │ [optimizer.ts:111-151] │ │ │ │ detectBackgroundElement(elementRects, parentWidth, parentHeight) │ │ │ │ │ ┌─────────────┴─────────────┐ │ │ │ hasBackground: true │ │ │ │ backgroundIndex >= 0 │ │ │ └─────────────┬─────────────┘ │ │ │ │ │ isBackgroundElement() ──► extractBackgroundStyles() │ │ │ │ │ │ │ ▼ │ │ │ mergedBackgroundStyles = { │ │ │ backgroundColor, borderRadius, │ │ │ border, boxShadow... │ │ │ } │ │ │ │ │ ▼ │ │ filteredChildren = children.filter(非背景元素) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ STEP 2: Grid 检测 (Grid Detection) - 优先于 Flex │ │ [optimizer.ts:165-206] │ │ │ │ if (isContainer) { │ │ gridDetection = detectGridIfApplicable(filteredChildren) │ │ │ │ │ if (gridDetection) { │ │ ┌─────────────┴─────────────┐ │ │ │ gridResult │ │ │ │ gridIndices (参与Grid元素) │ │ │ └─────────────┬─────────────┘ │ │ │ │ │ gridStyles = generateGridCSS(gridResult) │ │ │ │ │ convertAbsoluteToRelative(..., gridIndices) │ │ │ │ │ return 带 Grid 布局的节点 ──────────────────► [END] │ │ } │ │ } │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ (Grid 未检测到) ┌─────────────────────────────────────────────────────────────────────────┐ │ STEP 3: Flex 检测 (Flex Detection) - 后备方案 │ │ [optimizer.ts:208-356] │ │ │ │ { isRow, isColumn, rowGap, columnGap, │ │ isGapConsistent, justifyContent, alignItems } │ │ = analyzeLayoutDirection(filteredChildren) │ │ │ │ │ ┌───────────────┴───────────────┐ │ │ │ │ │ │ isRow = true isColumn = true │ │ direction: 'row' direction: 'column' │ │ │ │ │ │ └───────────────┬───────────────┘ │ │ │ │ │ flexStyles = { │ │ display: 'flex', │ │ flexDirection: direction, // 只在 column 时设置 │ │ gap: `${gap}px`, // 只在一致时设置 │ │ justifyContent, │ │ alignItems │ │ } │ │ │ │ │ convertAbsoluteToRelative() → padding + 清理子元素样式 │ │ │ │ │ return 带 Flex 布局的节点 │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### B.4 Flex 布局检测详细流程 ``` analyzeLayoutDirection(nodes) - [optimizer.ts:361-438] ═══════════════════════════════════════════════════════════════════════════ 输入: SimplifiedNode[] (子节点数组) 输出: { isRow, isColumn, rowGap, columnGap, isGapConsistent, justifyContent, alignItems } ┌─────────────────────────────────┐ │ 提取位置信息 │ │ rects = nodes.map(node => { │ │ left, top, width, height │ │ }) │ └───────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ analyzeAlignment(rects) │ │ [optimizer.ts:443-489] │ │ │ │ 分析水平/垂直对齐情况: │ │ • leftAligned (左对齐) │ │ • rightAligned (右对齐) │ │ • centerHAligned (水平居中) │ │ • topAligned (顶对齐) │ │ • bottomAligned (底对齐) │ │ • centerVAligned (垂直居中) │ └───────────────┬─────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ┌─────────────────────────┐ ┌─────────────────────────┐ │ calculateRowScore() │ │ calculateColumnScore() │ │ [optimizer.ts:551-579] │ │ [optimizer.ts:584-612] │ │ │ │ │ │ 行布局分数计算: │ │ 列布局分数计算: │ │ │ │ │ │ 1. 按 left 排序 │ │ 1. 按 top 排序 │ │ │ │ │ │ 2. 计算水平间距 │ │ 2. 计算垂直间距 │ │ gap = next.left - │ │ gap = next.top - │ │ current.right │ │ current.bottom │ │ │ │ │ │ 3. 统计连续正间距数 │ │ 3. 统计连续正间距数 │ │ (0 <= gap <= 50px) │ │ (0 <= gap <= 50px) │ │ │ │ │ │ 4. 分数计算: │ │ 4. 分数计算: │ │ distribution * 0.7 │ │ distribution * 0.7 │ │ + alignment * 0.3 │ │ + alignment * 0.3 │ └───────────────┬─────────┘ └───────────────┬──────────┘ │ │ └───────────────┬───────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 布局方向决策 │ │ │ │ if (rowScore > columnScore │ │ && rowScore > 0.4) │ │ → isRow = true │ │ │ │ if (columnScore > rowScore │ │ && columnScore > 0.4) │ │ → isColumn = true │ └───────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ CSS 属性映射 │ │ │ │ Row 布局: │ │ justify = horizontal align │ │ align = vertical align │ │ │ │ Column 布局: │ │ justify = vertical align │ │ align = horizontal align │ └─────────────────────────────────┘ ``` ### B.5 重叠检测算法 (IoU) ``` detectOverlappingElements(rects, iouThreshold=0.1) - [detector.ts:246-281] ═══════════════════════════════════════════════════════════════════════════ IoU (Intersection over Union) 计算: ┌──────────────────────────────┐ │ Element A │ │ ┌──────────────────────┐ │ │ │ Intersection │ │ └────┼──────────────────────┼──┘ │ Element B │ └──────────────────────┘ IoU = Intersection Area / Union Area where: Intersection = overlap_width × overlap_height Union = Area_A + Area_B - Intersection 算法流程: ┌────────────────────────────────────────────────────────────────┐ │ for each pair (i, j) where i < j: │ │ │ │ iou = calculateIoU(rects[i], rects[j]) │ │ │ │ if (iou > 0.1): │ │ stackedIndices.add(rects[i].index) │ │ stackedIndices.add(rects[j].index) │ │ │ │ 结果: │ │ flowElements = 不在 stackedIndices 中的元素 │ │ stackedElements = 在 stackedIndices 中的元素 │ └────────────────────────────────────────────────────────────────┘ IoU 阈值分类: ┌────────────┬───────────────────────────────────────────────────┐ │ IoU 范围 │ 分类 │ ├────────────┼───────────────────────────────────────────────────┤ │ = 0 │ none/adjacent (无重叠或相邻) │ │ 0 < x < 0.1│ partial (轻微重叠 - 可参与布局) │ │ 0.1 ≤ x < 0.5│ significant (显著重叠 - 需要 absolute) │ │ ≥ 0.5 │ contained (严重重叠/包含关系) │ └────────────┴───────────────────────────────────────────────────┘ ``` ### B.6 间距分析算法 ``` calculateGaps() + analyzeGaps() - [detector.ts:447-509] ═══════════════════════════════════════════════════════════════════════════ 间距计算示例 (horizontal 方向): ┌────┐ gap1 ┌────┐ gap2 ┌────┐ │ A │ ──────── │ B │ ──────── │ C │ └────┘ └────┘ └────┘ right left right left gap1 = B.x - A.right gap2 = C.x - B.right 间距一致性分析: ┌────────────────────────────────────────────────────────────────┐ │ analyzeGaps(gaps, tolerancePercent=20) │ │ │ │ 1. 计算平均值: │ │ average = sum(gaps) / gaps.length │ │ │ │ 2. 计算标准差: │ │ variance = Σ(gap - average)² / n │ │ stdDev = √variance │ │ │ │ 3. 一致性判断: │ │ tolerance = average × 20% │ │ isConsistent = (stdDev <= tolerance) │ │ │ │ 4. 圆整到常用值: │ │ COMMON_VALUES = [0,2,4,6,8,10,12,16,20,24,32,40,48,64...] │ │ rounded = findClosest(average, COMMON_VALUES) │ └────────────────────────────────────────────────────────────────┘ 示例: ┌──────────────────────────────────────────────────────────────┐ │ gaps = [16, 15, 17, 16] │ │ average = 16 │ │ stdDev = 0.71 │ │ tolerance = 16 × 0.2 = 3.2 │ │ isConsistent = (0.71 <= 3.2) = true ✓ │ │ rounded = 16px │ └──────────────────────────────────────────────────────────────┘ ``` ### B.7 绝对定位转相对定位 ``` convertAbsoluteToRelative() - [optimizer.ts:1591-1685] ═══════════════════════════════════════════════════════════════════════════ 原始布局 (绝对定位): ┌────────────────────────────────────────────────┐ │ Parent Container │ │ ┌──────────────────────────────────────────┐ │ │ │ ← paddingLeft │ │ │ │ ┌────────┐ gap ┌────────┐ │ │ │ │ │ Child1 │ ──────── │ Child2 │ │ │ │ │ │left:20 │ │left:140│ │ │ │ │ │top:10 │ │top:10 │ │ │ │ │ └────────┘ └────────┘ │ │ │ │ paddingRight→│ │ └──────────────────────────────────────────┘ │ │ ↓ paddingBottom │ └────────────────────────────────────────────────┘ 转换后 (Flex 布局): ┌────────────────────────────────────────────────┐ │ Parent Container │ │ display: flex; │ │ padding: 10px 30px 20px 20px; │ │ gap: 20px; │ │ ┌──────────────────────────────────────────┐ │ │ │ ┌────────┐ ←gap→ ┌────────┐ │ │ │ │ │ Child1 │ │ Child2 │ │ │ │ │ │(无left)│ │(无left)│ │ │ │ │ │(无top) │ │(无top) │ │ │ │ │ └────────┘ └────────┘ │ │ │ └──────────────────────────────────────────┘ │ └────────────────────────────────────────────────┘ 转换步骤: ┌────────────────────────────────────────────────────────────────┐ │ 1. collectFlowChildOffsets() │ │ 收集所有流式子元素的位置信息 │ │ │ │ 2. inferContainerPadding() │ │ 从子元素位置推断容器 padding: │ │ • paddingLeft = min(children.left) │ │ • paddingTop = min(children.top) │ │ • paddingRight = parentWidth - max(children.right) │ │ • paddingBottom = parentHeight - max(children.bottom) │ │ │ │ 3. calculateChildMargins() │ │ 计算子元素的个别 margin 调整 (交叉轴偏移) │ │ │ │ 4. generatePaddingCSS() │ │ 生成 CSS padding 简写: │ │ • 全相同: "10px" │ │ • 上下相同,左右相同: "10px 20px" │ │ • 全不同: "10px 20px 30px 40px" │ │ │ │ 5. cleanChildStylesForLayout() │ │ 清理子元素样式: │ │ • 删除 position: absolute │ │ • 删除 left, top │ │ • 保留 width, height │ └────────────────────────────────────────────────────────────────┘ ``` ### B.8 核心数据结构 ```typescript // detector.ts 中的核心数据结构 ═══════════════════════════════════════════════════════════════════════════ // 基础边界框 interface BoundingBox { x: number; // 左边距 y: number; // 上边距 width: number; // 宽度 height: number; // 高度 } // 扩展元素矩形 (包含计算属性) interface ElementRect extends BoundingBox { index: number; // 在原始数组中的索引 right: number; // x + width bottom: number; // y + height centerX: number; // x + width/2 centerY: number; // y + height/2 } // 布局分析结果 interface LayoutAnalysisResult { direction: 'row' | 'column' | 'none'; confidence: number; // 0-1 置信度 gap: number; // 间距值 (px) isGapConsistent: boolean; // 间距是否一致 justifyContent: string; // CSS justify-content alignItems: string; // CSS align-items rows: ElementRect[][]; // 行分组结果 columns: ElementRect[][]; // 列分组结果 overlappingElements: ElementRect[]; // 重叠元素 } // 重叠检测结果 interface OverlapDetectionResult { flowElements: ElementRect[]; // 参与布局的元素 stackedElements: ElementRect[]; // 需要 absolute 的元素 stackedIndices: Set<number>; // 重叠元素索引集合 } // 背景检测结果 interface BackgroundDetectionResult { backgroundIndex: number; // 背景元素索引 (-1 表示无) contentIndices: number[]; // 内容元素索引 hasBackground: boolean; // 是否检测到背景 } ``` ### B.9 文件路径映射 | 功能模块 | 文件路径 | 行号范围 | | -------------- | ------------------------------------ | --------- | | 边界框提取 | `src/algorithms/layout/detector.ts` | 56-109 | | 重叠检测 (IoU) | `src/algorithms/layout/detector.ts` | 111-281 | | 背景元素检测 | `src/algorithms/layout/detector.ts` | 283-344 | | 行/列分组 | `src/algorithms/layout/detector.ts` | 346-422 | | 间距分析 | `src/algorithms/layout/detector.ts` | 442-535 | | 对齐检测 | `src/algorithms/layout/detector.ts` | 537-642 | | 布局方向检测 | `src/algorithms/layout/detector.ts` | 644-755 | | 布局树构建 | `src/algorithms/layout/detector.ts` | 847-982 | | 优化入口 | `src/algorithms/layout/optimizer.ts` | 36-53 | | 容器优化 | `src/algorithms/layout/optimizer.ts` | 89-356 | | CSS 生成 | `src/algorithms/layout/optimizer.ts` | 875-913 | | 位置转换 | `src/algorithms/layout/optimizer.ts` | 1591-1685 | --- _文档版本: 2.0_ _最后更新: 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