diff --git a/gt/__init__.py b/gt/__init__.py index a2c345c2..b4838b10 100644 --- a/gt/__init__.py +++ b/gt/__init__.py @@ -1,7 +1,7 @@ import sys # Package Variables -__version_tuple__ = (3, 2, 0) +__version_tuple__ = (3, 2, 1) __version_suffix__ = '' __version__ = '.'.join(str(n) for n in __version_tuple__) + __version_suffix__ __authors__ = ['Guilherme Trevisan'] diff --git a/gt/utils/attr_utils.py b/gt/utils/attr_utils.py index 1e8713fb..3e471610 100644 --- a/gt/utils/attr_utils.py +++ b/gt/utils/attr_utils.py @@ -494,17 +494,19 @@ def get_multiple_attr(attribute_path=None, obj_list=None, attr_list=None, enum_a return attribute_values -def get_trs_attr_as_list(obj): +def get_trs_attr_as_list(obj, verbose=True): """ Gets Translate, Rotation and Scale values as a list Args: obj (str): Name of the source object + verbose (bool, optional): If active, it will return a warning when the object is missing. Returns: list or None: A list with TRS values in order [TX, TY, TZ, RX, RY, RZ, SX, SY, SZ], None if missing object. e.g. [0, 0, 0, 15, 15, 15, 1, 1, 1] """ if not obj or not cmds.objExists(obj): - logger.warning(f'Unable to get TRS channels as list. Unable to find object "{obj}".') + if verbose: + logger.warning(f'Unable to get TRS channels as list. Unable to find object "{obj}".') return output = [] for channel in DEFAULT_CHANNELS: # TRS diff --git a/gt/utils/cleanup_utils.py b/gt/utils/cleanup_utils.py index 9f481b3a..a7608a4a 100644 --- a/gt/utils/cleanup_utils.py +++ b/gt/utils/cleanup_utils.py @@ -13,59 +13,74 @@ logger.setLevel(logging.INFO) -def delete_unused_nodes(): +def delete_unused_nodes(verbose=True): """ Deleted unused nodes (such as materials not connected to anything or nodes without any connections) This is done through a Maya MEL function "MLdeleteUnused()" but it's called here again for better feedback. + Args: + verbose (bool, optional): If True, it will print feedback with the number of unused deleted nodes. + Returns: + int: Number of unused deleted nodes. """ num_deleted_nodes = mel.eval('MLdeleteUnused();') - feedback = FeedbackMessage(quantity=num_deleted_nodes, - singular='unused node was', - plural='unused nodes were', - conclusion='deleted.', - zero_overwrite_message='No unused nodes found in this scene.') - feedback.print_inview_message() + if verbose: + feedback = FeedbackMessage(quantity=num_deleted_nodes, + singular='unused node was', + plural='unused nodes were', + conclusion='deleted.', + zero_overwrite_message='No unused nodes found in this scene.') + feedback.print_inview_message() + return num_deleted_nodes -def delete_nucleus_nodes(): - """ Deletes all elements related to particles """ +def delete_nucleus_nodes(verbose=True, include_fields=True): + """ + Deletes all elements related to particles. + Args: + verbose (bool, optional): If True, it will print feedback with the number of deleted nodes. + include_fields (bool, optional): If True, it will also count field as "nucleus nodes" to be deleted. + Returns: + int: Number of nucleus deleted nodes. + """ errors = '' function_name = 'Delete Nucleus Nodes' + deleted_counter = 0 try: cmds.undoInfo(openChunk=True, chunkName=function_name) - # Without Transform - emitters = cmds.ls(typ='pointEmitter') - solvers = cmds.ls(typ='nucleus') - instancers = cmds.ls(typ='instancer') - - no_transforms = emitters + instancers + solvers + instancers + # Without Transform Types + no_transform_types = ['nucleus', + 'pointEmitter', + 'instancer'] + # Fields/Solvers Types + if include_fields: + field_types = ['airField', + 'dragField', + 'newtonField', + 'radialField', + 'turbulenceField', + 'uniformField', + 'vortexField', + 'volumeAxisField'] + no_transform_types += field_types + no_transforms = [] + for node_type in no_transform_types: + no_transforms += cmds.ls(typ=node_type) or [] # With Transform - nparticle_nodes = cmds.ls(typ='nParticle') - spring_nodes = cmds.ls(typ='spring') - particle_nodes = cmds.ls(typ='particle') - nrigid_nodes = cmds.ls(typ='nRigid') - ncloth_nodes = cmds.ls(typ='nCloth') - pfxhair_nodes = cmds.ls(typ='pfxHair') - hair_nodes = cmds.ls(typ='hairSystem') - nconstraint_nodes = cmds.ls(typ='dynamicConstraint') - - transforms = nparticle_nodes + spring_nodes + particle_nodes + nrigid_nodes - transforms += ncloth_nodes + pfxhair_nodes + hair_nodes + nconstraint_nodes - - # Fields/Solvers Types - # airField - # dragField - # newtonField - # radialField - # turbulenceField - # uniformField - # vortexField - # volumeAxisField - - deleted_counter = 0 - for obj in transforms: + with_transform_types = ['nParticle', + 'spring', + 'particle', + 'nRigid', + 'nCloth', + 'pfxHair', + 'hairSystem', + 'dynamicConstraint'] + with_transforms = [] + for transform_node_type in with_transform_types: + with_transforms += cmds.ls(typ=transform_node_type) or [] + + for obj in with_transforms: try: parent = cmds.listRelatives(obj, parent=True) or [] cmds.delete(parent[0]) @@ -78,13 +93,13 @@ def delete_nucleus_nodes(): deleted_counter += 1 except Exception as e: logger.debug(str(e)) - - feedback = FeedbackMessage(quantity=deleted_counter, - singular='object was', - plural='objects were', - conclusion='deleted.', - zero_overwrite_message='No nucleus nodes found in this scene.') - feedback.print_inview_message() + if verbose: + feedback = FeedbackMessage(quantity=deleted_counter, + singular='object was', + plural='objects were', + conclusion='deleted.', + zero_overwrite_message='No nucleus nodes found in this scene.') + feedback.print_inview_message() except Exception as e: errors += str(e) + '\n' @@ -94,32 +109,50 @@ def delete_nucleus_nodes(): if errors != '': print('######## Errors: ########') print(errors) + return deleted_counter -def delete_all_locators(): - """ Deletes all locators """ +def delete_locators(verbose=True, filter_str=None): + """ + Deletes all locators + Args: + verbose (bool, optional): If True, it will print feedback when executing operation. + filter_str (str, optional): If provided, it will be used to filter locators. + Only locators containing this string will be deleted. + Returns: + int: Number of deleted locators + """ errors = '' - function_name = 'Delete All Locators' + function_name = 'Delete Locators' + deleted_counter = 0 try: cmds.undoInfo(openChunk=True, chunkName=function_name) # With Transform locators = cmds.ls(typ='locator') - deleted_counter = 0 - for obj in locators: + filtered_locators = [] + if filter_str and isinstance(filter_str, str): + for loc in locators: + if filter_str in loc: + filtered_locators.append(loc) + else: + filtered_locators = locators + + for obj in filtered_locators: try: - parent = cmds.listRelatives(obj, parent=True) or [] - cmds.delete(parent[0]) + loc_transform = cmds.listRelatives(obj, parent=True) or [] + cmds.delete(loc_transform[0]) deleted_counter += 1 except Exception as e: logger.debug(str(e)) - feedback = FeedbackMessage(quantity=deleted_counter, - singular='locator was', - plural='locators were', - conclusion='deleted.', - zero_overwrite_message='No locators found in this scene.') - feedback.print_inview_message() + if verbose: + feedback = FeedbackMessage(quantity=deleted_counter, + singular='locator was', + plural='locators were', + conclusion='deleted.', + zero_overwrite_message='No locators found in this scene.') + feedback.print_inview_message() except Exception as e: errors += str(e) + '\n' @@ -129,4 +162,8 @@ def delete_all_locators(): if errors != '': print('######## Errors: ########') print(errors) + return deleted_counter + +if __name__ == "__main__": + logger.setLevel(logging.DEBUG) diff --git a/gt/utils/color_utils.py b/gt/utils/color_utils.py index a5421743..ab5f00e4 100644 --- a/gt/utils/color_utils.py +++ b/gt/utils/color_utils.py @@ -17,6 +17,7 @@ def __init__(self): """ Constant tuple RGB values used for element colors. """ + CENTER = (1, 1, 0.65) LEFT = (0, 0.5, 1) RIGHT = (1, 0.5, 0.5) @@ -61,7 +62,9 @@ def set_color_override_outliner(obj, rgb_color=(1, 1, 1)): def add_side_color_setup(obj, color_attr_name="autoColor", - left_clr=ColorConstants.LEFT, right_clr=ColorConstants.RIGHT): + clr_default=ColorConstants.CENTER, + clr_left=ColorConstants.LEFT, + clr_right=ColorConstants.RIGHT): """ This function sets up a side color setup for the specified object in the Maya scene. It creates connections and attributes to control the color of the object based on its position in the scene. @@ -69,8 +72,9 @@ def add_side_color_setup(obj, color_attr_name="autoColor", Parameters: obj (str): The name of the object to set up the color for. color_attr_name (str, optional): Name of the attribute used to determine if auto color is active or not. - left_clr (tuple, optional): The RGB color values for the left side of the object. Default is (0, 0.5, 1). - right_clr (tuple, optional): The RGB color values for the right side of the object. Default is (1, 0.5, 0.5). + clr_default (tuple, optional): The RGB color for when the object is in the center or not automatically defined. + clr_left (tuple, optional): The RGB color for when on the left side. e.g. (0, 0.5, 1). + clr_right (tuple, optional): The RGB color for when on the right side. e.g.(1, 0.5, 0.5). Example: # Example usage in Maya Python script editor: @@ -80,7 +84,6 @@ def add_side_color_setup(obj, color_attr_name="autoColor", return # Setup Base Connections - default_clr = (1, 1, 0.65) cmds.setAttr(obj + ".overrideEnabled", 1) cmds.setAttr(obj + ".overrideRGBColors", 1) clr_side_condition = cmds.createNode("condition", name=obj + "_clr_side_condition") @@ -103,26 +106,26 @@ def add_side_color_setup(obj, color_attr_name="autoColor", # Setup Color Attributes clr_attr = "colorDefault" add_attr_double_three(obj, clr_attr, keyable=False) - cmds.setAttr(f'{obj}.{clr_attr}R', default_clr[0]) - cmds.setAttr(f'{obj}.{clr_attr}G', default_clr[1]) - cmds.setAttr(f'{obj}.{clr_attr}B', default_clr[2]) + cmds.setAttr(f'{obj}.{clr_attr}R', clr_default[0]) + cmds.setAttr(f'{obj}.{clr_attr}G', clr_default[1]) + cmds.setAttr(f'{obj}.{clr_attr}B', clr_default[2]) cmds.connectAttr(f'{obj}.{clr_attr}R', clr_center_condition + ".colorIfTrueR") cmds.connectAttr(f'{obj}.{clr_attr}G', clr_center_condition + ".colorIfTrueG") cmds.connectAttr(f'{obj}.{clr_attr}B', clr_center_condition + ".colorIfTrueB") cmds.connectAttr(f'{obj}.{clr_attr}', clr_auto_blend + ".color2") # Blend node input r_clr_attr = "colorRight" add_attr_double_three(obj, r_clr_attr, keyable=False) - cmds.setAttr(obj + "." + r_clr_attr + "R", left_clr[0]) - cmds.setAttr(obj + "." + r_clr_attr + "G", left_clr[1]) - cmds.setAttr(obj + "." + r_clr_attr + "B", left_clr[2]) + cmds.setAttr(obj + "." + r_clr_attr + "R", clr_left[0]) + cmds.setAttr(obj + "." + r_clr_attr + "G", clr_left[1]) + cmds.setAttr(obj + "." + r_clr_attr + "B", clr_left[2]) cmds.connectAttr(obj + "." + r_clr_attr + "R", clr_side_condition + ".colorIfTrueR") cmds.connectAttr(obj + "." + r_clr_attr + "G", clr_side_condition + ".colorIfTrueG") cmds.connectAttr(obj + "." + r_clr_attr + "B", clr_side_condition + ".colorIfTrueB") l_clr_attr = "colorLeft" add_attr_double_three(obj, l_clr_attr, keyable=False) - cmds.setAttr(obj + "." + l_clr_attr + "R", right_clr[0]) - cmds.setAttr(obj + "." + l_clr_attr + "G", right_clr[1]) - cmds.setAttr(obj + "." + l_clr_attr + "B", right_clr[2]) + cmds.setAttr(obj + "." + l_clr_attr + "R", clr_right[0]) + cmds.setAttr(obj + "." + l_clr_attr + "G", clr_right[1]) + cmds.setAttr(obj + "." + l_clr_attr + "B", clr_right[2]) cmds.connectAttr(obj + "." + l_clr_attr + "R", clr_side_condition + ".colorIfFalseR") cmds.connectAttr(obj + "." + l_clr_attr + "G", clr_side_condition + ".colorIfFalseG") cmds.connectAttr(obj + "." + l_clr_attr + "B", clr_side_condition + ".colorIfFalseB") @@ -130,7 +133,4 @@ def add_side_color_setup(obj, color_attr_name="autoColor", if __name__ == "__main__": logger.setLevel(logging.DEBUG) - from pprint import pprint - out = None - pprint(out) - + add_side_color_setup("pSphere1") diff --git a/gt/utils/data/controls/cluster_driven.py b/gt/utils/data/controls/cluster_driven.py index 598abed2..7b45df43 100644 --- a/gt/utils/data/controls/cluster_driven.py +++ b/gt/utils/data/controls/cluster_driven.py @@ -105,7 +105,7 @@ def create_scalable_one_side_arrow(name='scalable_one_side_arrow', initial_scale def create_scalable_two_sides_arrow(name='scalable_two_sides_arrow', initial_scale=1, - min_scale_apply=False, min_scale=0.01): + min_scale_apply=False, min_scale=0.01): """ Creates a curve in the shape of an arrow and rigs it so when scaling it up the curve doesn't lose its shape. Instead, it scales only in the direction of the arrow heads. Use the "_scaleCtrl" to determine the scale. @@ -197,3 +197,4 @@ def create_scalable_two_sides_arrow(name='scalable_two_sides_arrow', initial_sca logger.setLevel(logging.DEBUG) cmds.file(new=True, force=True) create_scalable_one_side_arrow() + diff --git a/gt/utils/data/controls/slider.py b/gt/utils/data/controls/slider.py index 05b8712f..4560ed44 100644 --- a/gt/utils/data/controls/slider.py +++ b/gt/utils/data/controls/slider.py @@ -32,7 +32,7 @@ def create_slider_squared_one_dimension(name="slider_one_dimension", lock_unused_channels (bool, optional): locks and hides unused channels (TX, TZ, ROT...) Returns: - ctrl_elements: A list with the control name and control group name + ControlData: A tuple with the control name and control offset group name. """ default_ctrl_line_width = 3 @@ -89,7 +89,7 @@ def create_slider_squared_one_dimension(name="slider_one_dimension", cmds.setAttr(ctrl + '.' + attr + ax, lock=True, k=False, channelBox=False) cmds.setAttr(ctrl + '.v', lock=True, k=False, channelBox=False) - return [ctrl, ctrl_grp] + return ControlData(name=ctrl, offset=ctrl_grp) def create_slider_squared_two_dimensions(name="slider_two_dimensions", @@ -107,7 +107,7 @@ def create_slider_squared_two_dimensions(name="slider_two_dimensions", Can be: "right", "left", "bottom" or "up". Returns: - ctrl_elements: A list with the control name and control group name + ControlData: A tuple with the control name and control offset group name. """ default_ctrl_line_width = 3 @@ -198,7 +198,7 @@ def create_slider_squared_two_dimensions(name="slider_two_dimensions", cmds.move(-5, ctrl_bg + '.cv[1:2]', moveY=True, relative=True) cmds.setAttr(ctrl + '.maxTransYLimit', 0) - return [ctrl, ctrl_grp] + return ControlData(name=ctrl, offset=ctrl_grp) def create_sliders_squared_mouth(name="mouth"): @@ -209,8 +209,8 @@ def create_sliders_squared_mouth(name="mouth"): name (str): Name of the mouth group/control. Returns: - control_tuple: A tuple with the parent group name and a list with all generated controls. - E.g. ('eyebrow_gui_grp', ['ctrl_one', 'ctrl_two']) + ControlData: A tuple with the control name, control offset group name and drivers (sliders). + ControlData(name=gui_grp, offset=gui_grp, drivers=controls) """ # Naming ctrl = NamingConstants.Suffix.CTRL @@ -258,36 +258,36 @@ def create_sliders_squared_mouth(name="mouth"): in_out_tongue_ctrl = create_slider_squared_one_dimension(f'inOutTongue_{offset}_{ctrl}', initial_position='top') # TY - cmds.setAttr(mid_upper_lip_ctrl[1] + '.ty', 6) - cmds.setAttr(mid_lower_lip_ctrl[1] + '.ty', -5) - cmds.setAttr(left_upper_outer_lip_ctrl[1] + '.ty', 5) - cmds.setAttr(left_lower_outer_lip_ctrl[1] + '.ty', -4) - cmds.setAttr(left_upper_corner_lip_ctrl[1] + '.ty', 4) - cmds.setAttr(left_lower_corner_lip_ctrl[1] + '.ty', -3) - cmds.setAttr(right_upper_outer_lip_ctrl[1] + '.ty', 5) - cmds.setAttr(right_lower_outer_lip_ctrl[1] + '.ty', -4) - cmds.setAttr(right_upper_corner_lip_ctrl[1] + '.ty', 4) - cmds.setAttr(right_lower_corner_lip_ctrl[1] + '.ty', -3) - cmds.setAttr(main_mouth_offset_ctrl[1] + '.tx', 13) - cmds.setAttr(main_mouth_offset_ctrl[1] + '.ty', -13.8) - cmds.setAttr(in_out_tongue_ctrl[1] + '.ty', -9.5) + cmds.setAttr(mid_upper_lip_ctrl.offset + '.ty', 6) + cmds.setAttr(mid_lower_lip_ctrl.offset + '.ty', -5) + cmds.setAttr(left_upper_outer_lip_ctrl.offset + '.ty', 5) + cmds.setAttr(left_lower_outer_lip_ctrl.offset + '.ty', -4) + cmds.setAttr(left_upper_corner_lip_ctrl.offset + '.ty', 4) + cmds.setAttr(left_lower_corner_lip_ctrl.offset + '.ty', -3) + cmds.setAttr(right_upper_outer_lip_ctrl.offset + '.ty', 5) + cmds.setAttr(right_lower_outer_lip_ctrl.offset + '.ty', -4) + cmds.setAttr(right_upper_corner_lip_ctrl.offset + '.ty', 4) + cmds.setAttr(right_lower_corner_lip_ctrl.offset + '.ty', -3) + cmds.setAttr(main_mouth_offset_ctrl.offset + '.tx', 13) + cmds.setAttr(main_mouth_offset_ctrl.offset + '.ty', -13.8) + cmds.setAttr(in_out_tongue_ctrl.offset + '.ty', -9.5) # TX - cmds.setAttr(left_upper_outer_lip_ctrl[1] + '.tx', 2) - cmds.setAttr(left_lower_outer_lip_ctrl[1] + '.tx', 2) - cmds.setAttr(left_upper_corner_lip_ctrl[1] + '.tx', 4) - cmds.setAttr(left_lower_corner_lip_ctrl[1] + '.tx', 4) - cmds.setAttr(right_upper_outer_lip_ctrl[1] + '.tx', -2) - cmds.setAttr(right_lower_outer_lip_ctrl[1] + '.tx', -2) - cmds.setAttr(right_upper_corner_lip_ctrl[1] + '.tx', -4) - cmds.setAttr(right_lower_corner_lip_ctrl[1] + '.tx', -4) - cmds.setAttr(in_out_tongue_ctrl[1] + '.tx', -13) + cmds.setAttr(left_upper_outer_lip_ctrl.offset + '.tx', 2) + cmds.setAttr(left_lower_outer_lip_ctrl.offset + '.tx', 2) + cmds.setAttr(left_upper_corner_lip_ctrl.offset + '.tx', 4) + cmds.setAttr(left_lower_corner_lip_ctrl.offset + '.tx', 4) + cmds.setAttr(right_upper_outer_lip_ctrl.offset + '.tx', -2) + cmds.setAttr(right_lower_outer_lip_ctrl.offset + '.tx', -2) + cmds.setAttr(right_upper_corner_lip_ctrl.offset + '.tx', -4) + cmds.setAttr(right_lower_corner_lip_ctrl.offset + '.tx', -4) + cmds.setAttr(in_out_tongue_ctrl.offset + '.tx', -13) # Misc - cmds.setAttr(main_mouth_offset_ctrl[1] + '.sx', 0.8) - cmds.setAttr(main_mouth_offset_ctrl[1] + '.sy', 0.8) - cmds.setAttr(main_mouth_offset_ctrl[1] + '.sz', 0.8) - cmds.setAttr(in_out_tongue_ctrl[1] + '.rz', 90) + cmds.setAttr(main_mouth_offset_ctrl.offset + '.sx', 0.8) + cmds.setAttr(main_mouth_offset_ctrl.offset + '.sy', 0.8) + cmds.setAttr(main_mouth_offset_ctrl.offset + '.sz', 0.8) + cmds.setAttr(in_out_tongue_ctrl.offset + '.rz', 90) half_size_ctrls = [left_upper_outer_lip_ctrl, left_lower_outer_lip_ctrl, left_upper_corner_lip_ctrl, left_lower_corner_lip_ctrl, right_upper_outer_lip_ctrl, right_lower_outer_lip_ctrl, @@ -295,9 +295,9 @@ def create_sliders_squared_mouth(name="mouth"): mid_lower_lip_ctrl, in_out_tongue_ctrl] for ctrl in half_size_ctrls: - cmds.setAttr(ctrl[1] + '.sx', 0.5) - cmds.setAttr(ctrl[1] + '.sy', 0.5) - cmds.setAttr(ctrl[1] + '.sz', 0.5) + cmds.setAttr(ctrl.offset + '.sx', 0.5) + cmds.setAttr(ctrl.offset + '.sy', 0.5) + cmds.setAttr(ctrl.offset + '.sz', 0.5) # 2D Controls left_corner_lip_ctrl = create_slider_squared_two_dimensions(f'{left}_cornerLip_{offset}_{ctrl}') @@ -306,14 +306,14 @@ def create_sliders_squared_mouth(name="mouth"): tongue_ctrl = create_slider_squared_two_dimensions(f'tongue_{offset}_{ctrl}') # Inverted Right Controls - cmds.setAttr(right_corner_lip_ctrl[1] + '.ry', 180) + cmds.setAttr(right_corner_lip_ctrl.offset + '.ry', 180) - cmds.setAttr(left_corner_lip_ctrl[1] + '.tx', 12) - cmds.setAttr(right_corner_lip_ctrl[1] + '.tx', -12) - cmds.setAttr(jaw_ctrl[1] + '.ty', -15) - rescale(tongue_ctrl[1], 0.5, freeze=False) - cmds.setAttr(tongue_ctrl[1] + '.ty', -15) - cmds.setAttr(tongue_ctrl[1] + '.tx', -13) + cmds.setAttr(left_corner_lip_ctrl.offset + '.tx', 12) + cmds.setAttr(right_corner_lip_ctrl.offset + '.tx', -12) + cmds.setAttr(jaw_ctrl.offset + '.ty', -15) + rescale(tongue_ctrl.offset, 0.5, freeze=False) + cmds.setAttr(tongue_ctrl.offset + '.ty', -15) + cmds.setAttr(tongue_ctrl.offset + '.tx', -13) # Determine Grp Order controls.append(left_corner_lip_ctrl) @@ -396,16 +396,16 @@ def create_sliders_squared_mouth(name="mouth"): background.append(mouth_bg_crv) for obj in controls: - cmds.parent(obj[1], gui_grp) - if f'{left}_' in obj[0]: - set_color_override_viewport(obj[0], LEFT_CTRL_COLOR) - set_color_override_outliner(obj[1], (0.21, 0.59, 1)) # Soft Blue - elif f'{right}_' in obj[0]: - set_color_override_viewport(obj[0], RIGHT_CTRL_COLOR) - set_color_override_outliner(obj[1], RIGHT_CTRL_COLOR) + cmds.parent(obj.offset, gui_grp) + if f'{left}_' in obj.offset: + set_color_override_viewport(obj.offset, LEFT_CTRL_COLOR) + set_color_override_outliner(obj.offset, (0.21, 0.59, 1)) # Soft Blue + elif f'{right}_' in obj.offset: + set_color_override_viewport(obj.offset, RIGHT_CTRL_COLOR) + set_color_override_outliner(obj.offset, RIGHT_CTRL_COLOR) else: - set_color_override_viewport(obj[0], CENTER_CTRL_COLOR) - set_color_override_outliner(obj[1], CENTER_CTRL_COLOR) + set_color_override_viewport(obj.offset, CENTER_CTRL_COLOR) + set_color_override_outliner(obj.offset, CENTER_CTRL_COLOR) for obj in background: cmds.parent(obj, bg_grp) @@ -416,28 +416,20 @@ def create_sliders_squared_mouth(name="mouth"): set_color_override_outliner(bg_grp, (0, 0, 0)) # Final Color Adjustments - set_color_override_viewport(main_mouth_offset_ctrl[0], (1, 0.35, 0.55)) - set_color_override_viewport(tongue_ctrl[0], (1, 0.35, 0.55)) - set_color_override_viewport(in_out_tongue_ctrl[0], (1, 0.35, 0.55)) - - return gui_grp, controls + set_color_override_viewport(main_mouth_offset_ctrl.offset, (1, 0.35, 0.55)) + set_color_override_viewport(tongue_ctrl.offset, (1, 0.35, 0.55)) + set_color_override_viewport(in_out_tongue_ctrl.offset, (1, 0.35, 0.55)) + cmds.select(clear=True) + return ControlData(name=gui_grp, offset=gui_grp, drivers=controls) -def create_sliders_squared_eyebrows(): +def create_sliders_squared_eyebrows(name="eyebrow"): """ - Dependencies: - rescale() - create_slider_control() - create_2d_slider_control() - create_text() - move_to_origin() - set_color_override_outliner() - set_color_override_viewport() - + Args: + name (str, optional): Prefix for the control group (name of the control) Returns: control_tuple: A tuple with the parent group name and a list with all generated controls. E.g. ('eyebrow_gui_grp', ['ctrl_one', 'ctrl_two']) - """ # Containers controls = [] @@ -452,25 +444,26 @@ def create_sliders_squared_eyebrows(): background.append(eyebrows_crv) # 1D Controls - left_mid_brow_ctrl = create_slider_squared_one_dimension('left_midBrow_offset_{suffix_ctrl}') - left_outer_brow_ctrl = create_slider_squared_one_dimension('left_outerBrow_offset_{suffix_ctrl}') - right_mid_brow_ctrl = create_slider_squared_one_dimension('right_midBrow_offset_{suffix_ctrl}') - right_outer_brow_ctrl = create_slider_squared_one_dimension('right_outerBrow_offset_{suffix_ctrl}') + suffix_ctrl = NamingConstants.Suffix.CTRL + left_mid_brow_ctrl = create_slider_squared_one_dimension(f'left_midBrow_offset_{suffix_ctrl}') + left_outer_brow_ctrl = create_slider_squared_one_dimension(f'left_outerBrow_offset_{suffix_ctrl}') + right_mid_brow_ctrl = create_slider_squared_one_dimension(f'right_midBrow_offset_{suffix_ctrl}') + right_outer_brow_ctrl = create_slider_squared_one_dimension(f'right_outerBrow_offset_{suffix_ctrl}') # TY - cmds.setAttr(left_mid_brow_ctrl[1] + '.tx', 11) - cmds.setAttr(left_outer_brow_ctrl[1] + '.tx', 15) - cmds.setAttr(right_mid_brow_ctrl[1] + '.tx', -11) - cmds.setAttr(right_outer_brow_ctrl[1] + '.tx', -15) + cmds.setAttr(left_mid_brow_ctrl.offset + '.tx', 11) + cmds.setAttr(left_outer_brow_ctrl.offset + '.tx', 15) + cmds.setAttr(right_mid_brow_ctrl.offset + '.tx', -11) + cmds.setAttr(right_outer_brow_ctrl.offset + '.tx', -15) left_inner_brow_ctrl = create_slider_squared_two_dimensions('left_innerBrow_offset_ctrl', ignore_range='right') right_inner_brow_ctrl = create_slider_squared_two_dimensions('right_innerBrow_offset_ctrl', ignore_range='right') # Invert Right Side - cmds.setAttr(right_inner_brow_ctrl[1] + '.ry', 180) + cmds.setAttr(right_inner_brow_ctrl.offset + '.ry', 180) - cmds.setAttr(left_inner_brow_ctrl[1] + '.tx', 7) - cmds.setAttr(right_inner_brow_ctrl[1] + '.tx', -7) + cmds.setAttr(left_inner_brow_ctrl.offset + '.tx', 7) + cmds.setAttr(right_inner_brow_ctrl.offset + '.tx', -7) # Determine Grp Order controls.append(left_inner_brow_ctrl) @@ -511,27 +504,27 @@ def create_sliders_squared_eyebrows(): background.append(r_crv) # Parent Groups - gui_grp = cmds.group(name='eyebrow_gui_grp', world=True, empty=True) - bg_grp = cmds.group(name='eyebrow_background_grp', world=True, empty=True) + gui_grp = cmds.group(name=f'{name}_gui_grp', world=True, empty=True) + bg_grp = cmds.group(name=f'{name}_background_grp', world=True, empty=True) # General Background - eyebrow_bg_crv = cmds.curve(name='eyebrow_bg_crv', p=[[-20.0, 10.0, 0.0], [-20.0, -8.0, 0.0], [20.0, -8.0, 0.0], + eyebrow_bg_crv = cmds.curve(name=f'{name}_bg_crv', p=[[-20.0, 10.0, 0.0], [-20.0, -8.0, 0.0], [20.0, -8.0, 0.0], [20.0, 10.0, 0.0], [-20.0, 10.0, 0.0]], d=1) cmds.setAttr(eyebrow_bg_crv + '.overrideDisplayType', 1) background.append(eyebrow_bg_crv) for obj in controls: - cmds.parent(obj[1], gui_grp) - if 'left_' in obj[0]: - set_color_override_viewport(obj[0], LEFT_CTRL_COLOR) - set_color_override_outliner(obj[1], (0.21, 0.59, 1)) # Soft Blue - elif 'right_' in obj[0]: - set_color_override_viewport(obj[0], RIGHT_CTRL_COLOR) - set_color_override_outliner(obj[1], RIGHT_CTRL_COLOR) + cmds.parent(obj.offset, gui_grp) + if 'left_' in obj.offset: + set_color_override_viewport(obj.offset, LEFT_CTRL_COLOR) + set_color_override_outliner(obj.offset, (0.21, 0.59, 1)) # Soft Blue + elif 'right_' in obj.offset: + set_color_override_viewport(obj.offset, RIGHT_CTRL_COLOR) + set_color_override_outliner(obj.offset, RIGHT_CTRL_COLOR) else: - set_color_override_viewport(obj[0], CENTER_CTRL_COLOR) - set_color_override_outliner(obj[1], CENTER_CTRL_COLOR) + set_color_override_viewport(obj.offset, CENTER_CTRL_COLOR) + set_color_override_outliner(obj.offset, CENTER_CTRL_COLOR) for obj in background: cmds.parent(obj, bg_grp) @@ -540,25 +533,18 @@ def create_sliders_squared_eyebrows(): # Background Group cmds.parent(bg_grp, gui_grp) set_color_override_outliner(bg_grp, (0, 0, 0)) + cmds.select(clear=True) - return gui_grp, controls + return ControlData(name=gui_grp, offset=gui_grp, drivers=controls) -def create_sliders_squared_cheek_nose(): +def create_sliders_squared_cheek_nose(name="cheek_nose"): """ - Dependencies: - rescale() - create_slider_control() - create_2d_slider_control() - create_text() - move_to_origin() - set_color_override_outliner() - set_color_override_viewport() - + Args: + name (str, optional): Prefix for the control group (name of the control) Returns: control_tuple: A tuple with the parent group name and a list with all generated controls. E.g. ('eyebrow_gui_grp', ['ctrl_one', 'ctrl_two']) - """ # Containers controls = [] @@ -619,39 +605,39 @@ def create_sliders_squared_cheek_nose(): cheek_tx = 13.5 cheek_ty = -1 cheek_scale = .75 - cmds.setAttr(left_cheek_ctrl[1] + '.tx', cheek_tx) - cmds.setAttr(right_cheek_ctrl[1] + '.tx', -cheek_tx) - cmds.setAttr(left_cheek_ctrl[1] + '.ty', cheek_ty) - cmds.setAttr(right_cheek_ctrl[1] + '.ty', cheek_ty) - rescale(left_cheek_ctrl[1], cheek_scale, freeze=False) - rescale(right_cheek_ctrl[1], cheek_scale, freeze=False) + cmds.setAttr(left_cheek_ctrl.offset + '.tx', cheek_tx) + cmds.setAttr(right_cheek_ctrl.offset + '.tx', -cheek_tx) + cmds.setAttr(left_cheek_ctrl.offset + '.ty', cheek_ty) + cmds.setAttr(right_cheek_ctrl.offset + '.ty', cheek_ty) + rescale(left_cheek_ctrl.offset, cheek_scale, freeze=False) + rescale(right_cheek_ctrl.offset, cheek_scale, freeze=False) nose_tx = 2.5 nose_ty = -3 nose_scale = .25 - cmds.setAttr(left_nose_ctrl[1] + '.tx', nose_tx) - cmds.setAttr(right_nose_ctrl[1] + '.tx', -nose_tx) - cmds.setAttr(left_nose_ctrl[1] + '.ty', nose_ty) - cmds.setAttr(right_nose_ctrl[1] + '.ty', nose_ty) - rescale(left_nose_ctrl[1], nose_scale, freeze=False) - rescale(right_nose_ctrl[1], nose_scale, freeze=False) + cmds.setAttr(left_nose_ctrl.offset + '.tx', nose_tx) + cmds.setAttr(right_nose_ctrl.offset + '.tx', -nose_tx) + cmds.setAttr(left_nose_ctrl.offset + '.ty', nose_ty) + cmds.setAttr(right_nose_ctrl.offset + '.ty', nose_ty) + rescale(left_nose_ctrl.offset, nose_scale, freeze=False) + rescale(right_nose_ctrl.offset, nose_scale, freeze=False) - cmds.setAttr(main_nose_ctrl[1] + '.ty', 1.7) - rescale(main_nose_ctrl[1], .3, freeze=False) + cmds.setAttr(main_nose_ctrl.offset + '.ty', 1.7) + rescale(main_nose_ctrl.offset, .3, freeze=False) cheek_in_out_tx = 7 cheek_in_out_ty = -.1 cheek_in_out_scale = cheek_scale*.8 - cmds.setAttr(left_cheek_in_out_ctrl[1] + '.tx', cheek_in_out_tx) - cmds.setAttr(right_cheek_in_out_ctrl[1] + '.tx', -cheek_in_out_tx) - cmds.setAttr(left_cheek_in_out_ctrl[1] + '.ty', cheek_in_out_ty) - cmds.setAttr(right_cheek_in_out_ctrl[1] + '.ty', cheek_in_out_ty) - rescale(left_cheek_in_out_ctrl[1], cheek_in_out_scale, freeze=False) - rescale(right_cheek_in_out_ctrl[1], cheek_in_out_scale, freeze=False) + cmds.setAttr(left_cheek_in_out_ctrl.offset + '.tx', cheek_in_out_tx) + cmds.setAttr(right_cheek_in_out_ctrl.offset + '.tx', -cheek_in_out_tx) + cmds.setAttr(left_cheek_in_out_ctrl.offset + '.ty', cheek_in_out_ty) + cmds.setAttr(right_cheek_in_out_ctrl.offset + '.ty', cheek_in_out_ty) + rescale(left_cheek_in_out_ctrl.offset, cheek_in_out_scale, freeze=False) + rescale(right_cheek_in_out_ctrl.offset, cheek_in_out_scale, freeze=False) # Invert Right Side for obj in [right_cheek_ctrl, right_nose_ctrl]: - cmds.setAttr(obj[1] + '.sx', cmds.getAttr(obj[1] + '.sx')*-1) + cmds.setAttr(obj.offset + '.sx', cmds.getAttr(obj.offset + '.sx')*-1) # Determine Grp Order controls.append(left_cheek_ctrl) @@ -693,27 +679,27 @@ def create_sliders_squared_cheek_nose(): background.append(r_crv) # Parent Groups - gui_grp = cmds.group(name='cheek_nose_gui_grp', world=True, empty=True) - bg_grp = cmds.group(name='cheek_nose_background_grp', world=True, empty=True) + gui_grp = cmds.group(name=f'{name}_gui_grp', world=True, empty=True) + bg_grp = cmds.group(name=f'{name}_background_grp', world=True, empty=True) # General Background - eyebrow_bg_crv = cmds.curve(name='cheek_nose_bg_crv', p=[[-20.0, 10.0, 0.0], [-20.0, -8.0, 0.0], [20.0, -8.0, 0.0], + eyebrow_bg_crv = cmds.curve(name=f'{name}_bg_crv', p=[[-20.0, 10.0, 0.0], [-20.0, -8.0, 0.0], [20.0, -8.0, 0.0], [20.0, 10.0, 0.0], [-20.0, 10.0, 0.0]], d=1) cmds.setAttr(eyebrow_bg_crv + '.overrideDisplayType', 1) background.append(eyebrow_bg_crv) for obj in controls: - cmds.parent(obj[1], gui_grp) - if 'left_' in obj[0]: - set_color_override_viewport(obj[0], LEFT_CTRL_COLOR) - set_color_override_outliner(obj[1], (0.21, 0.59, 1)) # Soft Blue - elif 'right_' in obj[0]: - set_color_override_viewport(obj[0], RIGHT_CTRL_COLOR) - set_color_override_outliner(obj[1], RIGHT_CTRL_COLOR) + cmds.parent(obj.offset, gui_grp) + if 'left_' in obj.offset: + set_color_override_viewport(obj.offset, LEFT_CTRL_COLOR) + set_color_override_outliner(obj.offset, (0.21, 0.59, 1)) # Soft Blue + elif 'right_' in obj.offset: + set_color_override_viewport(obj.offset, RIGHT_CTRL_COLOR) + set_color_override_outliner(obj.offset, RIGHT_CTRL_COLOR) else: - set_color_override_viewport(obj[0], CENTER_CTRL_COLOR) - set_color_override_outliner(obj[1], CENTER_CTRL_COLOR) + set_color_override_viewport(obj.offset, CENTER_CTRL_COLOR) + set_color_override_outliner(obj.offset, CENTER_CTRL_COLOR) for obj in background: cmds.parent(obj, bg_grp) @@ -722,25 +708,18 @@ def create_sliders_squared_cheek_nose(): # Background Group cmds.parent(bg_grp, gui_grp) set_color_override_outliner(bg_grp, (0, 0, 0)) + cmds.select(clear=True) - return gui_grp, controls + return ControlData(name=gui_grp, offset=gui_grp, drivers=controls) -def create_sliders_squared_eyes(): +def create_sliders_squared_eyes(name="eyes"): """ - Dependencies: - rescale() - create_slider_control() - create_2d_slider_control() - create_text() - move_to_origin() - set_color_override_outliner() - set_color_override_viewport() - + Args: + name (str, optional): Prefix for the control group (name of the control) Returns: control_tuple: A tuple with the parent group name and a list with all generated controls. E.g. ('eyebrow_gui_grp', ['ctrl_one', 'ctrl_two']) - """ # Containers controls = [] @@ -774,26 +753,26 @@ def create_sliders_squared_eyes(): # right_upper_eyelid_ctrl, right_lower_eyelid_ctrl, right_blink_eyelid_ctrl] to_scale_down = [left_blink_eyelid_ctrl, right_blink_eyelid_ctrl] for ctrl in to_scale_down: - cmds.setAttr(ctrl[1] + '.sx', 0.5) - cmds.setAttr(ctrl[1] + '.sy', 0.5) - cmds.setAttr(ctrl[1] + '.sz', 0.5) + cmds.setAttr(ctrl.offset + '.sx', 0.5) + cmds.setAttr(ctrl.offset + '.sy', 0.5) + cmds.setAttr(ctrl.offset + '.sz', 0.5) # TY - rescale(left_upper_eyelid_ctrl[1], 0.25, freeze=False) - rescale(left_lower_eyelid_ctrl[1], 0.25, freeze=False) - cmds.setAttr(left_upper_eyelid_ctrl[1] + '.tx', 15) - cmds.setAttr(left_lower_eyelid_ctrl[1] + '.tx', 15) - cmds.setAttr(left_upper_eyelid_ctrl[1] + '.ty', 3) - cmds.setAttr(left_lower_eyelid_ctrl[1] + '.ty', -4) - cmds.setAttr(left_blink_eyelid_ctrl[1] + '.tx', 5) - - rescale(right_upper_eyelid_ctrl[1], 0.25, freeze=False) - rescale(right_lower_eyelid_ctrl[1], 0.25, freeze=False) - cmds.setAttr(right_upper_eyelid_ctrl[1] + '.tx', -15) - cmds.setAttr(right_lower_eyelid_ctrl[1] + '.tx', -15) - cmds.setAttr(right_upper_eyelid_ctrl[1] + '.ty', 3) - cmds.setAttr(right_lower_eyelid_ctrl[1] + '.ty', -4) - cmds.setAttr(right_blink_eyelid_ctrl[1] + '.tx', -5) + rescale(left_upper_eyelid_ctrl.offset, 0.25, freeze=False) + rescale(left_lower_eyelid_ctrl.offset, 0.25, freeze=False) + cmds.setAttr(left_upper_eyelid_ctrl.offset + '.tx', 15) + cmds.setAttr(left_lower_eyelid_ctrl.offset + '.tx', 15) + cmds.setAttr(left_upper_eyelid_ctrl.offset + '.ty', 3) + cmds.setAttr(left_lower_eyelid_ctrl.offset + '.ty', -4) + cmds.setAttr(left_blink_eyelid_ctrl.offset + '.tx', 5) + + rescale(right_upper_eyelid_ctrl.offset, 0.25, freeze=False) + rescale(right_lower_eyelid_ctrl.offset, 0.25, freeze=False) + cmds.setAttr(right_upper_eyelid_ctrl.offset + '.tx', -15) + cmds.setAttr(right_lower_eyelid_ctrl.offset + '.tx', -15) + cmds.setAttr(right_upper_eyelid_ctrl.offset + '.ty', 3) + cmds.setAttr(right_lower_eyelid_ctrl.offset + '.ty', -4) + cmds.setAttr(right_blink_eyelid_ctrl.offset + '.tx', -5) # Determine Grp Order controls.append(left_upper_eyelid_ctrl) @@ -847,27 +826,27 @@ def create_sliders_squared_eyes(): background.append(right_blink_crv) # Parent Groups - gui_grp = cmds.group(name='eyes_gui_grp', world=True, empty=True) - bg_grp = cmds.group(name='eyes_background_grp', world=True, empty=True) + gui_grp = cmds.group(name=f'{name}_gui_grp', world=True, empty=True) + bg_grp = cmds.group(name=f'{name}_background_grp', world=True, empty=True) # General Background - eyebrow_bg_crv = cmds.curve(name='eyes_bg_crv', p=[[-20.0, 11.0, 0.0], [-20.0, -9.0, 0.0], [20.0, -9.0, 0.0], - [20.0, 11.0, 0.0], [-20.0, 11.0, 0.0]], d=1) + eye_bg_crv = cmds.curve(name=f'{name}_bg_crv', p=[[-20.0, 11.0, 0.0], [-20.0, -9.0, 0.0], [20.0, -9.0, 0.0], + [20.0, 11.0, 0.0], [-20.0, 11.0, 0.0]], d=1) - cmds.setAttr(eyebrow_bg_crv + '.overrideDisplayType', 1) - background.append(eyebrow_bg_crv) + cmds.setAttr(eye_bg_crv + '.overrideDisplayType', 1) + background.append(eye_bg_crv) for obj in controls: - cmds.parent(obj[1], gui_grp) - if 'left_' in obj[0]: - set_color_override_viewport(obj[0], LEFT_CTRL_COLOR) - set_color_override_outliner(obj[1], (0.21, 0.59, 1)) # Soft Blue - elif 'right_' in obj[0]: - set_color_override_viewport(obj[0], RIGHT_CTRL_COLOR) - set_color_override_outliner(obj[1], RIGHT_CTRL_COLOR) + cmds.parent(obj.offset, gui_grp) + if 'left_' in obj.offset: + set_color_override_viewport(obj.offset, LEFT_CTRL_COLOR) + set_color_override_outliner(obj.offset, (0.21, 0.59, 1)) # Soft Blue + elif 'right_' in obj.offset: + set_color_override_viewport(obj.offset, RIGHT_CTRL_COLOR) + set_color_override_outliner(obj.offset, RIGHT_CTRL_COLOR) else: - set_color_override_viewport(obj[0], CENTER_CTRL_COLOR) - set_color_override_outliner(obj[1], CENTER_CTRL_COLOR) + set_color_override_viewport(obj.offset, CENTER_CTRL_COLOR) + set_color_override_outliner(obj.offset, CENTER_CTRL_COLOR) for obj in background: cmds.parent(obj, bg_grp) @@ -876,49 +855,51 @@ def create_sliders_squared_eyes(): # Background Group cmds.parent(bg_grp, gui_grp) set_color_override_outliner(bg_grp, (0, 0, 0)) + cmds.select(clear=True) - return gui_grp, controls + return ControlData(name=gui_grp, offset=gui_grp, drivers=controls) -def create_sliders_squared_facial_side_gui(add_nose_cheeks=True): +def create_sliders_squared_facial_side_gui(name="facial", add_nose_cheeks=True): """ Creates squared sliders for facial controls Args: + name (str, optional): Prefix for the control group (name of the control) add_nose_cheeks (bool): If active, the nose and cheek sliders will be included in the creation. Returns: - ControlData: object containing: name=parent_grp + control_tuple: A tuple with the parent group name and a list with all generated controls. + E.g. ('eyebrow_gui_grp', ['ctrl_one', 'ctrl_two']) """ selection = cmds.ls(selection=True) - parent_grp = cmds.group(empty=True, world=True, name='facial_side_gui_grp') + parent_grp = cmds.group(empty=True, world=True, name=f'{name}_gui_grp') eyebrow_ctrls = create_sliders_squared_eyebrows() eye_ctrls = create_sliders_squared_eyes() mouth_ctrls = create_sliders_squared_mouth() - cmds.move(43, eyebrow_ctrls[0], moveY=True) - cmds.move(23, eye_ctrls[0], moveY=True) - cmds.parent(eyebrow_ctrls[0], parent_grp) - cmds.parent(eye_ctrls[0], parent_grp) - cmds.parent(mouth_ctrls[0], parent_grp) + cmds.move(43, eyebrow_ctrls.name, moveY=True) + cmds.move(23, eye_ctrls.name, moveY=True) + cmds.parent(eyebrow_ctrls.name, parent_grp) + cmds.parent(eye_ctrls.name, parent_grp) + cmds.parent(mouth_ctrls.name, parent_grp) if add_nose_cheeks: nose_cheek_ctrls = create_sliders_squared_cheek_nose() - cmds.parent(nose_cheek_ctrls[0], parent_grp) - cmds.move(22, nose_cheek_ctrls[0], moveY=True) - cmds.move(42, eye_ctrls[0], moveY=True) - cmds.move(62, eyebrow_ctrls[0], moveY=True) + cmds.parent(nose_cheek_ctrls.name, parent_grp) + cmds.move(22, nose_cheek_ctrls.name, moveY=True) + cmds.move(42, eye_ctrls.name, moveY=True) + cmds.move(62, eyebrow_ctrls.name, moveY=True) cmds.select(selection) return ControlData(name=parent_grp) -def _offset_slider_range(create_slider_output, offset_by=5, offset_thickness=0): +def _offset_slider_range(slider_control_data, offset_by=5, offset_thickness=0): """ Offsets the slider range updating its limits and shapes to conform to the new values Args: - create_slider_output (tuple): The tuple output returned from the function "create_slider_control" + slider_control_data (ControlData): The namedtuple output returned from the function "create_slider_control" offset_by: How much to offset, use positive numbers to make it bigger or negative to make it smaller offset_thickness: Amount to update the shape curves, so it continues to look proportional after the offset. - """ - ctrl = create_slider_output[0] - ctrl_grp = create_slider_output[1] + ctrl = slider_control_data.name + ctrl_grp = slider_control_data.offset current_min_trans_y_limit = cmds.getAttr(ctrl + '.minTransYLimit') current_max_trans_y_limit = cmds.getAttr(ctrl + '.maxTransYLimit') @@ -960,4 +941,4 @@ def _offset_slider_range(create_slider_output, offset_by=5, offset_thickness=0): logger.setLevel(logging.DEBUG) # create_facial_side_gui() cmds.file(new=True, force=True) - create_sliders_squared_mouth() + offset_ctrl = create_slider_squared_one_dimension('offset_ctrl') diff --git a/gt/utils/data/curves/_rig_root.crv b/gt/utils/data/curves/_rig_root.crv new file mode 100644 index 00000000..2d634f83 --- /dev/null +++ b/gt/utils/data/curves/_rig_root.crv @@ -0,0 +1,179 @@ +{ + "name": "root", + "transform": null, + "shapes": [ + { + "name": "root_ctrlCircleShape", + "points": [ + [ + -11.7, + 0.0, + 45.484 + ], + [ + -16.907, + 0.0, + 44.279 + ], + [ + -25.594, + 0.0, + 40.072 + ], + [ + -35.492, + 0.0, + 31.953 + ], + [ + -42.968, + 0.0, + 20.627 + ], + [ + -47.157, + 0.0, + 7.511 + ], + [ + -47.209, + 0.0, + -6.195 + ], + [ + -43.776, + 0.0, + -19.451 + ], + [ + -36.112, + 0.0, + -31.134 + ], + [ + -26.009, + 0.0, + -39.961 + ], + [ + -13.56, + 0.0, + -45.63 + ], + [ + 0.0, + 0.0, + -47.66 + ], + [ + 13.56, + 0.0, + -45.63 + ], + [ + 26.009, + 0.0, + -39.961 + ], + [ + 36.112, + 0.0, + -31.134 + ], + [ + 43.776, + 0.0, + -19.451 + ], + [ + 47.209, + 0.0, + -6.195 + ], + [ + 47.157, + 0.0, + 7.511 + ], + [ + 42.968, + 0.0, + 20.627 + ], + [ + 35.492, + 0.0, + 31.953 + ], + [ + 25.594, + 0.0, + 40.072 + ], + [ + 16.907, + 0.0, + 44.279 + ], + [ + 11.7, + 0.0, + 45.484 + ] + ], + "degree": 3, + "knot": null, + "periodic": 0, + "is_bezier": false + }, + { + "name": "root_ctrlArrowShape", + "points": [ + [ + -11.7, + 0.0, + 45.484 + ], + [ + -11.7, + 0.0, + 59.009 + ], + [ + -23.4, + 0.0, + 59.009 + ], + [ + 0.0, + 0.0, + 82.409 + ], + [ + 23.4, + 0.0, + 59.009 + ], + [ + 11.7, + 0.0, + 59.009 + ], + [ + 11.7, + 0.0, + 45.484 + ] + ], + "degree": 1, + "knot": null, + "periodic": 0, + "is_bezier": false + } + ], + "metadata": { + "projectionAxis": "persp", + "projectionScale": 5, + "projectionFit": null + } +} \ No newline at end of file diff --git a/gt/utils/data/py_meshes/preview_images/studio_background.jpg b/gt/utils/data/py_meshes/preview_images/studio_background.jpg new file mode 100644 index 00000000..8c7c93c6 Binary files /dev/null and b/gt/utils/data/py_meshes/preview_images/studio_background.jpg differ diff --git a/gt/utils/data/py_meshes/scale_volume.py b/gt/utils/data/py_meshes/scale_volume.py index cf6693c4..5293f5ca 100644 --- a/gt/utils/data/py_meshes/scale_volume.py +++ b/gt/utils/data/py_meshes/scale_volume.py @@ -1,5 +1,5 @@ """ -Parametric Mesh Creation Scripts (Meshes with Logic or extra components) +Parametric Mesh Creation functions for Scale and Volume meshes (Meshes with Logic or extra components) """ from gt.utils.iterable_utils import round_numbers_in_list from gt.utils.data.py_meshes.mesh_data import MeshData diff --git a/gt/utils/data/py_meshes/scene_setup.py b/gt/utils/data/py_meshes/scene_setup.py new file mode 100644 index 00000000..824893de --- /dev/null +++ b/gt/utils/data/py_meshes/scene_setup.py @@ -0,0 +1,74 @@ +""" +Parametric Mesh Creation for Scene Setup +""" +from gt.utils.data.py_meshes.mesh_data import MeshData +import maya.cmds as cmds +import logging + +# Logging Setup +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def create_studio_background(name="studio_background", initial_scale=1): + """ + Creates a studio background mesh + Args: + name (str, optional): The name for the created mesh. + initial_scale (int, float, optional): Sets the initial scale of the mesh object + """ + selection = cmds.ls(selection=True) + plane_transform, poly_plane_node = cmds.polyPlane(name=name, w=1, h=1, sx=10, sy=10, ax=(0, 1, 0), cuv=2, ch=1) + + # Set attributes for the poly plane + cmds.setAttr(f"{poly_plane_node}.height", 40) + cmds.setAttr(f"{poly_plane_node}.width", 40) + cmds.setAttr(f"{poly_plane_node}.subdivisionsHeight", 50) + cmds.setAttr(f"{poly_plane_node}.subdivisionsWidth", 50) + + cmds.rename(poly_plane_node, f'{name}_polyPlane') + + # Create a bend deformer and set its attributes + bend_node_one, bend_handle_one = cmds.nonLinear(plane_transform, name=f'{name}_bendY', typ="bend", + lowBound=0, highBound=1, curvature=90) + + cmds.rotate(0, -90, 0, bend_handle_one, r=True, os=True, fo=True) + cmds.rotate(0, 0, 90, bend_handle_one, r=True, os=True, fo=True) + + bend_node_two, bend_handle_two = cmds.nonLinear(plane_transform, name=f'{name}_bendZ', typ="bend", + lowBound=-1, highBound=1, curvature=110) + + bend_handles = [bend_handle_one, bend_handle_two] + + cmds.rotate(0, -90, 0, bend_handle_two, r=True, os=True, fo=True) + cmds.rotate(-90, 0, 0, bend_handle_two, r=True, os=True, fo=True) + cmds.move(0, 0, 7, bend_handle_two, r=True) + + cmds.parent([bend_handle_one, bend_handle_two], plane_transform) + + cmds.move(0, 0, -10, plane_transform, r=True) + cmds.xform(plane_transform, piv=(0, 0, 11), ws=True) + cmds.move(0, 0, 0, plane_transform, a=True, rpr=True) # rpr flag moves it according to the pivot + + for handle in bend_handles: + cmds.setAttr(f'{handle}.v', 0) + + cmds.setAttr(f'{plane_transform}.sx', initial_scale) + cmds.setAttr(f'{plane_transform}.sy', initial_scale) + cmds.setAttr(f'{plane_transform}.sz', initial_scale) + + cmds.select(clear=True) + if selection: + try: + cmds.select(selection) + except Exception as e: + logger.debug(f'Unable to recover selection. Issue: {str(e)}') + + return MeshData(name=plane_transform, setup=bend_handles) + + +if __name__ == "__main__": + logger.setLevel(logging.DEBUG) + cmds.file(new=True, force=True) + create_studio_background() diff --git a/gt/utils/data_utils.py b/gt/utils/data_utils.py index 01a1cead..e285a518 100644 --- a/gt/utils/data_utils.py +++ b/gt/utils/data_utils.py @@ -289,6 +289,22 @@ def progress_callback(current_file, total_files): return extracted_files_list +def on_rm_error(func, file_path, exc_info): + """ + Handle an error encountered during file removal. + + Args: + func (callable): The function that raised the exception. + file_path (str): The path of the file that couldn't be removed. + exc_info (tuple): A tuple containing exception information. + """ + logger.debug(f'Function that raised exception: "{str(func)}".') + logger.debug(f'Exception info: "{str(exc_info)}".') + logger.debug(f'An exception was raised during a "rm" (remove) operation. Attempting again with new permissions...') + os.chmod(file_path, stat.S_IWRITE) + os.unlink(file_path) + + def delete_paths(paths): """ Deletes files or folders at the specified paths. @@ -319,7 +335,7 @@ def delete_paths(paths): if os.path.isfile(path): os.remove(path) elif os.path.isdir(path): - shutil.rmtree(path) + shutil.rmtree(path, onerror=on_rm_error) except OSError as e: logger.debug(f'Unable to delete "{path}". Issue: {e}') if not os.path.exists(path): @@ -388,6 +404,3 @@ def make_empty_file(path): if __name__ == "__main__": logger.setLevel(logging.DEBUG) - from pprint import pprint - out = None - pprint(out) diff --git a/gt/utils/display_utils.py b/gt/utils/display_utils.py index 0e00c6e5..c6257236 100644 --- a/gt/utils/display_utils.py +++ b/gt/utils/display_utils.py @@ -4,30 +4,41 @@ github.com/TrevisanGMW/gt-tools """ from gt.utils.feedback_utils import FeedbackMessage +from gt.utils.naming_utils import get_short_name import maya.cmds as cmds import maya.mel as mel import logging import sys # Logging Setup -from gt.utils.naming_utils import get_short_name - logging.basicConfig() logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -def toggle_uniform_lra(): +def toggle_uniform_lra(obj_list=None, verbose=True): """ - Makes the visibility of the Local Rotation Axis uniform among - the selected objects according to the current state of the majority of them. + Makes the visibility of the Local Rotation Axis uniform according to the current state of the majority of them. + + Args: + obj_list (list, str): A list with the name of the objects it should affect. (Strings are converted into list) + verbose (bool, optional): If True, it will return feedback about the operation. + Returns: + bool or None: Current status of the LRA visibility (toggle target) or None if operation failed. """ function_name = 'Uniform LRA Toggle' cmds.undoInfo(openChunk=True, chunkName=function_name) + lra_state_result = None try: errors = '' - selection = cmds.ls(selection=True, long=True) or [] - if not selection: + _target_list = None + if obj_list and isinstance(obj_list, list): + _target_list = obj_list + if obj_list and isinstance(obj_list, str): + _target_list = [obj_list] + if not _target_list: + _target_list = cmds.ls(selection=True, long=True) or [] + if not _target_list: cmds.warning('Select at least one object and try again.') return @@ -35,7 +46,7 @@ def toggle_uniform_lra(): active_lra = [] operation_result = 'off' - for obj in selection: + for obj in _target_list: try: current_lra_state = cmds.getAttr(obj + '.displayLocalAxis') if current_lra_state: @@ -50,12 +61,14 @@ def toggle_uniform_lra(): try: cmds.setAttr(obj + '.displayLocalAxis', 1) operation_result = 'on' + lra_state_result = True except Exception as e: errors += str(e) + '\n' elif len(inactive_lra) == 0: for obj in active_lra: try: cmds.setAttr(obj + '.displayLocalAxis', 0) + lra_state_result = False except Exception as e: errors += str(e) + '\n' elif len(active_lra) > len(inactive_lra): @@ -63,23 +76,25 @@ def toggle_uniform_lra(): try: cmds.setAttr(obj + '.displayLocalAxis', 1) operation_result = 'on' + lra_state_result = True except Exception as e: errors += str(e) + '\n' else: for obj in active_lra: try: cmds.setAttr(obj + '.displayLocalAxis', 0) + lra_state_result = False except Exception as e: errors += str(e) + '\n' - - feedback = FeedbackMessage(intro='LRA Visibility set to:', - conclusion=str(operation_result), - style_conclusion='color:#FF0000;text-decoration:underline;', - zero_overwrite_message='No user defined attributes were deleted.') - feedback.print_inview_message(system_write=False) - sys.stdout.write('\n' + 'Local Rotation Axes Visibility set to: "' + operation_result + '"') - - if errors != '': + if verbose: + feedback = FeedbackMessage(intro='LRA Visibility set to:', + conclusion=str(operation_result), + style_conclusion='color:#FF0000;text-decoration:underline;', + zero_overwrite_message='No user defined attributes were deleted.') + feedback.print_inview_message(system_write=False) + sys.stdout.write('\n' + 'Local Rotation Axes Visibility set to: "' + operation_result + '"') + + if errors != '' and verbose: print('#### Errors: ####') print(errors) cmds.warning("The script couldn't read or write some LRA states. Open script editor for more info.") @@ -87,24 +102,37 @@ def toggle_uniform_lra(): logger.debug(str(e)) finally: cmds.undoInfo(closeChunk=True, chunkName=function_name) + return lra_state_result -def toggle_uniform_jnt_label(): +def toggle_uniform_jnt_label(jnt_list=None, verbose=True): """ Makes the visibility of the Joint Labels uniform according to the current state of the majority of them. + Args: + jnt_list (list, str): A list with the name of the objects it should affect. (Strings are converted into list) + verbose (bool, optional): If True, it will return feedback about the operation. + Returns: + bool or None: Current status of the label visibility (toggle target) or None if operation failed. """ function_name = 'Uniform Joint Label Toggle' cmds.undoInfo(openChunk=True, chunkName=function_name) + label_state = None try: errors = '' - joints = cmds.ls(type='joint', long=True) + _joints = None + if jnt_list and isinstance(jnt_list, list): + _joints = jnt_list + if jnt_list and isinstance(jnt_list, str): + _joints = [jnt_list] + if not _joints: + _joints = cmds.ls(type='joint', long=True) or [] inactive_label = [] active_label = [] operation_result = 'off' - for obj in joints: + for obj in _joints: try: current_label_state = cmds.getAttr(obj + '.drawLabel') if current_label_state: @@ -119,12 +147,14 @@ def toggle_uniform_jnt_label(): try: cmds.setAttr(obj + '.drawLabel', 1) operation_result = 'on' + label_state = True except Exception as e: errors += str(e) + '\n' elif len(inactive_label) == 0: for obj in active_label: try: cmds.setAttr(obj + '.drawLabel', 0) + label_state = False except Exception as e: errors += str(e) + '\n' elif len(active_label) > len(inactive_label): @@ -132,25 +162,27 @@ def toggle_uniform_jnt_label(): try: cmds.setAttr(obj + '.drawLabel', 1) operation_result = 'on' + label_state = True except Exception as e: errors += str(e) + '\n' else: for obj in active_label: try: cmds.setAttr(obj + '.drawLabel', 0) + label_state = False except Exception as e: errors += str(e) + '\n' - - feedback = FeedbackMessage(quantity=len(joints), - skip_quantity_print=True, - intro='Joint Label Visibility set to:', - conclusion=str(operation_result), - style_conclusion="color:#FF0000;text-decoration:underline;", - zero_overwrite_message='No joints found in this scene.') - feedback.print_inview_message(system_write=False) - sys.stdout.write('\n' + 'Joint Label Visibility set to: "' + operation_result + '"') - - if errors != '': + if verbose: + feedback = FeedbackMessage(quantity=len(_joints), + skip_quantity_print=True, + intro='Joint Label Visibility set to:', + conclusion=str(operation_result), + style_conclusion="color:#FF0000;text-decoration:underline;", + zero_overwrite_message='No joints found in this scene.') + feedback.print_inview_message(system_write=False) + sys.stdout.write('\n' + 'Joint Label Visibility set to: "' + operation_result + '"') + + if errors != '' and verbose: print('#### Errors: ####') print(errors) cmds.warning("The script couldn't read or write some \"drawLabel\" states. " @@ -159,10 +191,17 @@ def toggle_uniform_jnt_label(): logger.debug(str(e)) finally: cmds.undoInfo(closeChunk=True, chunkName=function_name) + return label_state -def toggle_full_hud(): - """ Toggles common HUD options so all the common ones are either active or inactive """ +def toggle_full_hud(verbose=True): + """ + Toggles common HUD options so all the common ones are either active or inactive + Args: + verbose (bool, optional): If True, it will return feedback about the operation. + Returns: + bool or None: Current status of the hud visibility (toggle target) or None if operation failed. + """ hud_current_state = {} # 1 - Animation Details @@ -287,35 +326,47 @@ def toggle_full_hud(): mel.eval('setViewportRendererVisibility(false)') mel.eval('catchQuiet(setXGenHUDVisibility(false));') # Default states are preserved: camera names, caps lock, symmetry, view axis, in-view messages and in-view editor - print("?") # Give feedback operation_result = 'off' if toggle: operation_result = 'on' - feedback = FeedbackMessage(intro='Hud Visibility set to:', - conclusion=str(operation_result), - style_conclusion='color:#FF0000;text-decoration:underline;', - zero_overwrite_message='No user defined attributes were deleted.') - feedback.print_inview_message(system_write=False) - sys.stdout.write('\n' + 'Hud Visibility set to: "' + operation_result + '"') + if verbose: + feedback = FeedbackMessage(intro='Hud Visibility set to:', + conclusion=str(operation_result), + style_conclusion='color:#FF0000;text-decoration:underline;', + zero_overwrite_message='No user defined attributes were deleted.') + feedback.print_inview_message(system_write=False) + sys.stdout.write('\n' + 'Hud Visibility set to: "' + operation_result + '"') + return toggle -def set_joint_name_as_label(): +def set_joint_name_as_label(jnt_list=None, verbose=True): """ - Transfer the selected joint name to + Transfer the name of the joint to its label. + Args: + jnt_list (list, str): A list with the name of the objects it should affect. (Strings are converted into list) + verbose (bool, optional): If True, it will return feedback about the operation. + Returns: + int: Number of affected joints. """ - - selection_joints = cmds.ls(selection=True, typ="joint") or [] - - if not selection_joints: - cmds.warning("No joints found in selection. Select joints and try again.") + _joints = None + if jnt_list and isinstance(jnt_list, list): + _joints = jnt_list + if jnt_list and isinstance(jnt_list, str): + _joints = [jnt_list] + if not _joints: + _joints = cmds.ls(selection=True, typ="joint") or [] + + if not _joints: + if verbose: + cmds.warning("No joints found in selection. Select joints and try again.") return - function_name = 'GTU Set Joint Name as Label' + function_name = 'Set Joint Name as Label' counter = 0 cmds.undoInfo(openChunk=True, chunkName=function_name) try: - for joint in selection_joints: + for joint in _joints: short_name = get_short_name(joint) cmds.setAttr(joint + '.side', 0) # Center (No Extra String) cmds.setAttr(joint + '.type', 18) # Other @@ -325,47 +376,71 @@ def set_joint_name_as_label(): cmds.warning(str(e)) finally: cmds.undoInfo(closeChunk=True, chunkName=function_name) - - feedback = FeedbackMessage(quantity=counter, - singular='label was', - plural='labels were', - conclusion='updated.', - zero_overwrite_message='No labels were updated.') - feedback.print_inview_message() + if verbose: + feedback = FeedbackMessage(quantity=counter, + singular='label was', + plural='labels were', + conclusion='updated.', + zero_overwrite_message='No labels were updated.') + feedback.print_inview_message() + return counter -def generate_udim_previews(): - """ Generates UDIM previews for all file nodes """ +def generate_udim_previews(verbose=True): + """ + Generates UDIM previews for all file nodes + Args: + verbose (bool, optional): If True, it will return feedback about the operation. + Returns: + int: Number of affected file nodes. + """ errors = '' + counter = 0 all_file_nodes = cmds.ls(type='file') for file_node in all_file_nodes: try: mel.eval('generateUvTilePreview ' + file_node + ';') + counter += 1 except Exception as e: errors += str(e) + '\n' - if errors: + if errors and verbose: print(('#' * 50) + '\n') print(errors) print('#' * 50) - feedback = FeedbackMessage(prefix='Previews generated for all', - intro='UDIM', - style_intro='color:#FF0000;text-decoration:underline;', - conclusion='file nodes.') - feedback.print_inview_message() + if verbose: + feedback = FeedbackMessage(prefix='Previews generated for all', + intro='UDIM', + style_intro='color:#FF0000;text-decoration:underline;', + conclusion='file nodes.') + feedback.print_inview_message() + return counter -def reset_joint_display(): +def reset_joint_display(jnt_list=None, verbose=True): """ - Resets the radius and drawStyle attributes for all joints, + Resets the radius and drawStyle attributes for provided joints. (or all joints) then changes the global multiplier (jointDisplayScale) back to one + + Args: + jnt_list (list, str): A list with the name of the objects it should affect. (Strings are converted into list) + verbose (bool, optional): If True, it will return feedback about the operation. + Returns: + int: Number of affected joints. """ errors = '' target_radius = 1 counter = 0 - all_joints = cmds.ls(type='joint', long=True) - all_joints_short = cmds.ls(type='joint') - for obj in all_joints: + + _joints = None + if jnt_list and isinstance(jnt_list, list): + _joints = jnt_list + if jnt_list and isinstance(jnt_list, str): + _joints = [jnt_list] + if not _joints: + _joints = cmds.ls(type='joint', long=True) or [] + # Update joints + for obj in _joints: try: if cmds.objExists(obj): if cmds.getAttr(obj + ".radius", lock=True) is False: @@ -381,49 +456,68 @@ def reset_joint_display(): logger.debug(str(exception)) errors += str(exception) + '\n' cmds.jointDisplayScale(target_radius) - - feedback = FeedbackMessage(quantity=counter, - singular='joint had its', - plural='joints had their', - conclusion='display reset.', - zero_overwrite_message='No joints found in this scene.') - feedback.print_inview_message(system_write=False) - feedback.conclusion = '"radius", "drawStyle" and "visibility" attributes reset.' - sys.stdout.write(f'\n{feedback.get_string_message()}') - - if errors: + # Give feedback + if verbose: + feedback = FeedbackMessage(quantity=counter, + singular='joint had its', + plural='joints had their', + conclusion='display reset.', + zero_overwrite_message='No joints found in this scene.') + feedback.print_inview_message(system_write=False) + feedback.conclusion = '"radius", "drawStyle" and "visibility" attributes reset.' + sys.stdout.write(f'\n{feedback.get_string_message()}') + # Print errors + if errors and verbose: print(('#' * 50) + '\n') print(errors) print('#' * 50) cmds.warning('A few joints were not fully reset. Open script editor for more details.') + return counter + +def delete_display_layers(layer_list=None, verbose=True): + """ + Deletes provided (or all) display layers -def delete_display_layers(): - """ Deletes all display layers """ + Args: + layer_list (list, str): A list with the name of the layers it should affect. (Strings are converted into list) + verbose (bool, optional): If True, it will return feedback about the operation. + Returns: + int: Number of affected joints. + """ function_name = 'Delete All Display Layers' cmds.undoInfo(openChunk=True, chunkName=function_name) + deleted_counter = 0 try: - display_layers = cmds.ls(type='displayLayer') - deleted_counter = 0 - for layer in display_layers: + _layers = None + if layer_list and isinstance(layer_list, list): + _layers = layer_list + if layer_list and isinstance(layer_list, str): + _layers = [layer_list] + if not _layers: + _layers = cmds.ls(type='displayLayer', long=True) or [] + for layer in _layers: if layer != 'defaultLayer': cmds.delete(layer) deleted_counter += 1 - feedback = FeedbackMessage(quantity=deleted_counter, - singular='layer was', - plural='layers were', - conclusion='deleted.', - zero_overwrite_message='No display layers found in this scene.') - feedback.print_inview_message() + if verbose: + feedback = FeedbackMessage(quantity=deleted_counter, + singular='layer was', + plural='layers were', + conclusion='deleted.', + zero_overwrite_message='No display layers found in this scene.') + feedback.print_inview_message() except Exception as e: - cmds.warning(str(e)) + message = f'Error while deleting display layers: {str(e)}' + if verbose: + cmds.warning(message) + else: + logger.debug(message) finally: cmds.undoInfo(closeChunk=True, chunkName=function_name) + return deleted_counter if __name__ == "__main__": logger.setLevel(logging.DEBUG) - from pprint import pprint - out = None - pprint(out) diff --git a/gt/utils/hierarchy_utils.py b/gt/utils/hierarchy_utils.py index bcb9780a..915705cd 100644 --- a/gt/utils/hierarchy_utils.py +++ b/gt/utils/hierarchy_utils.py @@ -2,32 +2,71 @@ Hierarchy Utilities github.com/TrevisanGMW/gt-tools """ +from gt.utils.feedback_utils import log_when_true import maya.cmds as cmds import logging + # Logging Setup logging.basicConfig() logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -def enforce_parent(obj_name, desired_parent): +def parent(source_objects, target_parent, verbose=False): """ Makes sure that the provided object is really parented under the desired parent element. Args: - obj_name (str): Name of the source object enforce parenting (e.g. "pSphere1") - desired_parent (str): Name of the desired parent element. You would expect to find obj_name inside it. - - Returns: True if re-parented, false if not re-parented or not found + source_objects (list, str): Name of the source objects (children) to be parented (e.g. "pSphere1" or ["obj"]) + target_parent (str): Name of the desired parent object. + verbose (bool, optional): If True, it will print feedback in case the operation failed. Default is False. + Returns: + list: A list of the parented objects. (Long if not unique) """ - if not obj_name or not cmds.objExists(obj_name): - return False # Source Object doesn't exist - if not desired_parent or not cmds.objExists(desired_parent): - return False # Target Object doesn't exist - current_parent = cmds.listRelatives(obj_name, parent=True) or [] - if current_parent: - current_parent = current_parent[0] - if current_parent != desired_parent: - cmds.parent(obj_name, desired_parent) - else: - cmds.parent(obj_name, desired_parent) \ No newline at end of file + store_selection = cmds.ls(selection=True) or [] + if not target_parent or not cmds.objExists(target_parent): + log_when_true(input_logger=logger, + input_string=f'Unable to execute parenting operation.' + f'Missing target parent object "{str(target_parent)}".', + do_log=verbose) + return [] + if source_objects and isinstance(source_objects, str): # If a string, convert to list + source_objects = [source_objects] + parented_objects = [] + for child in source_objects: + if not child or not cmds.objExists(child): + log_when_true(input_logger=logger, + input_string=f'Missing source object "{str(child)}" while ' + f'parenting it to "{str(target_parent)}".', + do_log=verbose) + continue + current_parent = cmds.listRelatives(child, parent=True) or [] + if current_parent: + current_parent = current_parent[0] + if current_parent != target_parent: + for obj in cmds.parent(child, target_parent) or []: + parented_objects.append(obj) + else: + for obj in cmds.parent(child, target_parent) or []: + parented_objects.append(obj) + if store_selection: + try: + cmds.select(store_selection) + except Exception as e: + log_when_true(input_logger=logger, + input_string=f'Unable to recover previous selection. Issue: "{str(e)}".', + do_log=verbose, + level=logging.DEBUG) + try: + parented_objects_long = cmds.ls(parented_objects, long=True) + except Exception as e: + log_when_true(input_logger=logger, + input_string=f'Unable to convert parented to long names. Issue: "{str(e)}".', + do_log=verbose, + level=logging.DEBUG) + parented_objects_long = parented_objects + return parented_objects_long + + +if __name__ == "__main__": + logger.setLevel(logging.DEBUG) diff --git a/gt/utils/joint_utils.py b/gt/utils/joint_utils.py index ac0f9d8f..0effff17 100644 --- a/gt/utils/joint_utils.py +++ b/gt/utils/joint_utils.py @@ -184,20 +184,33 @@ def get_cross_direction(obj_a, obj_b, obj_c): return get_cross_product(pos_a, pos_b, pos_c).normal() -def convert_joints_to_mesh(combine_mesh=True): +def convert_joints_to_mesh(root_jnt=None, combine_mesh=True, verbose=True): """ Converts a joint hierarchy into a mesh representation of it (Helpful when sending it to sculpting apps) Args: - combine_mesh: Combines generated meshes into one + root_jnt (list, str, optional): Path to the root joint of the skeleton used in the conversion. + If not provided, the selection is used instead. + If a list, must contain exactly one object, the root joint. (top parent joint) + combine_mesh: Combines generated meshes into one. Each joint produces a mesh. + when combining, the output is one single combined mesh. (Entire skeleton) + verbose (bool, optional): If True, it will return feedback about the operation. Returns: list: A list of generated meshes """ - selection = cmds.ls(selection=True, type='joint') - if len(selection) != 1: - cmds.warning('Please selection only the root joint and try again.') + _joints = None + if root_jnt and isinstance(root_jnt, list): + _joints = root_jnt + if root_jnt and isinstance(root_jnt, str): + _joints = [root_jnt] + if not _joints: + _joints = cmds.ls(selection=True, typ="joint") or [] + + if len(_joints) != 1: + if verbose: + cmds.warning('Please selection only the root joint and try again.') return - cmds.select(selection[0], replace=True) + cmds.select(_joints[0], replace=True) cmds.select(hierarchy=True) joints = cmds.ls(selection=True, type='joint', long=True) @@ -236,14 +249,15 @@ def convert_joints_to_mesh(combine_mesh=True): child_pos = cmds.xform(obj, t=True, ws=True, query=True) cmds.xform(joint_cone[0] + '.vtx[4]', t=child_pos, ws=True) - if combine_mesh: + if combine_mesh and len(generated_mesh) > 1: # Needs at least two meshes to combine cmds.select(generated_mesh, replace=True) mesh = cmds.polyUnite() cmds.select(clear=True) cmds.delete(mesh, constructionHistory=True) - mesh = cmds.rename(mesh[0], selection[0] + 'AsMesh') + mesh = cmds.rename(mesh[0], _joints[0] + 'AsMesh') return [mesh] else: + cmds.select(clear=True) return generated_mesh diff --git a/gt/utils/mesh_utils.py b/gt/utils/mesh_utils.py index 77611cc5..38302278 100644 --- a/gt/utils/mesh_utils.py +++ b/gt/utils/mesh_utils.py @@ -2,9 +2,10 @@ Mesh (Geometry) Utilities github.com/TrevisanGMW/gt-tools """ +from gt.utils.data.py_meshes import scale_volume, scene_setup from gt.utils import system_utils, iterable_utils -from gt.utils.data.py_meshes import scale_volume from gt.utils.data_utils import DataDirConstants +from collections import namedtuple import maya.cmds as cmds import logging import ast @@ -17,7 +18,6 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) - # Constants MESH_TYPE_DEFAULT = "mesh" MESH_TYPE_SURFACE = "nurbsSurface" @@ -282,6 +282,39 @@ def is_vertex_string(input_str): return bool(re.match(pattern, input_str)) +def extract_components_from_face(face_name): + """ + Extract two edges from the given face based on the specified direction. + + Args: + face_name (str): The name of the face in Maya. e.g. "pCube.f[0]" + + Returns: + namedtuple (FaceComponents): Containing twp tuple elements: 'vertices', 'edges' + """ + FaceComponents = namedtuple('FaceComponents', ['vertices', 'edges']) + try: + # Check if the given face exists + if not cmds.objExists(face_name): + logger.debug(f'Unable to extract components from provided face. Missing face "{face_name}".') + return + + # Get the vertices of the face + vertices = cmds.polyListComponentConversion(face_name, toVertex=True) + vertices = cmds.ls(vertices, flatten=True) + + # Find the edges connected to the vertices + _vertices_edges = cmds.polyListComponentConversion(vertices, toEdge=True) + edges = cmds.polyListComponentConversion(face_name, toEdge=True) + edges = cmds.ls(edges, flatten=True) + + return FaceComponents(vertices=vertices, edges=edges) + + except Exception as e: + logger.debug(f"Unable to extract components from provided face. Issue : {str(e)}") + return + + class MeshFile: def __init__(self, file_path=None, @@ -618,6 +651,7 @@ def __init__(self): A library of parametric mesh objects. Use "build()" to create them in Maya. """ + # Primitives scale_cube = ParametricMesh(build_function=scale_volume.create_scale_cube) scale_cylinder = ParametricMesh(build_function=scale_volume.create_scale_cylinder) @@ -631,6 +665,7 @@ def __init__(self): # Creatures/Bipeds scale_human_male = ParametricMesh(build_function=scale_volume.create_scale_human_male) scale_human_female = ParametricMesh(build_function=scale_volume.create_scale_human_female) + studio_background = ParametricMesh(build_function=scene_setup.create_studio_background) def print_code_for_obj_files(target_dir=None, ignore_private=True, use_output_window=False): @@ -672,13 +707,16 @@ def print_code_for_obj_files(target_dir=None, ignore_private=True, use_output_wi window.show() sys.stdout.write(f'Python lines for "Meshes" class were printed to output window.') else: - print("_"*80) + print("_" * 80) print(output) - print("_"*80) + print("_" * 80) sys.stdout.write(f'Python lines for "Meshes" class were printed. (If in Maya, open the script editor)') return output if __name__ == "__main__": logger.setLevel(logging.DEBUG) - print_code_for_obj_files() + # print_code_for_obj_files() + test_face_name = "pCube1.f[0]" + # test_face_name = "pPlane1.f[0]" + result = extract_components_from_face(test_face_name) # Change parallel to True for parallel edges diff --git a/gt/utils/naming_utils.py b/gt/utils/naming_utils.py index 754190f7..d7deeab6 100644 --- a/gt/utils/naming_utils.py +++ b/gt/utils/naming_utils.py @@ -73,8 +73,9 @@ def get_long_name(short_name): try: long_name = cmds.ls(short_name, long=True)[0] return long_name - except IndexError: - return None + except (IndexError, RuntimeError) as e: + logger.debug(f'Unable to retrieve long name. Issue: {str(e)}') + return None def get_short_name(long_name, remove_namespace=False): @@ -98,3 +99,7 @@ def get_short_name(long_name, remove_namespace=False): if remove_namespace: output_short_name = output_short_name.split(":")[-1] return output_short_name + + +if __name__ == "__main__": + logger.setLevel(logging.DEBUG) diff --git a/gt/utils/proxy_utils.py b/gt/utils/proxy_utils.py deleted file mode 100644 index f28a7ea0..00000000 --- a/gt/utils/proxy_utils.py +++ /dev/null @@ -1,390 +0,0 @@ -""" -Proxy Utilities -github.com/TrevisanGMW/gt-tools - -TODO: - Proxy (single joint) - RigComponent (carry proxies, can be complex) - RigSkeleton, RigBase (carry components) -""" -from gt.utils.uuid_utils import add_uuid_attribute, is_uuid_valid, is_short_uuid_valid, generate_uuid -from gt.utils.curve_utils import Curve, get_curve, add_shape_scale_cluster -from gt.utils.attr_utils import add_separator_attr, set_attr, add_attr -from gt.utils.naming_utils import NamingConstants, get_long_name -from gt.utils.control_utils import add_snapping_shape -from gt.utils.transform_utils import Transform -from dataclasses import dataclass -import maya.cmds as cmds -import logging - -# Logging Setup -logging.basicConfig() -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - - -class ProxyConstants: - def __init__(self): - """ - Constant values used by all proxy elements. - """ - JOINT_ATTR_UUID = "jointUUID" - PROXY_ATTR_UUID = "proxyUUID" - PROXY_ATTR_SCALE = "locatorScale" - PROXY_MAIN_CRV = "proxy_main_crv" # Main control that holds many proxies - SEPARATOR_ATTR = "proxyPreferences" # Locked attribute at the top of the proxy options - - -@dataclass -class ProxyData: - """ - A proxy data class used as the proxy response for when the proxy is built. - """ - name: str # Long name of the generated proxy (full Maya path) - offset: str # Name of the proxy offset (parent of the proxy) - setup: tuple # Name of the proxy setup items (rig setup items) - - def __repr__(self): - """ - String conversion returns the name of the proxy - Returns: - str: Proxy long name. - """ - return self.name - - def get_short_name(self): - """ - Gets the short version of the proxy name (default name is its long name) - Note, this name might not be unique - Returns: - str: Short name of the proxy (short version of self.name) - Last name after "|" characters - """ - from gt.utils.naming_utils import get_short_name - return get_short_name(self.name) - - def get_long_name(self): - """ - Gets the long version of the proxy name. - Returns: - str: Long name of the proxy. (a.k.a. Full Path) - """ - return self.name - - def get_offset(self): - """ - Gets the long version of the offset proxy group. - Returns: - str: Long name of the proxy group. (a.k.a. Full Path) - """ - return self.offset - - def get_setup(self): - """ - Gets the setup items tuple from the proxy data. This is a list of objects used to set up the proxy. (rig setup) - Returns: - tuple: A tuple with strings (full paths to the rig elements) - """ - return self.setup - - -class Proxy: - def __init__(self, - name=None, - transform=None, - offset_transform=None, - curve=None, - uuid=None, - parent_uuid=None, - locator_scale=None, - metadata=None): - # Default Values - self.name = "proxy" - self.transform = Transform() # Default is T:(0,0,0) R:(0,0,0) and S:(1,1,1) - self.offset_transform = Transform() - self.curve = get_curve('_proxy_joint') - self.curve.set_name(name=self.name) - self.locator_scale = 1 # 100% - Initial curve scale - self.uuid = generate_uuid(remove_dashes=True) - self.parent_uuid = None - self.metadata = None - - if name: - self.set_name(name) - if transform: - self.set_transform(transform) - if offset_transform: - self.set_offset_transform(offset_transform) - if curve: - self.set_curve(curve) - if uuid: - self.set_uuid(uuid) - if parent_uuid: - self.set_parent_uuid(parent_uuid) - if locator_scale: - self.set_locator_scale(locator_scale) - if metadata: - self.set_metadata_dict(metadata=metadata) - - def is_proxy_valid(self): - """ - Checks if the current proxy element is valid - """ - if not self.name: - logger.warning('Invalid proxy object. Missing name.') - return False - if not self.curve: - logger.warning('Invalid proxy object. Missing curve.') - return False - return True - - def build(self): - """ - Builds a proxy object. - Returns: - ProxyData: Name of the proxy that was generated/built. - """ - if not self.is_proxy_valid(): - logger.warning(f'Unable to build proxy. Invalid proxy object.') - return - proxy_offset = cmds.group(name=f'{self.name}_{NamingConstants.Suffix.OFFSET}', world=True, empty=True) - proxy_crv = self.curve.build() - cmds.parent(proxy_crv, proxy_offset) - proxy_offset = get_long_name(proxy_offset) - proxy_crv = get_long_name(proxy_crv) - add_snapping_shape(proxy_crv) - add_separator_attr(target_object=proxy_crv, attr_name=ProxyConstants.SEPARATOR_ATTR) - uuid_attrs = add_uuid_attribute(obj_list=proxy_crv, - attr_name=ProxyConstants.PROXY_ATTR_UUID, - set_initial_uuid_value=False) - scale_attr = add_attr(target_list=proxy_crv, attributes=ProxyConstants.PROXY_ATTR_SCALE, default=1) or [] - loc_scale_cluster = None - if scale_attr and len(scale_attr) == 1: - scale_attr = scale_attr[0] - loc_scale_cluster = add_shape_scale_cluster(proxy_crv, scale_driver_attr=scale_attr) - for attr in uuid_attrs: - set_attr(attribute_path=attr, value=self.uuid) - if self.offset_transform: - self.offset_transform.apply_transform(target_object=proxy_offset, world_space=True) - if self.transform: - self.transform.apply_transform(target_object=proxy_crv, object_space=True) - if self.locator_scale and scale_attr: - cmds.refresh() # Without refresh, it fails to show the correct scale - set_attr(scale_attr, self.locator_scale) - - return ProxyData(name=proxy_crv, offset=proxy_offset, setup=(loc_scale_cluster,)) - - # ------------------------------------------------- Setters ------------------------------------------------- - def set_name(self, name): - """ - Sets a new proxy name. Useful when ingesting data from dictionary or file with undesired name. - Args: - name (str): New name to use on the proxy. - """ - if not name or not isinstance(name, str): - logger.warning(f'Unable to set new name. Expected string but got "{str(type(name))}"') - return - self.curve.set_name(name) - self.name = name - - def set_transform(self, transform): - """ - Sets the transform for this proxy element - Args: - transform (Transform): A transform object describing position, rotation and scale. - """ - if not transform or not isinstance(transform, Transform): - logger.warning(f'Unable to set proxy transform. ' - f'Must be a "Transform" object, but got "{str(type(transform))}".') - return - self.transform = transform - - def set_position(self, x=None, y=None, z=None, xyz=None): - """ - Sets the position of the proxy element (introduce values to its curve) - Args: - x (float, int, optional): X value for the position. If provided, you must provide Y and Z too. - y (float, int, optional): Y value for the position. If provided, you must provide X and Z too. - z (float, int, optional): Z value for the position. If provided, you must provide X and Y too. - xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. - """ - self.transform.set_position(x=x, y=y, z=z, xyz=xyz) - - def set_rotation(self, x=None, y=None, z=None, xyz=None): - """ - Sets the rotation of the proxy element (introduce values to its curve) - Args: - x (float, int, optional): X value for the rotation. If provided, you must provide Y and Z too. - y (float, int, optional): Y value for the rotation. If provided, you must provide X and Z too. - z (float, int, optional): Z value for the rotation. If provided, you must provide X and Y too. - xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. - """ - self.transform.set_rotation(x=x, y=y, z=z, xyz=xyz) - - def set_scale(self, x=None, y=None, z=None, xyz=None): - """ - Sets the scale of the proxy element (introduce values to its curve) - Args: - x (float, int, optional): X value for the scale. If provided, you must provide Y and Z too. - y (float, int, optional): Y value for the scale. If provided, you must provide X and Z too. - z (float, int, optional): Z value for the scale. If provided, you must provide X and Y too. - xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. - """ - self.transform.set_scale(x=x, y=y, z=z, xyz=xyz) - - def set_offset_transform(self, transform): - """ - Sets the transform for this proxy element - Args: - transform (Transform): A transform object describing position, rotation and scale. - """ - if not transform or not isinstance(transform, Transform): - logger.warning(f'Unable to set proxy transform. ' - f'Must be a "Transform" object, but got "{str(type(transform))}".') - return - self.offset_transform = transform - - def set_offset_position(self, x=None, y=None, z=None, xyz=None): - """ - Sets the position of the proxy element (introduce values to its curve) - Args: - x (float, int, optional): X value for the position. If provided, you must provide Y and Z too. - y (float, int, optional): Y value for the position. If provided, you must provide X and Z too. - z (float, int, optional): Z value for the position. If provided, you must provide X and Y too. - xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. - """ - self.offset_transform.set_position(x=x, y=y, z=z, xyz=xyz) - - def set_offset_rotation(self, x=None, y=None, z=None, xyz=None): - """ - Sets the rotation of the proxy element (introduce values to its curve) - Args: - x (float, int, optional): X value for the rotation. If provided, you must provide Y and Z too. - y (float, int, optional): Y value for the rotation. If provided, you must provide X and Z too. - z (float, int, optional): Z value for the rotation. If provided, you must provide X and Y too. - xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. - """ - self.offset_transform.set_rotation(x=x, y=y, z=z, xyz=xyz) - - def set_offset_scale(self, x=None, y=None, z=None, xyz=None): - """ - Sets the scale of the proxy element (introduce values to its curve) - Args: - x (float, int, optional): X value for the scale. If provided, you must provide Y and Z too. - y (float, int, optional): Y value for the scale. If provided, you must provide X and Z too. - z (float, int, optional): Z value for the scale. If provided, you must provide X and Y too. - xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. - """ - self.offset_transform.set_scale(x=x, y=y, z=z, xyz=xyz) - - def set_curve(self, curve, inherit_curve_name=False): - """ - Sets the curve used to build the proxy element - Args: - curve (Curve) A Curve object to be used for building the proxy element (its shape) - inherit_curve_name (bool, optional): If active, this function try to extract the name of the curve and - change the name of the proxy to match it. Does nothing if name is None. - """ - if not curve or not isinstance(curve, Curve): - logger.debug(f'Unable to set proxy curve. Invalid input. Must be a valid Curve object.') - return - if not curve.is_curve_valid(): - logger.debug(f'Unable to set proxy curve. Curve object failed validation.') - return - if inherit_curve_name: - self.set_name(curve.get_name()) - else: - curve.set_name(name=self.name) - self.curve = curve - - def set_locator_scale(self, scale): - if not isinstance(scale, (float, int)): - logger.debug(f'Unable to set locator scale. Invalid input.') - self.locator_scale = scale - - def set_metadata_dict(self, metadata): - """ - Sets the metadata property. The metadata is any extra value used to further describe the curve. - Args: - metadata (dict): A dictionary describing extra information about the curve - """ - if not isinstance(metadata, dict): - logger.warning(f'Unable to set curve metadata. Expected a dictionary, but got: "{str(type(metadata))}"') - return - self.metadata = metadata - - def add_to_metadata(self, key, value): - """ - Adds a new item to the metadata dictionary. Initializes it in case it was not yet initialized. - If an element with the same key already exists in the metadata dictionary, it will be overwritten - Args: - key (str): Key of the new metadata element - value (Any): Value of the new metadata element - """ - if not self.metadata: # Initialize metadata in case it was never used. - self.metadata = {} - self.metadata[key] = value - - def set_uuid(self, uuid): - """ - Sets a new UUID for the proxy. - If no UUID is provided or set a new one will be generated automatically, - this function is used to force a specific value as UUID. - Args: - uuid (str): A new UUID for this proxy - """ - error_message = f'Unable to set proxy UUID. Invalid UUID input.' - if not uuid or not isinstance(uuid, str): - logger.warning(error_message) - return - if is_uuid_valid(uuid) or is_short_uuid_valid(uuid): - self.uuid = uuid - else: - logger.warning(error_message) - - def set_parent_uuid(self, uuid): - """ - Sets a new parent UUID for the proxy. - If a parent UUID is set, the proxy has enough information be re-parented when part of a set. - Args: - uuid (str): A new UUID for the parent of this proxy - """ - error_message = f'Unable to set proxy parent UUID. Invalid UUID input.' - if not uuid or not isinstance(uuid, str): - logger.warning(error_message) - return - if is_uuid_valid(uuid) or is_short_uuid_valid(uuid): - self.parent_uuid = uuid - else: - logger.warning(error_message) - - # ------------------------------------------------- Getters ------------------------------------------------- - def get_metadata(self): - """ - Gets the metadata property. - Returns: - dict: Metadata dictionary - """ - return self.metadata - - def get_name(self): - """ - Gets the name property of the proxy. - Returns: - str or None: Name of the proxy, None if it's not set. - """ - return self.name - - -if __name__ == "__main__": - logger.setLevel(logging.DEBUG) - cmds.file(new=True, force=True) - test_parent_uuid = generate_uuid() - temp_trans = Transform() - temp_trans.set_position(0, 10, 0) - proxy = Proxy() - proxy.set_transform(temp_trans) - proxy.set_offset_position(0, 5, 5) - proxy.set_parent_uuid(test_parent_uuid) - proxy.set_curve(get_curve("_proxy_joint_handle")) - proxy.set_locator_scale(5) - proxy.build() diff --git a/gt/utils/rigger_utils.py b/gt/utils/rigger_utils.py new file mode 100644 index 00000000..e0ab13e0 --- /dev/null +++ b/gt/utils/rigger_utils.py @@ -0,0 +1,1138 @@ +""" +Auto Rigger Utilities +github.com/TrevisanGMW/gt-tools +""" +from gt.utils.uuid_utils import add_uuid_attribute, is_uuid_valid, is_short_uuid_valid, generate_uuid +from gt.utils.curve_utils import Curve, get_curve, add_shape_scale_cluster +from gt.utils.attr_utils import add_separator_attr, set_attr, add_attr +from gt.utils.naming_utils import NamingConstants, get_long_name +from gt.utils.uuid_utils import find_object_with_uuid +from gt.utils.control_utils import add_snapping_shape +from gt.utils.string_utils import remove_prefix +from gt.utils.transform_utils import Transform +from gt.utils.hierarchy_utils import parent +from dataclasses import dataclass +import maya.cmds as cmds +import logging + + +# Logging Setup +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class RiggerConstants: + def __init__(self): + """ + Constant values used by all proxy elements. + """ + JOINT_ATTR_UUID = "jointUUID" + PROXY_ATTR_UUID = "proxyUUID" + PROXY_ATTR_SCALE = "locatorScale" + PROXY_MAIN_CRV = "proxy_main_crv" # Main control that holds many proxies + SEPARATOR_ATTR = "proxyPreferences" # Locked attribute at the top of the proxy options + + +@dataclass +class ProxyData: + """ + A proxy data class used as the proxy response for when the proxy is built. + """ + name: str # Long name of the generated proxy (full Maya path) + offset: str # Name of the proxy offset (parent of the proxy) + setup: tuple # Name of the proxy setup items (rig setup items) + uuid: str # Proxy UUID (Unique string pointing to generated proxy) - Not Maya UUID + + def __repr__(self): + """ + String conversion returns the name of the proxy + Returns: + str: Proxy long name. + """ + return self.name + + def get_short_name(self): + """ + Gets the short version of the proxy name (default name is its long name) + Note, this name might not be unique + Returns: + str: Short name of the proxy (short version of self.name) - Last name after "|" characters + """ + from gt.utils.naming_utils import get_short_name + return get_short_name(self.name) + + def get_long_name(self): + """ + Gets the long version of the proxy name. + Returns: + str: Long name of the proxy. (a.k.a. Full Path) + """ + return self.name + + def get_offset(self): + """ + Gets the long version of the offset proxy group. + Returns: + str: Long name of the proxy group. (a.k.a. Full Path) + """ + return self.offset + + def get_setup(self): + """ + Gets the setup items tuple from the proxy data. This is a list of objects used to set up the proxy. (rig setup) + Returns: + tuple: A tuple with strings (full paths to the rig elements) + """ + return self.setup + + def get_uuid(self): + """ + Gets the proxy UUID + Returns: + str: Proxy UUID string + """ + return self.uuid + + +class Proxy: + def __init__(self, + name=None, + transform=None, + offset_transform=None, + curve=None, + uuid=None, + parent_uuid=None, + locator_scale=None, + attr_dict=None, + metadata=None): + + # Default Values + self.name = "proxy" + self.transform = Transform() # Default is T:(0,0,0) R:(0,0,0) and S:(1,1,1) + self.offset_transform = None + self.curve = get_curve('_proxy_joint') + self.curve.set_name(name=self.name) + self.uuid = generate_uuid(remove_dashes=True) + self.parent_uuid = None + self.locator_scale = 1 # 100% - Initial curve scale + self.attr_dict = {} + self.metadata = None + + if name: + self.set_name(name) + if transform: + self.set_transform(transform) + if offset_transform: + self.set_offset_transform(offset_transform) + if curve: + self.set_curve(curve) + if uuid: + self.set_uuid(uuid) + if parent_uuid: + self.set_parent_uuid(parent_uuid) + if locator_scale: + self.set_locator_scale(locator_scale) + if attr_dict: + self.set_attr_dict(attr_dict=attr_dict) + if metadata: + self.set_metadata_dict(metadata=metadata) + + def is_valid(self): + """ + Checks if the current proxy element is valid + """ + if not self.name: + logger.warning('Invalid proxy object. Missing name.') + return False + if not self.curve: + logger.warning('Invalid proxy object. Missing curve.') + return False + return True + + def build(self): + """ + Builds a proxy object. + Returns: + ProxyData: Name of the proxy that was generated/built. + """ + if not self.is_valid(): + logger.warning(f'Unable to build proxy. Invalid proxy object.') + return + proxy_offset = cmds.group(name=f'{self.name}_{NamingConstants.Suffix.OFFSET}', world=True, empty=True) + proxy_crv = self.curve.build() + proxy_crv = cmds.parent(proxy_crv, proxy_offset)[0] + proxy_offset = get_long_name(proxy_offset) + proxy_crv = get_long_name(proxy_crv) + add_snapping_shape(proxy_crv) + add_separator_attr(target_object=proxy_crv, attr_name=RiggerConstants.SEPARATOR_ATTR) + uuid_attrs = add_uuid_attribute(obj_list=proxy_crv, + attr_name=RiggerConstants.PROXY_ATTR_UUID, + set_initial_uuid_value=False) + scale_attr = add_attr(target_list=proxy_crv, attributes=RiggerConstants.PROXY_ATTR_SCALE, default=1) or [] + loc_scale_cluster = None + if scale_attr and len(scale_attr) == 1: + scale_attr = scale_attr[0] + loc_scale_cluster = add_shape_scale_cluster(proxy_crv, scale_driver_attr=scale_attr) + for attr in uuid_attrs: + set_attr(attribute_path=attr, value=self.uuid) + if self.offset_transform: + self.offset_transform.apply_transform(target_object=proxy_offset, world_space=True) + if self.transform: + self.transform.apply_transform(target_object=proxy_crv, object_space=True) + if self.locator_scale and scale_attr: + cmds.refresh() # Without refresh, it fails to show the correct scale + set_attr(scale_attr, self.locator_scale) + + return ProxyData(name=proxy_crv, offset=proxy_offset, setup=(loc_scale_cluster,), uuid=self.get_uuid()) + + # ------------------------------------------------- Setters ------------------------------------------------- + def set_name(self, name): + """ + Sets a new proxy name. + Args: + name (str): New name to use on the proxy. + """ + if not name or not isinstance(name, str): + logger.warning(f'Unable to set new name. Expected string but got "{str(type(name))}"') + return + self.curve.set_name(name) + self.name = name + + def set_transform(self, transform): + """ + Sets the transform for this proxy element + Args: + transform (Transform): A transform object describing position, rotation and scale. + """ + if not transform or not isinstance(transform, Transform): + logger.warning(f'Unable to set proxy transform. ' + f'Must be a "Transform" object, but got "{str(type(transform))}".') + return + self.transform = transform + + def set_position(self, x=None, y=None, z=None, xyz=None): + """ + Sets the position of the proxy element (introduce values to its curve) + Args: + x (float, int, optional): X value for the position. If provided, you must provide Y and Z too. + y (float, int, optional): Y value for the position. If provided, you must provide X and Z too. + z (float, int, optional): Z value for the position. If provided, you must provide X and Y too. + xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. + """ + self.transform.set_position(x=x, y=y, z=z, xyz=xyz) + + def set_rotation(self, x=None, y=None, z=None, xyz=None): + """ + Sets the rotation of the proxy element (introduce values to its curve) + Args: + x (float, int, optional): X value for the rotation. If provided, you must provide Y and Z too. + y (float, int, optional): Y value for the rotation. If provided, you must provide X and Z too. + z (float, int, optional): Z value for the rotation. If provided, you must provide X and Y too. + xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. + """ + self.transform.set_rotation(x=x, y=y, z=z, xyz=xyz) + + def set_scale(self, x=None, y=None, z=None, xyz=None): + """ + Sets the scale of the proxy element (introduce values to its curve) + Args: + x (float, int, optional): X value for the scale. If provided, you must provide Y and Z too. + y (float, int, optional): Y value for the scale. If provided, you must provide X and Z too. + z (float, int, optional): Z value for the scale. If provided, you must provide X and Y too. + xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. + """ + self.transform.set_scale(x=x, y=y, z=z, xyz=xyz) + + def set_offset_transform(self, transform): + """ + Sets the transform for this proxy element + Args: + transform (Transform): A transform object describing position, rotation and scale. + """ + if not transform or not isinstance(transform, Transform): + logger.warning(f'Unable to set proxy transform. ' + f'Must be a "Transform" object, but got "{str(type(transform))}".') + return + self.offset_transform = transform + + def set_offset_position(self, x=None, y=None, z=None, xyz=None): + """ + Sets the position of the proxy element (introduce values to its curve) + Args: + x (float, int, optional): X value for the position. If provided, you must provide Y and Z too. + y (float, int, optional): Y value for the position. If provided, you must provide X and Z too. + z (float, int, optional): Z value for the position. If provided, you must provide X and Y too. + xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. + """ + if not self.offset_transform: + self.offset_transform = Transform() + self.offset_transform.set_position(x=x, y=y, z=z, xyz=xyz) + + def set_offset_rotation(self, x=None, y=None, z=None, xyz=None): + """ + Sets the rotation of the proxy element (introduce values to its curve) + Args: + x (float, int, optional): X value for the rotation. If provided, you must provide Y and Z too. + y (float, int, optional): Y value for the rotation. If provided, you must provide X and Z too. + z (float, int, optional): Z value for the rotation. If provided, you must provide X and Y too. + xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. + """ + if not self.offset_transform: + self.offset_transform = Transform() + self.offset_transform.set_rotation(x=x, y=y, z=z, xyz=xyz) + + def set_offset_scale(self, x=None, y=None, z=None, xyz=None): + """ + Sets the scale of the proxy element (introduce values to its curve) + Args: + x (float, int, optional): X value for the scale. If provided, you must provide Y and Z too. + y (float, int, optional): Y value for the scale. If provided, you must provide X and Z too. + z (float, int, optional): Z value for the scale. If provided, you must provide X and Y too. + xyz (Vector3, list, tuple) A Vector3 with the new position or a tuple/list with X, Y and Z values. + """ + if not self.offset_transform: + self.offset_transform = Transform() + self.offset_transform.set_scale(x=x, y=y, z=z, xyz=xyz) + + def set_curve(self, curve, inherit_curve_name=False): + """ + Sets the curve used to build the proxy element + Args: + curve (Curve) A Curve object to be used for building the proxy element (its shape) + inherit_curve_name (bool, optional): If active, this function try to extract the name of the curve and + change the name of the proxy to match it. Does nothing if name is None. + """ + if not curve or not isinstance(curve, Curve): + logger.debug(f'Unable to set proxy curve. Invalid input. Must be a valid Curve object.') + return + if not curve.is_curve_valid(): + logger.debug(f'Unable to set proxy curve. Curve object failed validation.') + return + if inherit_curve_name: + self.set_name(curve.get_name()) + else: + curve.set_name(name=self.name) + self.curve = curve + + def set_locator_scale(self, scale): + if not isinstance(scale, (float, int)): + logger.debug(f'Unable to set locator scale. Invalid input.') + self.locator_scale = scale + + def set_attr_dict(self, attr_dict): + """ + Sets the attributes dictionary for this proxy. Attributes are any key/value pairs further describing the proxy. + Args: + attr_dict (dict): An attribute dictionary where the key is the attribute and value is the attribute value. + e.g. {"locatorScale": 1, "isVisible": True} + """ + if not isinstance(attr_dict, dict): + logger.warning(f'Unable to set attribute dictionary. ' + f'Expected a dictionary, but got: "{str(type(attr_dict))}"') + return + self.attr_dict = attr_dict + + def set_metadata_dict(self, metadata): + """ + Sets the metadata property. The metadata is any extra value used to further describe the curve. + Args: + metadata (dict): A dictionary describing extra information about the curve + """ + if not isinstance(metadata, dict): + logger.warning(f'Unable to set proxy metadata. Expected a dictionary, but got: "{str(type(metadata))}"') + return + self.metadata = metadata + + def add_to_metadata(self, key, value): + """ + Adds a new item to the metadata dictionary. Initializes it in case it was not yet initialized. + If an element with the same key already exists in the metadata dictionary, it will be overwritten + Args: + key (str): Key of the new metadata element + value (Any): Value of the new metadata element + """ + if not self.metadata: # Initialize metadata in case it was never used. + self.metadata = {} + self.metadata[key] = value + + def set_uuid(self, uuid): + """ + Sets a new UUID for the proxy. + If no UUID is provided or set a new one will be generated automatically, + this function is used to force a specific value as UUID. + Args: + uuid (str): A new UUID for this proxy + """ + error_message = f'Unable to set proxy UUID. Invalid UUID input.' + if not uuid or not isinstance(uuid, str): + logger.warning(error_message) + return + if is_uuid_valid(uuid) or is_short_uuid_valid(uuid): + self.uuid = uuid + else: + logger.warning(error_message) + + def set_parent_uuid(self, uuid): + """ + Sets a new parent UUID for the proxy. + If a parent UUID is set, the proxy has enough information be re-parented when part of a set. + Args: + uuid (str): A new UUID for the parent of this proxy + """ + error_message = f'Unable to set proxy parent UUID. Invalid UUID input.' + if not uuid or not isinstance(uuid, str): + logger.warning(error_message) + return + if is_uuid_valid(uuid) or is_short_uuid_valid(uuid): + self.parent_uuid = uuid + else: + logger.warning(error_message) + + def set_parent_uuid_from_proxy(self, parent_proxy): + """ + Sets the provided proxy as the parent of this proxy. Its UUID is extracted as parent_UUID for this proxy. + If a parent UUID is set, the proxy has enough information be re-parented when part of a set. + Args: + parent_proxy (Proxy): A proxy object. The UUID for the parent will be extracted from it. + Will be the parent of this proxy when being parented. + """ + error_message = f'Unable to set proxy parent UUID. Invalid proxy input.' + if not parent_proxy or not isinstance(parent_proxy, Proxy): + logger.warning(error_message) + return + parent_uuid = parent_proxy.get_uuid() + self.set_parent_uuid(parent_uuid) + + def read_data_from_dict(self, proxy_dict): + """ + Reads the data from a proxy dictionary and updates the values of this proxy to match it. + Args: + proxy_dict (dict): A dictionary describing the proxy data. e.g. {"name": "proxy", "parent": "1234...", ...} + Returns: + Proxy: This object (self) + """ + if proxy_dict and not isinstance(proxy_dict, dict): + logger.debug(f'Unable o read data from dict. Input must be a dictionary.') + return + + _name = proxy_dict.get('name') + if _name: + self.set_name(name=_name) + + _parent = proxy_dict.get('parent') + if _parent: + self.set_parent_uuid(uuid=_parent) + + _loc_scale = proxy_dict.get('locatorScale') + if _loc_scale: + self.set_locator_scale(scale=_loc_scale) + + transform = proxy_dict.get('transform') + if transform and len(transform) == 3: + self.transform.set_transform_from_dict(transform_dict=transform) + + offset_transform = proxy_dict.get('offsetTransform') + if offset_transform and len(offset_transform) == 3: + if not self.offset_transform: + self.offset_transform = Transform() + self.offset_transform.set_transform_from_dict(transform_dict=transform) + + attributes = proxy_dict.get('attributes') + if attributes: + self.set_attr_dict(attr_dict=attributes) + + metadata = proxy_dict.get('metadata') + if metadata: + self.set_metadata_dict(metadata=metadata) + + _uuid = proxy_dict.get('uuid') + if _uuid: + self.set_uuid(uuid=_uuid) + return self + + def read_data_from_scene(self): + """ + Attempts to find the proxy in the scene. If found, it reads the data into the proxy object. + e.g. The user moved the proxy, a new position will be read and saved to this proxy. + New custom attributes or anything else added to the proxy will also be saved. + Returns: + Proxy: This object (self) + """ + ignore_attr_list = [RiggerConstants.PROXY_ATTR_UUID, + RiggerConstants.PROXY_ATTR_SCALE] + proxy = find_object_with_uuid(uuid_string=self.uuid, attr_name=RiggerConstants.PROXY_ATTR_UUID) + if proxy: + try: + self.transform.set_transform_from_object(proxy) + attr_dict = {} + user_attrs = cmds.listAttr(proxy, userDefined=True) or [] + for attr in user_attrs: + if not cmds.getAttr(f'{proxy}.{attr}', lock=True) and attr not in ignore_attr_list: + attr_dict[attr] = cmds.getAttr(f'{proxy}.{attr}') + if attr_dict: + self.set_attr_dict(attr_dict=attr_dict) + except Exception as e: + logger.debug(f'Unable to read proxy data for "{str(self.name)}". Issue: {str(e)}') + return self + + # ------------------------------------------------- Getters ------------------------------------------------- + def get_metadata(self): + """ + Gets the metadata property. + Returns: + dict: Metadata dictionary + """ + return self.metadata + + def get_name(self): + """ + Gets the name property of the proxy. + Returns: + str or None: Name of the proxy, None if it's not set. + """ + return self.name + + def get_uuid(self): + """ + Gets the uuid value of this proxy. + Returns: + str: uuid string + """ + return self.uuid + + def get_parent_uuid(self): + """ + Gets the parent uuid value of this proxy. + Returns: + str: uuid string for the potential parent of this proxy. + """ + return self.parent_uuid + + def get_attr_dict(self): + """ + Gets the attribute dictionary for this proxy + Returns: + dict: a dictionary where the key is the attribute name and the value is the value of the attribute. + e.g. {"locatorScale": 1, "isVisible": True} + """ + return self.attr_dict + + def get_proxy_as_dict(self, include_uuid=False): + """ + Returns all necessary information to recreate this proxy as a dictionary + Args: + include_uuid (bool, optional): If True, it will also include an "uuid" key and value in the dictionary. + Returns: + dict: Proxy data as a dictionary + """ + # Create Proxy Data + proxy_data = {"name": self.name, + "parent": self.get_parent_uuid(), + "locatorScale": self.locator_scale, + "transform": self.transform.get_transform_as_dict(), + } + + if self.offset_transform: + proxy_data["offsetTransform"] = self.offset_transform.get_transform_as_dict() + + if self.get_attr_dict(): + proxy_data["attributes"] = self.get_attr_dict() + + if self.get_metadata(): + proxy_data["metadata"] = self.get_metadata() + + if include_uuid and self.get_uuid(): + proxy_data["uuid"] = self.get_uuid() + + return proxy_data + + +class ModuleGeneric: + def __init__(self, + name=None, + prefix=None, + proxies=None, + parent_uuid=None, + metadata=None): + # Default Values + self.name = None + self.prefix = None + self.proxies = [] + self.parent_uuid = None # Module is parented to this object + self.metadata = None + + if name: + self.set_name(name) + if prefix: + self.set_prefix(prefix) + if proxies: + self.set_proxies(proxies) + if parent_uuid: + self.set_parent_uuid(parent_uuid) + if metadata: + self.set_metadata_dict(metadata=metadata) + + # ------------------------------------------------- Setters ------------------------------------------------- + def set_name(self, name): + """ + Sets a new module name. + Args: + name (str): New name to use on the proxy. + """ + if not name or not isinstance(name, str): + logger.warning(f'Unable to set name. Expected string but got "{str(type(name))}"') + return + self.prefix = name + + def set_prefix(self, prefix): + """ + Sets a new module prefix. + Args: + prefix (str): New name to use on the proxy. + """ + if not prefix or not isinstance(prefix, str): + logger.warning(f'Unable to set prefix. Expected string but got "{str(type(prefix))}"') + return + self.prefix = prefix + + def set_proxies(self, proxy_list): + """ + Sets a new proxy name. + Args: + proxy_list (str): New name to use on the proxy. + """ + if not proxy_list or not isinstance(proxy_list, list): + logger.warning(f'Unable to set new list of proxies. ' + f'Expected list of proxies but got "{str(proxy_list)}"') + return + self.proxies = proxy_list + + def add_to_proxies(self, proxy): + """ + Adds a new item to the metadata dictionary. Initializes it in case it was not yet initialized. + If an element with the same key already exists in the metadata dictionary, it will be overwritten + Args: + proxy (Proxy, List[Proxy]): New proxy element to be added to this module or a list of proxies + """ + if proxy and isinstance(proxy, Proxy): + proxy = [proxy] + if proxy and isinstance(proxy, list): + for obj in proxy: + if isinstance(obj, Proxy): + self.proxies.append(obj) + else: + logger.debug(f'Unable to add "{str(obj)}". Incompatible type.') + return + logger.debug(f'Unable to add proxy to module. ' + f'Must be of the type "Proxy" or a list containing only Proxy elements.') + + def set_metadata_dict(self, metadata): + """ + Sets the metadata property. The metadata is any extra value used to further describe the curve. + Args: + metadata (dict): A dictionary describing extra information about the curve + """ + if not isinstance(metadata, dict): + logger.warning(f'Unable to set module metadata. ' + f'Expected a dictionary, but got: "{str(type(metadata))}"') + return + self.metadata = metadata + + def add_to_metadata(self, key, value): + """ + Adds a new item to the metadata dictionary. Initializes it in case it was not yet initialized. + If an element with the same key already exists in the metadata dictionary, it will be overwritten + Args: + key (str): Key of the new metadata element + value (Any): Value of the new metadata element + """ + if not self.metadata: # Initialize metadata in case it was never used. + self.metadata = {} + self.metadata[key] = value + + def set_parent_uuid(self, uuid): + """ + Sets a new parent UUID for the proxy. + If a parent UUID is set, the proxy has enough information be re-parented when part of a set. + Args: + uuid (str): A new UUID for the parent of this proxy + """ + error_message = f'Unable to set proxy parent UUID. Invalid UUID input.' + if not uuid or not isinstance(uuid, str): + logger.warning(error_message) + return + if is_uuid_valid(uuid) or is_short_uuid_valid(uuid): + self.parent_uuid = uuid + else: + logger.warning(error_message) + + def read_proxies_from_dict(self, proxy_dict): + """ + Reads a proxy description dictionary and populates (after resetting) the proxies list with the dict proxies. + Args: + proxy_dict (dict): A proxy description dictionary. It must match an expected pattern for this to work: + Acceptable pattern: {"uuid_str": {}} + "uuid_str" being the actual uuid string value of the proxy. + "" being the output of the operation "proxy.get_proxy_as_dict()". + """ + if not proxy_dict or not isinstance(proxy_dict, dict): + logger.debug(f'Unable to read proxies from dictionary. Input must be a dictionary.') + return + + self.proxies = [] + for uuid, description in proxy_dict.items(): + _proxy = Proxy() + _proxy.set_uuid(uuid) + _proxy.read_data_from_dict(proxy_dict=description) + self.proxies.append(_proxy) + + def read_data_from_dict(self, module_dict): + """ + Reads the data from a module dictionary and updates the values of this module to match it. + Args: + module_dict (dict): A dictionary describing the module data. e.g. {"name": "generic"} + Returns: + ModuleGeneric: This module (self) + """ + if module_dict and not isinstance(module_dict, dict): + logger.debug(f'Unable o read data from dict. Input must be a dictionary.') + return + + _name = module_dict.get('name') + if _name: + self.set_name(name=_name) + + _prefix = module_dict.get('prefix') + if _prefix: + self.set_prefix(prefix=_prefix) + + _parent = module_dict.get('parent') + if _parent: + self.set_parent_uuid(uuid=_parent) + + _proxies = module_dict.get('proxies') + if _proxies and isinstance(_proxies, dict): + self.read_proxies_from_dict(proxy_dict=_proxies) + + metadata = module_dict.get('metadata') + if metadata: + self.set_metadata_dict(metadata=metadata) + return self + + # ------------------------------------------------- Getters ------------------------------------------------- + def get_name(self): + """ + Gets the name property of the rig module. + Returns: + str or None: Name of the rig module, None if it's not set. + """ + return self.prefix + + def get_prefix(self): + """ + Gets the prefix property of the rig module. + Returns: + str or None: Prefix of the rig module, None if it's not set. + """ + return self.prefix + + def get_proxies(self): + """ + Gets the proxies in this rig module. + Returns: + list: A list of proxies found in this rig module. + """ + return self.proxies + + def get_metadata(self): + """ + Gets the metadata property. + Returns: + dict: Metadata dictionary + """ + return self.metadata + + def get_module_as_dict(self, include_module_name=False): + """ + Gets the properties of this module (including proxies) as a dictionary + Args: + include_module_name (bool, optional): If True, it will also include the name of the class in the dictionary. + e.g. "ModuleGeneric" + Returns: + dict: Dictionary describing this module + """ + module_data = {} + if self.name: + module_data["name"] = self.name + if self.prefix: + module_data["prefix"] = self.prefix + if self.parent_uuid: + module_data["parent"] = self.parent_uuid + if self.metadata: + module_data["metadata"] = self.metadata + module_proxies = {} + for proxy in self.proxies: + module_proxies[proxy.get_uuid()] = proxy.get_proxy_as_dict() + module_data["proxies"] = module_proxies + if include_module_name: + module_name = self.get_module_class_name() + module_data["module"] = module_name + return module_data + + def get_module_class_name(self, remove_module_prefix=False): + """ + Gets the name of this class + Args: + remove_module_prefix (bool, optional): If True, it will remove the prefix word "Module" from class name. + Used to reduce the size of the string in JSON outputs. + Returns: + str: Class name as a string + """ + if remove_module_prefix: + return remove_prefix(input_string=str(self.__class__.__name__), prefix="Module") + return str(self.__class__.__name__) + + # --------------------------------------------------- Misc --------------------------------------------------- + def is_valid(self): + """ + Checks if the rig module is valid (can be used) + """ + if not self.proxies: + logger.warning('Missing proxies. A rig module needs at least one proxy to function.') + return False + return True + + def build_proxy(self): + for proxy in self.proxies: + proxy.build() + + +class ModuleBipedLeg(ModuleGeneric): + def __init__(self, + name="Leg", + prefix=None, + parent_uuid=None, + metadata=None): + super().__init__(name=name, prefix=prefix, parent_uuid=parent_uuid, metadata=metadata) + + # Default Proxies + hip = Proxy(name="hip") + hip.set_position(y=84.5) + hip.set_locator_scale(scale=0.4) + knee = Proxy(name="knee") + knee.set_position(y=47.05) + knee.set_locator_scale(scale=0.5) + ankle = Proxy(name="ankle") + ankle.set_position(y=9.6) + ankle.set_locator_scale(scale=0.4) + ball = Proxy(name="ball") + ball.set_position(z=13.1) + ball.set_locator_scale(scale=0.4) + toe = Proxy(name="toe") + toe.set_position(z=23.4) + toe.set_locator_scale(scale=0.4) + toe.set_parent_uuid(uuid=ball.get_uuid()) + toe.set_parent_uuid_from_proxy(parent_proxy=ball) + heel_pivot = Proxy(name="heelPivot") + heel_pivot.set_locator_scale(scale=0.1) + self.proxies.extend([hip, knee, ankle, ball, toe, heel_pivot]) + + # --------------------------------------------------- Misc --------------------------------------------------- + def is_valid(self): + """ + Checks if the rig module is valid (can be used) + """ + # TODO Other checks here + return super().is_valid() + + +class RigModules: + import gt.utils.rigger_utils as rigger_utils + ModuleGeneric = rigger_utils.ModuleGeneric + ModuleBipedLeg = rigger_utils.ModuleBipedLeg + + +class RigProject: + def __init__(self, + name=None, + prefix=None, + metadata=None): + # Default Values + self.name = "Untitled" + self.prefix = None + self.modules = [] + self.metadata = None + + if name: + self.set_name(name=name) + if prefix: + self.set_prefix(prefix=prefix) + if metadata: + self.set_metadata_dict(metadata=metadata) + + # ------------------------------------------------- Setters ------------------------------------------------- + def set_name(self, name): + """ + Sets a new project name. + Args: + name (str): New name to use on the proxy. + """ + if not name or not isinstance(name, str): + logger.warning(f'Unable to set name. Expected string but got "{str(type(name))}"') + return + self.prefix = name + + def set_prefix(self, prefix): + """ + Sets a new module prefix. + Args: + prefix (str): New name to use on the proxy. + """ + if not prefix or not isinstance(prefix, str): + logger.warning(f'Unable to set prefix. Expected string but got "{str(type(prefix))}"') + return + self.prefix = prefix + + def add_to_modules(self, module): + """ + Adds a new item to the metadata dictionary. Initializes it in case it was not yet initialized. + If an element with the same key already exists in the metadata dictionary, it will be overwritten + Args: + module (ModuleGeneric, List[ModuleGeneric]): New module element to be added to this project. + """ + if module and isinstance(module, ModuleGeneric): + module = [module] + if module and isinstance(module, list): + for obj in module: + if isinstance(obj, ModuleGeneric): + self.modules.append(obj) + else: + logger.debug(f'Unable to add "{str(obj)}". Incompatible type.') + return + logger.debug(f'Unable to add provided module to rig project. ' + f'Must be of the type "ModuleGeneric" or a list containing only ModuleGeneric elements.') + + def set_metadata_dict(self, metadata): + """ + Sets the metadata property. The metadata is any extra value used to further describe the curve. + Args: + metadata (dict): A dictionary describing extra information about the curve + """ + if not isinstance(metadata, dict): + logger.warning(f'Unable to set rig project metadata. ' + f'Expected a dictionary, but got: "{str(type(metadata))}"') + return + self.metadata = metadata + + def add_to_metadata(self, key, value): + """ + Adds a new item to the metadata dictionary. Initializes it in case it was not yet initialized. + If an element with the same key already exists in the metadata dictionary, it will be overwritten + Args: + key (str): Key of the new metadata element + value (Any): Value of the new metadata element + """ + if not self.metadata: # Initialize metadata in case it was never used. + self.metadata = {} + self.metadata[key] = value + + def read_modules_from_dict(self, modules_dict): + """ + Reads a proxy description dictionary and populates (after resetting) the proxies list with the dict proxies. + Args: + modules_dict (dict): A proxy description dictionary. It must match an expected pattern for this to work: + Acceptable pattern: {"uuid_str": {}} + "uuid_str" being the actual uuid string value of the proxy. + "" being the output of the operation "proxy.get_proxy_as_dict()". + """ + if not modules_dict or not isinstance(modules_dict, dict): + logger.debug(f'Unable to read modules from dictionary. Input must be a dictionary.') + return + + self.modules = [] + available_modules = vars(RigModules) + for class_name, description in modules_dict.items(): + if not class_name.startswith("Module"): + class_name = f'Module{class_name}' + if class_name in available_modules: + _module = available_modules.get(class_name)() + else: + _module = ModuleGeneric() + _module.read_data_from_dict(module_dict=description) + self.modules.append(_module) + + def read_data_from_dict(self, module_dict): + """ + Reads the data from a project dictionary and updates the values of this project to match it. + Args: + module_dict (dict): A dictionary describing the project data. e.g. {"name": "untitled", "modules": ...} + Returns: + RigProject: This project (self) + """ + + self.modules = [] + self.metadata = None + + if module_dict and not isinstance(module_dict, dict): + logger.debug(f'Unable o read data from dict. Input must be a dictionary.') + return + + _name = module_dict.get('name') + if _name: + self.set_name(name=_name) + + _prefix = module_dict.get('prefix') + if _prefix: + self.set_prefix(prefix=_prefix) + + _modules = module_dict.get('modules') + if _modules and isinstance(_modules, dict): + self.read_modules_from_dict(modules_dict=_modules) + + metadata = module_dict.get('metadata') + if metadata: + self.set_metadata_dict(metadata=metadata) + return self + + # ------------------------------------------------- Getters ------------------------------------------------- + def get_name(self): + """ + Gets the name property of the rig project. + Returns: + str or None: Name of the rig project, None if it's not set. + """ + return self.prefix + + def get_prefix(self): + """ + Gets the prefix property of the rig project. + Returns: + str or None: Prefix of the rig project, None if it's not set. + """ + return self.prefix + + def get_modules(self): + """ + Gets the modules of this rig project. + Returns: + list: A list of modules found in this project + """ + return self.modules + + def get_metadata(self): + """ + Gets the metadata property. + Returns: + dict: Metadata dictionary + """ + return self.metadata + + def get_project_as_dict(self): + """ + Gets the description for this project (including modules and its proxies) as a dictionary. + Returns: + dict: Dictionary describing this project. + """ + project_modules = {} + for module in self.modules: + module_class_name = module.get_module_class_name(remove_module_prefix=True) + project_modules[module_class_name] = module.get_module_as_dict() + + project_data = {} + if self.name: + project_data["name"] = self.name + if self.prefix: + project_data["prefix"] = self.prefix + project_data["modules"] = project_modules + if self.metadata: + project_data["metadata"] = self.metadata + + return project_data + + # --------------------------------------------------- Misc --------------------------------------------------- + def is_valid(self): + """ + Checks if the rig project is valid (can be used) + """ + if not self.modules: + logger.warning('Missing modules. A rig project needs at least one module to function.') + return False + return True + + def build_proxy(self): + # Build Proxy + for module in self.modules: + module.build_proxy() + + # Parent Proxy + for module in self.modules: + parent_proxies(module.get_proxies()) + + +def parent_proxies(proxy_list): + # Parent Joints + for proxy in proxy_list: + built_proxy = find_object_with_uuid(proxy.get_uuid(), RiggerConstants.PROXY_ATTR_UUID) + parent_proxy = find_object_with_uuid(proxy.get_parent_uuid(), RiggerConstants.PROXY_ATTR_UUID) + print(f'built_proxy: {built_proxy}') + print(f'parent_proxy: {parent_proxy}') + if built_proxy and parent_proxy and cmds.objExists(built_proxy) and cmds.objExists(parent_proxy): + offset = cmds.listRelatives(built_proxy, parent=True, fullPath=True) + if offset: + parent(source_objects=offset, target_parent=parent_proxy) + + +def create_root_curve(name): + root_curve = get_curve('_rig_root') + root_curve.set_name(name=name) + root_crv = root_curve.build() + root_grp = cmds.group(empty=True, world=True, name="tempGrp") + cmds.parent(root_crv, root_grp) + + +if __name__ == "__main__": + logger.setLevel(logging.DEBUG) + cmds.file(new=True, force=True) + + a_leg = ModuleBipedLeg() + a_module = ModuleGeneric() + + a_hip = Proxy() + a_hip.set_position(y=5.5) + a_hip.set_locator_scale(scale=0.4) + built_hip = a_hip.build() + cmds.setAttr(f'{built_hip}.tx', 5) + add_attr(target_list=str(built_hip), attributes=["customOne", "customTwo"], attr_type='double') + cmds.setAttr(f'{built_hip}.customOne', 5) + a_hip.read_data_from_scene() + + a_knee = Proxy(name="knee") + a_knee.set_position(y=2.05) + a_knee.set_locator_scale(scale=0.5) + a_knee.set_parent_uuid_from_proxy(parent_proxy=a_hip) + + a_module.add_to_proxies([a_hip, a_knee]) + + a_module_dict = a_module.get_module_as_dict() + + cmds.file(new=True, force=True) + + a_project = RigProject() + a_project.add_to_modules(a_module) + a_project.add_to_modules(a_leg) + a_project.build_proxy() + a_project_dict = a_project.get_project_as_dict() + cmds.file(new=True, force=True) + + another_project = RigProject() + another_project.read_data_from_dict(a_project_dict) + another_project.build_proxy() + # import json + # # json_string = json.dumps(a_hip.get_proxy_as_dict(), indent=4) + # json_string = json.dumps(a_project.get_project_as_dict(), indent=4) + # print(json_string) + # from gt.utils.data_utils import write_json + # a_path = r"C:\Users\guilherme.trevisan\Desktop\out.json" + # write_json(path=a_path, data=a_project.get_project_as_dict()) + # create_root_curve("main") diff --git a/gt/utils/setup_utils.py b/gt/utils/setup_utils.py index 9901409f..2bc0955a 100644 --- a/gt/utils/setup_utils.py +++ b/gt/utils/setup_utils.py @@ -5,8 +5,8 @@ from gt.utils.system_utils import get_available_maya_preferences_dirs, load_package_menu from gt.utils.session_utils import remove_modules_startswith, get_maya_version from gt.utils.session_utils import get_loaded_package_module_paths +from gt.utils.data_utils import DataDirConstants, delete_paths, set_file_permission_modifiable from gt.utils.feedback_utils import print_when_true -from gt.utils.data_utils import DataDirConstants import maya.cmds as cmds import logging import shutil @@ -110,14 +110,14 @@ def remove_previous_install(target_path, clear_prefs=False): if folder == PACKAGE_MAIN_MODULE: module_path = os.path.join(target_path, folder) logger.debug(f'Removing previous install: "{module_path}"') - shutil.rmtree(module_path) + delete_paths(module_path) if clear_prefs and folder == PACKAGE_PREFS_DIR: prefs_path = os.path.join(target_path, folder) logger.debug(f'Removing previous preferences: "{prefs_path}"') - shutil.rmtree(prefs_path) + delete_paths(prefs_path) contents = os.listdir(target_path) or [] if len(contents) == 0: # If parent folder is empty, remove it too. - shutil.rmtree(target_path) + delete_paths(target_path) def check_installation_integrity(package_target_folder): diff --git a/gt/utils/system_utils.py b/gt/utils/system_utils.py index 1618d2d6..77285fd6 100644 --- a/gt/utils/system_utils.py +++ b/gt/utils/system_utils.py @@ -197,8 +197,7 @@ def get_maya_preferences_dir(system): logger.debug(f'Got Maya preferences path from outside Maya. Reason: {str(e)}') win_maya_preferences_dir = os.path.join(win_maya_preferences_dir, "maya") elif system == OS_MAC: - mac_maya_preferences_dir = os.path.expanduser('~') - mac_maya_preferences_dir = os.path.join(mac_maya_preferences_dir, "Library", "Preferences", "Autodesk", "maya") + mac_maya_preferences_dir = os.path.join(os.path.expanduser('~'), "Library", "Preferences", "Autodesk", "maya") maya_preferences_paths = { OS_LINUX: "/usr/bin/", @@ -798,6 +797,85 @@ def execute_python_code(code, import_cmds=False, use_maya_warning=False, verbose log_when_true(input_logger=_logger, input_string=str(e), do_log=verbose, level=log_level) +def create_object(class_name, raise_errors=True, class_path=None, *args, **kwargs): + """ + Creates an instance of a class based on the provided class name. + + Args: + class_name (str): The name of the class to be instantiated. + raise_errors (bool, optional): Whether to raise errors or log warnings for exceptions. Default is True. + class_path (str, dict, optional): The module path where the class is located. Default is None. + A dictionary can also be provided, for example "locals()" + *args: Positional arguments to pass when creating the object. + **kwargs: Keyword arguments to pass when creating the object. + + Returns: + object: An instance of the specified class. + + Raises: + TypeError: If the specified class_name does not correspond to a valid class. + ImportError: If the specified module or class cannot be imported. + AttributeError: If the specified class_name is not found in the module. + NameError: If the specified class_name is not found in the global namespace. + """ + if class_path and isinstance(class_path, str): + # Attempt to import the module dynamically + try: + module = importlib.import_module(class_path) + except ImportError as e: + message = f"Error importing module '{class_path}': {str(e)}" + if raise_errors: + raise ImportError(message) + else: + logger.warning(message) + return None + + # Check if the class name exists in the imported module + if hasattr(module, class_name): + class_obj = getattr(module, class_name) + else: + message = f"{class_name} not found in module '{class_path}'." + if raise_errors: + raise AttributeError(message) + else: + logger.warning(message) + return None + elif class_name in globals(): + # Get the class object using globals() + class_obj = globals()[class_name] + elif class_path and isinstance(class_path, dict) and class_name in class_path: + class_obj = class_path[class_name] + else: + message = (f'Unable to create object. A path for "{class_name}" was not provided and it was not found in the ' + f'global namespace.') + if raise_errors: + raise NameError(message) + else: + logger.warning(message) + return None + + # Check if the object is a class + if isinstance(class_obj, type): + try: + # Create an instance of the class with args and kwargs + obj = class_obj(*args, **kwargs) + return obj + except Exception as e: + message = f"Error creating an instance of '{class_name}': {str(e)}" + if raise_errors: + raise TypeError(message) + else: + logger.warning(message) + return None + else: + message = f"{class_name} is not a class." + if raise_errors: + raise TypeError(message) + else: + logger.warning(message) + return None + + if __name__ == "__main__": from pprint import pprint out = None diff --git a/gt/utils/transform_utils.py b/gt/utils/transform_utils.py index 010bca7e..9046bb5d 100644 --- a/gt/utils/transform_utils.py +++ b/gt/utils/transform_utils.py @@ -2,8 +2,8 @@ Transform Utilities github.com/TrevisanGMW/gt-tools """ +from gt.utils.attr_utils import set_trs_attr, get_multiple_attr from gt.utils.feedback_utils import FeedbackMessage -from gt.utils.attr_utils import set_trs_attr from gt.utils.math_utils import matrix_mult import maya.cmds as cmds import logging @@ -254,6 +254,39 @@ def set_from_tuple(self, values): else: raise ValueError("Input list must contain exactly 3 numeric values" + str(values)) + def set_x(self, x): + """ + Sets only the X value for this object. + Args: + x (int, float): An integer or float number to be used as new X value. + """ + if x and not isinstance(x, (float, int)): + logger.debug(f'Unable to set X value. Input must be a float or integer.') + return + self.x = x + + def set_y(self, y): + """ + Sets only the X value for this object. + Args: + y (int, float): An integer or float number to be used as new X value. + """ + if y and not isinstance(y, (float, int)): + logger.debug(f'Unable to set Y value. Input must be a float or integer.') + return + self.y = y + + def set_z(self, z): + """ + Sets only the X value for this object. + Args: + z (int, float): An integer or float number to be used as new X value. + """ + if z and not isinstance(z, (float, int)): + logger.debug(f'Unable to set X value. Input must be a float or integer.') + return + self.z = z + # ------------------------------------------------- Transform Start ----------------------------------------------- class Transform: @@ -389,6 +422,16 @@ def set_position(self, x=None, y=None, z=None, xyz=None): if all(isinstance(val, (float, int)) for val in (x, y, z)): self.position = Vector3(x=x, y=y, z=z) return + # Not all channels + if x is not None or y is not None or z is not None: + if any(isinstance(val, (float, int)) for val in (x, y, z)): + if x is not None and isinstance(x, (float, int)): + self.position.set_x(x=x) + if y is not None and isinstance(y, (float, int)): + self.position.set_y(y=y) + if z is not None and isinstance(z, (float, int)): + self.position.set_z(z=z) + return logger.warning(f'Unable to set position. Invalid input.') def set_rotation(self, x=None, y=None, z=None, xyz=None): @@ -411,6 +454,16 @@ def set_rotation(self, x=None, y=None, z=None, xyz=None): if all(isinstance(val, (float, int)) for val in (x, y, z)): self.rotation = Vector3(x=x, y=y, z=z) return + # Not all channels + if x is not None or y is not None or z is not None: + if any(isinstance(val, (float, int)) for val in (x, y, z)): + if x is not None and isinstance(x, (float, int)): + self.rotation.set_x(x=x) + if y is not None and isinstance(y, (float, int)): + self.rotation.set_y(y=y) + if z is not None and isinstance(z, (float, int)): + self.rotation.set_z(z=z) + return logger.warning(f'Unable to set rotation. Invalid input.') def set_scale(self, x=None, y=None, z=None, xyz=None): @@ -433,6 +486,16 @@ def set_scale(self, x=None, y=None, z=None, xyz=None): if all(isinstance(val, (float, int)) for val in (x, y, z)): self.scale = Vector3(x=x, y=y, z=z) return + # Not all channels + if x is not None or y is not None or z is not None: + if any(isinstance(val, (float, int)) for val in (x, y, z)): + if x is not None and isinstance(x, (float, int)): + self.scale.set_x(x=x) + if y is not None and isinstance(y, (float, int)): + self.scale.set_y(y=y) + if z is not None and isinstance(z, (float, int)): + self.scale.set_z(z=z) + return logger.warning(f'Unable to set scale. Invalid input.') def to_matrix(self): @@ -522,6 +585,64 @@ def set_scale_from_tuple(self, scale_tuple): """ self.scale.set_from_tuple(scale_tuple) + def set_transform_from_object(self, obj_name, world_space=True): + """ + Attempts to extract translation, rotation and scale data from the provided object. + Updates the transform object with these extracted values. + No changes in case object is missing or function fails to extract data. + Args: + obj_name (str): Name of the object to get the data from. + world_space (bool, optional): Space used to extract values. True uses world-space, False uses object-space. + Returns: + Transform: it returns itself (The updated transform object) + """ + if obj_name and not cmds.objExists(obj_name): + logger.debug(f'Unable to extract transform data. Missing provided object: "{str(obj_name)}".') + return self + + if world_space: + position = cmds.xform(obj_name, q=True, t=True, ws=True) + if position and len(position) == 3: + self.set_position(xyz=position) + rotation = cmds.xform(obj_name, q=True, ro=True, ws=True) + if rotation and len(rotation) == 3: + self.set_rotation(xyz=rotation) + else: + position = get_multiple_attr(obj_list=[obj_name], attr_list=['tx', 'ty', 'tz'], verbose=False) + if position and len(position) == 3: + self.set_position(xyz=list(position.values())) + rotation = get_multiple_attr(obj_list=[obj_name], attr_list=['rx', 'ry', 'rz'], verbose=False) + if rotation and len(rotation) == 3: + self.set_rotation(xyz=list(rotation.values())) + scale = get_multiple_attr(obj_list=[obj_name], attr_list=['sx', 'sy', 'sz'], verbose=False) + if scale and len(scale) == 3: + self.set_scale(xyz=list(scale.values())) + + return self + + def set_transform_from_dict(self, transform_dict): + """ + Sets transform data from dictionary + Args: + transform_dict (dict): Dictionary with "position", "rotation", and "scale" keys. + Their values should be tuples with three floats or integers each. + """ + if transform_dict and not isinstance(transform_dict, dict): + logger.debug(f'Unable to set transform from dictionary. ' + f'Invalid input, argument must be a dictionary.') + return + position = transform_dict.get('position') + rotation = transform_dict.get('rotation') + scale = transform_dict.get('scale') + for data in [position, rotation, scale]: + if not data or not isinstance(data, (tuple, list)) or len(data) != 3: + logger.debug(f'Unable to set transform from dictionary. ' + f'Provide position, rotation and scale keys with tuples as their values.') + return + self.set_position(xyz=position) + self.set_rotation(xyz=rotation) + self.set_scale(xyz=scale) + def apply_transform(self, target_object, world_space=True, object_space=False, relative=False): if not target_object or not cmds.objExists(target_object): logger.warning(f'Unable to apply transform. Missing object: "{target_object}".') @@ -542,6 +663,49 @@ def apply_transform(self, target_object, world_space=True, object_space=False, r set_trs_attr(target_obj=target_object, value_tuple=rotation, rotate=True) set_trs_attr(target_obj=target_object, value_tuple=scale, scale=True) + def get_position(self, as_tuple=False): + """ + Gets the transform position + Returns: + Vector3 or tuple: Position value stored in this transform + """ + if as_tuple: + return self.position.get_as_tuple() + return self.position + + def get_rotation(self, as_tuple=False): + """ + Gets the transform rotation + Returns: + Vector3 or tuple: Rotation value stored in this transform + """ + if as_tuple: + return self.rotation.get_as_tuple() + return self.rotation + + def get_scale(self, as_tuple=False): + """ + Gets the transform scale + Returns: + Vector3 or tuple: Scale value stored in this transform + """ + if as_tuple: + return self.scale.get_as_tuple() + return self.scale + + def get_transform_as_dict(self): + """ + Gets the transform as a dictionary (used to serialize) + Returns: + dict: Dictionary with the transform data. Keys: "position", "rotation", "scale". Values: tuples (3 floats) + """ + transform_dict = {"position": self.get_position(as_tuple=True), + "rotation": self.get_rotation(as_tuple=True), + "scale": self.get_scale(as_tuple=True), + } + return transform_dict + + # -------------------------------------------------- Transform End ------------------------------------------------ diff --git a/tests/__init__.py b/tests/__init__.py index 300f810f..16641353 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -45,20 +45,25 @@ test_utils.test_anim_utils, test_utils.test_attr_utils, test_utils.test_color_utils, + test_utils.test_camera_utils, + test_utils.test_cleanup_utils, test_utils.test_constraint_utils, test_utils.test_control_data, test_utils.test_control_utils, test_utils.test_curve_utils, test_utils.test_data_utils, + test_utils.test_display_utils, test_utils.test_feedback_utils, + test_utils.test_hierarchy_utils, test_utils.test_iterable_utils, + test_utils.test_joint_utils, test_utils.test_math_utils, test_utils.test_namespace_utils, test_utils.test_naming_utils, test_utils.test_playblast_utils, test_utils.test_prefs_utils, - test_utils.test_proxy_utils, test_utils.test_request_utils, + test_utils.test_rigger_utils, test_utils.test_scene_utils, test_utils.test_session_utils, test_utils.test_setup_utils, diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py index c6223260..26144f23 100644 --- a/tests/test_utils/__init__.py +++ b/tests/test_utils/__init__.py @@ -1,21 +1,26 @@ from . import test_alembic_utils from . import test_anim_utils from . import test_attr_utils +from . import test_camera_utils +from . import test_cleanup_utils from . import test_color_utils from . import test_constraint_utils from . import test_control_data from . import test_control_utils from . import test_curve_utils from . import test_data_utils +from . import test_display_utils from . import test_feedback_utils +from . import test_hierarchy_utils from . import test_iterable_utils +from . import test_joint_utils from . import test_math_utils from . import test_namespace_utils from . import test_naming_utils from . import test_playblast_utils from . import test_prefs_utils -from . import test_proxy_utils from . import test_request_utils +from . import test_rigger_utils from . import test_scene_utils from . import test_session_utils from . import test_setup_utils diff --git a/tests/test_utils/test_alembic_utils.py b/tests/test_utils/test_alembic_utils.py index a2122865..d0ca5fa4 100644 --- a/tests/test_utils/test_alembic_utils.py +++ b/tests/test_utils/test_alembic_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_anim_utils.py b/tests/test_utils/test_anim_utils.py index 3e2fd437..de96bcba 100644 --- a/tests/test_utils/test_anim_utils.py +++ b/tests/test_utils/test_anim_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_attr_utils.py b/tests/test_utils/test_attr_utils.py index 196e6bc3..49a507eb 100644 --- a/tests/test_utils/test_attr_utils.py +++ b/tests/test_utils/test_attr_utils.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_camera_utils.py b/tests/test_utils/test_camera_utils.py index dd021e80..8657b2e7 100644 --- a/tests/test_utils/test_camera_utils.py +++ b/tests/test_utils/test_camera_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_cleanup_utils.py b/tests/test_utils/test_cleanup_utils.py new file mode 100644 index 00000000..465c4e1a --- /dev/null +++ b/tests/test_utils/test_cleanup_utils.py @@ -0,0 +1,106 @@ +import os +import sys +import logging +import unittest + +# Logging Setup +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Import Utility and Maya Test Tools +test_utils_dir = os.path.dirname(__file__) +tests_dir = os.path.dirname(test_utils_dir) +package_root_dir = os.path.dirname(tests_dir) +for to_append in [package_root_dir, tests_dir]: + if to_append not in sys.path: + sys.path.append(to_append) +from tests import maya_test_tools +from gt.utils import cleanup_utils + + +class TestCleanUpUtils(unittest.TestCase): + def setUp(self): + maya_test_tools.force_new_scene() + + @classmethod + def setUpClass(cls): + maya_test_tools.import_maya_standalone(initialize=True) # Start Maya Headless (mayapy.exe) + + def test_delete_unused_nodes(self): + node = maya_test_tools.cmds.createNode("multiplyDivide") + num_deleted_nodes = cleanup_utils.delete_unused_nodes(verbose=False) + result = maya_test_tools.cmds.objExists(node) + self.assertFalse(result, f'Expected to not find "{node}. But it was found."') + expected_num_deleted_nodes = 1 + self.assertEqual(expected_num_deleted_nodes, num_deleted_nodes) + + def test_delete_nucleus_nodes(self): + types_to_test = ['nParticle', + 'spring', + 'particle', + 'nRigid', + 'nCloth', + 'pfxHair', + 'hairSystem', + 'dynamicConstraint', + 'pointEmitter', + 'nucleus', + 'instancer'] + nodes = [] + for node_type in types_to_test: + new_node = maya_test_tools.cmds.createNode(node_type) + nodes.append(new_node) + exists_result = maya_test_tools.cmds.objExists(new_node) + self.assertTrue(exists_result, f'Missing expected node: "{str(new_node)}".') + + num_deleted_nodes = cleanup_utils.delete_nucleus_nodes(verbose=False, include_fields=False) + expected_num_deleted_nodes = len(types_to_test) + self.assertEqual(expected_num_deleted_nodes, num_deleted_nodes) + + for node in nodes: + exists_result = maya_test_tools.cmds.objExists(node) + self.assertFalse(exists_result, f'Found unexpected node: "{node}".') + + def test_delete_nucleus_nodes_include_fields(self): + types_to_test = ['airField', + 'dragField', + 'newtonField', + 'radialField', + 'turbulenceField', + 'uniformField', + 'vortexField', + 'volumeAxisField'] + nodes = [] + for node_type in types_to_test: + new_node = maya_test_tools.cmds.createNode(node_type) + nodes.append(new_node) + exists_result = maya_test_tools.cmds.objExists(new_node) + self.assertTrue(exists_result, f'Missing expected node: "{str(new_node)}".') + + num_deleted_nodes = cleanup_utils.delete_nucleus_nodes(verbose=False, include_fields=False) + expected_num_deleted_nodes = 0 + self.assertEqual(expected_num_deleted_nodes, num_deleted_nodes) + num_deleted_nodes = cleanup_utils.delete_nucleus_nodes(verbose=False, include_fields=True) + expected_num_deleted_nodes = len(types_to_test) + self.assertEqual(expected_num_deleted_nodes, num_deleted_nodes) + + for node in nodes: + exists_result = maya_test_tools.cmds.objExists(node) + self.assertFalse(exists_result, f'Found unexpected node: "{node}".') + + def test_delete_all_locators(self): + mocked_locators = [] + for node_type in range(0, 10): + new_loc = maya_test_tools.cmds.spaceLocator()[0] + mocked_locators.append(new_loc) + exists_result = maya_test_tools.cmds.objExists(new_loc) + self.assertTrue(exists_result, f'Missing expected node: "{str(new_loc)}".') + + num_deleted_nodes = cleanup_utils.delete_locators(verbose=False, filter_str=None) + expected_num_deleted_nodes = 10 + self.assertEqual(expected_num_deleted_nodes, num_deleted_nodes) + + for node in mocked_locators: + exists_result = maya_test_tools.cmds.objExists(node) + self.assertFalse(exists_result, f'Found unexpected node: "{node}".') diff --git a/tests/test_utils/test_color_utils.py b/tests/test_utils/test_color_utils.py index a88528c8..96f54385 100644 --- a/tests/test_utils/test_color_utils.py +++ b/tests/test_utils/test_color_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) @@ -27,6 +27,18 @@ def setUp(self): def setUpClass(cls): maya_test_tools.import_maya_standalone(initialize=True) # Start Maya Headless (mayapy.exe) + def test_color_constants_class(self): + attributes = vars(color_utils.ColorConstants) + keys = [attr for attr in attributes if not (attr.startswith('__') and attr.endswith('__'))] + for clr_key in keys: + color = getattr(color_utils.ColorConstants, clr_key) + if not color: + raise Exception(f'Missing color: {clr_key}') + if not isinstance(color, tuple): + raise Exception(f'Incorrect color type. Expected tuple, but got: "{type(color)}".') + if len(color) != 3: + raise Exception(f'Incorrect color length. Expected 3, but got: "{str(len(color))}".') + def test_set_color_override_viewport(self): test_obj = 'test_cube' maya_test_tools.create_poly_cube(name=test_obj) @@ -42,3 +54,31 @@ def test_set_color_override_outliner(self): expected = (0, 0.5, 1) result = color_utils.set_color_override_outliner(test_obj, rgb_color=expected) self.assertEqual(expected, result) + + def test_add_side_color_setup(self): + test_obj = 'test_cube' + maya_test_tools.create_poly_cube(name=test_obj) + + color_utils.add_side_color_setup(obj=test_obj, color_attr_name="autoColor") + + expected_bool_attrs = ['autoColor'] + expected_double_attrs = ['colorDefault', 'colorRight', 'colorLeft'] + for attr in expected_bool_attrs + expected_double_attrs: + self.assertTrue(maya_test_tools.cmds.objExists(f'{test_obj}.{attr}'), + f'Missing expected attribute: "{attr}".') + + expected = 'bool' + for attr in expected_bool_attrs: + attr_type = maya_test_tools.cmds.getAttr(f'{test_obj}.{attr}', type=True) + self.assertEqual(expected, attr_type) + expected = 'double3' + for attr in expected_double_attrs: + attr_type = maya_test_tools.cmds.getAttr(f'{test_obj}.{attr}', type=True) + self.assertEqual(expected, attr_type) + + expected = True + result = maya_test_tools.cmds.getAttr(f'{test_obj}.overrideEnabled') + self.assertEqual(expected, result) + expected = 1 + result = maya_test_tools.cmds.getAttr(f'{test_obj}.overrideRGBColors') + self.assertEqual(expected, result) diff --git a/tests/test_utils/test_constraint_utils.py b/tests/test_utils/test_constraint_utils.py index 890eaf95..7de148e6 100644 --- a/tests/test_utils/test_constraint_utils.py +++ b/tests/test_utils/test_constraint_utils.py @@ -1,8 +1,5 @@ -from unittest.mock import patch -from io import StringIO import unittest import logging -import json import sys import os @@ -11,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_control_data.py b/tests/test_utils/test_control_data.py index cdbe2d7c..7d7df16d 100644 --- a/tests/test_utils/test_control_data.py +++ b/tests/test_utils/test_control_data.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) @@ -141,3 +141,61 @@ def test_create_scalable_two_sides_arrow(self): expected = "mocked_scalable_arrow" self.assertEqual(expected, result.name) self.assertIsInstance(result, ControlData) + + def test_create_slider_squared_one_dimension(self): + expected = "mocked_slider_one_dimension" + result = slider.create_slider_squared_one_dimension(name=expected) + self.assertEqual(expected, result.name) + self.assertIsInstance(result, ControlData) + + def test_create_slider_squared_two_dimensions(self): + expected = "mocked_create_slider_squared_two_dimensions" + result = slider.create_slider_squared_two_dimensions(name=expected) + self.assertEqual(expected, result.name) + self.assertIsInstance(result, ControlData) + + def test_create_sliders_squared_mouth(self): + result = slider.create_sliders_squared_mouth(name="mocked_mouth") + expected = "mocked_mouth_gui_grp" + self.assertEqual(expected, result.name) + self.assertIsInstance(result, ControlData) + + def test_create_sliders_squared_eyebrows(self): + result = slider.create_sliders_squared_eyebrows(name="mocked_eyebrow") + expected = "mocked_eyebrow_gui_grp" + self.assertEqual(expected, result.name) + self.assertIsInstance(result, ControlData) + + def test_create_sliders_squared_cheek_nose(self): + result = slider.create_sliders_squared_cheek_nose(name="mocked_nose_cheek") + expected = "mocked_nose_cheek_gui_grp" + self.assertEqual(expected, result.name) + self.assertIsInstance(result, ControlData) + + def test_create_sliders_squared_eye(self): + result = slider.create_sliders_squared_eyes(name="mocked_eyes") + expected = "mocked_eyes_gui_grp" + self.assertEqual(expected, result.name) + self.assertIsInstance(result, ControlData) + + def test_create_sliders_squared_facial_side_gui(self): + result = slider.create_sliders_squared_facial_side_gui(name="mocked_facial") + expected = "mocked_facial_gui_grp" + self.assertEqual(expected, result.name) + self.assertIsInstance(result, ControlData) + + def test_offset_slider_range(self): + offset_ctrl = slider.create_slider_squared_one_dimension('offset_ctrl') + expected = 5 + for driver in offset_ctrl.drivers: + min_trans_y_limit = maya_test_tools.cmds.getAttr(f'{driver}.minTransYLimit') + max_trans_y_limit = maya_test_tools.cmds.getAttr(f'{driver}.maxTransYLimit') + self.assertEqual(expected, max_trans_y_limit) + self.assertEqual(-expected, min_trans_y_limit) + slider._offset_slider_range(slider_control_data=offset_ctrl, offset_by=5, offset_thickness=1) + expected = 10 + for driver in offset_ctrl.drivers: + min_trans_y_limit = maya_test_tools.cmds.getAttr(f'{driver}.minTransYLimit') + max_trans_y_limit = maya_test_tools.cmds.getAttr(f'{driver}.maxTransYLimit') + self.assertEqual(expected, max_trans_y_limit) + self.assertEqual(-expected, min_trans_y_limit) diff --git a/tests/test_utils/test_control_utils.py b/tests/test_utils/test_control_utils.py index 0a77d330..d6caec66 100644 --- a/tests/test_utils/test_control_utils.py +++ b/tests/test_utils/test_control_utils.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_curve_utils.py b/tests/test_utils/test_curve_utils.py index 1d1b627d..a1ba07ea 100644 --- a/tests/test_utils/test_curve_utils.py +++ b/tests/test_utils/test_curve_utils.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_data_utils.py b/tests/test_utils/test_data_utils.py index d05a9684..803cfe26 100644 --- a/tests/test_utils/test_data_utils.py +++ b/tests/test_utils/test_data_utils.py @@ -1,3 +1,4 @@ +from unittest.mock import patch import unittest import logging import json @@ -10,7 +11,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) @@ -319,4 +320,12 @@ def test_make_empty_file(self): temp_file = os.path.join(test_temp_dir, "temp_file.temp") data_utils.make_empty_file(temp_file) self.assertTrue(os.path.exists(temp_file)) - self.assertTrue(os.path.isfile(temp_file)) \ No newline at end of file + self.assertTrue(os.path.isfile(temp_file)) + + @patch('os.chmod') + @patch('os.unlink') + def test_on_rm_error(self, mock_chmod, mock_unlink): + test_temp_dir = maya_test_tools.generate_test_temp_dir() + data_utils.on_rm_error(func=None, file_path=test_temp_dir, exc_info=(None,)) + mock_chmod.assert_called() + mock_unlink.assert_called() diff --git a/tests/test_utils/test_display_utils.py b/tests/test_utils/test_display_utils.py new file mode 100644 index 00000000..d1496a4d --- /dev/null +++ b/tests/test_utils/test_display_utils.py @@ -0,0 +1,163 @@ +from unittest.mock import patch, MagicMock +from io import StringIO +import unittest +import logging +import sys +import os + +# Logging Setup +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Import Utility and Maya Test Tools +test_utils_dir = os.path.dirname(__file__) +tests_dir = os.path.dirname(test_utils_dir) +package_root_dir = os.path.dirname(tests_dir) +for to_append in [package_root_dir, tests_dir]: + if to_append not in sys.path: + sys.path.append(to_append) +from tests import maya_test_tools +from gt.utils import display_utils + + +class TestDisplayUtils(unittest.TestCase): + def setUp(self): + maya_test_tools.force_new_scene() + + @classmethod + def setUpClass(cls): + maya_test_tools.import_maya_standalone(initialize=True) # Start Maya Headless (mayapy.exe)\ + + def test_toggle_uniform_lra(self): + cube = maya_test_tools.create_poly_cube() + lra_visibility_result = display_utils.toggle_uniform_lra(obj_list=cube, verbose=False) + result = maya_test_tools.cmds.getAttr(f'{cube}.displayLocalAxis') + expected = True + self.assertEqual(expected, lra_visibility_result) + self.assertEqual(expected, result) + lra_visibility_result = display_utils.toggle_uniform_lra(obj_list=cube, verbose=False) + result = maya_test_tools.cmds.getAttr(f'{cube}.displayLocalAxis') + expected = False + self.assertEqual(expected, lra_visibility_result) + self.assertEqual(expected, result) + + def test_toggle_uniform_jnt_label(self): + joint = maya_test_tools.cmds.joint() + label_visibility_state = display_utils.toggle_uniform_jnt_label(jnt_list=joint, verbose=False) + result = maya_test_tools.cmds.getAttr(f'{joint}.drawLabel') + expected = True + self.assertEqual(expected, label_visibility_state) + self.assertEqual(expected, result) + label_visibility_state = display_utils.toggle_uniform_jnt_label(jnt_list=joint, verbose=False) + result = maya_test_tools.cmds.getAttr(f'{joint}.drawLabel') + expected = False + self.assertEqual(expected, label_visibility_state) + self.assertEqual(expected, result) + + @patch('gt.utils.display_utils.cmds') + @patch('gt.utils.display_utils.mel') + def test_toggle_full_hud(self, mock_mel, mock_cmds): + mock_eval = MagicMock() + mock_mel.eval = mock_eval + label_visibility_state = display_utils.toggle_full_hud(verbose=False) + mock_eval.assert_called() + expected = False + self.assertEqual(expected, label_visibility_state) + + def test_set_joint_name_as_label(self): + joint_one = maya_test_tools.cmds.joint() + joint_two = maya_test_tools.cmds.joint() + joints_to_test = [joint_one, joint_two] + + for jnt in joints_to_test: + side_value = maya_test_tools.cmds.getAttr(f'{jnt}.side') + expected = 0 # Center + self.assertEqual(expected, side_value) + type_value = maya_test_tools.cmds.getAttr(f'{jnt}.type') + expected = 0 # None + self.assertEqual(expected, type_value) + label_value = maya_test_tools.cmds.getAttr(f'{jnt}.otherType') + expected = "jaw" + self.assertEqual(expected, label_value) + # Update Label + affected_joints = display_utils.set_joint_name_as_label(jnt_list=joints_to_test, verbose=False) + expected = 2 + self.assertEqual(expected, affected_joints) + for jnt in joints_to_test: + side_value = maya_test_tools.cmds.getAttr(f'{jnt}.side') + expected = 0 # Center + self.assertEqual(expected, side_value) + type_value = maya_test_tools.cmds.getAttr(f'{jnt}.type') + expected = 18 # Other + self.assertEqual(expected, type_value) + label_value = maya_test_tools.cmds.getAttr(f'{jnt}.otherType') + expected = jnt + self.assertEqual(expected, label_value) + + @patch('gt.utils.display_utils.mel') + def test_generate_udim_previews(self, mock_mel): + for index in range(0, 10): + maya_test_tools.cmds.createNode("file") + mock_eval = MagicMock() + mock_mel.eval = mock_eval + affected_nodes = display_utils.generate_udim_previews(verbose=False) + mock_eval.assert_called() + expected = 10 + self.assertEqual(expected, affected_nodes) + + def test_reset_joint_display(self): + joint_one = maya_test_tools.cmds.joint() + joint_two = maya_test_tools.cmds.joint() + maya_test_tools.cmds.joint() # Purposely left out of affected joints + joints_to_test = [joint_one, joint_two] + + expected = 2 + maya_test_tools.cmds.jointDisplayScale(expected) # Something other than 1 + display_scale = maya_test_tools.cmds.jointDisplayScale(query=True) + self.assertEqual(expected, display_scale) + + for jnt in joints_to_test: + expected = 2 + maya_test_tools.cmds.setAttr(f'{jnt}.radius', expected) # Something other than 1 + radius_value = maya_test_tools.cmds.getAttr(f'{jnt}.radius') + self.assertEqual(expected, radius_value) + expected = 2 + maya_test_tools.cmds.setAttr(f'{jnt}.drawStyle', expected) # None + draw_style_value = maya_test_tools.cmds.getAttr(f'{jnt}.drawStyle') + self.assertEqual(expected, draw_style_value) + expected = False + maya_test_tools.cmds.setAttr(f'{jnt}.visibility', expected) + visibility_value = maya_test_tools.cmds.getAttr(f'{jnt}.visibility') + self.assertEqual(expected, visibility_value) + # Update Label + affected_joints = display_utils.reset_joint_display(jnt_list=joints_to_test, verbose=False) + expected_affected_joints = 2 + self.assertEqual(expected_affected_joints, affected_joints) + + expected_display_scale = 1 + display_scale = maya_test_tools.cmds.jointDisplayScale(query=True) + self.assertEqual(expected_display_scale, display_scale) + + for jnt in joints_to_test: + expected = 1 + maya_test_tools.cmds.setAttr(f'{jnt}.radius', expected) # Something other than 1 + radius_value = maya_test_tools.cmds.getAttr(f'{jnt}.radius') + self.assertEqual(expected, radius_value) + expected = 1 + maya_test_tools.cmds.setAttr(f'{jnt}.drawStyle', expected) # None + draw_style_value = maya_test_tools.cmds.getAttr(f'{jnt}.drawStyle') + self.assertEqual(expected, draw_style_value) + expected = True + maya_test_tools.cmds.setAttr(f'{jnt}.visibility', expected) + visibility_value = maya_test_tools.cmds.getAttr(f'{jnt}.visibility') + self.assertEqual(expected, visibility_value) + + def test_delete_display_layers(self): + display_layer_one = maya_test_tools.cmds.createDisplayLayer(name="mocked_layer_one", empty=True) + display_layer_two = maya_test_tools.cmds.createDisplayLayer(name="mocked_layer_two", empty=True) + affected_layers = display_utils.delete_display_layers(layer_list=display_layer_one, verbose=False) + expected_affected_layers = 1 + self.assertEqual(expected_affected_layers, affected_layers) + self.assertFalse(maya_test_tools.cmds.objExists(display_layer_one), "Found unexpected layer.") + self.assertTrue(maya_test_tools.cmds.objExists(display_layer_two), "Missing expected layer.") diff --git a/tests/test_utils/test_feedback_utils.py b/tests/test_utils/test_feedback_utils.py index 930eca3f..085156c8 100644 --- a/tests/test_utils/test_feedback_utils.py +++ b/tests/test_utils/test_feedback_utils.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_hierarchy_utils.py b/tests/test_utils/test_hierarchy_utils.py new file mode 100644 index 00000000..a17c9da1 --- /dev/null +++ b/tests/test_utils/test_hierarchy_utils.py @@ -0,0 +1,62 @@ +import unittest +import logging +import sys +import os + +# Logging Setup +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Import Utility and Maya Test Tools +test_utils_dir = os.path.dirname(__file__) +tests_dir = os.path.dirname(test_utils_dir) +package_root_dir = os.path.dirname(tests_dir) +for to_append in [package_root_dir, tests_dir]: + if to_append not in sys.path: + sys.path.append(to_append) +from tests import maya_test_tools +from gt.utils import hierarchy_utils + + +class TestHierarchyUtils(unittest.TestCase): + def setUp(self): + maya_test_tools.force_new_scene() + self.cube_one = maya_test_tools.create_poly_cube(name="cube_one") + self.cube_two = maya_test_tools.create_poly_cube(name="cube_two") + self.cube_three = maya_test_tools.create_poly_cube(name="cube_three") + self.transform_one = maya_test_tools.cmds.group(name="transform_one", empty=True, world=True) + self.transform_two = maya_test_tools.cmds.group(name="transform_two", empty=True, world=True) + self.cubes = [self.cube_one, self.cube_two, self.cube_three] + self.transforms = [self.transform_one, self.transform_two] + + @classmethod + def setUpClass(cls): + maya_test_tools.import_maya_standalone(initialize=True) # Start Maya Headless (mayapy.exe) + + def test_parent_basics(self): + result = hierarchy_utils.parent(source_objects=self.cubes, target_parent=self.transform_one) + expected = [] + for cube in self.cubes: + expected.append(f"|{self.transform_one}|{cube}") + self.assertEqual(expected, result) + expected = self.cubes + children = maya_test_tools.cmds.listRelatives(self.transform_one, children=True) + self.assertEqual(expected, children) + + def test_parent_str_input(self): + result = hierarchy_utils.parent(source_objects=self.cube_one, target_parent=self.transform_one) + expected = [f'|{self.transform_one}|{self.cube_one}'] + self.assertEqual(expected, result) + expected = [self.cube_one] + children = maya_test_tools.cmds.listRelatives(self.transform_one, children=True) + self.assertEqual(expected, children) + + def test_parent_non_unique(self): + hierarchy_utils.parent(source_objects=self.cube_one, target_parent=self.transform_one) + maya_test_tools.cmds.rename(self.cube_two, "cube_one") + result = hierarchy_utils.parent(source_objects="|cube_one", target_parent=self.transform_two) + expected = [f"|{self.transform_two}|cube_one"] + self.assertEqual(expected, result) + children = maya_test_tools.cmds.listRelatives(self.transform_two, children=True, fullPath=True) + self.assertEqual(expected, children) diff --git a/tests/test_utils/test_iterable_utils.py b/tests/test_utils/test_iterable_utils.py index 2bbdda72..68b433bf 100644 --- a/tests/test_utils/test_iterable_utils.py +++ b/tests/test_utils/test_iterable_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Test String Utilities +# Import Utility tools_root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) if tools_root_dir not in sys.path: sys.path.append(tools_root_dir) diff --git a/tests/test_utils/test_joint_utils.py b/tests/test_utils/test_joint_utils.py new file mode 100644 index 00000000..05a11ce6 --- /dev/null +++ b/tests/test_utils/test_joint_utils.py @@ -0,0 +1,49 @@ +import unittest +import logging +import sys +import os + +# Logging Setup +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Import Utility and Maya Test Tools +test_utils_dir = os.path.dirname(__file__) +tests_dir = os.path.dirname(test_utils_dir) +package_root_dir = os.path.dirname(tests_dir) +for to_append in [package_root_dir, tests_dir]: + if to_append not in sys.path: + sys.path.append(to_append) +from tests import maya_test_tools +from gt.utils import joint_utils + + +class TestJointUtils(unittest.TestCase): + def setUp(self): + maya_test_tools.force_new_scene() + + @classmethod + def setUpClass(cls): + maya_test_tools.import_maya_standalone(initialize=True) # Start Maya Headless (mayapy.exe) + + def test_convert_joints_to_mesh_selected_one(self): + joint = maya_test_tools.cmds.joint() + maya_test_tools.cmds.select(joint) + result = joint_utils.convert_joints_to_mesh() + expected = [f'{joint}JointMesh'] + self.assertEqual(expected, result) + + def test_convert_joints_to_mesh_selected_hierarchy(self): + joint_one = maya_test_tools.cmds.joint() + maya_test_tools.cmds.joint() + maya_test_tools.cmds.select(joint_one) + result = joint_utils.convert_joints_to_mesh() + expected = [f'{joint_one}AsMesh'] + self.assertEqual(expected, result) + + def test_convert_joints_to_mesh_str_input(self): + joint_one = maya_test_tools.cmds.joint() + result = joint_utils.convert_joints_to_mesh(root_jnt=joint_one) + expected = [f'{joint_one}JointMesh'] + self.assertEqual(expected, result) diff --git a/tests/test_utils/test_math_utils.py b/tests/test_utils/test_math_utils.py index b3fb18a0..4cb083af 100644 --- a/tests/test_utils/test_math_utils.py +++ b/tests/test_utils/test_math_utils.py @@ -1,4 +1,3 @@ -from unittest.mock import MagicMock, patch import unittest import logging import sys @@ -9,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_mesh_utils.py b/tests/test_utils/test_mesh_utils.py index ca757dce..2a0ae2c5 100644 --- a/tests/test_utils/test_mesh_utils.py +++ b/tests/test_utils/test_mesh_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) @@ -275,3 +275,10 @@ def test_is_vertex_string_invalid_strings(self): for input_str in invalid_strings: with self.subTest(input_str=input_str): self.assertFalse(mesh_utils.is_vertex_string(input_str), f"Expected {input_str} to be invalid") + + def test_extract_components_from_face(self): + cube = maya_test_tools.create_poly_cube() + result = mesh_utils.extract_components_from_face(f'{cube}.f[0]') + expected = ("FaceComponents(vertices=['pCube1.vtx[0]', 'pCube1.vtx[1]', 'pCube1.vtx[2]', " + "'pCube1.vtx[3]'], edges=['pCube1.e[0]', 'pCube1.e[1]', 'pCube1.e[4]', 'pCube1.e[5]'])") + self.assertEqual(expected, str(result)) diff --git a/tests/test_utils/test_namespace_utils.py b/tests/test_utils/test_namespace_utils.py index ca84e3f3..1bfac511 100644 --- a/tests/test_utils/test_namespace_utils.py +++ b/tests/test_utils/test_namespace_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_naming_utils.py b/tests/test_utils/test_naming_utils.py index 14a55d55..16548985 100644 --- a/tests/test_utils/test_naming_utils.py +++ b/tests/test_utils/test_naming_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_playblast_utils.py b/tests/test_utils/test_playblast_utils.py index 705f8838..05d864ca 100644 --- a/tests/test_utils/test_playblast_utils.py +++ b/tests/test_utils/test_playblast_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_plugin_utils.py b/tests/test_utils/test_plugin_utils.py index 8dbd0248..3b96c8fb 100644 --- a/tests/test_utils/test_plugin_utils.py +++ b/tests/test_utils/test_plugin_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_prefs_utils.py b/tests/test_utils/test_prefs_utils.py index 27234f3e..abe31352 100644 --- a/tests/test_utils/test_prefs_utils.py +++ b/tests/test_utils/test_prefs_utils.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_request_utils.py b/tests/test_utils/test_request_utils.py index 92413624..bc19f1e3 100644 --- a/tests/test_utils/test_request_utils.py +++ b/tests/test_utils/test_request_utils.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Test Session Utilities +# Import Utility tools_root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) if tools_root_dir not in sys.path: sys.path.append(tools_root_dir) diff --git a/tests/test_utils/test_proxy_utils.py b/tests/test_utils/test_rigger_utils.py similarity index 83% rename from tests/test_utils/test_proxy_utils.py rename to tests/test_utils/test_rigger_utils.py index 6e24fc70..f50e3a5d 100644 --- a/tests/test_utils/test_proxy_utils.py +++ b/tests/test_utils/test_rigger_utils.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) @@ -17,26 +17,28 @@ if to_append not in sys.path: sys.path.append(to_append) from gt.utils.transform_utils import Transform -from gt.utils.proxy_utils import Proxy +from gt.utils.rigger_utils import Proxy from tests import maya_test_tools -from gt.utils import proxy_utils +from gt.utils import rigger_utils class TestProxyUtils(unittest.TestCase): def setUp(self): maya_test_tools.force_new_scene() self.proxy = Proxy() - self.proxy_data = proxy_utils.ProxyData(name="proxy1", offset="offset1", setup=("setup1", "setup2")) + self.proxy.uuid = "123e4567-e89b-12d3-a456-426655440000" + self.proxy_data = rigger_utils.ProxyData(name="proxy1", offset="offset1", setup=("setup1", "setup2"), + uuid="123e4567-e89b-12d3-a456-426655440000") @classmethod def setUpClass(cls): maya_test_tools.import_maya_standalone(initialize=True) # Start Maya Headless (mayapy.exe) def test_proxy_constants(self): - attributes = vars(proxy_utils.ProxyConstants) + attributes = vars(rigger_utils.RiggerConstants) keys = [attr for attr in attributes if not (attr.startswith('__') and attr.endswith('__'))] for key in keys: - constant = getattr(proxy_utils.ProxyConstants, key) + constant = getattr(rigger_utils.RiggerConstants, key) if not constant: raise Exception(f'Missing proxy constant data: {key}') if not isinstance(constant, str): @@ -69,12 +71,12 @@ def test_get_setup(self): def test_proxy_default(self): result = self.proxy.build() - self.assertTrue(self.proxy.is_proxy_valid()) + self.assertTrue(self.proxy.is_valid()) expected = "|proxy_offset|proxy" self.assertEqual(expected, str(result)) expected = "proxy" self.assertEqual(expected, result.get_short_name()) - self.assertTrue(isinstance(result, proxy_utils.ProxyData)) + self.assertTrue(isinstance(result, rigger_utils.ProxyData)) expected = "|proxy_offset" self.assertEqual(expected, result.offset) expected = ("proxy_LocScaleHandle",) @@ -102,7 +104,7 @@ def test_proxy_init(self): self.assertEqual(expected_uuid, proxy.uuid) self.assertEqual(expected_uuid, proxy.parent_uuid) self.assertEqual(expected_metadata, proxy.metadata) - self.assertTrue(proxy.is_proxy_valid()) + self.assertTrue(proxy.is_valid()) def test_proxy_build(self): result = self.proxy.build() @@ -110,16 +112,16 @@ def test_proxy_build(self): expected_short_name = "proxy" self.assertEqual(expected_long_name, str(result)) self.assertEqual(expected_short_name, result.get_short_name()) - self.assertTrue(isinstance(result, proxy_utils.ProxyData)) - self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{proxy_utils.ProxyConstants.SEPARATOR_ATTR}')) - self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{proxy_utils.ProxyConstants.PROXY_ATTR_UUID}')) - self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{proxy_utils.ProxyConstants.PROXY_ATTR_UUID}')) + self.assertTrue(isinstance(result, rigger_utils.ProxyData)) + self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{rigger_utils.RiggerConstants.SEPARATOR_ATTR}')) + self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{rigger_utils.RiggerConstants.PROXY_ATTR_UUID}')) + self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{rigger_utils.RiggerConstants.PROXY_ATTR_UUID}')) def test_proxy_custom_curve(self): from gt.utils.curve_utils import Curves proxy = Proxy(curve=Curves.circle) result = proxy.build() - self.assertTrue(proxy.is_proxy_valid()) + self.assertTrue(proxy.is_valid()) expected = "proxy" self.assertEqual(expected, result.get_short_name()) @@ -128,6 +130,18 @@ def test_proxy_get_name_default(self): expected = "proxy" self.assertEqual(expected, result) + def test_proxy_get_uuid_default(self): + expected_uuid = "123e4567-e89b-12d3-a456-426655440000" + proxy = Proxy(uuid=expected_uuid) + result = proxy.get_uuid() + self.assertEqual(expected_uuid, result) + + def test_proxy_get_parent_uuid_default(self): + expected_parent_uuid = "123e4567-e89b-12d3-a456-426655440002" + proxy = Proxy(parent_uuid=expected_parent_uuid) + result = proxy.get_parent_uuid() + self.assertEqual(expected_parent_uuid, result) + def test_proxy_set_name(self): self.proxy.set_name("description") result = self.proxy.get_name() @@ -208,6 +222,12 @@ def test_proxy_set_locator_scale(self): expected = 2 self.assertEqual(expected, result) + def test_proxy_set_attr_dict(self): + expected = {"attrName": 2} + self.proxy.set_attr_dict(expected) + result = self.proxy.attr_dict + self.assertEqual(expected, result) + def test_proxy_metadata_default(self): result = self.proxy.metadata expected = None @@ -255,6 +275,12 @@ def test_proxy_set_parent_uuid_valid(self): result = self.proxy.parent_uuid self.assertEqual(valid_uuid, result) + def test_proxy_set_parent_uuid_from_proxy(self): + mocked_proxy = Proxy() + self.proxy.set_parent_uuid_from_proxy(mocked_proxy) + result = self.proxy.parent_uuid + self.assertEqual(mocked_proxy.uuid, result) + def test_proxy_get_metadata(self): mocked_dict = {"metadata_key": "metadata_value"} self.proxy.set_metadata_dict(mocked_dict) diff --git a/tests/test_utils/test_scene_utils.py b/tests/test_utils/test_scene_utils.py index c3253b8c..f675e9e4 100644 --- a/tests/test_utils/test_scene_utils.py +++ b/tests/test_utils/test_scene_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_session_utils.py b/tests/test_utils/test_session_utils.py index 7a4e3bb7..cc085d70 100644 --- a/tests/test_utils/test_session_utils.py +++ b/tests/test_utils/test_session_utils.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Test Session Utilities +# Import Utility and Maya Test Tools tools_root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) if tools_root_dir not in sys.path: sys.path.append(tools_root_dir) diff --git a/tests/test_utils/test_setup_utils.py b/tests/test_utils/test_setup_utils.py index be857819..a814b5d5 100644 --- a/tests/test_utils/test_setup_utils.py +++ b/tests/test_utils/test_setup_utils.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_skin_utils.py b/tests/test_utils/test_skin_utils.py index 7a51fef2..996a490c 100644 --- a/tests/test_utils/test_skin_utils.py +++ b/tests/test_utils/test_skin_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_string_utils.py b/tests/test_utils/test_string_utils.py index dc586918..f1e27a53 100644 --- a/tests/test_utils/test_string_utils.py +++ b/tests/test_utils/test_string_utils.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Test Session Utilities +# Import Utility tools_root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) if tools_root_dir not in sys.path: sys.path.append(tools_root_dir) diff --git a/tests/test_utils/test_system_utils.py b/tests/test_utils/test_system_utils.py index 64ff9453..c594855b 100644 --- a/tests/test_utils/test_system_utils.py +++ b/tests/test_utils/test_system_utils.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) @@ -159,8 +159,7 @@ def test_get_maya_preferences_dir_win32(self): def test_get_maya_preferences_dir_mac(self): result = system_utils.get_maya_preferences_dir(system=system_utils.OS_MAC) - generated_path = os.path.join(os.path.expanduser('~'), "Library", "Preferences", "Autodesk", "maya") - expected = os.path.normpath(generated_path) + expected = os.path.join(os.path.expanduser('~'), "Library", "Preferences", "Autodesk", "maya") self.assertEqual(expected, result) @patch('gt.utils.system_utils.get_maya_preferences_dir') @@ -648,7 +647,9 @@ def test_execution_success(self): def test_execution_error_without_raise(self): code = "result = 1 / 0" expected = None # Exception caught, no error raised + logging.disable(logging.WARNING) result = system_utils.execute_python_code(code) + logging.disable(logging.NOTSET) self.assertEqual(expected, result) def test_execution_error_with_raise(self): @@ -660,3 +661,30 @@ def test_log_and_raise(self): code = "result = 1 / 0" with self.assertRaises(ZeroDivisionError): system_utils.execute_python_code(code, raise_errors=True, verbose=True) + + def test_create_object_from_local_namespace(self): + class MyClass: + pass + obj = system_utils.create_object("MyClass", class_path=locals()) + self.assertIsInstance(obj, MyClass) + + def test_create_object_with_module_path(self): + # Test creating an object by specifying a module path + obj = system_utils.create_object("JSONDecoder", class_path="json") + import json + self.assertIsInstance(obj, json.JSONDecoder) + + def test_create_object_with_invalid_module_path(self): + # Test creating an object with an invalid module path + with self.assertRaises(ImportError): + system_utils.create_object("MyClass", class_path="non_existent_module") + + def test_create_object_with_missing_class_in_module(self): + # Test creating an object when the class is missing in the module + with self.assertRaises(NameError): + system_utils.create_object("NonExistentClass") + + def test_create_object_with_warning(self): + logging.disable(logging.WARNING) + system_utils.create_object("NonExistentClass", raise_errors=False) + logging.disable(logging.NOTSET) diff --git a/tests/test_utils/test_transform_utils.py b/tests/test_utils/test_transform_utils.py index 87e211b8..077f337b 100644 --- a/tests/test_utils/test_transform_utils.py +++ b/tests/test_utils/test_transform_utils.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) @@ -169,6 +169,25 @@ def test_Vector3_greater_than_or_equal(self): self.assertFalse(v1 >= v2) self.assertTrue(v2 >= v1) self.assertTrue(v1 >= v1) + + def test_Vector3_x_setter(self): + vector = transform_utils.Vector3(0, 0, 0) + vector.set_x(x=10) + expected = 10 + self.assertEqual(expected, vector.x) + + def test_Vector3_y_setter(self): + vector = transform_utils.Vector3(0, 0, 0) + vector.set_y(y=10) + expected = 10 + self.assertEqual(expected, vector.y) + + def test_Vector3_z_setter(self): + vector = transform_utils.Vector3(0, 0, 0) + vector.set_z(z=10) + expected = 10 + self.assertEqual(expected, vector.z) + # --------------------------------------------------- Vector3 End ------------------------------------------------- # ------------------------------------------------- Transform Start ----------------------------------------------- @@ -186,8 +205,8 @@ def test_transform_class_as_string(self): def test_transform_class_position_as_list(self): vector3_object = transform_utils.Vector3(x=1.2, y=3.4, z=5.6) transform_object = transform_utils.Transform(position=vector3_object, - rotation=vector3_object, - scale=vector3_object) + rotation=vector3_object, + scale=vector3_object) result = [transform_object.position.x, transform_object.position.y, transform_object.position.z] expected = [1.2, 3.4, 5.6] self.assertEqual(expected, result) @@ -195,8 +214,8 @@ def test_transform_class_position_as_list(self): def test_transform_class_rotation_as_list(self): vector3_object = transform_utils.Vector3(x=30, y=-45, z=90) transform_object = transform_utils.Transform(position=vector3_object, - rotation=vector3_object, - scale=vector3_object) + rotation=vector3_object, + scale=vector3_object) result = [transform_object.rotation.x, transform_object.rotation.y, transform_object.rotation.z] expected = [30, -45, 90] self.assertEqual(expected, result) @@ -204,8 +223,8 @@ def test_transform_class_rotation_as_list(self): def test_transform_class_scale_as_list(self): vector3_object = transform_utils.Vector3(x=1, y=2, z=3) transform_object = transform_utils.Transform(position=vector3_object, - rotation=vector3_object, - scale=vector3_object) + rotation=vector3_object, + scale=vector3_object) result = [transform_object.scale.x, transform_object.scale.y, transform_object.scale.z] expected = [1, 2, 3] self.assertEqual(expected, result) @@ -213,11 +232,11 @@ def test_transform_class_scale_as_list(self): def test_transform_class_equality_one(self): vector3_object = transform_utils.Vector3(x=1, y=2, z=3) transform_object_one = transform_utils.Transform(position=vector3_object, - rotation=vector3_object, - scale=vector3_object) + rotation=vector3_object, + scale=vector3_object) transform_object_two = transform_utils.Transform(position=vector3_object, - rotation=vector3_object, - scale=vector3_object) + rotation=vector3_object, + scale=vector3_object) result = transform_object_one == transform_object_two expected = True self.assertEqual(expected, result) @@ -226,11 +245,11 @@ def test_transform_class_equality_two(self): vector3_object_one = transform_utils.Vector3(x=1, y=2, z=3) vector3_object_two = transform_utils.Vector3(x=4, y=5, z=6) transform_object_one = transform_utils.Transform(position=vector3_object_one, - rotation=vector3_object_one, - scale=vector3_object_one) + rotation=vector3_object_one, + scale=vector3_object_one) transform_object_two = transform_utils.Transform(position=vector3_object_two, - rotation=vector3_object_two, - scale=vector3_object_two) + rotation=vector3_object_two, + scale=vector3_object_two) result = transform_object_one == transform_object_two expected = False self.assertEqual(expected, result) @@ -329,6 +348,72 @@ def test_transform_set_scale_arg(self): transform.set_scale(2, 2, 2) self.assertEqual(new_scale, transform.scale) + def test_transform_set_position_fewer_channels(self): + transform = transform_utils.Transform() + new_position = transform_utils.Vector3(1, 2, 3) + transform.set_position(xyz=new_position.get_as_tuple()) + transform.set_position(x=10) + new_position.set_x(x=10) + self.assertEqual(new_position, transform.position) + transform.set_position(y=15) + new_position.set_y(y=15) + self.assertEqual(new_position, transform.position) + transform.set_position(z=20) + new_position.set_z(z=20) + self.assertEqual(new_position, transform.position) + transform.set_position(x=0, z=20) + new_position.set_x(x=0) + new_position.set_z(z=20) + self.assertEqual(new_position, transform.position) + transform.set_position(x=5, y=10) + new_position.set_x(x=5) + new_position.set_y(y=10) + self.assertEqual(new_position.get_as_tuple(), transform.position.get_as_tuple()) + + def test_transform_set_rotation_fewer_channels(self): + transform = transform_utils.Transform() + new_rotation = transform_utils.Vector3(1, 2, 3) + transform.set_rotation(xyz=new_rotation.get_as_tuple()) + transform.set_rotation(x=10) + new_rotation.set_x(x=10) + self.assertEqual(new_rotation, transform.rotation) + transform.set_rotation(y=15) + new_rotation.set_y(y=15) + self.assertEqual(new_rotation, transform.rotation) + transform.set_rotation(z=20) + new_rotation.set_z(z=20) + self.assertEqual(new_rotation, transform.rotation) + transform.set_rotation(x=0, z=20) + new_rotation.set_x(x=0) + new_rotation.set_z(z=20) + self.assertEqual(new_rotation, transform.rotation) + transform.set_rotation(x=5, y=10) + new_rotation.set_x(x=5) + new_rotation.set_y(y=10) + self.assertEqual(new_rotation.get_as_tuple(), transform.rotation.get_as_tuple()) + + def test_transform_set_scale_fewer_channels(self): + transform = transform_utils.Transform() + new_scale = transform_utils.Vector3(1, 2, 3) + transform.set_scale(xyz=new_scale.get_as_tuple()) + transform.set_scale(x=10) + new_scale.set_x(x=10) + self.assertEqual(new_scale, transform.scale) + transform.set_scale(y=15) + new_scale.set_y(y=15) + self.assertEqual(new_scale, transform.scale) + transform.set_scale(z=20) + new_scale.set_z(z=20) + self.assertEqual(new_scale, transform.scale) + transform.set_scale(x=0, z=20) + new_scale.set_x(x=0) + new_scale.set_z(z=20) + self.assertEqual(new_scale, transform.scale) + transform.set_scale(x=5, y=10) + new_scale.set_x(x=5) + new_scale.set_y(y=10) + self.assertEqual(new_scale.get_as_tuple(), transform.scale.get_as_tuple()) + def test_transform_set_trs_invalid_input(self): transform = transform_utils.Transform() @@ -363,6 +448,75 @@ def test_transform_set_scale_tuple(self): transform.set_scale(xyz=new_scale) self.assertEqual(new_scale_vector3, transform.scale) + def test_set_transform_from_object(self): + cube = maya_test_tools.create_poly_cube() + maya_test_tools.cmds.setAttr(f'{cube}.ty', 5) + maya_test_tools.cmds.setAttr(f'{cube}.ry', 35) + maya_test_tools.cmds.setAttr(f'{cube}.sy', 2) + transform = transform_utils.Transform() + transform.set_transform_from_object(obj_name=cube) + expected_position = transform_utils.Vector3(0, 5, 0) + self.assertEqual(expected_position, transform.position) + expected_rotate = transform_utils.Vector3(0, 35, 0) + self.assertEqual(expected_rotate, transform.rotation) + expected_scale = transform_utils.Vector3(1, 2, 1) + self.assertEqual(expected_scale, transform.scale) + + def test_get_position(self): + transform = transform_utils.Transform() + new_pos = (2, 2, 2) + new_pos_vector3 = transform_utils.Vector3(*new_pos) + transform.set_position(xyz=new_pos_vector3) + self.assertEqual(new_pos_vector3, transform.get_position()) + self.assertEqual(new_pos_vector3.get_as_tuple(), transform.get_position(as_tuple=True)) + + def test_get_rotation(self): + transform = transform_utils.Transform() + new_rot = (2, 2, 2) + new_rot_vector3 = transform_utils.Vector3(*new_rot) + transform.set_rotation(xyz=new_rot_vector3) + self.assertEqual(new_rot_vector3, transform.get_rotation()) + self.assertEqual(new_rot_vector3.get_as_tuple(), transform.get_rotation(as_tuple=True)) + + def test_get_scale(self): + transform = transform_utils.Transform() + new_sca = (2, 2, 2) + new_sca_vector3 = transform_utils.Vector3(*new_sca) + transform.set_scale(xyz=new_sca_vector3) + self.assertEqual(new_sca_vector3, transform.get_scale()) + self.assertEqual(new_sca_vector3.get_as_tuple(), transform.get_scale(as_tuple=True)) + + def test_get_transform_as_dict(self): + transform = transform_utils.Transform() + new_pos = (1, 1, 1) + new_pos_vector3 = transform_utils.Vector3(*new_pos) + transform.set_position(xyz=new_pos_vector3) + new_rot = (2, 2, 2) + new_rot_vector3 = transform_utils.Vector3(*new_rot) + transform.set_rotation(xyz=new_rot_vector3) + new_sca = (3, 3, 3) + new_sca_vector3 = transform_utils.Vector3(*new_sca) + transform.set_scale(xyz=new_sca_vector3) + result = transform.get_transform_as_dict() + expected = {"position": new_pos, + "rotation": new_rot, + "scale": new_sca, + } + self.assertEqual(expected, result) + + def test_set_transform_from_dict(self): + transform = transform_utils.Transform() + new_pos = (1, 1, 1) + new_rot = (2, 2, 2) + new_sca = (3, 3, 3) + expected = {"position": new_pos, + "rotation": new_rot, + "scale": new_sca, + } + transform.set_transform_from_dict(transform_dict=expected) + result = transform.get_transform_as_dict() + self.assertEqual(expected, result) + # -------------------------------------------------- Transform End ------------------------------------------------ def test_move_to_origin(self): diff --git a/tests/test_utils/test_uuid_utils.py b/tests/test_utils/test_uuid_utils.py index 442d6278..ddb29c72 100644 --- a/tests/test_utils/test_uuid_utils.py +++ b/tests/test_utils/test_uuid_utils.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Tested Utility and Maya Test Tools +# Import Utility and Maya Test Tools test_utils_dir = os.path.dirname(__file__) tests_dir = os.path.dirname(test_utils_dir) package_root_dir = os.path.dirname(tests_dir) diff --git a/tests/test_utils/test_version_utils.py b/tests/test_utils/test_version_utils.py index 645a75f3..79b746d8 100644 --- a/tests/test_utils/test_version_utils.py +++ b/tests/test_utils/test_version_utils.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -# Import Test Session Utilities +# Import Utility and Maya Test Tools tools_root_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) if tools_root_dir not in sys.path: sys.path.append(tools_root_dir)