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