优化第一阶段
This commit is contained in:
@ -15,7 +15,12 @@
|
|||||||
"Bash(./bin/assimp.exe:*)",
|
"Bash(./bin/assimp.exe:*)",
|
||||||
"Bash(npm run build)",
|
"Bash(npm run build)",
|
||||||
"Bash(node:*)",
|
"Bash(node:*)",
|
||||||
"Bash(npm uninstall:*)"
|
"Bash(npm uninstall:*)",
|
||||||
|
"Bash(cp:*)",
|
||||||
|
"Bash(for dir in gltf ktx2 model scaffold)",
|
||||||
|
"Bash(do cp lib/$dir/*.js lib/plugins/$dir/)",
|
||||||
|
"Bash(done)",
|
||||||
|
"Bash(timeout 3 node:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
index.js
6
index.js
@ -33,7 +33,11 @@ while (true) {
|
|||||||
console.log(color.cyan("\n正在启动: " + selected.name + "...\n"));
|
console.log(color.cyan("\n正在启动: " + selected.name + "...\n"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await selected.tool.run();
|
const runner = selected.run || selected.tool?.run;
|
||||||
|
if (typeof runner !== "function") {
|
||||||
|
throw new Error("当前菜单项缺少 run 方法");
|
||||||
|
}
|
||||||
|
const result = await runner();
|
||||||
if (result === "back") continue;
|
if (result === "back") continue;
|
||||||
break;
|
break;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export async function run() {
|
|
||||||
throw new Error("尚未实现");
|
|
||||||
}
|
|
||||||
100
lib/grid.js
100
lib/grid.js
@ -1,21 +1,6 @@
|
|||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { initKeypress, onKey, stopKeypress } from "./keyboard.js";
|
import { initKeypress, onKey, stopKeypress } from "./keyboard.js";
|
||||||
|
import { clearScreen, padEnd, strWidth, centerPad } from "./utils/terminal.js";
|
||||||
function clearScreen() {
|
|
||||||
process.stdout.write('\x1Bc');
|
|
||||||
}
|
|
||||||
|
|
||||||
function strWidth(str) {
|
|
||||||
let width = 0;
|
|
||||||
for (const char of str) {
|
|
||||||
width += char.charCodeAt(0) > 127 ? 2 : 1;
|
|
||||||
}
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
function padEnd(str, width) {
|
|
||||||
return str + " ".repeat(Math.max(0, width - strWidth(str)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function gridSelect(options) {
|
export async function gridSelect(options) {
|
||||||
const {
|
const {
|
||||||
@ -25,15 +10,22 @@ export async function gridSelect(options) {
|
|||||||
title = "",
|
title = "",
|
||||||
renderHeader = null,
|
renderHeader = null,
|
||||||
updateInfo = null,
|
updateInfo = null,
|
||||||
headerGap = 2, // header后的空行数
|
headerGap = 2,
|
||||||
menuGap = 2, // 菜单和提示文字的间隔
|
menuGap = 2,
|
||||||
rowGap = 1 // 每行菜单后的空行数
|
rowGap = 1,
|
||||||
|
instructions = "←→ 选择 | Enter 确认 | Esc 退出",
|
||||||
|
onCancel = "exit", // "exit" | "back"
|
||||||
|
mapper,
|
||||||
|
showUpdateButton = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
const hasUpdate = showUpdateButton && !!updateInfo;
|
||||||
|
const mapResult = mapper || ((item, isUpdate) => (isUpdate ? { isUpdate: true } : item));
|
||||||
|
|
||||||
let current = 0;
|
let current = 0;
|
||||||
let onUpdate = updateInfo ? true : false; // 如果有更新,默认选中更新按钮
|
let onUpdate = hasUpdate;
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
const rows = Math.ceil(items.length / cols);
|
const rows = Math.ceil((items?.length || 0) / cols);
|
||||||
const termWidth = process.stdout.columns || 80;
|
const termWidth = process.stdout.columns || 80;
|
||||||
const totalWidth = cols * colWidth;
|
const totalWidth = cols * colWidth;
|
||||||
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
||||||
@ -48,13 +40,23 @@ export async function gridSelect(options) {
|
|||||||
for (let i = 0; i < headerGap; i++) console.log("");
|
for (let i = 0; i < headerGap; i++) console.log("");
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
const titlePad = " ".repeat(Math.max(0, Math.floor((termWidth - title.length - 4) / 2)));
|
const titlePad = centerPad(title, termWidth - 4);
|
||||||
console.log(titlePad + color.bgMagenta(color.white(` ${title} `)));
|
console.log(titlePad + color.bgMagenta(color.white(` ${title} `)));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 退出"));
|
if (instructions) {
|
||||||
|
console.log("\n" + pad + color.dim(instructions));
|
||||||
|
} else {
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < menuGap; i++) console.log("");
|
for (let i = 0; i < menuGap; i++) console.log("");
|
||||||
|
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
console.log(color.yellow("暂无可选项"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let row = 0; row < rows; row++) {
|
for (let row = 0; row < rows; row++) {
|
||||||
let line = "";
|
let line = "";
|
||||||
let descLine = "";
|
let descLine = "";
|
||||||
@ -68,7 +70,7 @@ export async function gridSelect(options) {
|
|||||||
} else {
|
} else {
|
||||||
line += " " + item.name + " ";
|
line += " " + item.name + " ";
|
||||||
}
|
}
|
||||||
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
line += " ".repeat(Math.max(0, colWidth - strWidth(item.name) - 2));
|
||||||
descLine += padEnd(item.desc || "", colWidth);
|
descLine += padEnd(item.desc || "", colWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,51 +93,67 @@ export async function gridSelect(options) {
|
|||||||
|
|
||||||
switch (key.name) {
|
switch (key.name) {
|
||||||
case "up":
|
case "up":
|
||||||
if (onUpdate) {
|
if (!onUpdate && row === 0 && hasUpdate) {
|
||||||
// 已在更新按钮,不能再上
|
onUpdate = true;
|
||||||
} else if (row === 0 && updateInfo) {
|
render();
|
||||||
onUpdate = true; render();
|
} else if (!onUpdate && row > 0) {
|
||||||
} else if (row > 0) {
|
current -= cols;
|
||||||
current -= cols; render();
|
render();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "down":
|
case "down":
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
onUpdate = false; render();
|
onUpdate = false;
|
||||||
|
render();
|
||||||
} else if (row < rows - 1 && current + cols < items.length) {
|
} else if (row < rows - 1 && current + cols < items.length) {
|
||||||
current += cols; render();
|
current += cols;
|
||||||
|
render();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "left":
|
case "left":
|
||||||
if (!onUpdate && col > 0) { current--; render(); }
|
if (!onUpdate && col > 0) {
|
||||||
|
current--;
|
||||||
|
render();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "right":
|
case "right":
|
||||||
if (!onUpdate && col < cols - 1 && current < items.length - 1) { current++; render(); }
|
if (!onUpdate && col < cols - 1 && current < items.length - 1) {
|
||||||
|
current++;
|
||||||
|
render();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "return":
|
case "return":
|
||||||
resolved = true;
|
resolved = true;
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
if (onUpdate) {
|
setImmediate(() => {
|
||||||
setImmediate(() => resolve({ isUpdate: true }));
|
const isUpdateSelection = onUpdate && hasUpdate;
|
||||||
} else {
|
const payload = isUpdateSelection ? null : items[current];
|
||||||
setImmediate(() => resolve(items[current]));
|
resolve(mapResult(payload, isUpdateSelection));
|
||||||
}
|
});
|
||||||
break;
|
break;
|
||||||
case "escape":
|
case "escape":
|
||||||
resolved = true;
|
resolved = true;
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
clearScreen();
|
clearScreen();
|
||||||
console.log(color.yellow("再见!"));
|
if (onCancel === "back") {
|
||||||
|
setImmediate(() => resolve("back"));
|
||||||
|
} else {
|
||||||
|
console.log(color.yellow("再见!"));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "c":
|
case "c":
|
||||||
if (key.ctrl) {
|
if (key.ctrl) {
|
||||||
resolved = true;
|
resolved = true;
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
clearScreen();
|
clearScreen();
|
||||||
console.log(color.yellow("再见!"));
|
if (onCancel === "back") {
|
||||||
|
setImmediate(() => resolve("back"));
|
||||||
|
} else {
|
||||||
|
console.log(color.yellow("再见!"));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export async function run() {
|
|
||||||
throw new Error("尚未实现");
|
|
||||||
}
|
|
||||||
@ -36,13 +36,15 @@ export function stopKeypress() {
|
|||||||
ensureRawMode(false);
|
ensureRawMode(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waitForKey(message = "按任意键返回...") {
|
export function waitForKey(message = "按任意键返回...", predicate = () => true) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
console.log("\n" + message);
|
console.log("\n" + message);
|
||||||
initKeypress();
|
initKeypress();
|
||||||
onKey(() => {
|
onKey((str, key) => {
|
||||||
|
const pressed = key || {};
|
||||||
|
if (!predicate(pressed)) return;
|
||||||
stopKeypress();
|
stopKeypress();
|
||||||
resolve();
|
resolve(pressed);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export async function run() {
|
|
||||||
throw new Error("尚未实现");
|
|
||||||
}
|
|
||||||
46
lib/menu.js
46
lib/menu.js
@ -1,18 +1,10 @@
|
|||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
import { gridSelect } from "./grid.js";
|
import { gridSelect } from "./grid.js";
|
||||||
import * as convertTool from "./convert/index.js";
|
import { getTools } from "./plugins/index.js";
|
||||||
import * as ktx2Tool from "./ktx2/index.js";
|
|
||||||
import * as gltfTool from "./gltf/index.js";
|
|
||||||
import * as modelTool from "./model/index.js";
|
|
||||||
import * as imageTool from "./image/index.js";
|
|
||||||
import * as spriteTool from "./sprite/index.js";
|
|
||||||
import * as lodTool from "./lod/index.js";
|
|
||||||
import * as audioTool from "./audio/index.js";
|
|
||||||
import * as scaffoldTool from "./scaffold/index.js";
|
|
||||||
|
|
||||||
let poemConfig = {
|
let poemConfig = {
|
||||||
lines: ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"],
|
lines: ["你我皆牛马", "生在人世间", "终日赶波涛", "一刻不得闲"],
|
||||||
perLine: 2,
|
perLine: 2,
|
||||||
padding: { top: 2, bottom: 2, left: 6, right: 6 },
|
padding: { top: 2, bottom: 2, left: 6, right: 6 },
|
||||||
borderStyle: "double",
|
borderStyle: "double",
|
||||||
@ -21,18 +13,6 @@ let poemConfig = {
|
|||||||
|
|
||||||
let updateInfo = null;
|
let updateInfo = null;
|
||||||
|
|
||||||
const tools = [
|
|
||||||
{ name: "格式转换", desc: "支持多种模型格式转换", tool: convertTool },
|
|
||||||
{ name: "KTX2 纹理压缩", desc: "图片转KTX2格式", tool: ktx2Tool },
|
|
||||||
{ name: "glTF扩展", desc: "添加KHR_texture_basisu", tool: gltfTool },
|
|
||||||
{ name: "模型压缩", desc: "压缩glTF/GLB模型", tool: modelTool },
|
|
||||||
{ name: "图片批量处理", desc: "裁剪/缩放/转换", tool: imageTool },
|
|
||||||
{ name: "Sprite图集", desc: "合并精灵图集", tool: spriteTool },
|
|
||||||
{ name: "LOD生成器", desc: "生成多级细节", tool: lodTool },
|
|
||||||
{ name: "音频压缩", desc: "压缩音频文件", tool: audioTool },
|
|
||||||
{ name: "项目脚手架", desc: "快速创建项目模板", tool: scaffoldTool },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function setPoem(lines, perLine = 2) {
|
export function setPoem(lines, perLine = 2) {
|
||||||
poemConfig.lines = lines;
|
poemConfig.lines = lines;
|
||||||
poemConfig.perLine = perLine;
|
poemConfig.perLine = perLine;
|
||||||
@ -49,7 +29,7 @@ export function setUpdateInfo(info) {
|
|||||||
function renderPoem() {
|
function renderPoem() {
|
||||||
const merged = [];
|
const merged = [];
|
||||||
for (let i = 0; i < poemConfig.lines.length; i += poemConfig.perLine) {
|
for (let i = 0; i < poemConfig.lines.length; i += poemConfig.perLine) {
|
||||||
merged.push(poemConfig.lines.slice(i, i + poemConfig.perLine).join(","));
|
merged.push(poemConfig.lines.slice(i, i + poemConfig.perLine).join(" | "));
|
||||||
}
|
}
|
||||||
return boxen(color.yellow(merged.join("\n")), {
|
return boxen(color.yellow(merged.join("\n")), {
|
||||||
padding: poemConfig.padding,
|
padding: poemConfig.padding,
|
||||||
@ -63,14 +43,17 @@ function renderPoem() {
|
|||||||
function renderUpdateInfo(selected) {
|
function renderUpdateInfo(selected) {
|
||||||
if (!updateInfo) return "";
|
if (!updateInfo) return "";
|
||||||
const btn = selected ? color.cyan("[ 立即更新 ]") : color.white(" 立即更新 ");
|
const btn = selected ? color.cyan("[ 立即更新 ]") : color.white(" 立即更新 ");
|
||||||
const msg = `📦 发现新版本: ${color.red(updateInfo.current)} → ${color.green(updateInfo.latest)} ${btn}`;
|
const msg = `📦 发现新版本 ${color.red(updateInfo.current)} → ${color.green(updateInfo.latest)} ${btn}`;
|
||||||
return "\n" + boxen(color.yellow(msg), {
|
return (
|
||||||
|
"\n" +
|
||||||
|
boxen(color.yellow(msg), {
|
||||||
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
||||||
borderStyle: "round",
|
borderStyle: "round",
|
||||||
borderColor: "green",
|
borderColor: "green",
|
||||||
textAlignment: "center",
|
textAlignment: "center",
|
||||||
float: "center",
|
float: "center",
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHeader(onUpdate) {
|
function renderHeader(onUpdate) {
|
||||||
@ -78,13 +61,18 @@ function renderHeader(onUpdate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showMainMenu() {
|
export async function showMainMenu() {
|
||||||
|
const tools = getTools();
|
||||||
|
|
||||||
return gridSelect({
|
return gridSelect({
|
||||||
items: tools,
|
items: tools,
|
||||||
cols: 3,
|
cols: 3,
|
||||||
colWidth: 24,
|
colWidth: 24,
|
||||||
renderHeader: renderHeader,
|
renderHeader,
|
||||||
updateInfo: updateInfo,
|
updateInfo,
|
||||||
|
instructions: "←→ 选择 | Enter 确认 | Esc 退出",
|
||||||
|
mapper: (item, isUpdate) => (isUpdate ? { isUpdate: true } : item),
|
||||||
|
showUpdateButton: !!updateInfo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { tools, poemConfig };
|
export { poemConfig };
|
||||||
|
|||||||
10
lib/plugins/audio/index.js
Normal file
10
lib/plugins/audio/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
async function run() {
|
||||||
|
throw new Error("尚未实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "audio",
|
||||||
|
name: "音频压缩",
|
||||||
|
desc: "压缩音频文件",
|
||||||
|
run,
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { getImportExtensions, getExportFormats } from "./converters.js";
|
import { getImportExtensions, getExportFormats } from "./converters.js";
|
||||||
import { sortByUsage } from "../stats.js";
|
import { sortByUsage } from "../../stats.js";
|
||||||
|
|
||||||
const EXCLUDED_FORMATS = ["x", "glb", "gltf"];
|
const EXCLUDED_FORMATS = ["x", "glb", "gltf"];
|
||||||
|
|
||||||
@ -3,7 +3,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const ASSIMP_PATH = path.join(__dirname, "../../bin/model_converter.exe");
|
const ASSIMP_PATH = path.join(__dirname, "../../../bin/model_converter.exe");
|
||||||
|
|
||||||
let importExts = null;
|
let importExts = null;
|
||||||
let exportFormats = null;
|
let exportFormats = null;
|
||||||
@ -11,6 +11,7 @@ let exportFormats = null;
|
|||||||
export function getImportExtensions() {
|
export function getImportExtensions() {
|
||||||
if (!importExts) {
|
if (!importExts) {
|
||||||
const r = spawnSync(ASSIMP_PATH, ["listext"], { encoding: "utf8" });
|
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());
|
importExts = r.stdout.trim().split(";").map(e => e.replace("*", "").toLowerCase());
|
||||||
}
|
}
|
||||||
return importExts;
|
return importExts;
|
||||||
@ -19,6 +20,7 @@ export function getImportExtensions() {
|
|||||||
export function getExportFormats() {
|
export function getExportFormats() {
|
||||||
if (!exportFormats) {
|
if (!exportFormats) {
|
||||||
const r = spawnSync(ASSIMP_PATH, ["listexport"], { encoding: "utf8" });
|
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);
|
exportFormats = r.stdout.trim().split(/\r?\n/).filter(Boolean);
|
||||||
}
|
}
|
||||||
return exportFormats;
|
return exportFormats;
|
||||||
@ -3,8 +3,8 @@ import path from "path";
|
|||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { convert } from "./converters.js";
|
import { convert } from "./converters.js";
|
||||||
import { runInteractive, showSummary } from "./ui.js";
|
import { runInteractive, showSummary } from "./ui.js";
|
||||||
import { stopKeypress, initKeypress, onKey } from "../keyboard.js";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
import { record } from "../stats.js";
|
import { record } from "../../stats.js";
|
||||||
|
|
||||||
const FORMAT_EXT = {
|
const FORMAT_EXT = {
|
||||||
collada: ".dae",stp: ".stp", obj: ".obj", objnomtl: ".obj",
|
collada: ".dae",stp: ".stp", obj: ".obj", objnomtl: ".obj",
|
||||||
@ -20,11 +20,12 @@ function getOutputExt(format) {
|
|||||||
|
|
||||||
async function processFile(file, config) {
|
async function processFile(file, config) {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const baseName = file.slice(0, file.lastIndexOf("."));
|
const { dir, name } = path.parse(file);
|
||||||
const outputExt = getOutputExt(config.outputFormat);
|
const outputExt = getOutputExt(config.outputFormat);
|
||||||
const outputFile = baseName + outputExt;
|
const outputFile = path.join(dir || "", `${name}${outputExt}`);
|
||||||
|
const absolutePath = path.join(cwd, file);
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(cwd, file))) {
|
if (!fs.existsSync(absolutePath)) {
|
||||||
return { ok: false, file, reason: "文件不存在" };
|
return { ok: false, file, reason: "文件不存在" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,21 +34,14 @@ async function processFile(file, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function waitForEsc() {
|
async function waitForEsc() {
|
||||||
initKeypress();
|
return waitForKey(color.dim("按 Esc 返回"), key => key?.name === "escape" || (key?.ctrl && key?.name === "c"));
|
||||||
return new Promise(resolve => {
|
|
||||||
onKey((str, key) => {
|
|
||||||
if (key?.name === "escape" || (key?.ctrl && key?.name === "c")) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run() {
|
async function run() {
|
||||||
const result = await runInteractive();
|
const result = await runInteractive();
|
||||||
if (!result) return "back";
|
if (!result) return "back";
|
||||||
|
|
||||||
const { steps, results } = result;
|
const { results } = result;
|
||||||
const config = {
|
const config = {
|
||||||
files: results[0] || [],
|
files: results[0] || [],
|
||||||
outputFormat: results[1] || "glb2"
|
outputFormat: results[1] || "glb2"
|
||||||
@ -61,7 +55,6 @@ export async function run() {
|
|||||||
|
|
||||||
if (!config.files.length) {
|
if (!config.files.length) {
|
||||||
console.log(color.yellow("未选择任何模型文件"));
|
console.log(color.yellow("未选择任何模型文件"));
|
||||||
console.log(color.dim("\n按 Esc 返回"));
|
|
||||||
await waitForEsc();
|
await waitForEsc();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
}
|
||||||
@ -102,7 +95,13 @@ export async function run() {
|
|||||||
console.log(color.yellow(`失败: ${failed.length} 个`));
|
console.log(color.yellow(`失败: ${failed.length} 个`));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(color.dim("\n按 Esc 返回"));
|
|
||||||
await waitForEsc();
|
await waitForEsc();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "convert",
|
||||||
|
name: "格式转换",
|
||||||
|
desc: "支持多种模型格式转换",
|
||||||
|
run,
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { createStepUI } from "../utils/stepui.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { getSteps } from "./config.js";
|
import { getSteps } from "./config.js";
|
||||||
|
|
||||||
const ui = createStepUI({
|
const ui = createStepUI({
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { listGltfFiles } from "../utils/gltf.js";
|
import { listGltfFiles } from "../../utils/gltf.js";
|
||||||
|
|
||||||
const extensionStep = {
|
const extensionStep = {
|
||||||
name: "扩展选项",
|
name: "扩展选项",
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { checkRequiredFiles, modifyGltfContent, listGltfFiles, BACKUP_SUFFIX } from "../utils/gltf.js";
|
import { checkRequiredFiles, modifyGltfContent, listGltfFiles, BACKUP_SUFFIX } from "../../utils/gltf.js";
|
||||||
import { runInteractive, showSummary } from "./ui.js";
|
import { runInteractive, showSummary } from "./ui.js";
|
||||||
import { stopKeypress, waitForKey } from "../keyboard.js";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
|
|
||||||
export function runGltfExtension(config = { files: [], extensions: ["textureBasisu"] }) {
|
export function runGltfExtension(config = { files: [], extensions: ["textureBasisu"] }) {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
@ -46,7 +46,7 @@ export function runGltfExtension(config = { files: [], extensions: ["textureBasi
|
|||||||
return { success: count > 0, count };
|
return { success: count > 0, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run() {
|
async function run() {
|
||||||
const result = await runInteractive();
|
const result = await runInteractive();
|
||||||
if (!result) return "back";
|
if (!result) return "back";
|
||||||
|
|
||||||
@ -71,3 +71,10 @@ export async function run() {
|
|||||||
await waitForKey();
|
await waitForKey();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "gltf",
|
||||||
|
name: "glTF扩展",
|
||||||
|
desc: "添加KTX2纹理扩展",
|
||||||
|
run,
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { createStepUI } from "../utils/stepui.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { getSteps } from "./config.js";
|
import { getSteps } from "./config.js";
|
||||||
|
|
||||||
const ui = createStepUI({
|
const ui = createStepUI({
|
||||||
10
lib/plugins/image/index.js
Normal file
10
lib/plugins/image/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
async function run() {
|
||||||
|
throw new Error("尚未实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "image",
|
||||||
|
name: "图片批量处理",
|
||||||
|
desc: "裁剪/缩放/转换",
|
||||||
|
run,
|
||||||
|
};
|
||||||
14
lib/plugins/index.js
Normal file
14
lib/plugins/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { registerTool, getTools } from "./registry.js";
|
||||||
|
import convert from "./convert/index.js";
|
||||||
|
import ktx2 from "./ktx2/index.js";
|
||||||
|
import gltf from "./gltf/index.js";
|
||||||
|
import model from "./model/index.js";
|
||||||
|
import image from "./image/index.js";
|
||||||
|
import sprite from "./sprite/index.js";
|
||||||
|
import lod from "./lod/index.js";
|
||||||
|
import audio from "./audio/index.js";
|
||||||
|
import scaffold from "./scaffold/index.js";
|
||||||
|
|
||||||
|
[convert, ktx2, gltf, model, image, sprite, lod, audio, scaffold].forEach(registerTool);
|
||||||
|
|
||||||
|
export { getTools, registerTool };
|
||||||
@ -6,7 +6,7 @@ import { fileURLToPath } from "url";
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const toktx = path.join(__dirname, "..", "..", "bin", "texture_tool.exe");
|
const toktx = path.join(__dirname, "..", "..", "..", "bin", "texture_tool.exe");
|
||||||
|
|
||||||
// 检查 toktx 是否存在
|
// 检查 toktx 是否存在
|
||||||
export function checkToktx() {
|
export function checkToktx() {
|
||||||
@ -1,3 +1,3 @@
|
|||||||
// 从 utils 导出,保持兼容
|
// 从 utils 导出,保持兼容
|
||||||
export { hasGltfFile, checkRequiredFiles, modifyGltfContent } from "../utils/gltf.js";
|
export { hasGltfFile, checkRequiredFiles, modifyGltfContent } from "../../utils/gltf.js";
|
||||||
export { runGltfExtension } from "../gltf/index.js";
|
export { runGltfExtension } from "../gltf/index.js";
|
||||||
@ -2,9 +2,9 @@ import color from "picocolors";
|
|||||||
import { checkToktx, scanImages, compressAll } from "./compressor.js";
|
import { checkToktx, scanImages, compressAll } from "./compressor.js";
|
||||||
import { runInteractive, showSummary } from "./ui.js";
|
import { runInteractive, showSummary } from "./ui.js";
|
||||||
import { runGltfExtension } from "./gltf.js";
|
import { runGltfExtension } from "./gltf.js";
|
||||||
import { stopKeypress, waitForKey } from "../keyboard.js";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
|
|
||||||
export async function run() {
|
async function run() {
|
||||||
checkToktx();
|
checkToktx();
|
||||||
const result = await runInteractive();
|
const result = await runInteractive();
|
||||||
if (!result) return "back";
|
if (!result) return "back";
|
||||||
@ -47,3 +47,10 @@ export async function run() {
|
|||||||
await waitForKey();
|
await waitForKey();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "ktx2",
|
||||||
|
name: "KTX2压缩",
|
||||||
|
desc: "纹理压缩为KTX2",
|
||||||
|
run,
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { createStepUI } from "../utils/stepui.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { steps } from "./config.js";
|
import { steps } from "./config.js";
|
||||||
import { hasGltfFile } from "./gltf.js";
|
import { hasGltfFile } from "./gltf.js";
|
||||||
|
|
||||||
10
lib/plugins/lod/index.js
Normal file
10
lib/plugins/lod/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
async function run() {
|
||||||
|
throw new Error("尚未实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "lod",
|
||||||
|
name: "LOD生成器",
|
||||||
|
desc: "生成多级细节",
|
||||||
|
run,
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { listAllModelFiles } from "../utils/gltf.js";
|
import { listAllModelFiles } from "../../utils/gltf.js";
|
||||||
|
|
||||||
const transformOptions = [
|
const transformOptions = [
|
||||||
{ value: "dedup", label: "dedup(去重)", hint: "删除重复的访问器、材质、网格" },
|
{ value: "dedup", label: "dedup(去重)", hint: "删除重复的访问器、材质、网格" },
|
||||||
@ -4,10 +4,10 @@ import color from "picocolors";
|
|||||||
import { NodeIO } from "@gltf-transform/core";
|
import { NodeIO } from "@gltf-transform/core";
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
import { dedup, prune, resample, weld, quantize } from "@gltf-transform/functions";
|
import { dedup, prune, resample, weld, quantize } from "@gltf-transform/functions";
|
||||||
import { BACKUP_SUFFIX, needsConversion } from "../utils/gltf.js";
|
import { BACKUP_SUFFIX, needsConversion } from "../../utils/gltf.js";
|
||||||
import { convert } from "../convert/converters.js";
|
import { convert } from "../convert/converters.js";
|
||||||
import { runInteractive, showSummary } from "./ui.js";
|
import { runInteractive, showSummary } from "./ui.js";
|
||||||
import { stopKeypress, waitForKey } from "../keyboard.js";
|
import { stopKeypress, waitForKey } from "../../keyboard.js";
|
||||||
|
|
||||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ async function processFile(file, config, transforms) {
|
|||||||
return { ok: true, file, output: output.targetPath };
|
return { ok: true, file, output: output.targetPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run() {
|
async function run() {
|
||||||
const result = await runInteractive();
|
const result = await runInteractive();
|
||||||
if (!result) return "back";
|
if (!result) return "back";
|
||||||
|
|
||||||
@ -193,3 +193,10 @@ export async function run() {
|
|||||||
await waitForKey();
|
await waitForKey();
|
||||||
return "back";
|
return "back";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "model",
|
||||||
|
name: "模型压缩",
|
||||||
|
desc: "glTF/GLB/OBJ/FBX压缩",
|
||||||
|
run,
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { createStepUI } from "../utils/stepui.js";
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
import { getSteps } from "./config.js";
|
import { getSteps } from "./config.js";
|
||||||
|
|
||||||
const ui = createStepUI({
|
const ui = createStepUI({
|
||||||
23
lib/plugins/registry.js
Normal file
23
lib/plugins/registry.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const tools = [];
|
||||||
|
|
||||||
|
export function registerTool(definition) {
|
||||||
|
const { id, name, desc, run } = definition || {};
|
||||||
|
if (!id || !name || !run) {
|
||||||
|
throw new Error("Tool definition must include id, name and run");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = tools.findIndex(t => t.id === id);
|
||||||
|
const tool = { ...definition };
|
||||||
|
|
||||||
|
if (existing >= 0) {
|
||||||
|
tools[existing] = tool;
|
||||||
|
} else {
|
||||||
|
tools.push(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTools() {
|
||||||
|
return [...tools];
|
||||||
|
}
|
||||||
42
lib/plugins/scaffold/index.js
Normal file
42
lib/plugins/scaffold/index.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import color from "picocolors";
|
||||||
|
import { projectTypes } from "./config.js";
|
||||||
|
import { gridSelect } from "../../grid.js";
|
||||||
|
import { createScaffoldUI, formatResults, waitKey } from "./ui.js";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
while (true) {
|
||||||
|
// 二级菜单 - 项目类型
|
||||||
|
const typeResult = await gridSelect({
|
||||||
|
items: projectTypes,
|
||||||
|
title: "项目搭建器 - 选择类型",
|
||||||
|
cols: Math.min(3, projectTypes.length),
|
||||||
|
colWidth: 20,
|
||||||
|
instructions: "←→ 选择 | Enter 确认 | Esc 返回",
|
||||||
|
onCancel: "back",
|
||||||
|
mapper: item => ({ action: "select", item }),
|
||||||
|
showUpdateButton: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeResult === "back") return "back";
|
||||||
|
|
||||||
|
const projectType = typeResult.item.name;
|
||||||
|
|
||||||
|
// 三级页面 - 框架+组件配置(合并)
|
||||||
|
const ui = createScaffoldUI(projectType);
|
||||||
|
const result = await ui.runInteractive();
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const summary = formatResults(result.steps, result.results);
|
||||||
|
ui.showSummary(summary);
|
||||||
|
console.log(color.yellow("功能开发中,敬请期待..."));
|
||||||
|
await waitKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "scaffold",
|
||||||
|
name: "项目搭建",
|
||||||
|
desc: "快速创建项目模板",
|
||||||
|
run,
|
||||||
|
};
|
||||||
41
lib/plugins/scaffold/ui.js
Normal file
41
lib/plugins/scaffold/ui.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import color from "picocolors";
|
||||||
|
import { initKeypress, onKey, stopKeypress } from "../../keyboard.js";
|
||||||
|
import { createStepUI } from "../../utils/stepui.js";
|
||||||
|
import { generateSteps } from "./config.js";
|
||||||
|
|
||||||
|
// 框架 + 组件配置 UI
|
||||||
|
export function createScaffoldUI(projectType) {
|
||||||
|
return createStepUI({
|
||||||
|
title: `${projectType} - 项目配置`,
|
||||||
|
getSteps: () => generateSteps(projectType, null),
|
||||||
|
onStepChange: framework => generateSteps(projectType, framework),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析配置结果
|
||||||
|
export function formatResults(steps, results) {
|
||||||
|
const summary = [];
|
||||||
|
|
||||||
|
results.forEach((val, i) => {
|
||||||
|
if (Array.isArray(val) && val.length > 0) {
|
||||||
|
summary.push(`${steps[i].name}: ${val.join(", ")}`);
|
||||||
|
} else if (val && val !== "none") {
|
||||||
|
summary.push(`${steps[i].name}: ${val}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return summary.length ? summary : ["未选择任何组件"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待按键
|
||||||
|
export async function waitKey(message = "按任意键返回") {
|
||||||
|
console.log(color.dim(`\n${message}`));
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
initKeypress();
|
||||||
|
onKey(() => {
|
||||||
|
stopKeypress();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
10
lib/plugins/sprite/index.js
Normal file
10
lib/plugins/sprite/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
async function run() {
|
||||||
|
throw new Error("尚未实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "sprite",
|
||||||
|
name: "Sprite图集",
|
||||||
|
desc: "合并精灵图集",
|
||||||
|
run,
|
||||||
|
};
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import color from "picocolors";
|
|
||||||
import { projectTypes } from "./config.js";
|
|
||||||
import { gridSelect, createScaffoldUI, formatResults, waitKey } from "./ui.js";
|
|
||||||
|
|
||||||
export async function run() {
|
|
||||||
while (true) {
|
|
||||||
// 二级菜单 - 项目类型
|
|
||||||
const typeResult = await gridSelect(projectTypes, "项目脚手架 - 选择类型");
|
|
||||||
if (typeResult.action === "back") return "back";
|
|
||||||
|
|
||||||
const projectType = typeResult.item.name;
|
|
||||||
|
|
||||||
// 三级页面 - 框架+组件配置(合并)
|
|
||||||
const ui = createScaffoldUI(projectType);
|
|
||||||
const result = await ui.runInteractive();
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
const summary = formatResults(result.steps, result.results);
|
|
||||||
ui.showSummary(summary);
|
|
||||||
console.log(color.yellow("功能开发中,敬请期待..."));
|
|
||||||
await waitKey();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
import color from "picocolors";
|
|
||||||
import { initKeypress, onKey, stopKeypress } from "../keyboard.js";
|
|
||||||
import { createStepUI } from "../utils/stepui.js";
|
|
||||||
import { frameworkOptions, generateSteps } from "./config.js";
|
|
||||||
|
|
||||||
function clearScreen() {
|
|
||||||
process.stdout.write('\x1Bc');
|
|
||||||
}
|
|
||||||
|
|
||||||
function strWidth(str) {
|
|
||||||
let width = 0;
|
|
||||||
for (const char of str) {
|
|
||||||
width += char.charCodeAt(0) > 127 ? 2 : 1;
|
|
||||||
}
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
function padEnd(str, width) {
|
|
||||||
return str + " ".repeat(Math.max(0, width - strWidth(str)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 网格选择器
|
|
||||||
export async function gridSelect(items, title) {
|
|
||||||
let current = 0;
|
|
||||||
let resolved = false;
|
|
||||||
const termWidth = process.stdout.columns || 80;
|
|
||||||
const cols = Math.min(3, items.length);
|
|
||||||
const colWidth = 20;
|
|
||||||
const rows = Math.ceil(items.length / cols);
|
|
||||||
const totalWidth = cols * colWidth;
|
|
||||||
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
clearScreen();
|
|
||||||
console.log("");
|
|
||||||
const titlePad = " ".repeat(Math.max(0, Math.floor((termWidth - strWidth(title) - 4) / 2)));
|
|
||||||
console.log(titlePad + color.bgMagenta(color.white(` ${title} `)));
|
|
||||||
console.log("");
|
|
||||||
console.log(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 返回"));
|
|
||||||
console.log("\n");
|
|
||||||
|
|
||||||
for (let row = 0; row < rows; row++) {
|
|
||||||
let line = "";
|
|
||||||
let descLine = "";
|
|
||||||
|
|
||||||
for (let col = 0; col < cols; col++) {
|
|
||||||
const idx = row * cols + col;
|
|
||||||
if (idx < items.length) {
|
|
||||||
const item = items[idx];
|
|
||||||
const isSelected = idx === current;
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
line += color.cyan("[" + item.name + "]");
|
|
||||||
} else {
|
|
||||||
line += " " + item.name + " ";
|
|
||||||
}
|
|
||||||
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
|
||||||
descLine += padEnd(item.desc || "", colWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(pad + line);
|
|
||||||
console.log(pad + color.dim(descLine));
|
|
||||||
console.log("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
initKeypress();
|
|
||||||
render();
|
|
||||||
|
|
||||||
onKey((str, key) => {
|
|
||||||
if (!key || resolved) return;
|
|
||||||
|
|
||||||
const row = Math.floor(current / cols);
|
|
||||||
const col = current % cols;
|
|
||||||
|
|
||||||
switch (key.name) {
|
|
||||||
case "up":
|
|
||||||
if (row > 0) { current -= cols; render(); }
|
|
||||||
break;
|
|
||||||
case "down":
|
|
||||||
if (row < rows - 1 && current + cols < items.length) { current += cols; render(); }
|
|
||||||
break;
|
|
||||||
case "left":
|
|
||||||
if (col > 0) { current--; render(); }
|
|
||||||
break;
|
|
||||||
case "right":
|
|
||||||
if (col < cols - 1 && current < items.length - 1) { current++; render(); }
|
|
||||||
break;
|
|
||||||
case "return":
|
|
||||||
resolved = true;
|
|
||||||
stopKeypress();
|
|
||||||
setImmediate(() => resolve({ action: "select", item: items[current] }));
|
|
||||||
break;
|
|
||||||
case "escape":
|
|
||||||
resolved = true;
|
|
||||||
stopKeypress();
|
|
||||||
setImmediate(() => resolve({ action: "back" }));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建框架+组件配置UI
|
|
||||||
export function createScaffoldUI(projectType) {
|
|
||||||
return createStepUI({
|
|
||||||
title: `${projectType} - 项目配置`,
|
|
||||||
getSteps: () => generateSteps(projectType, null),
|
|
||||||
onStepChange: (framework) => generateSteps(projectType, framework)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析配置结果
|
|
||||||
export function formatResults(steps, results) {
|
|
||||||
const summary = [];
|
|
||||||
|
|
||||||
results.forEach((val, i) => {
|
|
||||||
if (Array.isArray(val) && val.length > 0) {
|
|
||||||
summary.push(`${steps[i].name}: ${val.join(", ")}`);
|
|
||||||
} else if (val && val !== "none") {
|
|
||||||
summary.push(`${steps[i].name}: ${val}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return summary.length ? summary : ["未选择任何组件"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待按键
|
|
||||||
export async function waitKey(message = "按任意键返回") {
|
|
||||||
console.log(color.dim(`\n${message}`));
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
initKeypress();
|
|
||||||
onKey(() => {
|
|
||||||
stopKeypress();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export async function run() {
|
|
||||||
throw new Error("尚未实现");
|
|
||||||
}
|
|
||||||
@ -1,9 +1,6 @@
|
|||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { initKeypress, onKey, stopKeypress } from "../keyboard.js";
|
import { initKeypress, onKey, stopKeypress } from "../keyboard.js";
|
||||||
|
import { clearScreen } from "./terminal.js";
|
||||||
function clearScreen() {
|
|
||||||
process.stdout.write('\x1Bc');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createStepUI(options) {
|
export function createStepUI(options) {
|
||||||
const { title, getSteps, onStepChange } = options;
|
const { title, getSteps, onStepChange } = options;
|
||||||
|
|||||||
21
lib/utils/terminal.js
Normal file
21
lib/utils/terminal.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export function clearScreen() {
|
||||||
|
process.stdout.write("\x1Bc");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function strWidth(str = "") {
|
||||||
|
let width = 0;
|
||||||
|
for (const char of String(str)) {
|
||||||
|
width += char.charCodeAt(0) > 127 ? 2 : 1;
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function padEnd(str, width) {
|
||||||
|
const value = String(str);
|
||||||
|
return value + " ".repeat(Math.max(0, width - strWidth(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function centerPad(text, totalWidth) {
|
||||||
|
const pad = Math.max(0, Math.floor((totalWidth - strWidth(text)) / 2));
|
||||||
|
return " ".repeat(pad);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user