#
tokens: 39192/50000 4/73 files (page 3/6)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 6. Use http://codebase.md/1yhy/figma-context-mcp?page={x} to view the full context.

# Directory Structure

```
├── .editorconfig
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .lintstagedrc.json
├── .nvmrc
├── .prettierrc
├── CHANGELOG.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── Dockerfile
├── docs
│   ├── en
│   │   ├── absolute-to-relative-research.md
│   │   ├── architecture.md
│   │   ├── cache-architecture.md
│   │   ├── grid-layout-research.md
│   │   ├── icon-detection.md
│   │   ├── layout-detection-research.md
│   │   └── layout-detection.md
│   └── zh-CN
│       ├── absolute-to-relative-research.md
│       ├── architecture.md
│       ├── cache-architecture.md
│       ├── grid-layout-research.md
│       ├── icon-detection.md
│       ├── layout-detection-research.md
│       ├── layout-detection.md
│       └── TODO-feature-enhancements.md
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── README.zh-CN.md
├── scripts
│   ├── fetch-test-data.ts
│   └── optimize-figma-json.ts
├── smithery.yaml
├── src
│   ├── algorithms
│   │   ├── icon
│   │   │   ├── detector.ts
│   │   │   └── index.ts
│   │   └── layout
│   │       ├── detector.ts
│   │       ├── index.ts
│   │       ├── optimizer.ts
│   │       └── spatial.ts
│   ├── config.ts
│   ├── core
│   │   ├── effects.ts
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   └── style.ts
│   ├── index.ts
│   ├── prompts
│   │   ├── design-to-code.ts
│   │   └── index.ts
│   ├── resources
│   │   ├── figma-resources.ts
│   │   └── index.ts
│   ├── server.ts
│   ├── services
│   │   ├── cache
│   │   │   ├── cache-manager.ts
│   │   │   ├── disk-cache.ts
│   │   │   ├── index.ts
│   │   │   ├── lru-cache.ts
│   │   │   └── types.ts
│   │   ├── cache.ts
│   │   ├── figma.ts
│   │   └── simplify-node-response.ts
│   ├── types
│   │   ├── figma.ts
│   │   ├── index.ts
│   │   └── simplified.ts
│   └── utils
│       ├── color.ts
│       ├── css.ts
│       ├── file.ts
│       └── validation.ts
├── tests
│   ├── fixtures
│   │   ├── expected
│   │   │   ├── node-240-32163-optimized.json
│   │   │   ├── node-402-34955-optimized.json
│   │   │   └── real-node-data-optimized.json
│   │   └── figma-data
│   │       ├── node-240-32163.json
│   │       ├── node-402-34955.json
│   │       └── real-node-data.json
│   ├── integration
│   │   ├── __snapshots__
│   │   │   ├── layout-optimization.test.ts.snap
│   │   │   └── output-quality.test.ts.snap
│   │   ├── layout-optimization.test.ts
│   │   ├── output-quality.test.ts
│   │   └── parser.test.ts
│   ├── unit
│   │   ├── algorithms
│   │   │   ├── icon-optimization.test.ts
│   │   │   ├── icon.test.ts
│   │   │   └── layout.test.ts
│   │   ├── resources
│   │   │   └── figma-resources.test.ts
│   │   └── services
│   │       └── cache.test.ts
│   └── utils
│       ├── preview-generator.ts
│       ├── preview.ts
│       ├── run-simplification.ts
│       └── viewer.html
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/docs/en/layout-detection.md:
--------------------------------------------------------------------------------

```markdown
# Design-to-Code: Flex Layout Detection Algorithm

## Table of Contents

