#!/usr/bin/env node import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, copyFileSync } from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { ROOT, HOST_PROJECT, frameworkUiDirOf } from './lib/paths.mjs'; import { loadSource } from './ui-migration/gameabc-source.mjs'; import { imageBuckets } from './ui-migration/buckets.mjs'; import { convertAll } from './ui-migration/convert.mjs'; import { splitSheet } from './ui-migration/frame-io.mjs'; import { frameNames } from './ui-migration/frames.mjs'; // 与 verify-frames.mjs 一致:相对 ROOT 推导,不硬编码绝对路径。 const DEFAULT_SOURCE = join(ROOT, '..', 'projects', 'Game_Surface_3'); /** * 全量转换:产出中间描述 + 切好的散图。 * 幂等:每次先清空产出目录(spec §4.4),避免残留帧文件混进图集。 * 清空对象包括中间描述目录,以及本次实际会写入的每个 atlas-{bucket} 目录 * (按 bucket 惰性清空一次,防止在同一次运行中被同 bucket 的后续图片覆盖式清掉)。 */ export function runConvert(opts = {}) { const projectDir = opts.projectDir ?? DEFAULT_SOURCE; const intermediateDir = opts.intermediateDir ?? join(ROOT, 'ui-migration', 'intermediate'); const uiDir = opts.uiDir ?? frameworkUiDirOf(HOST_PROJECT); const source = loadSource(join(projectDir, 'output')); const bucketMap = imageBuckets(source); const { layers, manifest, warnings } = convertAll(source); // 1) 中间描述 rmSync(intermediateDir, { recursive: true, force: true }); mkdirSync(intermediateDir, { recursive: true }); writeFileSync(join(intermediateDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`); writeFileSync(join(intermediateDir, 'warnings.json'), `${JSON.stringify(warnings, null, 2)}\n`); for (const doc of layers) { const entry = manifest.layers.find((l) => l.layerId === doc.layerId); writeFileSync(join(intermediateDir, entry.file), `${JSON.stringify(doc, null, 2)}\n`); } // 2) 散图:多帧图切分,单帧图直拷 let frameCount = 0; const cleanedBuckets = new Set(); for (const image of source.images.values()) { const bucket = bucketMap.get(image.id) ?? 'unassigned'; const outDir = join(uiDir, `atlas-${bucket}`); if (!cleanedBuckets.has(bucket)) { rmSync(outDir, { recursive: true, force: true }); mkdirSync(outDir, { recursive: true }); cleanedBuckets.add(bucket); } const src = join(projectDir, 'assets', 'bmp', image.bmp); if (!existsSync(src)) { throw new Error(`图片文件不存在: ${src}(图 id=${image.id})`); } if (image.frame_all <= 1) { copyFileSync(src, join(outDir, image.bmp)); frameCount++; continue; } const names = frameNames(image); splitSheet(readFileSync(src), image).forEach((buf, i) => { writeFileSync(join(outDir, names[i]), buf); frameCount++; }); } const unassigned = layers.filter((l) => l.bucket === 'unassigned').map((l) => l.name); return { layerCount: layers.length, nodeCount: layers.reduce((s, l) => s + l.nodes.length, 0), imageCount: source.images.size, frameCount, unassigned, warningCount: warnings.length, }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const r = runConvert(); console.log(`界面 ${r.layerCount} 个 / 节点 ${r.nodeCount} 个`); console.log(`图片 ${r.imageCount} 张 → 散图 ${r.frameCount} 张`); if (r.unassigned.length) { console.warn(`\n⚠️ ${r.unassigned.length} 个层的 bucket 未定,资源已落入 atlas-unassigned,需人工归位:`); for (const n of r.unassigned) console.warn(` ${n}`); } if (r.warningCount > 0) { console.warn( `\n⚠️ ${r.warningCount} 条已知的源数据遗留问题(FrameIndex 超出图帧数,已回退为帧 1),` + '详情见 ui-migration/intermediate/warnings.json', ); } }