download-v2-assets.mjs 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. // 下载新版全部切图到 design-archive-v2/,按 PNG 实际像素尺寸匹配回导出图层
  2. import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs'
  3. import { createHash } from 'node:crypto'
  4. import { join } from 'node:path'
  5. const raw = '.scratch/lanhu-cockpit-asset-replacement/research/raw-v2'
  6. const outDir = '.scratch/lanhu-cockpit-asset-replacement/design-archive-v2'
  7. mkdirSync(outDir, { recursive: true })
  8. const files = readdirSync(raw).filter((f) => /^desktop-\d+\.json$/.test(f)).sort()
  9. function pngSize(buf) {
  10. if (buf.length < 24 || buf.readUInt32BE(0) !== 0x89504e47) return null
  11. return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) }
  12. }
  13. const perArtboard = []
  14. for (const file of files) {
  15. const j = JSON.parse(readFileSync(join(raw, file), 'utf8'))
  16. const ab = j.artboard
  17. const exported = []
  18. ;(function walk(node, path) {
  19. const p = path ? `${path}/${node.name}` : node.name
  20. if (node.hasExportImage) exported.push({ path: p, w: Math.round(node.frame?.width ?? 0), h: Math.round(node.frame?.height ?? 0), type: node.type })
  21. for (const ch of node.layers || []) walk(ch, p)
  22. })(ab, '')
  23. perArtboard.push({ artboard: ab.name, exported, assets: j.assets || [] })
  24. }
  25. const index = []
  26. for (const { artboard, exported, assets } of perArtboard) {
  27. const pngs = []
  28. for (let i = 0; i < assets.length; i++) {
  29. const url = assets[i]
  30. if (!/\.png($|\?)/i.test(url)) continue
  31. const res = await fetch(url)
  32. if (!res.ok) continue
  33. const buf = Buffer.from(await res.arrayBuffer())
  34. const size = pngSize(buf)
  35. pngs.push({ url, buf, ...(size || {}) })
  36. }
  37. // 按像素尺寸匹配回图层(sliceScale 在旧版为 4,新版以实际像素为准)
  38. const used = new Set()
  39. for (const p of pngs) {
  40. const scale = p.w && exported.find((e) => e.w) ? p.w / exported.find((e) => e.w).w : 0
  41. let best = null
  42. let bestScore = Infinity
  43. for (const e of exported) {
  44. if (used.has(e.path)) continue
  45. const sr = e.w ? p.w / e.w : 0
  46. const shr = e.h ? p.h / e.h : 0
  47. if (sr <= 0 || Math.abs(sr - shr) > 0.02) continue
  48. const score = Math.abs(sr - Math.max(1, Math.round(sr)))
  49. if (score < bestScore) {
  50. bestScore = score
  51. best = e
  52. }
  53. }
  54. if (best) used.add(best.path)
  55. const hash8 = (p.url.match(/([0-9a-f]{8})[0-9a-f]*\.png$/i) || [, 'x'])[1]
  56. const name = `${artboard.replace(/\s+/g, '_')}__${hash8}_${p.w}x${p.h}.png`
  57. writeFileSync(join(outDir, name), p.buf)
  58. index.push({
  59. artboard,
  60. layer: best ? best.path : '(unmatched)',
  61. frame: best ? `${best.w}x${best.h}` : '',
  62. file: name,
  63. px: `${p.w}x${p.h}`,
  64. bytes: p.buf.length,
  65. sha256: createHash('sha256').update(p.buf).digest('hex'),
  66. url: p.url,
  67. })
  68. }
  69. }
  70. writeFileSync(join(outDir, 'index.json'), JSON.stringify({ generatedFrom: 'raw-v2', count: index.length, files: index }, null, 1))
  71. console.log('downloaded png slices:', index.length)
  72. const bySize = {}
  73. for (const f of index) {
  74. const k = f.px
  75. bySize[k] = bySize[k] || []
  76. bySize[k].push(f)
  77. }
  78. console.log('\nsmall slices (likely icons):')
  79. for (const [px, list] of Object.entries(bySize).sort()) {
  80. const [w, h] = px.split('x').map(Number)
  81. if (w <= 64 && h <= 64) {
  82. for (const f of list) console.log(` ${px} | ${f.artboard} | ${f.layer}`)
  83. }
  84. }
  85. console.log('\nall sizes:', Object.keys(bySize).sort((a, b) => parseInt(a) - parseInt(b)).join(' '))