1. [Background](#1-background)
2. [Industry Solutions Analysis](#2-industry-solutions-analysis)
3. [Core Algorithm Principles](#3-core-algorithm-principles)
4. [Implementation Details](#4-implementation-details)
5. [Testing & Verification](#5-testing--verification)
6. [Best Practices](#6-best-practices)

---

## 1. Background

### 1.1 Problem Statement

Design files (like Figma) typically use **absolute positioning** (x, y, width, height), while frontend code requires **relative layouts** (Flexbox, Grid) for responsive design.

**Core Challenge**: How to accurately infer Flex layout structure from a flat list of absolutely positioned elements?

### 1.2 Key Difficulties

| Challenge            | Description                                                                |
| -------------------- | -------------------------------------------------------------------------- |
| Row/Column Detection | How to determine if elements are arranged horizontally or vertically?      |
| Nested Structure     | How to convert a flat list into a nested DOM tree?                         |
| Gap Calculation      | How to determine if gaps are consistent and should use the `gap` property? |
| Alignment Detection  | How to detect `justify-content` and `align-items`?                         |
| Overlap Handling     | How to handle overlapping elements that need absolute positioning?         |
| Tolerance Handling   | How to handle small offsets in design files?                               |

---

## 2. Industry Solutions Analysis

### 2.1 Major Tools Comparison

| Tool            | Developer    | Layout Detection                 | Open Source |
| --------------- | ------------ | -------------------------------- | ----------- |
| **FigmaToCode** | bernaferrari | Relies on Figma Auto Layout data | ✓           |
| **Grida**       | gridaco      | Rules + ML hybrid                | ✓           |
| **imgcook**     | Alibaba      | Rule system + Machine Learning   | ✗           |
| **Anima**       | Anima        | Constraint inference             | ✗           |

### 2.2 FigmaToCode Analysis

**GitHub**: https://github.com/bernaferrari/FigmaToCode

**Features**:

- No layout inference, directly maps Figma Auto Layout properties
- Uses AltNodes as intermediate representation
- Uses absolute positioning for non-Auto Layout designs

**Limitations**:

- Depends on designers correctly using Auto Layout
- Cannot handle legacy designs or manually positioned layouts

### 2.3 imgcook Layout Algorithm (Alibaba)

**Core Flow**:

```
Flattened JSON → Row/Column Grouping → Layout Inference → Semantics → Code Generation
```

**Key Technologies**:

1. **Page Segmentation**: Split page into different sub-modules
2. **Grouping Algorithm**: Determine element containment relationships
3. **Loop Detection**: Identify lists/repeated elements
4. **Multi-state Recognition**: Identify different states of the same component

---

## 3. Core Algorithm Principles

### 3.1 Y-Axis Overlap Detection (Row Grouping)

**Principle**: If two elements overlap on the Y-axis, they belong to the same row.

```
Element A: y=10, height=30  →  Y range [10, 40]
Element B: y=20, height=30  →  Y range [20, 50]

[10, 40] and [20, 50] intersect → Same row
```

**Implementation**:

```typescript
function isOverlappingY(a: ElementRect, b: ElementRect, tolerance = 0): boolean {
  return !(a.bottom + tolerance < b.y || b.bottom + tolerance < a.y);
}
```

### 3.2 X-Axis Overlap Detection (Column Grouping)

**Principle**: If two elements overlap on the X-axis, they belong to the same column.

```typescript
function isOverlappingX(a: ElementRect, b: ElementRect, tolerance = 0): boolean {
  return !(a.right + tolerance < b.x || b.right + tolerance < a.x);
}
```

### 3.3 Gap Consistency Analysis

**Principle**: Calculate the standard deviation of all gaps to determine consistency.

```typescript
function analyzeGaps(gaps: number[], tolerancePercent = 20) {
  const average = gaps.reduce((a, b) => a + b, 0) / gaps.length;
  const variance = gaps.reduce((sum, g) => sum + Math.pow(g - average, 2), 0) / gaps.length;
  const stdDev = Math.sqrt(variance);

  return {
    isConsistent: stdDev <= average * (tolerancePercent / 100),
    average,
    rounded: Math.round(average / 4) * 4, // Round to 4px grid
    stdDev,
  };
}
```

### 3.4 Alignment Detection

**Horizontal Alignment**:

- Left aligned: All elements have the same `left` value
- Center aligned: All elements have the same center X coordinate
- Right aligned: All elements have the same `right` value

**Vertical Alignment**:

- Top aligned: All elements have the same `top` value
- Center aligned: All elements have the same center Y coordinate
- Bottom aligned: All elements have the same `bottom` value

---

## 4. Implementation Details

### 4.1 Layout Detection Flow

```
1. Extract bounding boxes from child elements
2. Group elements into rows (Y-axis overlap)
3. If single row → check column grouping (X-axis overlap)
4. Determine layout direction (row vs column)
5. Calculate gaps and check consistency
6. Detect alignment (justify-content, align-items)
7. Handle overlapping elements (mark for absolute positioning)
```

### 4.2 Confidence Scoring

The algorithm calculates a confidence score based on:

- Number of elements that fit the detected pattern
- Gap consistency
- Alignment accuracy

---

## 5. Testing & Verification

### 5.1 Test Results

| Test Case                  | Status | Result                             |
| -------------------------- | ------ | ---------------------------------- |
| Bounding box extraction    | ✓      | Extracted 2 child elements         |
| Row grouping algorithm     | ✓      | Grouped into 1 row                 |
| Column grouping algorithm  | ✓      | Grouped into 1 column              |
| Consistent gap detection   | ✓      | Avg: 16px, StdDev: 0.63            |
| Inconsistent gap detection | ✓      | Avg: 25px, StdDev: 14.14           |
| Left alignment detection   | ✓      | Detected: left                     |
| Center alignment detection | ✓      | Detected: center                   |
| Layout tree building       | ✓      | Type: container, Direction: column |

**Overall: 8/9 tests passed**

---

## 6. Best Practices

### 6.1 Recommended Parameters

| Parameter           | Recommended Value | Description                  |
| ------------------- | ----------------- | ---------------------------- |
| Y-axis tolerance    | 2px               | For row grouping             |
| Gap tolerance       | 20%               | Standard deviation threshold |
| Alignment tolerance | 2px               | For alignment detection      |
| Max recursion depth | 5                 | For nested layouts           |

### 6.2 When to Use Absolute Positioning

- Elements with significant overlap (IoU > 0.1)
- Complex icon compositions
- Decorative elements with specific positioning

---

## 7. Complete Implementation Analysis

This section provides an in-depth analysis of the layout detection algorithm implementation, including the complete call chain and core functions.

### 7.1 System Architecture Overview

```
┌─────────────────────────────────────────────────────────────────────────┐
│                        Layout Optimization System                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │                    optimizer.ts (Orchestration Layer)             │   │
│  │  ┌─────────────────────────────────────────────────────────────┐ │   │
│  │  │  optimizeDesign()  →  optimizeNodeTree()  →  optimizeContainer() │ │
│  │  └─────────────────────────────────────────────────────────────┘ │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                    │                                     │
│                    ┌───────────────┼───────────────┐                    │
│                    ▼               ▼               ▼                    │
│  ┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐   │
│  │  detector.ts        │ │  detector.ts    │ │  optimizer.ts       │   │
│  │  (Overlap Detection)│ │  (Layout Detect)│ │  (Style Generation) │   │
│  │                     │ │                 │ │                     │   │
│  │ detectOverlapping() │ │ analyzeLayout() │ │ generateGridCSS()   │   │
│  │ detectBackground()  │ │ detectGrid()    │ │ convertToRelative() │   │
│  └─────────────────────┘ └─────────────────┘ └─────────────────────┘   │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

### 7.2 Core Entry Function Call Chain

```
src/algorithms/layout/optimizer.ts
═══════════════════════════════════════════════════════════════════════════

                          ┌─────────────────────────┐
                          │  LayoutOptimizer.       │
                          │  optimizeDesign(design) │
                          │  [optimizer.ts:36]      │
                          └───────────┬─────────────┘
                                      │
                    Reset containerIdCounter
                                      │
                                      ▼
                    ┌─────────────────────────────────┐
                    │  design.nodes.map((node) =>    │
                    │    optimizeNodeTree(node)      │
                    │  )                             │
                    │  [optimizer.ts:46]             │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼ (Recursively process each node)
                    ┌─────────────────────────────────┐
                    │  optimizeNodeTree(node)         │
                    │  [optimizer.ts:61]              │
                    │                                 │
                    │  Recursively call itself for    │
                    │  each child, then call          │
                    │  optimizeContainer()            │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
                    ┌─────────────────────────────────┐
                    │  optimizeContainer(node)        │
                    │  [optimizer.ts:89]              │
                    │                                 │
                    │  Core optimization logic (7.3)  │
                    └─────────────────────────────────┘
```

### 7.3 optimizeContainer Four-Step Algorithm

```
optimizeContainer(node) - [optimizer.ts:89-356]
═══════════════════════════════════════════════════════════════════════════

Input: SimplifiedNode (container node with children)
Output: Optimized SimplifiedNode

┌─────────────────────────────────────────────────────────────────────────┐
│ Pre-checks                                                               │
│ • children.length <= 1 → return immediately                             │
│ • Check if FRAME/GROUP container                                        │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ STEP 1: Overlap Detection                                               │
│ [optimizer.ts:98-109]                                                   │
│                                                                          │
│   elementRects = nodesToElementRects(children)                          │
│                         │                                                │
│                         ▼                                                │
│   overlapResult = detectOverlappingElements(elementRects, 0.1)          │
│                         │                                                │
│         ┌───────────────┴───────────────┐                               │
│         │                               │                               │
│         ▼                               ▼                               │
│   flowElements[]              stackedElements[]                         │
│   (participate in layout)     (keep absolute)                           │
│                                                                          │
│   If flowElements.length < 2 → skip layout optimization                 │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ STEP 1.5: Background Element Detection                                  │
│ [optimizer.ts:111-151]                                                  │
│                                                                          │
│   detectBackgroundElement(elementRects, parentWidth, parentHeight)      │
│                         │                                                │
│           ┌─────────────┴─────────────┐                                 │
│           │ hasBackground: true       │                                 │
│           │ backgroundIndex >= 0      │                                 │
│           └─────────────┬─────────────┘                                 │
│                         │                                                │
│       isBackgroundElement() ──► extractBackgroundStyles()               │
│                         │             │                                  │
│                         │             ▼                                  │
│                         │   mergedBackgroundStyles = {                  │
│                         │     backgroundColor, borderRadius,            │
│                         │     border, boxShadow...                      │
│                         │   }                                           │
│                         │                                                │
│                         ▼                                                │
│   filteredChildren = children.filter(non-background)                    │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ STEP 2: Grid Detection (Priority over Flex)                            │
│ [optimizer.ts:165-206]                                                  │
│                                                                          │
│   if (isContainer) {                                                    │
│       gridDetection = detectGridIfApplicable(filteredChildren)          │
│                         │                                                │
│       if (gridDetection) {                                              │
│           ┌─────────────┴─────────────┐                                 │
│           │ gridResult                 │                                 │
│           │ gridIndices (Grid elements)│                                 │
│           └─────────────┬─────────────┘                                 │
│                         │                                                │
│           gridStyles = generateGridCSS(gridResult)                      │
│                         │                                                │
│           convertAbsoluteToRelative(..., gridIndices)                   │
│                         │                                                │
│           return Grid layout node ──────────────────────► [END]         │
│       }                                                                  │
│   }                                                                      │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼ (Grid not detected)
┌─────────────────────────────────────────────────────────────────────────┐
│ STEP 3: Flex Detection (Fallback)                                       │
│ [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,  // only set for column                  │
│     gap: `${gap}px`,           // only when consistent                 │
│     justifyContent,                                                      │
│     alignItems                                                           │
│   }                                                                      │
│                         │                                                │
│   convertAbsoluteToRelative() → padding + clean child styles            │
│                         │                                                │
│   return Flex layout node                                               │
└─────────────────────────────────────────────────────────────────────────┘
```

### 7.4 Flex Layout Detection Flow

```
analyzeLayoutDirection(nodes) - [optimizer.ts:361-438]
═══════════════════════════════════════════════════════════════════════════

Input: SimplifiedNode[] (child nodes array)
Output: { isRow, isColumn, rowGap, columnGap, isGapConsistent,
          justifyContent, alignItems }

                    ┌─────────────────────────────────┐
                    │  Extract position info          │
                    │  rects = nodes.map(node => {    │
                    │    left, top, width, height     │
                    │  })                             │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
                    ┌─────────────────────────────────┐
                    │  analyzeAlignment(rects)        │
                    │  [optimizer.ts:443-489]         │
                    │                                 │
                    │  Analyze horizontal/vertical    │
                    │  alignment:                     │
                    │  • leftAligned                  │
                    │  • rightAligned                 │
                    │  • centerHAligned               │
                    │  • topAligned                   │
                    │  • bottomAligned                │
                    │  • centerVAligned               │
                    └───────────────┬─────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    ▼                               ▼
     ┌─────────────────────────┐     ┌─────────────────────────┐
     │  calculateRowScore()    │     │  calculateColumnScore()  │
     │  [optimizer.ts:551-579] │     │  [optimizer.ts:584-612]  │
     │                         │     │                          │
     │  Row layout scoring:    │     │  Column layout scoring:  │
     │                         │     │                          │
     │  1. Sort by left        │     │  1. Sort by top          │
     │                         │     │                          │
     │  2. Calculate h-gaps    │     │  2. Calculate v-gaps     │
     │     gap = next.left -   │     │     gap = next.top -     │
     │           current.right │     │           current.bottom │
     │                         │     │                          │
     │  3. Count consecutive   │     │  3. Count consecutive    │
     │     positive gaps       │     │     positive gaps        │
     │     (0 <= gap <= 50px)  │     │     (0 <= gap <= 50px)   │
     │                         │     │                          │
     │  4. Score calculation:  │     │  4. Score calculation:   │
     │     distribution * 0.7  │     │     distribution * 0.7   │
     │     + alignment * 0.3   │     │     + alignment * 0.3    │
     └───────────────┬─────────┘     └───────────────┬──────────┘
                     │                               │
                     └───────────────┬───────────────┘
                                     │
                                     ▼
                    ┌─────────────────────────────────┐
                    │  Layout direction decision      │
                    │                                 │
                    │  if (rowScore > columnScore     │
                    │      && rowScore > 0.4)         │
                    │      → isRow = true             │
                    │                                 │
                    │  if (columnScore > rowScore     │
                    │      && columnScore > 0.4)      │
                    │      → isColumn = true          │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
                    ┌─────────────────────────────────┐
                    │  CSS property mapping           │
                    │                                 │
                    │  Row layout:                    │
                    │    justify = horizontal align  │
                    │    align   = vertical align    │
                    │                                 │
                    │  Column layout:                 │
                    │    justify = vertical align    │
                    │    align   = horizontal align  │
                    └─────────────────────────────────┘
```

### 7.5 Overlap Detection Algorithm (IoU)

```
detectOverlappingElements(rects, iouThreshold=0.1) - [detector.ts:246-281]
═══════════════════════════════════════════════════════════════════════════

IoU (Intersection over Union) Calculation:

          ┌──────────────────────────────┐
          │         Element A            │
          │    ┌──────────────────────┐  │
          │    │     Intersection     │  │
          └────┼──────────────────────┼──┘
               │     Element B        │
               └──────────────────────┘

  IoU = Intersection Area / Union Area

  where:
    Intersection = overlap_width × overlap_height
    Union = Area_A + Area_B - Intersection


Algorithm Flow:
┌────────────────────────────────────────────────────────────────┐
│  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)                        │
│                                                                 │
│  Result:                                                        │
│    flowElements     = elements NOT in stackedIndices           │
│    stackedElements  = elements in stackedIndices               │
└────────────────────────────────────────────────────────────────┘

IoU Threshold Classification:
┌────────────┬───────────────────────────────────────────────────┐
│ IoU Range  │ Classification                                    │
├────────────┼───────────────────────────────────────────────────┤
│ = 0        │ none/adjacent (no overlap or adjacent)            │
│ 0 < x < 0.1│ partial (slight overlap - can participate)        │
│ 0.1 ≤ x < 0.5│ significant (needs absolute positioning)       │
│ ≥ 0.5      │ contained (severe overlap/containment)            │
└────────────┴───────────────────────────────────────────────────┘
```

### 7.6 Gap Analysis Algorithm

```
calculateGaps() + analyzeGaps() - [detector.ts:447-509]
═══════════════════════════════════════════════════════════════════════════

Gap Calculation Example (horizontal direction):

     ┌────┐   gap1   ┌────┐   gap2   ┌────┐
     │ A  │ ──────── │ B  │ ──────── │ C  │
     └────┘          └────┘          └────┘
      right          left  right     left

  gap1 = B.x - A.right
  gap2 = C.x - B.right


Gap Consistency Analysis:
┌────────────────────────────────────────────────────────────────┐
│  analyzeGaps(gaps, tolerancePercent=20)                        │
│                                                                 │
│  1. Calculate average:                                          │
│     average = sum(gaps) / gaps.length                          │
│                                                                 │
│  2. Calculate standard deviation:                               │
│     variance = Σ(gap - average)² / n                           │
│     stdDev = √variance                                         │
│                                                                 │
│  3. Consistency check:                                          │
│     tolerance = average × 20%                                  │
│     isConsistent = (stdDev <= tolerance)                       │
│                                                                 │
│  4. Round to common values:                                     │
│     COMMON_VALUES = [0,2,4,6,8,10,12,16,20,24,32,40,48,64...]  │
│     rounded = findClosest(average, COMMON_VALUES)              │
└────────────────────────────────────────────────────────────────┘

Example:
┌──────────────────────────────────────────────────────────────┐
│  gaps = [16, 15, 17, 16]                                     │
│  average = 16                                                 │
│  stdDev = 0.71                                               │
│  tolerance = 16 × 0.2 = 3.2                                  │
│  isConsistent = (0.71 <= 3.2) = true ✓                       │
│  rounded = 16px                                              │
└──────────────────────────────────────────────────────────────┘
```

### 7.7 Absolute to Relative Position Conversion

```
convertAbsoluteToRelative() - [optimizer.ts:1591-1685]
═══════════════════════════════════════════════════════════════════════════

Original Layout (Absolute Positioning):
┌────────────────────────────────────────────────┐
│ Parent Container                               │
│  ┌──────────────────────────────────────────┐  │
│  │ ← paddingLeft                             │  │
│  │    ┌────────┐   gap    ┌────────┐        │  │
│  │    │ Child1 │ ──────── │ Child2 │        │  │
│  │    │left:20 │          │left:140│        │  │
│  │    │top:10  │          │top:10  │        │  │
│  │    └────────┘          └────────┘        │  │
│  │                                 paddingRight→│
│  └──────────────────────────────────────────┘  │
│                              ↓ paddingBottom   │
└────────────────────────────────────────────────┘

After Conversion (Flex Layout):
┌────────────────────────────────────────────────┐
│ Parent Container                               │
│ display: flex;                                 │
│ padding: 10px 30px 20px 20px;                 │
│ gap: 20px;                                     │
│  ┌──────────────────────────────────────────┐  │
│  │    ┌────────┐   ←gap→  ┌────────┐        │  │
│  │    │ Child1 │          │ Child2 │        │  │
│  │    │(no left)│          │(no left)│        │  │
│  │    │(no top) │          │(no top) │        │  │
│  │    └────────┘          └────────┘        │  │
│  └──────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘


Conversion Steps:
┌────────────────────────────────────────────────────────────────┐
│  1. collectFlowChildOffsets()                                  │
│     Collect position info for all flow children               │
│                                                                 │
│  2. inferContainerPadding()                                    │
│     Infer container padding from child positions:              │
│     • paddingLeft   = min(children.left)                       │
│     • paddingTop    = min(children.top)                        │
│     • paddingRight  = parentWidth - max(children.right)        │
│     • paddingBottom = parentHeight - max(children.bottom)      │
│                                                                 │
│  3. calculateChildMargins()                                    │
│     Calculate individual child margin adjustments              │
│     (cross-axis offset)                                        │
│                                                                 │
│  4. generatePaddingCSS()                                       │
│     Generate CSS padding shorthand:                            │
│     • All same: "10px"                                         │
│     • Top/bottom same, left/right same: "10px 20px"            │
│     • All different: "10px 20px 30px 40px"                     │
│                                                                 │
│  5. cleanChildStylesForLayout()                                │
│     Clean child element styles:                                │
│     • Remove position: absolute                                │
│     • Remove left, top                                         │
│     • Keep width, height                                       │
└────────────────────────────────────────────────────────────────┘
```

### 7.8 Core Data Structures

```typescript
// Core data structures in detector.ts
═══════════════════════════════════════════════════════════════════════════

// Basic bounding box
interface BoundingBox {
  x: number;      // left offset
  y: number;      // top offset
  width: number;  // width
  height: number; // height
}

// Extended element rect (with computed properties)
interface ElementRect extends BoundingBox {
  index: number;   // index in original array
  right: number;   // x + width
  bottom: number;  // y + height
  centerX: number; // x + width/2
  centerY: number; // y + height/2
}

// Layout analysis result
interface LayoutAnalysisResult {
  direction: 'row' | 'column' | 'none';
  confidence: number;           // 0-1 confidence score
  gap: number;                  // gap value (px)
  isGapConsistent: boolean;     // is gap consistent
  justifyContent: string;       // CSS justify-content
  alignItems: string;           // CSS align-items
  rows: ElementRect[][];        // row grouping result
  columns: ElementRect[][];     // column grouping result
  overlappingElements: ElementRect[]; // overlapping elements
}

// Overlap detection result
interface OverlapDetectionResult {
  flowElements: ElementRect[];     // elements participating in layout
  stackedElements: ElementRect[];  // elements needing absolute
  stackedIndices: Set<number>;     // overlapping element indices
}

// Background detection result
interface BackgroundDetectionResult {
  backgroundIndex: number;    // background element index (-1 if none)
  contentIndices: number[];   // content element indices
  hasBackground: boolean;     // whether background detected
}
```

### 7.9 File Path Mapping

| Module                     | File Path                            | Line Range |
| -------------------------- | ------------------------------------ | ---------- |
| Bounding box extraction    | `src/algorithms/layout/detector.ts`  | 56-109     |
| Overlap detection (IoU)    | `src/algorithms/layout/detector.ts`  | 111-281    |
| Background detection       | `src/algorithms/layout/detector.ts`  | 283-344    |
| Row/column grouping        | `src/algorithms/layout/detector.ts`  | 346-422    |
| Gap analysis               | `src/algorithms/layout/detector.ts`  | 442-535    |
| Alignment detection        | `src/algorithms/layout/detector.ts`  | 537-642    |
| Layout direction detection | `src/algorithms/layout/detector.ts`  | 644-755    |
| Layout tree building       | `src/algorithms/layout/detector.ts`  | 847-982    |
| Optimization entry         | `src/algorithms/layout/optimizer.ts` | 36-53      |
| Container optimization     | `src/algorithms/layout/optimizer.ts` | 89-356     |
| CSS generation             | `src/algorithms/layout/optimizer.ts` | 875-913    |
| Position conversion        | `src/algorithms/layout/optimizer.ts` | 1591-1685  |

---

_Document version: 2.0_
_Last updated: 2025-12-06_

_For the Chinese version of this document, see [docs/zh-CN/layout-detection.md](../zh-CN/layout-detection.md)_

```

--------------------------------------------------------------------------------
/docs/en/icon-detection.md:
--------------------------------------------------------------------------------

```markdown
# Icon Layer Merge Algorithm

## Overview

This document describes the algorithm for detecting and merging icon layers in Figma designs. The goal is to identify groups of vector elements that should be exported as a single icon image rather than individual elements.

## Problem Statement

In Figma designs, icons are often composed of multiple vector layers:

- A magnifying glass icon might have a circle and a line
- A settings icon might have multiple gear shapes
- Complex icons might have dozens of individual elements

Exporting each layer separately would result in fragmented assets that are difficult to use.

## Algorithm Design

### 1. Detection Criteria

An element group is considered an icon if:

| Criterion         | Threshold  | Rationale                            |
| ----------------- | ---------- | ------------------------------------ |
| Maximum size      | 300×300 px | Icons are typically small            |
| Minimum size      | 8×8 px     | Avoid detecting tiny decorations     |
| Mergeable ratio   | 80%        | Most children should be vector types |
| Max nesting depth | 5 levels   | Avoid complex UI components          |

### 2. Mergeable Node Types

The following node types are considered mergeable:

- `VECTOR`
- `ELLIPSE`
- `RECTANGLE`
- `STAR`
- `POLYGON`
- `LINE`
- `BOOLEAN_OPERATION`

### 3. Export Format Selection

| Condition                       | Format       | Reason                       |
| ------------------------------- | ------------ | ---------------------------- |
| All vector elements             | SVG          | Best quality, smallest size  |
| Contains effects (blur, shadow) | PNG          | Effects not supported in SVG |
| Designer specified format       | As specified | Respect design intent        |

### 4. Algorithm Flow

```
1. Traverse node tree depth-first
2. For each GROUP/FRAME node:
   a. Check size constraints
   b. Count mergeable vs non-mergeable children
   c. Calculate mergeable ratio
   d. If ratio >= threshold, mark as icon
3. Determine export format
4. Skip processing children of icon nodes
```

## Implementation

### Core Detection Function

```typescript
function shouldMergeAsIcon(node: FigmaNode, config: IconConfig): IconResult {
  // Size check
  if (node.width > config.maxSize || node.height > config.maxSize) {
    return { shouldMerge: false, reason: "Size too large" };
  }

  // Check for text nodes (exclude UI components)
  if (containsTextNode(node)) {
    return { shouldMerge: false, reason: "Contains TEXT elements" };
  }

  // Calculate mergeable ratio
  const ratio = countMergeableChildren(node) / node.children.length;
  if (ratio < config.mergeableRatio) {
    return { shouldMerge: false, reason: "Low mergeable ratio" };
  }

  return {
    shouldMerge: true,
    format: hasComplexEffects(node) ? "PNG" : "SVG",
    reason: "All criteria met",
  };
}
```

## Test Results

### Test Cases

| Test Case                        | Expected                     | Result |
| -------------------------------- | ---------------------------- | ------ |
| Icon container (designer marked) | ✓ Export as PNG              | ✓ PASS |
| Core icon group                  | ✓ Export as whole            | ✓ PASS |
| Magnifying glass icon            | ✓ Small icon group           | ✓ PASS |
| Exclamation icon                 | ✓ Export as icon             | ✓ PASS |
| Star icon in AI button           | ✓ Export as icon             | ✓ PASS |
| Text node                        | ✗ Should not merge           | ✓ PASS |
| Root node (too large)            | ✗ Should not export          | ✓ PASS |
| Group with TEXT                  | ✗ Should not export as image | ✓ PASS |
| Background rectangle             | ✗ Too large                  | ✓ PASS |

**Result: 9/9 tests passed**

### Optimization Results

- **Before optimization**: ~45 potential exports (fragmented)
- **After optimization**: 2 exports (merged icons)
- **Reduction**: 96%

## Configuration

### Default Configuration

```typescript
const DEFAULT_CONFIG: IconDetectionConfig = {
  maxIconSize: 300,
  minIconSize: 8,
  mergeableRatio: 0.8,
  maxDepth: 5,
  maxChildren: 50,
  respectExportSettingsMaxSize: 500,
};
```

### Tuning Guidelines

| Scenario         | Adjustment                            |
| ---------------- | ------------------------------------- |
| Large icons      | Increase `maxIconSize`                |
| Strict detection | Increase `mergeableRatio`             |
| Complex icons    | Increase `maxDepth` and `maxChildren` |

---

## Complete Implementation Analysis

### Architecture Overview

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Icon Detection Algorithm                             │
│                      src/algorithms/icon/detector.ts                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────┐                                                         │
│  │ Figma Node Tree │                                                         │
│  └────────┬────────┘                                                         │
│           │                                                                  │
│           ▼                                                                  │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                      analyzeNodeTree() [Entry Point]                    │ │
│  │  Orchestrates the detection flow, returns results and summary           │ │
│  └────────────────────────────────────┬───────────────────────────────────┘ │
│                                       │                                      │
│           ┌───────────────────────────┴───────────────────────────┐          │
│           ▼                                                       ▼          │
│  ┌─────────────────────┐                              ┌────────────────────┐ │
│  │  processNodeTree()  │                              │collectExportable   │ │
│  │  Bottom-up recursive│─────── After processing ────▶│    Icons()         │ │
│  │  Mark _iconDetection│                              │ Collect exportable │ │
│  └──────────┬──────────┘                              │     icon list      │ │
│             │                                         └────────────────────┘ │
│             │ For each node                                                  │
│             ▼                                                                │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                         detectIcon() [Core Detection]                   │ │
│  │                                                                         │ │
│  │   ┌─────────────┐    ┌─────────────┐    ┌─────────────────────────┐    │ │
│  │   │ 9-Step Rules │───▶│collectNode  │───▶│ IconDetectionResult     │    │ │
│  │   │ (see below)  │    │  Stats()    │    │ {shouldMerge, format}   │    │ │
│  │   └─────────────┘    │ O(n) single │    └─────────────────────────┘    │ │
│  │                      │   pass      │                                    │ │
│  │                      └─────────────┘                                    │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

### detectIcon() 9-Step Detection Flow

```
                              ┌─────────────────────┐
                              │     detectIcon()     │
                              │   Input: FigmaNode   │
                              └──────────┬──────────┘
                                         │
                    ┌────────────────────┴────────────────────┐
                    ▼                                         │
        ┌───────────────────────┐                             │
 Step 1 │ exportSettings Check  │                             │
        │ Designer marked export?│                             │
        └───────────┬───────────┘                             │
                    │                                         │
          ┌────────YES────────┐                               │
          ▼                   │                               │
  ┌───────────────────┐      │                               │
  │ Size ≤ 400px?     │      │                               │
  │ No TEXT?          │      │                               │
  └─────────┬─────────┘      │                               │
            │                 │                               │
     YES────┴────NO           │                               │
      │          │            │                               │
      ▼          │            │                               │
 ┌────────┐      │            │                               │
 │✅ Merge │      └────────────┼───────────────┐               │
 │Use      │                   │               │               │
 │designer │                   │               │               │
 │settings │                   │               │               │
 └────────┘                   │               │               │
                              ▼               ▼               │
                    ┌───────────────────────────────┐         │
             Step 2 │ Container or mergeable type?  │         │
                    │ CONTAINER: GROUP/FRAME/...    │         │
                    │ MERGEABLE: VECTOR/ELLIPSE/... │         │
                    └───────────────┬───────────────┘         │
                                    │                         │
                         ┌─────────NO─────────┐               │
                         ▼                    │               │
                    ┌─────────┐               │               │
                    │❌ Skip   │               │               │
                    │Non-target│               │               │
                    │type      │               │               │
                    └─────────┘               │               │
                                              ▼               │
                              ┌─────────────────────────┐     │
                              │ Single element (VECTOR)?│     │
                              └───────────┬─────────────┘     │
                                          │                   │
                               YES────────┴────────NO         │
                                │                  │          │
                ┌───────────────┘                  │          │
                ▼                                  │          │
    ┌─────────────────────┐                        │          │
    │ Exclude RECTANGLE   │                        │          │
    │ (usually background)│                        │          │
    │ Check size ≤ 300px  │                        │          │
    └──────────┬──────────┘                        │          │
               │                                   │          │
        PASS───┴───FAIL                            │          │
         │         │                               │          │
         ▼         ▼                               │          │
    ┌────────┐ ┌─────────┐                         │          │
    │✅ Merge │ │❌ Skip   │                         │          │
    │Single  │ │Background│                         │          │
    │vector  │ │/too large│                         │          │
    └────────┘ └─────────┘                         │          │
                                                   │          │
                         ┌─────────────────────────┘          │
                         ▼                                    │
               ┌───────────────────────────┐                  │
        Step 3 │ Size Check (containers)   │                  │
               │ 8px ≤ size ≤ 300px        │                  │
               └─────────────┬─────────────┘                  │
                             │                                │
                   PASS──────┴──────FAIL                      │
                    │                 │                       │
                    │                 ▼                       │
                    │          ┌─────────────┐                │
                    │          │ ❌ Skip      │                │
                    │          │ Too large/  │                │
                    │          │ small       │                │
                    │          └─────────────┘                │
                    ▼                                         │
    ╔═══════════════════════════════════════════════════════╗ │
    ║           collectNodeStats() Single-Pass              ║ │
    ║     ┌────────────────────────────────────────────┐    ║ │
    ║     │  O(n) complexity collects 7 statistics:    │    ║ │
    ║     │  • depth          - Tree depth             │    ║ │
    ║     │  • totalChildren  - Total descendants      │    ║ │
    ║     │  • hasExcludeType - Contains TEXT etc.     │    ║ │
    ║     │  • hasImageFill   - Contains image fills   │    ║ │
    ║     │  • hasComplexEffects - Contains effects    │    ║ │
    ║     │  • allLeavesMergeable - All leaves OK      │    ║ │
    ║     │  • mergeableRatio - Mergeable type ratio   │    ║ │
    ║     └────────────────────────────────────────────┘    ║ │
    ╚═══════════════════════════════╤═══════════════════════╝ │
                                    │                         │
                                    ▼                         │
               ┌───────────────────────────────────┐          │
        Step 4 │ Exclude Type Check                │          │
               │ hasExcludeType? (TEXT/COMPONENT)  │          │
               └─────────────────┬─────────────────┘          │
                                 │                            │
                       YES───────┴───────NO                   │
                        │                 │                   │
                        ▼                 │                   │
                   ┌─────────┐            │                   │
                   │❌ Skip   │            │                   │
                   │Has text │            │                   │
                   └─────────┘            │                   │
                                          ▼                   │
               ┌───────────────────────────────────┐          │
        Step 5 │ Depth Check                       │          │
               │ depth ≤ 5?                        │          │
               └─────────────────┬─────────────────┘          │
                                 │                            │
                       NO────────┴────────YES                 │
                        │                  │                  │
                        ▼                  │                  │
                   ┌─────────┐             │                  │
                   │❌ Skip   │             │                  │
                   │Too deep │             │                  │
                   └─────────┘             │                  │
                                           ▼                  │
               ┌───────────────────────────────────┐          │
        Step 6 │ Child Count Check                 │          │
               │ totalChildren ≤ 100?              │          │
               └─────────────────┬─────────────────┘          │
                                 │                            │
                       NO────────┴────────YES                 │
                        │                  │                  │
                        ▼                  │                  │
                   ┌─────────┐             │                  │
                   │❌ Skip   │             │                  │
                   │Too many │             │                  │
                   │children │             │                  │
                   └─────────┘             │                  │
                                           ▼                  │
               ┌───────────────────────────────────┐          │
        Step 7 │ Mergeable Ratio Check             │          │
               │ mergeableRatio ≥ 60%?             │          │
               └─────────────────┬─────────────────┘          │
                                 │                            │
                       NO────────┴────────YES                 │
                        │                  │                  │
                        ▼                  │                  │
                   ┌─────────┐             │                  │
                   │❌ Skip   │             │                  │
                   │Ratio low│             │                  │
                   └─────────┘             │                  │
                                           ▼                  │
               ┌───────────────────────────────────┐          │
        Step 8 │ Leaf Mergeability Check           │          │
               │ allLeavesMergeable?               │          │
               └─────────────────┬─────────────────┘          │
                                 │                            │
                       NO────────┴────────YES                 │
                        │                  │                  │
                        ▼                  │                  │
                   ┌─────────┐             │                  │
                   │❌ Skip   │             │                  │
                   │Non-     │             │                  │
                   │mergeable│             │                  │
                   │leaves   │             │                  │
                   └─────────┘             │                  │
                                           ▼                  │
               ┌───────────────────────────────────┐          │
        Step 9 │ Export Format Decision            │          │
               │ Based on hasImageFill/hasComplex  │          │
               └─────────────────┬─────────────────┘          │
                                 │                            │
            ┌────────────────────┼────────────────────┐       │
            ▼                    ▼                    ▼       │
    ┌───────────────┐   ┌───────────────┐   ┌───────────────┐│
    │ hasImageFill  │   │hasComplex     │   │ Pure vector   ││
    │    = true     │   │Effects = true │   │               ││
    └───────┬───────┘   └───────┬───────┘   └───────┬───────┘│
            │                   │                   │        │
            ▼                   ▼                   ▼        │
    ┌───────────────┐   ┌───────────────┐   ┌───────────────┐│
    │ ✅ shouldMerge │   │ ✅ shouldMerge │   │ ✅ shouldMerge ││
    │ format: PNG   │   │ format: PNG   │   │ format: SVG   ││
    └───────────────┘   └───────────────┘   └───────────────┘│
                                                              │
└─────────────────────────────────────────────────────────────┘
```

### Bottom-Up Processing Flow

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                    processNodeTree() Bottom-Up Processing                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Example Input:                                                              │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ Frame "Card"                                                          │   │
│  │ ├── Frame "Header"                                                    │   │
│  │ │   ├── Group "Logo"          ◄─── Potential icon                    │   │
│  │ │   │   ├── Vector (path1)                                            │   │
│  │ │   │   └── Ellipse (circle)                                          │   │
│  │ │   └── Text "Title"                                                  │   │
│  │ └── Frame "Content"                                                   │   │
│  │     └── Group "Icon-Set"                                              │   │
│  │         ├── Group "Search"    ◄─── Potential icon                    │   │
│  │         │   ├── Ellipse                                               │   │
│  │         │   └── Line                                                  │   │
│  │         └── Group "Menu"      ◄─── Potential icon                    │   │
│  │             ├── Line                                                  │   │
│  │             ├── Line                                                  │   │
│  │             └── Line                                                  │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  Processing Order (bottom-up):                                               │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ Round 1: Process deepest leaf nodes                                  │    │
│  │ Vector, Ellipse, Text, Line... → All leaves, no _iconDetection       │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                              │                                              │
│                              ▼                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ Round 2: Process first-level containers                              │    │
│  │                                                                      │    │
│  │  Group "Logo"    → detectIcon() → ✅ shouldMerge (pure vector)        │    │
│  │  Group "Search"  → detectIcon() → ✅ shouldMerge (pure vector)        │    │
│  │  Group "Menu"    → detectIcon() → ✅ shouldMerge (pure vector)        │    │
│  │                                                                      │    │
│  │  ⚠️  After marking, child _iconDetection is cleared (will be merged)  │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                              │                                              │
│                              ▼                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ Round 3: Process upper containers                                    │    │
│  │                                                                      │    │
│  │  Frame "Header"                                                      │    │
│  │    → Children include Logo(icon) + Text                              │    │
│  │    → Not all children are icons                                      │    │
│  │    → detectIcon() → ❌ Contains TEXT                                  │    │
│  │    → Logo keeps _iconDetection marker                                │    │
│  │                                                                      │    │
│  │  Group "Icon-Set"                                                    │    │
│  │    → All children (Search, Menu) already marked as icons             │    │
│  │    → allChildrenAreIcons = true                                      │    │
│  │    → detectIcon() → Check if can promote merge                       │    │
│  │    → If size/depth allows → ✅ Merge as single icon                   │    │
│  │    → Clear Search, Menu _iconDetection                               │    │
│  │    OR                                                                │    │
│  │    → If criteria not met → Keep individual _iconDetection            │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                              │                                              │
│                              ▼                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ Final: Collect exportable icons                                      │    │
│  │                                                                      │    │
│  │  collectExportableIcons() traverses processed tree:                  │    │
│  │                                                                      │    │
│  │  - Node with _iconDetection → Add to export list                     │    │
│  │  - Don't recurse into marked nodes (children will be merged)         │    │
│  │  - Continue traversing unmarked nodes' children                      │    │
│  │                                                                      │    │
│  │  Output: [Logo, Search, Menu] or [Logo, Icon-Set]                    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

### collectNodeStats() Single-Pass Optimization

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                   collectNodeStats() Performance Optimization                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Before: 6 separate recursive functions, O(6n) complexity                    │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                                                                        │  │
│  │   calculateDepth()          ─────▶ Traverse tree ────▶ depth          │  │
│  │   countTotalChildren()      ─────▶ Traverse tree ────▶ count          │  │
│  │   hasExcludeTypeInTree()    ─────▶ Traverse tree ────▶ boolean        │  │
│  │   hasImageFillInTree()      ─────▶ Traverse tree ────▶ boolean        │  │
│  │   hasComplexEffectsInTree() ─────▶ Traverse tree ────▶ boolean        │  │
│  │   areAllLeavesMergeable()   ─────▶ Traverse tree ────▶ boolean        │  │
│  │                                                                        │  │
│  │   Total traversals: 6n                                                 │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                                                              │
│                                    │                                        │
│                                    ▼                                        │
│                                                                              │
│  After: Single-pass collection, O(n) complexity                              │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                                                                        │  │
│  │                      collectNodeStats(node)                            │  │
│  │                              │                                         │  │
│  │              ┌───────────────┴───────────────┐                         │  │
│  │              ▼                               ▼                         │  │
│  │      ┌─────────────┐                 ┌─────────────────┐               │  │
│  │      │  Leaf node   │                 │  Container node │               │  │
│  │      │  No children │                 │  Has children   │               │  │
│  │      └──────┬──────┘                 └────────┬────────┘               │  │
│  │             │                                 │                        │  │
│  │             ▼                                 ▼                        │  │
│  │      Compute directly:           Recurse then aggregate:               │  │
│  │      • depth = 0                  • depth = max(child.depth) + 1       │  │
│  │      • totalChildren = 0          • totalChildren = Σ(1 + child.total) │  │
│  │      • hasExcludeType             • hasExcludeType = any(children)     │  │
│  │      • hasImageFill               • hasImageFill = any(children)       │  │
│  │      • hasComplexEffects          • hasComplexEffects = any(children)  │  │
│  │      • allLeavesMergeable         • allLeavesMergeable = all(children) │  │
│  │      • mergeableRatio             • mergeableRatio = count/total       │  │
│  │                                                                        │  │
│  │   Total traversals: n                                                  │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                                                              │
│  Performance improvement: ~28% (64 → 82 nodes/ms)                            │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Type Constants and Configuration

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                              Type Classification Constants                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ CONTAINER_TYPES (Can contain icon children)                         │    │
│  │ ├── GROUP      Figma group                                          │    │
│  │ ├── FRAME      Figma frame                                          │    │
│  │ ├── COMPONENT  Component definition                                 │    │
│  │ └── INSTANCE   Component instance                                   │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ MERGEABLE_TYPES (Can be part of an icon)                            │    │
│  │ ├── VECTOR           Vector path                                    │    │
│  │ ├── RECTANGLE        Rectangle                                      │    │
│  │ ├── ELLIPSE          Ellipse/Circle                                 │    │
│  │ ├── LINE             Line                                           │    │
│  │ ├── POLYGON          Polygon                                        │    │
│  │ ├── STAR             Star                                           │    │
│  │ ├── BOOLEAN_OPERATION Boolean operation result                      │    │
│  │ └── REGULAR_POLYGON  Regular polygon                                │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ EXCLUDE_TYPES (Presence prevents merging)                           │    │
│  │ ├── TEXT       Text element (needs separate rendering)              │    │
│  │ ├── COMPONENT  Component definition (has logic)                     │    │
│  │ └── INSTANCE   Component instance (may have interactions)           │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │ PNG_REQUIRED_EFFECTS (Require PNG format)                           │    │
│  │ ├── DROP_SHADOW      Drop shadow                                    │    │
│  │ ├── INNER_SHADOW     Inner shadow                                   │    │
│  │ ├── LAYER_BLUR       Layer blur                                     │    │
│  │ └── BACKGROUND_BLUR  Background blur                                │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                              Default Configuration                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  DEFAULT_CONFIG = {                                                         │
│    maxIconSize: 300,               // Maximum icon size (px)                │
│    minIconSize: 8,                 // Minimum icon size (px)                │
│    mergeableRatio: 0.6,            // Minimum mergeable ratio (60%)         │
│    maxDepth: 5,                    // Maximum nesting depth                 │
│    maxChildren: 100,               // Maximum child count                   │
│    respectExportSettingsMaxSize: 400  // Max size for designer settings    │
│  }                                                                          │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Export Format Decision Tree

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Export Format Decision Logic                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│                        ┌───────────────────┐                                │
│                        │ Confirmed export  │                                │
│                        │  shouldMerge=true │                                │
│                        └─────────┬─────────┘                                │
│                                  │                                          │
│                                  ▼                                          │
│                 ┌────────────────────────────────┐                          │
│                 │ Designer set export format     │                          │
│                 │ in Figma?                      │                          │
│                 │     node.exportSettings[0]     │                          │
│                 └────────────────┬───────────────┘                          │
│                                  │                                          │
│                    YES───────────┴───────────NO                             │
│                     │                        │                              │
│                     ▼                        │                              │
│         ┌─────────────────────┐              │                              │
│         │ Use designer format │              │                              │
│         │ format = settings   │              │                              │
│         └─────────────────────┘              │                              │
│                                              ▼                              │
│                               ┌────────────────────────┐                    │
│                               │ Tree contains image    │                    │
│                               │ fills?                 │                    │
│                               │   hasImageFill = true  │                    │
│                               └───────────┬────────────┘                    │
│                                           │                                 │
│                              YES──────────┴──────────NO                     │
│                               │                       │                     │
│                               ▼                       ▼                     │
│                   ┌───────────────────┐  ┌─────────────────────────┐       │
│                   │ format = "PNG"    │  │ Tree contains complex   │       │
│                   │                   │  │ effects?                │       │
│                   │ Reason:           │  │ hasComplexEffects=true  │       │
│                   │ Images can't be   │  └───────────┬─────────────┘       │
│                   │ vectorized        │              │                     │
│                   └───────────────────┘   YES────────┴────────NO           │
│                                            │                  │            │
│                                            ▼                  ▼            │
│                               ┌───────────────────┐ ┌───────────────────┐  │
│                               │ format = "PNG"    │ │ format = "SVG"    │  │
│                               │                   │ │                   │  │
│                               │ Reason:           │ │ Reason:           │  │
│                               │ Shadow/blur       │ │ Pure vector       │  │
│                               │ effects can't     │ │ Lossless scaling  │  │
│                               │ render in SVG     │ │                   │  │
│                               └───────────────────┘ └───────────────────┘  │
│                                                                              │
│  ════════════════════════════════════════════════════════════════════════   │
│                                                                              │
│  PNG Priority Scenarios:                    SVG Priority Scenarios:          │
│  ┌────────────────────────────┐         ┌────────────────────────────┐     │
│  │ • Contains bitmap fills    │         │ • Pure vector paths        │     │
│  │ • DROP_SHADOW              │         │ • Simple shape combos      │     │
│  │ • INNER_SHADOW             │         │ • No complex effects       │     │
│  │ • LAYER_BLUR               │         │ • Needs lossless scaling   │     │
│  │ • BACKGROUND_BLUR          │         │ • Needs CSS coloring       │     │
│  │ • Complex gradient+effects │         │ • Small file size needed   │     │
│  └────────────────────────────┘         └────────────────────────────┘     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Complete Call Chain

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                          Complete Call Chain Diagram                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  External Entry (parser.ts / server.ts)                                      │
│           │                                                                  │
│           ▼                                                                  │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                        analyzeNodeTree(node)                             ││
│  │                                                                          ││
│  │  Params: node: FigmaNode, config?: DetectionConfig                       ││
│  │  Returns: { processedTree, exportableIcons, summary }                    ││
│  └────────────────────────────────┬────────────────────────────────────────┘│
│                                   │                                          │
│           ┌───────────────────────┴───────────────────────┐                  │
│           ▼                                               ▼                  │
│  ┌────────────────────────┐                  ┌─────────────────────────────┐│
│  │   processNodeTree()    │                  │  collectExportableIcons()   ││
│  │                        │                  │                             ││
│  │  Recursive processing: │  After complete  │  Traverse processed tree:   ││
│  │  1. Process children   │ ────────────────▶│  1. Has marker → add to list││
│  │     first              │                  │  2. No marker → recurse     ││
│  │  2. Check if all       │                  │  3. Return all exportable   ││
│  │     children are icons │                  │     icons                   ││
│  │  3. Try parent promote │                  │                             ││
│  │  4. Mark _iconDetection│                  │                             ││
│  └───────────┬────────────┘                  └─────────────────────────────┘│
│              │                                                               │
│              │ For each node                                                 │
│              ▼                                                               │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                          detectIcon(node)                                ││
│  │                                                                          ││
│  │  Core detection, executes 9 steps:                                       ││
│  │  Step 1: exportSettings check                                            ││
│  │  Step 2: Type check (container/mergeable)                                ││
│  │  Step 3: Size check (8-300px)                                            ││
│  │  Step 4-8: Use collectNodeStats() for tree properties                    ││
│  │  Step 9: Determine export format (SVG/PNG)                               ││
│  └────────────────────────────────┬────────────────────────────────────────┘│
│                                   │                                          │
│              ┌────────────────────┴────────────────────┐                     │
│              ▼                                         ▼                     │
│  ┌─────────────────────────────┐         ┌────────────────────────────────┐ │
│  │   collectNodeStats(node)    │         │  Helper Functions:              │ │
│  │                             │         │                                 │ │
│  │   Single-pass collection    │         │  • isContainerType()            │ │
│  │   of 7 statistics:          │         │  • isMergeableType()            │ │
│  │   O(n) complexity           │         │  • isExcludeType()              │ │
│  │                             │         │  • hasImageFill()               │ │
│  │   Replaces 6 original       │         │  • hasComplexEffects()          │ │
│  │   recursive functions       │         │  • getNodeSize()                │ │
│  │   ~28% performance boost    │         │                                 │ │
│  └─────────────────────────────┘         └────────────────────────────────┘ │
│                                                                              │
│  ════════════════════════════════════════════════════════════════════════   │
│                                                                              │
│  Return Structure:                                                           │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │ {                                                                        ││
│  │   processedTree: FigmaNode & { _iconDetection?: IconDetectionResult },  ││
│  │   exportableIcons: IconDetectionResult[],                                ││
│  │   summary: {                                                             ││
│  │     totalIcons: number,    // Total exportable icons                     ││
│  │     svgCount: number,      // SVG format count                           ││
│  │     pngCount: number       // PNG format count                           ││
│  │   }                                                                      ││
│  │ }                                                                        ││
│  └─────────────────────────────────────────────────────────────────────────┘│
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

_Last updated: 2025-12-06 (Added complete implementation analysis)_

_For the Chinese version of this document, see [docs/zh-CN/icon-detection.md](../zh-CN/icon-detection.md)_

```

--------------------------------------------------------------------------------
/src/algorithms/layout/detector.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Layout Detection Algorithm
 *
 * Infers Flex layout from absolutely positioned design elements.
 *
 * Core algorithm flow:
 * 1. Extract element bounding boxes
 * 2. Group by Y-axis overlap into "rows"
 * 3. Group by X-axis overlap into "columns"
 * 4. Analyze gap consistency
 * 5. Detect alignment
 * 6. Recursively build layout tree
 *
 * @module algorithms/layout/detector
 */

// ==================== Type Definitions ====================

export interface BoundingBox {
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface ElementRect extends BoundingBox {
  index: number;
  right: number;
  bottom: number;
  centerX: number;
  centerY: number;
}

export interface LayoutGroup {
  elements: ElementRect[];
  direction: "row" | "column" | "none";
  gap: number;
  isGapConsistent: boolean;
  justifyContent: string;
  alignItems: string;
  bounds: BoundingBox;
}

export interface LayoutAnalysisResult {
  direction: "row" | "column" | "none";
  confidence: number;
  gap: number;
  isGapConsistent: boolean;
  justifyContent: string;
  alignItems: string;
  rows: ElementRect[][];
  columns: ElementRect[][];
  overlappingElements: ElementRect[];
}

// ==================== Bounding Box Utilities ====================

/**
 * Extract bounding box from CSS styles object
 */
export function extractBoundingBox(cssStyles: Record<string, unknown>): BoundingBox | null {
  if (!cssStyles) return null;

  const x = parseFloat(String(cssStyles.left || "0").replace("px", ""));
  const y = parseFloat(String(cssStyles.top || "0").replace("px", ""));
  const width = parseFloat(String(cssStyles.width || "0").replace("px", ""));
  const height = parseFloat(String(cssStyles.height || "0").replace("px", ""));

  if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
    return null;
  }

  return { x, y, width, height };
}

/**
 * Convert bounding box to element rect (with computed properties)
 */
export function toElementRect(box: BoundingBox, index: number): ElementRect {
  return {
    ...box,
    index,
    right: box.x + box.width,
    bottom: box.y + box.height,
    centerX: box.x + box.width / 2,
    centerY: box.y + box.height / 2,
  };
}

/**
 * Calculate bounding rect of a group of elements
 */
export function calculateBounds(rects: ElementRect[]): BoundingBox {
  if (rects.length === 0) {
    return { x: 0, y: 0, width: 0, height: 0 };
  }

  const minX = Math.min(...rects.map((r) => r.x));
  const minY = Math.min(...rects.map((r) => r.y));
  const maxX = Math.max(...rects.map((r) => r.right));
  const maxY = Math.max(...rects.map((r) => r.bottom));

  return {
    x: minX,
    y: minY,
    width: maxX - minX,
    height: maxY - minY,
  };
}

// ==================== Overlap Detection ====================

/**
 * Check if two elements overlap on Y-axis (for row detection)
 * If two elements have overlapping vertical ranges, they are in the same row
 */
export function isOverlappingY(a: ElementRect, b: ElementRect, tolerance: number = 0): boolean {
  return !(a.bottom + tolerance < b.y || b.bottom + tolerance < a.y);
}

/**
 * Check if two elements overlap on X-axis (for column detection)
 * If two elements have overlapping horizontal ranges, they are in the same column
 */
export function isOverlappingX(a: ElementRect, b: ElementRect, tolerance: number = 0): boolean {
  return !(a.right + tolerance < b.x || b.right + tolerance < a.x);
}

/**
 * Check if two elements fully overlap (requires absolute positioning)
 */
export function isFullyOverlapping(
  a: ElementRect,
  b: ElementRect,
  threshold: number = 0.5,
): boolean {
  const overlapX = Math.max(0, Math.min(a.right, b.right) - Math.max(a.x, b.x));
  const overlapY = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.y, b.y));
  const overlapArea = overlapX * overlapY;

  const areaA = a.width * a.height;
  const areaB = b.width * b.height;
  const minArea = Math.min(areaA, areaB);

  return minArea > 0 && overlapArea / minArea > threshold;
}

/**
 * Calculate IoU (Intersection over Union) between two elements
 *
 * IoU is a standard metric for measuring overlap:
 * - IoU = 0: No overlap
 * - IoU = 1: Perfect overlap (same box)
 *
 * Industry standard thresholds:
 * - IoU > 0.1: Partial overlap (consider absolute positioning)
 * - IoU > 0.5: Significant overlap (definitely needs absolute)
 */
export function calculateIoU(a: ElementRect, b: ElementRect): number {
  // Calculate intersection
  const xOverlap = Math.max(0, Math.min(a.right, b.right) - Math.max(a.x, b.x));
  const yOverlap = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.y, b.y));
  const intersection = xOverlap * yOverlap;

  if (intersection === 0) return 0;

  // Calculate union
  const areaA = a.width * a.height;
  const areaB = b.width * b.height;
  const union = areaA + areaB - intersection;

  return union > 0 ? intersection / union : 0;
}

/**
 * Overlap type classification based on IoU
 */
export type OverlapType = "none" | "adjacent" | "partial" | "significant" | "contained";

/**
 * Classify overlap type between two elements
 *
 * @param a First element
 * @param b Second element
 * @returns Overlap classification
 */
export function classifyOverlap(a: ElementRect, b: ElementRect): OverlapType {
  const iou = calculateIoU(a, b);

  if (iou === 0) {
    // Check if adjacent (touching but not overlapping)
    // Calculate gap on each axis
    const gapX = Math.max(a.x, b.x) - Math.min(a.right, b.right);
    const gapY = Math.max(a.y, b.y) - Math.min(a.bottom, b.bottom);

    // Elements are adjacent only if gap on the separating axis is small
    // If they overlap on one axis (gap < 0), check gap on the other axis
    let effectiveGap: number;
    if (gapX > 0 && gapY > 0) {
      // Don't overlap on either axis - use the maximum gap (corner distance)
      effectiveGap = Math.max(gapX, gapY);
    } else if (gapX > 0) {
      // Don't overlap on X, but overlap on Y
      effectiveGap = gapX;
    } else if (gapY > 0) {
      // Don't overlap on Y, but overlap on X
      effectiveGap = gapY;
    } else {
      // This shouldn't happen if IoU is 0, but handle it
      effectiveGap = 0;
    }

    return effectiveGap <= 2 ? "adjacent" : "none";
  }

  if (iou < 0.1) return "partial";
  if (iou < 0.5) return "significant";
  return "contained";
}

/**
 * Overlap detection result
 */
export interface OverlapDetectionResult {
  /** Elements that can participate in flow layout (flex/grid) */
  flowElements: ElementRect[];
  /** Elements that need absolute positioning due to overlap */
  stackedElements: ElementRect[];
  /** Indices of stacked elements */
  stackedIndices: Set<number>;
}

/**
 * Detect overlapping elements and separate them from flow elements
 *
 * Uses IoU (Intersection over Union) to detect overlaps:
 * - IoU > 0.1: Element is considered overlapping and needs absolute positioning
 *
 * This follows the imgcook algorithm approach where overlapping elements
 * are marked for absolute positioning while the rest participate in flex/grid layout.
 *
 * @param rects Element rectangles to analyze
 * @param iouThreshold IoU threshold for overlap detection (default: 0.1)
 * @returns Separated flow and stacked elements
 */
export function detectOverlappingElements(
  rects: ElementRect[],
  iouThreshold: number = 0.1,
): OverlapDetectionResult {
  const stackedIndices = new Set<number>();

  // Check each pair of elements for overlap
  for (let i = 0; i < rects.length; i++) {
    for (let j = i + 1; j < rects.length; j++) {
      const iou = calculateIoU(rects[i], rects[j]);
      if (iou > iouThreshold) {
        // Both overlapping elements need absolute positioning
        stackedIndices.add(rects[i].index);
        stackedIndices.add(rects[j].index);
      }
    }
  }

  // Separate elements into flow and stacked groups
  const flowElements: ElementRect[] = [];
  const stackedElements: ElementRect[] = [];

  for (const rect of rects) {
    if (stackedIndices.has(rect.index)) {
      stackedElements.push(rect);
    } else {
      flowElements.push(rect);
    }
  }

  return {
    flowElements,
    stackedElements,
    stackedIndices,
  };
}

/**
 * Background element detection result
 */
export interface BackgroundDetectionResult {
  /** Index of background element (or -1 if none) */
  backgroundIndex: number;
  /** Indices of content elements */
  contentIndices: number[];
  /** Whether a valid background was detected */
  hasBackground: boolean;
}

/**
 * Detect if a container has a background element pattern
 *
 * Background element pattern:
 * - Element at position 0,0 (or very close)
 * - Same size as parent container (or very close)
 * - Typically a RECTANGLE type
 * - Other elements are positioned on top of it
 *
 * @param rects All child element rectangles
 * @param parentWidth Parent container width
 * @param parentHeight Parent container height
 * @returns Background detection result
 */
export function detectBackgroundElement(
  rects: ElementRect[],
  parentWidth: number,
  parentHeight: number,
): BackgroundDetectionResult {
  const emptyResult: BackgroundDetectionResult = {
    backgroundIndex: -1,
    contentIndices: rects.map((r) => r.index),
    hasBackground: false,
  };

  if (rects.length < 2) return emptyResult;

  // Find element at origin that matches parent size
  for (const rect of rects) {
    // Must be at origin (within 2px tolerance)
    if (rect.x > 2 || rect.y > 2) continue;

    // Must match parent size (within 5% tolerance)
    const widthMatch = Math.abs(rect.width - parentWidth) / parentWidth < 0.05;
    const heightMatch = Math.abs(rect.height - parentHeight) / parentHeight < 0.05;

    if (widthMatch && heightMatch) {
      // Found background element
      const contentIndices = rects.filter((r) => r.index !== rect.index).map((r) => r.index);

      return {
        backgroundIndex: rect.index,
        contentIndices,
        hasBackground: true,
      };
    }
  }

  return emptyResult;
}

// ==================== Row/Column Grouping Algorithm ====================

/**
 * Group elements by Y-axis overlap into "rows"
 * Core algorithm: if two elements overlap on Y-axis, they belong to the same row
 */
export function groupIntoRows(rects: ElementRect[], tolerance: number = 2): ElementRect[][] {
  if (rects.length === 0) return [];
  if (rects.length === 1) return [[rects[0]]];

  // Sort by Y coordinate
  const sorted = [...rects].sort((a, b) => a.y - b.y);

  const rows: ElementRect[][] = [];
  let currentRow: ElementRect[] = [sorted[0]];

  for (let i = 1; i < sorted.length; i++) {
    const elem = sorted[i];

    // Check if overlaps with any element in current row on Y-axis
    const overlapsWithRow = currentRow.some((rowElem) => isOverlappingY(rowElem, elem, tolerance));

    if (overlapsWithRow) {
      currentRow.push(elem);
    } else {
      // Current row complete, sort by X and save
      rows.push(currentRow.sort((a, b) => a.x - b.x));
      currentRow = [elem];
    }
  }

  // Save last row
  if (currentRow.length > 0) {
    rows.push(currentRow.sort((a, b) => a.x - b.x));
  }

  return rows;
}

/**
 * Group elements by X-axis overlap into "columns"
 * Core algorithm: if two elements overlap on X-axis, they belong to the same column
 */
export function groupIntoColumns(rects: ElementRect[], tolerance: number = 2): ElementRect[][] {
  if (rects.length === 0) return [];
  if (rects.length === 1) return [[rects[0]]];

  // Sort by X coordinate
  const sorted = [...rects].sort((a, b) => a.x - b.x);

  const columns: ElementRect[][] = [];
  let currentColumn: ElementRect[] = [sorted[0]];

  for (let i = 1; i < sorted.length; i++) {
    const elem = sorted[i];

    // Check if overlaps with any element in current column on X-axis
    const overlapsWithColumn = currentColumn.some((colElem) =>
      isOverlappingX(colElem, elem, tolerance),
    );

    if (overlapsWithColumn) {
      currentColumn.push(elem);
    } else {
      // Current column complete, sort by Y and save
      columns.push(currentColumn.sort((a, b) => a.y - b.y));
      currentColumn = [elem];
    }
  }

  // Save last column
  if (currentColumn.length > 0) {
    columns.push(currentColumn.sort((a, b) => a.y - b.y));
  }

  return columns;
}

/**
 * Find fully overlapping elements (requires absolute positioning)
 */
export function findOverlappingElements(rects: ElementRect[]): ElementRect[] {
  const overlapping: Set<number> = new Set();

  for (let i = 0; i < rects.length; i++) {
    for (let j = i + 1; j < rects.length; j++) {
      if (isFullyOverlapping(rects[i], rects[j])) {
        overlapping.add(rects[i].index);
        overlapping.add(rects[j].index);
      }
    }
  }

  return rects.filter((r) => overlapping.has(r.index));
}

// ==================== Gap Analysis ====================

/**
 * Calculate gaps between a group of elements
 */
export function calculateGaps(
  rects: ElementRect[],
  direction: "horizontal" | "vertical",
): number[] {
  if (rects.length < 2) return [];

  const sorted =
    direction === "horizontal"
      ? [...rects].sort((a, b) => a.x - b.x)
      : [...rects].sort((a, b) => a.y - b.y);

  const gaps: number[] = [];

  for (let i = 0; i < sorted.length - 1; i++) {
    const current = sorted[i];
    const next = sorted[i + 1];

    const gap = direction === "horizontal" ? next.x - current.right : next.y - current.bottom;

    // Only record positive gaps
    if (gap >= 0) {
      gaps.push(gap);
    }
  }

  return gaps;
}

/**
 * Analyze gap consistency
 */
export function analyzeGaps(
  gaps: number[],
  tolerancePercent: number = 20,
): {
  isConsistent: boolean;
  average: number;
  rounded: number;
  stdDev: number;
} {
  if (gaps.length === 0) {
    return { isConsistent: true, average: 0, rounded: 0, stdDev: 0 };
  }

  if (gaps.length === 1) {
    const rounded = roundToCommonValue(gaps[0]);
    return { isConsistent: true, average: gaps[0], rounded, stdDev: 0 };
  }

  const sum = gaps.reduce((a, b) => a + b, 0);
  const average = sum / gaps.length;

  const variance = gaps.reduce((acc, gap) => acc + Math.pow(gap - average, 2), 0) / gaps.length;
  const stdDev = Math.sqrt(variance);

  // Consistency check: standard deviation less than specified percentage of average
  const tolerance = average * (tolerancePercent / 100);
  const isConsistent = average === 0 || stdDev <= tolerance;

  const rounded = roundToCommonValue(average);

  return { isConsistent, average, rounded, stdDev };
}

/**
 * Round gap to common design values
 */
export function roundToCommonValue(value: number): number {
  const COMMON_VALUES = [0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128];

  // Find closest common value
  let closest = COMMON_VALUES[0];
  let minDiff = Math.abs(value - closest);

  for (const common of COMMON_VALUES) {
    const diff = Math.abs(value - common);
    if (diff < minDiff) {
      minDiff = diff;
      closest = common;
    }
  }

  // If difference is too large (> 4px), use rounded value
  if (minDiff > 4) {
    return Math.round(value);
  }

  return closest;
}

// ==================== Alignment Detection ====================

/**
 * Check if a group of values are aligned
 */
export function areValuesAligned(values: number[], tolerance: number = 3): boolean {
  if (values.length < 2) return true;

  const first = values[0];
  return values.every((v) => Math.abs(v - first) <= tolerance);
}

/**
 * Analyze alignment
 */
export function analyzeAlignment(
  rects: ElementRect[],
  bounds: BoundingBox,
): {
  horizontal: "left" | "center" | "right" | "stretch" | "none";
  vertical: "top" | "center" | "bottom" | "stretch" | "none";
} {
  if (rects.length === 0) {
    return { horizontal: "none", vertical: "none" };
  }

  const tolerance = Math.max(3, Math.min(bounds.width, bounds.height) * 0.02);

  // Horizontal alignment analysis
  const lefts = rects.map((r) => r.x);
  const rights = rects.map((r) => r.right);
  const centerXs = rects.map((r) => r.centerX);
  const widths = rects.map((r) => r.width);

  let horizontal: "left" | "center" | "right" | "stretch" | "none" = "none";

  if (areValuesAligned(lefts, tolerance)) {
    horizontal = "left";
  } else if (areValuesAligned(rights, tolerance)) {
    horizontal = "right";
  } else if (areValuesAligned(centerXs, tolerance)) {
    horizontal = "center";
  } else if (areValuesAligned(widths, tolerance) && widths[0] >= bounds.width * 0.9) {
    horizontal = "stretch";
  }

  // Vertical alignment analysis
  const tops = rects.map((r) => r.y);
  const bottoms = rects.map((r) => r.bottom);
  const centerYs = rects.map((r) => r.centerY);
  const heights = rects.map((r) => r.height);

  let vertical: "top" | "center" | "bottom" | "stretch" | "none" = "none";

  if (areValuesAligned(tops, tolerance)) {
    vertical = "top";
  } else if (areValuesAligned(bottoms, tolerance)) {
    vertical = "bottom";
  } else if (areValuesAligned(centerYs, tolerance)) {
    vertical = "center";
  } else if (areValuesAligned(heights, tolerance) && heights[0] >= bounds.height * 0.9) {
    vertical = "stretch";
  }

  return { horizontal, vertical };
}

/**
 * Convert alignment to CSS justify-content value
 */
export function toJustifyContent(alignment: string, hasGaps: boolean): string {
  switch (alignment) {
    case "left":
    case "top":
      return "flex-start";
    case "right":
    case "bottom":
      return "flex-end";
    case "center":
      return "center";
    case "stretch":
      return hasGaps ? "space-between" : "flex-start";
    default:
      return "flex-start";
  }
}

/**
 * Convert alignment to CSS align-items value
 */
export function toAlignItems(alignment: string): string {
  switch (alignment) {
    case "left":
    case "top":
      return "flex-start";
    case "right":
    case "bottom":
      return "flex-end";
    case "center":
      return "center";
    case "stretch":
      return "stretch";
    default:
      return "stretch";
  }
}

// ==================== Layout Direction Detection ====================

/**
 * Detect optimal layout direction
 * Core logic: compare quality of row grouping vs column grouping
 */
export function detectLayoutDirection(rects: ElementRect[]): {
  direction: "row" | "column" | "none";
  confidence: number;
  reason: string;
} {
  if (rects.length < 2) {
    return { direction: "none", confidence: 0, reason: "Insufficient elements" };
  }

  const rows = groupIntoRows(rects);
  const columns = groupIntoColumns(rects);

  // Calculate row layout score
  const rowScore = calculateLayoutScore(rows, "row", rects.length);

  // Calculate column layout score
  const columnScore = calculateLayoutScore(columns, "column", rects.length);

  // Select layout with higher score
  if (rowScore.score > columnScore.score && rowScore.score > 0.3) {
    return {
      direction: "row",
      confidence: rowScore.score,
      reason: rowScore.reason,
    };
  } else if (columnScore.score > rowScore.score && columnScore.score > 0.3) {
    return {
      direction: "column",
      confidence: columnScore.score,
      reason: columnScore.reason,
    };
  }

  return { direction: "none", confidence: 0, reason: "No clear layout pattern" };
}

/**
 * Calculate layout score
 */
function calculateLayoutScore(
  groups: ElementRect[][],
  direction: "row" | "column",
  totalElements: number,
): { score: number; reason: string } {
  if (groups.length === 0) {
    return { score: 0, reason: "No groups" };
  }

  // Score factors:
  // 1. Group count rationality (ideal: one or few groups per row/column)
  // 2. Gap consistency within each group
  // 3. Element coverage

  let score = 0;
  const reasons: string[] = [];

  // 1. For row layout, ideal is single row (all elements horizontal)
  //    For column layout, ideal is single column (all elements vertical)
  if (groups.length === 1 && groups[0].length === totalElements) {
    score += 0.4;
    reasons.push("Perfect grouping");
  } else if (groups.length <= 3) {
    score += 0.2;
    reasons.push("Reasonable grouping");
  }

  // 2. Analyze gap consistency
  for (const group of groups) {
    if (group.length >= 2) {
      const gapDirection = direction === "row" ? "horizontal" : "vertical";
      const gaps = calculateGaps(group, gapDirection);
      const gapAnalysis = analyzeGaps(gaps);

      if (gapAnalysis.isConsistent && gaps.length > 0) {
        score += 0.3 / groups.length;
        reasons.push(`Consistent gap (${Math.round(gapAnalysis.average)}px)`);
      }
    }
  }

  // 3. Check cross-axis alignment
  for (const group of groups) {
    if (group.length >= 2) {
      const bounds = calculateBounds(group);
      const alignment = analyzeAlignment(group, bounds);
      const crossAlignment = direction === "row" ? alignment.vertical : alignment.horizontal;

      if (crossAlignment !== "none") {
        score += 0.2 / groups.length;
        reasons.push(`Good alignment (${crossAlignment})`);
      }
    }
  }

  // 4. Check main axis element distribution
  const largestGroup = groups.reduce((a, b) => (a.length > b.length ? a : b));
  if (largestGroup.length >= totalElements * 0.7) {
    score += 0.1;
    reasons.push("Concentrated distribution");
  }

  return {
    score: Math.min(1, score),
    reason: reasons.join(", ") || "No obvious features",
  };
}

// ==================== Complete Layout Analysis ====================

/**
 * Complete layout analysis
 * Returns layout direction, gap, alignment, and all other information
 */
export function analyzeLayout(rects: ElementRect[]): LayoutAnalysisResult {
  if (rects.length < 2) {
    return {
      direction: "none",
      confidence: 0,
      gap: 0,
      isGapConsistent: true,
      justifyContent: "flex-start",
      alignItems: "stretch",
      rows: [rects],
      columns: [rects],
      overlappingElements: [],
    };
  }

  // Detect overlapping elements
  const overlappingElements = findOverlappingElements(rects);

  // Analyze layout after filtering out overlapping elements
  const nonOverlapping = rects.filter((r) => !overlappingElements.some((o) => o.index === r.index));

  if (nonOverlapping.length < 2) {
    return {
      direction: "none",
      confidence: 0,
      gap: 0,
      isGapConsistent: true,
      justifyContent: "flex-start",
      alignItems: "stretch",
      rows: [rects],
      columns: [rects],
      overlappingElements,
    };
  }

  // Detect layout direction
  const { direction, confidence } = detectLayoutDirection(nonOverlapping);

  // Grouping
  const rows = groupIntoRows(nonOverlapping);
  const columns = groupIntoColumns(nonOverlapping);

  // Calculate gaps
  const gapDirection = direction === "row" ? "horizontal" : "vertical";
  const gaps = calculateGaps(
    direction === "row"
      ? nonOverlapping.sort((a, b) => a.x - b.x)
      : nonOverlapping.sort((a, b) => a.y - b.y),
    gapDirection,
  );
  const gapAnalysis = analyzeGaps(gaps);

  // Analyze alignment
  const bounds = calculateBounds(nonOverlapping);
  const alignment = analyzeAlignment(nonOverlapping, bounds);

  // Determine CSS properties
  let justifyContent: string;
  let alignItems: string;

  if (direction === "row") {
    justifyContent = toJustifyContent(alignment.horizontal, gaps.length > 0);
    alignItems = toAlignItems(alignment.vertical);
  } else if (direction === "column") {
    justifyContent = toJustifyContent(alignment.vertical, gaps.length > 0);
    alignItems = toAlignItems(alignment.horizontal);
  } else {
    justifyContent = "flex-start";
    alignItems = "stretch";
  }

  return {
    direction,
    confidence,
    gap: gapAnalysis.rounded,
    isGapConsistent: gapAnalysis.isConsistent,
    justifyContent,
    alignItems,
    rows,
    columns,
    overlappingElements,
  };
}

// ==================== Recursive Layout Tree Building ====================

export interface LayoutNode {
  type: "container" | "element";
  direction?: "row" | "column";
  gap?: number;
  justifyContent?: string;
  alignItems?: string;
  children?: LayoutNode[];
  elementIndex?: number;
  bounds: BoundingBox;
  needsAbsolute?: boolean;
}

/**
 * Recursively build layout tree
 * Convert flat element list to nested layout structure
 */
export function buildLayoutTree(
  rects: ElementRect[],
  depth: number = 0,
  maxDepth: number = 5,
): LayoutNode {
  const bounds = calculateBounds(rects);

  // Single element returns directly
  if (rects.length === 1) {
    return {
      type: "element",
      elementIndex: rects[0].index,
      bounds,
    };
  }

  // Max depth reached, return simple container
  if (depth >= maxDepth) {
    return {
      type: "container",
      direction: "column",
      children: rects.map((r) => ({
        type: "element" as const,
        elementIndex: r.index,
        bounds: { x: r.x, y: r.y, width: r.width, height: r.height },
      })),
      bounds,
    };
  }

  // Analyze layout
  const analysis = analyzeLayout(rects);

  // Handle overlapping elements
  const overlappingNodes: LayoutNode[] = analysis.overlappingElements.map((r) => ({
    type: "element" as const,
    elementIndex: r.index,
    bounds: { x: r.x, y: r.y, width: r.width, height: r.height },
    needsAbsolute: true,
  }));

  // Filter out overlapping elements
  const nonOverlapping = rects.filter(
    (r) => !analysis.overlappingElements.some((o) => o.index === r.index),
  );

  if (nonOverlapping.length === 0) {
    // All elements overlap
    return {
      type: "container",
      children: overlappingNodes,
      bounds,
    };
  }

  if (analysis.direction === "none" || analysis.confidence < 0.3) {
    // No clear layout, use default vertical layout
    return {
      type: "container",
      direction: "column",
      children: [
        ...nonOverlapping.map((r) => ({
          type: "element" as const,
          elementIndex: r.index,
          bounds: { x: r.x, y: r.y, width: r.width, height: r.height },
        })),
        ...overlappingNodes,
      ],
      bounds,
    };
  }

  // Group by layout direction
  const groups = analysis.direction === "row" ? analysis.rows : analysis.columns;

  // Recursively process each group
  const children: LayoutNode[] = groups.map((group) => {
    if (group.length === 1) {
      return {
        type: "element" as const,
        elementIndex: group[0].index,
        bounds: { x: group[0].x, y: group[0].y, width: group[0].width, height: group[0].height },
      };
    }

    // For multi-element groups, check if further analysis needed (cross direction)
    const crossDirection = analysis.direction === "row" ? "column" : "row";
    const crossGroups = crossDirection === "row" ? groupIntoRows(group) : groupIntoColumns(group);

    if (crossGroups.length > 1) {
      // Nested layout needed
      return buildLayoutTree(group, depth + 1, maxDepth);
    }

    // Simple group, no nesting needed
    const groupBounds = calculateBounds(group);
    return {
      type: "container" as const,
      direction: crossDirection,
      children: group.map((r) => ({
        type: "element" as const,
        elementIndex: r.index,
        bounds: { x: r.x, y: r.y, width: r.width, height: r.height },
      })),
      bounds: groupBounds,
    };
  });

  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],
    bounds,
  };
}

