mta-mcp 3.13.0 → 3.14.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mta-mcp",
3
- "version": "3.13.0",
3
+ "version": "3.14.2",
4
4
  "description": "MTA - 智能编码助手 MCP 服务器(规范 + 技能 + 诊断 + 模板 + 记忆 + 思考)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1072,6 +1072,43 @@ height = 文本高度 ÷ 字号 = 20 ÷ 14 = 1.43
1072
1072
  4. 设置 `Container.alignment: Alignment.center`
1073
1073
  5. 设置 `contentPadding: EdgeInsets.zero` + `isDense: true`
1074
1074
 
1075
+ ### TextField Focus 背景色全局修复
1076
+
1077
+ > ⚠️ **TextField 聚焦时出现蓝色半透明背景,需要在 ThemeData 两处同时消除**
1078
+
1079
+ Flutter 默认 `focusColor = Color(0x1F2196F3)`(蓝色 12% 透明度),在 TextField focus 时作为 State Layer 叠加在背景上,视觉效果很突兀。
1080
+
1081
+ ```dart
1082
+ // ✅ 必须在 ThemeData 和 InputDecorationTheme 两处同时设置
1083
+ ThemeData(
1084
+ focusColor: Colors.transparent, // 消除 focus state layer
1085
+ hoverColor: Colors.transparent, // 消除 hover state layer
1086
+ splashColor: Colors.transparent, // 消除水波纹
1087
+ highlightColor: Colors.transparent, // 消除高亮
1088
+ inputDecorationTheme: InputDecorationTheme(
1089
+ filled: true,
1090
+ fillColor: Colors.transparent,
1091
+ focusColor: Colors.transparent, // 双重保护
1092
+ hoverColor: Colors.transparent,
1093
+ border: InputBorder.none,
1094
+ enabledBorder: InputBorder.none,
1095
+ focusedBorder: InputBorder.none,
1096
+ ),
1097
+ )
1098
+
1099
+ // ❌ 错误 - 只改 InputDecorationTheme 不够,ThemeData 级别仍然生效
1100
+ inputDecorationTheme: InputDecorationTheme(
1101
+ focusColor: Colors.transparent, // 单独设置无效
1102
+ ),
1103
+ ```
1104
+
1105
+ | 属性 | 控制范围 |
1106
+ |------|----------|
1107
+ | `ThemeData.focusColor` | 全局 focus 状态叠加色 |
1108
+ | `ThemeData.hoverColor` | 全局 hover 状态叠加色 |
1109
+ | `InputDecorationTheme.focusColor` | TextField 专属 focus 色 |
1110
+ | `InputDecorationTheme.fillColor` | TextField 填充色 |
1111
+
1075
1112
  ---
1076
1113
 
1077
1114
  ## 🎨 Sketch/Figma 设计稿还原规范
@@ -1199,6 +1236,43 @@ SvgPicture.asset(
1199
1236
  )
1200
1237
  ```
1201
1238
 
1239
+ #### Bitmap 图层处理规范
1240
+
1241
+ Sketch 将 Bitmap(位图)图层"导出为 SVG"时,**并不生成矢量路径**,而是嵌入 base64 PNG 数据:
1242
+
1243
+ ```xml
1244
+ <!-- Bitmap 导出 SVG 的实际内容 - 只有 base64 图片,无矢量路径 -->
1245
+ <svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
1246
+ <image xlink:href="data:image/png;base64,iVBORw0KGgo..." width="12" height="12"/>
1247
+ </svg>
1248
+ ```
1249
+
1250
+ **正确处理方式:识别 Bitmap 图层后,改为 PNG 导出,Flutter 使用 `Image.asset`**:
1251
+
1252
+ ```javascript
1253
+ // 判断图层是否为 Bitmap
1254
+ const isBitmap = layer.type === 'Image';
1255
+
1256
+ // Bitmap 图层导出 PNG(@3x 高清)
1257
+ if (isBitmap) {
1258
+ sketch.export(layer, { formats: 'png', scales: '3', output: '/path/to/assets/icons/' });
1259
+ }
1260
+ ```
1261
+
1262
+ ```dart
1263
+ // ✅ 正确 - Bitmap 图层用 Image.asset,不用 SvgPicture.asset
1264
+ Image.asset(
1265
+ 'assets/icons/ic_example.png',
1266
+ width: 12,
1267
+ height: 12,
1268
+ color: AppColors.textDarkSecondary, // 按需染色
1269
+ colorBlendMode: BlendMode.srcIn,
1270
+ )
1271
+
1272
+ // ❌ 错误 - 对 Bitmap SVG 用 SvgPicture 会显示模糊的 base64 位图
1273
+ SvgPicture.asset('assets/icons/ic_example.svg') // 实际是嵌套 PNG,非矢量
1274
+ ```
1275
+
1202
1276
  #### 颜色透明度转换
1203
1277
 
1204
1278
  设计稿颜色格式:`#RRGGBBAA`(最后两位是透明度)
