增加转格式
This commit is contained in:
@ -8,7 +8,11 @@
|
|||||||
"Bash(timeout 2 node:*)",
|
"Bash(timeout 2 node:*)",
|
||||||
"Bash(npm ls:*)",
|
"Bash(npm ls:*)",
|
||||||
"Bash(timeout 1 node:*)",
|
"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 color from "picocolors";
|
||||||
import { showMainMenu } from "./lib/menu.js";
|
import { showMainMenu } from "./lib/menu.js";
|
||||||
|
|
||||||
// 主循环
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const selected = await showMainMenu();
|
const selected = await showMainMenu();
|
||||||
|
|
||||||
console.clear();
|
console.clear();
|
||||||
console.log(color.cyan(`\n正在启动: ${selected.name}...\n`));
|
console.log(color.cyan("\n正在启动: " + selected.name + "...\n"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tool = await import(selected.module);
|
const tool = await import(selected.module);
|
||||||
const result = await tool.run();
|
const result = await tool.run();
|
||||||
|
|
||||||
// 返回主菜单
|
|
||||||
if (result === "back") continue;
|
if (result === "back") continue;
|
||||||
|
|
||||||
// 工具完成后退出
|
|
||||||
break;
|
break;
|
||||||
} catch (err) {
|
} 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(err.message));
|
||||||
console.log(color.dim("\n按任意键返回主菜单..."));
|
console.log(color.dim("\n按任意键返回主菜单..."));
|
||||||
|
|
||||||
// 等待按键
|
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
process.stdin.setRawMode(true);
|
process.stdin.setRawMode(true);
|
||||||
process.stdin.once("data", () => {
|
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 color from "picocolors";
|
||||||
import { initKeypress, onKey } from "./keyboard.js";
|
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) {
|
function strWidth(str) {
|
||||||
let width = 0;
|
let width = 0;
|
||||||
@ -35,7 +45,7 @@ export async function gridSelect(options) {
|
|||||||
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
console.clear();
|
clearScreen();
|
||||||
|
|
||||||
if (renderHeader) {
|
if (renderHeader) {
|
||||||
console.log(renderHeader());
|
console.log(renderHeader());
|
||||||
@ -110,7 +120,7 @@ export async function gridSelect(options) {
|
|||||||
setImmediate(() => resolve(items[current]));
|
setImmediate(() => resolve(items[current]));
|
||||||
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
||||||
process.stdin.setRawMode(false);
|
process.stdin.setRawMode(false);
|
||||||
console.clear();
|
clearScreen();
|
||||||
console.log(color.yellow("👋 再见!"));
|
console.log(color.yellow("👋 再见!"));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,3 +16,22 @@ export function initKeypress() {
|
|||||||
export function onKey(handler) {
|
export function onKey(handler) {
|
||||||
process.stdin.on("keypress", 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: "overwrite", label: "覆盖已存在文件(自动替换同名文件)" },
|
||||||
{ value: "keepOriginal", label: "保留原文件(压缩后不删除源文件)" },
|
{ value: "keepOriginal", label: "保留原文件(压缩后不删除源文件)" },
|
||||||
{ value: "report", label: "生成压缩报告(输出详细的压缩统计信息)" },
|
{ value: "report", label: "生成压缩报告(输出详细的压缩统计信息)" },
|
||||||
{ value: "silent", label: "静默模式(减少控制台输出信息)" }
|
{ value: "silent", label: "静默模式(减少控制台输出信息)" },
|
||||||
|
{ value: "gltfExtension", label: "修改glTF扩展(添加KHR_texture_basisu)", dynamic: true }
|
||||||
],
|
],
|
||||||
default: ["overwrite", "keepOriginal"]
|
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 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 { stopKeypress, waitForKey } from "../keyboard.js";
|
||||||
|
|
||||||
export async function run() {
|
export async function run() {
|
||||||
// 检查 toktx
|
|
||||||
checkToktx();
|
checkToktx();
|
||||||
|
|
||||||
// 运行交互界面
|
|
||||||
const result = await runInteractive();
|
const result = await runInteractive();
|
||||||
|
|
||||||
// ESC 返回主菜单
|
|
||||||
if (!result) return "back";
|
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 };
|
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);
|
const { images, cwd } = scanImages(exts);
|
||||||
|
|
||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
console.log(color.yellow("当前目录没有匹配的图片"));
|
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);
|
const { total, failed } = await compressAll(images, config, cwd);
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
console.log(color.yellow(`\n⚠️ 完成,但有 ${failed} 个文件失败`));
|
console.log(color.yellow("\n⚠️ 完成,但有 " + failed + " 个文件失败"));
|
||||||
} else {
|
} else {
|
||||||
console.log(color.green("\n🎉 全部文件压缩完成!"));
|
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 { steps } from "./config.js";
|
||||||
import { initKeypress, onKey } from "../keyboard.js";
|
import { hasGltfFile } from "./gltf.js";
|
||||||
|
|
||||||
// 存储结果和状态
|
function getFilteredSteps() {
|
||||||
let results = [];
|
const hasGltf = hasGltfFile();
|
||||||
let completed = new Set();
|
return steps.map(step => {
|
||||||
let currentStep = 0;
|
if (step.name === "输出选项") {
|
||||||
let currentOption = 0;
|
return { ...step, options: step.options.filter(opt => !opt.dynamic || hasGltf) };
|
||||||
|
|
||||||
// 初始化结果
|
|
||||||
export function initResults() {
|
|
||||||
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}`);
|
|
||||||
} else if (i === currentStep) {
|
|
||||||
return color.bgCyan(color.black(` ${step.name} `));
|
|
||||||
} else {
|
|
||||||
return color.dim(`□ ${step.name}`);
|
|
||||||
}
|
}
|
||||||
});
|
return step;
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示配置摘要
|
const ui = createStepUI({
|
||||||
export function showSummary(config) {
|
title: "KTX2 纹理压缩工具",
|
||||||
console.clear();
|
getSteps: getFilteredSteps
|
||||||
console.log(color.bgCyan(color.black(" KTX2 纹理压缩工具 ")));
|
});
|
||||||
console.log("\n" + color.green("配置完成!当前设置:"));
|
|
||||||
console.log(` 文件格式: ${config.exts.join(", ")}`);
|
export const { runInteractive, showSummary } = ui;
|
||||||
console.log(` 压缩程度: ${config.quality}`);
|
|
||||||
console.log(` 编码格式: ${config.encoding}`);
|
|
||||||
console.log(` Mipmap: ${config.mipmap}`);
|
|
||||||
console.log(` 输出选项: ${config.outputOpts.join(", ")}\n`);
|
|
||||||
}
|
|
||||||
|
|||||||
12
lib/menu.js
12
lib/menu.js
@ -3,7 +3,6 @@ import boxen from "boxen";
|
|||||||
import figlet from "figlet";
|
import figlet from "figlet";
|
||||||
import { gridSelect } from "./grid.js";
|
import { gridSelect } from "./grid.js";
|
||||||
|
|
||||||
// 古诗配置
|
|
||||||
let poemConfig = {
|
let poemConfig = {
|
||||||
lines: ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"],
|
lines: ["你我皆牛马", "生在人世间", "终日奔波苦", "一刻不得闲"],
|
||||||
perLine: 2,
|
perLine: 2,
|
||||||
@ -12,16 +11,16 @@ let poemConfig = {
|
|||||||
borderColor: "cyan",
|
borderColor: "cyan",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标题配置
|
|
||||||
let titleConfig = {
|
let titleConfig = {
|
||||||
text: "Zguiy Tool Box",
|
text: "Zguiy Tool Box",
|
||||||
font: "Standard",
|
font: "Standard",
|
||||||
color: "magenta",
|
color: "magenta",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 工具列表
|
|
||||||
const tools = [
|
const tools = [
|
||||||
|
{ name: "格式转换", desc: "OBJ/FBX转glTF", module: "./lib/convert/index.js" },
|
||||||
{ name: "KTX2 纹理压缩", desc: "图片转KTX2格式", module: "./lib/ktx2/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: "压缩glTF/GLB模型", module: "./lib/model/index.js" },
|
||||||
{ name: "图片批量处理", desc: "裁剪/缩放/转换", module: "./lib/image/index.js" },
|
{ name: "图片批量处理", desc: "裁剪/缩放/转换", module: "./lib/image/index.js" },
|
||||||
{ name: "Sprite图集", desc: "合并精灵图集", module: "./lib/sprite/index.js" },
|
{ name: "Sprite图集", desc: "合并精灵图集", module: "./lib/sprite/index.js" },
|
||||||
@ -29,25 +28,21 @@ const tools = [
|
|||||||
{ name: "音频压缩", desc: "压缩音频文件", module: "./lib/audio/index.js" },
|
{ name: "音频压缩", desc: "压缩音频文件", module: "./lib/audio/index.js" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 设置古诗
|
|
||||||
export function setPoem(lines, perLine = 2) {
|
export function setPoem(lines, perLine = 2) {
|
||||||
poemConfig.lines = lines;
|
poemConfig.lines = lines;
|
||||||
poemConfig.perLine = perLine;
|
poemConfig.perLine = perLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置古诗框样式
|
|
||||||
export function setPoemStyle(style) {
|
export function setPoemStyle(style) {
|
||||||
Object.assign(poemConfig, style);
|
Object.assign(poemConfig, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置标题
|
|
||||||
export function setTitle(text, font = "Standard", titleColor = "magenta") {
|
export function setTitle(text, font = "Standard", titleColor = "magenta") {
|
||||||
titleConfig.text = text;
|
titleConfig.text = text;
|
||||||
titleConfig.font = font;
|
titleConfig.font = font;
|
||||||
titleConfig.color = titleColor;
|
titleConfig.color = titleColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染古诗
|
|
||||||
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) {
|
||||||
@ -62,7 +57,6 @@ function renderPoem() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染标题
|
|
||||||
function renderTitle() {
|
function renderTitle() {
|
||||||
const art = figlet.textSync(titleConfig.text, { font: titleConfig.font });
|
const art = figlet.textSync(titleConfig.text, { font: titleConfig.font });
|
||||||
const termWidth = process.stdout.columns || 80;
|
const termWidth = process.stdout.columns || 80;
|
||||||
@ -72,12 +66,10 @@ function renderTitle() {
|
|||||||
}).join("\n");
|
}).join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染头部
|
|
||||||
function renderHeader() {
|
function renderHeader() {
|
||||||
return renderPoem() + "\n\n" + renderTitle();
|
return renderPoem() + "\n\n" + renderTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主菜单
|
|
||||||
export async function showMainMenu() {
|
export async function showMainMenu() {
|
||||||
return gridSelect({
|
return gridSelect({
|
||||||
items: tools,
|
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",
|
"name": "@yinshuangxi/yinx-cli",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"description": "游戏资源工具箱 - KTX2纹理压缩、模型压缩等",
|
"description": "游戏资源工具箱 - KTX2纹理压缩、模型压缩等",
|
||||||
"main": "index.js",
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"build": "node scripts/build.js",
|
||||||
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"yinx": "./index.js"
|
"yinx": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"dist"
|
||||||
"lib",
|
|
||||||
"bin"
|
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ktx2",
|
"ktx2",
|
||||||
@ -30,8 +33,17 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"boxen": "^8.0.1",
|
||||||
"figlet": "^1.9.4",
|
"figlet": "^1.9.4",
|
||||||
|
"obj2gltf": "^3.2.0",
|
||||||
"picocolors": "^1.1.1"
|
"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