// ==================== Grid Layout Detection ====================

/**
 * Result of grid layout analysis
 */
export interface GridAnalysisResult {
  /** Whether elements form a valid grid layout */
  isGrid: boolean;
  /** Confidence score (0-1) for grid detection */
  confidence: number;
  /** Number of rows in the grid */
  rowCount: number;
  /** Number of columns in the grid */
  columnCount: number;
  /** Gap between rows in pixels */
  rowGap: number;
  /** Gap between columns in pixels */
  columnGap: number;
  /** Whether row gap is consistent */
  isRowGapConsistent: boolean;
  /** Whether column gap is consistent */
  isColumnGapConsistent: boolean;
  /** Width of each column track */
  trackWidths: number[];
  /** Height of each row track */
  trackHeights: number[];
  /** X positions where columns are aligned */
  alignedColumnPositions: number[];
  /** Grouped rows of elements */
  rows: ElementRect[][];
  /** Map of element indices to grid cells [row][col] */
  cellMap: (number | null)[][];
}

// ==================== Homogeneity Detection ====================

/**
 * Result of homogeneity analysis for a group of elements
 */
export interface HomogeneityResult {
  /** Whether the group is homogeneous (similar size/type) */
  isHomogeneous: boolean;
  /** Coefficient of variation for widths (lower = more similar) */
  widthCV: number;
  /** Coefficient of variation for heights (lower = more similar) */
  heightCV: number;
  /** Unique element types in the group */
  types: string[];
  /** Elements that belong to the homogeneous group */
  homogeneousElements: ElementRect[];
  /** Elements that don't fit the homogeneous pattern */
  outlierElements: ElementRect[];
}