@@ -1217,6 +1291,57 @@ Flutter: Color(0xB31C2B45) 或 SVG fill-opacity="0.7"
1217
1291
  | 50% | 80 | #00000080 |
1218
1292
  | 15% | 26 | #1C2B4526 |
1219
1293
 
1294
+ ### 字重(FontWeight)还原规范
1295
+
1296
+ > ⚠️ **Sketch `style.fontWeight` 返回的是内部索引值,≠ Flutter FontWeight 数值**(高频错误)
1297
+
1298
+ #### 错误根因
1299
+
1300
+ Sketch JS API 的 `layer.style.fontWeight` 返回 5、6、8、9 等内部索引,与 CSS/Flutter 的字重体系(400、500、600、700)**没有直接倍数关系**,不能 `×100` 使用。
1301
+
1302
+ | Sketch `fontWeight` 值 | 实际字体面 | Flutter 正确值 | 常见错误推断 |
1303
+ |---|---|---|---|
1304
+ | 5 | PingFangSC-Regular | `FontWeight.w400` | ~~w500~~(直接 ×100) |
1305
+ | 6 | PingFangSC-Medium | `FontWeight.w500` | ~~w600~~ |
1306
+ | 8 | PingFangSC-Semibold | `FontWeight.w600` | ~~w800~~(最常见错误!)|
1307
+ | 9 | PingFangSC-Bold | `FontWeight.w700` | ~~w900~~ |
1308
+
1309
+ #### 正确获取方式(必须用 native API)
1310
+
1311
+ ```javascript
1312
+ // ❌ 错误 - fontWeight 数字不等于 Flutter FontWeight 值
1313
+ const weight = layer.style.fontWeight; // 返回 8,不代表 w800
1314
+
1315
+ // ✅ 正确 - 获取字体面完整名称,再按规则映射
1316
+ const fontName = String(layer.sketchObject.font().fontName());
1317
+ // "PingFangSC-Regular" → FontWeight.w400
1318
+ // "PingFangSC-Medium" → FontWeight.w500
1319
+ // "PingFangSC-Semibold" → FontWeight.w600
1320
+ // "PingFangSC-Bold" → FontWeight.w700
1321
+
1322
+ // 批量提取 Text 图层字重的完整脚本
1323
+ function getLayerFontWeight(layer) {
1324
+ const fontName = String(layer.sketchObject.font().fontName());
1325
+ const map = {
1326
+ 'Regular': 'FontWeight.w400',
1327
+ 'Medium': 'FontWeight.w500',
1328
+ 'Semibold': 'FontWeight.w600',
1329
+ 'Bold': 'FontWeight.w700',
1330
+ };
1331
+ const variant = Object.keys(map).find(k => fontName.includes(k)) || 'Regular';
1332
+ return { fontName, fontWeight: map[variant] };
1333
+ }
1334
+ ```
1335
+
1336
+ #### 字重与 UI 元素对照(PingFangSC 典型规律)
1337
+
1338
+ | UI 元素 | 常用字体面 | Flutter FontWeight |
1339
+ |--------|--------|-------------------|
1340
+ | 说明文字、普通标签 | PingFangSC-Regular | `w400` |
1341
+ | 次标题、中等强调 | PingFangSC-Medium | `w500` |
1342
+ | 重要数值、价格金额 | PingFangSC-Semibold | `w600` |
1343
+ | 页面主标题 | PingFangSC-Bold | `w700` |
1344
+
1220
1345
  ### 问题速查表
1221
1346
 
1222
1347
  > ⚠️ **修改代码前,先检查是否属于已知问题类型**
@@ -1231,6 +1356,10 @@ Flutter: Color(0xB31C2B45) 或 SVG fill-opacity="0.7"
1231
1356
  | Row 内 Gap 间距无效 | #6 Gap 方向错误 | `SizedBox(width:)` 或 `Gap.h()` |
1232
1357
  | **SVG 颜色比设计稿浅** | #7 ColorFilter 覆盖 | **移除 ColorFilter,保留 SVG 原有样式** |
1233
1358
  | **SVG 图标未居中** | #8 viewBox 不匹配 | **SVG viewBox 与使用尺寸一致** |
1359
+ | **字重比设计稿更粗/更细** | #9 fontWeight 数字误读 | **用 `font().fontName()` 获取字体面名称映射** |
1360
+ | **SVG 图标模糊/非矢量** | #10 Bitmap 图层误用 | **位图图层改用 PNG 导出 + `Image.asset`** |
1361
+ | **TextField 聚焦出现蓝色背景** | #11 Focus 颜色未清除 | **ThemeData + InputDecorationTheme 两处设 transparent** |
1362
+ | **Obx 不触发 RxSet 刷新** | #12 GetX 引用未订阅 | **Obx 内用 `Set.from(rxSet)` 创建新集合触发订阅** |
1234
1363
 
