%% %% wings_palette.erl -- %% %% Maintains the vertex palette window. %% %% Copyright (c) 2004-2005 Bjorn Gustavsson, Dan Gudmundsson %% %% See the file "license.terms" for information on usage and redistribution %% of this file, and for a DISCLAIMER OF ALL WARRANTIES. %% %% $Id: wings_palette.erl,v 1.17 2005/01/09 09:30:38 bjorng Exp $ %% -module(wings_palette). -export([window/1,window/3]). -export([palette/1]). -define(NEED_ESDL, 1). -define(NEED_OPENGL, 1). -include("wings.hrl"). -import(lists, [map/2,foldl/3,reverse/1,reverse/2, keymember/3,keysearch/3,keydelete/3,sort/1]). -define(BOX_W, 16). -define(BOX_H, 16). -define(COLS_W, 8). -define(COLS_H, 8). -define(BORD, 2). -record(pst, {st, sel=none, cols, w=?COLS_W, h=?COLS_H, knob=0}). window(St) -> case wings_wm:is_window(palette) of true -> wings_wm:raise(palette), keep; false -> {{_,DeskY},{DeskW,_DeskH}} = wings_wm:win_rect(desktop), Pos = {DeskW-5,DeskY+55}, Size = {?COLS_W*?BOX_W+?COLS_W*?BORD+?BORD*2, ?COLS_H*?BOX_H+?COLS_H*?BORD+?BORD*2}, window(Pos, Size, St), keep end. window(Pos, {W,_}=Size, St) -> Cols = get_all_colors(St), {ColsW,ColsH} = calc_size(Cols,W), Pst = #pst{st=St, cols=add_empty(Cols,ColsW,ColsH), w=ColsW, h=ColsH}, Op = {seq,push,event({current_state,St}, Pst)}, Props = [{display_lists,geom_display_lists}], wings_wm:toplevel(palette, title(), Pos, Size, [{sizeable,?PANE_COLOR}, closable, vscroller, {anchor,ne}, {properties,Props}], Op), F = fun({color,_}) -> yes; ({material, _}) -> yes; (_) -> no end, wings_wm:set_prop(palette, drag_filter, F), wings_wm:dirty(). title() -> ?__(1,"Palette"). palette(#st{pal=[]}) -> []; palette(#st{pal=Pal0}) -> Pal = del_trailing(Pal0), case Pal == default_cols() of true -> []; false -> Pal end. default_cols() -> [{1.0,1.0,1.0},{1.0,0.0,0.0},{1.0,1.0,0.0},{0.0,1.0,0.0}, {0.0,1.0,1.0},{0.0,0.0,1.0},{1.0,0.0,1.0},{0.0,0.0,0.0}]. get_all_colors(#st{pal=Pal}) -> case Pal of [] -> Def = default_cols(), Def ++ lists:duplicate(?COLS_W*?COLS_H-length(Def), none); Cols -> Cols end. get_event(Pst) -> wings_wm:dirty(), {replace,fun(Ev) -> event(Ev, Pst) end}. event(redraw, Pst) -> wings_io:ortho_setup(), {W,H} = wings_wm:win_size(), wings_io:border(0, 0, W-1, H-1, ?PANE_COLOR), draw_objs(Pst), keep; event(resized, Pst=#pst{cols=Cols0,w=CW,h=CH}) -> {_,_,W,H} = wings_wm:viewport(), {ColsW, _} = calc_size(Cols0, W), Visible = H div (?BOX_H+?BORD), NewCols = ColsW*Visible, OldCols = CW*CH, ColsH = if OldCols < NewCols -> Visible; (OldCols rem ColsW) == 0 -> ColsH1 = OldCols div ColsW, ColsH1; true -> {N, _} = del_empty(reverse(Cols0)), Old = OldCols - N, ColsH1 = (Old div ColsW) + 1, if ColsH1 > Visible -> ColsH1; true -> Visible end end, update_scroller(0,Visible,ColsH), Cols1 = del_trailing(Cols0), Cols = add_empty(Cols1,ColsW,ColsH), get_event(Pst#pst{knob=0,cols=Cols,w=ColsW,h=ColsH}); event({set_knob_pos, Pos}, Pst = #pst{h=N,knob=Knob}) -> case round(Pos*N) of Knob -> keep; New0 -> New = if New0 < N -> New0; true -> N end, update_scroller(New,N), get_event(Pst#pst{knob=New}) end; event(close, _) -> delete; event(got_focus, _) -> L = wings_msg:button_format(?__(1,"Assign color to selection")), MR = wings_msg:button_format([], ?__(2,"Edit color"), ?__(3,"Show menu")), Mods = wings_msg:free_modifier(), ModName = wings_msg:mod_name(Mods), CL = [ModName,$+,wings_msg:button_format(?__(4,"Clear color"))], Msg = wings_msg:join([L,CL,MR]), wings_wm:message(Msg), wings_wm:dirty(); event({current_state,St = #st{pal=StPal}}, Pst=#pst{w=W,h=H}) -> case StPal of [] -> %%% Hm... think this through... get_event(Pst#pst{st=St}); _ -> get_event(Pst#pst{st=St, cols=add_empty(StPal,W,H)}) end; event(#mousemotion{state=Bst,x=X,y=Y,mod=Mod}=Ev, #pst{sel=Sel,cols=Cols}=Pst) when Bst band ?SDL_BUTTON_LMASK =/= 0 -> Delete = Mod band ?CTRL_BITS =/= 0, case select(X,Y,Pst) of none -> keep; Sel -> keep; Id when Delete -> {Bef,[_Prev|Rest]} = lists:split(Id, Cols), get_event(update(Bef++[none|Rest],Pst#pst{sel=none})); _ when Sel == none -> keep; _ -> case lists:nth(Sel+1, Cols) of none -> get_event(Pst#pst{sel=none}); _ -> drag_and_drop(Ev, lists:nth(Sel+1, Cols)), keep end end; event(#mousebutton{button=Butt,x=X,y=Y,mod=Mod,state=?SDL_PRESSED}, #pst{cols=Cols0}=Pst) when Butt =:= 1; Butt =:= 2 -> case Mod band wings_msg:free_modifier() =/= 0 of false -> get_event(Pst#pst{sel=select(X,Y,Pst)}); true when Butt =:= 1 -> case select(X,Y,Pst) of none -> keep; Id -> {Bef,[_Prev|Rest]} = lists:split(Id, Cols0), get_event(update(Bef++[none|Rest],Pst#pst{sel=none})) end; true -> keep end; event(#mousebutton{button=1,state=?SDL_RELEASED}, #pst{sel=none}) -> keep; event(#mousebutton{button=2,x=X,y=Y,state=?SDL_RELEASED}, Pst = #pst{sel=Sel}) -> case select(X,Y,Pst) of Sel -> command({edit,Sel}, Pst); _ -> get_event(Pst#pst{sel=none}) end; event(#mousebutton{button=1,x=X,y=Y,state=?SDL_RELEASED}, #pst{sel=Sel,cols=Cols,st=St0}=Pst) -> Color = lists:nth(Sel+1, Cols), case select(X,Y,Pst) of none -> keep; Sel when Color /= none -> St = case St0#st.selmode of vertex -> wings_vertex_cmd:set_color(Color, St0); edge -> wings_edge_cmd:set_color(Color, St0); face -> wings_face_cmd:set_color(Color, St0); body -> St1 = wings_sel_conv:mode(face, St0), St2 = wings_face_cmd:set_color(Color, St1), St2#st{sel=St0#st.sel,selmode=St0#st.selmode}; _ -> St0 end, wings_wm:send(geom, {new_state,St#st{pal=Cols}}), get_event(Pst#pst{sel=none}); Sel when Color == none -> command({edit,Sel}, Pst); _DropSpot -> get_event(Pst#pst{sel=none}) end; event(#mousebutton{x=X0,y=Y0}=Ev, Pst) -> Id = select(X0,Y0,Pst), case wings_menu:is_popup_event(Ev) of {yes,X,Y,_} when is_integer(Id) -> do_menu(Id, X, Y, Pst); _ -> keep end; event({action,{palette,Cmd}}, Pst) -> command(Cmd, Pst); event(lost_focus, Pst) -> wings_wm:allow_drag(false), get_event(Pst#pst{sel=none}); event({new_color,Cols}, Pst = #pst{w=W,h=H0,knob=Knob}) -> Sz = length(Cols), case Sz == W*H0 of true -> get_event(update(Cols,Pst)); false -> H = if Sz > W*H0 -> if (Sz rem W) == 0 -> Sz div W; true -> (Sz div W) + 1 end; true -> H0 end, update_scroller(Knob, H), get_event(update(add_empty(Cols,W,H),Pst#pst{h=H})) end; event({drop,{X,Y},{color,Color}}, #pst{cols=Cols0}=Pst) -> case select(X,Y,Pst) of none -> keep; Id -> {Bef,[_Prev|Rest]} = lists:split(Id, Cols0), Cols = Bef ++ [color(Color)|Rest], get_event(update(Cols, Pst)) end; event({drop,{X,Y},{material,Name}}, #pst{cols=Cols0,st=St}=Pst) -> case select(X,Y,Pst) of none -> keep; Id -> {Bef,[_Prev|Rest]} = lists:split(Id, Cols0), Mat = gb_trees:get(list_to_atom(Name), St#st.mat), Color = proplists:get_value(diffuse, proplists:get_value(opengl, Mat)), Cols = Bef ++ [color(Color)|Rest], get_event(update(Cols, Pst)) end; event(language_changed, _) -> wings_wm:toplevel_title(title()), keep; event(_Ev, _Pst) -> keep. update_scroller(First,Total) -> {_,_,_,H} = wings_wm:viewport(), Visible = H div (?BOX_H+?BORD), update_scroller(First,Visible,Total). update_scroller(First,Visible,Total) -> Name = wings_wm:this(), wings_wm:set_knob(Name, First/Total, Visible/Total). do_menu(Id,X,Y,#pst{cols=Cols}) -> Menu = [{?__(1,"Edit"),{'VALUE',{edit,Id}},?__(2,"Edit color")}], Smooth = case lists:nth(Id+1, Cols) of none -> [{?__(3,"Interpolate"),{'VALUE',{smooth,Id}}, ?__(4,"Interpolate Empty Colors")}]; _ -> [] end, Rest = [separator, {?__(5,"Clear All"), clear_all,?__(6,"Clear palette")}, {?__(7,"Compact"), compact,?__(8,"Compact Palette")}, {?__(9,"Scan Colors"), scan_all, ?__(10,"Scan colors from selected objects")}, separator, {?__(11,"Export"), export,?__(12,"Export palette to file")}, {?__(13,"Import"), import,?__(14,"Import palette from file")}], wings_menu:popup_menu(X,Y,palette,Menu ++ Smooth ++ Rest). command(clear_all, Pst = #pst{w=W,h=H}) -> get_event(update(lists:duplicate(W*H, none),Pst#pst{sel=none})); command(compact, Pst = #pst{w=W,h=H,cols=Cols0}) -> Cols = foldl(fun(none,Acc) -> Acc; (Col, Acc) -> [Col|Acc] end, [], Cols0), get_event(update(add_empty(reverse(Cols),W,H),Pst#pst{sel=none})); command({_,none}, _) -> keep; command({smooth,Id}, Pst = #pst{cols=Cols0}) -> {Bef0,After0} = lists:split(Id, Cols0), {BC, Bef1} = del_empty(reverse(Bef0)), {AC, Aft1} = del_empty(After0), case interpolate(Bef1,Aft1,AC+BC) of no_start -> wings_u:message(?__(1,"No start color found.")), keep; no_end -> wings_u:message(?__(2,"No end color found.")), keep; IntCols -> Cols = reverse(Bef1) ++ IntCols ++ Aft1, get_event(update(Cols,Pst#pst{sel=none})) end; command({edit,Id}, #pst{cols=Cols0}) -> {Bef,[Prev|Rest]} = lists:split(Id, Cols0), Send = fun(Color) -> Cols = Bef ++ [Color|Rest], wings_wm:send(palette, {new_color,Cols}), ignore end, wings_color:choose(color(Prev), Send); command(scan_all, #pst{st=St, cols=Cols}) -> New = scan_colors(St, del_trailing(Cols)), wings_wm:send(palette,{new_color,New}), keep; command(export, #pst{cols=Cols}) -> Dir = wings_pref:get_value(current_directory), Ps = [{title,?__(3,"Export")},{directory,Dir}, {ext,".wpal"},{ext_desc,?__(5,"Wings Palette")}], Fun = fun(Name) -> case write_file(Name, Cols) of ok -> keep; {error,Reason} -> Msg = io_lib:format(?__(6,"Export error: ~w"), [Reason]), wings_u:message(Msg), keep end end, wings_plugin:call_ui({file,save_dialog,Ps,Fun}); command(import, #pst{cols=Cols0}) -> Dir = wings_pref:get_value(current_directory), Ps = [{title,?__(7,"Import")},{directory,Dir}, {ext,".wpal"},{ext_desc,?__(9,"Wings Palette")}], Fun = fun(Name) -> case file:consult(Name) of {ok,Content} -> case lists:keysearch(palette,1,Content) of {value, {palette, Pal}} when list(Pal) -> Cols = del_trailing(Cols0) ++ Pal, wings_wm:send(palette, {new_color,Cols}), keep; _ -> Reason = ?__(10,"No palette found"), Msg = io_lib:format(?__(11,"Import error: ~w"), [Reason]), wings_u:message(Msg), keep end; {error,Reason} -> Msg = io_lib:format(?__(12,"Import error: ~w"), [Reason]), wings_u:message(Msg), keep end end, wings_plugin:call_ui({file,open_dialog,Ps,Fun}). write_file(Name, Cols) -> case file:open(Name, [write]) of {ok,F} -> io:format(F, "%% Wings Palette~n", []), io:format(F, "{palette, ~p}.~n", [del_trailing(Cols)]), file:close(F); Error -> Error end. del_trailing(L) -> {_,R} = del_empty(reverse(L)), reverse(R). del_empty(L) -> del_empty(L,0). del_empty([none|L],N) -> del_empty(L,N+1); del_empty(L,N) -> {N,L}. add_empty(L,W,H) when list(L) -> Sz = length(L), if Sz >= W*H -> L; true -> add_empty(L,W*H-Sz) end. add_empty(L,N) -> L ++ lists:duplicate(N,none). calc_size(Cols, W) -> ColsW0 = W div (?BOX_W+?BORD), ColsW = if ColsW0 < 1 -> 1; true -> ColsW0 end, ColsH0 = length(Cols) div ColsW, ColsH = if (length(Cols) rem ColsW) == 0 -> ColsH0; true -> ColsH0+1 end, {ColsW,ColsH}. update(Cols,Pst=#pst{st=St0}) -> St = St0#st{pal=Cols}, wings_wm:send(geom, {new_state,St}), Pst#pst{cols=Cols, st=St}. interpolate([{R1,G1,B1}|_],[Start={R2,G2,B2}|_], N) -> R = (R2-R1)/(N+1), B = (B2-B1)/(N+1), G = (G2-G1)/(N+1), interpolate(N,R,G,B,Start,[]); interpolate([],_,_) -> no_start; interpolate(_,[],_) -> no_end. interpolate(0,_R,_G,_B,_,Acc) -> Acc; interpolate(N,R,G,B,{PR,PG,PB},Acc) -> Col = {PR-R,PG-G,PB-B}, interpolate(N-1,R,G,B,Col,[Col|Acc]). drag_and_drop(Ev, What) -> DropData = {color, color(What)}, wings_wm:drag(Ev, {?BOX_W,?BOX_H}, DropData). draw_objs(#pst{cols=Cols0, w=W, h=_H, knob=Knob}) -> length(Cols0) == W*_H, %% Assert {_Bef,Cols} = lists:split(Knob*W, Cols0), draw_objs(0, ?BORD, ?BORD, W, Cols). draw_objs(_,_,_,_,[]) -> ok; draw_objs(N,X,Y,W,[Color|Cols]) when N < W -> case Color of none -> wings_io:border(X, Y, ?BOX_W, ?BOX_H, ?PANE_COLOR); _ -> wings_io:border(X, Y, ?BOX_W, ?BOX_H, Color) end, draw_objs(N+1, X+?BOX_W+?BORD, Y, W, Cols); draw_objs(_,_,Y,W,Cols) -> draw_objs(0, ?BORD, Y+?BOX_H+?BORD,W,Cols). color(none) -> {1.0,1.0,1.0}; color({_,_,_}=C) -> C; color({R,G,B,_}) -> {R,G,B}. select(X,Y,#pst{w=ColsW,h=ColsH, knob=Knob}) -> Col = X div (?BORD+?BOX_W), Row = Knob + (Y div (?BORD+?BOX_H)), Id = (Row*ColsW+Col), if X > ColsW *(?BORD+?BOX_W) -> none; Y > ColsH *(?BORD+?BOX_H) -> none; Id < 0 -> none; Id >= (ColsW*ColsH) -> none; true -> Id end. scan_colors(St = #st{mat=Mtab}, Old) -> Cols = wings_sel:fold(fun(_, We, Cols) -> scan_color(We,Cols) end, Old, St), Mats = scan_materials(gb_trees:values(Mtab), Cols), lists:usort(Mats). scan_color(#we{mode = material}, Acc) -> Acc; scan_color(#we{mode = vertex, es=Etab}, Acc) -> foldl(fun(#edge{a=Ca,b=Cb}, Cols) -> [color(Ca),color(Cb)|Cols] end, Acc, gb_trees:values(Etab)). scan_materials([Mat|Ms], Cols) -> Opengl = proplists:get_value(opengl, Mat), Diff = proplists:get_value(diffuse, Opengl), Amb = proplists:get_value(ambient, Opengl), Spec = proplists:get_value(specular, Opengl), Emis = proplists:get_value(emission, Opengl), scan_materials(Ms, [color(Diff),color(Amb),color(Spec), color(Emis) |Cols]); scan_materials([], Cols) -> Cols.