figma-restoration-tools
Version:
Professional Figma Component Restoration Kit - MCP tools with snapDOM-powered high-quality screenshots, intelligent shadow detection, and smart debugging for Vue component restoration. Includes figma_compare and snapdom_screenshot tools.
227 lines (190 loc) • 8.61 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* 智能图片对比 - 使用sharp调整尺寸后进行对比
*/
async function smartCompareImages(expectedPath, actualPath, diffOutputPath) {
console.log('🔍 开始智能图片对比...');
try {
// 读取图片
let expectedBuffer = fs.readFileSync(expectedPath);
let actualBuffer = fs.readFileSync(actualPath);
const expectedSharp = sharp(expectedBuffer);
const actualSharp = sharp(actualBuffer);
const expectedMeta = await expectedSharp.metadata();
const actualMeta = await actualSharp.metadata();
console.log(`📐 期望图片尺寸: ${expectedMeta.width} × ${expectedMeta.height}`);
console.log(`📐 实际图片尺寸: ${actualMeta.width} × ${actualMeta.height}`);
// 如果尺寸不匹配,调整期望图片尺寸
if (expectedMeta.width !== actualMeta.width || expectedMeta.height !== actualMeta.height) {
console.warn('⚠️ 图片尺寸不匹配,正在调整期望图片尺寸...');
const scaleX = (actualMeta.width / expectedMeta.width).toFixed(3);
const scaleY = (actualMeta.height / expectedMeta.height).toFixed(3);
console.warn(` 缩放比例: X轴 ${scaleX}x, Y轴 ${scaleY}x`);
expectedBuffer = await expectedSharp
.resize(actualMeta.width, actualMeta.height, {
fit: 'fill',
kernel: sharp.kernel.nearest
})
.png()
.toBuffer();
console.log('✅ 期望图片已调整为实际图片尺寸');
} else {
console.log('✅ 图片尺寸完全匹配,无需调整');
}
// 使用 PNG.js 解析图片
const expectedPng = PNG.sync.read(expectedBuffer);
const actualPng = PNG.sync.read(actualBuffer);
const { width, height } = actualPng;
// 创建差异图
const diff = new PNG({ width, height });
// 进行像素匹配
const diffPixelCount = pixelmatch(expectedPng.data, actualPng.data, diff.data, width, height, {
threshold: 0.1,
includeAA: false,
alpha: 0.1,
diffColor: [255, 0, 0], // 红色
aaColor: [255, 255, 0] // 黄色
});
// 保存差异图
fs.writeFileSync(diffOutputPath, PNG.sync.write(diff));
const totalPixels = width * height;
const matchPercentage = ((totalPixels - diffPixelCount) / totalPixels * 100).toFixed(2);
console.log(`\n📊 对比结果:`);
console.log(` 匹配度: ${matchPercentage}%`);
console.log(` 差异像素: ${diffPixelCount}/${totalPixels}`);
console.log(` 差异图已保存: ${diffOutputPath}`);
return {
success: true,
matchPercentage: parseFloat(matchPercentage),
diffPixels: diffPixelCount,
totalPixels,
dimensions: { width, height }
};
} catch (error) {
console.error('❌ 对比失败:', error.message);
return {
success: false,
error: error.message
};
}
}
/**
* 处理所有组件的智能对比
*/
async function processAllComponents() {
const components = ['ModalRemoveMember', 'ExchangeSuccess', 'AssignmentComplete', 'ScanComplete', 'ScanResult'];
const results = {};
console.log('🚀 开始智能对比所有组件...\n');
for (const component of components) {
console.log(`🔧 处理 ${component}...`);
const resultsDir = path.join(__dirname, '../results', component);
const publicResultsDir = path.join(__dirname, '../public/results', component);
const expectedPath = path.join(resultsDir, `${component}_expected.png`);
const actualPath = path.join(resultsDir, 'actual.png');
const diffPath = path.join(resultsDir, 'diff.png');
const publicDiffPath = path.join(publicResultsDir, 'diff.png');
if (fs.existsSync(expectedPath) && fs.existsSync(actualPath)) {
const result = await smartCompareImages(expectedPath, actualPath, diffPath);
if (result.success) {
// 复制到public目录
fs.copyFileSync(diffPath, publicDiffPath);
// 生成更新的报告
const reportData = {
componentName: component,
timestamp: new Date().toISOString(),
summary: {
matchPercentage: result.matchPercentage,
pixelMatch: result.diffPixels,
componentCreated: true,
componentRendered: true,
screenshotTaken: true,
comparisonAvailable: true,
files: {
component: `/mcp-vue-tools/src/components/${component}/index.vue`,
screenshot: `/results/${component}/actual.png`,
expected: `/results/${component}/expected.png`,
diff: `/results/${component}/diff.png`
},
urls: {
test: `http://localhost:83/component/${component}`,
figma: getFigmaUrl(component)
}
},
comparison: {
expectedDimensions: result.dimensions,
actualDimensions: result.dimensions,
diffDimensions: result.dimensions,
dimensionMismatch: false
},
steps: {
create: { success: true, message: "Component created successfully" },
render: { success: true, message: "Component rendered successfully" },
screenshot: { success: true, message: "Screenshot captured successfully" },
comparison: { success: true, message: `Image comparison completed with ${result.matchPercentage}% match` }
}
};
// 保存报告
const publicApiDir = path.join(__dirname, '../public/api');
const reportPath = path.join(publicApiDir, `${component}.json`);
fs.writeFileSync(reportPath, JSON.stringify(reportData, null, 2));
console.log(`✅ ${component} 对比完成,匹配度: ${result.matchPercentage}%`);
results[component] = result;
} else {
console.log(`❌ ${component} 对比失败: ${result.error}`);
results[component] = result;
}
} else {
console.log(`⚠️ ${component} 缺少图片文件`);
results[component] = { success: false, error: 'Missing images' };
}
console.log(''); // 空行分隔
}
// 输出总结
console.log('📊 智能对比结果总结:');
for (const [component, result] of Object.entries(results)) {
if (result.success) {
const status = result.matchPercentage >= 99 ? '🎯 完美' :
result.matchPercentage >= 95 ? '✅ 优秀' :
result.matchPercentage >= 90 ? '👍 良好' : '⚠️ 需改进';
console.log(`${component}: ${result.matchPercentage}% ${status}`);
} else {
console.log(`${component}: ❌ 失败`);
}
}
return results;
}
function getFigmaUrl(componentName) {
const urls = {
'ModalRemoveMember': 'https://www.figma.com/design/Mbz0mgLIVbz46bxPwBFnSl/CS%E4%BC%81%E4%B8%9A%E7%89%88?node-id=16343-60515&m=dev',
'ExchangeSuccess': 'https://www.figma.com/design/D59IG2u1r4gfGQ56qdIlyB/6.92.0?node-id=5276-10661&m=dev',
'AssignmentComplete': 'https://www.figma.com/design/WLaxbF2J6FAm6eBmXmG4tx/v20.14.0-v20.5.0%E8%9C%9C%E8%9C%82%E5%AE%B6%E6%A0%A1%E6%A0%A1%E5%9B%AD%E7%89%88--%E4%BA%A7%E5%93%81%E8%AE%BE%E8%AE%A1%E7%89%88%E6%9C%AC?node-id=12712-178431&m=dev',
'ScanComplete': 'https://www.figma.com/design/WLaxbF2J6FAm6eBmXmG4tx/v20.14.0-v20.5.0%E8%9C%9C%E8%9C%82%E5%AE%B6%E6%A0%A1%E6%A0%A1%E5%9B%AD%E7%89%88--%E4%BA%A7%E5%93%81%E8%AE%BE%E8%AE%A1%E7%89%88%E6%9C%AC?node-id=12739-182336&m=dev'
};
return urls[componentName] || 'https://www.figma.com/design/';
}
// 命令行工具
if (import.meta.url === `file://${process.argv[1]}`) {
const command = process.argv[2];
if (command === 'single') {
const componentName = process.argv[3];
if (!componentName) {
console.error('用法: node smart-compare.js single <组件名>');
process.exit(1);
}
const resultsDir = path.join(__dirname, '../results', componentName);
const expectedPath = path.join(resultsDir, `${componentName}_expected.png`);
const actualPath = path.join(resultsDir, 'actual.png');
const diffPath = path.join(resultsDir, 'diff.png');
smartCompareImages(expectedPath, actualPath, diffPath);
} else {
processAllComponents();
}
}
export { smartCompareImages, processAllComponents };