/**
 * Size cluster for grouping elements by similar dimensions
 */
export interface SizeCluster {
  /** Representative width for this cluster */
  width: number;
  /** Representative height for this cluster */
  height: number;
  /** Elements belonging to this cluster */
  elements: ElementRect[];
  /** Original node types (if available) */
  types?: string[];
}

/**
 * Calculate coefficient of variation (CV) for a set of values
 * CV = stddev / mean, lower values indicate more consistency
 * Returns 0 for single values or empty arrays
 */
export function coefficientOfVariation(values: number[]): number {
  if (values.length <= 1) return 0;

  const mean = values.reduce((a, b) => a + b, 0) / values.length;
  if (mean === 0) return 0;

  const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
  const stddev = Math.sqrt(variance);

  return stddev / mean;
}

/**
 * Cluster elements by similar size (width × height)
 * Groups elements whose dimensions are within the tolerance percentage
 *
 * @param rects - Elements to cluster
 * @param tolerancePercent - Size tolerance as decimal (0.2 = 20%)
 * @returns Array of size clusters, sorted by element count (largest first)
 */
export function clusterBySimilarSize(
  rects: ElementRect[],
  tolerancePercent: number = 0.2,
): SizeCluster[] {
  if (rects.length === 0) return [];

  const clusters: SizeCluster[] = [];

  for (const rect of rects) {
    let foundCluster = false;

    for (const cluster of clusters) {
      // Check if rect dimensions are within tolerance of cluster
      const widthDiff = Math.abs(rect.width - cluster.width) / Math.max(cluster.width, 1);
      const heightDiff = Math.abs(rect.height - cluster.height) / Math.max(cluster.height, 1);

      if (widthDiff <= tolerancePercent && heightDiff <= tolerancePercent) {
        cluster.elements.push(rect);
        // Update cluster center to average
        const allWidths = cluster.elements.map((e) => e.width);
        const allHeights = cluster.elements.map((e) => e.height);
        cluster.width = allWidths.reduce((a, b) => a + b, 0) / allWidths.length;
        cluster.height = allHeights.reduce((a, b) => a + b, 0) / allHeights.length;
        foundCluster = true;
        break;
      }
    }

    if (!foundCluster) {
      clusters.push({
        width: rect.width,
        height: rect.height,
        elements: [rect],
      });
    }
  }

  // Sort by element count (largest cluster first)
  return clusters.sort((a, b) => b.elements.length - a.elements.length);
}

