from __future__ import print_function import numpy as NP import os import re import sys from PIL import Image as IMG from PIL import ImageDraw try: import poser except NameError: raise RuntimeError("Script must run in Poser.") try: range = xrange except NameError: pass SCENE = poser.Scene() ACTOR = SCENE.CurrentActor() # Choose what you like for Pathnames: Object Name or object InternalName. # Used by some functions to get objects name or internalname. PATHNAME_FUNC = "Name" # PATHNAME_FUNC = "InternalName" def is_iterable(obj): return hasattr(obj, "__iter__") def is_actor(obj): return isinstance(obj, poser.ActorType) def is_parm(obj): return isinstance(obj, poser.ParmType) def is_morph(obj): return is_parm(obj) and obj.IsMorphTarget() def morph_empty(morph): return morph.NumMorphTargetDeltas() == 0 if is_morph(morph) else True def yield_morphs(actor, with_delta=None, with_value=None): _filter = [] if with_delta is not None: _filter.append("NumDeltas") if with_value is not None: _filter.append("Value") for obj in actor.Parameters(): if is_morph(obj) and all(getattr(obj, f)() != 0 for f in _filter): yield obj def __yield_pv(actor_or_geom, polytype="p"): """ !!! Don't use directly !!! Use yield_poly_vertices(..) or yield_texpoly_vertices(..) insteed. Yields all vertices or texvertices (depending on <char>polytype) in polygon-order, avoiding duplicate entries. Usefull for building UV-maps or find borders. :param actor_or_geom: Actor name or actor object or geometry. :param polytype: "p" for polygons, "t" for texpolygons. """ if isinstance(actor_or_geom, basestring): actor_or_geom = _get_actor_(actor_or_geom) if isinstance(actor_or_geom, poser.ActorType): geom = actor_or_geom.Geometry() else: geom = actor_or_geom if not isinstance(geom, poser.GeomType): raise GeneratorExit if polytype.lower().startswith("p"): polyfunc = geom.Polygons sets = geom.Sets() vertsattr = "Vertices" numv = geom.NumVertices() getter = lambda v: (v.X(), v.Y(), v.Z()) else: polyfunc = geom.TexPolygons sets = geom.TexSets() vertsattr = "TexVertices" numv = geom.NumTexVertices() getter = lambda v: (v.U(), v.V()) if numv == 0: raise GeneratorExit seen = set() for poly in polyfunc(): start_idx = poly.Start() for idx, vert in enumerate(getattr(poly, vertsattr)()): set_idx = start_idx + idx if set_idx not in seen: seen.add(set_idx) yield sets[set_idx], getter(vert) def yield_poly_vertices(actor_or_geom): """ Yields all vertices in polygon-order, avoiding duplicate entries. """ return __yield_pv(actor_or_geom, "p") def yield_texpoly_vertices(actor_or_geom): """ Yields all texvertices in texpolygon-order, avoiding duplicate entries. """ return __yield_pv(actor_or_geom, "t") def _get_name_(obj): if obj is None: return None try: return getattr(obj, "Name")() \ if PATHNAME_FUNC.startswith("N") \ else getattr(obj, "InternalName")() except poser.error: return None def _get_figure_(figure_name): """ Returns poser.Scene().Figure(<str>figure_name) if <str>PATHNAME_FUNC's first character is "N" (for "Name") or poser.Scene().FigureByInternalName(<str>figure_name) otherwise. :type figure_name: str, basestring :rtype: poser.FigureType, None """ if isinstance(figure_name, poser.FigureType): return figure_name try: return SCENE.Figure(figure_name) \ if PATHNAME_FUNC.startswith("N") \ else SCENE.FigureByInternalName(figure_name) except poser.error: return None def _get_actor_(actor_name, _from=SCENE): """ Returns <_from>.Actor(<str>actor_name) if <str>PATHNAME_FUNC's first character is "N" (for "Name") or <_from>.ActorByInternalName(<str>actor_name) otherwise, where <_from> is a callable object like SCENE or Figure. ATTENTION: Poser may ignore numbering in actor internalnames. poser.Scene().ActorByInternalName("BODY:1234") returns the first body-actor found. Even if more than one BODY-actors is in your scene. To be sure to get the wanted BODY actor, use the right figure, not SCENE for lookup. HINT: <figure>.ParentActor() returns whatever actor is declared as bodyactor. :param actor_name: Actorname :param _from: Scene or Figure to get actor from. :type actor_name: str, basestring :type _from: poser.SceneType, poser.figureType :rtype: poser.ActorType, None """ if isinstance(actor_name, poser.ActorType): return actor_name try: return _from.Actor(actor_name) \ if PATHNAME_FUNC.startswith("N") \ else _from.ActorByInternalName(actor_name) except poser.error: return None def master_ref(parameter, _masterdial="Body"): """ Try to find and return MasterDials in <Poser.ActorType> with name <str>_masterdial for given parameter. Returns None if no MasterDial is found. ATTENTION: <str>_masterdial must match <actor>.Name() or <actor>.InternalName() depending on PATHNAME_FUNC. :param parameter: Parameter to search for. :param _masterdial: Actorname where masterdial is located. :return: SourceParameter in body-actor. :type parameter: poser.ParmType :rtype: poser.ParmType, None """ assert isinstance(parameter, poser.ParmType) for vop in parameter.ValueOperations(): if _get_name_(vop.SourceParameter().Actor()) == _masterdial: return vop.SourceParameter() return None def clear_morph(morph, value=None): """ Set all deltas in morphtarget to (0,0,0). If value is not None, it must be an integer value (morph-strength, eg. <poser-param>.Value()). """ if not morph_empty(morph): actor = morph.Actor() for idx in range(actor.Geometry().NumVertices()): morph.SetMorphTargetDelta(idx, 0, 0, 0) if isinstance(value, int): morph.SetValue(value) return morph def create_morphtarget(actor, name, set_empty=True): """ Creates a new morph-target if it does not exist yet. Return <poser.ParmTyp> parameter found or created. If <bool> setEmpty is True, the new (or found) morph is set to all zeros. """ assert is_actor(actor) assert isinstance(name, basestring) param = actor.Parameter(name) if not param: # Create new parameter if it does not exist # and try to get a handle into <param> try: actor.SpawnTarget(name) except poser.error as err: print("Something wrong with SpawnTarget:") print("Actor:", actor.Name()) print("Param:", name) raise err else: try: param = actor.Parameter(name) except Exception as err: print("Something wrong with actor.Parameter:") print("Actor:", actor.Name()) print("Param:", name) raise err # if <bool> setEmpty is set, make sure the morph is clean if set_empty: clear_morph(param) return param def get_morph(arg, actor=None): """ Returns one or more morphtargets with name <str>arg from <poser.ActorType>actor. If actor is not given (None), current actor is used. If name is a simple string, this one target is returned (or None if not found). If name is a regular expression, a tuple of targets is returned. E.g: regex = re.compile("[l|r]_morph") get_morph(regex) # returns <tuple("l_morph", "r_morph")> :param arg: Poser parameter (morph) or morphname. :param actor: Poser actor (only important if morph is a name and not an obj). :return: Found morph or None. :type arg: str, poser.ParmType :type actor: poser.ActorType, None :rtype: poser.ParmType """ if is_morph(arg): return arg if actor is None: actor = SCENE.CurrentActor() if isinstance(arg, basestring): if isinstance(arg, re._pattern_type): return (m for m in yield_morphs(actor) if arg.search(m.Name())) elif arg.endswith("*"): return (m for m in yield_morphs(actor) if m.Name().startswith(arg[:-1])) try: return actor.Parameter(arg) except poser.error: pass return None def morph2numarray(morph=None): """ Return numpy array containing raw morph-deltas (vectors). :param morph: poser.ParmType aka morph. :return: Numpy array. :type morph: poser.ParmType, None :rtype: NP.ndarray """ if morph_empty(morph): return None assert is_parm(morph) geom = morph.Actor().Geometry() if geom is None or geom.NumVertices() == 0: return None return NP.array([morph.MorphTargetDelta(i) for i in range(geom.NumVertices())], NP.float64) def numarray2morph(num_ar, morph, actor=None): """ Puts data from numpy-array into morphtarget. :param num_ar: :param morph: :param actor: :return: """ if isinstance(morph, basestring) and actor is not None: morph = create_morphtarget(actor, morph, True) assert is_morph(morph) numv = morph.Actor().Geometry().NumVertices() assert numv == len(num_ar) for i, v in enumerate(num_ar): morph.SetMorphTargetDelta(i, *v) return morph def normalize_morph(morph, actor=None): """ Scales current morph-deltas so that Value = 1.0 reflects current morph state. Useful if a morph needs a big or low value and better should have a range with 0 to 1. """ actor = actor or SCENE.CurrentActor() if isinstance(morph, basestring): morph = get_morph(morph, actor or SCENE.CurrentActor()) if morph_empty(morph): return deltas = morph2numarray(morph) deltas *= morph.Value() numarray2morph(deltas, morph) masterdial = master_ref(morph) if masterdial: morph.SetValue(0.0) masterdial.SetValue(1.0) else: morph.SetValue(1.0) def zero_side(morph, side=False): """ Set a morphs left or right side to zero. Parameter <bool|str>side: 'left' or 'right', or False/True """ if not is_morph(morph): morph = get_morph(morph, SCENE.CurrentActor()) if morph_empty(morph): return False if isinstance(side, basestring): side = side == "left" if side: should_be_zero = lambda a: a > 0 else: should_be_zero = lambda a: a < 0 geom = morph.Actor().Geometry() for idx, v in enumerate(geom.Vertices()): if should_be_zero(v.X()): morph.SetMorphTargetDelta(idx, 0, 0, 0) return True def merge_morphs(targetmorph, morphlist, force_2_one=False): """ merge_morph(<Parameter>targetmorph, <iterable>morphlist, <bool>force_2_one) Merges morphs listed in morphlist into targetmorph. <Parameter>targetmorph: Must exist and must be able to hold morph-deltas. <iterable>morphlist: List, Tuple, Generator or any class with "__iter__". <bool>force_2_one: If False, resulting morph will reflect exact what is currently shown on screen. If True, all morphs are forced to Value 1 (mostly not what you want). Returns morphtarget <poser.ParmType>targetmorph. """ if not is_iterable(morphlist): raise TypeError("Morphlist not iterable.") if isinstance(targetmorph, basestring): targetmorph = create_morphtarget(SCENE.CurrentActor(), targetmorph) if not is_morph(targetmorph): raise TypeError("Target is not a morph.") targetActor = targetmorph.Actor() geom = targetActor.Geometry() deltas = NP.array([[0, 0, 0] for i in range(geom.NumVertices())], NP.float64) for morphparm in morphlist: morph = get_morph(morphparm, targetActor) if not is_morph(morph): continue if morph.NumMorphTargetDeltas() > 0: v = 1.0 if force_2_one else morph.Value() d = morph2numarray(morph) * v deltas += d numarray2morph(deltas, targetmorph) return targetmorph def subtract_morph(targetmorph, sub): """ subtract_morph(<morphParm|basestring>targetmorph, <morphParm|basestring>sub) Subtract <morphParm>sub from <morphParm>targetmorph. returns <morphParm>targetmorph """ if isinstance(targetmorph, basestring): targetmorph = get_morph(targetmorph, SCENE.CurrentActor()) if isinstance(sub, basestring): sub = get_morph(targetmorph, SCENE.CurrentActor()) assert is_morph(targetmorph), "Target must exist." assert is_morph(sub), "Morph to extract must exist." numarray2morph(targetmorph, morph2numarray(targetmorph) - morph2numarray(sub) ) return targetmorph def morph2actor_geometry(actor, morph): """ Add morphdata from <morph> found in actor <actor> to actors geometry. Because geometry is changed, Poser may write the geometry out to one of Posers folders. Parameter actor: Existing Poser actor with Geometry to change. Parameter morph: Existing morph-parameter or parameter-name or a numpy array with deltas. Returns: Actor <actor> or raised exeption-object in case of an error. """ def add_array2geometry(actor, ar_data): try: geom = actor.Geometry() for (x, y, z), vertex in zip(ar_data, geom.Vertices()): vertex.SetX(vertex.X() + x) vertex.SetY(vertex.Y() + y) vertex.SetZ(vertex.Z() + z) actor.MarkGeomChanged() except Exception as err: return err return actor if isinstance(morph, basestring): morph = get_morph(morph, actor) if is_morph(morph): ar = morph2numarray(morph) * morph.Value() elif isinstance(morph, NP.ndarray): ar = morph else: raise TypeError("Morph must be {} or {}.".format(poser.ParmType, NP.ndarray)) return add_array2geometry(actor=actor, ar_data=ar) def create_masterdial(masterdial, morphnamelist, defaultValue=None): """ Create a masterdial for a list of morphs in the body of a figure. Morphs without deltas are ignored. """ figure = SCENE.CurrentFigure() if isinstance(masterdial, basestring): body_actor = figure.ParentActor() masterdial = body_actor.Parameter(masterdial) if masterdial is None: masterdial = body_actor.CreateValueParameter(masterdial) elif isinstance(masterdial, poser.ParmType): figure = masterdial.Actor().ItsFigure() else: raise TypeError("'Masterdial' must be {} or {}.".format(poser.ParmType, type(""))) body_actor = figure.ParentActor() assert body_actor.IsBodyPart() assert masterdial.IsValueParameter() if isinstance(morphnamelist, basestring): # this allows one single morphname morphnamelist = [morphnamelist, ] for actor in (ac for ac in figure.Actors() if ac.IsBodyPart()): for morphname in (n for n in morphnamelist if actor.Parameter(n)): parm = actor.Parameter(morphname) if morph_empty(parm): while parm.NumValueOperations(): parm.DeleteValueOperation(0) parm.AddValueOperation(poser.kValueOpTypeCodeKEY, masterdial) v = parm.Value() if -0.0001 <= v <= 0.0001 and defaultValue is not None: v = defaultValue vop = parm.ValueOperations()[0] vop.InsertKey(0, 0) vop.InsertKey(1, v) parm.SetValue(0) masterdial.SetValue(1) def collect_masterparms(actorlist=None): if actorlist is None: actorlist = SCENE.Actors() storage = dict() for actor in actorlist: fig = actor.ItsFigure() figurename = _get_name_(fig) if fig else "UNIVERSE" d = storage.setdefault(figurename, dict()) for parm in actor.Parameters(): if parm.NumValueOperations(): for vop in parm.ValueOperations(): sourceparm = vop.SourceParameter() d.setdefault(_get_name_(sourceparm.Actor()), dict()) \ .setdefault(_get_name_(sourceparm), list()) \ .append((_get_name_(parm.Actor()), _get_name_(parm))) if not d: del storage[figurename] return storage def frompath(path, storage=None): assert isinstance(path, basestring) path = path.split("/") if storage is None: if path[0] in (_get_name_(f) for f in SCENE.Figures()): figure = _get_figure_(path.pop(0)) if not path: return figure else: figure = SCENE.CurrentFigure() if path[0] in (_get_name_(a) for a in figure.Actors()): actor = _get_actor_(path.pop(0), figure) if not path: return actor else: actor = SCENE.CurrentActor() if path[0] in (_get_name_(p) for p in actor.Parameters()): parameter = actor.Parameter(path.pop(0)) if not path: return parameter else: return None if hasattr(parameter, path[0]): return getattr(parameter, path.pop(0))() else: return None elif isinstance(storage, dict): d = storage.get(path.pop(0)) while path and d: d = d.get(path.pop(0)) return d return None def its_path(obj): """ Return a path-string ("/" seperated) for given poser object. Assume a Prop parented to a figures right hand. The props path is: [figurename]/Bodyname/Hipname/.../Forearmname/Handname/Propname This is what a 2DTriangle-prop parented to LaFemme's hand will give: print its_path(SCENE.CurrentActor()) "[LaFemme 1R1]/Body/Hip/Waist/Abdomen/Chest/Right Collar/" +"Right Shoulder/Right Forearm/Right Hand/2DTriangle_1" Figurenames in a path are set in brackets ("[Figurename]"). :return: Path string or None if obj has no path. :rtype: str, None """ ref = list() figure = obj.ItsFigure() if hasattr(obj, "ItsFigure") else SCENE.CurrentFigure() while obj: ref.insert(0, _get_name_(obj)) if hasattr(obj, "Parent"): if _get_name_(obj.Parent()) == "UNIVERSE": ref.insert(0, "[%s]" % _get_name_(obj.ItsFigure())) break if _get_name_(figure) != _get_name_(obj.ItsFigure()): ref.insert(0, "[%s]" % _get_name_(figure)) figure = obj.ItsFigure() obj = obj.Parent() elif hasattr(obj, "Actor"): obj = obj.Actor() else: break return "/".join(ref) def export_UVmap(actorname, imgname, imagesize=(2048, 2048), matlist=None): actor = SCENE.CurrentActor() if actorname is None else _get_actor_(actorname) geom = actor.Geometry() polys = geom.Polygons() tverts = geom.TexVertices() tpolys = geom.TexPolygons() BLACK = (0, 0, 0) WHITE = (255, 255, 255) if not len(tverts): return None img = IMG.new("RGB", imagesize, (255, 255, 255)) draw = ImageDraw.Draw(img) width, height = imagesize def mapped_poly(uv_verts, width=width, height=width): return map(lambda uv: (int((uv[0] % 1.0) * width), int(height - uv[1] * height)), uv_verts) for idx, tpoly in enumerate(tpolys): if matlist and not polys[idx].MaterialName() in matlist: continue draw.polygon(mapped_poly((v.U(), v.V()) for v in tpoly.TexVertices()), fill=WHITE, outline=BLACK) del draw if not os.path.dirname(imgname) == imgname: imgname = os.path.dirname(sys.argv[0]) + os.sep + imgname print(os.path.abspath(imgname)) img.save(imgname) try: img.close() except: pass def create_morph_from_UV(morph, imagefile, grayscale_factor=1.0 / 300000): """ Creates a morph based on a bitmap. Bitmap is converted to grayscale before used. Each pixel from image at UV-Vertex-Position is divided by <float>grayscale_factor (actually delta offset or morph intensity). :param morph: str|morph-param :param imagefile: str :param reverse: bool :param grayscale_factor: float :return: morph """ if isinstance(imagefile, IMG.Image): image = imagefile else: assert os.path.isfile(imagefile) image = IMG.open(imagefile).convert("L") width, height = image.size[0] - 1, image.size[1] - 1 if isinstance(morph, basestring): morph = get_morph(morph) or create_morphtarget(SCENE.CurrentActor(), morph, False) assert is_morph(morph) clear_morph(morph) actor = morph.Actor() geom = actor.Geometry() nrmls = geom.Normals() sets = geom.Sets() seen = set() for tpoly in geom.TexPolygons(): start_idx = tpoly.Start() for idx, tp in enumerate(tpoly.TexVertices()): vertex_idx = sets[start_idx + idx] if vertex_idx not in seen: seen.add(vertex_idx) c = image.getpixel(((width * tp.U()) % width, (height - height * tp.V()) % height) ) * grayscale_factor n = nrmls[vertex_idx] morph.SetMorphTargetDelta(vertex_idx, c * n.X(), c * n.Y(), c * n.Z()) del image return morph morph_from_image = create_morph_from_UV def create_weightmap_from_UV(actorname, weightmapname, imagefile, poserfile, grayscale_factor=1.0 / 300000): """ Creates a weightmap based on a bitmap. Bitmap is converted to grayscale before used. Each pixel from image at UV-Vertex-Position is divided by <float>grayscale_factor (actually delta offset or morph intensity). :param actorname: str, poser.ActorType :param weightmapname: str, weightmap name :param imagefile: str :param reverse: bool :param grayscale_factor: float :return: morph """ __poserheader__ = """{ version { number 10 } actor %s { weightMap %s { """ % (actorname, weightmapname) if isinstance(imagefile, IMG.Image): image = imagefile else: assert os.path.isfile(imagefile) image = IMG.open(imagefile).convert("L") width, height = image.size[0] - 1, image.size[1] - 1 actor = _get_actor_(actorname) geom = actor.Geometry() sets = geom.Sets() seen = set() wm_ar = list() for tpoly in geom.TexPolygons(): start_idx = tpoly.Start() for idx, tp in enumerate(tpoly.TexVertices()): vertex_idx = sets[start_idx + idx] if vertex_idx not in seen: seen.add(vertex_idx) wm_ar.append(image.getpixel(((width * tp.U()) % width, (height - height * tp.V()) % height) ) * grayscale_factor ) with open(poserfile, "w") as fh: print(__poserheader__, file=fh) print("\t\t\tnumbVerts\t", geom.NumVertices(), file=fh) for idx, v in wm_ar: print("\t\t\tv {} {}".format(idx, v)) print("\t\t\n}" "\t\n}" "}", file=fh) del image weightmap_from_image = create_weightmap_from_UV # **************************************************************************** # wx routines # **************************************************************************** import wx from wx.lib.scrolledpanel import ScrolledPanel class _SelectDialog(wx.Dialog): """ Used by function SelectDialog([...]) Set up a dialog to choose or check from a list of items. Parameters: select_ctrl: wx.CheckBox or wx.RadioButton (choose or check). labels: List of <str> labels to choose from. header: Text displayed above message. footer: Text displayed below message. """ def __init__(self, select_ctrl=wx.CheckBox, *args, **kwargs): title = kwargs.pop("title", kwargs.pop("Title", "Select Dialog")) self.labels = kwargs.pop("labels", kwargs.pop("Labels", list())) self.header = kwargs.pop("header", kwargs.pop("Header", None)) self.footer = kwargs.pop("footer", kwargs.pop("Footer", None)) # List of generated controls (checkboxes or radiobuttons) self.ctrls = [] # Type of controls used (check/radio) self.select_ctrl = select_ctrl # Close dialog first time a selection changed (click)? # (Makes sense for Radiobuttons) self.closeOnClick = kwargs.pop("closeOnClick", False) wx.Dialog.__init__(self, *args, title=title, **kwargs) self.InitUI() def InitUI(self): sizer = wx.BoxSizer(wx.VERTICAL) btn_sizer = wx.StdDialogButtonSizer() if not self.closeOnClick: # No OK button needed if we close automatically. btn_ok = wx.Button(self, id=wx.ID_OK) btn_ok.SetDefault() btn_sizer.AddButton(btn_ok) btn_close = wx.Button(self, id=wx.ID_CANCEL) btn_close.SetDefault() btn_sizer.AddButton(btn_close) btn_sizer.Realize() font = self.GetFont() if self.header is not None: if isinstance(self.header, wx.Panel): # if header is a wx.Window, add it as is to sizer. h = self.header h.SetFont(font) elif isinstance(self.header, basestring): # Some Text displayed above of selection-list. h = wx.StaticText(self, label=self.header) h.SetFont(font) h.SetFont(font.Bold()) else: raise TypeError("'Header'") sizer.Add((1, 10), 0) sizer.Add(h, 0, wx.ALIGN_CENTER_HORIZONTAL) sizer.Add((1, 16), 0) # Get a scrolled panel to be able to display long lists too. panel = ScrolledPanel(self, size=self.GetSize(), style=wx.SIMPLE_BORDER) panel.SetupScrolling() ctrl_sizer = wx.BoxSizer(wx.VERTICAL) for idx, entry in enumerate(self.labels): # Add the labels we need to show. # Each entry in self.labels is a tuple # of (<string label>, <bool status>, <bool flag>). if len(entry) == 3: label, state, disabled = entry state = bool(state) if state is not None else False else: label = entry state, disabled = True, False self.ctrls.append(self.select_ctrl(panel, label=label)) self.ctrls[idx].index = idx self.ctrls[idx].state = state self.ctrls[idx].Enable(not disabled) self.ctrls[idx].SetValue(state) ctrl_sizer.Add(self.ctrls[idx], 0, wx.ALIGN_LEFT | wx.EXPAND) # Add the sizer containing the selection-controls to # the scrollable panel. panel.SetSizer(ctrl_sizer) if len(self.ctrls) and not isinstance(self.ctrls[0], wx.RadioButton): # If we have a list of checkboxes, we need to have # a "Select All" and "Deselect All" Button. btn_sizer2 = wx.BoxSizer(wx.HORIZONTAL) font = self.GetFont() font.SetPointSize(8) for (_label, _id) in (("Select All", wx.ID_SELECTALL), ("Deselect All", wx.NewId())): btn = wx.Button(self, id=_id, label=_label) btn.SetFont(font) btn.Bind(wx.EVT_BUTTON, self.onSelectButton) btn_sizer2.Add(btn, 0, wx.ALIGN_RIGHT, 10) sizer.Add(btn_sizer2, 0, wx.ALIGN_RIGHT) sizer.Add(panel, 1, wx.EXPAND) if self.footer is not None: if isinstance(self.footer, wx.Panel): h = self.footer h.SetFont(font) else: # Some Text displayed below of selection-list. h = wx.StaticText(self, label=self.footer) h.SetFont(font) h.SetFont(font.Bold()) sizer.Add((1, 16), 0) sizer.Add(h, 0, wx.ALIGN_CENTER_HORIZONTAL) sizer.Add((1, 16), 0) # Add OK and Cancel buttons last. sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALIGN_RIGHT) self.SetSizer(sizer) sizer.Fit(self) if self.closeOnClick: # Make sure we get informed if a control is clicked. self.Bind(wx.EVT_RADIOBUTTON, self.onClick) def onClick(self, ev): # Send EndModal-Event after the current event # is completly processed (to avoid a crash :) ). wx.CallAfter(self.EndModal(wx.ID_OK)) def onSelectButton(self, ev): obj = ev.GetEventObject() for ctrl in self.ctrls: ctrl.SetValue(obj.GetId() == wx.ID_SELECTALL) def SelectDialog(labels, **kwargs): """ Display a Selection-Dialog. :param labels: List of Strings to display in selectionlist. :param kwargs: <dict> with keywords/values: control: <wx.CheckBox> or <wx.RadioButton> (choose or check) closeOnClick: <bool>, close dialog after an item is clicked. :return: List of <str>item-names for each selected item from <list>labels. E.g.: sel = SelectDialog(["AAA", "BBB", "CCC"], wx.RadioButton, True) print("Result:", sel) # Returned list is type <namedtuple> with each item as (<str>Name, <int>Value). print("Names only:", [s.Name for s in sel]) """ assert is_iterable(labels) control = kwargs.pop("control", None) if control is None: control = wx.CheckBox if callable(control): control = control.__class__ if isinstance(control, str): if control.lower().startswith("radio"): control = wx.RadioButton elif control.lower().startswith("check"): control = wx.CheckBox if control is wx.RadioButton and not "closeOnClick" in kwargs: # Make "closeOnClick" default True. kwargs["closeOnClick"] = True man = poser.WxAuiManager() root = man.GetManagedWindow() style = kwargs.pop("style", 0) with _SelectDialog(parent=root, select_ctrl=control, style=wx.DEFAULT_DIALOG_STYLE | style, labels=labels, **kwargs) as dlg: res = dlg.ShowModal() if res == wx.ID_OK: # Return a list of named tuples (<str label>, <int value>) # with each list item selected. List may be empty. return [labels[i] for i, c in enumerate(dlg.ctrls) if c.GetValue() != 0] return None def wx_combine_morphs(targetmorph, whole_figure=False): if isinstance(targetmorph, basestring): targetmorph = create_morphtarget(SCENE.CurrentActor(), targetmorph, True) else: assert is_morph(targetmorph) targetmorph_name = targetmorph.Name() targetactor = targetmorph.Actor() selected_morphnames = SelectDialog( sorted(m.Name() for m in yield_morphs(targetactor)), control="check", title="Combine: Morph Selection", header="Select some morphs to combine in Actor {}.".format(targetactor.Name()), footer="(Only morphs with Deltas are shown)" ) if not selected_morphnames: return if not whole_figure: merge_morphs(targetmorph, selected_morphnames) else: global valueparm_actors figure = targetactor.ItsFigure() actor_list = set() for actor in (a for a in figure.Actors() if a.IsBodyPart()): if actor.InternalName().startswith("BODY"): continue hitlist = [n for n in selected_morphnames if actor.Parameter(n)] if len(hitlist): # we are here because at least one morph to combine is in hitlist. merge_morphs(create_morphtarget(actor, targetmorph_name), hitlist) actor_list.add(actor) # To avoid problems we set morph-values after all morphs are combined. body_actor = figure.Actor("Body") body_parm = body_actor.Parameter(targetmorph_name) if body_parm is None: body_parm = body_actor.CreateValueParameter(targetmorph_name) body_parm.SetValue(1) for actor in actor_list: for name in [n for n in selected_morphnames if actor.Parameter(n)]: parm = actor.Parameter(name) if parm: for vop in parm.ValueOperations() or []: vop.SourceParameter().SetValue(0) # Add new created morph to master-dial. parm = actor.Parameter(targetmorph_name) parm.SetValue(0) parm.AddValueOperation(poser.kValueOpTypeCodeDELTAADD, body_parm)