重构成功
This commit is contained in:
@ -20,7 +20,8 @@
|
|||||||
"Bash(for dir in gltf ktx2 model scaffold)",
|
"Bash(for dir in gltf ktx2 model scaffold)",
|
||||||
"Bash(do cp lib/$dir/*.js lib/plugins/$dir/)",
|
"Bash(do cp lib/$dir/*.js lib/plugins/$dir/)",
|
||||||
"Bash(done)",
|
"Bash(done)",
|
||||||
"Bash(timeout 3 node:*)"
|
"Bash(timeout 3 node:*)",
|
||||||
|
"Bash(find:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"convert_format": {
|
"convert_format": {
|
||||||
"stp": 1,
|
"stp": 1,
|
||||||
"glb2": 2
|
"glb2": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
21
lib/paths.js
Normal file
21
lib/paths.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// 项目根目录
|
||||||
|
export const ROOT_DIR = path.join(__dirname, "..");
|
||||||
|
|
||||||
|
// bin 目录
|
||||||
|
export const BIN_DIR = path.join(ROOT_DIR, "bin");
|
||||||
|
|
||||||
|
// 工具路径
|
||||||
|
export const MODEL_CONVERTER = path.join(BIN_DIR, "model_converter.exe");
|
||||||
|
export const TEXTURE_TOOL = path.join(BIN_DIR, "texture_tool.exe");
|
||||||
|
|
||||||
|
// lib 子目录
|
||||||
|
export const LIB_DIR = __dirname;
|
||||||
|
export const PLUGINS_DIR = path.join(LIB_DIR, "plugins");
|
||||||
|
export const UTILS_DIR = path.join(LIB_DIR, "utils");
|
||||||
|
export const TOOLS_DIR = path.join(LIB_DIR, "tools");
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { getImportExtensions, getExportFormats } from "./converters.js";
|
import { getImportExtensions, getExportFormats } from "./service.js";
|
||||||
import { sortByUsage } from "../../stats.js";
|
import { sortByUsage } from "../../stats.js";
|
||||||
|
|
||||||
const EXCLUDED_FORMATS = ["x", "glb", "gltf"];
|
const EXCLUDED_FORMATS = ["x", "glb", "gltf"];
|
||||||
|
|
||||||
|
export const title = "格式转换工具";
|
||||||
|
|
||||||
export function listConvertibleFiles() {
|
export function listConvertibleFiles() {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const exts = getImportExtensions();
|
const exts = getImportExtensions();
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
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/model_converter.exe");
|
|
||||||
|
|
||||||
let importExts = null;
|
|
||||||
let exportFormats = null;
|
|
||||||
|
|
||||||
export function getImportExtensions() {
|
|
||||||
if (!importExts) {
|
|
||||||
const r = spawnSync(ASSIMP_PATH, ["listext"], { encoding: "utf8" });
|
|
||||||
if (!r.stdout) throw new Error("无法获取支持的导入格式,请检查 model_converter.exe 是否存在");
|
|
||||||
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" });
|
|
||||||
if (!r.stdout) throw new Error("无法获取支持的导出格式,请检查 model_converter.exe 是否存在");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,61 +1,30 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { convert } from "./converters.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { runInteractive, showSummary } from "./ui.js";
|
import { title, getSteps } from "./config.js";
|
||||||
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
import { record } from "../../stats.js";
|
import { record } from "../../stats.js";
|
||||||
|
import { processFile } from "./service.js";
|
||||||
|
|
||||||
const FORMAT_EXT = {
|
const run = async () => {
|
||||||
collada: ".dae",stp: ".stp", obj: ".obj", objnomtl: ".obj",
|
const ui = createStepUI({ title, getSteps });
|
||||||
stl: ".stl", stlb: ".stl", ply: ".ply", plyb: ".ply", "3ds": ".3ds",
|
const result = await ui.runInteractive();
|
||||||
gltf2: ".gltf", glb2: ".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 { dir, name } = path.parse(file);
|
|
||||||
const outputExt = getOutputExt(config.outputFormat);
|
|
||||||
const outputFile = path.join(dir || "", `${name}${outputExt}`);
|
|
||||||
const absolutePath = path.join(cwd, file);
|
|
||||||
|
|
||||||
if (!fs.existsSync(absolutePath)) {
|
|
||||||
return { ok: false, file, reason: "文件不存在" };
|
|
||||||
}
|
|
||||||
|
|
||||||
await convert(file, outputFile, config.outputFormat, cwd);
|
|
||||||
return { ok: true, file, output: outputFile };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForEsc() {
|
|
||||||
return waitForKey(color.dim("按 Esc 返回"), key => key?.name === "escape" || (key?.ctrl && key?.name === "c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const result = await runInteractive();
|
|
||||||
if (!result) return "back";
|
if (!result) return "back";
|
||||||
|
|
||||||
const { results } = result;
|
|
||||||
const config = {
|
const config = {
|
||||||
files: results[0] || [],
|
files: result.results[0] || [],
|
||||||
outputFormat: results[1] || "glb2"
|
outputFormat: result.results[1] || "glb2"
|
||||||
};
|
};
|
||||||
|
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
showSummary([
|
ui.showSummary([
|
||||||
"源文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
"源文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||||
"目标格式: " + config.outputFormat.toUpperCase()
|
"目标格式: " + config.outputFormat.toUpperCase()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!config.files.length) {
|
if (!config.files.length) {
|
||||||
console.log(color.yellow("未选择任何模型文件"));
|
console.log(color.yellow("未选择任何模型文件"));
|
||||||
await waitForEsc();
|
await waitForKey(color.dim("按 Esc 返回"), key => key?.name === "escape" || (key?.ctrl && key?.name === "c"));
|
||||||
return "back";
|
return "back";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,16 +57,12 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("\n" + color.bgGreen(color.black(" 转换完成 ")));
|
console.log("\n" + color.bgGreen(color.black(" 转换完成 ")));
|
||||||
if (success) {
|
if (success) console.log(color.green(`成功: ${success} 个`));
|
||||||
console.log(color.green(`成功: ${success} 个`));
|
if (failed.length) console.log(color.yellow(`失败: ${failed.length} 个`));
|
||||||
}
|
|
||||||
if (failed.length) {
|
|
||||||
console.log(color.yellow(`失败: ${failed.length} 个`));
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitForEsc();
|
await waitForKey(color.dim("按 Esc 返回"), key => key?.name === "escape" || (key?.ctrl && key?.name === "c"));
|
||||||
return "back";
|
return "back";
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "convert",
|
id: "convert",
|
||||||
|
|||||||
65
lib/plugins/convert/service.js
Normal file
65
lib/plugins/convert/service.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { spawnSync } from "child_process";
|
||||||
|
import { MODEL_CONVERTER } from "../../paths.js";
|
||||||
|
|
||||||
|
// 格式映射
|
||||||
|
const FORMAT_EXT = {
|
||||||
|
collada: ".dae", stp: ".stp", obj: ".obj", objnomtl: ".obj",
|
||||||
|
stl: ".stl", stlb: ".stl", ply: ".ply", plyb: ".ply", "3ds": ".3ds",
|
||||||
|
gltf2: ".gltf", glb2: ".glb",
|
||||||
|
assbin: ".assbin", assxml: ".assxml", x3d: ".x3d",
|
||||||
|
fbx: ".fbx", fbxa: ".fbx", "3mf": ".3mf", pbrt: ".pbrt", assjson: ".json"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 缓存
|
||||||
|
let importExts = null;
|
||||||
|
let exportFormats = null;
|
||||||
|
|
||||||
|
// 获取支持的导入格式
|
||||||
|
export const getImportExtensions = () => {
|
||||||
|
if (!importExts) {
|
||||||
|
const r = spawnSync(MODEL_CONVERTER, ["listext"], { encoding: "utf8" });
|
||||||
|
if (!r.stdout) throw new Error("无法获取支持的导入格式,请检查 model_converter.exe 是否存在");
|
||||||
|
importExts = r.stdout.trim().split(";").map(e => e.replace("*", "").toLowerCase());
|
||||||
|
}
|
||||||
|
return importExts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取支持的导出格式
|
||||||
|
export const getExportFormats = () => {
|
||||||
|
if (!exportFormats) {
|
||||||
|
const r = spawnSync(MODEL_CONVERTER, ["listexport"], { encoding: "utf8" });
|
||||||
|
if (!r.stdout) throw new Error("无法获取支持的导出格式,请检查 model_converter.exe 是否存在");
|
||||||
|
exportFormats = r.stdout.trim().split(/\r?\n/).filter(Boolean);
|
||||||
|
}
|
||||||
|
return exportFormats;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换文件
|
||||||
|
export const convert = async (inputFile, outputFile, format, cwd = process.cwd()) => {
|
||||||
|
const r = spawnSync(MODEL_CONVERTER, ["export", inputFile, outputFile, `-f${format}`], { encoding: "utf8", cwd });
|
||||||
|
if (r.status !== 0) throw new Error(r.stderr || r.stdout || "转换失败");
|
||||||
|
return outputFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取输出文件扩展名
|
||||||
|
const getOutputExt = (format) => {
|
||||||
|
return FORMAT_EXT[format] || "." + format;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理单个文件
|
||||||
|
export const processFile = async (file, config) => {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const { dir, name } = path.parse(file);
|
||||||
|
const outputExt = getOutputExt(config.outputFormat);
|
||||||
|
const outputFile = path.join(dir || "", `${name}${outputExt}`);
|
||||||
|
const absolutePath = path.join(cwd, file);
|
||||||
|
|
||||||
|
if (!fs.existsSync(absolutePath)) {
|
||||||
|
return { ok: false, file, reason: "文件不存在" };
|
||||||
|
}
|
||||||
|
|
||||||
|
await convert(file, outputFile, config.outputFormat, cwd);
|
||||||
|
return { ok: true, file, output: outputFile };
|
||||||
|
};
|
||||||
@ -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;
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { listGltfFiles } from "../../utils/gltf.js";
|
import { listGltfFiles } from "../../utils/gltf.js";
|
||||||
|
|
||||||
|
export const title = "glTF 扩展工具";
|
||||||
|
|
||||||
const extensionStep = {
|
const extensionStep = {
|
||||||
name: "扩展选项",
|
name: "扩展选项",
|
||||||
type: "multiselect",
|
type: "multiselect",
|
||||||
@ -13,7 +15,7 @@ const extensionStep = {
|
|||||||
default: ["textureBasisu"]
|
default: ["textureBasisu"]
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getSteps() {
|
export const getSteps = () => {
|
||||||
const files = listGltfFiles();
|
const files = listGltfFiles();
|
||||||
const fileStep = {
|
const fileStep = {
|
||||||
name: "文件选择",
|
name: "文件选择",
|
||||||
@ -24,4 +26,4 @@ export function getSteps() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [fileStep, extensionStep];
|
return [fileStep, extensionStep];
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,76 +1,36 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { checkRequiredFiles, modifyGltfContent, listGltfFiles, BACKUP_SUFFIX } from "../../utils/gltf.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { runInteractive, showSummary } from "./ui.js";
|
import { title, getSteps } from "./config.js";
|
||||||
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
|
import { runGltfExtension } from "./service.js";
|
||||||
|
|
||||||
export function runGltfExtension(config = { files: [], extensions: ["textureBasisu"] }) {
|
const run = async () => {
|
||||||
const cwd = process.cwd();
|
const ui = createStepUI({ title, getSteps });
|
||||||
const { files = [], extensions = ["textureBasisu"] } = config;
|
const result = await ui.runInteractive();
|
||||||
const selectedExtensions = extensions.length ? extensions : ["textureBasisu"];
|
|
||||||
|
|
||||||
const { ok, missing } = checkRequiredFiles();
|
|
||||||
if (!ok) {
|
|
||||||
console.log(color.red("\n✖ 缺少必要文件: " + missing.join(", ")));
|
|
||||||
console.log(color.dim("请确保当前目录包含 .ktx2、.gltf 和 .bin 文件\n"));
|
|
||||||
return { success: false, count: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackFiles = listGltfFiles();
|
|
||||||
const gltfFiles = (files.length ? files : fallbackFiles).filter(f => f.toLowerCase().endsWith(".gltf"));
|
|
||||||
if (!gltfFiles.length) {
|
|
||||||
console.log(color.yellow("未选择可处理的 glTF 文件"));
|
|
||||||
return { success: false, count: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
for (const file of gltfFiles) {
|
|
||||||
const fullPath = path.join(cwd, file);
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
console.log(color.yellow("跳过缺失的文件: " + file));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const baseName = file.replace(/\.gltf$/i, "");
|
|
||||||
const backupName = baseName + BACKUP_SUFFIX + ".gltf";
|
|
||||||
const backupPath = path.join(cwd, backupName);
|
|
||||||
|
|
||||||
fs.copyFileSync(fullPath, backupPath);
|
|
||||||
const modified = modifyGltfContent(fullPath, selectedExtensions);
|
|
||||||
fs.writeFileSync(fullPath, JSON.stringify(modified, null, 2), "utf-8");
|
|
||||||
console.log(color.green("✓ " + file + " (备份: " + backupName + ")"));
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: count > 0, count };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const result = await runInteractive();
|
|
||||||
if (!result) return "back";
|
if (!result) return "back";
|
||||||
|
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
|
|
||||||
const { results } = result;
|
|
||||||
const config = {
|
const config = {
|
||||||
files: results[0] || [],
|
files: result.results[0] || [],
|
||||||
extensions: results[1] || []
|
extensions: result.results[1] || []
|
||||||
};
|
};
|
||||||
|
|
||||||
showSummary([
|
ui.showSummary([
|
||||||
"处理文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
"处理文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||||
"扩展选项: " + (config.extensions.length ? config.extensions.join(", ") : "未选择")
|
"扩展选项: " + (config.extensions.length ? config.extensions.join(", ") : "未选择")
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { success, count } = runGltfExtension(config);
|
const { success, count, error } = runGltfExtension(config);
|
||||||
if (success) {
|
if (error) {
|
||||||
|
console.log(color.red("\n✖ " + error));
|
||||||
|
} else if (success) {
|
||||||
console.log(color.green("\n✓ 已修改 " + count + " 个 glTF 文件"));
|
console.log(color.green("\n✓ 已修改 " + count + " 个 glTF 文件"));
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitForKey();
|
await waitForKey();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "gltf",
|
id: "gltf",
|
||||||
|
|||||||
39
lib/plugins/gltf/service.js
Normal file
39
lib/plugins/gltf/service.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { checkRequiredFiles, modifyGltfContent, listGltfFiles, BACKUP_SUFFIX } from "../../utils/gltf.js";
|
||||||
|
|
||||||
|
export const runGltfExtension = (config = { files: [], extensions: ["textureBasisu"] }) => {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const { files = [], extensions = ["textureBasisu"] } = config;
|
||||||
|
const selectedExtensions = extensions.length ? extensions : ["textureBasisu"];
|
||||||
|
|
||||||
|
const { ok, missing } = checkRequiredFiles();
|
||||||
|
if (!ok) {
|
||||||
|
return { success: false, count: 0, error: "缺少必要文件: " + missing.join(", ") };
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackFiles = listGltfFiles();
|
||||||
|
const gltfFiles = (files.length ? files : fallbackFiles).filter(f => f.toLowerCase().endsWith(".gltf"));
|
||||||
|
if (!gltfFiles.length) {
|
||||||
|
return { success: false, count: 0, error: "未选择可处理的 glTF 文件" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const file of gltfFiles) {
|
||||||
|
const fullPath = path.join(cwd, file);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const baseName = file.replace(/\.gltf$/i, "");
|
||||||
|
const backupName = baseName + BACKUP_SUFFIX + ".gltf";
|
||||||
|
const backupPath = path.join(cwd, backupName);
|
||||||
|
|
||||||
|
fs.copyFileSync(fullPath, backupPath);
|
||||||
|
const modified = modifyGltfContent(fullPath, selectedExtensions);
|
||||||
|
fs.writeFileSync(fullPath, JSON.stringify(modified, null, 2), "utf-8");
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: count > 0, count };
|
||||||
|
};
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { createStepUI } from "../../utils/stepui.js";
|
|
||||||
import { getSteps } from "./config.js";
|
|
||||||
|
|
||||||
const ui = createStepUI({
|
|
||||||
title: "glTF 扩展工具",
|
|
||||||
getSteps
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { runInteractive, showSummary } = ui;
|
|
||||||
@ -1,5 +1,9 @@
|
|||||||
|
import { hasGltfFile } from "../../utils/gltf.js";
|
||||||
|
|
||||||
|
export const title = "KTX2 纹理压缩工具";
|
||||||
|
|
||||||
// 步骤配置
|
// 步骤配置
|
||||||
export const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
name: "文件格式",
|
name: "文件格式",
|
||||||
type: "multiselect",
|
type: "multiselect",
|
||||||
@ -61,3 +65,13 @@ export const steps = [
|
|||||||
default: ["overwrite", "keepOriginal"]
|
default: ["overwrite", "keepOriginal"]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function getSteps() {
|
||||||
|
const hasGltf = hasGltfFile();
|
||||||
|
return steps.map(step => {
|
||||||
|
if (step.name === "输出选项") {
|
||||||
|
return { ...step, options: step.options.filter(opt => !opt.dynamic || hasGltf) };
|
||||||
|
}
|
||||||
|
return step;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
// 从 utils 导出,保持兼容
|
|
||||||
export { hasGltfFile, checkRequiredFiles, modifyGltfContent } from "../../utils/gltf.js";
|
|
||||||
export { runGltfExtension } from "../gltf/index.js";
|
|
||||||
@ -1,20 +1,22 @@
|
|||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { checkToktx, scanImages, compressAll } from "./compressor.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { runInteractive, showSummary } from "./ui.js";
|
import { title, getSteps } from "./config.js";
|
||||||
import { runGltfExtension } from "./gltf.js";
|
|
||||||
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
|
import { checkToktx, scanImages, compressAll, runGltfExtension } from "./service.js";
|
||||||
|
|
||||||
async function run() {
|
const run = async () => {
|
||||||
checkToktx();
|
checkToktx();
|
||||||
const result = await runInteractive();
|
|
||||||
|
const ui = createStepUI({ title, getSteps });
|
||||||
|
const result = await ui.runInteractive();
|
||||||
if (!result) return "back";
|
if (!result) return "back";
|
||||||
|
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
|
|
||||||
const { results } = result;
|
const [exts, quality, encoding, mipmap, outputOpts] = result.results;
|
||||||
const [exts, quality, encoding, mipmap, outputOpts] = results;
|
|
||||||
const config = { exts, quality, encoding, mipmap, outputOpts };
|
const config = { exts, quality, encoding, mipmap, outputOpts };
|
||||||
showSummary([
|
|
||||||
|
ui.showSummary([
|
||||||
"文件格式: " + config.exts.join(", "),
|
"文件格式: " + config.exts.join(", "),
|
||||||
"压缩程度: " + config.quality,
|
"压缩程度: " + config.quality,
|
||||||
"编码格式: " + config.encoding,
|
"编码格式: " + config.encoding,
|
||||||
@ -46,7 +48,7 @@ async function run() {
|
|||||||
|
|
||||||
await waitForKey();
|
await waitForKey();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "ktx2",
|
id: "ktx2",
|
||||||
|
|||||||
@ -2,32 +2,29 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { fileURLToPath } from "url";
|
import { TEXTURE_TOOL } from "../../paths.js";
|
||||||
|
import { runGltfExtension } from "../gltf/service.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
// 检查工具是否存在
|
||||||
const __dirname = path.dirname(__filename);
|
export const checkToktx = () => {
|
||||||
const toktx = path.join(__dirname, "..", "..", "..", "bin", "texture_tool.exe");
|
if (!fs.existsSync(TEXTURE_TOOL)) {
|
||||||
|
|
||||||
// 检查 toktx 是否存在
|
|
||||||
export function checkToktx() {
|
|
||||||
if (!fs.existsSync(toktx)) {
|
|
||||||
console.error("❌ 找不到 texture_tool.exe");
|
console.error("❌ 找不到 texture_tool.exe");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 扫描图片文件
|
// 扫描图片文件
|
||||||
export function scanImages(exts) {
|
export const scanImages = (exts) => {
|
||||||
console.log("🔍 扫描目标文件中...");
|
console.log("🔍 扫描目标文件中...");
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const images = fs.readdirSync(cwd).filter(f =>
|
const images = fs.readdirSync(cwd).filter(f =>
|
||||||
exts.some(ext => f.toLowerCase().endsWith("." + ext))
|
exts.some(ext => f.toLowerCase().endsWith("." + ext))
|
||||||
);
|
);
|
||||||
return { images, cwd };
|
return { images, cwd };
|
||||||
}
|
};
|
||||||
|
|
||||||
// 构建压缩参数
|
// 构建压缩参数
|
||||||
export function buildArgs(input, output, config) {
|
const buildArgs = (input, output, config) => {
|
||||||
const args = ["--t2"];
|
const args = ["--t2"];
|
||||||
|
|
||||||
if (config.encoding === "uastc") {
|
if (config.encoding === "uastc") {
|
||||||
@ -48,14 +45,13 @@ export function buildArgs(input, output, config) {
|
|||||||
|
|
||||||
args.push(output, input);
|
args.push(output, input);
|
||||||
return args;
|
return args;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 压缩单个文件
|
// 压缩单个文件
|
||||||
export function compressFile(img, config, cwd, progress) {
|
const compressFile = (img, config, cwd, progress) => {
|
||||||
const baseName = img.replace(/\.[^.]+$/, "");
|
const baseName = img.replace(/\.[^.]+$/, "");
|
||||||
const out = baseName + ".ktx2";
|
const out = baseName + ".ktx2";
|
||||||
|
|
||||||
// 点动画
|
|
||||||
let dots = 0;
|
let dots = 0;
|
||||||
const dotAnim = setInterval(() => {
|
const dotAnim = setInterval(() => {
|
||||||
const dotStr = ".".repeat(dots);
|
const dotStr = ".".repeat(dots);
|
||||||
@ -68,7 +64,7 @@ export function compressFile(img, config, cwd, progress) {
|
|||||||
const args = buildArgs(img, out, config);
|
const args = buildArgs(img, out, config);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const proc = spawn(toktx, args, { cwd });
|
const proc = spawn(TEXTURE_TOOL, args, { cwd });
|
||||||
|
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
proc.stderr?.on("data", data => {
|
proc.stderr?.on("data", data => {
|
||||||
@ -96,10 +92,10 @@ export function compressFile(img, config, cwd, progress) {
|
|||||||
resolve({ success: false, error: err.message });
|
resolve({ success: false, error: err.message });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 批量压缩
|
// 批量压缩
|
||||||
export async function compressAll(images, config, cwd) {
|
export const compressAll = async (images, config, cwd) => {
|
||||||
const total = images.length;
|
const total = images.length;
|
||||||
let finished = 0;
|
let finished = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@ -112,4 +108,7 @@ export async function compressAll(images, config, cwd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { total, failed };
|
return { total, failed };
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// 导出 gltf 扩展功能
|
||||||
|
export { runGltfExtension };
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { createStepUI } from "../../utils/stepui.js";
|
|
||||||
import { steps } from "./config.js";
|
|
||||||
import { hasGltfFile } from "./gltf.js";
|
|
||||||
|
|
||||||
function getFilteredSteps() {
|
|
||||||
const hasGltf = hasGltfFile();
|
|
||||||
return steps.map(step => {
|
|
||||||
if (step.name === "输出选项") {
|
|
||||||
return { ...step, options: step.options.filter(opt => !opt.dynamic || hasGltf) };
|
|
||||||
}
|
|
||||||
return step;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ui = createStepUI({
|
|
||||||
title: "KTX2 纹理压缩工具",
|
|
||||||
getSteps: getFilteredSteps
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { runInteractive, showSummary } = ui;
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { listAllModelFiles } from "../../utils/gltf.js";
|
import { listAllModelFiles } from "../../utils/gltf.js";
|
||||||
|
|
||||||
|
export const title = "模型压缩工具";
|
||||||
|
|
||||||
const transformOptions = [
|
const transformOptions = [
|
||||||
{ value: "dedup", label: "dedup(去重)", hint: "删除重复的访问器、材质、网格" },
|
{ value: "dedup", label: "dedup(去重)", hint: "删除重复的访问器、材质、网格" },
|
||||||
{ value: "prune", label: "prune(清理无用节点)", hint: "移除未被引用的节点、材质、动画" },
|
{ value: "prune", label: "prune(清理无用节点)", hint: "移除未被引用的节点、材质、动画" },
|
||||||
@ -27,7 +29,7 @@ const outputOptions = [
|
|||||||
{ value: "copy", label: "输出副本 (_compressed)", hint: "保留原文件不动,结果写入新文件" }
|
{ value: "copy", label: "输出副本 (_compressed)", hint: "保留原文件不动,结果写入新文件" }
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getSteps() {
|
export const getSteps = () => {
|
||||||
const files = listAllModelFiles();
|
const files = listAllModelFiles();
|
||||||
const fileStep = {
|
const fileStep = {
|
||||||
name: "模型选择",
|
name: "模型选择",
|
||||||
@ -68,4 +70,4 @@ export function getSteps() {
|
|||||||
default: ["overwrite", "backup"]
|
default: ["overwrite", "backup"]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,140 +1,26 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { NodeIO } from "@gltf-transform/core";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
import { title, getSteps } from "./config.js";
|
||||||
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";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
|
import { buildTransforms, processFile } from "./service.js";
|
||||||
|
|
||||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
const run = async () => {
|
||||||
|
const ui = createStepUI({ title, getSteps });
|
||||||
const QUANTIZE_PRESETS = {
|
const result = await ui.runInteractive();
|
||||||
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";
|
if (!result) return "back";
|
||||||
|
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
|
|
||||||
const { results } = result;
|
|
||||||
const config = {
|
const config = {
|
||||||
files: results[0] || [],
|
files: result.results[0] || [],
|
||||||
commands: results[1] || [],
|
commands: result.results[1] || [],
|
||||||
quantizePreset: results[2] || "balanced",
|
quantizePreset: result.results[2] || "balanced",
|
||||||
outputFormat: results[3] || "auto",
|
outputFormat: result.results[3] || "auto",
|
||||||
outputOptions: results[4] || []
|
outputOptions: result.results[4] || []
|
||||||
};
|
};
|
||||||
|
|
||||||
showSummary([
|
ui.showSummary([
|
||||||
"模型文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
"模型文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||||
"gltf-transform 命令: " + (config.commands.length ? config.commands.join(", ") : "无"),
|
"gltf-transform 命令: " + (config.commands.length ? config.commands.join(", ") : "无"),
|
||||||
"量化级别: " + config.quantizePreset,
|
"量化级别: " + config.quantizePreset,
|
||||||
@ -183,16 +69,12 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("\n" + color.bgGreen(color.black(" 压缩完成 ")));
|
console.log("\n" + color.bgGreen(color.black(" 压缩完成 ")));
|
||||||
if (success) {
|
if (success) console.log(color.green(`成功: ${success} 个`));
|
||||||
console.log(color.green(`成功: ${success} 个`));
|
if (failed.length) console.log(color.yellow(`失败: ${failed.length} 个 (${failed.join(", ")})`));
|
||||||
}
|
|
||||||
if (failed.length) {
|
|
||||||
console.log(color.yellow(`失败: ${failed.length} 个 (${failed.join(", ")})`));
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitForKey();
|
await waitForKey();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "model",
|
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;
|
|
||||||
@ -155,7 +155,7 @@ const commonComponents = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 根据框架获取组件配置
|
// 根据框架获取组件配置
|
||||||
export function getComponentsByFramework(framework) {
|
export const getComponentsByFramework = (framework) => {
|
||||||
if (!framework || framework === "none") return {};
|
if (!framework || framework === "none") return {};
|
||||||
|
|
||||||
const isReact = ["react", "nextjs"].includes(framework);
|
const isReact = ["react", "nextjs"].includes(framework);
|
||||||
@ -174,16 +174,16 @@ export function getComponentsByFramework(framework) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取默认框架
|
// 获取默认框架
|
||||||
function getDefaultFramework(projectType) {
|
const getDefaultFramework = (projectType) => {
|
||||||
const opts = frameworkOptions[projectType];
|
const opts = frameworkOptions[projectType];
|
||||||
return opts?.[0]?.value || "none";
|
return opts?.[0]?.value || "none";
|
||||||
}
|
};
|
||||||
|
|
||||||
// 生成前端/全栈项目的步骤配置
|
// 生成前端/全栈项目的步骤配置
|
||||||
export function generateSteps(projectType, selectedFramework) {
|
export const generateSteps = (projectType, selectedFramework) => {
|
||||||
const defaultFramework = getDefaultFramework(projectType);
|
const defaultFramework = getDefaultFramework(projectType);
|
||||||
const framework = selectedFramework || defaultFramework;
|
const framework = selectedFramework || defaultFramework;
|
||||||
|
|
||||||
@ -213,4 +213,4 @@ export function generateSteps(projectType, selectedFramework) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return steps;
|
return steps;
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { projectTypes } from "./config.js";
|
import { projectTypes } from "./config.js";
|
||||||
import { gridSelect } from "../../grid.js";
|
import { gridSelect } from "../../grid.js";
|
||||||
import { createScaffoldUI, formatResults, waitKey } from "./ui.js";
|
import { createScaffoldUI, formatResults, waitKey } from "./service.js";
|
||||||
|
|
||||||
async function run() {
|
const run = async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
// 二级菜单 - 项目类型
|
// 二级菜单 - 项目类型
|
||||||
const typeResult = await gridSelect({
|
const typeResult = await gridSelect({
|
||||||
@ -32,7 +32,7 @@ async function run() {
|
|||||||
await waitKey();
|
await waitKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "scaffold",
|
id: "scaffold",
|
||||||
|
|||||||
@ -1,21 +1,18 @@
|
|||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { initKeypress, onKey, stopKeypress } from "../../keyboard.js";
|
|
||||||
import { createStepUI } from "../../utils/stepui.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { generateSteps } from "./config.js";
|
import { generateSteps } from "./config.js";
|
||||||
|
import { initKeypress, onKey, stopKeypress } from "../../keyboard.js";
|
||||||
|
|
||||||
// 框架 + 组件配置 UI
|
export const createScaffoldUI = (projectType) => {
|
||||||
export function createScaffoldUI(projectType) {
|
|
||||||
return createStepUI({
|
return createStepUI({
|
||||||
title: `${projectType} - 项目配置`,
|
title: `${projectType} - 项目配置`,
|
||||||
getSteps: () => generateSteps(projectType, null),
|
getSteps: () => generateSteps(projectType, null),
|
||||||
onStepChange: framework => generateSteps(projectType, framework),
|
onStepChange: framework => generateSteps(projectType, framework),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// 解析配置结果
|
export const formatResults = (steps, results) => {
|
||||||
export function formatResults(steps, results) {
|
|
||||||
const summary = [];
|
const summary = [];
|
||||||
|
|
||||||
results.forEach((val, i) => {
|
results.forEach((val, i) => {
|
||||||
if (Array.isArray(val) && val.length > 0) {
|
if (Array.isArray(val) && val.length > 0) {
|
||||||
summary.push(`${steps[i].name}: ${val.join(", ")}`);
|
summary.push(`${steps[i].name}: ${val.join(", ")}`);
|
||||||
@ -23,14 +20,11 @@ export function formatResults(steps, results) {
|
|||||||
summary.push(`${steps[i].name}: ${val}`);
|
summary.push(`${steps[i].name}: ${val}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return summary.length ? summary : ["未选择任何组件"];
|
return summary.length ? summary : ["未选择任何组件"];
|
||||||
}
|
};
|
||||||
|
|
||||||
// 等待按键
|
export const waitKey = async (message = "按任意键返回") => {
|
||||||
export async function waitKey(message = "按任意键返回") {
|
|
||||||
console.log(color.dim(`\n${message}`));
|
console.log(color.dim(`\n${message}`));
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
initKeypress();
|
initKeypress();
|
||||||
onKey(() => {
|
onKey(() => {
|
||||||
@ -38,4 +32,4 @@ export async function waitKey(message = "按任意键返回") {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
Reference in New Issue
Block a user