重构成功
This commit is contained in:
@ -1,5 +1,7 @@
|
||||
import { listAllModelFiles } from "../../utils/gltf.js";
|
||||
|
||||
export const title = "模型压缩工具";
|
||||
|
||||
const transformOptions = [
|
||||
{ value: "dedup", label: "dedup(去重)", hint: "删除重复的访问器、材质、网格" },
|
||||
{ value: "prune", label: "prune(清理无用节点)", hint: "移除未被引用的节点、材质、动画" },
|
||||
@ -27,7 +29,7 @@ const outputOptions = [
|
||||
{ value: "copy", label: "输出副本 (_compressed)", hint: "保留原文件不动,结果写入新文件" }
|
||||
];
|
||||
|
||||
export function getSteps() {
|
||||
export const getSteps = () => {
|
||||
const files = listAllModelFiles();
|
||||
const fileStep = {
|
||||
name: "模型选择",
|
||||
@ -68,4 +70,4 @@ export function getSteps() {
|
||||
default: ["overwrite", "backup"]
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,140 +1,26 @@
|
||||
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 { createStepUI } from "../../utils/stepui.js";
|
||||
import { title, getSteps } from "./config.js";
|
||||
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||
import { buildTransforms, processFile } from "./service.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();
|
||||
const run = async () => {
|
||||
const ui = createStepUI({ title, getSteps });
|
||||
const result = await ui.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] || []
|
||||
files: result.results[0] || [],
|
||||
commands: result.results[1] || [],
|
||||
quantizePreset: result.results[2] || "balanced",
|
||||
outputFormat: result.results[3] || "auto",
|
||||
outputOptions: result.results[4] || []
|
||||
};
|
||||
|
||||
showSummary([
|
||||
ui.showSummary([
|
||||
"模型文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||
"gltf-transform 命令: " + (config.commands.length ? config.commands.join(", ") : "无"),
|
||||
"量化级别: " + config.quantizePreset,
|
||||
@ -183,16 +69,12 @@ async function run() {
|
||||
}
|
||||
|
||||
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(", ")})`));
|
||||
}
|
||||
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",
|
||||
|
||||
107
lib/plugins/model/service.js
Normal file
107
lib/plugins/model/service.js
Normal file
@ -0,0 +1,107 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
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/service.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 }
|
||||
};
|
||||
|
||||
export const 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;
|
||||
};
|
||||
|
||||
const 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);
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
export const processFile = async (file, config, transforms) => {
|
||||
const output = resolveOutput(file, config);
|
||||
if (!fs.existsSync(output.sourcePath)) {
|
||||
return { ok: false, file, reason: "missing" };
|
||||
}
|
||||
|
||||
if (!transforms.length) {
|
||||
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;
|
||||
|
||||
if (output.needsConversion) {
|
||||
const tempPath = path.join(output.cwd, `${output.baseName}_temp.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 });
|
||||
}
|
||||
|
||||
return { ok: true, file, output: output.targetPath };
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
import { createStepUI } from "../../utils/stepui.js";
|
||||
import { getSteps } from "./config.js";
|
||||
|
||||
const ui = createStepUI({
|
||||
title: "模型压缩工具",
|
||||
getSteps
|
||||
});
|
||||
|
||||
export const { runInteractive, showSummary } = ui;
|
||||
Reference in New Issue
Block a user