/**
 * Check if a group of elements is homogeneous
 * Homogeneous = similar sizes, compatible types
 *
 * @param rects - Elements to check
 * @param nodeTypes - Optional array of node types corresponding to rects
 * @param sizeToleranceCV - Max coefficient of variation for size (default 0.2 = 20%)
 * @returns Homogeneity analysis result
 */
export function analyzeHomogeneity(
  rects: ElementRect[],
  nodeTypes?: string[],
  sizeToleranceCV: number = 0.2,
): HomogeneityResult {
  const emptyResult: HomogeneityResult = {
    isHomogeneous: false,
    widthCV: 1,
    heightCV: 1,
    types: [],
    homogeneousElements: [],
    outlierElements: rects,
  };

  if (rects.length < 4) {
    return emptyResult;
  }

  // 1. Cluster by size first
  const sizeClusters = clusterBySimilarSize(rects, sizeToleranceCV);

  // If no cluster has 4+ elements, not homogeneous enough for grid
  const largestCluster = sizeClusters[0];
  if (!largestCluster || largestCluster.elements.length < 4) {
    return emptyResult;
  }

  // 2. Calculate CV for the largest cluster
  const widths = largestCluster.elements.map((e) => e.width);
  const heights = largestCluster.elements.map((e) => e.height);
  const widthCV = coefficientOfVariation(widths);
  const heightCV = coefficientOfVariation(heights);

  // 3. Check type consistency if provided
  let types: string[] = [];
  if (nodeTypes) {
    const clusterIndices = new Set(largestCluster.elements.map((e) => e.index));
    types = [...new Set(nodeTypes.filter((_, i) => clusterIndices.has(i)))];

    // Allow compatible container types
    const allowedTypes = new Set(["FRAME", "INSTANCE", "COMPONENT", "GROUP", "RECTANGLE"]);
    const hasIncompatibleType = types.some((t) => !allowedTypes.has(t));

    if (hasIncompatibleType) {
      return {
        ...emptyResult,
        widthCV,
        heightCV,
        types,
      };
    }
  }

  // 4. Determine if homogeneous
  const isHomogeneous = widthCV <= sizeToleranceCV && heightCV <= sizeToleranceCV;

  // 5. Separate homogeneous elements from outliers
  const homogeneousSet = new Set(largestCluster.elements.map((e) => e.index));
  const homogeneousElements = rects.filter((r) => homogeneousSet.has(r.index));
  const outlierElements = rects.filter((r) => !homogeneousSet.has(r.index));

  return {
    isHomogeneous,
    widthCV,
    heightCV,
    types,
    homogeneousElements,
    outlierElements,
  };
}

