Files
yinx-cli/lib/plugins/model/index.js
2025-12-22 12:07:12 +08:00

203 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from "fs";
import path from "path";
import color from "picocolors";
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { dedup, prune, resample, weld, quantize } from "@gltf-transform/functions";
import { BACKUP_SUFFIX, needsConversion } from "../../utils/gltf.js";
import { convert } from "../convert/converters.js";
import { runInteractive, showSummary } from "./ui.js";
import { stopKeypress, waitForKey } from "../../keyboard.js";
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const QUANTIZE_PRESETS = {
high: { position: 16, normal: 12, texcoord: 14, color: 10, generic: 12 },
balanced: { position: 14, normal: 10, texcoord: 12, color: 8, generic: 12 },
aggressive: { position: 12, normal: 8, texcoord: 10, color: 8, generic: 10 },
light: { position: 10, normal: 8, texcoord: 10, color: 8, generic: 10 }
};
function buildTransforms(config) {
const transforms = [];
const selected = new Set(config.commands);
if (selected.has("dedup")) transforms.push(dedup());
if (selected.has("prune")) transforms.push(prune());
if (selected.has("resample")) transforms.push(resample());
if (selected.has("weld")) transforms.push(weld());
if (selected.has("quantize")) {
const preset = QUANTIZE_PRESETS[config.quantizePreset] || QUANTIZE_PRESETS.balanced;
transforms.push(quantize({
quantizePosition: preset.position,
quantizeNormal: preset.normal,
quantizeTexcoord: preset.texcoord,
quantizeColor: preset.color,
quantizeGeneric: preset.generic
}));
}
return transforms;
}
function resolveOutput(file, config) {
const cwd = process.cwd();
const originalExt = path.extname(file).toLowerCase() || ".gltf";
const baseName = originalExt ? file.slice(0, -originalExt.length) : file;
const isConvertible = needsConversion(file);
// OBJ/FBX 默认输出 GLB除非指定了 gltf
const targetExt = config.outputFormat === "gltf" ? ".gltf"
: (config.outputFormat === "glb" || isConvertible) ? ".glb"
: originalExt;
const wantsCopy = config.outputOptions.includes("copy");
const wantsOverwrite = config.outputOptions.includes("overwrite");
const mode = wantsCopy ? "copy" : (wantsOverwrite ? "overwrite" : "copy");
const backup = config.outputOptions.includes("backup") && mode === "overwrite";
const targetName = mode === "copy"
? `${baseName}_compressed${targetExt}`
: `${baseName}${targetExt}`;
return {
cwd,
mode,
backup,
sourcePath: path.join(cwd, file),
sourceExt: originalExt,
targetExt,
targetPath: path.join(cwd, targetName),
baseName,
needsConversion: isConvertible
};
}
async function processFile(file, config, transforms) {
const output = resolveOutput(file, config);
if (!fs.existsSync(output.sourcePath)) {
console.log(color.yellow("跳过,文件不存在: " + file));
return { ok: false, file, reason: "missing" };
}
if (!transforms.length) {
console.log(color.yellow("未选择任何 gltf-transform 操作,已中止"));
return { ok: false, file, reason: "no-transform" };
}
if (output.backup) {
const backupName = `${output.baseName}${BACKUP_SUFFIX}${output.sourceExt}`;
const backupPath = path.join(output.cwd, backupName);
fs.copyFileSync(output.sourcePath, backupPath);
}
let inputPath = output.sourcePath;
// 如果是 OBJ/FBX先转换为临时 GLB
if (output.needsConversion) {
const tempPath = path.join(output.cwd, `${output.baseName}_temp.glb`);
console.log(color.dim("转换中: " + file + " → GLB"));
await convert(output.sourcePath, tempPath, { binary: true });
inputPath = tempPath;
}
const document = io.read(inputPath);
await document.transform(...transforms);
io.write(output.targetPath, document);
// 清理临时文件
if (output.needsConversion) {
fs.rmSync(inputPath, { force: true });
}
if (output.mode === "overwrite" && output.targetPath !== output.sourcePath) {
fs.rmSync(output.sourcePath, { force: true });
}
if (output.mode === "overwrite" && output.targetPath !== output.sourcePath) {
console.log(color.dim("已生成新文件: " + path.basename(output.targetPath)));
}
return { ok: true, file, output: output.targetPath };
}
async function run() {
const result = await runInteractive();
if (!result) return "back";
stopKeypress();
const { results } = result;
const config = {
files: results[0] || [],
commands: results[1] || [],
quantizePreset: results[2] || "balanced",
outputFormat: results[3] || "auto",
outputOptions: results[4] || []
};
showSummary([
"模型文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
"gltf-transform 命令: " + (config.commands.length ? config.commands.join(", ") : "无"),
"量化级别: " + config.quantizePreset,
"输出格式: " + config.outputFormat,
"输出选项: " + (config.outputOptions.length ? config.outputOptions.join(", ") : "默认")
]);
if (!config.files.length) {
console.log(color.yellow("未选择任何模型文件"));
await waitForKey();
return "back";
}
const transforms = buildTransforms(config);
if (!transforms.length) {
console.log(color.yellow("未配置任何压缩命令,请至少选择一项 gltf-transform 操作"));
await waitForKey();
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, transforms);
if (result.ok) {
success++;
console.log(color.green(`${progress}${file}${path.basename(result.output)}`));
} else {
failed.push(file);
console.log(color.yellow(`${progress} ⊘ 跳过: ${file}`));
}
} catch (err) {
failed.push(file);
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} 个 (${failed.join(", ")})`));
}
await waitForKey();
return "back";
}
export default {
id: "model",
name: "模型压缩",
desc: "glTF/GLB/OBJ/FBX压缩",
run,
};