增加转格式

This commit is contained in:
yinsx
2025-12-20 11:51:35 +08:00
parent 5315a97613
commit d9abc57b0b
32 changed files with 4339 additions and 229 deletions

35
lib/convert/config.js Normal file
View File

@ -0,0 +1,35 @@
import fs from "fs";
import { getImportExtensions, getExportFormats } from "./converters.js";
import { sortByUsage } from "../stats.js";
export function listConvertibleFiles() {
const cwd = process.cwd();
const exts = getImportExtensions();
return fs.readdirSync(cwd).filter(file => {
const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
return exts.includes(ext);
});
}
export function getSteps() {
const files = listConvertibleFiles();
const formats = sortByUsage("convert_format", getExportFormats());
return [
{
name: "源文件",
type: "multiselect",
message: files.length ? "选择要转换的模型文件" : "当前目录未找到可转换的模型文件",
options: files.map(file => ({ value: file, label: file })),
default: [],
emptyMessage: "请按 Esc 返回并放入模型文件后重试"
},
{
name: "输出格式",
type: "select",
message: "选择目标格式",
options: formats.map(f => ({ value: f, label: f })),
default: formats[0]
}
];
}

31
lib/convert/converters.js Normal file
View File

@ -0,0 +1,31 @@
import { spawnSync } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ASSIMP_PATH = path.join(__dirname, "../../bin/assimp.exe");
let importExts = null;
let exportFormats = null;
export function getImportExtensions() {
if (!importExts) {
const r = spawnSync(ASSIMP_PATH, ["listext"], { encoding: "utf8" });
importExts = r.stdout.trim().split(";").map(e => e.replace("*", "").toLowerCase());
}
return importExts;
}
export function getExportFormats() {
if (!exportFormats) {
const r = spawnSync(ASSIMP_PATH, ["listexport"], { encoding: "utf8" });
exportFormats = r.stdout.trim().split(/\r?\n/).filter(Boolean);
}
return exportFormats;
}
export async function convert(inputFile, outputFile, format, cwd = process.cwd()) {
const r = spawnSync(ASSIMP_PATH, ["export", inputFile, outputFile, `-f${format}`], { encoding: "utf8", cwd });
if (r.status !== 0) throw new Error(r.stderr || r.stdout || "转换失败");
return outputFile;
}

108
lib/convert/index.js Normal file
View File

@ -0,0 +1,108 @@
import fs from "fs";
import path from "path";
import color from "picocolors";
import { convert } from "./converters.js";
import { runInteractive, showSummary } from "./ui.js";
import { stopKeypress, initKeypress, onKey } from "../keyboard.js";
import { record } from "../stats.js";
const FORMAT_EXT = {
collada: ".dae", x: ".x", stp: ".stp", obj: ".obj", objnomtl: ".obj",
stl: ".stl", stlb: ".stl", ply: ".ply", plyb: ".ply", "3ds": ".3ds",
gltf2: ".gltf", glb2: ".glb", gltf: ".gltf", glb: ".glb",
assbin: ".assbin", assxml: ".assxml", x3d: ".x3d",
fbx: ".fbx", fbxa: ".fbx", "3mf": ".3mf", pbrt: ".pbrt", assjson: ".json"
};
function getOutputExt(format) {
return FORMAT_EXT[format] || "." + format;
}
async function processFile(file, config) {
const cwd = process.cwd();
const baseName = file.slice(0, file.lastIndexOf("."));
const outputExt = getOutputExt(config.outputFormat);
const outputFile = baseName + outputExt;
if (!fs.existsSync(path.join(cwd, file))) {
return { ok: false, file, reason: "文件不存在" };
}
await convert(file, outputFile, config.outputFormat, cwd);
return { ok: true, file, output: outputFile };
}
async function waitForEsc() {
initKeypress();
return new Promise(resolve => {
onKey((str, key) => {
if (key?.name === "escape" || (key?.ctrl && key?.name === "c")) {
resolve();
}
});
});
}
export async function run() {
const result = await runInteractive();
if (!result) return "back";
const { steps, results } = result;
const config = {
files: results[0] || [],
outputFormat: results[1] || "glb2"
};
stopKeypress();
showSummary([
"源文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
"目标格式: " + config.outputFormat.toUpperCase()
]);
if (!config.files.length) {
console.log(color.yellow("未选择任何模型文件"));
console.log(color.dim("\n按 Esc 返回"));
await waitForEsc();
return "back";
}
const total = config.files.length;
let success = 0;
const failed = [];
console.log(color.cyan(`开始转换 ${total} 个文件...\n`));
for (let i = 0; i < config.files.length; i++) {
const file = config.files[i];
const progress = `[${i + 1}/${total}]`;
console.log(color.dim(`${progress} 处理中: ${file}`));
try {
const result = await processFile(file, config);
if (result.ok) {
success++;
record("convert_format", config.outputFormat);
console.log(color.green(`${progress}${file}${path.basename(result.output)}`));
} else {
failed.push({ file, reason: result.reason });
console.log(color.yellow(`${progress} ⊘ 跳过: ${file} (${result.reason})`));
}
} catch (err) {
failed.push({ file, reason: err?.message || String(err) });
console.log(color.red(`${progress} ✖ 失败: ${file}`));
console.log(color.dim(" " + String(err?.message || err)));
}
}
console.log("\n" + color.bgGreen(color.black(" 转换完成 ")));
if (success) {
console.log(color.green(`成功: ${success}`));
}
if (failed.length) {
console.log(color.yellow(`失败: ${failed.length}`));
}
console.log(color.dim("\n按 Esc 返回"));
await waitForEsc();
return "back";
}

9
lib/convert/ui.js Normal file
View File

@ -0,0 +1,9 @@
import { createStepUI } from "../utils/stepui.js";
import { getSteps } from "./config.js";
const ui = createStepUI({
title: "格式转换工具",
getSteps
});
export const { runInteractive, showSummary } = ui;