/**
 * Result of filtering homogeneous elements for grid detection
 */
export interface HomogeneousFilterResult {
  /** Elements suitable for grid detection */
  elements: ElementRect[];
  /** Indices of grid elements in the original array */
  gridIndices: Set<number>;
}

/**
 * Filter elements for grid detection by keeping only homogeneous groups
 * This prevents mixed layouts from being incorrectly detected as grids
 *
 * @param rects - All child elements
 * @param nodeTypes - Optional node types for additional filtering
 * @returns Elements suitable for grid detection with their indices, or empty if not homogeneous
 */
export function filterHomogeneousForGrid(
  rects: ElementRect[],
  nodeTypes?: string[],
): HomogeneousFilterResult {
  const analysis = analyzeHomogeneity(rects, nodeTypes);

  if (analysis.isHomogeneous && analysis.homogeneousElements.length >= 4) {
    // Extract indices from homogeneous elements
    const gridIndices = new Set(analysis.homogeneousElements.map((el) => el.index));
    return {
      elements: analysis.homogeneousElements,
      gridIndices,
    };
  }

  return {
    elements: [],
    gridIndices: new Set(),
  };
}

/**
 * A cluster of similar values
 */
interface ValueCluster {
  center: number;
  values: number[];
  count: number;
}

/**
 * Column alignment analysis result
 */
interface ColumnAlignmentResult {
  isAligned: boolean;
  alignedPositions: number[];
  columnCount: number;
}

/**
 * Cluster similar values together
 * Used to find aligned column positions across rows
 */
export function clusterValues(values: number[], tolerance: number = 3): ValueCluster[] {
  if (values.length === 0) return [];

  const sorted = [...values].sort((a, b) => a - b);
  const clusters: ValueCluster[] = [];
  let currentCluster: ValueCluster = {
    center: sorted[0],
    values: [sorted[0]],
    count: 1,
  };

  for (let i = 1; i < sorted.length; i++) {
    const value = sorted[i];
    // Check if value is within tolerance of cluster center
    if (Math.abs(value - currentCluster.center) <= tolerance) {
      currentCluster.values.push(value);
      currentCluster.count++;
      // Update center to be the average
      currentCluster.center =
        currentCluster.values.reduce((a, b) => a + b, 0) / currentCluster.values.length;
    } else {
      // Start new cluster
      clusters.push(currentCluster);
      currentCluster = {
        center: value,
        values: [value],
        count: 1,
      };
    }
  }
  clusters.push(currentCluster);

  return clusters;
}

/**
 * Extract X positions of elements in each row
 */
function extractColumnPositions(rows: ElementRect[][]): number[][] {
  return rows.map((row) => row.map((el) => el.x).sort((a, b) => a - b));
}

/**
 * Check if columns are aligned across rows
 * Returns aligned column positions if alignment is detected
 */
function checkColumnAlignment(rows: ElementRect[][], tolerance: number = 3): ColumnAlignmentResult {
  if (rows.length < 2) {
    return { isAligned: false, alignedPositions: [], columnCount: 0 };
  }

  // Get column positions for each row
  const columnPositions = extractColumnPositions(rows);

  // Collect all X positions
  const allPositions = columnPositions.flat();

  // Cluster the positions
  const clusters = clusterValues(allPositions, tolerance);

  // Get aligned positions (cluster centers)
  const alignedPositions = clusters.map((c) => Math.round(c.center)).sort((a, b) => a - b);

  // Verify that most rows have elements at the aligned positions
  let alignedRows = 0;
  for (const row of rows) {
    const rowPositions = row.map((el) => el.x);
    const rowAligned = rowPositions.every((x) =>
      alignedPositions.some((ap) => Math.abs(x - ap) <= tolerance),
    );
    if (rowAligned) alignedRows++;
  }

  const alignmentRatio = alignedRows / rows.length;

  return {
    isAligned: alignmentRatio >= 0.8,
    alignedPositions,
    columnCount: alignedPositions.length,
  };
}

/**
 * Calculate row gaps (vertical spacing between rows)
 */
function calculateRowGaps(rows: ElementRect[][]): number[] {
  if (rows.length < 2) return [];

  const gaps: number[] = [];
  for (let i = 0; i < rows.length - 1; i++) {
    const currentRowBottom = Math.max(...rows[i].map((el) => el.bottom));
    const nextRowTop = Math.min(...rows[i + 1].map((el) => el.y));
    const gap = nextRowTop - currentRowBottom;
    if (gap >= 0) gaps.push(gap);
  }

  return gaps;
}

/**
 * Calculate column gaps (horizontal spacing between columns)
 */
function calculateColumnGaps(alignedPositions: number[], rows: ElementRect[][]): number[] {
  if (alignedPositions.length < 2) return [];

  const gaps: number[] = [];

  // For each row, calculate gaps between adjacent elements
  for (const row of rows) {
    const sortedByX = [...row].sort((a, b) => a.x - b.x);
    for (let i = 0; i < sortedByX.length - 1; i++) {
      const gap = sortedByX[i + 1].x - sortedByX[i].right;
      if (gap >= 0) gaps.push(gap);
    }
  }

  return gaps;
}

/**
 * Calculate track widths (column widths)
 */
function calculateTrackWidths(
  rows: ElementRect[][],
  alignedPositions: number[],
  tolerance: number = 3,
): number[] {
  // Group elements by column
  const columns: ElementRect[][] = alignedPositions.map(() => []);

  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);
      }
    }
  }

  // Calculate width for each column (use max width in column)
  return columns.map((col) => {
    if (col.length === 0) return 0;
    const widths = col.map((el) => el.width);
    return roundToCommonValue(Math.max(...widths));
  });
}

/**
 * Calculate track heights (row heights)
 */
function calculateTrackHeights(rows: ElementRect[][]): number[] {
  return rows.map((row) => {
    if (row.length === 0) return 0;
    const heights = row.map((el) => el.height);
    return roundToCommonValue(Math.max(...heights));
  });
}

/**
 * Build cell map - maps grid positions to element indices
 */
function buildCellMap(
  rows: ElementRect[][],
  alignedPositions: number[],
  tolerance: number = 3,
): (number | null)[][] {
  const cellMap: (number | null)[][] = [];

  for (const row of rows) {
    const rowCells: (number | null)[] = new Array(alignedPositions.length).fill(null);

    for (const el of row) {
      const colIndex = alignedPositions.findIndex((pos) => Math.abs(el.x - pos) <= tolerance);
      if (colIndex >= 0) {
        rowCells[colIndex] = el.index;
      }
    }

    cellMap.push(rowCells);
  }

  return cellMap;
}

/**
 * Calculate grid confidence score
 */
function calculateGridConfidence(
  rows: ElementRect[][],
  columnAlignment: ColumnAlignmentResult,
  rowGapAnalysis: { isConsistent: boolean; rounded: number },
  columnGapAnalysis: { isConsistent: boolean; rounded: number },
): number {
  let score = 0;

  // 1. Multiple rows required for grid (0.3 max)
  if (rows.length >= 2) score += 0.2;
  if (rows.length >= 3) score += 0.1;

  // 2. Consistent column count across rows (0.2)
  const columnCounts = rows.map((r) => r.length);
  const allSameCount = columnCounts.every((c) => c === columnCounts[0]);
  if (allSameCount && columnCounts[0] >= 2) score += 0.2;

  // 3. Columns aligned across rows (0.25)
  if (columnAlignment.isAligned) score += 0.25;

  // 4. Consistent row gap (0.1)
  if (rowGapAnalysis.isConsistent) score += 0.1;

  // 5. Consistent column gap (0.1)
  if (columnGapAnalysis.isConsistent) score += 0.1;

  // 6. Grid fill ratio - elements fill most of expected cells (0.05)
  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);
}

/**
 * Detect grid layout from element rectangles
 *
 * Grid detection criteria:
 * - Multiple rows (2+)
 * - Columns aligned across rows
 * - Consistent column count per row
 * - Consistent gaps
 */
export function detectGridLayout(rects: ElementRect[]): GridAnalysisResult {
  const emptyResult: GridAnalysisResult = {
    isGrid: false,
    confidence: 0,
    rowCount: 0,
    columnCount: 0,
    rowGap: 0,
    columnGap: 0,
    isRowGapConsistent: false,
    isColumnGapConsistent: false,
    trackWidths: [],
    trackHeights: [],
    alignedColumnPositions: [],
    rows: [],
    cellMap: [],
  };

  // Need at least 4 elements for a meaningful grid (2x2)
  if (rects.length < 4) {
    return emptyResult;
  }

  // Step 1: Group into rows
  const rows = groupIntoRows(rects, 2);

  // Need at least 2 rows for grid
  if (rows.length < 2) {
    return emptyResult;
  }

  // Step 2: Check column alignment
  const columnAlignment = checkColumnAlignment(rows, 3);

  // Step 3: Analyze row gaps
  const rowGaps = calculateRowGaps(rows);
  const rowGapAnalysis = analyzeGaps(rowGaps);

  // Step 4: Analyze column gaps
  const columnGaps = calculateColumnGaps(columnAlignment.alignedPositions, rows);
  const columnGapAnalysis = analyzeGaps(columnGaps);

  // Step 5: Calculate confidence
  const confidence = calculateGridConfidence(
    rows,
    columnAlignment,
    rowGapAnalysis,
    columnGapAnalysis,
  );

  // Grid threshold: confidence >= 0.6
  const isGrid = confidence >= 0.6;

  if (!isGrid) {
    return {
      ...emptyResult,
      confidence,
      rows,
      rowCount: rows.length,
    };
  }

  // Step 6: Calculate track sizes
  const trackWidths = calculateTrackWidths(rows, columnAlignment.alignedPositions);
  const trackHeights = calculateTrackHeights(rows);

  // Step 7: Build cell map
  const cellMap = buildCellMap(rows, columnAlignment.alignedPositions);

  return {
    isGrid: true,
    confidence,
    rowCount: rows.length,
    columnCount: columnAlignment.columnCount,
    rowGap: rowGapAnalysis.rounded,
    columnGap: columnGapAnalysis.rounded,
    isRowGapConsistent: rowGapAnalysis.isConsistent,
    isColumnGapConsistent: columnGapAnalysis.isConsistent,
    trackWidths,
    trackHeights,
    alignedColumnPositions: columnAlignment.alignedPositions,
    rows,
    cellMap,
  };
}

