mta-mcp 3.15.5 → 3.15.6

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.15.5",
3
+ "version": "3.15.6",
4
4
  "description": "MTA - 智能编码助手 MCP 服务器(规范 + 技能 + 诊断 + 模板 + 记忆 + 思考)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,8 +12,8 @@
12
12
  }
13
13
  },
14
14
  "bin": {
15
- "mta": "./bin/mta.cjs",
16
- "mta-mcp": "./bin/mta.cjs"
15
+ "mta": "bin/mta.cjs",
16
+ "mta-mcp": "bin/mta.cjs"
17
17
  },
18
18
  "files": [
19
19
  "bin/",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "repository": {
81
81
  "type": "git",
82
- "url": "https://github.com/ForLear/copilot-prompts.git",
82
+ "url": "git+https://github.com/ForLear/copilot-prompts.git",
83
83
  "directory": "mcp-server"
84
84
  }
85
85
  }
@@ -1,585 +0,0 @@
1
- /**
2
- * MTA Sketch Measure Plugin - measure.js
3
- * @version 2.0.0
4
- *
5
- * 完整测量选中 Sketch 图层/画板,输出 Flutter 还原数据。
6
- *
7
- * @changelog
8
- * v2.1.0 (2026-03-05):
9
- * - 架构修正: 移除决策层函数 computeLayoutIntent / buildFlutterDecorationHint
10
- * 插件职责 = 采集原始像素数据;AI 职责 = 根据规范文档解读数据并生成 Flutter 代码
11
- * - 新增 relativePosition: 图层相对父容器四边的原始距离(纯数据,不做判断)
12
- * - 新增 detectIfIcon() + extractIconContentBounds(): Icon 容器/内容双尺寸
13
- * containerSize = Group frame(占位),contentSize = 矢量路径实际边界(渲染)
14
- * - 新增 detectTagContainer(): tag/badge 背景+文字+圆角原始数据
15
- * - 新增 detectNeumorphicPattern(): 新拟态双阴影模式识别 → AppShadows token 标签
16
- * - 新增 detectAppColorToken(): 颜色值 → AppColors token 反查
17
- *
18
- * 颜色格式 (关键修复):
19
- * Sketch JS API 返回颜色为 "#rrggbbaa" 格式字符串(8位十六进制)
20
- * 正确解析: slice(0,6)=RRGGBB, slice(6,8)=AA → Color(0xAARRGGBB)
21
- * 错误写法: color.red / color.green → undefined → NaN (已修复)
22
- *
23
- * 双模式运行:
24
- * 1. Sketch 插件命令: Plugins > MTA Measure > 测量选中图层 (Ctrl+Shift+M)
25
- * 2. Sketch MCP: mcp_sketch_run_code({ code: fs.readFileSync('measure.js') })
26
- *
27
- * 字体权重规则(继承自历史项目优化):
28
- * PingFang SC: 最高可用 w600(超出触发合成加粗,视觉失真)
29
- * Helvetica / 系统: 最高可用 w700
30
- *
31
- * 新拟态阴影规则 (AppShadows token):
32
- * neumorphicCard: (6,6,12) dark=0x1F1C2B45 + light=0xB3FFFFFF
33
- * neumorphicSmall: (4,4,8) dark=0x261C2B45 + light=0xB3FFFFFF
34
- * neumorphicInput: (3,3,6) dark=0x1F1C2B45 + light=0xB3FFFFFF (concave)
35
- * neumorphicButton: (2,2,4) dark=0x141C2B45 + light=0x99FFFFFF (tab bar)
36
- */
37
-
38
- /* global context */
39
-
40
- // 防止在 Sketch 插件模式下因脚本求值 + handler 回调导致双次执行
41
- var _mtaMeasureRan = false;
42
-
43
- // ─────────────────────────────────────────────────────────────────────────────
44
- // 颜色工具
45
- // ─────────────────────────────────────────────────────────────────────────────
46
-
47
- /**
48
- * 将 Sketch 颜色字符串 "#rrggbbaa" 转换为 Flutter Color(0xAARRGGBB)
49
- * Sketch API 返回 8 位 hex 字符串,最后两位是 alpha
50
- */
51
- function c2f(c) {
52
- if (!c) return null;
53
- var h = String(c).replace('#', '');
54
- if (h.length === 8) {
55
- // #rrggbbaa → Color(0xAARRGGBB)
56
- var rr = h.slice(0, 2).toUpperCase();
57
- var gg = h.slice(2, 4).toUpperCase();
58
- var bb = h.slice(4, 6).toUpperCase();
59
- var aa = h.slice(6, 8).toUpperCase();
60
- return 'Color(0x' + aa + rr + gg + bb + ')';
61
- }
62
- if (h.length === 6) return 'Color(0xFF' + h.toUpperCase() + ')';
63
- return null;
64
- }
65
-
66
- /**
67
- * AppColors token 反查:将 Flutter Color 字符串映射回项目 Token 名
68
- * 基于 rate_alert 项目 AppColors 定义整理
69
- */
70
- function detectAppColorToken(flutterColor) {
71
- var tokenMap = {
72
- 'Color(0xFF1C2B45)': 'AppColors.textDark',
73
- 'Color(0xB31C2B45)': 'AppColors.textDarkSecondary',
74
- 'Color(0x801C2B45)': 'AppColors.textDarkTertiary',
75
- 'Color(0xFFE8EDF5)': 'AppColors.neumorphicBackground',
76
- 'Color(0xFF4CAF50)': 'AppColors.brandGreen',
77
- 'Color(0xFFD0121B)': 'AppColors.brandRed',
78
- 'Color(0xFFD6101E)': 'AppColors.brandRed',
79
- 'Color(0xFFFFFFFF)': 'Colors.white',
80
- 'Color(0xFF000000)': 'Colors.black',
81
- };
82
- return tokenMap[flutterColor] || null;
83
- }
84
-
85
- // ─────────────────────────────────────────────────────────────────────────────
86
- // 字体权重映射
87
- // ─────────────────────────────────────────────────────────────────────────────
88
-
89
- /**
90
- * Sketch fontWeight (1-9) → Flutter FontWeight 数值
91
- * PingFang SC: 真实可用最粗 = Semibold (w600),超出会触发合成加粗(视觉失真)
92
- * Helvetica/系统: 真实可用最粗 = Bold (w700)
93
- */
94
- function fw2f(sketchFw, family) {
95
- var isPF = /PingFang|Hiragino|STHei/.test(family || '');
96
- var map = { 3: 300, 4: 400, 5: 500, 6: 600, 7: 700, 8: 800, 9: 900 };
97
- var weight = map[sketchFw] || 400;
98
- return isPF ? Math.min(weight, 600) : Math.min(weight, 700);
99
- }
100
-
101
- // ─────────────────────────────────────────────────────────────────────────────
102
- // 新拟态模式识别
103
- // ─────────────────────────────────────────────────────────────────────────────
104
-
105
- /**
106
- * 识别新拟态双阴影模式,输出 AppShadows token 建议
107
- * 规则来自汇率提醒项目 Sketch 实测总结:
108
- * neumorphicCard: right-bottom dark(6,6,12,0x1F1C2B45) + left-top light(-6,-6,12,0xB3FFFFFF)
109
- * neumorphicSmall: (4,4,8,0x261C2B45) + (-4,-4,8,0xB3FFFFFF)
110
- * neumorphicInput: (3,3,6,0x1F1C2B45) + (-3,-3,6,0xB3FFFFFF)
111
- * neumorphicButton: tab bar style (2,2,4,0x141C2B45) + (-2,-2,4,0x99FFFFFF)
112
- */
113
- function detectNeumorphicPattern(shadows) {
114
- if (!shadows || shadows.length < 2) return null;
115
-
116
- // 找正向(右下)和负向(左上)阴影
117
- var dark = null;
118
- var light = null;
119
- for (var i = 0; i < shadows.length; i++) {
120
- var s = shadows[i];
121
- if (s.x > 0 && s.y > 0) dark = s;
122
- if (s.x < 0 && s.y < 0) light = s;
123
- }
124
- if (!dark || !light) return null;
125
-
126
- var ax = Math.abs(dark.x);
127
- var ay = Math.abs(dark.y);
128
- var b = dark.blur;
129
-
130
- if (ax === 6 && ay === 6 && b === 12) return 'AppShadows.neumorphicCard';
131
- if (ax === 4 && ay === 4 && b === 8) return 'AppShadows.neumorphicSmall';
132
- if (ax === 3 && ay === 3 && b === 6) return 'AppShadows.neumorphicInput';
133
- if (ax === 2 && ay === 2 && b === 4) return 'AppShadows.neumorphicButton';
134
-
135
- // 未命中已知 token,仍然标记为 neumorphic 模式
136
- return 'neumorphic(' + ax + ',' + ay + ',' + b + ')';
137
- }
138
-
139
- // ─────────────────────────────────────────────────────────────────────────────
140
- // Icon 识别 + 双尺寸提取
141
- // ─────────────────────────────────────────────────────────────────────────────
142
-
143
- /**
144
- * 判断图层是否为 Icon
145
- * 移植自 sketch-tools.js v4.6.0 detectIfIcon()
146
- */
147
- function detectIfIcon(layer) {
148
- var name = (layer.name || '').toLowerCase();
149
- var size = Math.max(layer.frame.width, layer.frame.height);
150
-
151
- var iconKeywords = ['icon', 'ico', 'logo', 'symbol', 'arrow', 'close', 'menu',
152
- 'search', 'star', 'check', 'back', 'more', 'nav', 'btn', 'button'];
153
- var hasKeyword = false;
154
- for (var k = 0; k < iconKeywords.length; k++) {
155
- if (name.indexOf(iconKeywords[k]) !== -1) { hasKeyword = true; break; }
156
- }
157
-
158
- var isIconSize = size >= 8 && size <= 64;
159
- var ratio = layer.frame.width / layer.frame.height;
160
- var isSquarish = ratio >= 0.5 && ratio <= 2.0;
161
- var isVectorType = layer.type === 'ShapePath' || layer.type === 'SymbolInstance' ||
162
- (layer.type === 'Group' && (layer.layers ? layer.layers.length : 0) <= 5);
163
- var hasNoText = layer.type !== 'Text';
164
-
165
- var matched = 0;
166
- if (hasKeyword) matched++;
167
- if (isIconSize) matched++;
168
- if (isSquarish) matched++;
169
- if (isVectorType) matched++;
170
- if (hasNoText) matched++;
171
-
172
- return matched >= 3;
173
- }
174
-
175
- /**
176
- * 提取 Icon Group 内部真实矢量内容边界(containerSize vs contentSize)
177
- * 解决 Icon 用 Group frame 渲染偏大 2-4px 的问题
178
- * 移植自 sketch-tools.js v4.6.0 extractIconContentBounds()
179
- */
180
- function extractIconContentBounds(layer) {
181
- if (!layer.layers || !layer.layers.length) return null;
182
-
183
- var containerW = layer.frame.width;
184
- var containerH = layer.frame.height;
185
- var minX = 999999, minY = 999999, maxX = -999999, maxY = -999999;
186
- var pathCount = 0;
187
-
188
- function collectPaths(l, ox, oy) {
189
- if (l.hidden) return;
190
- var lx = ox + l.frame.x;
191
- var ly = oy + l.frame.y;
192
- var n = (l.name || '').toLowerCase();
193
- // ShapePath / Shape,排除辅助背景层
194
- if ((l.type === 'ShapePath' || l.type === 'Shape') &&
195
- n.indexOf('_background') === -1 && n.indexOf('_bg') === -1) {
196
- pathCount++;
197
- if (lx < minX) minX = lx;
198
- if (ly < minY) minY = ly;
199
- if (lx + l.frame.width > maxX) maxX = lx + l.frame.width;
200
- if (ly + l.frame.height > maxY) maxY = ly + l.frame.height;
201
- }
202
- if (l.layers) {
203
- for (var i = 0; i < l.layers.length; i++) {
204
- collectPaths(l.layers[i], lx, ly);
205
- }
206
- }
207
- }
208
- for (var i = 0; i < layer.layers.length; i++) {
209
- collectPaths(layer.layers[i], 0, 0);
210
- }
211
-
212
- if (pathCount === 0) return null;
213
-
214
- var cw = Math.round(maxX - minX);
215
- var ch = Math.round(maxY - minY);
216
- var pl = Math.round(minX);
217
- var pt = Math.round(minY);
218
- var pr = Math.round(containerW - maxX);
219
- var pb = Math.round(containerH - maxY);
220
- var hasPad = pl > 0 || pt > 0 || pr > 0 || pb > 0;
221
-
222
- return {
223
- containerSize: { width: Math.round(containerW), height: Math.round(containerH) },
224
- contentSize: { width: cw, height: ch },
225
- contentPadding: hasPad ? { left: pl, top: pt, right: pr, bottom: pb } : null,
226
- // AI 读取规则: containerSize 用于 SizedBox 占位,contentSize 用于 SvgPicture 渲染尺寸
227
- hasPadding: hasPad,
228
- };
229
- }
230
-
231
- // ─────────────────────────────────────────────────────────────────────────────
232
- // Tag / Badge / Chip 容器识别
233
- // ─────────────────────────────────────────────────────────────────────────────
234
-
235
- /**
236
- * 识别 Tag/Badge 小容器(背景 Shape + 文字 + 圆角),输出精确样式
237
- * 移植自 sketch-tools.js v4.6.0 detectTagContainer()
238
- */
239
- function detectTagContainer(layer) {
240
- if (layer.type !== 'Group' || !layer.layers) return null;
241
- var visible = [];
242
- for (var i = 0; i < layer.layers.length; i++) {
243
- if (!layer.layers[i].hidden) visible.push(layer.layers[i]);
244
- }
245
- if (visible.length < 2) return null;
246
-
247
- var shapes = [], texts = [];
248
- for (var i = 0; i < visible.length; i++) {
249
- var t = visible[i].type;
250
- if (t === 'ShapePath' || t === 'Shape') shapes.push(visible[i]);
251
- if (t === 'Text') texts.push(visible[i]);
252
- }
253
- if (!shapes.length || !texts.length) return null;
254
-
255
- // 最大 Shape 作为背景容器
256
- var bgShape = shapes[0];
257
- for (var i = 1; i < shapes.length; i++) {
258
- if (shapes[i].frame.width * shapes[i].frame.height >
259
- bgShape.frame.width * bgShape.frame.height) bgShape = shapes[i];
260
- }
261
- var text = texts[0];
262
-
263
- // 计算文字相对背景的内边距
264
- var padH = Math.round((layer.frame.width - text.frame.width) / 2);
265
- var padV = Math.round((layer.frame.height - text.frame.height) / 2);
266
-
267
- var fill = getFill(bgShape.style);
268
- var bgColor = fill ? fill.flutterColor : null;
269
- var radius = bgShape.cornerRadius || 0;
270
-
271
- // 纯数据输出,不包含 Flutter 代码 - 由 AI 根据规范解读
272
- return {
273
- containerSize: { width: Math.round(layer.frame.width), height: Math.round(layer.frame.height) },
274
- background: bgColor,
275
- padding: { horizontal: padH, vertical: padV },
276
- cornerRadius: radius,
277
- };
278
- }
279
-
280
- // ─────────────────────────────────────────────────────────────────────────────
281
- // 样式提取
282
- // ─────────────────────────────────────────────────────────────────────────────
283
-
284
- /**
285
- * 提取图层填充信息(纯色 / 渐变)
286
- * 渐变坐标系: Sketch 0-1 归一化 → Flutter Alignment(-1, +1)
287
- */
288
- function getFill(style) {
289
- if (!style || !style.fills) return null;
290
- var enabled = style.fills.filter(function(f) { return f.enabled; });
291
- if (!enabled.length) return null;
292
- var f = enabled[0];
293
-
294
- if (f.fillType === 'Color') {
295
- return { type: 'solid', flutterColor: c2f(f.color) };
296
- }
297
-
298
- if (f.fillType === 'Gradient') {
299
- var g = f.gradient;
300
- var toAl = function(v) { return +(v * 2 - 1).toFixed(2); };
301
- var bx = toAl(g.from.x), by = toAl(g.from.y);
302
- var ex = toAl(g.to.x), ey = toAl(g.to.y);
303
- var stops = g.stops.map(function(s) {
304
- return { pos: +s.position.toFixed(3), flutterColor: c2f(s.color) };
305
- });
306
- var colorList = stops.map(function(s) { return s.flutterColor; }).join(', ');
307
- return {
308
- type: 'gradient',
309
- sketchFrom: { x: g.from.x, y: g.from.y },
310
- sketchTo: { x: g.to.x, y: g.to.y },
311
- stops: stops,
312
- flutterLinearGradient: 'LinearGradient(begin: Alignment(' + bx + ', ' + by + '), end: Alignment(' + ex + ', ' + ey + '), colors: [' + colorList + '])',
313
- };
314
- }
315
- return null;
316
- }
317
-
318
- /** 提取边框信息 */
319
- function getBorder(style) {
320
- if (!style || !style.borders) return null;
321
- var enabled = style.borders.filter(function(b) { return b.enabled; });
322
- if (!enabled.length) return null;
323
- var b = enabled[0];
324
- var color = c2f(b.color);
325
- return {
326
- flutterColor: color,
327
- width: b.thickness,
328
- flutterBorder: 'Border.all(color: ' + color + ', width: ' + b.thickness + ')',
329
- };
330
- }
331
-
332
- /** 提取阴影列表(外阴影 / 内阴影通用) */
333
- function getShadowList(shadows) {
334
- if (!shadows || !shadows.length) return null;
335
- var enabled = shadows.filter(function(s) { return s.enabled; });
336
- if (!enabled.length) return null;
337
- return enabled.map(function(s) {
338
- var color = c2f(s.color);
339
- var spread = s.spread ? ', spreadRadius: ' + s.spread : '';
340
- return {
341
- x: s.x, y: s.y, blur: s.blur, spread: s.spread || 0,
342
- flutterColor: color,
343
- flutterBoxShadow: 'BoxShadow(color: ' + color + ', offset: Offset(' + s.x + ', ' + s.y + '), blurRadius: ' + s.blur + spread + ')',
344
- };
345
- });
346
- }
347
-
348
- /** 提取文字样式 */
349
- function getTextStyle(layer) {
350
- var s = layer.style;
351
- var family = s.fontFamily || s.fontName || s.font || '';
352
- var flutterFw = fw2f(s.fontWeight, family);
353
- var color = c2f(s.textColor);
354
- var lh = s.lineHeight ? +(s.lineHeight / s.fontSize).toFixed(2) : null;
355
- return {
356
- text: layer.text,
357
- fontSize: s.fontSize,
358
- fontWeightSketch: s.fontWeight,
359
- fontFamily: family,
360
- flutterFontWeight: 'FontWeight.w' + flutterFw,
361
- lineHeightSketch: s.lineHeight || null,
362
- flutterHeight: lh,
363
- flutterColor: color,
364
- flutterTextStyle: 'TextStyle(fontSize: ' + s.fontSize + ', fontWeight: FontWeight.w' + flutterFw + ', color: ' + color + (lh ? ', height: ' + lh : '') + ')',
365
- };
366
- }
367
-
368
- // ─────────────────────────────────────────────────────────────────────────────
369
- // 布局分析
370
- // ─────────────────────────────────────────────────────────────────────────────
371
-
372
- /**
373
- * 分析直接子图层的间距,推断 Flutter 布局意图
374
- * 仅分析同方向(横向 or 纵向)排列的子元素
375
- */
376
- function analyzeLayout(parent, children) {
377
- if (!children || children.length < 2) return null;
378
- var pw = parent.frame.width, ph = parent.frame.height;
379
-
380
- // 按 Y 排序判断是纵向列表还是横向行
381
- var byY = children.slice().sort(function(a, b) { return a.frame.y - b.frame.y; });
382
- var byX = children.slice().sort(function(a, b) { return a.frame.x - b.frame.x; });
383
-
384
- // 判断是横向(Row)还是纵向(Column)
385
- var ySpan = byY[byY.length - 1].frame.y - byY[0].frame.y;
386
- var xSpan = byX[byX.length - 1].frame.x - byX[0].frame.x;
387
- var isRow = xSpan > ySpan;
388
-
389
- var sorted = isRow ? byX : byY;
390
- var gaps = [];
391
- for (var i = 1; i < sorted.length; i++) {
392
- var prev = sorted[i - 1];
393
- var cur = sorted[i];
394
- var gap = isRow
395
- ? cur.frame.x - (prev.frame.x + prev.frame.width)
396
- : cur.frame.y - (prev.frame.y + prev.frame.height);
397
- gaps.push(+gap.toFixed(1));
398
- }
399
-
400
- var first = sorted[0];
401
- var last = sorted[sorted.length - 1];
402
- var edgeStart = isRow ? first.frame.x : first.frame.y;
403
- var edgeEnd = isRow
404
- ? pw - (last.frame.x + last.frame.width)
405
- : ph - (last.frame.y + last.frame.height);
406
-
407
- var uniformGap = gaps.every(function(g) { return Math.abs(g - gaps[0]) < 2; });
408
- var mainAlign = 'start';
409
- if (Math.abs(edgeStart - edgeEnd) < 3) {
410
- mainAlign = uniformGap ? 'center' : 'spaceBetween';
411
- } else if (edgeStart < 3) {
412
- mainAlign = 'start';
413
- }
414
-
415
- return {
416
- direction: isRow ? 'Row' : 'Column',
417
- gaps: gaps,
418
- uniformGap: uniformGap,
419
- edgeStart: +edgeStart.toFixed(1),
420
- edgeEnd: +edgeEnd.toFixed(1),
421
- suggestedMainAxisAlignment: 'MainAxisAlignment.' + mainAlign,
422
- suggestedGap: uniformGap ? gaps[0] : null,
423
- };
424
- }
425
-
426
- // ─────────────────────────────────────────────────────────────────────────────
427
- // 图层遍历
428
- // ─────────────────────────────────────────────────────────────────────────────
429
-
430
- function measureLayer(layer, depth, parentFrame) {
431
- if (layer.hidden) return null;
432
- var f = layer.frame;
433
- var r = function(v) { return Math.round(v * 10) / 10; };
434
-
435
- var out = {
436
- name: layer.name,
437
- type: layer.type,
438
- frame: { x: r(f.x), y: r(f.y), w: r(f.width), h: r(f.height) },
439
- };
440
-
441
- var s = layer.style;
442
- if (s) {
443
- var fill = getFill(s);
444
- if (fill) {
445
- out.fill = fill;
446
- // AppColors token 反查
447
- if (fill.type === 'solid') {
448
- var token = detectAppColorToken(fill.flutterColor);
449
- if (token) out.fill.appColorsToken = token;
450
- }
451
- }
452
-
453
- var border = getBorder(s);
454
- if (border) out.border = border;
455
-
456
- var shadows = getShadowList(s.shadows);
457
- if (shadows) {
458
- out.shadows = shadows;
459
- // 新拟态模式识别
460
- var neumorphic = detectNeumorphicPattern(
461
- s.shadows.filter(function(sh) { return sh.enabled; })
462
- );
463
- if (neumorphic) out.neumorphicPattern = neumorphic;
464
- }
465
-
466
- var innerShadows = getShadowList(s.innerShadows);
467
- if (innerShadows) out.innerShadows = innerShadows;
468
-
469
- if (layer.opacity !== undefined && layer.opacity < 1) {
470
- out.opacity = layer.opacity;
471
- }
472
- }
473
-
474
- // 圆角
475
- if (layer.cornerRadius && layer.cornerRadius > 0) {
476
- out.cornerRadius = layer.cornerRadius;
477
- out.flutterRadius = 'BorderRadius.circular(' + layer.cornerRadius + ')';
478
- }
479
-
480
- // 文字
481
- if (layer.type === 'Text') {
482
- out.textStyle = getTextStyle(layer);
483
- }
484
-
485
- // 相对父容器的原始四边距离(原始像素数据,由 AI 解读布局意图)
486
- if (parentFrame) {
487
- out.relativePosition = {
488
- left: Math.round(f.x),
489
- top: Math.round(f.y),
490
- right: Math.round(parentFrame.width - (f.x + f.width)),
491
- bottom: Math.round(parentFrame.height - (f.y + f.height)),
492
- parentW: Math.round(parentFrame.width),
493
- parentH: Math.round(parentFrame.height),
494
- };
495
- }
496
-
497
- // Tag/Badge 容器识别(输出原始样式数据)
498
- if (layer.type === 'Group') {
499
- var tagStyle = detectTagContainer(layer);
500
- if (tagStyle) out.tagStyle = tagStyle;
501
-
502
- // Icon 识别 + 双尺寸提取
503
- if (detectIfIcon(layer)) {
504
- out.isIcon = true;
505
- var iconBounds = extractIconContentBounds(layer);
506
- if (iconBounds) out.iconContentBounds = iconBounds;
507
- }
508
- }
509
-
510
- // 子图层(限深度 4 层)
511
- if (layer.layers && layer.layers.length && depth < 4) {
512
- var currentFrame = { width: f.width, height: f.height };
513
- var children = [];
514
- for (var i = 0; i < layer.layers.length; i++) {
515
- var child = measureLayer(layer.layers[i], depth + 1, currentFrame);
516
- if (child) children.push(child);
517
- }
518
- if (children.length) {
519
- out.children = children;
520
- // 布局分析(子元素间距 + Row/Column)
521
- var layout = analyzeLayout(layer, layer.layers.filter(function(c) { return !c.hidden; }));
522
- if (layout) out.layoutHint = layout;
523
- }
524
- }
525
-
526
- return out;
527
- }
528
-
529
- // ─────────────────────────────────────────────────────────────────────────────
530
- // 主逻辑
531
- // ─────────────────────────────────────────────────────────────────────────────
532
-
533
- function doMeasure() {
534
- var sketch = require('sketch');
535
- var doc = sketch.getSelectedDocument();
536
- if (!doc) {
537
- console.log(JSON.stringify({ error: 'No Sketch document open' }));
538
- return;
539
- }
540
-
541
- var sel = doc.selectedLayers.layers;
542
- var target = sel.length > 0
543
- ? sel[0]
544
- : (doc.selectedPage.canvasLevelFrames[0] || null);
545
-
546
- if (!target) {
547
- var msg = 'Please select a layer or artboard in Sketch first';
548
- console.log(JSON.stringify({ error: msg }));
549
- try { sketch.UI.alert('MTA Measure', msg); } catch (e) { /* headless */ }
550
- return;
551
- }
552
-
553
- var layers = target.layers
554
- ? target.layers.map(function(l) { return measureLayer(l, 0, { width: target.frame.width, height: target.frame.height }); }).filter(Boolean)
555
- : [measureLayer(target, 0, null)].filter(Boolean);
556
-
557
- var result = {
558
- _tool: 'mta-measure',
559
- _version: '2.1.0',
560
- _colorFormat: 'Flutter Color(0xAARRGGBB)',
561
- // 插件只采集原始数据,所有 Flutter 代码决策由 AI 根据规范文档完成
562
- _features: ['relativePosition', 'iconContentBounds', 'tagStyle', 'neumorphicPattern', 'appColorsToken', 'analyzeLayout'],
563
- _target: target.name,
564
- _size: { w: Math.round(target.frame.width), h: Math.round(target.frame.height) },
565
- layers: layers,
566
- };
567
-
568
- var json = JSON.stringify(result, null, 2);
569
- console.log(json);
570
-
571
- try { sketch.UI.message('MTA Measure 完成 - 查看开发者控制台 (Cmd+Option+J)'); } catch (e) { /* headless */ }
572
- }
573
-
574
- // ─────────────────────────────────────────────────────────────────────────────
575
- // 入口 (双模式兼容)
576
- // ─────────────────────────────────────────────────────────────────────────────
577
-
578
- // 立即执行:适用于 mcp_sketch_run_code 模式
579
- // 在 Sketch 插件模式下,此 IIFE 在脚本求值阶段运行,
580
- // 之后 Sketch 调用 manifest 指定的命令,由于无 handler 字段,不会二次调用
581
- (function() {
582
- if (_mtaMeasureRan) return;
583
- _mtaMeasureRan = true;
584
- doMeasure();
585
- })();