From efd8d2ad2508e94cc51ab25eee041bbd25afc5fe Mon Sep 17 00:00:00 2001 From: Edward Blake Date: Mon, 11 May 2026 09:56:34 -0400 Subject: [PATCH] New plugin: Metadata Added metadata plugins for the user to add information pertinent to the scene or each object in the scene. wpc_metadata creates the "Metadata" menu in the tools menu and queries the other plugins for those that export functions metadata_names/1, metadata_dialog/3, and metadata_update/4 . It also implements a generic comments field which can be used by the user to enter any text information. Two metadata plugins are implemented, one for dublin core meta data and the other for units of measure. Values are stored in the 'metadata' key in st#pst and we#pst. NOTE: Added metadata plugins --- plugins_src/commands/Makefile | 3 + plugins_src/commands/wpc_metadata.erl | 223 ++++++++++++++++++++ plugins_src/commands/wpc_metadata_dc.erl | 121 +++++++++++ plugins_src/commands/wpc_metadata_units.erl | 78 +++++++ 4 files changed, 425 insertions(+) create mode 100644 plugins_src/commands/wpc_metadata.erl create mode 100644 plugins_src/commands/wpc_metadata_dc.erl create mode 100644 plugins_src/commands/wpc_metadata_units.erl diff --git a/plugins_src/commands/Makefile b/plugins_src/commands/Makefile index a095eabe..40a5115b 100644 --- a/plugins_src/commands/Makefile +++ b/plugins_src/commands/Makefile @@ -51,6 +51,9 @@ MODULES= \ wpc_intersect_vertex \ wpc_isometric_view \ wpc_magnet_mask \ + wpc_metadata \ + wpc_metadata_dc \ + wpc_metadata_units \ wpc_move_planar \ wpc_numeric_camera \ wpc_plane_cut \ diff --git a/plugins_src/commands/wpc_metadata.erl b/plugins_src/commands/wpc_metadata.erl new file mode 100644 index 00000000..32b1710b --- /dev/null +++ b/plugins_src/commands/wpc_metadata.erl @@ -0,0 +1,223 @@ +%% +%% wpc_metadata.erl -- +%% +%% Metadata functionality +%% +%% Copyright (c) 2026 Edward Blake +%% +%% See the file "license.terms" for information on usage and redistribution +%% of this file, and for a DISCLAIMER OF ALL WARRANTIES. +%% +%% $Id$ +%% + +-module(wpc_metadata). +-export([init/0,menu/2,command/2]). + +-include_lib("wings/src/wings.hrl"). + +init() -> + true. + +menu({tools},Menu) -> + metadata_submenu(Menu); +menu({tools,metadata},Menu) -> + metadata_menu_entry(Menu); +menu(_,Menu) -> Menu. + +metadata_submenu([]) -> + [{?__(1,"Metadata"), {metadata, []}}]; +metadata_submenu([A|Menu]) -> + [A|metadata_submenu(Menu)]. + +metadata_menu_entry([]) -> + [{?__(1,"Scene"),scene, + ?__(2,"Set scene metadata")}, + {?__(3,"Object"),object, + ?__(4,"Set object metadata")}]; +metadata_menu_entry([A|Menu]) -> + [A|metadata_menu_entry(Menu)]. + +command({tools,{metadata,scene}},St) -> + set_scene_metadata(St); +command({tools,{metadata,object}},St) -> + set_obj_metadata(St); +command(_,_) -> + next. + +%%% +%%% + +%% +%% Set Scene Metadata +%% + +set_scene_metadata(#st{pst=Pst}=St) -> + List = metadata_plugins(scene), + QsFrames = metadata_tab_frames(scene, + fun (Name) -> get_metadata(Name,Pst) end, List), + Frame = [{oframe, QsFrames, 1, [{style, buttons}]}], + wings_dialog:dialog(?__(1,"Scene Metadata"), {preview,Frame}, + fun + ({dialog_preview,Args}) -> + {preview,St,set_scene_metadata_1(Args, List, St)}; + (cancel) -> + St; + (Args) -> + {commit,St,set_scene_metadata_1(Args, List, St)} + end). + +set_scene_metadata_1(Args, List, #st{pst=Pst}=St) -> + Metadata = get_pst_metadata(Pst), + Metadata_2 = lists:foldl( + fun({Name, _, Mod}, Metadata_1) -> + update_metadata(Name, Mod, scene, Args, Metadata_1) + end, Metadata, List), + St#st{pst=update_pst_metadata(Metadata_2, Pst)}. + + +%% +%% Object metadata. +%% +set_obj_metadata(#st{sel=[_|_],selmode=body}=St) -> + List = metadata_plugins(object), + QsFrames = metadata_tab_frames(object, + fun (Name) -> get_obj_metadata(Name, St) end, List), + Frame = [{oframe, QsFrames, 1, [{style, buttons}]}], + wings_dialog:dialog(?__(1,"Object Metadata: ") ++ obj_names(St), {preview,Frame}, + fun + ({dialog_preview,Args}) -> + {preview,St,set_obj_metadata_1(Args, List, St)}; + (cancel) -> + St; + (Args) -> + {commit,St,set_obj_metadata_1(Args, List, St)} + end); +set_obj_metadata(#st{sel=[]}=_St) -> + wings_u:error_msg(?__(3,"Need at least one object selected.")); +set_obj_metadata(#st{selmode=SelMode}=St) when SelMode =/= body -> + set_obj_metadata(wings_sel_conv:mode(body, St)). + +set_obj_metadata_1(Args, List, St) -> + wings_sel:map(fun (_, #we{pst=Pst}=We) -> + Metadata = get_pst_metadata(Pst), + Metadata_2 = lists:foldl( + fun({Name, _, Mod}, Metadata_1) -> + update_metadata(Name, Mod, object, Args, Metadata_1) + end, Metadata, List), + We#we{pst=update_pst_metadata(Metadata_2, Pst)} + end, St). + +obj_names(St) -> + {Extra, StrList} = + wings_sel:fold( + fun + (_, #we{name=ObjName}=_We, {0, Acc}) when length(Acc) < 3 -> + {0, [ObjName|Acc]}; + (_, #we{name=_}=_We, {C, Acc}) -> + {C + 1, Acc} + end, {0, []}, St), + lists:flatten(lists:join(?__(1,", "), lists:reverse(StrList)) ++ + if + Extra =:= 1 -> io_lib:format(?__(2,", ~w more object"), [Extra]); + Extra > 1 -> io_lib:format(?__(3,", ~w more objects"), [Extra]); + true -> "" + end). + +%%% +%%% + +%% Find the metadata plugins +%% +metadata_plugins(Scope) -> + Plugins = get(wings_plugins), + lists:append([ try_metadata_plugin(Scope, Pl) || Pl <- Plugins]) + ++ [{comments, ?__(2,"Comments"), 0}]. + +%% Create Tab frames for dialog +%% +metadata_tab_frames(Scope, Fun, List) -> + [{TabName, module_metadata_dialog(Mod, Name, Scope, Fun(Name))} + || {Name, TabName, Mod} <- List]. + +%%% +%%% + +update_metadata(Name, Mod, Scope, Args, Metadata) -> + MetadataC = case proplists:get_value(Name, Metadata, []) of + MetadataC_0 when is_list(MetadataC_0) -> MetadataC_0; + _ -> [] + end, + MetadataC_1 = module_metadata_update(Mod, Name, Scope, MetadataC, Args), + case length(MetadataC_1) =:= 0 of + true -> + proplists:delete(Name, Metadata); + false -> + orddict:store(Name, MetadataC_1, orddict:from_list(proplists:delete(Name, Metadata))) + end. + +get_pst_metadata(Pst) -> + case gb_trees:lookup(metadata,Pst) of + none -> []; + {value, Metadata} when is_list(Metadata) -> Metadata; + {value, _} -> [] + end. + +update_pst_metadata(Metadata, Pst) -> + gb_trees:enter(metadata, Metadata, Pst). + +get_metadata(Name, Pst) -> + Metadata = get_pst_metadata(Pst), + case proplists:get_value(Name, Metadata, []) of + MetadataC when is_list(MetadataC) -> MetadataC; + _ -> [] + end. + + +get_obj_metadata(Name, St) -> + wings_sel:fold(fun (_, #we{pst=Pst}=_We, Acc) -> + Metadata = get_pst_metadata(Pst), + case proplists:get_value(Name,Metadata,Acc) of + MetadataC when is_list(MetadataC) -> MetadataC; + _ -> [] + end + end, [], St). + + +module_metadata_dialog(0, comments, _Scope, MetadataC) -> + metadata_comments_dialog(MetadataC); +module_metadata_dialog(Mod, Name, Scope, MetadataC) when is_atom(Mod) -> + Mod:metadata_dialog(Scope, Name, MetadataC). + +module_metadata_update(0, comments, _Scope, MetadataC, Args) -> + metadata_comments_update(MetadataC, Args); +module_metadata_update(Mod, Name, Scope, MetadataC, Args) when is_atom(Mod) -> + Mod:metadata_update(Scope, Name, MetadataC, Args). + +try_metadata_plugin(Scope, Pl) -> + try Pl:metadata_names(Scope) of + Ret -> Ret + catch + error:_ -> [] + end. + + +%% Free-form comments +%% + +metadata_comments_dialog(MetadataComments) when is_list(MetadataComments) -> + Val = proplists:get_value(comments, MetadataComments, ""), + {vframe,[ + {label,?__(12,"Comments")}, + {text,Val,[{key,comments},{width,50}]} + ]}. + +metadata_comments_update(MetadataComments, Args) when is_list(MetadataComments) -> + case proplists:get_value(comments, Args, "") of + "" -> + proplists:delete(comments, MetadataComments); + Creator -> + orddict:store(comments, Creator, orddict:from_list(proplists:delete(comments, MetadataComments))) + end. + + diff --git a/plugins_src/commands/wpc_metadata_dc.erl b/plugins_src/commands/wpc_metadata_dc.erl new file mode 100644 index 00000000..6b89f9bb --- /dev/null +++ b/plugins_src/commands/wpc_metadata_dc.erl @@ -0,0 +1,121 @@ +%% +%% wpc_metadata_dc.erl -- +%% +%% Plugin that implements Dublin Core metadata +%% +%% Copyright (c) 2026 Edward Blake +%% +%% See the file "license.terms" for information on usage and redistribution +%% of this file, and for a DISCLAIMER OF ALL WARRANTIES. +%% +%% $Id$ +%% + +-module(wpc_metadata_dc). +-export([init/0,menu/2,command/2]). +-export([metadata_names/1]). +-export([metadata_dialog/3]). +-export([metadata_update/4]). + +-include_lib("wings/src/wings.hrl"). + +init() -> + true. +menu(_,Menu) -> Menu. +command(_,_) -> + next. + +%%% +%%% + +%% The dublin core elements +%% +elements() -> + [ + {text,{title, + ?__(1,"Title:"), + ?__(2,"The name of the resource")}}, + {text,{creator, + ?__(3,"Creator:"), + ?__(4,"The person or entity that primarily created the resource.")}}, + {text,{subject, + ?__(5,"Subject:"), + ?__(6,"The topic or keywords of the resource.")}}, + {text,{description, + ?__(7,"Description:"), + ?__(8,"A description of the resource")}}, + {text,{publisher, + ?__(9,"Publisher:"), + ?__(10,"The publisher of the resource")}}, + {text,{contributor, + ?__(11,"Other Contributors:"), + ?__(12,"Other persons or entities that contributed towards the creation of the resource")}}, + {text,{date, + ?__(13,"Date:"), + ?__(14,"A date specifying the creation or availability of the resource")}}, + {text,{identifier, + ?__(15,"Identifier:"), + ?__(16,"An identifier (such as a URI, string or number) that distinguishes the resource.")}}, + {text,{format, + ?__(17,"Format:"), + ?__(18,"The data format and dimensions of the resource. Software and hardware related to the resource can be specified here.")}}, + {text,{source, + ?__(19,"Source:"), + ?__(20,"Identifiers to other resources that are a source for the current resource.")}}, + {text,{coverage, + ?__(21,"Coverage:"), + ?__(22,"The geographic or temporal coverage of the resource.")}}, + {text,{language, + ?__(23,"Language:"), + ?__(24,"The language of the resource.")}}, + {text,{relation, + ?__(25,"Relation:"), + ?__(26,"Identifiers to other resources with a relation to the current resource.")}}, + {text,{rights, + ?__(27,"Rights:"), + ?__(28,"Information on rights of the resource.")}}, + {text,{type, + ?__(29,"Type:"), + ?__(30,"A category of media of the resource.")}} + ]. + + +%%% +%%% + +metadata_names(_Scope) -> + [{dc,?__(1,"General"),?MODULE}]. + +metadata_dialog(_Scope, _Name, MetadataDC) when is_list(MetadataDC) -> + List_0 = [V || {C,V} <- elements(), C =:= text], + {vframe,[{label_column,[ + text_field(Name, FieldStr, FieldInfo, MetadataDC) + || {Name,FieldStr,FieldInfo} <- List_0]}]}. + +metadata_update(_Scope, _Name, MetadataDC, Args) when is_list(MetadataDC) -> + List = [Name || {Name,_,_} <- [V || {C,V} <- elements(), C =:= text]], + lists:foldl( + fun (FieldName, Acc) -> + update(FieldName, Acc, Args) + end, MetadataDC, List). + +%%% +%%% + +text_field(Name, FieldName, FieldInfo, MetadataDC) when is_list(MetadataDC) -> + Val = proplists:get_value(Name, MetadataDC, ""), + {FieldName,{text,Val,[{key,Name},{width,50},{info,FieldInfo}]}}. + + +update(Name, Acc, Args) -> + case proplists:get_value(Name, Args, 1) of + "" -> + proplists:delete(Name, Acc); + Creator -> + orddict:store(Name, Creator, orddict:from_list(proplists:delete(Name, Acc))) + end. + + + + + diff --git a/plugins_src/commands/wpc_metadata_units.erl b/plugins_src/commands/wpc_metadata_units.erl new file mode 100644 index 00000000..047e7430 --- /dev/null +++ b/plugins_src/commands/wpc_metadata_units.erl @@ -0,0 +1,78 @@ +%% +%% wpc_metadata_units.erl -- +%% +%% Plugin that implements units metadata +%% +%% Copyright (c) 2026 Edward Blake +%% +%% See the file "license.terms" for information on usage and redistribution +%% of this file, and for a DISCLAIMER OF ALL WARRANTIES. +%% +%% $Id$ +%% + +-module(wpc_metadata_units). +-export([init/0,menu/2,command/2]). +-export([metadata_names/1]). +-export([metadata_dialog/3]). +-export([metadata_update/4]). + +-include_lib("wings/src/wings.hrl"). + +init() -> + true. +menu(_,Menu) -> Menu. +command(_,_) -> + next. + + +%%% +%%% + +%% Units of measure +%% +units() -> + [ + {none,?__(1,"None")}, + {m,?__(2, "Meters")}, + {dm,?__(3, "Decimeters")}, + {cm,?__(4, "Centimeters")}, + {mm,?__(5, "Millimeters")}, + {micron,?__(6, "Microns")}, + {yd,?__(7, "Yards")}, + {ft,?__(8, "Feet")}, + {in,?__(9, "Inches")} + ]. + + +%%% +%%% + +metadata_names(_Scope) -> + [{units,?__(1,"Units"),?MODULE}]. + +metadata_dialog(_Scope, _Name, MetadataUnits) when is_list(MetadataUnits) -> + Units = proplists:get_value(units, MetadataUnits, none), + {vframe,[ + {label,?__(12,"Units")}, + {menu, + [{MenuStr, MenuUnit} + || {MenuUnit,MenuStr} <- units()], + Units,[{key,units}]} + ]}. + +metadata_update(_Scope, _Name, MetadataUnits, Args) when is_list(MetadataUnits) -> + lists:foldl( + fun (FieldName, Acc) -> + update(FieldName, Acc, Args) + end, MetadataUnits, [units]). + +update(Name, Acc, Args) -> + case proplists:get_value(Name, Args, 1) of + none -> + proplists:delete(Name, Acc); + Val -> + orddict:store(Name, Val, orddict:from_list(proplists:delete(Name, Acc))) + end. + +