// ==================== Debug and Visualization ====================

/**
 * Generate layout analysis report (for debugging)
 */
export function generateLayoutReport(rects: ElementRect[]): string {
  const analysis = analyzeLayout(rects);
  const tree = buildLayoutTree(rects);

  const lines: string[] = [
    "=== Layout Analysis Report ===",
    "",
    `Element count: ${rects.length}`,
    `Detected direction: ${analysis.direction} (confidence: ${(analysis.confidence * 100).toFixed(1)}%)`,
    `Gap: ${analysis.gap}px (consistent: ${analysis.isGapConsistent ? "yes" : "no"})`,
    `justifyContent: ${analysis.justifyContent}`,
    `alignItems: ${analysis.alignItems}`,
    "",
    `Row groups: ${analysis.rows.length} rows`,
    ...analysis.rows.map(
      (row, i) => `  Row ${i + 1}: ${row.length} elements [${row.map((r) => r.index).join(", ")}]`,
    ),
    "",
    `Column groups: ${analysis.columns.length} columns`,
    ...analysis.columns.map(
      (col, i) =>
        `  Column ${i + 1}: ${col.length} elements [${col.map((r) => r.index).join(", ")}]`,
    ),
    "",
    `Overlapping elements: ${analysis.overlappingElements.length}`,
    "",
    "=== Layout Tree ===",
    JSON.stringify(tree, null, 2),
  ];

  return lines.join("\n");
}

```

--------------------------------------------------------------------------------
/docs/en/grid-layout-research.md:
--------------------------------------------------------------------------------

```markdown
# Grid Layout Detection Algorithm Research

## Research Summary

This document synthesizes research findings from multiple sources to design a Grid layout detection algorithm for converting Figma designs to CSS Grid.

## Sources Analyzed

### 1. Academic Research

**"A Layout Inference Algorithm for GUIs" (ScienceDirect)**

- Uses **Allen's Interval Algebra** for spatial relationship modeling
- Two-phase algorithm:
  1. Convert absolute coordinates to relative positioning using directed graphs
  2. Apply pattern matching and graph rewriting for layout inference
- Achieves 97% accuracy in maintaining original layout faithfulness
- 84% of views maintain proportions when resized

**Allen's 13 Interval Relations** (applicable to 1D spatial analysis):

- `before/after`: Element A completely before/after B
- `meets/met-by`: Element A ends exactly where B starts
- `overlaps/overlapped-by`: Element A partially overlaps B
- `starts/started-by`: Element A starts at same position as B
- `finishes/finished-by`: Element A ends at same position as B
- `during/contains`: Element A completely inside/outside B
- `equals`: Element A same position as B

### 2. Open Source Implementations

**FigmaToCode (bernaferrari/FigmaToCode)**

- Uses intermediate "AltNodes" representation
- Analyzes auto-layouts, responsive constraints, color variables
- Handles mixed positioning (absolute + auto-layout)
- Makes intelligent decisions about code structure

**imgcook (Alibaba)**

- Layout restoration is "core of entire D2C process"
- Uses flat JSON with absolute position, size, style
- Expert rule system + machine learning hybrid
- Components: Page splitting, Grouping, Loop detection, Multi-status handling
- Rule system preferred for controllable, near 100% availability

**GRIDS (Aalto University)**

- MILP (Mixed Integer Linear Programming) based approach
- Mathematical optimization for grid generation
- Python implementation with Gurobi optimizer

### 3. Industry Implementations

**Figma's Native Grid Auto-Layout (May 2025)**

- New Grid flow option alongside horizontal/vertical
- Supports span, row/column count
- Limited compared to full CSS Grid specification
- Missing: `fr` units, named grid areas, subgrid

**Screen Parsing (CMU - UIST 2021)**

- ML-based UI structure inference
- Detects 7 container types including grids
- Trained on 130K iOS + 80K Android screens

## Current Implementation Analysis

### Existing Flex Layout Detection

The current codebase has robust Flex detection in `src/algorithms/layout/detector.ts`:

```
Flow: Elements → Bounding Boxes → Row/Column Grouping → Gap Analysis → Alignment Detection → CSS Generation
```

**Key Algorithms:**

1. **Y-axis overlap** → Row grouping (elements in same row)
2. **X-axis overlap** → Column grouping (elements in same column)
3. **Gap analysis** → Consistent spacing detection (20% std dev tolerance)
4. **Alignment detection** → left/center/right/stretch
5. **Layout scoring** → Confidence-based direction selection

### Gap in Current Implementation

The `LayoutInfo` interface already defines `type: "flex" | "absolute" | "grid"` but Grid detection is **NOT IMPLEMENTED**.

## Grid Layout Detection Algorithm Design

### Core Concept: When to Use Grid vs Flex

| Criterion       | Flexbox                     | CSS Grid                           |
| --------------- | --------------------------- | ---------------------------------- |
| Dimension       | 1D (row OR column)          | 2D (rows AND columns)              |
| Rows consistent | Single row spans full width | Multiple rows with aligned columns |
| Column count    | Variable per row            | Consistent across rows             |
| Cell alignment  | Only within row             | Both row AND column aligned        |
| Gaps            | Single gap value            | row-gap and column-gap             |

### Algorithm: Grid Detection

```typescript
interface GridAnalysisResult {
  isGrid: boolean;
  confidence: number;
  rows: number;
  columns: number;
  rowGap: number;
  columnGap: number;
  trackWidths: number[]; // For grid-template-columns
  trackHeights: number[]; // For grid-template-rows
  cellMap: (number | null)[][]; // Element indices in grid positions
}

function detectGridLayout(rects: ElementRect[]): GridAnalysisResult {
  // Step 1: Group into rows (Y-axis overlap)
  const rows = groupIntoRows(rects);

  // Step 2: Check if column count is consistent across rows
  const columnCounts = rows.map((row) => row.length);
  const isConsistentColumns = areValuesAligned(columnCounts, 0);

  // Step 3: Check column alignment across rows
  const columnPositions = extractColumnPositions(rows);
  const areColumnsAligned = checkColumnAlignment(columnPositions);

  // Step 4: Calculate confidence
  // Grid confidence higher when:
  // - Multiple rows exist (> 1)
  // - Columns are aligned across rows
  // - Both row and column gaps are consistent

  // Step 5: Extract track sizes
  const trackWidths = calculateTrackWidths(rows, columnPositions);
  const trackHeights = calculateTrackHeights(rows);

  return result;
}
```

### Step-by-Step Algorithm

#### Step 1: Row Detection (existing)

```typescript
// Already implemented: groupIntoRows()
const rows = groupIntoRows(rects, tolerance);
```

#### Step 2: Column Alignment Detection (NEW)

```typescript
function extractColumnPositions(rows: ElementRect[][]): number[][] {
  // For each row, extract the X positions of elements
  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 };
  }

  // Merge all X positions and cluster them
  const allPositions = columnPositions.flat();
  const clusters = clusterValues(allPositions, tolerance);

  // Check if each row has elements at cluster positions
  const alignedPositions = clusters.map((c) => c.center);

  // Verify alignment across rows
  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,
  };
}
```

#### Step 3: Grid Confidence Scoring (NEW)

```typescript
function calculateGridConfidence(
  rows: ElementRect[][],
  columnAlignment: ColumnAlignmentResult,
  rowGapAnalysis: GapAnalysis,
  columnGapAnalysis: GapAnalysis,
): number {
  let score = 0;

  // 1. Multiple rows (required for grid)
  if (rows.length >= 2) score += 0.2;
  if (rows.length >= 3) score += 0.1;

  // 2. Consistent column count
  const columnCounts = rows.map((r) => r.length);
  if (areValuesEqual(columnCounts)) score += 0.2;

  // 3. Column alignment across rows
  if (columnAlignment.isAligned) score += 0.25;

  // 4. Consistent row gap
  if (rowGapAnalysis.isConsistent) score += 0.1;

  // 5. Consistent column gap
  if (columnGapAnalysis.isConsistent) score += 0.1;

  // 6. 2D regularity (elements form a regular matrix)
  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);
}
```

#### Step 4: Track Size Extraction (NEW)

```typescript
function calculateTrackWidths(
  rows: ElementRect[][],
  alignedPositions: number[],
): (number | "auto" | "fr")[] {
  // Group elements by column position
  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);
      }
    }
  }

  // Calculate width for each column
  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);
  });
}
```

#### Step 5: CSS Grid Generation (NEW)

```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 (optional, often 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;
}
```

### Decision Tree: Grid vs Flex vs Absolute

```
                    Start
                      │
                      ▼
              ┌─────────────────┐
              │ Elements ≥ 2?   │──No──▶ position: absolute
              └────────┬────────┘
                       │Yes
                       ▼
              ┌─────────────────┐
              │ Overlapping     │──Yes─▶ position: absolute (for overlaps)
              │ elements?       │
              └────────┬────────┘
                       │No
                       ▼
              ┌─────────────────┐
              │ Multiple rows   │──No──▶ display: flex (row)
              │ detected?       │
              └────────┬────────┘
                       │Yes
                       ▼
              ┌─────────────────┐
              │ Columns aligned │──No──▶ display: flex (column)
              │ across rows?    │        with nested flex rows
              └────────┬────────┘
                       │Yes
                       ▼
              ┌─────────────────┐
              │ Grid confidence │──No──▶ display: flex (column)
              │ ≥ 0.6?          │
              └────────┬────────┘
                       │Yes
                       ▼
                  display: grid
```

## Additional CSS Properties to Support

### Grid-Specific Properties

| Property                         | Priority | Description                       |
| -------------------------------- | -------- | --------------------------------- |
| `display: grid`                  | P0       | Enable grid layout                |
| `grid-template-columns`          | P0       | Define column tracks              |
| `grid-template-rows`             | P1       | Define row tracks                 |
| `gap` / `row-gap` / `column-gap` | P0       | Spacing between tracks            |
| `grid-auto-flow`                 | P2       | Auto-placement algorithm          |
| `justify-items`                  | P1       | Align items in cells horizontally |
| `align-items`                    | P1       | Align items in cells vertically   |
| `place-items`                    | P2       | Shorthand for align + justify     |

### Child Element Properties

| Property       | Priority | Description               |
| -------------- | -------- | ------------------------- |
| `grid-column`  | P1       | Column span/position      |
| `grid-row`     | P1       | Row span/position         |
| `grid-area`    | P2       | Named grid area           |
| `justify-self` | P2       | Self horizontal alignment |
| `align-self`   | P2       | Self vertical alignment   |

### Enhanced Flex Properties (Missing)

| Property      | Priority | Description                     |
| ------------- | -------- | ------------------------------- |
| `flex-grow`   | P1       | Element growth factor           |
| `flex-shrink` | P2       | Element shrink factor           |
| `flex-basis`  | P2       | Initial size before grow/shrink |
| `flex`        | P1       | Shorthand (grow shrink basis)   |
| `order`       | P2       | Element ordering                |

## Implementation Plan

### Phase 1: Fix Existing TODO (layout.ts)

Split `convertAlign` into two functions:

- `convertJustifyContent()` - for main axis alignment
- `convertAlignItems()` - for cross axis alignment

### Phase 2: Add Grid Detection (detector.ts)

1. Add `GridAnalysisResult` interface
2. Implement `detectGridLayout()` function
3. Add column alignment detection
4. Implement grid confidence scoring
5. Add track size extraction

### Phase 3: CSS Generation (optimizer.ts)

1. Update `LayoutInfo` usage to include grid type
2. Add `generateGridCSS()` function
3. Integrate into `optimizeDesign()` pipeline
4. Add decision tree for grid vs flex

### Phase 4: Type Updates (simplified.ts)

1. Add Grid CSS properties to `CSSStyle`
2. Extend `LayoutInfo` for grid-specific data

### Phase 5: Testing

1. Add unit tests for grid detection
2. Add integration tests with real Figma data
3. Test edge cases (irregular grids, mixed layouts)

## Current Implementation Issues (2024-12 Analysis)

### Problem: Mixed Layout False Positives

Testing with real Figma data revealed that the Grid detection algorithm incorrectly identifies mixed layouts as grids.

**Example Case: Keywords Management Panel (node-402-34955)**

```
Container: 1580px × 340px
Children:
  1. Tabs (320×41)        left: 630px   top: 0px    ← centered tabs
  2. Divider (1580×1)     left: 0px     top: 41px   ← full-width line
  3. Info Bar (1528×88)   left: 26px    top: 62px   ← nearly full-width
  4. Card 1 (500×78)      left: 26px    top: 170px  ┐
  5. Card 2 (500×78)      left: 540px   top: 170px  ├─ actual grid candidates
  6. Card 3 (500×78)      left: 1054px  top: 170px  ┘
  7. Card 4 (500×78)      left: 26px    top: 262px  ← second row
```

**Detected Result (WRONG):**

```css
display: grid;
grid-template-columns: 1580px 1528px 500px 320px 500px; /* ❌ Sum = 4428px > 1580px */
```

**Expected Result:**

- Overall container: `flex-direction: column` or `position: absolute`
- Cards only (items 4-7): `display: grid; grid-template-columns: repeat(3, 500px);`

### Root Cause Analysis

| Issue                           | Code Location         | Description                                            |
| ------------------------------- | --------------------- | ------------------------------------------------------ |
| **No element type filtering**   | `detector.ts:1073`    | All children analyzed together regardless of size/type |
| **Y-overlap only row grouping** | `detector.ts:154-185` | 2px tolerance ignores visual/functional differences    |
| **No homogeneity check**        | N/A                   | Missing validation that elements "look similar"        |
| **Strict column alignment**     | `detector.ts:908-911` | 80% threshold fails on intentionally mixed layouts     |

### Research: Industry Approaches

#### 1. UI Semantic Group Detection (2024)

