mta-mcp 3.10.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +2 -1
  2. package/standards/mcp-tools/sketch-mcp.md +14 -1
  3. package/standards/troubleshooting-cases/flutter/text-vertical-centering-strutstyle.md +161 -0
  4. package/standards/workflows/design-restoration-guide.md +4 -2
  5. package/templates/design-measurement/component-measurement.js +72 -19
  6. package/templates/design-measurement/gap-measurement.js +83 -48
  7. package/templates/design-measurement/style-extraction.js +13 -0
  8. package/troubleshooting/README.md +368 -0
  9. package/troubleshooting/USAGE_GUIDE.md +289 -0
  10. package/troubleshooting/flutter/clip-/351/230/264/345/275/261/350/243/201/345/211/252.md +244 -0
  11. package/troubleshooting/flutter/component-/351/200/232/347/224/250/345/214/226/346/217/220/345/217/226.md +269 -0
  12. package/troubleshooting/flutter/input-/345/255/227/346/256/265/347/274/272/345/244/261.md +240 -0
  13. package/troubleshooting/flutter/input-/350/276/271/346/241/206/351/227/256/351/242/230.md +236 -0
  14. package/troubleshooting/flutter/layout-/345/260/272/345/257/270/344/270/215/345/214/271/351/205/215.md +214 -0
  15. package/troubleshooting/flutter/shadow-/351/200/217/345/207/272/351/227/256/351/242/230.md +172 -0
  16. package/troubleshooting/flutter/sketch-overflow-container.md +200 -0
  17. package/troubleshooting/flutter/sketch-/345/210/227/350/241/250item/345/214/272/345/237/237.md +212 -0
  18. package/troubleshooting/flutter/sketch-/345/233/276/346/240/207/345/260/272/345/257/270.md +135 -0
  19. package/troubleshooting/flutter/sketch-/345/256/214/346/225/264/346/217/220/345/217/226.md +201 -0
  20. package/troubleshooting/flutter/sketch-/345/261/236/346/200/247/346/234/252/344/275/277/347/224/250.md +139 -0
  21. package/troubleshooting/flutter/sketch-/350/203/214/346/231/257/345/261/202/351/253/230/345/272/246.md +264 -0
  22. package/troubleshooting/flutter/svg-/345/220/253/350/203/214/346/231/257/350/267/257/345/276/204.md +172 -0
  23. package/troubleshooting/flutter/svg-/346/234/252/345/261/205/344/270/255.md +120 -0
  24. package/troubleshooting/flutter/svg-/351/242/234/350/211/262/345/274/202/345/270/270.md +117 -0
  25. package/troubleshooting/flutter/tabbar-/345/212/250/347/224/273/345/220/214/346/255/245.md +107 -0
  26. package/troubleshooting/flutter/withopacity-/345/274/203/347/224/250.md +81 -0
  27. package/troubleshooting/vue3/cascader-/350/257/257/346/233/277/346/215/242.md +130 -0
  28. package/troubleshooting/vue3/drawer-input-/346/240/267/345/274/217.md +181 -0
  29. package/troubleshooting/vue3/table-/347/274/226/350/276/221/345/217/226/346/266/210.md +148 -0
  30. package/troubleshooting/vue3/table-/350/276/271/346/241/206/351/227/256/351/242/230.md +178 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mta-mcp",
3
- "version": "3.10.0",
3
+ "version": "3.10.1",
4
4
  "description": "MTA - 智能编码助手 MCP 服务器(规范 + 技能 + 诊断 + 模板 + 记忆 + 思考)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,6 +21,7 @@
21
21
  "standards/",
22
22
  "agents/",
23
23
  "templates/",
24
+ "troubleshooting/",
24
25
  "README.md"
25
26
  ],
