1#!/usr/bin/env escript 2%% -*- erlang -*- 3%% Wings 3D File conversion. 4%% 5%% Copyright (c) 2010 Dan Gudmundsson 6%% 7%% See the file "license.terms" for information on usage and redistribution 8%% of this file, and for a DISCLAIMER OF ALL WARRANTIES. 9%% 10 11-mode(compile). 12 13%% If moved outside of wings directory modify 14-define(WINGS_DIR, "c:/src/wings/ebin"). 15 16-record(opts, 17 {dir = ".", %% Output to directory 18 out_module, %% Output format 19 verbose=false, %% Verbose output 20 in_format, %% In format (if unknown extension). 21 image_format, %% Image out format 22 in_formats=[], %% Scanned, all import formats 23 out_formats=[], %% Scanned, all export formats 24 modify=[] %% Conversion modifications 25 }). 26 27-record(format, 28 {mod, %% Module 29 ext_type, %% Extension 30 str="", %% Description string 31 option=false %% Allows options 32 }). 33 34%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 35 36main(Args) -> 37 Wings_dir = setup_paths(), 38 ok = wings_start:start(script_usage), 39 IEDir = filename:join(Wings_dir, "plugins/import_export"), 40 code:add_patha(IEDir), 41 Opts0 = scan_format([IEDir]), 42 put(verbose, false), 43 case parse_args(Args, Opts0) of 44 {#opts{out_module=undefined},_} -> 45 io:format("**** Error: Out format not specified~n~n"), 46 usage(Opts0); 47 {Opts = #opts{}, Files} -> 48 convert(Files, Opts), 49 quit(0); 50 error -> 51 usage(Opts0) 52 end. 53 54convert(Fs, Opts) -> 55 Convert = fun(File) -> 56 call_wings({file, confirmed_new}), 57 ok = import_file(File, Opts), 58 ok = do_mods(Opts), 59 Out = filename:rootname(filename:basename(File)), 60 export_file(Out, Opts) 61 end, 62 [Convert(File) || File <- Fs], 63 ok. 64 65%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 66 67scan_format(Dir) -> 68 Files = filelib:wildcard(filename:join(Dir, "wpc_*.beam")), 69 Plugin = fun(File, {Type, Acc}) -> 70 Mod = list_to_atom(filename:rootname(filename:basename(File))), 71 case Mod:menu({file, Type}, []) of 72 [{Str, ExtT, Extra}] -> 73 F = #format{mod=Mod, ext_type=ExtT, str = strip(Str), 74 option = lists:member(option, Extra) 75 }, 76 {Type,[F|Acc]}; 77 [{Str, ExtT}] -> 78 F = #format{mod=Mod, ext_type=ExtT, str = strip(Str)}, 79 {Type,[F|Acc]}; 80 [] -> 81 {Type, Acc} 82 end 83 end, 84 Default = [#format{mod=nendo, ext_type=ndo, str="Nendo (.ndo)"}, 85 #format{mod=wings, ext_type=wings, str="Wings (.wings)"}], 86 {_,Export} = lists:foldl(Plugin, {export, Default}, Files), 87 {_,Import} = lists:foldl(Plugin, {import, Default}, Files), 88 89 #opts{in_formats=Import, out_formats=Export}. 90 91strip(Str) -> 92 strip_1(lists:reverse(Str)). 93 94strip_1([$.|Rest]) -> 95 strip_1(Rest); 96strip_1(Str) -> 97 lists:reverse(Str). 98 99%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 100 101parse_args(["-o", Dir|Rest], Opts) -> 102 parse_args(Rest, Opts#opts{dir=Dir}); 103parse_args(["--outdir", Dir|Rest], Opts) -> 104 parse_args(Rest, Opts#opts{dir=Dir}); 105parse_args(["-v"|Rest], Opts) -> 106 put(verbose, true), 107 parse_args(Rest, Opts#opts{verbose=true}); 108parse_args(["--verbose"|Rest], Opts) -> 109 put(verbose, true), 110 parse_args(Rest, Opts#opts{verbose=true}); 111 112parse_args(["--subdiv", N0|Rest], Opts=#opts{modify=Mod}) -> 113 N = try 114 list_to_integer(N0) 115 catch _:_ -> 116 io:format("**** Error: Option --subdiv ~p Not an integer ~n~n", [N0]), 117 usage(Opts) 118 end, 119 parse_args(Rest, Opts#opts{modify=[{subdivisions, N}|Mod]}); 120parse_args(["--tess"++_, "tri"++_|Rest], Opts=#opts{modify=Mod}) -> 121 parse_args(Rest, Opts#opts{modify=[{tesselation,triangulate}|Mod]}); 122parse_args(["--tess"++_, "quad"++_|Rest], Opts=#opts{modify=Mod}) -> 123 parse_args(Rest, Opts#opts{modify=[{tesselation,quadrangulate}|Mod]}); 124parse_args(["--name", Name|Rest], Opts=#opts{modify=Mod}) -> 125 parse_args(Rest, Opts#opts{modify=[{exp_name,Name}|Mod]}); 126 127parse_args(["--swap_y_z"++_|Rest], Opts=#opts{modify=Mod}) -> 128 parse_args(Rest, Opts#opts{modify=[{swap_y_z,true}|Mod]}); 129parse_args(["--scale", Scale|Rest], Opts=#opts{modify=Mod}) -> 130 F = try list_to_float(Scale) 131 catch _:_ -> 132 try float(list_to_integer(Scale)) 133 catch _:_ -> 134 io:format("**** Error: Option --scale ~p not a ~n~n", [Scale]), 135 usage(Opts) 136 end 137 end, 138 parse_args(Rest, Opts#opts{modify=[{scale, F}|Mod]}); 139parse_args(["--uv", Bool|Rest], Opts=#opts{modify=Mod}) -> 140 parse_args(Rest, Opts#opts{modify=[{include_uvs,bool(Bool, Opts)}|Mod]}); 141parse_args(["--n", Bool|Rest], Opts=#opts{modify=Mod}) -> 142 parse_args(Rest, Opts#opts{modify=[{include_normals,bool(Bool, Opts)}|Mod]}); 143parse_args(["--image", Ext|Rest], Opts=#opts{modify=Mod}) -> 144 parse_args(Rest, Opts#opts{modify=[{image,Ext}|Mod]}); 145 146parse_args(["--informat", Format|Rest], Opts) -> 147 parse_args(Rest, Opts#opts{in_format=check_format(in, Format, Opts)}); 148parse_args(["-f", Format|Rest], Opts) -> 149 parse_args(Rest, Opts#opts{out_module=check_format(out, Format, Opts)}); 150parse_args([Opt=[$-|_]| _], Opts) -> 151 io:format("**** Error: Unknown option ~p~n~n", [Opt]), 152 usage(Opts); 153parse_args(Files, Opts) -> 154 {Opts, Files}. 155 156check_format(Dir, Ext = [A|_], Opts) when A =/= $. -> 157 check_format(Dir, [$.|Ext], Opts); 158check_format(in, Ext, O=#opts{in_formats=In}) -> 159 case get_module(Ext, In) of 160 error -> 161 check_format_err(in, Ext, O); 162 Mod -> Mod 163 end; 164check_format(out, Ext, O=#opts{out_formats=Out}) -> 165 case get_module(Ext, Out) of 166 error -> 167 check_format_err(out, Ext, O); 168 Mod -> Mod 169 end. 170 171check_format_err(Dir, Format, Opts) -> 172 io:format("**** Error: Format ~p for ~pput is not supported ~n~n", [Format,Dir]), 173 usage(Opts). 174 175bool(Str, Opts) -> 176 case string:lowercase(Str) of 177 "false" -> false; 178 "true" -> true; 179 _ -> io:format("**** Error ~p is not a boolean ~n~n",[Str]), 180 usage(Opts) 181 end. 182 183usage(#opts{in_formats=In, out_formats=Out}) -> 184 io:format("Usage: wings_convert -f OutFormat [Opts] Files ~n" 185 " Converts between file formats. ~n" 186 " Output is written to the current directory by default.~n~n" 187 " Example: wings_convert -f obj --subdiv 1 --tess quad ../model.wings~n~n" 188 " Options:~n" 189 " -o, --outdir DIR Write converted files to DIR.~n" 190 " -v, --verbose Verbose output.~n" 191 " --informat FORMAT Ignore file extension and use FORMAT as input.~n" 192 " --subdiv N Subdivide object N times (default 0).~n" 193 " --tess TYPE Tesselate object none|tri|quad (default none)~n" 194 " --name Name Only Export Object with Name~n" 195 " --swap_y_z Swap axis (if supported by exporter) (default false)~n" 196 " --scale Float Scale (if supported by exporter) (default 1.0)~n" 197 " --uv Bool Export uv (if supported by exporter) (default true)~n" 198 " --n Bool Export normals (if supported by exporter) (default true)~n" 199 " --image Ext Convert images (if supported by exporter) (default .png)~n" 200 "~n" 201 ), 202 io:format("~nSupported import formats:~n",[]), 203 [io:format(" ~s~n", [Str]) || #format{str=Str} <- In], 204 io:format("~nSupported export formats:~n",[]), 205 [io:format(" ~s~n", [Str]) || #format{str=Str} <- Out], 206 io:nl(), 207 quit(1). 208 209quit(Exit) -> 210 verbose("Script: exiting\n", []), 211 Ref = monitor(process, wings), 212 wings ! {action, {file, confirmed_quit}}, 213 receive 214 {'DOWN',Ref,_,_,_Reason} -> 215 timer:sleep(100), 216 halt(Exit) 217 after 1000 -> 218 halt(Exit) 219 end. 220 221get_module(Ext, [F=#format{str=Str}|List]) -> 222 case string:str(Str,Ext) of 223 0 -> 224 get_module(Ext,List); 225 _ -> 226 F 227 end; 228get_module(_, []) -> 229 error. 230 231%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 232 233call_wings(Cmd) -> 234 verbose("Script: ~p~n",[{action, Cmd}]), 235 wings ! {action, Cmd}, 236 Me = self(), 237 Sync = fun(_St) -> 238 Me ! {cmd, sync}, 239 keep 240 end, 241 wings ! {external, Sync}, 242 receive {cmd, sync} -> ok end, 243 ok. 244 245import_file(File, Opts) -> 246 verbose("~s => ~n", [File]), 247 wings ! {action, fun() -> put(wings_not_running, {import, File}), keep end}, 248 try import_file_1(File,Opts) of 249 {error,Reason} -> 250 io:format("**** Import Failed: ~p On file: ~p~n~n", [Reason, File]), 251 quit(1); 252 ok -> 253 ok 254 catch 255 _:{command_error,Message} -> 256 io:format("**** Import Failed: ~s On file: ~p~n~n", [Message, File]), 257 quit(1); 258 _:Reason:Stk -> 259 io:format("**** Import crashed: ~p On file: ~p~n~n", [Reason, File]), 260 io:format("Debug info: ~p~n~n",[Stk]), 261 quit(1) 262 end. 263 264import_file_1(File, Opts=#opts{in_format=undefined}) -> 265 import_file_2(filename:extension(File),File,Opts); 266 267import_file_1(File, Opts=#opts{in_format=InFormat}) -> 268 import_file_2(InFormat,File,Opts). 269 270import_file_2(#format{mod=wings}, File, _) -> 271 call_wings({file, {confirmed_open, File}}); 272import_file_2(#format{mod=nendo}, File, _) -> 273 call_wings({file, {import, {ndo, File}}}); 274import_file_2(#format{ext_type=Type, option=false}, _File, _Opts) -> 275 call_wings({file,{import,Type}}); 276import_file_2(#format{ext_type=Type, option=true}, _File, _) -> 277 call_wings({file,{import,{Type,[]}}}); 278 279import_file_2(Str, File, Opts = #opts{in_formats=In}) -> 280 case get_module(Str, In) of 281 error -> 282 io:format("**** Error: Import Failed: ~p On file: ~p~n~n", 283 ["Unknown import format", File]), 284 quit(1); 285 Mod -> import_file_2(Mod, File, Opts) 286 end. 287 288%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 289do_mods(#opts{modify=Mods}) -> 290 modify_model(Mods). 291 292%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 293 294export_file(File, Opts=#opts{dir=Dir, out_module=F=#format{ext_type=Ext}}) -> 295 FileName = filename:join(Dir, File++"."++ atom_to_list(Ext)), 296 verbose("Export to: ~s~n", [FileName]), 297 wings ! {action, fun() -> put(wings_not_running, {export, FileName}), keep end}, 298 299 try export_file_1(F, FileName, Opts) of 300 {error,Reason} -> 301 io:format("**** Export Failed: ~p On file: ~p~n~n", [Reason, FileName]), 302 quit(1); 303 ok -> 304 ok 305 catch 306 _:{command_error,Message} -> 307 io:format("**** Export Failed: ~s On file: ~p~n~n", [Message, File]), 308 quit(1); 309 _:Reason:ST -> 310 io:format("**** Export crashed: ~p On file: ~p~n~n", [Reason, FileName]), 311 io:format("Debug info: ~p~n~n",[ST]), 312 quit(1) 313 end. 314 315export_file_1(F, FileName, #opts{modify=Mods}) -> 316 Opts = pick_export_opts(Mods), 317 case proplists:get_value(exp_name, Mods, false) of 318 false -> export_file_2(F, FileName, false, Opts); 319 _Name -> export_file_2(F, FileName, true, Opts) 320 end. 321 322export_file_2(#format{mod=wings}, FileName, Selected, _) -> 323 call_wings({file, {exp_cmd(save_as, Selected), {FileName, ignore}}}); 324export_file_2(#format{mod=nendo}, FileName, Selected, _) -> 325 call_wings({file, {exp_cmd(export, Selected), {ndo, FileName}}}); 326export_file_2(#format{ext_type=Type, option=false}, _FN, Selected, _) -> 327 call_wings({file,{exp_cmd(export, Selected),Type}}); 328export_file_2(#format{ext_type=Type, option=true}, _F, Selected, Opts) -> 329 call_wings({file,{exp_cmd(export, Selected),{Type, Opts}}}). 330 331exp_cmd(export, false) -> export; 332exp_cmd(export, true) -> export_selected; 333exp_cmd(save_as, false) -> save_as; 334exp_cmd(save_as, true) -> save_selected. 335 336pick_export_opts(Mods) -> 337 [{include_uvs, proplists:get_value(include_uvs, Mods, true)}, 338 {include_normals, proplists:get_value(include_normals, Mods, true)}, 339 {swap_y_z, proplists:get_value(swap_y_z, Mods, false)}, 340 {export_scale, proplists:get_value(scale, Mods, 1.0)}, 341 {default_filetype, string:lowercase(proplists:get_value(image, Mods, ".png"))} 342 ]. 343 344verbose(F,A) -> 345 get(verbose) andalso io:format(F,A). 346 347modify_model([]) -> 348 ok; 349modify_model(Ps) -> 350 SubDivs = proplists:get_value(subdivisions, Ps, 0), 351 Tess = proplists:get_value(tesselation, Ps, none), 352 call_wings({select, body}), 353 case proplists:get_value(exp_name, Ps, false) of 354 false -> call_wings({select, all}); 355 Name -> call_wings({select, {by, {by_name_with, Name}}}) 356 end, 357 sub_divide(SubDivs), 358 tesselate(Tess), 359 ok. 360 361sub_divide(0) -> ok; 362sub_divide(N) -> 363 call_wings({body, smooth}), 364 sub_divide(N-1). 365 366tesselate(none) -> ok; 367tesselate(triangulate) -> 368 call_wings({tools,{virtual_mirror, freeze}}), 369 call_wings({select, face}), 370 call_wings({face, {tesselate, triangulate}}); 371tesselate(quadrangulate) -> 372 call_wings({tools,{virtual_mirror, freeze}}), 373 call_wings({select, face}), 374 call_wings({face, {tesselate, quadrangulate}}). 375 376 377%%%%%%%%%%%%%%%%%%%%%%%%%%% 378 379setup_paths() -> 380 Escript = filename:dirname(filename:absname(escript:script_name())), 381 Installed = installed(Escript), 382 EnvDir = os:getenv("WINGS_DIR"), 383 DefDir = ?WINGS_DIR, 384 case test_paths([Installed, Escript,EnvDir,DefDir]) of 385 {ok, Path} -> 386 code:add_patha(filename:join([Path, "ebin"])), 387 Path; 388 _ -> 389 io:format("**** Error: Compiled wings files not found~n~n"), 390 io:format(" use 'set WINGS_DIR=c:\PATH_TO_WINGS_INSTALL~n~n") 391 end. 392 393test_paths([false|Rest]) -> test_paths(Rest); 394test_paths([Path0|Rest]) -> 395 Path = strip_path(lists:reverse(Path0)), 396 case filelib:is_regular(filename:join([Path, "ebin", "wings.beam"])) of 397 true -> {ok, Path}; 398 false -> test_paths(Rest) 399 end; 400test_paths([]) -> not_found. 401 402strip_path("nibe/" ++ Path) -> lists:reverse(Path); 403strip_path("crs/" ++ Path) -> lists:reverse(Path); 404strip_path(Path) -> lists:reverse(Path). 405 406installed(Path) -> 407 Lib = filename:join(Path, "lib"), 408 case filelib:wildcard("wings-*", Lib) of 409 [WingsDir] -> 410 filename:join(Lib, WingsDir); 411 [] -> 412 false; 413 [_|_] = Strange -> 414 io:format("Ignore bad installation (please report):~n ~p~n", 415 [Strange]), 416 false 417 end. 418