> Source: [arxiv.org/html/2403.04984v1](https://arxiv.org/html/2403.04984v1)

- **Key Insight**: "Group adjacent elements with similar semantics before layout detection"
- **Method**: Transformer-based detector using Gestalt principles
- **Relevance**: Pre-clustering homogeneous elements

#### 2. UIHASH - Grid-Based UI Similarity

> Source: [jun-zeng.github.io](https://jun-zeng.github.io/file/uihash_paper.pdf)

- **Key Insight**: "Proximity principle - users group adjacent elements as unified entity"
- **Method**: Partition screen into regions, encode by constituent controls
- **Relevance**: Size-based clustering before grid analysis

#### 3. GUI Layout Inference Algorithm

> Source: [ScienceDirect](https://www.sciencedirect.com/science/article/abs/pii/S0950584915001718)

- **Key Insight**: "Two-phase: relative positioning → pattern matching"
- **Method**: Allen relations + exploratory algorithm for layout composition
- **Relevance**: Hierarchical layout detection

#### 4. Multilevel Homogeneity Structure

> Source: [ScienceDirect](https://www.sciencedirect.com/science/article/abs/pii/S0957417417303469)

- **Key Insight**: "Bottom-up aggregation into homogeneous regions"
- **Method**: Connected components → words → text lines → regions
- **Relevance**: Progressive homogeneous grouping

## Optimization Plan

### Solution: Homogeneous Element Pre-filtering

Add a pre-filtering step before Grid detection to identify elements that "look similar" and should be considered together.

#### Homogeneity Criteria

```typescript
interface HomogeneityCheck {
  sizeVariance: number; // Width/height variance (threshold: 20%)
  typeConsistency: boolean; // Same node types
  stylesSimilar: boolean; // Similar CSS properties
}

function isHomogeneousGroup(nodes: SimplifiedNode[]): boolean {
  if (nodes.length < 4) return false;

  // 1. Size clustering - width/height within 20% variance
  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. Type consistency - allow 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. Style similarity (optional) - background, border, etc.
  // ...

  return true;
}
```

#### Updated Detection Flow

```
Before:
  detectGridLayout(allChildren) → wrong grid

After:
  1. clusterBySimilarSize(allChildren) → size groups
  2. For each group with 4+ elements:
     a. isHomogeneousGroup(group) → true/false
     b. If true: detectGridLayout(group)
  3. Remaining elements: use flex/absolute
```

#### Implementation Steps

1. **Add `isHomogeneousGroup()` function** - `detector.ts`
2. **Add `clusterBySimilarSize()` function** - `detector.ts`
3. **Update `LayoutOptimizer.optimizeContainer()`** - `optimizer.ts`
   - Call homogeneity check before grid detection
   - Only pass homogeneous elements to `detectGridLayout()`
4. **Add tests** - `layout.test.ts`

### Expected Results

| Scenario                      | Before             | After                |
| ----------------------------- | ------------------ | -------------------- |
| Mixed layout (tabs + cards)   | Wrong grid for all | Cards only → grid    |
| Pure card grid (4+ same size) | Correct grid       | Correct grid         |
| Single row items              | Flex row           | Flex row (no change) |
| Irregular sizes               | Wrong grid         | Flex/absolute        |

## References

- [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/)

---

## Complete Implementation Analysis

This section provides an in-depth analysis of the Grid layout detection algorithm implementation, including the complete call chain and core functions.

### System Architecture: Grid vs Flex Decision

```
┌─────────────────────────────────────────────────────────────────────────┐
│                    Layout Detection Decision Tree                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│                    ┌─────────────────────────┐                          │
│                    │  optimizeContainer()    │                          │
│                    │  [optimizer.ts:89]      │                          │
│                    └───────────┬─────────────┘                          │
│                                │                                         │
│            ┌───────────────────┼───────────────────┐                    │
│            ▼                   ▼                   ▼                    │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐         │
│  │ STEP 1: Overlap │  │ STEP 1.5:      │  │ STEP 2: Grid   │         │
│  │ Detection       │  │ Background     │  │ Detection      │         │
│  │ IoU > 0.1 ?     │  │ Detection      │  │ (Priority)     │         │
│  └────────┬────────┘  └────────────────┘  └────────┬────────┘         │
│           │                                         │                   │
│           │                    ┌────────────────────┤                   │
│           │                    │                    │                   │
│           │            Grid detection OK?       Grid detection failed   │
│           │            confidence >= 0.6            │                   │
│           │                    │                    │                   │
│           │                    ▼                    ▼                   │
│           │           ┌─────────────────┐  ┌─────────────────┐         │
│           │           │  display: grid  │  │ STEP 3: Flex   │         │
│           │           │  grid-template- │  │ Detection      │         │
│           │           │  columns/rows   │  │ (Fallback)     │         │
│           │           └─────────────────┘  └────────┬────────┘         │
│           │                    │                    │                   │
│           │                    │            Flex detection OK?          │
│           │                    │            score > 0.4                 │
│           │                    │                    │                   │
│           │                    ▼                    ▼                   │
│           │           ┌─────────────────┐  ┌─────────────────┐         │
│  Overlapping keeps    │  Generate Grid  │  │  display: flex  │         │
│  position: absolute   │  CSS + padding  │  │  + gap + align  │         │
│           │           └─────────────────┘  └─────────────────┘         │
│           │                                                             │
└───────────┴─────────────────────────────────────────────────────────────┘
```

### Grid Detection Entry: detectGridIfApplicable

```
detectGridIfApplicable(nodes) - [optimizer.ts:918-961]
═══════════════════════════════════════════════════════════════════════════

                    ┌─────────────────────────────────┐
                    │  detectGridIfApplicable(nodes)  │
                    │  [optimizer.ts:918]             │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
                    ┌─────────────────────────────────┐
                    │  Pre-checks                      │
                    │  • nodes.length < 4 → return null│
                    │  • Need at least 2×2 grid        │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
                    ┌─────────────────────────────────┐
                    │  nodesToElementRects(nodes)     │
                    │  Convert to ElementRect[]       │
                    │  [optimizer.ts:856-870]         │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
                    ┌─────────────────────────────────────────────────────┐
                    │  filterHomogeneousForGrid(elementRects, nodeTypes)  │
                    │  [detector.ts:1216-1235]                            │
                    │                                                      │
                    │  ★ Key step: Homogeneity filtering                  │
                    │  • Filter elements with similar sizes                │
                    │  • Prevent mixed layouts from being detected as Grid │
                    └───────────────────────────┬─────────────────────────┘
                                                │
                              ┌─────────────────┴─────────────────┐
                              │                                   │
                    homogeneousElements.length < 4        homogeneousElements >= 4
                              │                                   │
                              ▼                                   ▼
                        return null                  ┌─────────────────────────┐
                                                     │  detectGridLayout()     │
                                                     │  [detector.ts:1490]     │
                                                     │  Run Grid detection on  │
                                                     │  homogeneous elements   │
                                                     └───────────────┬─────────┘
                                                                     │
                              ┌───────────────────────────────────────┤
                              │                                       │
                    isGrid && confidence >= 0.6 ?              confidence < 0.6
                    rowCount >= 2 && columnCount >= 2                 │
                              │                                       │
                              ▼                                       ▼
                    ┌─────────────────────────┐              return null
                    │  return {               │
                    │    gridResult,          │
                    │    gridIndices          │
                    │  }                      │
                    └─────────────────────────┘
```

### Homogeneity Analysis: analyzeHomogeneity

```
analyzeHomogeneity(rects, nodeTypes) - [detector.ts:1127-1196]
═══════════════════════════════════════════════════════════════════════════

Purpose: Ensure only "similar" elements participate in Grid detection,
         avoiding misdetection of mixed layouts

Mixed Layout Example (should be filtered):
┌──────────────────────────────────────────────────────────────────────────┐
│ Container 1580px × 340px                                                 │
│                                                                          │
│        ┌─────────────────────┐   ← Tabs 320×41 (heterogeneous)          │
│        │      Tabs           │                                           │
│        └─────────────────────┘                                           │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   ← Divider 1580×1 (heterogeneous)      │
│ ┌──────────────────────────────────────────────────────────────────────┐│
│ │                    Info Bar 1528×88                                  ││
│ └──────────────────────────────────────────────────────────────────────┘│
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   ← Homogeneous (Grid)│
│  │  Card 500×78│  │  Card 500×78│  │  Card 500×78│                      │
│  └─────────────┘  └─────────────┘  └─────────────┘                      │
│  ┌─────────────┐                                                         │
│  │  Card 500×78│                                                         │
│  └─────────────┘                                                         │
└──────────────────────────────────────────────────────────────────────────┘

Algorithm Flow:
┌────────────────────────────────────────────────────────────────┐
│  1. Size Clustering (clusterBySimilarSize)                     │
│                                                                 │
│     Input: All child ElementRect[]                             │
│     Tolerance: 20% (widthDiff <= 0.2 && heightDiff <= 0.2)     │
│                                                                 │
│     Process:                                                    │
│     for each rect:                                              │
│       Find existing cluster with similar size                   │
│       If found → add to that cluster                            │
│       If not found → create new cluster                         │
│                                                                 │
│     Output: SizeCluster[] (sorted by element count descending)  │
│                                                                 │
│  Example:                                                       │
│     Cluster 1: [Card1, Card2, Card3, Card4] ← 500×78           │
│     Cluster 2: [InfoBar]                    ← 1528×88          │
│     Cluster 3: [Tabs]                       ← 320×41           │
│     Cluster 4: [Divider]                    ← 1580×1           │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  2. Get Largest Cluster (largestCluster)                       │
│                                                                 │
│     If largestCluster.length < 4 → not homogeneous              │
│                                                                 │
│  Example: [Card1, Card2, Card3, Card4] = 4 elements ✓          │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  3. Calculate Coefficient of Variation                         │
│                                                                 │
│     widthCV  = stddev(widths) / mean(widths)                   │
│     heightCV = stddev(heights) / mean(heights)                 │
│                                                                 │
│     If widthCV > 0.2 || heightCV > 0.2 → not homogeneous       │
│                                                                 │
│  Example:                                                       │
│     widths = [500, 500, 500, 500] → CV = 0 ✓                   │
│     heights = [78, 78, 78, 78]   → CV = 0 ✓                    │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  4. Type Consistency Check (optional)                          │
│                                                                 │
│     Allowed types: FRAME, INSTANCE, COMPONENT, GROUP, RECTANGLE│
│     If other types exist → not homogeneous                      │
│                                                                 │
│  Example: All Cards are FRAME → pass ✓                         │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  Output: HomogeneityResult                                      │
│  {                                                              │
│    isHomogeneous: true,                                         │
│    widthCV: 0,                                                  │
│    heightCV: 0,                                                 │
│    types: ['FRAME'],                                            │
│    homogeneousElements: [Card1, Card2, Card3, Card4],          │
│    outlierElements: [Tabs, Divider, InfoBar]                   │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
```

### Grid Detection Core: detectGridLayout

```
detectGridLayout(rects) - [detector.ts:1490-1573]
═══════════════════════════════════════════════════════════════════════════

                    ┌─────────────────────────────────┐
                    │  detectGridLayout(rects)        │
                    │  Input: Filtered homogeneous    │
                    │         elements                │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  STEP 1: Row Grouping (groupIntoRows)                          │
│  [detector.ts:1513]                                             │
│                                                                 │
│  Use Y-axis overlap detection to group elements into "rows"    │
│                                                                 │
│  Input (4 cards):                                               │
│      ┌────┐   ┌────┐   ┌────┐                                  │
│      │ 1  │   │ 2  │   │ 3  │   y: 170, height: 78             │
│      └────┘   └────┘   └────┘                                  │
│      ┌────┐                                                     │
│      │ 4  │                     y: 262, height: 78             │
│      └────┘                                                     │
│                                                                 │
│  Output: rows = [[1, 2, 3], [4]]                                │
│          rowCount = 2                                           │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  STEP 2: Column Alignment Detection (checkColumnAlignment)     │
│  [detector.ts:1305-1339]                                        │
│                                                                 │
│  Check if X positions across rows are aligned                  │
│                                                                 │
│  1. Extract all X positions:                                    │
│     Row 1: [26, 540, 1054]                                      │
│     Row 2: [26]                                                 │
│                                                                 │
│  2. Cluster all X positions (tolerance=3px):                    │
│     Cluster 1: center=26                                        │
│     Cluster 2: center=540                                       │
│     Cluster 3: center=1054                                      │
│                                                                 │
│  3. Verify row elements are at cluster positions:               │
│     Row 1: [26 ≈ 26 ✓, 540 ≈ 540 ✓, 1054 ≈ 1054 ✓]             │
│     Row 2: [26 ≈ 26 ✓]                                          │
│                                                                 │
│  Output: { isAligned: true, alignedPositions: [26, 540, 1054] } │
│          columnCount = 3                                        │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  STEP 3: Gap Analysis                                           │
│  [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 Confidence Calculation (calculateGridConfidence) │
│  [detector.ts:1446-1479]                                        │
│                                                                 │
│  6 Scoring Factors:                                             │
│                                                                 │
│  ┌────────────────────────────────────────────────────────────┐│
│  │ Factor                       │ Condition          │ Score  ││
│  ├────────────────────────────────────────────────────────────┤│
│  │ 1. Multiple rows (>= 2)      │ 2 rows            │ +0.2   ││
│  │ 2. Multiple rows (>= 3)      │ 2 rows < 3        │ +0.0   ││
│  │ 3. Consistent column count   │ [3, 1] inconsistent│ +0.0   ││
│  │ 4. Column alignment          │ isAligned = true  │ +0.25  ││
│  │ 5. Row gap consistency       │ isConsistent = true│ +0.1  ││
│  │ 6. Column gap consistency    │ isConsistent = true│ +0.1  ││
│  │ 7. Fill rate >= 75%          │ 4/6 = 67% < 75%   │ +0.0   ││
│  └────────────────────────────────────────────────────────────┘│
│                                                                 │
│  Total: 0.2 + 0.25 + 0.1 + 0.1 = 0.65                          │
│  Threshold: 0.6                                                 │
│  Result: 0.65 >= 0.6 → isGrid = true ✓                         │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  STEP 5-6: Track Size Calculation                              │
│  [detector.ts:1552-1557]                                        │
│                                                                 │
│  trackWidths  = calculateTrackWidths(rows, alignedPositions)   │
│              = [500, 500, 500]                                 │
│                                                                 │
│  trackHeights = calculateTrackHeights(rows)                    │
│              = [78, 78]                                        │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  STEP 7: Cell Map Building (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 Generation

```
generateGridCSS(gridResult) - [optimizer.ts:875-913]
═══════════════════════════════════════════════════════════════════════════

Input: GridAnalysisResult
Output: CSS Grid style object

                    ┌─────────────────────────────────┐
                    │  GridAnalysisResult             │
                    │  {                              │
                    │    isGrid: true,                │
                    │    rowCount: 2,                 │
                    │    columnCount: 3,              │
                    │    rowGap: 16,                  │
                    │    columnGap: 16,               │
                    │    trackWidths: [500, 500, 500],│
                    │    trackHeights: [78, 78]       │
                    │  }                              │
                    └───────────────┬─────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  Generate grid-template-columns                                │
│                                                                 │
│  trackWidths = [500, 500, 500]                                 │
│  → "500px 500px 500px"                                         │
│                                                                 │
│  (Can be optimized to repeat(3, 500px) if all same width)      │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  Generate grid-template-rows (only if heights differ)          │
│                                                                 │
│  trackHeights = [78, 78]                                       │
│  All row heights same → don't set (use auto)                   │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  Generate gap                                                   │
│                                                                 │
│  rowGap = 16, columnGap = 16                                   │
│  rowGap === columnGap → gap: "16px"                            │
│                                                                 │
│  If different → gap: "${rowGap}px ${columnGap}px"              │
└────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────┐
│  Output CSS:                                                    │
│  {                                                              │
│    display: "grid",                                             │
│    gridTemplateColumns: "500px 500px 500px",                   │
│    gap: "16px"                                                  │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
```

### Core Data Structures

```typescript
// Grid-related data structures in detector.ts
═══════════════════════════════════════════════════════════════════════════

// Grid analysis result
interface GridAnalysisResult {
  isGrid: boolean;              // whether valid Grid detected
  confidence: number;           // confidence (0-1)
  rowCount: number;             // row count
  columnCount: number;          // column count
  rowGap: number;               // row gap (px)
  columnGap: number;            // column gap (px)
  isRowGapConsistent: boolean;  // is row gap consistent
  isColumnGapConsistent: boolean; // is column gap consistent
  trackWidths: number[];        // column widths [col0Width, col1Width, ...]
  trackHeights: number[];       // row heights [row0Height, row1Height, ...]
  alignedColumnPositions: number[]; // aligned column X coordinates
  rows: ElementRect[][];        // grouped row data
  cellMap: (number | null)[][]; // cell to element index mapping
}

// Homogeneity analysis result
interface HomogeneityResult {
  isHomogeneous: boolean;       // is homogeneous
  widthCV: number;              // width coefficient of variation
  heightCV: number;             // height coefficient of variation
  types: string[];              // element type list
  homogeneousElements: ElementRect[]; // homogeneous elements
  outlierElements: ElementRect[];     // outlier elements (not in Grid)
}

// Size cluster
interface SizeCluster {
  width: number;                // cluster representative width
  height: number;               // cluster representative height
  elements: ElementRect[];      // elements in this cluster
  types?: string[];             // element types
}
```

### File Path Mapping

| Module                     | File Path                            | Line Range |
| -------------------------- | ------------------------------------ | ---------- |
| Grid entry                 | `src/algorithms/layout/optimizer.ts` | 918-961    |
| Grid CSS generation        | `src/algorithms/layout/optimizer.ts` | 875-913    |
| Homogeneity filtering      | `src/algorithms/layout/detector.ts`  | 1216-1235  |
| Homogeneity analysis       | `src/algorithms/layout/detector.ts`  | 1127-1196  |
| Size clustering            | `src/algorithms/layout/detector.ts`  | 1077-1116  |
| CV calculation             | `src/algorithms/layout/detector.ts`  | 1057-1067  |
| Grid detection core        | `src/algorithms/layout/detector.ts`  | 1490-1573  |
| Column alignment detection | `src/algorithms/layout/detector.ts`  | 1305-1339  |
| Row gap calculation        | `src/algorithms/layout/detector.ts`  | 1344-1356  |
| Column gap calculation     | `src/algorithms/layout/detector.ts`  | 1361-1376  |
| Confidence calculation     | `src/algorithms/layout/detector.ts`  | 1446-1479  |
| Track width calculation    | `src/algorithms/layout/detector.ts`  | 1381-1404  |
| Track height calculation   | `src/algorithms/layout/detector.ts`  | 1409-1415  |
| Cell map building          | `src/algorithms/layout/detector.ts`  | 1420-1441  |

---

_Last updated: 2025-12-06_

```
Page 3/6FirstPrevNextLast