UNPKG

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