26
27
  "scripts": {
@@ -20,6 +20,7 @@ Sketch MCP 是用于从 Sketch 设计文件中提取 UI 参数的工具。本文
20
20
  | **居中边距** | 上边距=8, 下边距=8 | 容器用 `alignment: center` |
21
21
  | **渐变方向** | from={x:0,y:0}, to={x:1,y:1} | Flutter `Alignment(-1,-1) → (1,1)` |
22
22
  | **透明度** | `#1C2B4580` | `0x80 = 128/255 = 0.5` |
23
+ | **Text topOffset** | 字号=24, 帧高=33 | `topOffset=4.5`, `StrutStyle.height=33/24=1.375` |
23
24
 
24
25
  ### 标准流程
25
26
 
@@ -44,9 +45,20 @@ const angle = Math.atan2(to.y - from.y, to.x - from.x) * 180 / Math.PI + 90;
44
45
 
45
46
  // 透明度(从 8 位十六进制)
46
47
  const alpha = parseInt(colorHex.slice(7, 9), 16) / 255;
48
+
49
+ // Sketch "适应布局" Text 帧高留白(topOffset)
50
+ // Sketch Text 图层在「适应布局」模式下:frameHeight ≈ fontSize × 1.375
51
+ // 文字视觉上沿不在 frame.y,而是 frame.y + topOffset
52
+ const topOffset = (frameHeight - fontSize) / 2; // 如 (33-24)/2 = 4.5px
53
+
54
+ // Flutter StrutStyle.height(消除 CJK 字体 ascent/descent 不对称导致的视觉偏下)
55
+ const strutHeight = frameHeight / fontSize; // 如 33/24 = 1.375
56
+ // 使用方式:StrutStyle(fontSize: fs, height: strutHeight, forceStrutHeight: true, leading: 0)
47
57
  ```
48
58
 
49
- > 📖 **详细案例**:参考 [TextField 垂直居中问题](../troubleshooting-cases/flutter/textfield-vertical-centering.md),展示了如何从测量数据计算行高和配置参数。
59
+ > 📖 **详细案例**:
60
+ > - [Text 普通文字垂直居中偏下(StrutStyle)](../troubleshooting-cases/flutter/text-vertical-centering-strutstyle.md)
61
+ > - [TextField 垂直居中问题](../troubleshooting-cases/flutter/textfield-vertical-centering.md)
50
62
 
51
63
  ---
52
64
 
@@ -1201,6 +1213,7 @@ function convertGradient(gradient, framework) {
1201
1213
  - [设计稿还原指南](../workflows/design-restoration-guide.md)
1202
1214
  - [测量模板](../../templates/design-measurement/README.md)
1203
1215
  - [TextField 居中案例](../troubleshooting-cases/flutter/textfield-vertical-centering.md)
1216
+ - [Text 普通文字垂直居中偏下(StrutStyle)](../troubleshooting-cases/flutter/text-vertical-centering-strutstyle.md)
1204
1217
 
1205
1218
  ---
1206
1219
 
@@ -0,0 +1,161 @@
1
+ # Text 普通文字垂直居中偏下问题
2
+
3
+ **问题标签**: `Text`, `居中`, `偏下`, `垂直对齐`, `StrutStyle`, `字体`, `行高`, `topOffset`
4
+ **问题类型**: `UI 对齐`
5
+ **严重程度**: 高
6
+ **节省时间**: 多轮对话 → 1 次修复
7
+
8
+ ---
9
+
10
+ ## 问题描述
11
+
12
+ 在 Row/Column/Container 中放置 `Text` widget,设计稿上文字在容器内垂直居中,但 Flutter 实际渲染时文字位置**视觉偏下**。
13
+
14
+ 典型场景:
15
+ - 邀请码卡片中的大号文字(fs=24)在 height=105 的 Row 里偏下
16
+ - 任何固定高度容器内的中文 Text 视觉上不居中
17
+
18
+ ---
19
+
20
+ ## 问题根因
21
+
22
+ ### 两重偏差叠加
23
+
24
+ **① Sketch "适应布局" 帧高留白(topOffset)**
25
+
26
+ Sketch Text 图层在"适应布局(Fit Content)"模式下:
27
+
28
+ ```
29
+ 帧高(frameHeight) ≈ fontSize × 1.375
30
+ topOffset = (frameHeight - fontSize) / 2
31
+ ```
32
+
33
+ 示例:fontSize=24 → frameHeight=33 → topOffset=4.5px
34
+
35
+ 设计稿测量的 Y 坐标是帧的起点,**实际文字视觉上沿 = Y + topOffset**。
36
+ 如果用原始 Y 坐标计算间距,偏差最大可达 4.5px。
37
+
38
+ **② Flutter 字体 ascent/descent 不对称**
39
+
40
+ Flutter 渲染 CJK 字体时,字形的上行距(ascent)默认大于下行距(descent),
41
+ 即使容器用 `crossAxisAlignment.center`,字形的视觉重心也会**偏下**。
42
+
43
+ 系统主动做补偿的唯一方式:通过 `StrutStyle.forceStrutHeight` 强制等量分配 leading。
44
+
45
+ ---
46
+
47
+ ## 测量数据解读
48
+
49
+ 使用 `artboard-measure.js` 运行后,Text 图层会输出:
50
+
51
+ ```json
52
+ {
53
+ "text": {
54
+ "content": "邀请码 :",
55
+ "fontSize": 24,
56
+ "topOffset": 4.5,
57
+ "visualHeight": 24
58
+ },
59
+ "layoutIntent": {
60
+ "visualSpacing": {
61
+ "note": "Text 图层存在 topOffset=4.5px,视觉上沿比 frame.y 低 4.5px",
62
+ "paddingTopVisual": 40.5,
63
+ "paddingBottomVisual": 40.5
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ `topOffset` 即是两重偏差中第一重的来源,同时也是 Flutter 修正所需的行高比例基准:
70
+
71
+ ```
72
+ StrutStyle.height = frameHeight / fontSize = 33 / 24 = 1.375
73
+ ```
74
+
75
+ ---
76
+
77
+ ## 正确方案
78
+
79
+ ### 从 Sketch 获取数值
80
+
81
+ 运行 `artboard-measure.js` 后查看:
82
+ - `text.fontSize`:字号
83
+ - `text.topOffset`:帧留白(= (frameHeight - fontSize) / 2)
84
+ - `layoutIntent.visualSpacing`:视觉修正后的实际间距
85
+
86
+ 计算 `StrutStyle.height`:
87
+
88
+ ```javascript
89
+ // 方式一:从 topOffset 反推
90
+ const strutHeight = (fontSize + topOffset * 2) / fontSize;
91
+ // 等价于:strutHeight = frameHeight / fontSize
92
+
93
+ // 方式二:直接用 frameHeight
94
+ const strutHeight = frameHeight / fontSize; // 通常约 1.375
95
+ ```
96
+
97
+ ### Flutter 代码
98
+
99
+ ```dart
100
+ Row(
101
+ crossAxisAlignment: CrossAxisAlignment.center,
102
+ children: [
103
+ Text(
104
+ '邀请码 :',
105
+ // forceStrutHeight 强制 leading 均匀分配于字形上下
106
+ // height = frameHeight / fontSize = 33 / 24 = 1.375
107
+ strutStyle: const StrutStyle(
108
+ fontSize: 24,
109
+ height: 1.375,
110
+ forceStrutHeight: true,
111
+ leading: 0,
112
+ ),
113
+ style: TextStyle(
114
+ fontSize: 24,
115
+ fontWeight: FontWeight.w600,
116
+ color: DesignColors.textPrimary,
117
+ ),
118
+ ),
119
+ ],
120
+ )
121
+ ```
122
+
123
+ ### 参数说明
124
+
125
+ | 参数 | 值 | 说明 |
126
+ |------|----|------|
127
+ | `fontSize` | 与 TextStyle 相同 | StrutStyle 必须指定字号 |
128
+ | `height` | `frameHeight / fontSize` ≈ 1.375 | 与 Sketch 帧高比例一致 |
129
+ | `forceStrutHeight` | `true` | 强制启用,否则 height 对 CJK 字体可能无效 |
130
+ | `leading` | `0` | 禁用额外行间距,让 height 完全控制 |
131
+
132
+ > ⚠️ **注意**:`StrutStyle` 只需设置在含文字的 `Text` widget 上,不是 `TextStyle`。
133
+ > `TextStyle.height` 控制行高,**不等于** `StrutStyle`,两者机制不同,不可混用。
134
+
135
+ ---
136
+
137
+ ## 与 TextField 居中问题的区别
138
+
139
+ | 场景 | 使用方案 | 关键参数 |
140
+ |------|---------|---------|
141
+ | **Text** 在固定容器里偏下 | `StrutStyle` | `forceStrutHeight: true, leading: 0` |
142
+ | **TextField** placeholder/光标偏下 | `style.height` + `contentPadding` | `isDense: true, contentPadding: EdgeInsets.zero` |
143
+
144
+ 两个问题表现相似,但修复路径完全不同,不可混用。
145
+
146
+ ---
147
+
148
+ ## 举一反三
149
+
150
+ 以下场景均可使用本方案:
151
+
152
+ - 大号标题文字在卡片中视觉偏下
153
+ - Icon + Text 水平排列,文字视觉位置比图标低
154
+ - 数字(Helvetica Bold)与中文单位(PingFang SC)混排时竖向不对齐
155
+ → 分别给数字文字和单位文字设置各自的 `StrutStyle`
156
+
157
+ ---
158
+
159
+ **维护团队**: MTA工作室
160
+ **创建日期**: 2026-02-25
161
+ **来源**: welfare_page.dart 邀请码卡片深度还原实战
@@ -111,6 +111,7 @@ Container: height=36, alignment=Alignment.center
111
111
  | 问题场景 | 关键属性 | 注意事项 |
112
112
  |----------|----------|----------|
113
113
  | TextField 居中 | `style.height`, `hintStyle.height` | 两者必须相同 |
114
+ | **Text 垂直居中偏下** | `StrutStyle` | `forceStrutHeight:true, leading:0, height=frameHeight/fontSize` |
114
115
  | 容器阴影 | `clipBehavior` | 设为 `Clip.none` 避免裁剪 |
115
116
  | Gap 间距 | `Gap()` vs `Gap.h()` | 默认垂直,`.h()` 水平 |
116
117
  | SVG 颜色 | `colorFilter` | 不要覆盖,保留原色 |
@@ -151,7 +152,8 @@ Container: height=36, alignment=Alignment.center
151
152
 
152
153
  - [ ] 已获取目标元素的**容器尺寸**
153
154
  - [ ] 已获取**所有子元素**的位置和尺寸
154
- - [ ] 已获取文本的**Y 坐标、高度、字号**
155
+ - [ ] 已获取文本的**Y 坐标、帧高、字号**(注意帧高 ≠ 字号,差值为 topOffset)
156
+ - [ ] Text 图层已计算 `topOffset = (frameHeight - fontSize) / 2`(Sketch 适应布局偏差)
155
157
  - [ ] 已**计算**行高倍数和边距
156
158
  - [ ] 已确认相关属性(style/hintStyle)**配置一致**
157
159
  - [ ] 已一次性配置**完整方案**
@@ -161,4 +163,4 @@ Container: height=36, alignment=Alignment.center
161
163
 
162
164
  **维护团队**: MTA工作室
163
165
  **创建日期**: 2026-01-19
164
- **版本**: v1.0
166
+ **版本**: v1.1(2026-02-25 新增 Text topOffset / StrutStyle 垂直居中修正)
@@ -1,52 +1,105 @@
1
1
  /**
2
- * 组件测量模板 - 自动计算相对边距
2
+ * 组件测量模板 - 自动计算相对边距(含文字垂直对齐修正)
3
3
  * 用途:测量单个组件的完整布局参数
4
- *
4
+ *
5
+ * 文字垂直修正说明:
6
+ * Sketch 中 Text 选择"适应布局"时,layer.frame.height = fontSize × 自动行高系数(≈1.375)。
7
+ * 文字像素内容在该高度内垂直居中,因此框架顶部到可视文字上沿存在偏移量:
8
+ * topOffset = (frameHeight - fontSize) / 2
9
+ *
10
+ * 相对边距中"视觉距父容器顶部"= 原始 paddingTop + topOffset,
11
+ * 这才是设计稿中视觉上希望体现的间距。
12
+ *
5
13
  * 使用方式:在 Sketch 中选中目标组件,运行此脚本
6
14
  */
7
15
  const sketch = require('sketch')
8
16
 
17
+ // 计算 Text 图层的垂直光学偏移量
18
+ // 返回 null 表示非文字图层或无需修正
19
+ function getTextVerticalOffset(layer) {
20
+ if (layer.type !== 'Text') return null
21
+ var fontSize = (layer.style && layer.style.fontSize) ? layer.style.fontSize : 0
22
+ if (!fontSize) return null
23
+ var frameH = layer.frame.height
24
+ var topOffset = (frameH - fontSize) / 2
25
+ return {
26
+ fontSize: fontSize,
27
+ frameHeight: frameH,
28
+ topOffset: topOffset,
29
+ // 视觉文字上沿相对于 frame 顶部的偏移(正值 = 文字内容比 frame 低 topOffset px)
30
+ visualTop: layer.frame.y + topOffset,
31
+ visualBottom: layer.frame.y + frameH - topOffset,
32
+ visualHeight: fontSize
33
+ }
34
+ }
35
+
9
36
  const sel = sketch.getSelectedDocument().selectedLayers.layers
10
37
  if (!sel.length) {
11
38
  console.log('请先选中一个组件')
12
39
  } else {
13
40
  const layer = sel[0]
14
41
  const parent = layer.parent
15
-
42
+ const textMetrics = getTextVerticalOffset(layer)
43
+
16
44
  // 基础信息
17
45
  console.log('=== 组件测量结果 ===')
18
46
  console.log(`名称: ${layer.name}`)
19
47
  console.log(`类型: ${layer.type}`)
20
-
48
+
21
49
  // 尺寸信息
22
50
  console.log(`\n--- 尺寸 ---`)
23
51
  console.log(`宽度: ${layer.frame.width}`)
24
52
  console.log(`高度: ${layer.frame.height}`)
25
-
53
+ if (textMetrics) {
54
+ console.log(`[文字] 字号: ${textMetrics.fontSize} 自动行高框高: ${textMetrics.frameHeight}`)
55
+ console.log(`[文字] 顶部光学偏移 topOffset: ${textMetrics.topOffset.toFixed(1)}px (可视文字高度: ${textMetrics.visualHeight})`)
56
+ }
57
+
26
58
  // 绝对坐标(供参考)
27
59
  console.log(`\n--- 绝对坐标(供参考)---`)
28
- console.log(`x: ${layer.frame.x}`)
29
- console.log(`y: ${layer.frame.y}`)
30
-
31
- // ⭐ 关键:计算相对边距(可直接使用)
32
- console.log(`\n--- 相对边距(可直接使用)---`)
60
+ console.log(`frame.x: ${layer.frame.x} frame.y: ${layer.frame.y}`)
61
+ if (textMetrics) {
62
+ console.log(`[文字] 视觉上沿 y: ${textMetrics.visualTop.toFixed(1)} 视觉下沿 y: ${textMetrics.visualBottom.toFixed(1)}`)
63
+ }
64
+
65
+ // 关键:计算相对边距
66
+ console.log(`\n--- 相对边距 ---`)
33
67
  if (parent && parent.frame) {
34
68
  const paddingLeft = layer.frame.x - parent.frame.x
35
69
  const paddingTop = layer.frame.y - parent.frame.y
36
70
  const paddingRight = (parent.frame.x + parent.frame.width) - (layer.frame.x + layer.frame.width)
37
71
  const paddingBottom = (parent.frame.y + parent.frame.height) - (layer.frame.y + layer.frame.height)
38
-
39
- console.log(`距父容器左边: ${paddingLeft}`)
40
- console.log(`距父容器顶部: ${paddingTop}`)
41
- console.log(`距父容器右边: ${paddingRight}`)
42
- console.log(`距父容器底部: ${paddingBottom}`)
72
+
73
+ console.log(`距父容器左边: ${paddingLeft.toFixed(1)}`)
74
+ console.log(`距父容器顶部: ${paddingTop.toFixed(1)} (frame.y 原始值)`)
75
+ console.log(`距父容器右边: ${paddingRight.toFixed(1)}`)
76
+ console.log(`距父容器底部: ${paddingBottom.toFixed(1)} (frame 原始值)`)
43
77
  console.log(`父容器: ${parent.name} (${parent.frame.width}x${parent.frame.height})`)
78
+
79
+ if (textMetrics) {
80
+ // 视觉边距:从文字像素内容计算,而非从 frame 计算
81
+ const visualPaddingTop = paddingTop + textMetrics.topOffset
82
+ const visualPaddingBottom = paddingBottom + textMetrics.topOffset
83
+ console.log(`\n--- 文字视觉边距(修正后,推荐使用)---`)
84
+ console.log(`视觉距父容器顶部: ${visualPaddingTop.toFixed(1)} (= framePaddingTop ${paddingTop.toFixed(1)} + topOffset ${textMetrics.topOffset.toFixed(1)})`)
85
+ console.log(`视觉距父容器底部: ${visualPaddingBottom.toFixed(1)}`)
86
+ console.log(`\n 说明: 设计师在 Sketch 中看到的上下间距是"视觉边距",`)
87
+ console.log(` Flutter Positioned.top 应使用"frame paddingTop"(${paddingTop.toFixed(1)}),`)
88
+ console.log(` 因为 Flutter Text 自身也会在字号上下添加相似行高空间。`)
89
+ console.log(` 若视觉偏差明显,可尝试 top: ${(paddingTop - textMetrics.topOffset).toFixed(1)} 使视觉上沿对齐。`)
90
+ }
44
91
  } else {
45
92
  console.log('无父容器或为顶层元素')
46
93
  }
47
-
94
+
48
95
  // 框架转换提示
49
- console.log(`\n--- 框架转换 ---`)
50
- console.log(`Flutter: padding: EdgeInsets.fromLTRB(${layer.frame.x}, ${layer.frame.y}, 0, 0)`)
51
- console.log(`CSS: padding: ${layer.frame.y}px ${layer.frame.x}px;`)
96
+ console.log(`\n--- Flutter 代码参考 ---`)
97
+ if (parent && parent.frame) {
98
+ const paddingLeft = layer.frame.x - parent.frame.x
99
+ const paddingTop = layer.frame.y - parent.frame.y
100
+ console.log(`Positioned(left: ${paddingLeft.toFixed(1)}, top: ${paddingTop.toFixed(1)}, child: ...)`)
101
+ if (textMetrics) {
102
+ console.log(`// 若需要视觉上沿对齐: top: ${(paddingTop - textMetrics.topOffset).toFixed(1)}`)
103
+ }
104
+ }
52
105
  }
@@ -1,79 +1,114 @@
1
1
  /**
2
- * 间距测量模板 - 计算同级元素间距
2
+ * 间距测量模板 - 计算同级元素间距(含文字垂直对齐修正)
3
3
  * 用途:测量多个元素之间的垂直/水平间距
4
- *
4
+ *
5
+ * 文字垂直修正说明:
6
+ * Sketch Text"适应布局"时 frameHeight = fontSize × 行高系数(≈1.375),
7
+ * 文字像素内容在框内垂直居中,topOffset = (frameH - fontSize) / 2。
8
+ * 两个元素之间的"视觉间距"需要减去相邻文字图层的 topOffset:
9
+ * · 下方是 Text:visualGap = rawGap + topOffset(next)
10
+ * · 上方是 Text:visualGap = rawGap + bottomOffset(current) = rawGap + topOffset(current)
11
+ * · 两者都是 Text:visualGap = rawGap + topOffset(current) + topOffset(next)
12
+ *
5
13
  * 使用方式:在 Sketch 中选中多个同级元素,运行此脚本
6
14
  */
7
15
  const sketch = require('sketch')
8
16
 
17
+ // 计算 Text 图层的垂直光学偏移量,非 Text 返回 topOffset=0
18
+ function textOffset(layer) {
19
+ if (layer.type !== 'Text') return 0
20
+ var fs = (layer.style && layer.style.fontSize) ? layer.style.fontSize : 0
21
+ if (!fs) return 0
22
+ return (layer.frame.height - fs) / 2
23
+ }
24
+
25
+ // 格式化显示名(类型前缀 + 文字内容或图层名)
26
+ function labelOf(layer) {
27
+ if (layer.type === 'Text') return `[T]"${layer.text || layer.name}"`
28
+ return `[${layer.type}]${layer.name}`
29
+ }
30
+
9
31
  const sel = sketch.getSelectedDocument().selectedLayers.layers
10
32
  if (sel.length < 2) {
11
33
  console.log('请选中至少2个元素来计算间距')
12
34
  } else {
13
35
  // 按 Y 坐标排序(从上到下)
14
36
  const sortedByY = [...sel].sort((a, b) => a.frame.y - b.frame.y)
15
-
37
+
16
38
  console.log('=== 垂直间距测量 ===')
17
39
  console.log(`选中元素数量: ${sel.length}`)
18
-
40
+
19
41
  for (let i = 0; i < sortedByY.length - 1; i++) {
20
- const current = sortedByY[i]
21
- const next = sortedByY[i + 1]
22
-
23
- // 计算垂直间距 = 下一个元素顶部 - 当前元素底部
24
- const gap = next.frame.y - (current.frame.y + current.frame.height)
25
-
26
- console.log(`\n${current.name} ↓ ${next.name}`)
27
- console.log(` 垂直间距: ${gap}px`)
28
- console.log(` CSS: margin-bottom: ${gap}px; gap: ${gap}px;`)
29
- console.log(` Flutter: SizedBox(height: ${gap})`)
42
+ const cur = sortedByY[i]
43
+ const nxt = sortedByY[i + 1]
44
+
45
+ // frame 间距(Sketch 坐标原始值)
46
+ const rawGap = nxt.frame.y - (cur.frame.y + cur.frame.height)
47
+
48
+ // 视觉间距修正:减去当前元素底部光学空白,减去下一元素顶部光学空白
49
+ const curBottomOffset = textOffset(cur) // Text 底部行高空白
50
+ const nxtTopOffset = textOffset(nxt) // Text 顶部行高空白
51
+ const visualGap = rawGap + curBottomOffset + nxtTopOffset
52
+
53
+ console.log(`\n${labelOf(cur)} ↓ ${labelOf(nxt)}`)
54
+ console.log(` frame 原始间距: ${rawGap.toFixed(1)}px`)
55
+
56
+ if (curBottomOffset || nxtTopOffset) {
57
+ const parts = []
58
+ if (curBottomOffset) parts.push(`上方文字底部偏移 +${curBottomOffset.toFixed(1)}`)
59
+ if (nxtTopOffset) parts.push(`下方文字顶部偏移 +${nxtTopOffset.toFixed(1)}`)
60
+ console.log(` 视觉修正来源: ${parts.join(', ')}`)
61
+ console.log(` 视觉间距(推荐): ${visualGap.toFixed(1)}px`)
62
+ }
63
+
64
+ console.log(` SizedBox(height: ${visualGap.toFixed(1)})`)
30
65
  }
31
-
66
+
32
67
  // 按 X 坐标排序(从左到右)
33
68
  const sortedByX = [...sel].sort((a, b) => a.frame.x - b.frame.x)
34
-
69
+
35
70
  console.log('\n=== 水平间距测量 ===')
36
-
71
+
37
72
  for (let i = 0; i < sortedByX.length - 1; i++) {
38
- const current = sortedByX[i]
39
- const next = sortedByX[i + 1]
40
-
41
- // 计算水平间距 = 下一个元素左边 - 当前元素右边
42
- const gap = next.frame.x - (current.frame.x + current.frame.width)
43
-
44
- console.log(`\n${current.name} ${next.name}`)
45
- console.log(` 水平间距: ${gap}px`)
46
- console.log(` CSS: margin-right: ${gap}px; 或 gap: ${gap}px;`)
47
- console.log(` Flutter: SizedBox(width: ${gap})`)
73
+ const cur = sortedByX[i]
74
+ const nxt = sortedByX[i + 1]
75
+
76
+ const rawGap = nxt.frame.x - (cur.frame.x + cur.frame.width)
77
+
78
+ console.log(`\n${labelOf(cur)} → ${labelOf(nxt)}`)
79
+ console.log(` 水平间距: ${rawGap.toFixed(1)}px`)
80
+ console.log(` SizedBox(width: ${rawGap.toFixed(1)})`)
81
+ // 水平方向文字偏移通常不影响左右间距,不做修正
48
82
  }
49
-
50
- // 汇总信息
83
+
84
+ // 汇总
51
85
  console.log('\n=== 汇总 ===')
52
- const verticalGaps = []
53
- const horizontalGaps = []
54
-
86
+ const vGaps = []
87
+ const hGaps = []
88
+
55
89
  for (let i = 0; i < sortedByY.length - 1; i++) {
56
- const gap = sortedByY[i + 1].frame.y - (sortedByY[i].frame.y + sortedByY[i].frame.height)
57
- verticalGaps.push(gap)
90
+ const cur = sortedByY[i]
91
+ const nxt = sortedByY[i + 1]
92
+ const vis = nxt.frame.y - (cur.frame.y + cur.frame.height) + textOffset(cur) + textOffset(nxt)
93
+ vGaps.push(+vis.toFixed(1))
58
94
  }
59
-
60
95
  for (let i = 0; i < sortedByX.length - 1; i++) {
61
- const gap = sortedByX[i + 1].frame.x - (sortedByX[i].frame.x + sortedByX[i].frame.width)
62
- horizontalGaps.push(gap)
96
+ const g = sortedByX[i + 1].frame.x - (sortedByX[i].frame.x + sortedByX[i].frame.width)
97
+ hGaps.push(+g.toFixed(1))
63
98
  }
64
-
65
- const uniqueVGaps = [...new Set(verticalGaps)]
66
- const uniqueHGaps = [...new Set(horizontalGaps)]
67
-
68
- if (uniqueVGaps.length === 1) {
69
- console.log(`垂直间距统一: ${uniqueVGaps[0]}px → 可使用 flex gap 或 Column mainAxisSpacing`)
99
+
100
+ const uniqueV = [...new Set(vGaps)]
101
+ const uniqueH = [...new Set(hGaps)]
102
+
103
+ if (uniqueV.length === 1) {
104
+ console.log(`垂直视觉间距统一: ${uniqueV[0]}px → Column spacing: ${uniqueV[0]}`)
70
105
  } else {
71
- console.log(`垂直间距不统一: ${verticalGaps.join(', ')} → 需要单独设置`)
106
+ console.log(`垂直视觉间距不统一: ${vGaps.join(', ')} → 需要单独设置`)
72
107
  }
73
-
74
- if (uniqueHGaps.length === 1) {
75
- console.log(`水平间距统一: ${uniqueHGaps[0]}px → 可使用 flex gap 或 Row mainAxisSpacing`)
108
+
109
+ if (uniqueH.length === 1) {
110
+ console.log(`水平间距统一: ${uniqueH[0]}px → Row spacing: ${uniqueH[0]}`)
76
111
  } else {
77
- console.log(`水平间距不统一: ${horizontalGaps.join(', ')} → 需要单独设置`)
112
+ console.log(`水平间距不统一: ${hGaps.join(', ')} → 需要单独设置`)
78
113
  }
79
114
  }
@@ -105,5 +105,18 @@ if (!sel.length) {
105
105
  console.log(`行高: ${textStyle.lineHeight || 'auto'}`)
106
106
  console.log(`颜色: ${textStyle.textColor}`)
107
107
  console.log(`对齐: ${textStyle.alignment}`)
108
+
109
+ // 文字垂直光学偏移(适应布局时 frameHeight > fontSize,文字居中)
110
+ const fs = textStyle.fontSize || 0
111
+ const frameH = layer.frame.height
112
+ if (fs && frameH > fs) {
113
+ const topOffset = (frameH - fs) / 2
114
+ console.log(`\n--- 文字垂直居中修正(适应布局)---`)
115
+ console.log(`frame 高度: ${frameH} 字号: ${fs}`)
116
+ console.log(`topOffset: ${topOffset.toFixed(1)}px (frame 顶到可视文字上沿的间距)`)
117
+ console.log(`视觉文字上沿 y: ${(layer.frame.y + topOffset).toFixed(1)} (= frame.y ${layer.frame.y} + topOffset ${topOffset.toFixed(1)})`)
118
+ console.log(`注意: Flutter Positioned.top 使用 frame.y 即可,因为 Flutter Text 本身有等效行高空间;`)
119
+ console.log(` 若定位出现 ${topOffset.toFixed(1)}px 偏差,可将 top 调整为 ${(layer.frame.y - topOffset).toFixed(1)}`)
120
+ }
108
121
  }
109
122
  }