1235
1364
  ### 还原检查清单
1236
1365
 
@@ -1247,6 +1376,8 @@ Flutter: Color(0xB31C2B45) 或 SVG fill-opacity="0.7"
1247
1376
  | **边框** | color, thickness, position | `border: Border.all(...)` |
1248
1377
  | **不透明度** | opacity (颜色末尾两位) | 颜色 alpha 或 `Opacity` widget |
1249
1378
  | **图标** | SVG path, fill color, opacity | `SvgPicture.asset` |
1379
+ | **字重** | fontFace 名称(非 fontWeight 数字) | `FontWeight.w400/w500/w600/w700` |
1380
+ | **图标类型** | 矢量(Shape) vs 位图(Image) | Shape→SVG;Image→PNG+`Image.asset` |
1250
1381
 
1251
1382
  ### 禁止事项
1252
1383
 
@@ -1257,6 +1388,10 @@ Flutter: Color(0xB31C2B45) 或 SVG fill-opacity="0.7"
1257
1388
  5. ❌ **禁止遗漏阴影参数** - 必须读取全部 5 个参数
1258
1389
  6. ❌ **禁止忽略透明度** - 颜色 `#RRGGBBAA` 最后两位是透明度
1259
1390
  7. ❌ **禁止 ColorFilter 覆盖 SVG** - 除非明确需要改变颜色
1391
+ 8. ❌ **禁止用 Sketch `fontWeight` 数字推断 Flutter FontWeight** - 必须通过字体面名称(`font().fontName()`)映射
1392
+ 9. ❌ **禁止对 Bitmap 图层使用 SvgPicture.asset** - 必须导出 PNG,用 `Image.asset`
1393
+ 10. ❌ **禁止只在 InputDecorationTheme 修复 focus 背景色** - ThemeData 级别的 focusColor 也必须设置
1394
+ 11. ❌ **禁止在 Obx 外传入 RxSet 引用** - 必须在 Obx lambda 内用 `Set.from()` 触发响应式订阅
1260
1395
 
1261
1396
  ---
1262
1397
 
