batch-analyze.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. // 批量分析 9 个画板 FigmaJSON,输出汇总 JSON + 紧凑表格
  2. import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
  3. import { join } from 'node:path'
  4. const dir = '.scratch/lanhu-cockpit-asset-replacement/research/raw'
  5. const files = readdirSync(dir).filter((f) => /^desktop-\d+\.json$/.test(f)).sort()
  6. function hex(c) {
  7. const h = (v) => Math.round(v * 255).toString(16).padStart(2, '0')
  8. return `#${h(c.r)}${h(c.g)}${h(c.b)}`
  9. }
  10. const summary = []
  11. for (const file of files) {
  12. const j = JSON.parse(readFileSync(join(dir, file), 'utf8'))
  13. const ab = j.artboard
  14. const fonts = new Map()
  15. const slices = []
  16. const images = []
  17. const colors = new Map()
  18. const textSamples = []
  19. let nodeCount = 0
  20. function walk(node, path) {
  21. nodeCount += 1
  22. const p = path ? `${path}/${node.name}` : node.name
  23. if (node.type === 'textLayer' && node.text) {
  24. const f = node.text.style?.font
  25. if (f && f.name) {
  26. const key = `${f.name} | ${f.size}px | w${f.fontWeight ?? ''}`
  27. fonts.set(key, (fonts.get(key) || 0) + 1)
  28. }
  29. if (textSamples.length < 12 && node.text.style?.content) textSamples.push(String(node.text.style.content).slice(0, 20))
  30. for (const r of node.text.runs || []) {
  31. const rf = r.font || r.style?.font
  32. if (rf && rf.name) {
  33. const key = `${rf.name} | ${rf.size}px | w${rf.fontWeight ?? ''}`
  34. fonts.set(key, (fonts.get(key) || 0) + 1)
  35. }
  36. }
  37. }
  38. if (node.hasExportImage || node.hasExportDDSImage) {
  39. slices.push({ path: p, w: Math.round(node.frame?.width ?? 0), h: Math.round(node.frame?.height ?? 0), dds: !!node.hasExportDDSImage })
  40. }
  41. for (const f of node.style?.fills || []) {
  42. if (f.type === 'color' && f.color) {
  43. const c = hex(f.color)
  44. colors.set(c, (colors.get(c) || 0) + 1)
  45. }
  46. const url = f.ddsImage?.imageUrl || f.image?.imageUrl || f.imageUrl
  47. if (f.type === 'image' && url) {
  48. images.push({ path: p, url, w: Math.round(node.frame?.width ?? 0), h: Math.round(node.frame?.height ?? 0) })
  49. }
  50. }
  51. for (const ch of node.layers || []) walk(ch, p)
  52. }
  53. for (const l of ab.layers || []) walk(l, '')
  54. summary.push({
  55. file,
  56. artboard: ab.name,
  57. frame: { w: ab.frame.width, h: ab.frame.height },
  58. rootExportFlags: { hasExportImage: ab.hasExportImage, hasExportDDSImage: ab.hasExportDDSImage },
  59. nodeCount,
  60. sliceCount: slices.length,
  61. ddsSliceCount: slices.filter((s) => s.dds).length,
  62. topSlices: slices.filter((s) => s.w >= 24 && s.h >= 24).slice(0, 12),
  63. fonts: [...fonts.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ({ font: k, count: v })),
  64. imageRefCount: images.length,
  65. images: images.map((i) => ({ path: i.path, w: i.w, h: i.h, url: i.url })),
  66. topColors: [...colors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 16).map(([k, v]) => ({ color: k, count: v })),
  67. assetFiles: j.assets || [],
  68. textSamples,
  69. })
  70. }
  71. writeFileSync(join(dir, 'analysis-summary.json'), JSON.stringify(summary, null, 1))
  72. console.log('artboard | frame | nodes | slices(dds) | fonts | imgFills | colors | assets')
  73. for (const s of summary) {
  74. console.log(
  75. `${s.artboard} | ${s.frame.w}x${s.frame.h} | ${s.nodeCount} | ${s.sliceCount}(${s.ddsSliceCount}) | ${s.fonts.length} | ${s.imageRefCount} | ${s.topColors.length} | ${s.assetFiles.length}`,
  76. )
  77. }
  78. const allFonts = new Set()
  79. for (const s of summary) for (const f of s.fonts) allFonts.add(f.font.split(' | ')[0])
  80. console.log('\nall font families:', [...allFonts].join(' / '))