增加转格式
This commit is contained in:
@ -8,7 +8,11 @@
|
||||
"Bash(timeout 2 node:*)",
|
||||
"Bash(npm ls:*)",
|
||||
"Bash(timeout 1 node:*)",
|
||||
"Bash(node -e:*)"
|
||||
"Bash(node -e:*)",
|
||||
"Bash(node --check:*)",
|
||||
"Bash(npm search:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(./bin/assimp.exe:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
6
.stats.json
Normal file
6
.stats.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"convert_format": {
|
||||
"stp": 1,
|
||||
"glb2": 2
|
||||
}
|
||||
}
|
||||
BIN
bin/assimp-vc143-mt.dll
Normal file
BIN
bin/assimp-vc143-mt.dll
Normal file
Binary file not shown.
BIN
bin/assimp.exe
Normal file
BIN
bin/assimp.exe
Normal file
Binary file not shown.
296
cuba.stp
Normal file
296
cuba.stp
Normal file
@ -0,0 +1,296 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('STEP AP214'),'1');
|
||||
FILE_NAME('cuba.stp','2025-12-20T11:13:44',(' '),(' '),'Spatial InterOp 3D',' ',' ');
|
||||
FILE_SCHEMA(('automotive_design'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION(' ',(#148,#183,#218,#253,#288,#323),#6);
|
||||
#2=PRODUCT_DEFINITION_CONTEXT('',#7,'design');
|
||||
#3=APPLICATION_PROTOCOL_DEFINITION('INTERNATIONAL STANDARD','automotive_design',1994,#7);
|
||||
#4=PRODUCT_CATEGORY_RELATIONSHIP('NONE','NONE',#8,#9);
|
||||
#5=SHAPE_DEFINITION_REPRESENTATION(#10,#11);
|
||||
#6= (GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#12))GLOBAL_UNIT_ASSIGNED_CONTEXT((#13,#14,#15))REPRESENTATION_CONTEXT('NONE','WORKSPACE'));
|
||||
#7=APPLICATION_CONTEXT(' ');
|
||||
#8=PRODUCT_CATEGORY('part','NONE');
|
||||
#9=PRODUCT_RELATED_PRODUCT_CATEGORY('detail',' ',(#17));
|
||||
#10=PRODUCT_DEFINITION_SHAPE('NONE','NONE',#18);
|
||||
#11=MANIFOLD_SURFACE_SHAPE_REPRESENTATION('Root',(#16,#19),#6);
|
||||
#12=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.0E-006),#13,'','');
|
||||
#13=(CONVERSION_BASED_UNIT('METRE',#20)LENGTH_UNIT()NAMED_UNIT(#21));
|
||||
#14=(NAMED_UNIT(#22)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));
|
||||
#15=(NAMED_UNIT(#22)SOLID_ANGLE_UNIT()SI_UNIT($,.STERADIAN.));
|
||||
#16=SHELL_BASED_SURFACE_MODEL('Root',(#29));
|
||||
#17=PRODUCT('Root','Root','Root',(#23));
|
||||
#18=PRODUCT_DEFINITION('NONE','NONE',#24,#2);
|
||||
#19=AXIS2_PLACEMENT_3D('',#25,#26,#27);
|
||||
#20=LENGTH_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.0),#28);
|
||||
#21=DIMENSIONAL_EXPONENTS(1.0,0.0,0.0,0.0,0.0,0.0,0.0);
|
||||
#22=DIMENSIONAL_EXPONENTS(0.0,0.0,0.0,0.0,0.0,0.0,0.0);
|
||||
#23=PRODUCT_CONTEXT('',#7,'mechanical');
|
||||
#24=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE(' ','NONE',#17,.NOT_KNOWN.);
|
||||
#25=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#26=DIRECTION('',(0.0,0.0,1.0));
|
||||
#27=DIRECTION('',(1.0,0.0,0.0));
|
||||
#28= (NAMED_UNIT(#21)LENGTH_UNIT()SI_UNIT(.MILLI.,.METRE.));
|
||||
#29=CLOSED_SHELL('',(#156,#191,#226,#261,#296,#331));
|
||||
#100=CARTESIAN_POINT('',(-100.000000000,-99.999984741,100.000015259));
|
||||
#101=VERTEX_POINT('',#100);
|
||||
#102=CARTESIAN_POINT('',(-100.000000000,100.000015259,99.999984741));
|
||||
#103=VERTEX_POINT('',#102);
|
||||
#104=CARTESIAN_POINT('',(-100.000000000,99.999984741,-100.000015259));
|
||||
#105=VERTEX_POINT('',#104);
|
||||
#106=CARTESIAN_POINT('',(-100.000000000,-100.000015259,-99.999984741));
|
||||
#107=VERTEX_POINT('',#106);
|
||||
#108=CARTESIAN_POINT('',(-100.000000000,-100.000015259,-99.999984741));
|
||||
#109=VERTEX_POINT('',#108);
|
||||
#110=CARTESIAN_POINT('',(-100.000000000,99.999984741,-100.000015259));
|
||||
#111=VERTEX_POINT('',#110);
|
||||
#112=CARTESIAN_POINT('',(100.000000000,99.999984741,-100.000015259));
|
||||
#113=VERTEX_POINT('',#112);
|
||||
#114=CARTESIAN_POINT('',(100.000000000,-100.000015259,-99.999984741));
|
||||
#115=VERTEX_POINT('',#114);
|
||||
#116=CARTESIAN_POINT('',(100.000000000,-100.000015259,-99.999984741));
|
||||
#117=VERTEX_POINT('',#116);
|
||||
#118=CARTESIAN_POINT('',(100.000000000,99.999984741,-100.000015259));
|
||||
#119=VERTEX_POINT('',#118);
|
||||
#120=CARTESIAN_POINT('',(100.000000000,100.000015259,99.999984741));
|
||||
#121=VERTEX_POINT('',#120);
|
||||
#122=CARTESIAN_POINT('',(100.000000000,-99.999984741,100.000015259));
|
||||
#123=VERTEX_POINT('',#122);
|
||||
#124=CARTESIAN_POINT('',(100.000000000,-99.999984741,100.000015259));
|
||||
#125=VERTEX_POINT('',#124);
|
||||
#126=CARTESIAN_POINT('',(100.000000000,100.000015259,99.999984741));
|
||||
#127=VERTEX_POINT('',#126);
|
||||
#128=CARTESIAN_POINT('',(-100.000000000,100.000015259,99.999984741));
|
||||
#129=VERTEX_POINT('',#128);
|
||||
#130=CARTESIAN_POINT('',(-100.000000000,-99.999984741,100.000015259));
|
||||
#131=VERTEX_POINT('',#130);
|
||||
#132=CARTESIAN_POINT('',(-100.000000000,-100.000015259,-99.999984741));
|
||||
#133=VERTEX_POINT('',#132);
|
||||
#134=CARTESIAN_POINT('',(100.000000000,-100.000015259,-99.999984741));
|
||||
#135=VERTEX_POINT('',#134);
|
||||
#136=CARTESIAN_POINT('',(100.000000000,-99.999984741,100.000015259));
|
||||
#137=VERTEX_POINT('',#136);
|
||||
#138=CARTESIAN_POINT('',(-100.000000000,-99.999984741,100.000015259));
|
||||
#139=VERTEX_POINT('',#138);
|
||||
#140=CARTESIAN_POINT('',(100.000000000,99.999984741,-100.000015259));
|
||||
#141=VERTEX_POINT('',#140);
|
||||
#142=CARTESIAN_POINT('',(-100.000000000,99.999984741,-100.000015259));
|
||||
#143=VERTEX_POINT('',#142);
|
||||
#144=CARTESIAN_POINT('',(-100.000000000,100.000015259,99.999984741));
|
||||
#145=VERTEX_POINT('',#144);
|
||||
#146=CARTESIAN_POINT('',(100.000000000,100.000015259,99.999984741));
|
||||
#147=VERTEX_POINT('',#146);
|
||||
#148=STYLED_ITEM('',(#149),#156);
|
||||
#149=PRESENTATION_STYLE_ASSIGNMENT((#150));
|
||||
#150=SURFACE_STYLE_USAGE(.BOTH.,#151);
|
||||
#151=SURFACE_SIDE_STYLE('',(#152));
|
||||
#152=SURFACE_STYLE_FILL_AREA(#153);
|
||||
#153=FILL_AREA_STYLE('',(#154));
|
||||
#154=FILL_AREA_STYLE_COLOUR('',#155);
|
||||
#155=COLOUR_RGB('',0.800000012,0.800000012,0.800000012);
|
||||
#156=FACE_SURFACE('',(#161),#157,.T.);
|
||||
#157=PLANE('',#158);
|
||||
#158=AXIS2_PLACEMENT_3D('',#100,#159,#160);
|
||||
#159=DIRECTION('',(1.000000000,0.000000000,0.000000000));
|
||||
#160=DIRECTION('',(0.000000000,1.000000000,0.000000000));
|
||||
#161=FACE_BOUND('',#162,.T.);
|
||||
#162=EDGE_LOOP('',(#163,#164,#165,#166));
|
||||
#163=ORIENTED_EDGE('',*,*,#167,.T.);
|
||||
#164=ORIENTED_EDGE('',*,*,#168,.T.);
|
||||
#165=ORIENTED_EDGE('',*,*,#169,.T.);
|
||||
#166=ORIENTED_EDGE('',*,*,#170,.T.);
|
||||
#167=EDGE_CURVE('',#101,#103,#171,.F.);
|
||||
#168=EDGE_CURVE('',#103,#105,#172,.T.);
|
||||
#169=EDGE_CURVE('',#105,#107,#173,.T.);
|
||||
#170=EDGE_CURVE('',#107,#101,#174,.T.);
|
||||
#171=LINE('',#100,#175);
|
||||
#172=LINE('',#102,#176);
|
||||
#173=LINE('',#104,#177);
|
||||
#174=LINE('',#106,#178);
|
||||
#175=VECTOR('',#179,1.0);
|
||||
#176=VECTOR('',#180,1.0);
|
||||
#177=VECTOR('',#181,1.0);
|
||||
#178=VECTOR('',#182,1.0);
|
||||
#179=DIRECTION('',(0.000000000,0.000000000,1.000000000));
|
||||
#180=DIRECTION('',(0.000000000,1.000000000,0.000000000));
|
||||
#181=DIRECTION('',(0.000000000,0.000000000,-1.000000000));
|
||||
#182=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#183=STYLED_ITEM('',(#184),#191);
|
||||
#184=PRESENTATION_STYLE_ASSIGNMENT((#185));
|
||||
#185=SURFACE_STYLE_USAGE(.BOTH.,#186);
|
||||
#186=SURFACE_SIDE_STYLE('',(#187));
|
||||
#187=SURFACE_STYLE_FILL_AREA(#188);
|
||||
#188=FILL_AREA_STYLE('',(#189));
|
||||
#189=FILL_AREA_STYLE_COLOUR('',#190);
|
||||
#190=COLOUR_RGB('',0.800000012,0.800000012,0.800000012);
|
||||
#191=FACE_SURFACE('',(#196),#192,.T.);
|
||||
#192=PLANE('',#193);
|
||||
#193=AXIS2_PLACEMENT_3D('',#108,#194,#195);
|
||||
#194=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#195=DIRECTION('',(1.000000000,0.000000000,0.000000000));
|
||||
#196=FACE_BOUND('',#197,.T.);
|
||||
#197=EDGE_LOOP('',(#198,#199,#200,#201));
|
||||
#198=ORIENTED_EDGE('',*,*,#202,.T.);
|
||||
#199=ORIENTED_EDGE('',*,*,#203,.T.);
|
||||
#200=ORIENTED_EDGE('',*,*,#204,.T.);
|
||||
#201=ORIENTED_EDGE('',*,*,#205,.T.);
|
||||
#202=EDGE_CURVE('',#109,#111,#206,.F.);
|
||||
#203=EDGE_CURVE('',#111,#113,#207,.T.);
|
||||
#204=EDGE_CURVE('',#113,#115,#208,.T.);
|
||||
#205=EDGE_CURVE('',#115,#109,#209,.T.);
|
||||
#206=LINE('',#108,#210);
|
||||
#207=LINE('',#110,#211);
|
||||
#208=LINE('',#112,#212);
|
||||
#209=LINE('',#114,#213);
|
||||
#210=VECTOR('',#214,1.0);
|
||||
#211=VECTOR('',#215,1.0);
|
||||
#212=VECTOR('',#216,1.0);
|
||||
#213=VECTOR('',#217,1.0);
|
||||
#214=DIRECTION('',(0.000000000,0.000000000,1.000000000));
|
||||
#215=DIRECTION('',(1.000000000,0.000000000,0.000000000));
|
||||
#216=DIRECTION('',(0.000000000,0.000000000,-1.000000000));
|
||||
#217=DIRECTION('',(-1.000000000,0.000000000,0.000000000));
|
||||
#218=STYLED_ITEM('',(#219),#226);
|
||||
#219=PRESENTATION_STYLE_ASSIGNMENT((#220));
|
||||
#220=SURFACE_STYLE_USAGE(.BOTH.,#221);
|
||||
#221=SURFACE_SIDE_STYLE('',(#222));
|
||||
#222=SURFACE_STYLE_FILL_AREA(#223);
|
||||
#223=FILL_AREA_STYLE('',(#224));
|
||||
#224=FILL_AREA_STYLE_COLOUR('',#225);
|
||||
#225=COLOUR_RGB('',0.800000012,0.800000012,0.800000012);
|
||||
#226=FACE_SURFACE('',(#231),#227,.T.);
|
||||
#227=PLANE('',#228);
|
||||
#228=AXIS2_PLACEMENT_3D('',#116,#229,#230);
|
||||
#229=DIRECTION('',(-1.000000000,0.000000000,0.000000000));
|
||||
#230=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#231=FACE_BOUND('',#232,.T.);
|
||||
#232=EDGE_LOOP('',(#233,#234,#235,#236));
|
||||
#233=ORIENTED_EDGE('',*,*,#237,.T.);
|
||||
#234=ORIENTED_EDGE('',*,*,#238,.T.);
|
||||
#235=ORIENTED_EDGE('',*,*,#239,.T.);
|
||||
#236=ORIENTED_EDGE('',*,*,#240,.T.);
|
||||
#237=EDGE_CURVE('',#117,#119,#241,.F.);
|
||||
#238=EDGE_CURVE('',#119,#121,#242,.T.);
|
||||
#239=EDGE_CURVE('',#121,#123,#243,.T.);
|
||||
#240=EDGE_CURVE('',#123,#117,#244,.T.);
|
||||
#241=LINE('',#116,#245);
|
||||
#242=LINE('',#118,#246);
|
||||
#243=LINE('',#120,#247);
|
||||
#244=LINE('',#122,#248);
|
||||
#245=VECTOR('',#249,1.0);
|
||||
#246=VECTOR('',#250,1.0);
|
||||
#247=VECTOR('',#251,1.0);
|
||||
#248=VECTOR('',#252,1.0);
|
||||
#249=DIRECTION('',(0.000000000,0.000000000,1.000000000));
|
||||
#250=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#251=DIRECTION('',(0.000000000,0.000000000,-1.000000000));
|
||||
#252=DIRECTION('',(0.000000000,1.000000000,0.000000000));
|
||||
#253=STYLED_ITEM('',(#254),#261);
|
||||
#254=PRESENTATION_STYLE_ASSIGNMENT((#255));
|
||||
#255=SURFACE_STYLE_USAGE(.BOTH.,#256);
|
||||
#256=SURFACE_SIDE_STYLE('',(#257));
|
||||
#257=SURFACE_STYLE_FILL_AREA(#258);
|
||||
#258=FILL_AREA_STYLE('',(#259));
|
||||
#259=FILL_AREA_STYLE_COLOUR('',#260);
|
||||
#260=COLOUR_RGB('',0.800000012,0.800000012,0.800000012);
|
||||
#261=FACE_SURFACE('',(#266),#262,.T.);
|
||||
#262=PLANE('',#263);
|
||||
#263=AXIS2_PLACEMENT_3D('',#124,#264,#265);
|
||||
#264=DIRECTION('',(0.000000000,1.000000000,-0.000000000));
|
||||
#265=DIRECTION('',(-1.000000000,0.000000000,0.000000000));
|
||||
#266=FACE_BOUND('',#267,.T.);
|
||||
#267=EDGE_LOOP('',(#268,#269,#270,#271));
|
||||
#268=ORIENTED_EDGE('',*,*,#272,.T.);
|
||||
#269=ORIENTED_EDGE('',*,*,#273,.T.);
|
||||
#270=ORIENTED_EDGE('',*,*,#274,.T.);
|
||||
#271=ORIENTED_EDGE('',*,*,#275,.T.);
|
||||
#272=EDGE_CURVE('',#125,#127,#276,.F.);
|
||||
#273=EDGE_CURVE('',#127,#129,#277,.T.);
|
||||
#274=EDGE_CURVE('',#129,#131,#278,.T.);
|
||||
#275=EDGE_CURVE('',#131,#125,#279,.T.);
|
||||
#276=LINE('',#124,#280);
|
||||
#277=LINE('',#126,#281);
|
||||
#278=LINE('',#128,#282);
|
||||
#279=LINE('',#130,#283);
|
||||
#280=VECTOR('',#284,1.0);
|
||||
#281=VECTOR('',#285,1.0);
|
||||
#282=VECTOR('',#286,1.0);
|
||||
#283=VECTOR('',#287,1.0);
|
||||
#284=DIRECTION('',(0.000000000,0.000000000,1.000000000));
|
||||
#285=DIRECTION('',(-1.000000000,0.000000000,0.000000000));
|
||||
#286=DIRECTION('',(0.000000000,0.000000000,-1.000000000));
|
||||
#287=DIRECTION('',(1.000000000,0.000000000,0.000000000));
|
||||
#288=STYLED_ITEM('',(#289),#296);
|
||||
#289=PRESENTATION_STYLE_ASSIGNMENT((#290));
|
||||
#290=SURFACE_STYLE_USAGE(.BOTH.,#291);
|
||||
#291=SURFACE_SIDE_STYLE('',(#292));
|
||||
#292=SURFACE_STYLE_FILL_AREA(#293);
|
||||
#293=FILL_AREA_STYLE('',(#294));
|
||||
#294=FILL_AREA_STYLE_COLOUR('',#295);
|
||||
#295=COLOUR_RGB('',0.800000012,0.800000012,0.800000012);
|
||||
#296=FACE_SURFACE('',(#301),#297,.T.);
|
||||
#297=PLANE('',#298);
|
||||
#298=AXIS2_PLACEMENT_3D('',#132,#299,#300);
|
||||
#299=DIRECTION('',(-0.000000000,0.000000000,1.000000000));
|
||||
#300=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#301=FACE_BOUND('',#302,.T.);
|
||||
#302=EDGE_LOOP('',(#303,#304,#305,#306));
|
||||
#303=ORIENTED_EDGE('',*,*,#307,.T.);
|
||||
#304=ORIENTED_EDGE('',*,*,#308,.T.);
|
||||
#305=ORIENTED_EDGE('',*,*,#309,.T.);
|
||||
#306=ORIENTED_EDGE('',*,*,#310,.T.);
|
||||
#307=EDGE_CURVE('',#133,#135,#311,.F.);
|
||||
#308=EDGE_CURVE('',#135,#137,#312,.T.);
|
||||
#309=EDGE_CURVE('',#137,#139,#313,.T.);
|
||||
#310=EDGE_CURVE('',#139,#133,#314,.T.);
|
||||
#311=LINE('',#132,#315);
|
||||
#312=LINE('',#134,#316);
|
||||
#313=LINE('',#136,#317);
|
||||
#314=LINE('',#138,#318);
|
||||
#315=VECTOR('',#319,1.0);
|
||||
#316=VECTOR('',#320,1.0);
|
||||
#317=VECTOR('',#321,1.0);
|
||||
#318=VECTOR('',#322,1.0);
|
||||
#319=DIRECTION('',(1.000000000,0.000000000,0.000000000));
|
||||
#320=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#321=DIRECTION('',(-1.000000000,0.000000000,0.000000000));
|
||||
#322=DIRECTION('',(0.000000000,1.000000000,0.000000000));
|
||||
#323=STYLED_ITEM('',(#324),#331);
|
||||
#324=PRESENTATION_STYLE_ASSIGNMENT((#325));
|
||||
#325=SURFACE_STYLE_USAGE(.BOTH.,#326);
|
||||
#326=SURFACE_SIDE_STYLE('',(#327));
|
||||
#327=SURFACE_STYLE_FILL_AREA(#328);
|
||||
#328=FILL_AREA_STYLE('',(#329));
|
||||
#329=FILL_AREA_STYLE_COLOUR('',#330);
|
||||
#330=COLOUR_RGB('',0.800000012,0.800000012,0.800000012);
|
||||
#331=FACE_SURFACE('',(#336),#332,.T.);
|
||||
#332=PLANE('',#333);
|
||||
#333=AXIS2_PLACEMENT_3D('',#140,#334,#335);
|
||||
#334=DIRECTION('',(-0.000000000,-0.000000000,-1.000000000));
|
||||
#335=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#336=FACE_BOUND('',#337,.T.);
|
||||
#337=EDGE_LOOP('',(#338,#339,#340,#341));
|
||||
#338=ORIENTED_EDGE('',*,*,#342,.T.);
|
||||
#339=ORIENTED_EDGE('',*,*,#343,.T.);
|
||||
#340=ORIENTED_EDGE('',*,*,#344,.T.);
|
||||
#341=ORIENTED_EDGE('',*,*,#345,.T.);
|
||||
#342=EDGE_CURVE('',#141,#143,#346,.F.);
|
||||
#343=EDGE_CURVE('',#143,#145,#347,.T.);
|
||||
#344=EDGE_CURVE('',#145,#147,#348,.T.);
|
||||
#345=EDGE_CURVE('',#147,#141,#349,.T.);
|
||||
#346=LINE('',#140,#350);
|
||||
#347=LINE('',#142,#351);
|
||||
#348=LINE('',#144,#352);
|
||||
#349=LINE('',#146,#353);
|
||||
#350=VECTOR('',#354,1.0);
|
||||
#351=VECTOR('',#355,1.0);
|
||||
#352=VECTOR('',#356,1.0);
|
||||
#353=VECTOR('',#357,1.0);
|
||||
#354=DIRECTION('',(-1.000000000,0.000000000,0.000000000));
|
||||
#355=DIRECTION('',(0.000000000,-1.000000000,0.000000000));
|
||||
#356=DIRECTION('',(1.000000000,0.000000000,0.000000000));
|
||||
#357=DIRECTION('',(0.000000000,1.000000000,0.000000000));
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
11
index.js
11
index.js
@ -2,28 +2,21 @@
|
||||
import color from "picocolors";
|
||||
import { showMainMenu } from "./lib/menu.js";
|
||||
|
||||
// 主循环
|
||||
while (true) {
|
||||
const selected = await showMainMenu();
|
||||
|
||||
console.clear();
|
||||
console.log(color.cyan(`\n正在启动: ${selected.name}...\n`));
|
||||
console.log(color.cyan("\n正在启动: " + selected.name + "...\n"));
|
||||
|
||||
try {
|
||||
const tool = await import(selected.module);
|
||||
const result = await tool.run();
|
||||
|
||||
// 返回主菜单
|
||||
if (result === "back") continue;
|
||||
|
||||
// 工具完成后退出
|
||||
break;
|
||||
} catch (err) {
|
||||
console.log(color.yellow(`\n⚠️ ${selected.name} 模块尚未实现`));
|
||||
console.log(color.yellow("\n⚠️ " + selected.name + " 模块尚未实现"));
|
||||
console.log(color.dim(err.message));
|
||||
console.log(color.dim("\n按任意键返回主菜单..."));
|
||||
|
||||
// 等待按键
|
||||
await new Promise(resolve => {
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.once("data", () => {
|
||||
|
||||
35
lib/convert/config.js
Normal file
35
lib/convert/config.js
Normal file
@ -0,0 +1,35 @@
|
||||
import fs from "fs";
|
||||
import { getImportExtensions, getExportFormats } from "./converters.js";
|
||||
import { sortByUsage } from "../stats.js";
|
||||
|
||||
export function listConvertibleFiles() {
|
||||
const cwd = process.cwd();
|
||||
const exts = getImportExtensions();
|
||||
return fs.readdirSync(cwd).filter(file => {
|
||||
const ext = file.slice(file.lastIndexOf(".")).toLowerCase();
|
||||
return exts.includes(ext);
|
||||
});
|
||||
}
|
||||
|
||||
export function getSteps() {
|
||||
const files = listConvertibleFiles();
|
||||
const formats = sortByUsage("convert_format", getExportFormats());
|
||||
|
||||
return [
|
||||
{
|
||||
name: "源文件",
|
||||
type: "multiselect",
|
||||
message: files.length ? "选择要转换的模型文件" : "当前目录未找到可转换的模型文件",
|
||||
options: files.map(file => ({ value: file, label: file })),
|
||||
default: [],
|
||||
emptyMessage: "请按 Esc 返回并放入模型文件后重试"
|
||||
},
|
||||
{
|
||||
name: "输出格式",
|
||||
type: "select",
|
||||
message: "选择目标格式",
|
||||
options: formats.map(f => ({ value: f, label: f })),
|
||||
default: formats[0]
|
||||
}
|
||||
];
|
||||
}
|
||||
31
lib/convert/converters.js
Normal file
31
lib/convert/converters.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { spawnSync } from "child_process";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ASSIMP_PATH = path.join(__dirname, "../../bin/assimp.exe");
|
||||
|
||||
let importExts = null;
|
||||
let exportFormats = null;
|
||||
|
||||
export function getImportExtensions() {
|
||||
if (!importExts) {
|
||||
const r = spawnSync(ASSIMP_PATH, ["listext"], { encoding: "utf8" });
|
||||
importExts = r.stdout.trim().split(";").map(e => e.replace("*", "").toLowerCase());
|
||||
}
|
||||
return importExts;
|
||||
}
|
||||
|
||||
export function getExportFormats() {
|
||||
if (!exportFormats) {
|
||||
const r = spawnSync(ASSIMP_PATH, ["listexport"], { encoding: "utf8" });
|
||||
exportFormats = r.stdout.trim().split(/\r?\n/).filter(Boolean);
|
||||
}
|
||||
return exportFormats;
|
||||
}
|
||||
|
||||
export async function convert(inputFile, outputFile, format, cwd = process.cwd()) {
|
||||
const r = spawnSync(ASSIMP_PATH, ["export", inputFile, outputFile, `-f${format}`], { encoding: "utf8", cwd });
|
||||
if (r.status !== 0) throw new Error(r.stderr || r.stdout || "转换失败");
|
||||
return outputFile;
|
||||
}
|
||||
108
lib/convert/index.js
Normal file
108
lib/convert/index.js
Normal file
@ -0,0 +1,108 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import color from "picocolors";
|
||||
import { convert } from "./converters.js";
|
||||
import { runInteractive, showSummary } from "./ui.js";
|
||||
import { stopKeypress, initKeypress, onKey } from "../keyboard.js";
|
||||
import { record } from "../stats.js";
|
||||
|
||||
const FORMAT_EXT = {
|
||||
collada: ".dae", x: ".x", stp: ".stp", obj: ".obj", objnomtl: ".obj",
|
||||
stl: ".stl", stlb: ".stl", ply: ".ply", plyb: ".ply", "3ds": ".3ds",
|
||||
gltf2: ".gltf", glb2: ".glb", gltf: ".gltf", glb: ".glb",
|
||||
assbin: ".assbin", assxml: ".assxml", x3d: ".x3d",
|
||||
fbx: ".fbx", fbxa: ".fbx", "3mf": ".3mf", pbrt: ".pbrt", assjson: ".json"
|
||||
};
|
||||
|
||||
function getOutputExt(format) {
|
||||
return FORMAT_EXT[format] || "." + format;
|
||||
}
|
||||
|
||||
async function processFile(file, config) {
|
||||
const cwd = process.cwd();
|
||||
const baseName = file.slice(0, file.lastIndexOf("."));
|
||||
const outputExt = getOutputExt(config.outputFormat);
|
||||
const outputFile = baseName + outputExt;
|
||||
|
||||
if (!fs.existsSync(path.join(cwd, file))) {
|
||||
return { ok: false, file, reason: "文件不存在" };
|
||||
}
|
||||
|
||||
await convert(file, outputFile, config.outputFormat, cwd);
|
||||
return { ok: true, file, output: outputFile };
|
||||
}
|
||||
|
||||
async function waitForEsc() {
|
||||
initKeypress();
|
||||
return new Promise(resolve => {
|
||||
onKey((str, key) => {
|
||||
if (key?.name === "escape" || (key?.ctrl && key?.name === "c")) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
const result = await runInteractive();
|
||||
if (!result) return "back";
|
||||
|
||||
const { steps, results } = result;
|
||||
const config = {
|
||||
files: results[0] || [],
|
||||
outputFormat: results[1] || "glb2"
|
||||
};
|
||||
|
||||
stopKeypress();
|
||||
showSummary([
|
||||
"源文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||
"目标格式: " + config.outputFormat.toUpperCase()
|
||||
]);
|
||||
|
||||
if (!config.files.length) {
|
||||
console.log(color.yellow("未选择任何模型文件"));
|
||||
console.log(color.dim("\n按 Esc 返回"));
|
||||
await waitForEsc();
|
||||
return "back";
|
||||
}
|
||||
|
||||
const total = config.files.length;
|
||||
let success = 0;
|
||||
const failed = [];
|
||||
|
||||
console.log(color.cyan(`开始转换 ${total} 个文件...\n`));
|
||||
|
||||
for (let i = 0; i < config.files.length; i++) {
|
||||
const file = config.files[i];
|
||||
const progress = `[${i + 1}/${total}]`;
|
||||
console.log(color.dim(`${progress} 处理中: ${file}`));
|
||||
|
||||
try {
|
||||
const result = await processFile(file, config);
|
||||
if (result.ok) {
|
||||
success++;
|
||||
record("convert_format", config.outputFormat);
|
||||
console.log(color.green(`${progress} ✓ ${file} → ${path.basename(result.output)}`));
|
||||
} else {
|
||||
failed.push({ file, reason: result.reason });
|
||||
console.log(color.yellow(`${progress} ⊘ 跳过: ${file} (${result.reason})`));
|
||||
}
|
||||
} catch (err) {
|
||||
failed.push({ file, reason: err?.message || String(err) });
|
||||
console.log(color.red(`${progress} ✖ 失败: ${file}`));
|
||||
console.log(color.dim(" " + String(err?.message || err)));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n" + color.bgGreen(color.black(" 转换完成 ")));
|
||||
if (success) {
|
||||
console.log(color.green(`成功: ${success} 个`));
|
||||
}
|
||||
if (failed.length) {
|
||||
console.log(color.yellow(`失败: ${failed.length} 个`));
|
||||
}
|
||||
|
||||
console.log(color.dim("\n按 Esc 返回"));
|
||||
await waitForEsc();
|
||||
return "back";
|
||||
}
|
||||
9
lib/convert/ui.js
Normal file
9
lib/convert/ui.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { createStepUI } from "../utils/stepui.js";
|
||||
import { getSteps } from "./config.js";
|
||||
|
||||
const ui = createStepUI({
|
||||
title: "格式转换工具",
|
||||
getSteps
|
||||
});
|
||||
|
||||
export const { runInteractive, showSummary } = ui;
|
||||
27
lib/gltf/config.js
Normal file
27
lib/gltf/config.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { listGltfFiles } from "../utils/gltf.js";
|
||||
|
||||
const extensionStep = {
|
||||
name: "扩展选项",
|
||||
type: "multiselect",
|
||||
message: "请选择要添加的glTF扩展",
|
||||
options: [
|
||||
{ value: "textureBasisu", label: "KHR_texture_basisu(纹理自动扩展)", hint: "自动添加KTX2纹理扩展支持" },
|
||||
{ value: "placeholder1", label: "预留选项1", hint: "功能开发中..." },
|
||||
{ value: "placeholder2", label: "预留选项2", hint: "功能开发中..." },
|
||||
{ value: "placeholder3", label: "预留选项3", hint: "功能开发中..." }
|
||||
],
|
||||
default: ["textureBasisu"]
|
||||
};
|
||||
|
||||
export function getSteps() {
|
||||
const files = listGltfFiles();
|
||||
const fileStep = {
|
||||
name: "文件选择",
|
||||
type: "multiselect",
|
||||
message: files.length ? "请选择要处理的glTF文件" : "当前目录未找到glTF文件",
|
||||
options: files.map(file => ({ value: file, label: file })),
|
||||
default: [...files]
|
||||
};
|
||||
|
||||
return [fileStep, extensionStep];
|
||||
}
|
||||
73
lib/gltf/index.js
Normal file
73
lib/gltf/index.js
Normal file
@ -0,0 +1,73 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import color from "picocolors";
|
||||
import { checkRequiredFiles, modifyGltfContent, listGltfFiles, BACKUP_SUFFIX } from "../utils/gltf.js";
|
||||
import { runInteractive, showSummary } from "./ui.js";
|
||||
import { stopKeypress, waitForKey } from "../keyboard.js";
|
||||
|
||||
export function 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) {
|
||||
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 };
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
const result = await runInteractive();
|
||||
if (!result) return "back";
|
||||
|
||||
stopKeypress();
|
||||
|
||||
const { results } = result;
|
||||
const config = {
|
||||
files: results[0] || [],
|
||||
extensions: results[1] || []
|
||||
};
|
||||
|
||||
showSummary([
|
||||
"处理文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||
"扩展选项: " + (config.extensions.length ? config.extensions.join(", ") : "未选择")
|
||||
]);
|
||||
|
||||
const { success, count } = runGltfExtension(config);
|
||||
if (success) {
|
||||
console.log(color.green("\n✓ 已修改 " + count + " 个 glTF 文件"));
|
||||
}
|
||||
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
9
lib/gltf/ui.js
Normal file
9
lib/gltf/ui.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { createStepUI } from "../utils/stepui.js";
|
||||
import { getSteps } from "./config.js";
|
||||
|
||||
const ui = createStepUI({
|
||||
title: "glTF 扩展工具",
|
||||
getSteps
|
||||
});
|
||||
|
||||
export const { runInteractive, showSummary } = ui;
|
||||
14
lib/grid.js
14
lib/grid.js
@ -1,6 +1,16 @@
|
||||
import color from "picocolors";
|
||||
import { initKeypress, onKey } from "./keyboard.js";
|
||||
|
||||
function clearScreen() {
|
||||
if (process.stdout.isTTY) {
|
||||
process.stdout.write("\x1b[2J"); // clear screen
|
||||
process.stdout.write("\x1b[3J"); // clear scrollback
|
||||
process.stdout.write("\x1b[H"); // move cursor home
|
||||
} else {
|
||||
console.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 计算字符串显示宽度
|
||||
function strWidth(str) {
|
||||
let width = 0;
|
||||
@ -35,7 +45,7 @@ export async function gridSelect(options) {
|
||||
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
||||
|
||||
function render() {
|
||||
console.clear();
|
||||
clearScreen();
|
||||
|
||||
if (renderHeader) {
|
||||
console.log(renderHeader());
|
||||
@ -110,7 +120,7 @@ export async function gridSelect(options) {
|
||||
setImmediate(() => resolve(items[current]));
|
||||
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||
process.stdin.setRawMode(false);
|
||||
console.clear();
|
||||
clearScreen();
|
||||
console.log(color.yellow("👋 再见!"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@ -16,3 +16,22 @@ export function initKeypress() {
|
||||
export function onKey(handler) {
|
||||
process.stdin.on("keypress", handler);
|
||||
}
|
||||
|
||||
export function stopKeypress() {
|
||||
process.stdin.removeAllListeners("keypress");
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function waitForKey(message = "按任意键返回...") {
|
||||
return new Promise(resolve => {
|
||||
console.log("\n" + message);
|
||||
initKeypress();
|
||||
const handler = () => {
|
||||
stopKeypress();
|
||||
resolve();
|
||||
};
|
||||
process.stdin.once("keypress", handler);
|
||||
});
|
||||
}
|
||||
|
||||
@ -55,7 +55,8 @@ export const steps = [
|
||||
{ value: "overwrite", label: "覆盖已存在文件(自动替换同名文件)" },
|
||||
{ value: "keepOriginal", label: "保留原文件(压缩后不删除源文件)" },
|
||||
{ value: "report", label: "生成压缩报告(输出详细的压缩统计信息)" },
|
||||
{ value: "silent", label: "静默模式(减少控制台输出信息)" }
|
||||
{ value: "silent", label: "静默模式(减少控制台输出信息)" },
|
||||
{ value: "gltfExtension", label: "修改glTF扩展(添加KHR_texture_basisu)", dynamic: true }
|
||||
],
|
||||
default: ["overwrite", "keepOriginal"]
|
||||
}
|
||||
|
||||
3
lib/ktx2/gltf.js
Normal file
3
lib/ktx2/gltf.js
Normal file
@ -0,0 +1,3 @@
|
||||
// 从 utils 导出,保持兼容
|
||||
export { hasGltfFile, checkRequiredFiles, modifyGltfContent } from "../utils/gltf.js";
|
||||
export { runGltfExtension } from "../gltf/index.js";
|
||||
@ -1,40 +1,49 @@
|
||||
import color from "picocolors";
|
||||
import { checkToktx, scanImages, compressAll } from "./compressor.js";
|
||||
import { runInteractive, showSummary } from "./ui.js";
|
||||
import { runGltfExtension } from "./gltf.js";
|
||||
import { stopKeypress, waitForKey } from "../keyboard.js";
|
||||
|
||||
export async function run() {
|
||||
// 检查 toktx
|
||||
checkToktx();
|
||||
|
||||
// 运行交互界面
|
||||
const result = await runInteractive();
|
||||
|
||||
// ESC 返回主菜单
|
||||
if (!result) return "back";
|
||||
|
||||
const [exts, quality, encoding, mipmap, outputOpts] = result;
|
||||
stopKeypress();
|
||||
|
||||
const { results } = result;
|
||||
const [exts, quality, encoding, mipmap, outputOpts] = results;
|
||||
const config = { exts, quality, encoding, mipmap, outputOpts };
|
||||
showSummary([
|
||||
"文件格式: " + config.exts.join(", "),
|
||||
"压缩程度: " + config.quality,
|
||||
"编码格式: " + config.encoding,
|
||||
"Mipmap: " + config.mipmap,
|
||||
"输出选项: " + config.outputOpts.join(", ")
|
||||
]);
|
||||
|
||||
// 显示配置摘要
|
||||
showSummary(config);
|
||||
|
||||
// 扫描文件
|
||||
const { images, cwd } = scanImages(exts);
|
||||
|
||||
if (images.length === 0) {
|
||||
console.log(color.yellow("当前目录没有匹配的图片"));
|
||||
return;
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
|
||||
console.log(`📁 找到 ${color.cyan(images.length)} 个待转换文件\n`);
|
||||
|
||||
// 执行压缩
|
||||
console.log("📁 找到 " + color.cyan(images.length) + " 个待转换文件\n");
|
||||
const { total, failed } = await compressAll(images, config, cwd);
|
||||
|
||||
// 显示结果
|
||||
if (failed > 0) {
|
||||
console.log(color.yellow(`\n⚠️ 完成,但有 ${failed} 个文件失败`));
|
||||
console.log(color.yellow("\n⚠️ 完成,但有 " + failed + " 个文件失败"));
|
||||
} else {
|
||||
console.log(color.green("\n🎉 全部文件压缩完成!"));
|
||||
}
|
||||
|
||||
if (outputOpts.includes("gltfExtension")) {
|
||||
console.log(color.cyan("\n正在修改 glTF 文件..."));
|
||||
const { success, count } = runGltfExtension();
|
||||
if (success) console.log(color.green("✓ 已修改 " + count + " 个 glTF 文件"));
|
||||
}
|
||||
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
|
||||
159
lib/ktx2/ui.js
159
lib/ktx2/ui.js
@ -1,151 +1,20 @@
|
||||
import color from "picocolors";
|
||||
import { createStepUI } from "../utils/stepui.js";
|
||||
import { steps } from "./config.js";
|
||||
import { initKeypress, onKey } from "../keyboard.js";
|
||||
import { hasGltfFile } from "./gltf.js";
|
||||
|
||||
// 存储结果和状态
|
||||
let results = [];
|
||||
let completed = new Set();
|
||||
let currentStep = 0;
|
||||
let currentOption = 0;
|
||||
|
||||
// 初始化结果
|
||||
export function initResults() {
|
||||
results = steps.map(s => s.type === "multiselect" ? [...s.default] : s.default);
|
||||
completed = new Set();
|
||||
currentStep = 0;
|
||||
currentOption = 0;
|
||||
function getFilteredSteps() {
|
||||
const hasGltf = hasGltfFile();
|
||||
return steps.map(step => {
|
||||
if (step.name === "输出选项") {
|
||||
return { ...step, options: step.options.filter(opt => !opt.dynamic || hasGltf) };
|
||||
}
|
||||
|
||||
// 渲染导航栏
|
||||
function renderNav() {
|
||||
const nav = steps.map((step, i) => {
|
||||
if (completed.has(i)) {
|
||||
return color.green(`☑ ${step.name}`);
|
||||
} else if (i === currentStep) {
|
||||
return color.bgCyan(color.black(` ${step.name} `));
|
||||
} else {
|
||||
return color.dim(`□ ${step.name}`);
|
||||
}
|
||||
});
|
||||
return `← ${nav.join(" ")} ${color.green("✓Submit")} →`;
|
||||
}
|
||||
|
||||
// 渲染选项列表
|
||||
function renderOptions() {
|
||||
const step = steps[currentStep];
|
||||
const lines = [];
|
||||
|
||||
lines.push(color.cyan(step.message));
|
||||
lines.push("");
|
||||
|
||||
step.options.forEach((opt, i) => {
|
||||
const isCurrent = i === currentOption;
|
||||
const isSelected = step.type === "multiselect"
|
||||
? results[currentStep].includes(opt.value)
|
||||
: results[currentStep] === opt.value;
|
||||
|
||||
let prefix;
|
||||
if (step.type === "multiselect") {
|
||||
prefix = isSelected ? color.green("◉ ") : "○ ";
|
||||
} else {
|
||||
prefix = isSelected ? color.green("● ") : "○ ";
|
||||
}
|
||||
|
||||
const cursor = isCurrent ? color.cyan("❯ ") : " ";
|
||||
const label = isCurrent ? color.cyan(opt.label) : opt.label;
|
||||
const check = isSelected ? color.green(" ✓") : "";
|
||||
|
||||
lines.push(`${cursor}${prefix}${label}${check}`);
|
||||
if (opt.hint) {
|
||||
lines.push(` ${color.dim(opt.hint)}`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// 渲染整个界面
|
||||
function render() {
|
||||
console.clear();
|
||||
console.log(color.bgCyan(color.black(" KTX2 纹理压缩工具 ")));
|
||||
console.log("\n" + renderNav());
|
||||
console.log(color.dim("\n← → 切换步骤 | ↑ ↓ 选择 | Space 选中 | Tab 提交 | Esc 返回\n"));
|
||||
console.log(renderOptions());
|
||||
}
|
||||
|
||||
// 主交互循环
|
||||
export async function runInteractive() {
|
||||
initResults();
|
||||
initKeypress();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
render();
|
||||
|
||||
onKey((str, key) => {
|
||||
if (!key) return;
|
||||
|
||||
const step = steps[currentStep];
|
||||
const optCount = step.options.length;
|
||||
|
||||
if (key.name === "left") {
|
||||
if (currentStep > 0) {
|
||||
currentStep--;
|
||||
currentOption = 0;
|
||||
}
|
||||
render();
|
||||
} else if (key.name === "right") {
|
||||
if (currentStep < steps.length - 1) {
|
||||
currentStep++;
|
||||
currentOption = 0;
|
||||
}
|
||||
render();
|
||||
} else if (key.name === "up") {
|
||||
currentOption = (currentOption - 1 + optCount) % optCount;
|
||||
render();
|
||||
} else if (key.name === "down") {
|
||||
currentOption = (currentOption + 1) % optCount;
|
||||
render();
|
||||
} else if (key.name === "space") {
|
||||
const opt = step.options[currentOption];
|
||||
if (step.type === "multiselect") {
|
||||
const idx = results[currentStep].indexOf(opt.value);
|
||||
if (idx >= 0) {
|
||||
results[currentStep].splice(idx, 1);
|
||||
} else {
|
||||
results[currentStep].push(opt.value);
|
||||
}
|
||||
} else {
|
||||
results[currentStep] = opt.value;
|
||||
}
|
||||
completed.add(currentStep);
|
||||
render();
|
||||
} else if (key.name === "return") {
|
||||
if (step.type === "select") {
|
||||
results[currentStep] = step.options[currentOption].value;
|
||||
}
|
||||
completed.add(currentStep);
|
||||
if (currentStep < steps.length - 1) {
|
||||
currentStep++;
|
||||
currentOption = 0;
|
||||
}
|
||||
render();
|
||||
} else if (key.name === "tab") {
|
||||
resolve(results);
|
||||
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
return step;
|
||||
});
|
||||
}
|
||||
|
||||
// 显示配置摘要
|
||||
export function showSummary(config) {
|
||||
console.clear();
|
||||
console.log(color.bgCyan(color.black(" KTX2 纹理压缩工具 ")));
|
||||
console.log("\n" + color.green("配置完成!当前设置:"));
|
||||
console.log(` 文件格式: ${config.exts.join(", ")}`);
|
||||
console.log(` 压缩程度: ${config.quality}`);
|
||||
console.log(` 编码格式: ${config.encoding}`);
|
||||
console.log(` Mipmap: ${config.mipmap}`);
|
||||
console.log(` 输出选项: ${config.outputOpts.join(", ")}\n`);
|
||||
}
|
||||
const ui = createStepUI({
|
||||
title: "KTX2 纹理压缩工具",
|
||||
getSteps: getFilteredSteps
|
||||
});
|
||||
|
||||
export const { runInteractive, showSummary } = ui;
|
||||
|
||||
12
lib/menu.js
12
lib/menu.js
@ -3,7 +3,6 @@ import boxen from "boxen";
|
||||
import figlet from "figlet";
|
||||
import { gridSelect } from "./grid.js";
|
||||
|
||||
// 古诗配置
|
||||
let poemConfig = {
|
||||
lines: ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"],
|
||||
perLine: 2,
|
||||
@ -12,16 +11,16 @@ let poemConfig = {
|
||||
borderColor: "cyan",
|
||||
};
|
||||
|
||||
// 标题配置
|
||||
let titleConfig = {
|
||||
text: "Zguiy Tool Box",
|
||||
font: "Standard",
|
||||
color: "magenta",
|
||||
};
|
||||
|
||||
// 工具列表
|
||||
const tools = [
|
||||
{ name: "格式转换", desc: "OBJ/FBX转glTF", module: "./lib/convert/index.js" },
|
||||
{ name: "KTX2 纹理压缩", desc: "图片转KTX2格式", module: "./lib/ktx2/index.js" },
|
||||
{ name: "glTF扩展", desc: "添加KHR_texture_basisu", module: "./lib/gltf/index.js" },
|
||||
{ name: "模型压缩", desc: "压缩glTF/GLB模型", module: "./lib/model/index.js" },
|
||||
{ name: "图片批量处理", desc: "裁剪/缩放/转换", module: "./lib/image/index.js" },
|
||||
{ name: "Sprite图集", desc: "合并精灵图集", module: "./lib/sprite/index.js" },
|
||||
@ -29,25 +28,21 @@ const tools = [
|
||||
{ name: "音频压缩", desc: "压缩音频文件", module: "./lib/audio/index.js" },
|
||||
];
|
||||
|
||||
// 设置古诗
|
||||
export function setPoem(lines, perLine = 2) {
|
||||
poemConfig.lines = lines;
|
||||
poemConfig.perLine = perLine;
|
||||
}
|
||||
|
||||
// 设置古诗框样式
|
||||
export function setPoemStyle(style) {
|
||||
Object.assign(poemConfig, style);
|
||||
}
|
||||
|
||||
// 设置标题
|
||||
export function setTitle(text, font = "Standard", titleColor = "magenta") {
|
||||
titleConfig.text = text;
|
||||
titleConfig.font = font;
|
||||
titleConfig.color = titleColor;
|
||||
}
|
||||
|
||||
// 渲染古诗
|
||||
function renderPoem() {
|
||||
const merged = [];
|
||||
for (let i = 0; i < poemConfig.lines.length; i += poemConfig.perLine) {
|
||||
@ -62,7 +57,6 @@ function renderPoem() {
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染标题
|
||||
function renderTitle() {
|
||||
const art = figlet.textSync(titleConfig.text, { font: titleConfig.font });
|
||||
const termWidth = process.stdout.columns || 80;
|
||||
@ -72,12 +66,10 @@ function renderTitle() {
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
// 渲染头部
|
||||
function renderHeader() {
|
||||
return renderPoem() + "\n\n" + renderTitle();
|
||||
}
|
||||
|
||||
// 主菜单
|
||||
export async function showMainMenu() {
|
||||
return gridSelect({
|
||||
items: tools,
|
||||
|
||||
71
lib/model/config.js
Normal file
71
lib/model/config.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { listAllModelFiles } from "../utils/gltf.js";
|
||||
|
||||
const transformOptions = [
|
||||
{ value: "dedup", label: "dedup(去重)", hint: "删除重复的访问器、材质、网格" },
|
||||
{ value: "prune", label: "prune(清理无用节点)", hint: "移除未被引用的节点、材质、动画" },
|
||||
{ value: "resample", label: "resample(动画重采样)", hint: "统一动画关键帧间隔,减少多余数据" },
|
||||
{ value: "weld", label: "weld(合并顶点)", hint: "合并共享位置的顶点以减少面数" },
|
||||
{ value: "quantize", label: "quantize(量化顶点数据)", hint: "降低顶点属性精度减小模型体积" }
|
||||
];
|
||||
|
||||
const quantizePresets = [
|
||||
{ value: "high", label: "高质量(位置16位,法线12位,UV14位)", hint: "尽量保证细节,适合高保真场景" },
|
||||
{ value: "balanced", label: "均衡(位置14位,法线10位,UV12位)", hint: "默认推荐,兼顾体积与质量" },
|
||||
{ value: "aggressive", label: "极限压缩(位置12位,法线8位,UV10位)", hint: "最小体积,但可能损失细节" },
|
||||
{ value: "light", label: "轻量(位置10位,法线8位,UV10位)", hint: "适合移动端、卡通等对精度不敏感场景" }
|
||||
];
|
||||
|
||||
const outputFormats = [
|
||||
{ value: "auto", label: "保持原格式", hint: "glTF/GLB保持原格式,OBJ/FBX转为GLB" },
|
||||
{ value: "glb", label: "统一导出为 GLB(二进制)", hint: "单文件发布更方便" },
|
||||
{ value: "gltf", label: "统一导出为 glTF(JSON)", hint: "可读性好,调试方便" }
|
||||
];
|
||||
|
||||
const outputOptions = [
|
||||
{ value: "overwrite", label: "覆盖原文件", hint: "直接把结果写回源文件" },
|
||||
{ value: "backup", label: "保留备份 (_备份)", hint: "覆盖前在同目录生成 _备份 副本" },
|
||||
{ value: "copy", label: "输出副本 (_compressed)", hint: "保留原文件不动,结果写入新文件" }
|
||||
];
|
||||
|
||||
export function getSteps() {
|
||||
const files = listAllModelFiles();
|
||||
const fileStep = {
|
||||
name: "模型选择",
|
||||
type: "multiselect",
|
||||
message: files.length ? "选择要处理的模型(支持 glTF/GLB/OBJ/FBX)" : "当前目录未找到模型文件",
|
||||
options: files.map(file => ({ value: file, label: file })),
|
||||
default: [...files]
|
||||
};
|
||||
|
||||
return [
|
||||
fileStep,
|
||||
{
|
||||
name: "压缩命令",
|
||||
type: "multiselect",
|
||||
message: "请选择要执行的 gltf-transform 操作",
|
||||
options: transformOptions,
|
||||
default: ["dedup", "prune", "weld", "quantize"]
|
||||
},
|
||||
{
|
||||
name: "量化级别",
|
||||
type: "select",
|
||||
message: "量化可显著减小体积(如未启用 quantize 可直接 Tab)",
|
||||
options: quantizePresets,
|
||||
default: "balanced"
|
||||
},
|
||||
{
|
||||
name: "输出格式",
|
||||
type: "select",
|
||||
message: "选择最终模型格式",
|
||||
options: outputFormats,
|
||||
default: "auto"
|
||||
},
|
||||
{
|
||||
name: "输出选项",
|
||||
type: "multiselect",
|
||||
message: "请选择输出行为",
|
||||
options: outputOptions,
|
||||
default: ["overwrite", "backup"]
|
||||
}
|
||||
];
|
||||
}
|
||||
195
lib/model/index.js
Normal file
195
lib/model/index.js
Normal file
@ -0,0 +1,195 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import color from "picocolors";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { dedup, prune, resample, weld, quantize } from "@gltf-transform/functions";
|
||||
import { BACKUP_SUFFIX, needsConversion } from "../utils/gltf.js";
|
||||
import { convert } from "../convert/converters.js";
|
||||
import { runInteractive, showSummary } from "./ui.js";
|
||||
import { stopKeypress, waitForKey } from "../keyboard.js";
|
||||
|
||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
|
||||
const QUANTIZE_PRESETS = {
|
||||
high: { position: 16, normal: 12, texcoord: 14, color: 10, generic: 12 },
|
||||
balanced: { position: 14, normal: 10, texcoord: 12, color: 8, generic: 12 },
|
||||
aggressive: { position: 12, normal: 8, texcoord: 10, color: 8, generic: 10 },
|
||||
light: { position: 10, normal: 8, texcoord: 10, color: 8, generic: 10 }
|
||||
};
|
||||
|
||||
function buildTransforms(config) {
|
||||
const transforms = [];
|
||||
const selected = new Set(config.commands);
|
||||
if (selected.has("dedup")) transforms.push(dedup());
|
||||
if (selected.has("prune")) transforms.push(prune());
|
||||
if (selected.has("resample")) transforms.push(resample());
|
||||
if (selected.has("weld")) transforms.push(weld());
|
||||
if (selected.has("quantize")) {
|
||||
const preset = QUANTIZE_PRESETS[config.quantizePreset] || QUANTIZE_PRESETS.balanced;
|
||||
transforms.push(quantize({
|
||||
quantizePosition: preset.position,
|
||||
quantizeNormal: preset.normal,
|
||||
quantizeTexcoord: preset.texcoord,
|
||||
quantizeColor: preset.color,
|
||||
quantizeGeneric: preset.generic
|
||||
}));
|
||||
}
|
||||
return transforms;
|
||||
}
|
||||
|
||||
function resolveOutput(file, config) {
|
||||
const cwd = process.cwd();
|
||||
const originalExt = path.extname(file).toLowerCase() || ".gltf";
|
||||
const baseName = originalExt ? file.slice(0, -originalExt.length) : file;
|
||||
const isConvertible = needsConversion(file);
|
||||
|
||||
// OBJ/FBX 默认输出 GLB,除非指定了 gltf
|
||||
const targetExt = config.outputFormat === "gltf" ? ".gltf"
|
||||
: (config.outputFormat === "glb" || isConvertible) ? ".glb"
|
||||
: originalExt;
|
||||
|
||||
const wantsCopy = config.outputOptions.includes("copy");
|
||||
const wantsOverwrite = config.outputOptions.includes("overwrite");
|
||||
const mode = wantsCopy ? "copy" : (wantsOverwrite ? "overwrite" : "copy");
|
||||
const backup = config.outputOptions.includes("backup") && mode === "overwrite";
|
||||
|
||||
const targetName = mode === "copy"
|
||||
? `${baseName}_compressed${targetExt}`
|
||||
: `${baseName}${targetExt}`;
|
||||
|
||||
return {
|
||||
cwd,
|
||||
mode,
|
||||
backup,
|
||||
sourcePath: path.join(cwd, file),
|
||||
sourceExt: originalExt,
|
||||
targetExt,
|
||||
targetPath: path.join(cwd, targetName),
|
||||
baseName,
|
||||
needsConversion: isConvertible
|
||||
};
|
||||
}
|
||||
|
||||
async function processFile(file, config, transforms) {
|
||||
const output = resolveOutput(file, config);
|
||||
if (!fs.existsSync(output.sourcePath)) {
|
||||
console.log(color.yellow("跳过,文件不存在: " + file));
|
||||
return { ok: false, file, reason: "missing" };
|
||||
}
|
||||
|
||||
if (!transforms.length) {
|
||||
console.log(color.yellow("未选择任何 gltf-transform 操作,已中止"));
|
||||
return { ok: false, file, reason: "no-transform" };
|
||||
}
|
||||
|
||||
if (output.backup) {
|
||||
const backupName = `${output.baseName}${BACKUP_SUFFIX}${output.sourceExt}`;
|
||||
const backupPath = path.join(output.cwd, backupName);
|
||||
fs.copyFileSync(output.sourcePath, backupPath);
|
||||
}
|
||||
|
||||
let inputPath = output.sourcePath;
|
||||
|
||||
// 如果是 OBJ/FBX,先转换为临时 GLB
|
||||
if (output.needsConversion) {
|
||||
const tempPath = path.join(output.cwd, `${output.baseName}_temp.glb`);
|
||||
console.log(color.dim("转换中: " + file + " → GLB"));
|
||||
await convert(output.sourcePath, tempPath, { binary: true });
|
||||
inputPath = tempPath;
|
||||
}
|
||||
|
||||
const document = io.read(inputPath);
|
||||
await document.transform(...transforms);
|
||||
io.write(output.targetPath, document);
|
||||
|
||||
// 清理临时文件
|
||||
if (output.needsConversion) {
|
||||
fs.rmSync(inputPath, { force: true });
|
||||
}
|
||||
|
||||
if (output.mode === "overwrite" && output.targetPath !== output.sourcePath) {
|
||||
fs.rmSync(output.sourcePath, { force: true });
|
||||
}
|
||||
|
||||
if (output.mode === "overwrite" && output.targetPath !== output.sourcePath) {
|
||||
console.log(color.dim("已生成新文件: " + path.basename(output.targetPath)));
|
||||
}
|
||||
|
||||
return { ok: true, file, output: output.targetPath };
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
const result = await runInteractive();
|
||||
if (!result) return "back";
|
||||
|
||||
stopKeypress();
|
||||
|
||||
const { results } = result;
|
||||
const config = {
|
||||
files: results[0] || [],
|
||||
commands: results[1] || [],
|
||||
quantizePreset: results[2] || "balanced",
|
||||
outputFormat: results[3] || "auto",
|
||||
outputOptions: results[4] || []
|
||||
};
|
||||
|
||||
showSummary([
|
||||
"模型文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||
"gltf-transform 命令: " + (config.commands.length ? config.commands.join(", ") : "无"),
|
||||
"量化级别: " + config.quantizePreset,
|
||||
"输出格式: " + config.outputFormat,
|
||||
"输出选项: " + (config.outputOptions.length ? config.outputOptions.join(", ") : "默认")
|
||||
]);
|
||||
|
||||
if (!config.files.length) {
|
||||
console.log(color.yellow("未选择任何模型文件"));
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
|
||||
const transforms = buildTransforms(config);
|
||||
if (!transforms.length) {
|
||||
console.log(color.yellow("未配置任何压缩命令,请至少选择一项 gltf-transform 操作"));
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
|
||||
const total = config.files.length;
|
||||
let success = 0;
|
||||
const failed = [];
|
||||
|
||||
console.log(color.cyan(`开始压缩 ${total} 个文件...\n`));
|
||||
|
||||
for (let i = 0; i < config.files.length; i++) {
|
||||
const file = config.files[i];
|
||||
const progress = `[${i + 1}/${total}]`;
|
||||
console.log(color.dim(`${progress} 处理中: ${file}`));
|
||||
|
||||
try {
|
||||
const result = await processFile(file, config, transforms);
|
||||
if (result.ok) {
|
||||
success++;
|
||||
console.log(color.green(`${progress} ✓ ${file} → ${path.basename(result.output)}`));
|
||||
} else {
|
||||
failed.push(file);
|
||||
console.log(color.yellow(`${progress} ⊘ 跳过: ${file}`));
|
||||
}
|
||||
} catch (err) {
|
||||
failed.push(file);
|
||||
console.log(color.red(`${progress} ✖ 失败: ${file}`));
|
||||
console.log(color.dim(" " + String(err?.message || err)));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n" + color.bgGreen(color.black(" 压缩完成 ")));
|
||||
if (success) {
|
||||
console.log(color.green(`成功: ${success} 个`));
|
||||
}
|
||||
if (failed.length) {
|
||||
console.log(color.yellow(`失败: ${failed.length} 个 (${failed.join(", ")})`));
|
||||
}
|
||||
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
9
lib/model/ui.js
Normal file
9
lib/model/ui.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { createStepUI } from "../utils/stepui.js";
|
||||
import { getSteps } from "./config.js";
|
||||
|
||||
const ui = createStepUI({
|
||||
title: "模型压缩工具",
|
||||
getSteps
|
||||
});
|
||||
|
||||
export const { runInteractive, showSummary } = ui;
|
||||
@ -28,4 +28,4 @@ export function showPoem(lines, perLine = 2) {
|
||||
}
|
||||
|
||||
// 默认古诗
|
||||
export const defaultPoem = ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"];
|
||||
export const defaultPoem = ["你我皆牛马1", "生在人世间", "终日奔波苦", "一刻不得闲"];
|
||||
|
||||
35
lib/stats.js
Normal file
35
lib/stats.js
Normal file
@ -0,0 +1,35 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const STATS_FILE = path.join(__dirname, "../.stats.json");
|
||||
|
||||
let data = null;
|
||||
|
||||
function load() {
|
||||
if (data) return data;
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(STATS_FILE, "utf8"));
|
||||
} catch {
|
||||
data = {};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function save() {
|
||||
fs.writeFileSync(STATS_FILE, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function record(category, key) {
|
||||
load();
|
||||
if (!data[category]) data[category] = {};
|
||||
data[category][key] = (data[category][key] || 0) + 1;
|
||||
save();
|
||||
}
|
||||
|
||||
export function sortByUsage(category, items, getKey = v => v) {
|
||||
load();
|
||||
const counts = data[category] || {};
|
||||
return [...items].sort((a, b) => (counts[getKey(b)] || 0) - (counts[getKey(a)] || 0));
|
||||
}
|
||||
82
lib/utils/gltf.js
Normal file
82
lib/utils/gltf.js
Normal file
@ -0,0 +1,82 @@
|
||||
import fs from "fs";
|
||||
|
||||
export const BACKUP_SUFFIX = "_备份";
|
||||
|
||||
// 检查当前目录是否有 gltf 文件
|
||||
export function hasGltfFile() {
|
||||
return listGltfFiles().length > 0;
|
||||
}
|
||||
|
||||
// 获取当前目录下可用的 gltf 文件列表
|
||||
export function listGltfFiles() {
|
||||
const cwd = process.cwd();
|
||||
return fs.readdirSync(cwd).filter(f => f.toLowerCase().endsWith(".gltf") && !f.includes(BACKUP_SUFFIX));
|
||||
}
|
||||
|
||||
// 获取 glTF / GLB 模型文件
|
||||
export function listModelFiles() {
|
||||
const cwd = process.cwd();
|
||||
return fs
|
||||
.readdirSync(cwd)
|
||||
.filter(file => /\.(gltf|glb)$/i.test(file) && !file.includes(BACKUP_SUFFIX));
|
||||
}
|
||||
|
||||
// 获取所有支持的模型文件(包括需要转换的格式)
|
||||
export function listAllModelFiles() {
|
||||
const cwd = process.cwd();
|
||||
return fs
|
||||
.readdirSync(cwd)
|
||||
.filter(file => /\.(gltf|glb|obj|fbx)$/i.test(file) && !file.includes(BACKUP_SUFFIX));
|
||||
}
|
||||
|
||||
// 判断是否需要先转换
|
||||
export function needsConversion(file) {
|
||||
return /\.(obj|fbx)$/i.test(file);
|
||||
}
|
||||
|
||||
// 检查是否满足执行条件(有 ktx2、gltf、bin 文件)
|
||||
export function checkRequiredFiles() {
|
||||
const cwd = process.cwd();
|
||||
const files = fs.readdirSync(cwd);
|
||||
const hasKtx2 = files.some(f => f.toLowerCase().endsWith(".ktx2"));
|
||||
const hasGltf = files.some(f => f.toLowerCase().endsWith(".gltf"));
|
||||
const hasBin = files.some(f => f.toLowerCase().endsWith(".bin"));
|
||||
|
||||
const missing = [];
|
||||
if (!hasKtx2) missing.push("ktx2");
|
||||
if (!hasGltf) missing.push("gltf");
|
||||
if (!hasBin) missing.push("bin");
|
||||
|
||||
return { ok: missing.length === 0, missing };
|
||||
}
|
||||
|
||||
// 修改 gltf 文件,根据选项添加扩展
|
||||
export function modifyGltfContent(gltfPath, options = ["textureBasisu"]) {
|
||||
const content = fs.readFileSync(gltfPath, "utf-8");
|
||||
const gltf = JSON.parse(content);
|
||||
|
||||
if (options.includes("textureBasisu")) {
|
||||
if (!gltf.extensionsUsed) gltf.extensionsUsed = [];
|
||||
if (!gltf.extensionsUsed.includes("KHR_texture_basisu")) {
|
||||
gltf.extensionsUsed.push("KHR_texture_basisu");
|
||||
}
|
||||
|
||||
if (gltf.textures) {
|
||||
gltf.textures = gltf.textures.map(tex => ({
|
||||
extensions: { KHR_texture_basisu: { source: tex.source } }
|
||||
}));
|
||||
}
|
||||
|
||||
if (gltf.images) {
|
||||
gltf.images = gltf.images.map(img => {
|
||||
const uri = img.uri || "";
|
||||
const newUri = uri.replace(/\.(png|jpg|jpeg|webp|tga)$/i, ".ktx2");
|
||||
return { uri: newUri, mimeType: "image/ktx2" };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// placeholder1, placeholder2, placeholder3 预留扩展点
|
||||
|
||||
return gltf;
|
||||
}
|
||||
124
lib/utils/stepui.js
Normal file
124
lib/utils/stepui.js
Normal file
@ -0,0 +1,124 @@
|
||||
import color from "picocolors";
|
||||
import { initKeypress, onKey } from "../keyboard.js";
|
||||
|
||||
export function createStepUI(options) {
|
||||
const { title, getSteps } = options;
|
||||
|
||||
let steps = [];
|
||||
let results = [];
|
||||
let completed = new Set();
|
||||
let currentStep = 0;
|
||||
let currentOption = 0;
|
||||
|
||||
function initResults() {
|
||||
steps = typeof getSteps === "function" ? getSteps() : getSteps;
|
||||
results = steps.map(s => s.type === "multiselect" ? [...(s.default || [])] : s.default);
|
||||
completed = new Set();
|
||||
currentStep = 0;
|
||||
currentOption = 0;
|
||||
}
|
||||
|
||||
function renderNav() {
|
||||
const nav = steps.map((step, i) => {
|
||||
if (completed.has(i)) return color.green("☑ " + step.name);
|
||||
if (i === currentStep) return color.bgCyan(color.black(" " + step.name + " "));
|
||||
return color.dim("□ " + step.name);
|
||||
});
|
||||
return "← " + nav.join(" ") + " " + color.green("✓Submit") + " →";
|
||||
}
|
||||
|
||||
function renderOptions() {
|
||||
const step = steps[currentStep];
|
||||
const lines = [color.cyan(step.message), ""];
|
||||
if (!step.options || !step.options.length) {
|
||||
lines.push(color.dim(step.emptyMessage || "无可用选项"));
|
||||
return lines.join("\n");
|
||||
}
|
||||
step.options.forEach((opt, i) => {
|
||||
const isCurrent = i === currentOption;
|
||||
const isSelected = step.type === "multiselect"
|
||||
? results[currentStep]?.includes(opt.value)
|
||||
: results[currentStep] === opt.value;
|
||||
const prefix = step.type === "multiselect"
|
||||
? (isSelected ? color.green("◉ ") : "○ ")
|
||||
: (isSelected ? color.green("● ") : "○ ");
|
||||
const cursor = isCurrent ? color.cyan("❯ ") : " ";
|
||||
const label = isCurrent ? color.cyan(opt.label) : opt.label;
|
||||
const check = isSelected ? color.green(" ✓") : "";
|
||||
lines.push(cursor + prefix + label + check);
|
||||
if (opt.hint) lines.push(" " + color.dim(opt.hint));
|
||||
});
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function render() {
|
||||
console.clear();
|
||||
console.log(color.bgCyan(color.black(` ${title} `)));
|
||||
console.log("\n" + renderNav());
|
||||
console.log(color.dim("\n← → 切换步骤 | ↑ ↓ 选择 | Space 选中 | Tab 提交 | Esc 返回\n"));
|
||||
console.log(renderOptions());
|
||||
}
|
||||
|
||||
function runInteractive() {
|
||||
initResults();
|
||||
initKeypress();
|
||||
return new Promise(resolve => {
|
||||
render();
|
||||
onKey((str, key) => {
|
||||
if (!key) return;
|
||||
const step = steps[currentStep];
|
||||
const optCount = step.options?.length || 0;
|
||||
if (key.name === "left") {
|
||||
if (currentStep > 0) { currentStep--; currentOption = 0; }
|
||||
render();
|
||||
} else if (key.name === "right") {
|
||||
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
|
||||
render();
|
||||
} else if (key.name === "up") {
|
||||
if (!optCount) return;
|
||||
currentOption = (currentOption - 1 + optCount) % optCount;
|
||||
render();
|
||||
} else if (key.name === "down") {
|
||||
if (!optCount) return;
|
||||
currentOption = (currentOption + 1) % optCount;
|
||||
render();
|
||||
} else if (key.name === "space") {
|
||||
if (!optCount) return;
|
||||
const opt = step.options[currentOption];
|
||||
if (step.type === "multiselect") {
|
||||
const list = results[currentStep];
|
||||
const idx = list.indexOf(opt.value);
|
||||
if (idx >= 0) list.splice(idx, 1);
|
||||
else list.push(opt.value);
|
||||
} else {
|
||||
results[currentStep] = opt.value;
|
||||
}
|
||||
completed.add(currentStep);
|
||||
render();
|
||||
} else if (key.name === "return") {
|
||||
if (!optCount) return;
|
||||
if (step.type === "select") {
|
||||
results[currentStep] = step.options[currentOption].value;
|
||||
}
|
||||
completed.add(currentStep);
|
||||
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
|
||||
render();
|
||||
} else if (key.name === "tab") {
|
||||
resolve({ steps, results });
|
||||
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showSummary(lines) {
|
||||
console.clear();
|
||||
console.log(color.bgCyan(color.black(` ${title} `)));
|
||||
console.log("\n" + color.green("配置完成!当前设置:"));
|
||||
lines.forEach(line => console.log(" " + line));
|
||||
console.log();
|
||||
}
|
||||
|
||||
return { runInteractive, showSummary, initResults };
|
||||
}
|
||||
3062
package-lock.json
generated
3062
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@ -1,16 +1,19 @@
|
||||
{
|
||||
"name": "@yinshuangxi/yinx-cli",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"description": "游戏资源工具箱 - KTX2纹理压缩、模型压缩等",
|
||||
"main": "index.js",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "node scripts/build.js",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"bin": {
|
||||
"yinx": "./index.js"
|
||||
"yinx": "./dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"lib",
|
||||
"bin"
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"ktx2",
|
||||
@ -30,8 +33,17 @@
|
||||
"node": ">=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cocos/fbx2gltf": "^1.0.8",
|
||||
"@gltf-transform/core": "^3.10.1",
|
||||
"@gltf-transform/extensions": "^3.10.1",
|
||||
"@gltf-transform/functions": "^3.10.1",
|
||||
"boxen": "^8.0.1",
|
||||
"figlet": "^1.9.4",
|
||||
"obj2gltf": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"javascript-obfuscator": "^5.1.0",
|
||||
"rimraf": "^6.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
100
scripts/build.js
Normal file
100
scripts/build.js
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import JavaScriptObfuscator from "javascript-obfuscator";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const distDir = path.join(rootDir, "dist");
|
||||
|
||||
const copyTargets = [
|
||||
{ from: path.join(rootDir, "index.js"), to: path.join(distDir, "index.js") },
|
||||
{ from: path.join(rootDir, "lib"), to: path.join(distDir, "lib") },
|
||||
{ from: path.join(rootDir, "bin"), to: path.join(distDir, "bin") }
|
||||
];
|
||||
|
||||
async function main() {
|
||||
await fs.rm(distDir, { recursive: true, force: true });
|
||||
await fs.mkdir(distDir, { recursive: true });
|
||||
|
||||
for (const target of copyTargets) {
|
||||
await copy(target.from, target.to);
|
||||
}
|
||||
|
||||
await obfuscateDirectory(distDir);
|
||||
console.log("Obfuscated build written to", distDir);
|
||||
}
|
||||
|
||||
async function copy(src, dest) {
|
||||
const stat = await fs.stat(src);
|
||||
if (stat.isDirectory()) {
|
||||
await fs.mkdir(dest, { recursive: true });
|
||||
const entries = await fs.readdir(src);
|
||||
for (const entry of entries) {
|
||||
await copy(path.join(src, entry), path.join(dest, entry));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.copyFile(src, dest);
|
||||
}
|
||||
|
||||
async function obfuscateDirectory(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
await Promise.all(
|
||||
entries.map(async entry => {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await obfuscateDirectory(entryPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith(".js")) {
|
||||
await obfuscateFile(entryPath);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function obfuscateFile(filePath) {
|
||||
const original = await fs.readFile(filePath, "utf8");
|
||||
const { shebang, body } = extractShebang(original);
|
||||
const obfuscated = JavaScriptObfuscator.obfuscate(body, {
|
||||
compact: true,
|
||||
controlFlowFlattening: true,
|
||||
stringArray: true,
|
||||
stringArrayEncoding: ["base64"],
|
||||
deadCodeInjection: true,
|
||||
target: "node",
|
||||
identifierNamesGenerator: "hexadecimal"
|
||||
});
|
||||
|
||||
const content = shebang
|
||||
? `${shebang}\n${obfuscated.getObfuscatedCode()}`
|
||||
: obfuscated.getObfuscatedCode();
|
||||
await fs.writeFile(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
function extractShebang(source) {
|
||||
if (!source.startsWith("#!")) {
|
||||
return { body: source };
|
||||
}
|
||||
|
||||
const newlineIndex = source.indexOf("\n");
|
||||
if (newlineIndex === -1) {
|
||||
return { shebang: source, body: "" };
|
||||
}
|
||||
|
||||
return {
|
||||
shebang: source.slice(0, newlineIndex),
|
||||
body: source.slice(newlineIndex + 1)
|
||||
};
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error("Build failed:");
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user