@@ -0,0 +1,79 @@
1
+ # TextField 聚焦时出现蓝色背景(Focus State Layer)
2
+
3
+ **问题标签**: `TextField`, `InputDecoration`, `ThemeData`, `focusColor`, `focus`, `背景色`
4
+ **问题类型**: `UI 样式异常`
5
+ **严重程度**: 中
6
+ **节省时间**: 多轮尝试 → 1 步修复
7
+
8
+ ---
9
+
10
+ ## 问题描述
11
+
12
+ TextField 点击聚焦后,输入框背景出现蓝色半透明叠加色(State Layer),与设计稿完全不符,视觉效果很突兀。
13
+
14
+ ## 问题根因
15
+
16
+ Flutter Material Design 默认会在 `ThemeData.focusColor` 设置带透明度的蓝色:
17
+
18
+ ```dart
19
+ // Flutter 框架默认值
20
+ ThemeData(
21
+ focusColor: Color(0x1F2196F3), // 蓝色 12% 透明度 → 即 12% 不透明蓝色叠加层
22
+ )
23
+ ```
24
+
25
+ 当 TextField 聚焦时,这个颜色作为 **State Layer** 叠加在 `fillColor` 上方,产生明显的蓝色背景。
26
+
27
+ **陷阱**:只在 `InputDecorationTheme` 中设置 `focusColor: Colors.transparent` 并不够——`ThemeData` 级别的 `focusColor` 仍然生效,在某些情况下依然会显示。
28
+
29
+ ## 错误尝试(导致反复)
30
+
31
+ | 尝试 | 结果 |
32
+ |------|------|
33
+ | 只设置 `InputDecorationTheme.focusColor: transparent` | 部分场景仍有蓝色 |
34
+ | 设置 `TextField.decoration.fillColor: transparent` | 填充色改了,但 focus 叠加色仍在 |
35
+ | 单独设置某个 TextField 的样式 | 新增 TextField 又出现问题,不是全局修复 |
36
+
37
+ ## 正确方案
38
+
39
+ 必须在 `ThemeData` 和 `InputDecorationTheme` **两处同时**设置:
40
+
41
+ ```dart
42
+ ThemeData(
43
+ // 全局消除 focus/hover/ripple State Layer
44
+ focusColor: Colors.transparent,
45
+ hoverColor: Colors.transparent,
46
+ splashColor: Colors.transparent,
47
+ highlightColor: Colors.transparent,
48
+
49
+ inputDecorationTheme: InputDecorationTheme(
50
+ filled: true,
51
+ fillColor: Colors.transparent,
52
+ focusColor: Colors.transparent, // 针对 TextField 的双重保护
53
+ hoverColor: Colors.transparent,
54
+ // 消除聚焦时的边框
55
+ border: InputBorder.none,
56
+ enabledBorder: InputBorder.none,
57
+ focusedBorder: InputBorder.none,
58
+ errorBorder: InputBorder.none,
59
+ focusedErrorBorder: InputBorder.none,
60
+ ),
61
+ )
62
+ ```
63
+
64
+ ## 各属性作用说明
65
+
66
+ | 属性 | 控制范围 | 默认值 |
67
+ |------|----------|--------|
68
+ | `ThemeData.focusColor` | 全局所有 Widget 的 focus 状态叠加色 | `Color(0x1F2196F3)` |
69
+ | `ThemeData.hoverColor` | 全局所有 Widget 的 hover 状态叠加色 | `Color(0x0A000000)` |
70
+ | `ThemeData.splashColor` | InkWell 水波纹颜色 | 蓝色变体 |
71
+ | `ThemeData.highlightColor` | 长按高亮颜色 | 半透明 |
72
+ | `InputDecorationTheme.focusColor` | TextField 专属 focus 层(二次覆盖) | 跟随 ThemeData |
73
+ | `InputDecorationTheme.fillColor` | TextField 填充背景色 | 跟随主题 |
74
+
75
+ ## 关键原则
76
+
77
+ 1. **ThemeData 和 InputDecorationTheme 都必须设置**,只改一处不够
78
+ 2. 如果项目有多套主题(亮色/暗色),**每套主题都需要单独设置**
79
+ 3. 在明确需要 focus 效果的按钮上(如 `ElevatedButton`),可单独通过 `ButtonStyle` 恢复,不影响全局配置
@@ -0,0 +1,105 @@
1
+ # Sketch fontWeight 数字 ≠ Flutter FontWeight 值
2
+
3
+ **问题标签**: `sketch`, `fontWeight`, `字重`, `PingFangSC`, `字体`, `设计稿还原`
4
+ **问题类型**: `UI 还原错误`
5
+ **严重程度**: 高
6
+ **节省时间**: 多轮调试 → 1 次确认
7
+
8
+ ---
9
+
10
+ ## 问题描述
11
+
12
+ 从 Sketch 设计稿读取 `layer.style.fontWeight` 后,按 `值 × 100` 推算 Flutter FontWeight,导致字重比设计稿偏粗或偏细:
13
+
14
+ - 设计师说"这是常规字重",但代码里写了 `FontWeight.w500`(实际应是 w400)
15
+ - 价格数值看起来异常加粗,代码里用了 `FontWeight.w800`(实际应是 w600)
16
+
17
+ ## 问题根因
18
+
19
+ Sketch JS API 的 `style.fontWeight` 返回的是**字体变体的内部枚举索引**(5、6、8、9...),与 CSS/Flutter 标准字重体系(100 ~ 900)**无数学对应关系**,不能直接 `×100` 使用。
20
+
21
+ | Sketch `style.fontWeight` | 实际字体面 | Flutter 正确值 | 错误推断 |
22
+ |---|---|---|---|
23
+ | 5 | PingFangSC-Regular | `FontWeight.w400` | ~~w500~~ |
24
+ | 6 | PingFangSC-Medium | `FontWeight.w500` | ~~w600~~ |
25
+ | 8 | PingFangSC-Semibold | `FontWeight.w600` | **~~w800~~(最常见!)** |
26
+ | 9 | PingFangSC-Bold | `FontWeight.w700` | ~~w900~~ |
27
+
28
+ ## 错误尝试(导致反复)
29
+
30
+ | 尝试 | 结果 |
31
+ |------|------|
32
+ | 直接 `fontWeight × 100` | 所有字重偏粗(w500→w600,w600→w800) |
33
+ | 猜测"5≈medium≈w500" | 有时正确,有时偏差一级 |
34
+ | 看字体名前半段"PingFangSC" | 没有变体信息,无法区分 Regular/Semibold |
35
+
36
+ ## 正确方案
37
+
38
+ ### 获取字体面完整名称(必须用 Sketch native API)
39
+
40
+ ```javascript
41
+ // ❌ 错误 - 返回内部索引,含义不明确
42
+ const weight = layer.style.fontWeight; // 返回 8,≠ w800
43
+
44
+ // ✅ 正确 - 获取字体面完整名称
45
+ const fontName = String(layer.sketchObject.font().fontName());
46
+ // 返回 "PingFangSC-Semibold" → 映射到 FontWeight.w600
47
+ ```
48
+
49
+ ### 标准映射表
50
+
51
+ ```javascript
52
+ // PingFangSC 字体映射(中文场景最常见)
53
+ const PINGFANG_WEIGHT_MAP = {
54
+ 'Ultralight': 'FontWeight.w100',
55
+ 'Thin': 'FontWeight.w200',
56
+ 'Light': 'FontWeight.w300',
57
+ 'Regular': 'FontWeight.w400',
58
+ 'Medium': 'FontWeight.w500',
59
+ 'Semibold': 'FontWeight.w600',
60
+ 'Bold': 'FontWeight.w700',
61
+ 'Heavy': 'FontWeight.w800',
62
+ 'Black': 'FontWeight.w900',
63
+ };
64
+
65
+ function getFlutterFontWeight(layer) {
66
+ const fontName = String(layer.sketchObject.font().fontName());
67
+ const variant = Object.keys(PINGFANG_WEIGHT_MAP).find(k => fontName.includes(k));
68
+ return PINGFANG_WEIGHT_MAP[variant || 'Regular'];
69
+ }
70
+ ```
71
+
72
+ ### 批量提取文本样式脚本
73
+
74
+ ```javascript
75
+ const sketch = require('sketch');
76
+ const page = sketch.getSelectedDocument().selectedPage;
77
+
78
+ function extractTextStyles(containerName) {
79
+ const container = sketch.find(`[name="${containerName}"]`, page)[0];
80
+ if (!container) return;
81
+
82
+ function walkLayers(layers) {
83
+ layers.forEach(layer => {
84
+ if (layer.type === 'Text') {
85
+ const fontName = String(layer.sketchObject.font().fontName());
86
+ const variant = ['Regular','Medium','Semibold','Bold','Light'].find(k => fontName.includes(k)) || 'Regular';
87
+ const weightMap = { Regular:'w400', Medium:'w500', Semibold:'w600', Bold:'w700', Light:'w300' };
88
+ console.log(`"${layer.name}": fontSize=${layer.style.fontSize}, fontWeight=FontWeight.${weightMap[variant]}, fontFace=${fontName}`);
89
+ }
90
+ if (layer.layers) walkLayers(layer.layers);
91
+ });
92
+ }
93
+
94
+ walkLayers(container.layers || []);
95
+ }
96
+
97
+ extractTextStyles('组件名称');
98
+ ```
99
+
100
+ ## 关键原则
101
+
102
+ 1. **永远不要根据 `style.fontWeight` 数字直接推断 Flutter FontWeight**
103
+ 2. **必须通过 `layer.sketchObject.font().fontName()` 获取字体面名称**
104
+ 3. 拿到字体面名称(如 `PingFangSC-Semibold`)后,取变体部分(`Semibold`)做映射
105
+ 4. 不同字体族(SF Pro、Roboto)的映射规则相同,变体名称是通用的
@@ -0,0 +1,112 @@
1
+ # Sketch SVG 导出:Bitmap 图层内嵌 base64 而非矢量路径
2
+
3
+ **问题标签**: `sketch`, `svg`, `bitmap`, `SvgPicture`, `Image.asset`, `图标导出`
4
+ **问题类型**: `资源处理错误`
5
+ **严重程度**: 中
6
+ **节省时间**: 多次排查 → 1 次识别
7
+
8
+ ---
9
+
10
+ ## 问题描述
11
+
12
+ 从 Sketch 导出 SVG 图标后,在 Flutter 中用 `SvgPicture.asset` 显示,但图标看起来模糊、不清晰,放大后出现像素化,或者根本无法正确着色。
13
+
14
+ ## 问题根因
15
+
16
+ Sketch 在导出 Bitmap(位图/Image)类型图层时,即使选择 SVG 格式,**实际写入的是 base64 编码的 PNG 数据**,并非矢量路径:
17
+
18
+ ```xml
19
+ <!-- 表面是 .svg 文件,实际内容完全不是矢量 -->
20
+ <svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
21
+ <image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA..." width="12" height="12"/>
22
+ </svg>
23
+ ```
24
+
25
+ 用 `SvgPicture.asset` 渲染这种文件时:
26
+ - 实际显示的是 base64 内嵌的栅格图像,存在分辨率上限
27
+ - `colorFilter` 无法对内嵌图像染色(只对矢量 path 有效)
28
+ - 在高 DPI 屏幕上仍然模糊
29
+
30
+ ## 如何识别 Bitmap 图层
31
+
32
+ ```javascript
33
+ // Sketch 脚本:检测图层类型
34
+ const sketch = require('sketch');
35
+ const layer = sketch.getSelectedDocument().selectedLayers.layers[0];
36
+
37
+ console.log(`图层类型: ${layer.type}`);
38
+ // Shape / Group / Text / SymbolInstance → 矢量,可导出 SVG
39
+ // Image → 位图,应导出 PNG
40
+ ```
41
+
42
+ 也可以在 Sketch UI 中:点击图层 → 右侧 Inspector → 图层类型标识为 "Image" 则为 Bitmap。
43
+
44
+ ## 正确方案
45
+
46
+ ### 步骤一:导出 PNG 而非 SVG
47
+
48
+ ```javascript
49
+ const sketch = require('sketch');
50
+ const layer = sketch.find('[name="ic_rate_chart"]', sketch.getSelectedDocument().selectedPage)[0];
51
+
52
+ if (layer) {
53
+ // 位图图层导出 PNG,@3x 保证高清
54
+ sketch.export(layer, {
55
+ formats: 'png',
56
+ scales: '3',
57
+ output: '/path/to/project/assets/icons/',
58
+ });
59
+ console.log('导出完成: ic_rate_chart@3x.png');
60
+ }
61
+ ```
62
+
63
+ ### 步骤二:Flutter 使用 Image.asset
64
+
65
+ ```dart
66
+ // ✅ 正确 - 位图图标用 Image.asset
67
+ Image.asset(
68
+ 'assets/icons/ic_rate_chart.png',
69
+ width: 12,
70
+ height: 12,
71
+ color: AppColors.textDarkSecondary, // 单色染色
72
+ colorBlendMode: BlendMode.srcIn,
73
+ )
74
+
75
+ // ✅ 不需要染色时
76
+ Image.asset(
77
+ 'assets/icons/ic_rate_chart.png',
78
+ width: 12,
79
+ height: 12,
80
+ )
81
+
82
+ // ❌ 错误 - 对 Bitmap SVG 用 SvgPicture,实际是渲染嵌套 PNG
83
+ SvgPicture.asset(
84
+ 'assets/icons/ic_rate_chart.svg',
85
+ colorFilter: ColorFilter.mode(color, BlendMode.srcIn), // 不生效
86
+ )
87
+ ```
88
+
89
+ ### 步骤三:注册资源
90
+
91
+ 在 `pubspec.yaml` 中确保声明:
92
+
93
+ ```yaml
94
+ flutter:
95
+ assets:
96
+ - assets/icons/ic_rate_chart.png
97
+ ```
98
+
99
+ ## 矢量图标 vs 位图图标的选择
100
+
101
+ | 图标来源 | 图层类型 | 导出格式 | Flutter 使用方式 |
102
+ |--------|--------|--------|----------------|
103
+ | Sketch Shape/Group 绘制 | Shape/Group | SVG | `SvgPicture.asset` |
104
+ | 粘贴进来的 PNG/JPG | Image | PNG (`scales: '3'`) | `Image.asset` |
105
+ | 外部导入的位图 | Image | PNG (`scales: '3'`) | `Image.asset` |
106
+ | 含 mask 的复合图标 | Group | SVG(先确认路径正确) | `SvgPicture.asset` |
107
+
108
+ ## 关键原则
109
+
110
+ 1. **导出前先检查图层类型**:`layer.type === 'Image'` 则为 Bitmap,必须用 PNG
111
+ 2. **不要对内嵌 base64 SVG 使用 colorFilter**:颜色叠加对栅格图像无效
112
+ 3. **PNG 导出用 `scales: '3'`**:保证 @3x 分辨率,避免在高 DPI 设备上模糊
@@ -670,7 +670,7 @@ function detectIfIcon(layer) {
670
670
 
671
671
  var name = (layer.name || '').toLowerCase();
672
672
  var size = Math.max(layer.frame.width, layer.frame.height);
673
- var iconKeywords = ['icon', 'ico', 'logo', 'symbol', 'arrow', 'close', 'menu',
673
+ var iconKeywords = ['icon', 'ico', 'ic_', '_ic', 'logo', 'symbol', 'arrow', 'close', 'menu',
674
674
  'search', 'star', 'check', 'back', 'more', 'nav', 'btn', 'button'];
675
675
  var hasKeyword = false;
676
676
  for (var k = 0; k < iconKeywords.length; k++) {
@@ -837,7 +837,13 @@ function extractShapePathSignature(layer) {
837
837
  guessedRole = 'background';
838
838
  guessedFlutter = 'Container with BoxDecoration';
839
839
  }
840
- // 描边图标
840
+ // v3.0.6: 极简几何描边(≤4直线点,尺寸≤24×24)→ CustomPainter 而非 SVG
841
+ // 根因: AI 看到勾选/箭头路径会用 Icons.check,但 Material Icon 是填充形状,与描边设计不符
842
+ else if (!isClosed && !hasCurves && pointCount <= 4 && Math.max(w, h) <= 24) {
843
+ guessedRole = 'geometric-stroke';
844
+ guessedFlutter = 'CustomPainter.drawPath() - 禁止用 Icons.xxx 或 SvgPicture.asset()';
845
+ }
846
+ // 描边图标(复杂曲线或点数较多的开放路径)
841
847
  else if (!isClosed && pointCount >= 2) {
842
848
  guessedRole = 'icon-stroke';
843
849
  guessedFlutter = 'SvgPicture.asset() - stroke path';
@@ -907,6 +913,18 @@ function extractShapePathSignature(layer) {
907
913
  shapeSigResult.strokeSvgHint = '此路径是开放描边路径 (isClosed=false),SVG 必须用 stroke 而非 fill。' +
908
914
  '模板: <polyline points="..." fill="none" stroke="' + strokeHexColor + '" stroke-width="' + b0.thickness + '" stroke-linecap="round" stroke-linejoin="round"/>。' +
909
915
  '禁止生成 fill 实心形状——该图形在设计稿中是纯描边无填充。';
916
+ // v3.0.6: geometric-stroke 专用 CustomPainter 提示——无需 SVG 文件,直接内联绘制
917
+ if (guessedRole === 'geometric-stroke') {
918
+ shapeSigResult.flutterCustomPainterHint =
919
+ '此路径为极简几何描边(' + pointCount + '点,' + w + '×' + h + '),禁止用 Icons.xxx 或 SvgPicture.asset()。' +
920
+ '用 CustomPainter: canvas.drawPath(path, Paint()' +
921
+ '..color=' + strokeFlutterColor +
922
+ '..style=PaintingStyle.stroke' +
923
+ '..strokeWidth=' + b0.thickness +
924
+ '..strokeCap=StrokeCap.round' +
925
+ '..strokeJoin=StrokeJoin.round)。' +
926
+ '典型场景: radio button勾选✓、小型折线箭头。';
927
+ }
910
928
  }
911
929
  }
912
930
 
@@ -1279,7 +1297,11 @@ function extractCompleteStyle(layer) {
1279
1297
  var ish = innerShadows[isi];
1280
1298
  var innerObj = { color: ish.color, blur: ish.blur, x: ish.x, y: ish.y, spread: ish.spread || 0 };
1281
1299
  var fc3 = sketchColorToFlutter(ish.color);
1282
- if (fc3) innerObj.flutterBoxShadow = 'BoxShadow(color: ' + fc3 + ', offset: Offset(' + ish.x + ', ' + ish.y + '), blurRadius: ' + ish.blur + ', spreadRadius: ' + (ish.spread || 0) + ')';
1300
+ if (fc3) {
1301
+ innerObj.flutterBoxShadow = 'BoxShadow(color: ' + fc3 + ', offset: Offset(' + ish.x + ', ' + ish.y + '), blurRadius: ' + ish.blur + ', spreadRadius: ' + (ish.spread || 0) + ')';
1302
+ // Flutter 不原生支持 innerShadow,以下给出实现方案
1303
+ innerObj.flutterNote = '⚠️ Flutter 不原生支持 innerShadow。实现方案: 在 Stack 中于内容层上方叠加一个同形状的 DecoratedBox,decoration 中 boxShadow 使用较大的 spreadRadius 使阴影向内延伸,再用 ClipRRect/ClipPath 裁剪为相同圆角,使阴影只在容器内部可见。或使用 CustomPainter 直接绘制。';
1304
+ }
1283
1305
  style.innerShadows.push(innerObj);
1284
1306
  }
1285
1307
  }
@@ -2415,8 +2437,28 @@ function measureRecursively(layer, depth, parentFrame, siblings) {
2415
2437
  if (iconFill.flutterColor) {
2416
2438
  var iw = Math.round(layer.frame.width);
2417
2439
  var ih = Math.round(layer.frame.height);
2440
+ // 将图层级别 opacity 合并进 colorFilter 颜色的 alpha 通道
2441
+ // 原因: Sketch 支持"填充色 alpha"和"图层整体 opacity"两个独立维度,
2442
+ // 若只用填充色 alpha 而忽略 layer opacity, colorFilter 渲染比设计稿更深
2443
+ var layerOpacity = 1.0;
2444
+ try {
2445
+ if (layer.style && layer.style.opacity !== undefined && layer.style.opacity !== 1) {
2446
+ layerOpacity = layer.style.opacity;
2447
+ }
2448
+ } catch (e2) { /* ignore */ }
2449
+ var fillAlpha = (iconFill.alpha !== undefined) ? iconFill.alpha : 1.0;
2450
+ var effectiveAlpha = fillAlpha * layerOpacity;
2451
+ var effectiveColor;
2452
+ if (Math.abs(effectiveAlpha - fillAlpha) > 0.01) {
2453
+ // layer opacity 有效,合并后重新生成 Flutter 颜色
2454
+ effectiveColor = toFlutterColor(iconFill.hex || iconFill.color, effectiveAlpha);
2455
+ element.iconOpacityMerged = true;
2456
+ element.iconOpacityNote = 'fill-alpha(' + Math.round(fillAlpha * 100) + '%) × layer-opacity(' + Math.round(layerOpacity * 100) + '%) = ' + Math.round(effectiveAlpha * 100) + '% → 已合并入 colorFilter alpha';
2457
+ } else {
2458
+ effectiveColor = iconFill.flutterColor;
2459
+ }
2418
2460
  // 路径后附带 TODO 注释,提示核对实际文件名
2419
- element.flutterSvgCode = 'SvgPicture.asset(\'assets/' + element.iconExportPath + '\' /* TODO: 核对 assets/icons/ 中实际文件名,禁止用 Icons.xxx 替代 */, width: ' + iw + ', height: ' + ih + ', colorFilter: ColorFilter.mode(' + iconFill.flutterColor + ', BlendMode.srcIn),)';
2461
+ element.flutterSvgCode = 'SvgPicture.asset(\'assets/' + element.iconExportPath + '\' /* TODO: 核对 assets/icons/ 中实际文件名,禁止用 Icons.xxx 替代 */, width: ' + iw + ', height: ' + ih + ', colorFilter: ColorFilter.mode(' + effectiveColor + ', BlendMode.srcIn),)';
2420
2462
  }
2421
2463
  }
2422
2464
  }
@@ -2508,7 +2550,13 @@ function measureRecursively(layer, depth, parentFrame, siblings) {
2508
2550
  if (nativeImgFills && nativeImgFills[0] && nativeImgFills[0].hex) {
2509
2551
  element.imageInfo.nativeTint = formatNativeColor(nativeImgFills[0]);
2510
2552
  }
2511
- element.imageInfo.note = '此图层为嵌入式位图(Bitmap), 需要导出为 SVG PNG 后使用';
2553
+ element.imageInfo.note = '此图层为嵌入式位图(Bitmap), 必须导出为 PNG (非SVG——Bitmap导出SVG只含base64嵌套图片, 无矢量路径), 放入 assets/icons/, 用 Image.asset 渲染';
2554
+ // 生成 Image.asset 代码片段,开发者直接复制使用
2555
+ var bitmapW = Math.round(layer.frame.width);
2556
+ var bitmapH = Math.round(layer.frame.height);
2557
+ var bitmapName = sanitizeFileName(layer.name);
2558
+ element.flutterImageCode = 'Image.asset(\'assets/icons/' + bitmapName + '.png\', width: ' + bitmapW + ', height: ' + bitmapH + ', color: YOUR_COLOR, colorBlendMode: BlendMode.srcIn,)';
2559
+ element.imageInfo.exportHint = 'Sketch 导出命令: File > Export Layers > 选择此图层 > 格式 PNG @3x, 放入 assets/icons/';
2512
2560
  }
2513
2561
 
2514
2562
  // Symbol Instance — override 真实值
@@ -1153,6 +1153,10 @@ function extractShapePathSignature(layer) {
1153
1153
  } else if (pointCount <= 4 && isClosed && hasCurves && aspect > 0.8 && aspect < 1.25) {
1154
1154
  guessedRole = 'circle-bg'
1155
1155
  guessedFlutter = 'Container(decoration: BoxDecoration(shape: BoxShape.circle, ...))'
1156
+ } else if (!isClosed && !hasCurves && pointCount <= 4 && w <= 24 && h <= 24) {
1157
+ // v3.0.6: 极简几何描边(≤4直线点,≤24×24),用 CustomPainter 而非 Icons.xxx 或 SVG
1158
+ guessedRole = 'geometric-stroke'
1159
+ guessedFlutter = '极简几何描边(' + pointCount + '点,' + w + '×' + h + ')。禁止用 Icons.xxx 或 SvgPicture.asset()。正确: CustomPainter → canvas.drawPath(path, Paint()..style=PaintingStyle.stroke..strokeCap=StrokeCap.round)。详见 flutterCustomPainterHint 字段。'
1156
1160
  } else if (isSmall && isSquarish && (pointCount > 4 || !isClosed)) {
1157
1161
  guessedRole = 'icon'
1158
1162
  guessedFlutter = '矢量图标路径。必须 exportIconAsSVG 导出后用 SvgPicture.asset() 加载。禁止用 Icons.xxx 猜测。'
@@ -1167,6 +1171,25 @@ function extractShapePathSignature(layer) {
1167
1171
  sig.guessedRole = guessedRole
1168
1172
  sig.guessedFlutter = guessedFlutter
1169
1173
 
1174
+ // v3.0.6: 开放路径—提取描边信息;geometric-stroke 补充 CustomPainter 提示
1175
+ if (!isClosed && layer.style && layer.style.borders) {
1176
+ const activeBorders = layer.style.borders.filter(b => b.enabled !== false)
1177
+ if (activeBorders.length > 0) {
1178
+ const b0 = activeBorders[0]
1179
+ const fColor = sketchColorToFlutter(b0.color) || 'Colors.black'
1180
+ sig.strokeInfo = { thickness: b0.thickness, color: b0.color, flutterColor: fColor }
1181
+ if (guessedRole === 'geometric-stroke') {
1182
+ sig.flutterCustomPainterHint =
1183
+ 'canvas.drawPath(path, Paint()' +
1184
+ '..color = ' + fColor +
1185
+ ' ..style = PaintingStyle.stroke' +
1186
+ ' ..strokeWidth = ' + (b0.thickness || 2) +
1187
+ ' ..strokeCap = StrokeCap.round' +
1188
+ ' ..strokeJoin = StrokeJoin.round)'
1189
+ }
1190
+ }
1191
+ }
1192
+
1170
1193
  return sig
1171
1194
  } catch (e) {
1172
1195
  return { error: String(e) }