diff --git a/README.md b/README.md index eeabfca0..e2661149 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,14 @@ variety of tools and utilities. This menu contains sub-menus that have been orga for example: modeling, rigging, utilities, etc… For help on how to use these scripts, click on the “Help” button at the top right of their window (within Maya) or -check their documentation by going to the "docs" folder. For changelog read the text at the top -of the tool init file (just open the “.py” file using any text editor, such as notepad) +check their documentation by going to the docs folder. For changelog see the releases page. All of these items are supplied as is. You alone are solely responsible for any issues. Use at your own risk. Hopefully these scripts are helpful to you as they are to me. Note: Python 2 is no longer supported. If you want to still use an older versions of Maya, make sure to use a GT-Tools version below "3.0.0" for compatibility. -

Package tested using Autodesk Maya 2022, 2023 and 2024 (Windows 10)

+

Package tested using Autodesk Maya 2022, 2023 and 2024 (Windows 11)

Organization

Utilities:

- +

Added attributes don't affect your attribute holder in any way, it's up to you do create necessary connections that will determine how these new values will be used.
For example, connecting "sideOutput" to "translateY" will case the object to move up and down according to the side curve.



+ +
+

GT Ribbon Tool

+ +GT Ribbon Tool GUI + +

Tool for automating the process of creating ribbons. A ribbon is a commonly used setup in rigging that serves as a flexible surface for attaching joints and controls. + +Ribbon rigging is particularly useful for creating smooth and natural-looking deformations, especially in areas of the character where complex movements are required, such as the spine, limbs, or facial features. By attaching joints and control objects to the ribbon, animators can easily manipulate and pose the character, achieving realistic movement and expressions.

+ + + +

+ +
diff --git a/docs/media/gt_ribbon_tool.jpg b/docs/media/gt_ribbon_tool.jpg new file mode 100644 index 00000000..330838b1 Binary files /dev/null and b/docs/media/gt_ribbon_tool.jpg differ diff --git a/gt/__init__.py b/gt/__init__.py index 05a140a9..742af18b 100644 --- a/gt/__init__.py +++ b/gt/__init__.py @@ -1,7 +1,7 @@ import sys # Package Variables -__version_tuple__ = (3, 2, 3) +__version_tuple__ = (3, 3, 0) __version_suffix__ = '' __version__ = '.'.join(str(n) for n in __version_tuple__) + __version_suffix__ __authors__ = ['Guilherme Trevisan'] diff --git a/gt/tools/auto_rigger/rig_constants.py b/gt/tools/auto_rigger/rig_constants.py index 83a9a6c2..ce280f1d 100644 --- a/gt/tools/auto_rigger/rig_constants.py +++ b/gt/tools/auto_rigger/rig_constants.py @@ -14,20 +14,27 @@ def __init__(self): # General Keys and Attributes PROJECT_EXTENSION = "rig" FILE_FILTER = f"Rig Project (*.{PROJECT_EXTENSION});;" - # Attributes and Keys - JOINT_ATTR_UUID = "jointUUID" - PROXY_ATTR_UUID = "proxyUUID" - CONTROL_ATTR_UUID = "controlUUID" - PROXY_ATTR_SCALE = "locatorScale" - PROXY_META_PARENT = "metaParentUUID" # Metadata key, may be different from actual parent (e.g. for lines) - PROXY_META_TYPE = "proxyType" # Metadata key, used to recognize rigged proxies within modules - PROXY_CLR = "color" # Metadata key, describes color to be used instead of side setup. - LINE_ATTR_CHILD_UUID = "lineProxySourceUUID" # Used by the proxy lines to store source - LINE_ATTR_PARENT_UUID = "lineProxyTargetUUID" # Used by the proxy lines to store target - JOINT_ATTR_DRIVEN_UUID = "jointDrivenUUID" + # System Attributes + ATTR_JOINT_UUID = "jointUUID" + ATTR_MODULE_UUID = "moduleUUID" + ATTR_PROXY_UUID = "proxyUUID" + ATTR_DRIVER_UUID = "driverUUID" + ATTR_JOINT_DRIVEN_UUID = "jointDrivenUUID" + # Misc Attributes + ATTR_PROXY_SCALE = "locatorScale" + ATTR_JOINT_PURPOSE = "jointPurpose" + ATTR_JOINT_DRIVERS = "jointDrivers" + ATTR_LINE_CHILD_UUID = "lineProxySourceUUID" # Used by the proxy lines to store source + ATTR_LINE_PARENT_UUID = "lineProxyTargetUUID" # Used by the proxy lines to store target + # Metadata Keys + META_PROXY_LINE_PARENT = "lineParentUUID" # Metadata key, line parent. Actual parent is ignored when present. + META_PROXY_PURPOSE = "proxyPurpose" # Metadata key, used to recognize proxy purpose within modules + META_PROXY_DRIVERS = "proxyDrivers" # Metadata key, used to find drivers (aka controls) driving the created joint + META_PROXY_CLR = "color" # Metadata key, describes color to be used instead of side setup. # Separator Attributes - SEPARATOR_STD_SUFFIX = "Options" # Standard (Std) Separator attribute name (a.k.a. header attribute) - SEPARATOR_BEHAVIOR = "Behavior" + SEPARATOR_OPTIONS = "options" + SEPARATOR_BEHAVIOR = "behavior" + SEPARATOR_AUTOMATION = "automation" # Group Names GRP_RIG_NAME = f'rig_{NamingConstants.Suffix.GRP}' GRP_PROXY_NAME = f'rig_proxy_{NamingConstants.Suffix.GRP}' @@ -37,16 +44,29 @@ def __init__(self): GRP_SETUP_NAME = f'setup_{NamingConstants.Suffix.GRP}' GRP_LINE_NAME = f'visualization_lines' # Reference Attributes - REF_ROOT_RIG_ATTR = "rootRigLookupAttr" - REF_ROOT_PROXY_ATTR = "rootProxyLookupAttr" - REF_ROOT_CONTROL_ATTR = "rootControlLookupAttr" - REF_DIR_CURVE_ATTR = "dirCrvLookupAttr" - REF_GEOMETRY_ATTR = "geometryGroupLookupAttr" - REF_SKELETON_ATTR = "skeletonGroupLookupAttr" - REF_CONTROL_ATTR = "controlGroupLookupAttr" - REF_SETUP_ATTR = "setupGroupLookupAttr" - REF_LINES_ATTR = "linesGroupLookupAttr" + REF_ATTR_ROOT_RIG = "rootRigLookupAttr" + REF_ATTR_ROOT_PROXY = "rootProxyLookupAttr" + REF_ATTR_ROOT_CONTROL = "rootControlLookupAttr" + REF_ATTR_DIR_CURVE = "dirCrvLookupAttr" + REF_ATTR_GEOMETRY = "geometryGroupLookupAttr" + REF_ATTR_SKELETON = "skeletonGroupLookupAttr" + REF_ATTR_CONTROL = "controlGroupLookupAttr" + REF_ATTR_SETUP = "setupGroupLookupAttr" + REF_ATTR_LINES = "linesGroupLookupAttr" # Multipliers LOC_RADIUS_MULTIPLIER_DRIVEN = .8 LOC_RADIUS_MULTIPLIER_FK = .3 LOC_RADIUS_MULTIPLIER_IK = .6 + # Misc + ENUM_ROTATE_ORDER = 'xyz:yzx:zxy:xzy:yxz:zyx' + + +class RiggerDriverTypes: + def __init__(self): + """ + Driver Type Constant values used by the drivers and controls. + """ + FK = "fk" + IK = "ik" + OFFSET = "offset" + COG = "cog" diff --git a/gt/tools/auto_rigger/rig_framework.py b/gt/tools/auto_rigger/rig_framework.py index 0beaff1a..dbe92c70 100644 --- a/gt/tools/auto_rigger/rig_framework.py +++ b/gt/tools/auto_rigger/rig_framework.py @@ -14,23 +14,23 @@ 5: build_rig 6: build_rig_post """ -from gt.tools.auto_rigger.rig_utils import create_control_root_curve, find_proxy_node_from_uuid, find_proxy_from_uuid +from gt.tools.auto_rigger.rig_utils import find_joint_from_uuid, get_proxy_offset, RiggerConstants, add_driver_to_joint from gt.tools.auto_rigger.rig_utils import parent_proxies, create_proxy_root_curve, create_proxy_visualization_lines -from gt.tools.auto_rigger.rig_utils import create_utility_groups, create_root_group, find_proxy_root_group_node -from gt.tools.auto_rigger.rig_utils import find_skeleton_group, create_direction_curve, get_meta_type_from_dict -from gt.tools.auto_rigger.rig_utils import find_joint_node_from_uuid, get_proxy_offset, RiggerConstants +from gt.tools.auto_rigger.rig_utils import find_skeleton_group, create_direction_curve, get_meta_purpose_from_dict +from gt.tools.auto_rigger.rig_utils import find_driver_from_uuid, find_proxy_from_uuid, create_control_root_curve +from gt.tools.auto_rigger.rig_utils import create_utility_groups, create_root_group, find_proxy_root_group from gt.utils.attr_utils import add_separator_attr, set_attr, add_attr, list_user_defined_attr, get_attr from gt.utils.uuid_utils import add_uuid_attr, is_uuid_valid, is_short_uuid_valid, generate_uuid +from gt.utils.color_utils import add_side_color_setup, ColorConstants, set_color_viewport from gt.utils.string_utils import remove_prefix, camel_case_split, remove_suffix from gt.utils.transform_utils import Transform, match_translate, match_rotate from gt.utils.curve_utils import Curve, get_curve, add_shape_scale_cluster -from gt.utils.color_utils import add_side_color_setup, ColorConstants, set_color_viewport from gt.utils.iterable_utils import get_highest_int_from_str_list from gt.utils.naming_utils import NamingConstants, get_long_name from gt.utils.uuid_utils import get_object_from_uuid_attr from gt.utils.control_utils import add_snapping_shape +from gt.utils.node_utils import create_node, Node from gt.utils.joint_utils import orient_joint -from gt.utils.node_utils import create_node from gt.utils import hierarchy_utils from gt.ui import resource_library from dataclasses import dataclass @@ -370,11 +370,11 @@ def build(self, prefix=None, suffix=None, apply_transforms=False, optimized=Fals proxy_offset = get_long_name(proxy_offset) proxy_crv = get_long_name(proxy_crv) - add_separator_attr(target_object=proxy_crv, attr_name=f'proxy{RiggerConstants.SEPARATOR_STD_SUFFIX}') + add_separator_attr(target_object=proxy_crv, attr_name=f'proxy{RiggerConstants.SEPARATOR_OPTIONS.title()}') uuid_attrs = add_uuid_attr(obj_list=proxy_crv, - attr_name=RiggerConstants.PROXY_ATTR_UUID, + attr_name=RiggerConstants.ATTR_PROXY_UUID, set_initial_uuid_value=False) - scale_attr = add_attr(obj_list=proxy_crv, attributes=RiggerConstants.PROXY_ATTR_SCALE, default=1) or [] + scale_attr = add_attr(obj_list=proxy_crv, attributes=RiggerConstants.ATTR_PROXY_SCALE, default=1) or [] loc_scale_cluster = None if not optimized and scale_attr and len(scale_attr) == 1: scale_attr = scale_attr[0] @@ -653,9 +653,9 @@ def add_to_metadata(self, key, value): self.metadata = {} self.metadata[key] = value - def add_meta_parent(self, line_parent): + def add_line_parent(self, line_parent): """ - Adds a meta parent UUID to the metadata dictionary. Initializes it in case it was not yet initialized. + Adds a line parent UUID to the metadata dictionary. Initializes it in case it was not yet initialized. This is used to created visualization lines or other elements without actually parenting the element. Args: line_parent (str, Proxy): New meta parent, if a UUID string. If Proxy, it will get the UUID (get_uuid). @@ -663,9 +663,39 @@ def add_meta_parent(self, line_parent): if not self.metadata: # Initialize metadata in case it was never used. self.metadata = {} if isinstance(line_parent, str) and is_uuid_valid(line_parent): - self.metadata[RiggerConstants.PROXY_META_PARENT] = line_parent + self.metadata[RiggerConstants.META_PROXY_LINE_PARENT] = line_parent if isinstance(line_parent, Proxy): - self.metadata[RiggerConstants.PROXY_META_PARENT] = line_parent.get_uuid() + self.metadata[RiggerConstants.META_PROXY_LINE_PARENT] = line_parent.get_uuid() + + def add_driver_type(self, driver_type): + """ + Adds a type/tag to the list of drivers. Initializes metadata in case it was not yet initialized. + A type/tag is used to determine controls driving the joint generated from this proxy + A proxy generates a joint, this joint can driven by multiple controls, the tag helps identify them. + Args: + driver_type (str, list): New type/tag to add. e.g. "fk", "ik", "offset", etc... + Can also be a list of new tags: e.g. ["fk", "ik"] + """ + if not driver_type: + logger.debug(f'Invalid tag was provided. Add driver operation was skipped.') + return + if not self.metadata: # Initialize metadata in case it was never used. + self.metadata = {} + if isinstance(driver_type, str): + driver_type = [driver_type] + new_tags = self.metadata.get(RiggerConstants.META_PROXY_DRIVERS, []) + for tag in driver_type: + if tag and isinstance(tag, str) and tag not in new_tags: + new_tags.append(tag) + if new_tags: + self.metadata[RiggerConstants.META_PROXY_DRIVERS] = new_tags + + def clear_driver_types(self): + """ + Clears any driver tags found in the metadata. + """ + if self.metadata: + self.metadata.pop(RiggerConstants.META_PROXY_DRIVERS, None) def add_color(self, rgb_color): """ @@ -737,14 +767,14 @@ def clear_parent_uuid(self): """ self.parent_uuid = None - def set_meta_type(self, value): + def set_meta_purpose(self, value): """ Adds a proxy meta type key and value to the metadata dictionary. Used to define proxy type in modules. Args: value (str, optional): Type "tag" used to determine overwrites. e.g. "hip", so the module knows it's a "hip" proxy. """ - self.add_to_metadata(key=RiggerConstants.PROXY_META_TYPE, value=value) + self.add_to_metadata(key=RiggerConstants.META_PROXY_PURPOSE, value=value) def read_data_from_dict(self, proxy_dict): """ @@ -801,9 +831,9 @@ def read_data_from_scene(self): Returns: Proxy: This object (self) """ - ignore_attr_list = [RiggerConstants.PROXY_ATTR_UUID, - RiggerConstants.PROXY_ATTR_SCALE] - proxy = get_object_from_uuid_attr(uuid_string=self.uuid, attr_name=RiggerConstants.PROXY_ATTR_UUID) + ignore_attr_list = [RiggerConstants.ATTR_PROXY_UUID, + RiggerConstants.ATTR_PROXY_SCALE] + proxy = get_object_from_uuid_attr(uuid_string=self.uuid, attr_name=RiggerConstants.ATTR_PROXY_UUID) if proxy: try: self._initialize_transform() @@ -828,14 +858,33 @@ def get_metadata(self): """ return self.metadata + def get_metadata_value(self, key): + """ + Gets the value stored in the metadata. If not found, returns None. + Args: + key (str): The value key. + Returns: + any: Value stored in the metadata key. If not found, it returns None + """ + if not self.metadata or not key: + return + return self.metadata.get(key) + def get_meta_parent_uuid(self): """ Gets the meta parent of this proxy (if present) Returns: str or None: The UUID set as meta parent, otherwise, None. """ - if self.metadata and isinstance(self.metadata, dict): - return self.metadata.get(RiggerConstants.PROXY_META_PARENT, None) + return self.get_metadata_value(RiggerConstants.META_PROXY_LINE_PARENT) + + def get_meta_purpose(self): + """ + Gets the meta purpose of this proxy (if present) + Returns: + str or None: The purpose of this proxy as stored in the metadata, otherwise None. + """ + return self.get_metadata_value(RiggerConstants.META_PROXY_PURPOSE) def get_name(self): """ @@ -878,6 +927,15 @@ def get_attr_dict(self): """ return self.attr_dict + def get_driver_types(self): + """ + Gets a list of available driver types. e.g. ["fk", "ik", "offset"] + Returns: + list or None: A list of driver types (strings) otherwise None. + """ + if self.metadata: + return self.metadata.get(RiggerConstants.META_PROXY_DRIVERS, None) + def get_proxy_as_dict(self, include_uuid=False, include_transform_data=True, include_offset_data=True): """ Returns all necessary information to recreate this proxy as a dictionary @@ -937,6 +995,8 @@ def __init__(self, name=None, prefix=None, suffix=None): if suffix: self.set_suffix(suffix) + self.module_children_drivers = [] # Cached elements to be parented to the "parentUUID" driver + # ------------------------------------------------- Setters ------------------------------------------------- def set_name(self, name): """ @@ -1224,7 +1284,7 @@ def read_data_from_dict(self, module_dict): self.set_metadata_dict(metadata=_metadata) return self - def read_type_matching_proxy_from_dict(self, proxy_dict): + def read_purpose_matching_proxy_from_dict(self, proxy_dict): """ Utility used by inherited modules to detect the proxy meta type when reading their dict data. Args: @@ -1237,13 +1297,13 @@ def read_type_matching_proxy_from_dict(self, proxy_dict): proxy_type_link = {} for proxy in proxies: metadata = proxy.get_metadata() - meta_type = get_meta_type_from_dict(metadata) + meta_type = get_meta_purpose_from_dict(metadata) if meta_type and isinstance(meta_type, str): proxy_type_link[meta_type] = proxy for uuid, description in proxy_dict.items(): metadata = description.get("metadata") - meta_type = get_meta_type_from_dict(metadata) + meta_type = get_meta_purpose_from_dict(metadata) if meta_type in proxy_type_link: proxy = proxy_type_link.get(meta_type) proxy.set_uuid(uuid) @@ -1444,6 +1504,123 @@ def get_description_name(self, add_class_len=2): _module_name = f'{_module_name} ({_class_name})' return _module_name + def find_driver(self, driver_type, proxy_purpose): + """ + Find driver (a.k.a. Control) is responsible for directly or indirectly driving a joint or a group of joints. + Args: + driver_type (str): A driver type (aka tag) used to identify the type of control. e.g. "fk", "ik", "offset". + proxy_purpose (str, Proxy): The purpose of the control (aka Description) e.g. "shoulder" + This can also be a proxy, in which case the purposed will be extracted. + Returns: + Node or None: A Node object pointing to an existing driver/control object, otherwise None. + """ + uuid = self.uuid + if driver_type: + uuid = f'{uuid}-{driver_type}' + if proxy_purpose and isinstance(proxy_purpose, Proxy): + proxy_purpose = proxy_purpose.get_meta_purpose() + if proxy_purpose: + uuid = f'{uuid}-{proxy_purpose}' + return find_driver_from_uuid(uuid_string=uuid) + + def find_module_drivers(self): + """ + Find driver nodes (a.k.a. Controls) that are responsible for directly or indirectly driving the proxy's joint. + Returns: + list: A list of transforms used as drivers/controls for this module. + """ + obj_list = cmds.ls(typ="transform", long=True) or [] + matches = [] + module_uuid = self.uuid + for obj in obj_list: + if cmds.objExists(f'{obj}.{RiggerConstants.ATTR_DRIVER_UUID}'): + uuid_value = cmds.getAttr(f'{obj}.{RiggerConstants.ATTR_DRIVER_UUID}') + if uuid_value.startswith(module_uuid): + matches.append(obj) + return matches + + def find_proxy_drivers(self, proxy, as_dict=True): + """ + Find driver nodes (a.k.a. Controls) that are responsible for directly or indirectly driving the proxy's joint. + Args: + proxy (Proxy): The proxy, used to get the driver purpose and types. + If missing metadata, an empty list is returned. + as_dict (bool, optional): If True, this function return a dictionary where the key is the driver type and + the value is the driver, if False, then it returns a list of drivers. + Returns: + dict, list: A list of transforms used as drivers/controls for the provided proxy. + """ + proxy_driver_types = proxy.get_driver_types() + proxy_purpose = proxy.get_meta_purpose() + if not proxy_driver_types: + logger.debug(f'Proxy does not have any driver types. No drivers can be found without a type.') + return [] + if not proxy_purpose: + logger.debug(f'Proxy does not have a defined purpose. No drivers can be found without a purpose.') + return [] + driver_uuids = [] + for proxy_type in proxy_driver_types: + driver_uuids.append(f'{self.uuid}-{proxy_type}-{proxy_purpose}') + obj_list = cmds.ls(typ="transform", long=True) or [] + module_matches = {} + module_uuid = self.uuid + for obj in obj_list: + if cmds.objExists(f'{obj}.{RiggerConstants.ATTR_DRIVER_UUID}'): + uuid_value = cmds.getAttr(f'{obj}.{RiggerConstants.ATTR_DRIVER_UUID}') + if uuid_value.startswith(module_uuid): + module_matches[uuid_value] = Node(obj) + matches = [] + matches_dict = {} + for driver_uuid in driver_uuids: + if driver_uuid in module_matches: + matches.append(module_matches.get(driver_uuid)) + driver_key = str(driver_uuid).split("-") + if len(driver_key) >= 3: + matches_dict[driver_key[1]] = module_matches.get(driver_uuid) + if len(matches) != driver_uuids: + logger.debug(f'Not all drivers were found. ' + f'Driver type list has a different length when compared to the list of matches.') + if as_dict: + matches = matches_dict + return matches + + def _assemble_ctrl_name(self, name, project_prefix=None, overwrite_prefix=None, overwrite_suffix=None): + """ + Assemble a new control name based on the given parameters and module prefix/suffix. + This function also automatically adds the control suffix at the end of the generated name. + Result pattern: "____" + Args: + name (str): The base name of the control. + project_prefix (str, optional): Prefix specific to the project. Defaults to None. + overwrite_prefix (str, optional): Prefix to overwrite the module's prefix. Defaults to None (use module) + When provided (even if empty) it will replace the module stored value. + overwrite_suffix (str, optional): Suffix to overwrite the module's suffix. Defaults to None (use module) + When provided (even if empty) it will replace the module stored value. + + Returns: + str: The assembled new node name. + + Example: + instance._assemble_new_node_name(name='NodeName', project_prefix='Project', overwrite_suffix='Custom') + 'Project_NodeName_Custom' + """ + _suffix = '' + module_suffix = self.suffix + if module_suffix: + module_suffix = f'{module_suffix}_{NamingConstants.Suffix.CTRL}' + else: + module_suffix = NamingConstants.Suffix.CTRL + if isinstance(overwrite_suffix, str): + module_suffix = overwrite_suffix + if overwrite_suffix: + module_suffix = overwrite_suffix + if module_suffix: + _suffix = f'_{module_suffix}' + return self._assemble_new_node_name(name=name, + project_prefix=project_prefix, + overwrite_prefix=overwrite_prefix, + overwrite_suffix=module_suffix) + def _assemble_new_node_name(self, name, project_prefix=None, overwrite_prefix=None, overwrite_suffix=None): """ Assemble a new node name based on the given parameters and module prefix/suffix. @@ -1508,6 +1685,34 @@ def is_valid(self): return False return True + def add_driver_uuid_attr(self, target, driver_type=None, proxy_purpose=None): + """ + Adds an attribute to be used as driver UUID to the object. + The value of the attribute is created using the module uuid, the driver type and proxy purpose combined. + Following this pattern: "--" e.g. "abcdef123456-fk-shoulder" + Args: + target (str): Path to the object that will receive the driver attributes. + driver_type (str, optional): A string or tag use to identify the control type. e.g. "fk", "ik", "offset" + proxy_purpose (str, Proxy, optional): This is the proxy purpose. It can be a string, e.g. "shoulder" or + the proxy object (if a Proxy object is provided, then it tries to extract + """ + uuid = f'{self.uuid}' + if driver_type: + uuid = f'{uuid}-{driver_type}' + if proxy_purpose and isinstance(proxy_purpose, Proxy): + proxy_purpose = proxy_purpose.get_meta_purpose() + if proxy_purpose: + uuid = f'{uuid}-{proxy_purpose}' + if not target or not cmds.objExists(target): + logger.debug(f'Unable to add UUID attribute. Target object is missing.') + return + uuid_attr = add_attr(obj_list=target, attr_type="string", is_keyable=False, + attributes=RiggerConstants.ATTR_DRIVER_UUID, verbose=True)[0] + if not uuid: + uuid = generate_uuid(remove_dashes=True) + set_attr(attribute_path=uuid_attr, value=str(uuid)) + return target + # --------------------------------------------------- Build --------------------------------------------------- def build_proxy(self, project_prefix=None, optimized=False): """ @@ -1560,7 +1765,7 @@ def build_skeleton_joints(self): logger.debug(f'"build_skeleton" function from "{self.get_module_class_name()}" was called.') skeleton_grp = find_skeleton_group() for proxy in self.proxies: - proxy_node = find_proxy_node_from_uuid(proxy.get_uuid()) + proxy_node = find_proxy_from_uuid(proxy.get_uuid()) if not proxy_node: continue joint = create_node(node_type="joint", name=proxy_node.get_short_name()) @@ -1568,11 +1773,29 @@ def build_skeleton_joints(self): cmds.setAttr(f'{joint}.radius', locator_scale) match_translate(source=proxy_node, target_list=joint) - # Add proxy data for reference + # Add proxy reference - Proxy/Joint UUID add_attr(obj_list=joint, - attributes=RiggerConstants.JOINT_ATTR_UUID, + attributes=RiggerConstants.ATTR_JOINT_UUID, attr_type="string") - set_attr(obj_list=joint, attr_list=RiggerConstants.JOINT_ATTR_UUID, value=proxy.get_uuid()) + set_attr(obj_list=joint, attr_list=RiggerConstants.ATTR_JOINT_UUID, value=proxy.get_uuid()) + # Add module reference - Module UUID + add_attr(obj_list=joint, + attributes=RiggerConstants.ATTR_MODULE_UUID, + attr_type="string") + set_attr(obj_list=joint, attr_list=RiggerConstants.ATTR_MODULE_UUID, value=self.get_uuid()) + # Add proxy purposes - Meta Purpose + add_attr(obj_list=joint, + attributes=RiggerConstants.ATTR_JOINT_PURPOSE, + attr_type="string") + set_attr(obj_list=joint, attr_list=RiggerConstants.ATTR_JOINT_PURPOSE, value=proxy.get_meta_purpose()) + # Add proxy purposes - Joint Drivers + add_attr(obj_list=joint, + attributes=RiggerConstants.ATTR_JOINT_DRIVERS, + attr_type="string") + drivers = proxy.get_driver_types() + if drivers: + add_driver_to_joint(target_joint=joint, new_drivers=drivers) + set_color_viewport(obj_list=joint, rgb_color=ColorConstants.RigJoint.GENERAL) hierarchy_utils.parent(source_objects=joint, target_parent=str(skeleton_grp)) @@ -1590,17 +1813,17 @@ def build_skeleton_hierarchy(self): module_uuids = self.get_proxies_uuids() jnt_nodes = [] for proxy in self.proxies: - joint = find_joint_node_from_uuid(proxy.get_uuid()) + joint = find_joint_from_uuid(proxy.get_uuid()) if not joint: continue # Inherit Orientation (Before Parenting) if self.get_orientation_method() == OrientationData.Methods.inherit: - proxy_obj_path = find_proxy_node_from_uuid(proxy.get_uuid()) + proxy_obj_path = find_proxy_from_uuid(proxy.get_uuid()) match_rotate(source=proxy_obj_path, target_list=joint) # Parent Joint (Internal Proxies) parent_uuid = proxy.get_parent_uuid() if parent_uuid in module_uuids: - parent_joint_node = find_joint_node_from_uuid(parent_uuid) + parent_joint_node = find_joint_from_uuid(parent_uuid) hierarchy_utils.parent(source_objects=joint, target_parent=parent_joint_node) jnt_nodes.append(joint) @@ -1612,8 +1835,8 @@ def build_skeleton_hierarchy(self): for proxy in self.proxies: parent_uuid = proxy.get_parent_uuid() if parent_uuid not in module_uuids: - joint = find_joint_node_from_uuid(proxy.get_uuid()) - parent_joint_node = find_joint_node_from_uuid(parent_uuid) + joint = find_joint_from_uuid(proxy.get_uuid()) + parent_joint_node = find_joint_from_uuid(parent_uuid) hierarchy_utils.parent(source_objects=joint, target_parent=parent_joint_node) cmds.select(clear=True) @@ -1778,15 +2001,17 @@ def read_modules_from_dict(self, modules_list): _module.read_data_from_dict(module_dict=module_description) self.modules.append(_module) - def read_data_from_dict(self, module_dict): + def read_data_from_dict(self, module_dict, clear_modules=True): """ 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": ...} + clear_modules (bool, optional): When active, the modules list is cleared before importing new data. Returns: RigProject: This project (self) """ - self.modules = [] + if clear_modules: + self.modules = [] self.metadata = None if module_dict and not isinstance(module_dict, dict): @@ -1905,7 +2130,7 @@ def build_proxy(self, optimized=False): root_transform = create_proxy_root_curve() hierarchy_utils.parent(source_objects=root_transform, target_parent=root_group) category_groups = create_utility_groups(line=True, target_parent=root_group) - line_grp = category_groups.get(RiggerConstants.REF_LINES_ATTR) + line_grp = category_groups.get(RiggerConstants.REF_ATTR_LINES) attr_to_activate = ['overrideEnabled', 'overrideDisplayType', "hiddenInOutliner"] set_attr(obj_list=line_grp, attr_list=attr_to_activate, value=1) add_attr(obj_list=str(root_transform), @@ -1961,8 +2186,8 @@ def build_rig(self, delete_proxy=True): control=True, setup=True, target_parent=root_group) - control_grp = category_groups.get(RiggerConstants.REF_CONTROL_ATTR) - setup_grp = category_groups.get(RiggerConstants.REF_SETUP_ATTR) + control_grp = category_groups.get(RiggerConstants.REF_ATTR_CONTROL) + setup_grp = category_groups.get(RiggerConstants.REF_ATTR_SETUP) set_attr(obj_list=setup_grp, attr_list=['overrideEnabled', 'overrideDisplayType'], value=1) hierarchy_utils.parent(source_objects=list(category_groups.values()), target_parent=root_group) hierarchy_utils.parent(source_objects=root_ctrl, target_parent=control_grp) @@ -1994,7 +2219,7 @@ def build_rig(self, delete_proxy=True): # Delete Proxy if delete_proxy: - proxy_root = find_proxy_root_group_node() + proxy_root = find_proxy_root_group() if proxy_root: cmds.delete(proxy_root) except Exception as e: @@ -2015,6 +2240,7 @@ def build_rig(self, delete_proxy=True): # a_biped_project.build_rig() root = Proxy(name="root") + root.set_meta_purpose("root") a_1st_proxy = Proxy(name="first") a_1st_proxy.set_position(y=5, x=-1) a_1st_proxy.set_parent_uuid_from_proxy(root) @@ -2025,8 +2251,11 @@ def build_rig(self, delete_proxy=True): a_root_module = ModuleGeneric() a_root_module.add_to_proxies(root) + test = cmds.polySphere() + a_root_module.add_driver_uuid_attr(test[0], "fk", root) a_module = ModuleGeneric() + print(a_module._assemble_ctrl_name(name="test")) a_module.add_to_proxies(a_1st_proxy) a_module.add_to_proxies(a_2nd_proxy) # a_module.set_prefix("prefix") @@ -2048,4 +2277,4 @@ def build_rig(self, delete_proxy=True): # pprint(a_project.get_modules()) a_project.get_project_as_dict() a_project.build_proxy() - # a_project.build_rig(delete_proxy=True) + a_project.build_rig(delete_proxy=True) diff --git a/gt/tools/auto_rigger/rig_module_biped_arm.py b/gt/tools/auto_rigger/rig_module_biped_arm.py index a989b0ec..076c35fe 100644 --- a/gt/tools/auto_rigger/rig_module_biped_arm.py +++ b/gt/tools/auto_rigger/rig_module_biped_arm.py @@ -2,10 +2,9 @@ Auto Rigger Arm Modules github.com/TrevisanGMW/gt-tools """ -from gt.tools.auto_rigger.rig_utils import find_objects_with_attr, find_proxy_node_from_uuid, get_driven_joint -from gt.tools.auto_rigger.rig_utils import find_direction_curve_node, find_or_create_joint_automation_group -from gt.tools.auto_rigger.rig_utils import find_joint_node_from_uuid, rescale_joint_radius -from gt.tools.auto_rigger.rig_utils import duplicate_joint_for_automation +from gt.tools.auto_rigger.rig_utils import find_joint_from_uuid, rescale_joint_radius, duplicate_joint_for_automation +from gt.tools.auto_rigger.rig_utils import find_objects_with_attr, find_proxy_from_uuid, get_driven_joint +from gt.tools.auto_rigger.rig_utils import find_direction_curve, find_or_create_joint_automation_group from gt.utils.transform_utils import match_translate, Vector3, set_equidistant_transforms from gt.utils.color_utils import set_color_viewport, ColorConstants, set_color_outliner from gt.tools.auto_rigger.rig_framework import Proxy, ModuleGeneric, OrientationData @@ -56,27 +55,27 @@ def __init__(self, name="Arm", prefix=None, suffix=None): self.clavicle.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.clavicle.set_initial_position(xyz=pos_clavicle) self.clavicle.set_locator_scale(scale=2) - self.clavicle.set_meta_type(value="clavicle") + self.clavicle.set_meta_purpose(value="clavicle") self.shoulder = Proxy(name=shoulder_name) self.shoulder.set_initial_position(xyz=pos_shoulder) self.shoulder.set_locator_scale(scale=2) self.shoulder.set_parent_uuid(self.clavicle.get_uuid()) - self.shoulder.set_meta_type(value="shoulder") + self.shoulder.set_meta_purpose(value="shoulder") self.elbow = Proxy(name=elbow_name) self.elbow.set_curve(curve=get_curve('_proxy_joint_arrow_neg_z')) self.elbow.set_initial_position(xyz=pos_elbow) self.elbow.set_locator_scale(scale=2.2) - self.elbow.add_meta_parent(line_parent=self.shoulder) - self.elbow.set_meta_type(value="elbow") + self.elbow.add_line_parent(line_parent=self.shoulder) + self.elbow.set_meta_purpose(value="elbow") self.wrist = Proxy(name=wrist_name) self.wrist.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.wrist.set_initial_position(xyz=pos_wrist) self.wrist.set_locator_scale(scale=2) - self.wrist.add_meta_parent(line_parent=self.elbow) - self.wrist.set_meta_type(value="wrist") + self.wrist.add_line_parent(line_parent=self.elbow) + self.wrist.set_meta_purpose(value="wrist") # Update Proxies self.proxies = [self.clavicle, self.shoulder, self.elbow, self.wrist] @@ -115,7 +114,7 @@ def read_proxies_from_dict(self, proxy_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.read_type_matching_proxy_from_dict(proxy_dict) + self.read_purpose_matching_proxy_from_dict(proxy_dict) # --------------------------------------------------- Misc --------------------------------------------------- def is_valid(self): @@ -145,11 +144,11 @@ def build_proxy_setup(self): When in a project, this runs after the "build_proxy" is done in all modules. """ # Get Maya Elements - root = find_objects_with_attr(RiggerConstants.REF_ROOT_PROXY_ATTR) - clavicle = find_proxy_node_from_uuid(self.clavicle.get_uuid()) - shoulder = find_proxy_node_from_uuid(self.shoulder.get_uuid()) - elbow = find_proxy_node_from_uuid(self.elbow.get_uuid()) - wrist = find_proxy_node_from_uuid(self.wrist.get_uuid()) + root = find_objects_with_attr(RiggerConstants.REF_ATTR_ROOT_PROXY) + clavicle = find_proxy_from_uuid(self.clavicle.get_uuid()) + shoulder = find_proxy_from_uuid(self.shoulder.get_uuid()) + elbow = find_proxy_from_uuid(self.elbow.get_uuid()) + wrist = find_proxy_from_uuid(self.wrist.get_uuid()) self.clavicle.apply_offset_transform() self.shoulder.apply_offset_transform() @@ -157,11 +156,11 @@ def build_proxy_setup(self): self.wrist.apply_offset_transform() # Shoulder ----------------------------------------------------------------------------------- - hide_lock_default_attrs(shoulder, translate=False) + hide_lock_default_attrs(shoulder, rotate=True, scale=True) # Elbow ------------------------------------------------------------------------------------- elbow_tag = elbow.get_short_name() - hide_lock_default_attrs(elbow, translate=False, rotate=False) + hide_lock_default_attrs(elbow, scale=True) # Elbow Setup elbow_offset = get_proxy_offset(elbow) @@ -249,12 +248,12 @@ def build_skeleton_hierarchy(self): def build_rig(self, project_prefix=None, **kwargs): # Get Elements - direction_crv = find_direction_curve_node() - module_parent_jnt = find_joint_node_from_uuid(self.get_parent_uuid()) # TODO TEMP @@@ - clavicle_jnt = find_joint_node_from_uuid(self.clavicle.get_uuid()) - shoulder_jnt = find_joint_node_from_uuid(self.shoulder.get_uuid()) - elbow_jnt = find_joint_node_from_uuid(self.elbow.get_uuid()) - wrist_jnt = find_joint_node_from_uuid(self.wrist.get_uuid()) + direction_crv = find_direction_curve() + module_parent_jnt = find_joint_from_uuid(self.get_parent_uuid()) # TODO TEMP @@@ + clavicle_jnt = find_joint_from_uuid(self.clavicle.get_uuid()) + shoulder_jnt = find_joint_from_uuid(self.shoulder.get_uuid()) + elbow_jnt = find_joint_from_uuid(self.elbow.get_uuid()) + wrist_jnt = find_joint_from_uuid(self.wrist.get_uuid()) arm_jnt_list = [clavicle_jnt, shoulder_jnt, elbow_jnt, wrist_jnt] # Set Colors @@ -352,25 +351,30 @@ def __init__(self, name="Right Arm", prefix=NamingConstants.Prefix.RIGHT, suffix if __name__ == "__main__": logger.setLevel(logging.DEBUG) + # Auto Reload Script - Must have been initialized using "Run-Only" mode. + from gt.utils.session_utils import remove_modules_startswith + remove_modules_startswith("gt.tools.auto_rigger.rig") cmds.file(new=True, force=True) from gt.tools.auto_rigger.rig_framework import RigProject - a_module = ModuleGeneric() - a_proxy = a_module.add_new_proxy() - a_proxy.set_initial_position(y=130) + from gt.tools.auto_rigger.rig_module_spine import ModuleSpine + + a_spine = ModuleSpine() a_arm = ModuleBipedArm() - a_arm_rt = ModuleBipedArmRight() a_arm_lf = ModuleBipedArmLeft() - a_arm_rt.set_parent_uuid(uuid=a_proxy.get_uuid()) - a_arm_lf.set_parent_uuid(uuid=a_proxy.get_uuid()) + a_arm_rt = ModuleBipedArmRight() + + spine_chest_uuid = a_spine.chest.get_uuid() + a_arm_lf.set_parent_uuid(spine_chest_uuid) + a_arm_rt.set_parent_uuid(spine_chest_uuid) + a_project = RigProject() - # a_project.add_to_modules(a_arm) # TODO Change it so it moves down - a_project.add_to_modules(a_module) + a_project.add_to_modules(a_spine) a_project.add_to_modules(a_arm_rt) a_project.add_to_modules(a_arm_lf) a_project.build_proxy() a_project.build_rig() - # + # cmds.setAttr(f'{a_arm_rt.get_prefix()}_{a_arm_rt.clavicle.get_name()}.ty', 15) # cmds.setAttr(f'{a_arm_rt.get_prefix()}_{a_arm_rt.elbow.get_name()}.tz', -15) # cmds.setAttr(f'{a_arm_lf.get_prefix()}_{a_arm_lf.clavicle.get_name()}.ty', 15) diff --git a/gt/tools/auto_rigger/rig_module_biped_finger.py b/gt/tools/auto_rigger/rig_module_biped_finger.py index aa33bc9e..af4326fe 100644 --- a/gt/tools/auto_rigger/rig_module_biped_finger.py +++ b/gt/tools/auto_rigger/rig_module_biped_finger.py @@ -2,7 +2,7 @@ Auto Rigger Digit Modules (Fingers, Toes) github.com/TrevisanGMW/gt-tools """ -from gt.tools.auto_rigger.rig_utils import find_joint_node_from_uuid, get_meta_type_from_dict +from gt.tools.auto_rigger.rig_utils import find_joint_from_uuid, get_meta_purpose_from_dict from gt.tools.auto_rigger.rig_framework import Proxy, ModuleGeneric, OrientationData from gt.utils.color_utils import ColorConstants, set_color_viewport from gt.tools.auto_rigger.rig_constants import RiggerConstants @@ -78,28 +78,28 @@ def __init__(self, name="Fingers", prefix=None, suffix=None): self.thumb01.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.thumb01.set_initial_position(xyz=pos_thumb01) self.thumb01.set_locator_scale(scale=loc_scale) - self.thumb01.set_meta_type(value=self.thumb01.get_name()) + self.thumb01.set_meta_purpose(value=self.thumb01.get_name()) self.thumb02 = Proxy(name=f"{self.tag_thumb}02") self.thumb02.set_parent_uuid(self.thumb01.get_uuid()) self.thumb02.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.thumb02.set_initial_position(xyz=pos_thumb02) self.thumb02.set_locator_scale(scale=loc_scale) - self.thumb02.set_meta_type(value=self.thumb02.get_name()) + self.thumb02.set_meta_purpose(value=self.thumb02.get_name()) self.thumb03 = Proxy(name=f"{self.tag_thumb}03") self.thumb03.set_parent_uuid(self.thumb02.get_uuid()) self.thumb03.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.thumb03.set_initial_position(xyz=pos_thumb03) self.thumb03.set_locator_scale(scale=loc_scale) - self.thumb03.set_meta_type(value=self.thumb03.get_name()) + self.thumb03.set_meta_purpose(value=self.thumb03.get_name()) self.thumb04 = Proxy(name=f"{self.tag_thumb}End") self.thumb04.set_parent_uuid(self.thumb03.get_uuid()) self.thumb04.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.thumb04.set_initial_position(xyz=pos_thumb04) self.thumb04.set_locator_scale(scale=loc_scale_end) - self.thumb04.set_meta_type(value=self.thumb04.get_name()) + self.thumb04.set_meta_purpose(value=self.thumb04.get_name()) self.thumb04.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) self.thumb_digits = [self.thumb01, self.thumb02, self.thumb03, self.thumb04] @@ -109,28 +109,28 @@ def __init__(self, name="Fingers", prefix=None, suffix=None): self.index01.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.index01.set_initial_position(xyz=pos_index01) self.index01.set_locator_scale(scale=loc_scale) - self.index01.set_meta_type(value=self.index01.get_name()) + self.index01.set_meta_purpose(value=self.index01.get_name()) self.index02 = Proxy(name=f"{self.tag_index}02") self.index02.set_parent_uuid(self.index01.get_uuid()) self.index02.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.index02.set_initial_position(xyz=pos_index02) self.index02.set_locator_scale(scale=loc_scale) - self.index02.set_meta_type(value=self.index02.get_name()) + self.index02.set_meta_purpose(value=self.index02.get_name()) self.index03 = Proxy(name=f"{self.tag_index}03") self.index03.set_parent_uuid(self.index02.get_uuid()) self.index03.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.index03.set_initial_position(xyz=pos_index03) self.index03.set_locator_scale(scale=loc_scale) - self.index03.set_meta_type(value=self.index03.get_name()) + self.index03.set_meta_purpose(value=self.index03.get_name()) self.index04 = Proxy(name=f"{self.tag_index}End") self.index04.set_parent_uuid(self.index03.get_uuid()) self.index04.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.index04.set_initial_position(xyz=pos_index04) self.index04.set_locator_scale(scale=loc_scale_end) - self.index04.set_meta_type(value=self.index04.get_name()) + self.index04.set_meta_purpose(value=self.index04.get_name()) self.index04.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) self.index_digits = [self.index01, self.index02, self.index03, self.index04] @@ -140,28 +140,28 @@ def __init__(self, name="Fingers", prefix=None, suffix=None): self.middle01.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.middle01.set_initial_position(xyz=pos_middle01) self.middle01.set_locator_scale(scale=loc_scale) - self.middle01.set_meta_type(value=self.middle01.get_name()) + self.middle01.set_meta_purpose(value=self.middle01.get_name()) self.middle02 = Proxy(name=f"{self.tag_middle}02") self.middle02.set_parent_uuid(self.middle01.get_uuid()) self.middle02.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.middle02.set_initial_position(xyz=pos_middle02) self.middle02.set_locator_scale(scale=loc_scale) - self.middle02.set_meta_type(value=self.middle02.get_name()) + self.middle02.set_meta_purpose(value=self.middle02.get_name()) self.middle03 = Proxy(name=f"{self.tag_middle}03") self.middle03.set_parent_uuid(self.middle02.get_uuid()) self.middle03.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.middle03.set_initial_position(xyz=pos_middle03) self.middle03.set_locator_scale(scale=loc_scale) - self.middle03.set_meta_type(value=self.middle03.get_name()) + self.middle03.set_meta_purpose(value=self.middle03.get_name()) self.middle04 = Proxy(name=f"{self.tag_middle}End") self.middle04.set_parent_uuid(self.middle03.get_uuid()) self.middle04.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.middle04.set_initial_position(xyz=pos_middle04) self.middle04.set_locator_scale(scale=loc_scale_end) - self.middle04.set_meta_type(value=self.middle04.get_name()) + self.middle04.set_meta_purpose(value=self.middle04.get_name()) self.middle04.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) self.middle_digits = [self.middle01, self.middle02, self.middle03, self.middle04] @@ -171,28 +171,28 @@ def __init__(self, name="Fingers", prefix=None, suffix=None): self.ring01.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.ring01.set_initial_position(xyz=pos_ring01) self.ring01.set_locator_scale(scale=loc_scale) - self.ring01.set_meta_type(value=self.ring01.get_name()) + self.ring01.set_meta_purpose(value=self.ring01.get_name()) self.ring02 = Proxy(name=f"{self.tag_ring}02") self.ring02.set_parent_uuid(self.ring01.get_uuid()) self.ring02.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.ring02.set_initial_position(xyz=pos_ring02) self.ring02.set_locator_scale(scale=loc_scale) - self.ring02.set_meta_type(value=self.ring02.get_name()) + self.ring02.set_meta_purpose(value=self.ring02.get_name()) self.ring03 = Proxy(name=f"{self.tag_ring}03") self.ring03.set_parent_uuid(self.ring02.get_uuid()) self.ring03.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.ring03.set_initial_position(xyz=pos_ring03) self.ring03.set_locator_scale(scale=loc_scale) - self.ring03.set_meta_type(value=self.ring03.get_name()) + self.ring03.set_meta_purpose(value=self.ring03.get_name()) self.ring04 = Proxy(name=f"{self.tag_ring}End") self.ring04.set_parent_uuid(self.ring03.get_uuid()) self.ring04.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.ring04.set_initial_position(xyz=pos_ring04) self.ring04.set_locator_scale(scale=loc_scale_end) - self.ring04.set_meta_type(value=self.ring04.get_name()) + self.ring04.set_meta_purpose(value=self.ring04.get_name()) self.ring04.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) self.ring_digits = [self.ring01, self.ring02, self.ring03, self.ring04] @@ -202,28 +202,28 @@ def __init__(self, name="Fingers", prefix=None, suffix=None): self.pinky01.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.pinky01.set_initial_position(xyz=pos_pinky01) self.pinky01.set_locator_scale(scale=loc_scale) - self.pinky01.set_meta_type(value=self.pinky01.get_name()) + self.pinky01.set_meta_purpose(value=self.pinky01.get_name()) self.pinky02 = Proxy(name=f"{self.tag_pinky}02") self.pinky02.set_parent_uuid(self.pinky01.get_uuid()) self.pinky02.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.pinky02.set_initial_position(xyz=pos_pinky02) self.pinky02.set_locator_scale(scale=loc_scale) - self.pinky02.set_meta_type(value=self.pinky02.get_name()) + self.pinky02.set_meta_purpose(value=self.pinky02.get_name()) self.pinky03 = Proxy(name=f"{self.tag_pinky}03") self.pinky03.set_parent_uuid(self.pinky02.get_uuid()) self.pinky03.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.pinky03.set_initial_position(xyz=pos_pinky03) self.pinky03.set_locator_scale(scale=loc_scale) - self.pinky03.set_meta_type(value=self.pinky03.get_name()) + self.pinky03.set_meta_purpose(value=self.pinky03.get_name()) self.pinky04 = Proxy(name=f"{self.tag_pinky}End") self.pinky04.set_parent_uuid(self.pinky03.get_uuid()) self.pinky04.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.pinky04.set_initial_position(xyz=pos_pinky04) self.pinky04.set_locator_scale(scale=loc_scale_end) - self.pinky04.set_meta_type(value=self.pinky04.get_name()) + self.pinky04.set_meta_purpose(value=self.pinky04.get_name()) self.pinky04.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) self.pinky_digits = [self.pinky01, self.pinky02, self.pinky03, self.pinky04] @@ -233,28 +233,28 @@ def __init__(self, name="Fingers", prefix=None, suffix=None): self.extra01.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.extra01.set_initial_position(xyz=pos_extra01) self.extra01.set_locator_scale(scale=loc_scale) - self.extra01.set_meta_type(value=self.extra01.get_name()) + self.extra01.set_meta_purpose(value=self.extra01.get_name()) self.extra02 = Proxy(name=f"{self.tag_extra}02") self.extra02.set_parent_uuid(self.extra01.get_uuid()) self.extra02.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.extra02.set_initial_position(xyz=pos_extra02) self.extra02.set_locator_scale(scale=loc_scale) - self.extra02.set_meta_type(value=self.extra02.get_name()) + self.extra02.set_meta_purpose(value=self.extra02.get_name()) self.extra03 = Proxy(name=f"{self.tag_extra}03") self.extra03.set_parent_uuid(self.extra02.get_uuid()) self.extra03.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.extra03.set_initial_position(xyz=pos_extra03) self.extra03.set_locator_scale(scale=loc_scale) - self.extra03.set_meta_type(value=self.extra03.get_name()) + self.extra03.set_meta_purpose(value=self.extra03.get_name()) self.extra04 = Proxy(name=f"{self.tag_extra}End") self.extra04.set_parent_uuid(self.extra03.get_uuid()) self.extra04.set_curve(curve=get_curve('_proxy_joint_dir_pos_y')) self.extra04.set_initial_position(xyz=pos_extra04) self.extra04.set_locator_scale(scale=loc_scale_end) - self.extra04.set_meta_type(value=self.extra04.get_name()) + self.extra04.set_meta_purpose(value=self.extra04.get_name()) self.extra04.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) self.extra_digits = [self.extra01, self.extra02, self.extra03, self.extra04] self.refresh_proxies_list() @@ -307,7 +307,7 @@ def read_proxies_from_dict(self, proxy_dict): for uuid, description in proxy_dict.items(): metadata = description.get("metadata") if metadata: - meta_type = metadata.get(RiggerConstants.PROXY_META_TYPE) + meta_type = metadata.get(RiggerConstants.META_PROXY_PURPOSE) if meta_type and self.tag_thumb in meta_type: _thumb = True elif meta_type and self.tag_index in meta_type: @@ -323,7 +323,7 @@ def read_proxies_from_dict(self, proxy_dict): self.refresh_proxies_list(thumb=_thumb, index=_index, middle=_middle, ring=_ring, pinky=_pinky, extra=_extra) print(proxy_dict) - self.read_type_matching_proxy_from_dict(proxy_dict) + self.read_purpose_matching_proxy_from_dict(proxy_dict) # --------------------------------------------------- Misc --------------------------------------------------- def is_valid(self): @@ -376,8 +376,8 @@ def build_rig(self, **kwargs): Runs post rig script. """ for digit in self.proxies: - digit_jnt = find_joint_node_from_uuid(digit.get_uuid()) - meta_type = get_meta_type_from_dict(digit.get_metadata()) + digit_jnt = find_joint_from_uuid(digit.get_uuid()) + meta_type = get_meta_purpose_from_dict(digit.get_metadata()) if meta_type and str(meta_type).endswith("End"): set_color_viewport(obj_list=digit_jnt, rgb_color=ColorConstants.RigJoint.END) else: diff --git a/gt/tools/auto_rigger/rig_module_biped_leg.py b/gt/tools/auto_rigger/rig_module_biped_leg.py index d0356725..38cd90e1 100644 --- a/gt/tools/auto_rigger/rig_module_biped_leg.py +++ b/gt/tools/auto_rigger/rig_module_biped_leg.py @@ -3,24 +3,24 @@ github.com/TrevisanGMW/gt-tools """ from gt.tools.auto_rigger.rig_utils import duplicate_joint_for_automation, get_proxy_offset, rescale_joint_radius -from gt.tools.auto_rigger.rig_utils import find_objects_with_attr, find_proxy_node_from_uuid, get_driven_joint -from gt.tools.auto_rigger.rig_utils import find_joint_node_from_uuid, find_or_create_joint_automation_group -from gt.tools.auto_rigger.rig_utils import find_direction_curve_node +from gt.tools.auto_rigger.rig_utils import find_direction_curve, create_ctrl_curve, find_drivers_from_joint +from gt.tools.auto_rigger.rig_utils import find_objects_with_attr, find_proxy_from_uuid, get_driven_joint +from gt.tools.auto_rigger.rig_utils import find_joint_from_uuid, find_or_create_joint_automation_group from gt.utils.attr_utils import add_attr, hide_lock_default_attrs, set_attr_state, set_attr from gt.utils.color_utils import ColorConstants, set_color_viewport, set_color_outliner from gt.tools.auto_rigger.rig_framework import Proxy, ModuleGeneric, OrientationData -from gt.tools.auto_rigger.rig_constants import RiggerConstants -from gt.utils.transform_utils import match_translate, Vector3 +from gt.tools.auto_rigger.rig_constants import RiggerConstants, RiggerDriverTypes +from gt.utils.transform_utils import match_translate, Vector3, match_transform +from gt.utils.hierarchy_utils import add_offset_transform from gt.utils.math_utils import dist_center_to_center from gt.utils.naming_utils import NamingConstants -from gt.utils.node_utils import create_node +from gt.utils.node_utils import create_node, Node from gt.utils.curve_utils import get_curve from gt.utils import hierarchy_utils from gt.ui import resource_library import maya.cmds as cmds import logging - # Logging Setup logging.basicConfig() logger = logging.getLogger(__name__) @@ -48,37 +48,38 @@ def __init__(self, name="Leg", prefix=None, suffix=None): # Default Proxies self.hip = Proxy(name=hip_name) self.hip.set_locator_scale(scale=2) - self.hip.set_meta_type(value="hip") + self.hip.set_meta_purpose(value="hip") + self.hip.add_driver_type(driver_type=["fk"]) self.knee = Proxy(name=knee_name) self.knee.set_curve(curve=get_curve('_proxy_joint_arrow_pos_z')) self.knee.set_locator_scale(scale=2) - self.knee.add_meta_parent(line_parent=self.hip) + self.knee.add_line_parent(line_parent=self.hip) self.knee.set_parent_uuid(uuid=self.hip.get_uuid()) - self.knee.set_meta_type(value="knee") + self.knee.set_meta_purpose(value="knee") self.ankle = Proxy(name=ankle_name) self.ankle.set_locator_scale(scale=2) - self.ankle.add_meta_parent(line_parent=self.knee) - self.ankle.set_meta_type(value="ankle") + self.ankle.add_line_parent(line_parent=self.knee) + self.ankle.set_meta_purpose(value="ankle") self.ball = Proxy(name=ball_name) self.ball.set_locator_scale(scale=2) - self.ball.add_meta_parent(line_parent=self.ankle) + self.ball.add_line_parent(line_parent=self.ankle) self.ball.set_parent_uuid(uuid=self.ankle.get_uuid()) - self.ball.set_meta_type(value="ball") + self.ball.set_meta_purpose(value="ball") self.toe = Proxy(name=toe_name) self.toe.set_locator_scale(scale=1) self.toe.set_parent_uuid(uuid=self.ball.get_uuid()) self.toe.set_parent_uuid_from_proxy(parent_proxy=self.ball) - self.toe.set_meta_type(value="toe") + self.toe.set_meta_purpose(value="toe") self.heel = Proxy(name=heel_name) self.heel.set_locator_scale(scale=1) - self.heel.add_meta_parent(line_parent=self.ankle) + self.heel.add_line_parent(line_parent=self.ankle) self.heel.add_color(rgb_color=ColorConstants.RigProxy.PIVOT) - self.heel.set_meta_type(value="heel") + self.heel.set_meta_purpose(value="heel") # Initial Pose hip_pos = Vector3(y=84.5) @@ -132,7 +133,7 @@ def read_proxies_from_dict(self, proxy_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.read_type_matching_proxy_from_dict(proxy_dict) + self.read_purpose_matching_proxy_from_dict(proxy_dict) # --------------------------------------------------- Misc --------------------------------------------------- def is_valid(self): @@ -161,13 +162,13 @@ def build_proxy_setup(self): When in a project, this runs after the "build_proxy" is done in all modules. """ # Get Maya Elements - root = find_objects_with_attr(RiggerConstants.REF_ROOT_PROXY_ATTR) - hip = find_proxy_node_from_uuid(self.hip.get_uuid()) - knee = find_proxy_node_from_uuid(self.knee.get_uuid()) - ankle = find_proxy_node_from_uuid(self.ankle.get_uuid()) - ball = find_proxy_node_from_uuid(self.ball.get_uuid()) - heel = find_proxy_node_from_uuid(self.heel.get_uuid()) - toe = find_proxy_node_from_uuid(self.toe.get_uuid()) + root = find_objects_with_attr(RiggerConstants.REF_ATTR_ROOT_PROXY) + hip = find_proxy_from_uuid(self.hip.get_uuid()) + knee = find_proxy_from_uuid(self.knee.get_uuid()) + ankle = find_proxy_from_uuid(self.ankle.get_uuid()) + ball = find_proxy_from_uuid(self.ball.get_uuid()) + heel = find_proxy_from_uuid(self.heel.get_uuid()) + toe = find_proxy_from_uuid(self.toe.get_uuid()) self.hip.apply_offset_transform() self.knee.apply_offset_transform() @@ -176,11 +177,11 @@ def build_proxy_setup(self): self.heel.apply_offset_transform() # Hip ----------------------------------------------------------------------------------- - hide_lock_default_attrs(hip, translate=False) + hide_lock_default_attrs(hip, rotate=True, scale=True) # Knee --------------------------------------------------------------------------------- knee_tag = knee.get_short_name() - hide_lock_default_attrs(knee, translate=False, rotate=False) + hide_lock_default_attrs(knee, scale=True) # Knee Setup - Always Between Hip and Ankle knee_offset = get_proxy_offset(knee) @@ -301,19 +302,19 @@ def build_skeleton_hierarchy(self): super().build_skeleton_hierarchy() # Passthrough self.ankle.clear_parent_uuid() - heel_jnt = find_joint_node_from_uuid(self.heel.get_uuid()) + heel_jnt = find_joint_from_uuid(self.heel.get_uuid()) if heel_jnt and cmds.objExists(heel_jnt): cmds.delete(heel_jnt) def build_rig(self, **kwargs): # Get Elements - direction_crv = find_direction_curve_node() - module_parent_jnt = find_joint_node_from_uuid(self.get_parent_uuid()) # TODO TEMP @@@ - hip_jnt = find_joint_node_from_uuid(self.hip.get_uuid()) - knee_jnt = find_joint_node_from_uuid(self.knee.get_uuid()) - ankle_jnt = find_joint_node_from_uuid(self.ankle.get_uuid()) - ball_jnt = find_joint_node_from_uuid(self.ball.get_uuid()) - toe_jnt = find_joint_node_from_uuid(self.toe.get_uuid()) + direction_crv = find_direction_curve() + # module_parent_jnt = find_joint_node_from_uuid(self.get_parent_uuid()) # TODO TEMP @@@ + hip_jnt = find_joint_from_uuid(self.hip.get_uuid()) + knee_jnt = find_joint_from_uuid(self.knee.get_uuid()) + ankle_jnt = find_joint_from_uuid(self.ankle.get_uuid()) + ball_jnt = find_joint_from_uuid(self.ball.get_uuid()) + toe_jnt = find_joint_from_uuid(self.toe.get_uuid()) # Set Colors leg_jnt_list = [hip_jnt, knee_jnt, ankle_jnt, ball_jnt, toe_jnt] @@ -331,6 +332,7 @@ def build_rig(self, **kwargs): cmds.setAttr(f'{hip_jnt}.preferredAngleZ', 90) cmds.setAttr(f'{knee_jnt}.preferredAngleZ', -90) + # Create Parent Automation Elements joint_automation_grp = find_or_create_joint_automation_group() module_parent_jnt = get_driven_joint(self.get_parent_uuid()) hierarchy_utils.parent(source_objects=module_parent_jnt, target_parent=joint_automation_grp) @@ -367,6 +369,30 @@ def build_rig(self, **kwargs): print(f'leg_scale: {leg_scale}') print("build leg rig!") + # Hip Control + hip_ctrl = self._assemble_ctrl_name(name=self.hip.get_name()) + hip_ctrl = create_ctrl_curve(name=hip_ctrl, curve_file_name="_circle_pos_x") + self.add_driver_uuid_attr(target=hip_ctrl, driver_type=RiggerDriverTypes.FK, proxy_purpose=self.hip) + hip_offset = add_offset_transform(target_list=hip_ctrl)[0] + hip_offset = Node(hip_offset) + match_transform(source=hip_jnt, target_list=hip_offset) + # scale_shapes(obj_transform=hip_ctrl, offset=spine_scale / 10) + hierarchy_utils.parent(source_objects=hip_offset, target_parent=direction_crv) + + self.module_children_drivers = [hip_offset] + + def build_rig_post(self): + """ + Runs post rig creation script. + This step runs after the execution of "build_rig" is complete in all modules. + Used to define automation or connections that require external elements to exist. + """ + module_parent_jnt = find_joint_from_uuid(self.get_parent_uuid()) + if module_parent_jnt: + drivers = find_drivers_from_joint(module_parent_jnt, as_list=True) + if drivers: + hierarchy_utils.parent(source_objects=self.module_children_drivers, target_parent=drivers[0]) + class ModuleBipedLegLeft(ModuleBipedLeg): def __init__(self, name="Left Leg", prefix=NamingConstants.Prefix.LEFT, suffix=None): @@ -418,43 +444,30 @@ def __init__(self, name="Right Leg", prefix=NamingConstants.Prefix.RIGHT, suffix if __name__ == "__main__": logger.setLevel(logging.DEBUG) + # Auto Reload Script - Must have been initialized using "Run-Only" mode. + from gt.utils.session_utils import remove_modules_startswith + remove_modules_startswith("gt.tools.auto_rigger.rig") cmds.file(new=True, force=True) from gt.tools.auto_rigger.rig_framework import RigProject - a_proxy = Proxy() - a_proxy.set_initial_position(y=84.5) - a_proxy.set_name("pelvis") + from gt.tools.auto_rigger.rig_module_spine import ModuleSpine + + a_spine = ModuleSpine() a_leg = ModuleBipedLeg() a_leg_lf = ModuleBipedLegLeft() a_leg_rt = ModuleBipedLegRight() a_module = ModuleGeneric() - a_module.add_to_proxies(a_proxy) - a_leg_lf.set_parent_uuid(a_proxy.get_uuid()) - a_leg_rt.set_parent_uuid(a_proxy.get_uuid()) + + spine_hip_uuid = a_spine.hip.get_uuid() + a_leg_lf.set_parent_uuid(spine_hip_uuid) + a_leg_rt.set_parent_uuid(spine_hip_uuid) a_project = RigProject() - a_project.add_to_modules(a_module) + a_project.add_to_modules(a_spine) a_project.add_to_modules(a_leg_lf) a_project.add_to_modules(a_leg_rt) - # a_project.add_to_modules(a_leg) a_project.build_proxy() a_project.build_rig() - # for obj in ["hip", "knee", "ankle", "ball", "toe", "heelPivot"]: - # cmds.setAttr(f'{obj}.displayLocalAxis', 1) - # cmds.setAttr(f'rt_{obj}.displayLocalAxis', 1) - # - # cmds.setAttr(f'{NamingConstants.Prefix.LEFT}_{a_leg_lf.hip.get_name()}.tx', 10) - # cmds.setAttr(f'{NamingConstants.Prefix.LEFT}_{a_leg_lf.ankle.get_name()}.tz', 5) - # cmds.setAttr(f'{NamingConstants.Prefix.LEFT}_{a_leg_lf.knee.get_name()}.tz', 3) - # cmds.setAttr(f'{NamingConstants.Prefix.LEFT}_{a_leg_lf.ankle.get_name()}.ry', 45) - # a_project.read_data_from_scene() - # dictionary = a_project.get_project_as_dict() - # - # cmds.file(new=True, force=True) - # a_project2 = RigProject() - # a_project2.read_data_from_dict(dictionary) - # a_project2.build_proxy() - # Frame all cmds.viewFit(all=True) diff --git a/gt/tools/auto_rigger/rig_module_head.py b/gt/tools/auto_rigger/rig_module_head.py index a3ecb351..6a2a7055 100644 --- a/gt/tools/auto_rigger/rig_module_head.py +++ b/gt/tools/auto_rigger/rig_module_head.py @@ -2,21 +2,25 @@ Auto Rigger Head Modules github.com/TrevisanGMW/gt-tools """ -from gt.tools.auto_rigger.rig_utils import find_proxy_node_from_uuid, find_joint_node_from_uuid +from gt.tools.auto_rigger.rig_utils import find_proxy_from_uuid, find_joint_from_uuid, find_direction_curve +from gt.tools.auto_rigger.rig_utils import create_ctrl_curve, find_drivers_from_joint from gt.tools.auto_rigger.rig_framework import Proxy, ModuleGeneric, OrientationData +from gt.tools.auto_rigger.rig_constants import RiggerConstants, RiggerDriverTypes from gt.utils.color_utils import ColorConstants, set_color_viewport from gt.utils.joint_utils import copy_parent_orients, reset_orients -from gt.tools.auto_rigger.rig_constants import RiggerConstants from gt.utils.constraint_utils import equidistant_constraints +from gt.utils.transform_utils import Vector3, match_transform from gt.tools.auto_rigger.rig_utils import get_proxy_offset +from gt.utils.hierarchy_utils import add_offset_transform +from gt.utils.math_utils import dist_center_to_center from gt.utils.naming_utils import NamingConstants -from gt.utils.transform_utils import Vector3 +from gt.utils.node_utils import Node +from gt.utils import hierarchy_utils from gt.ui import resource_library import maya.cmds as cmds import logging import re - # Logging Setup logging.basicConfig() logger = logging.getLogger(__name__) @@ -41,21 +45,21 @@ def __init__(self, name="Head", prefix=None, suffix=None): pos_neck_base = Vector3(y=137) self.neck_base.set_initial_position(xyz=pos_neck_base) self.neck_base.set_locator_scale(scale=1.5) - self.neck_base.set_meta_type(value="neckBase") + self.neck_base.set_meta_purpose(value="neckBase") # Head (Chain End) self.head = Proxy(name="head") pos_head = Vector3(y=142.5) self.head.set_initial_position(xyz=pos_head) self.head.set_locator_scale(scale=1.5) - self.head.set_meta_type(value="head") + self.head.set_meta_purpose(value="head") # Head End self.head_end = Proxy(name=f"head{_end_suffix}") pos_head_end = Vector3(y=160) self.head_end.set_initial_position(xyz=pos_head_end) self.head_end.set_locator_scale(scale=1) - self.head_end.set_meta_type(value="headEnd") + self.head_end.set_meta_purpose(value="headEnd") self.head_end.set_parent_uuid(self.head.get_uuid()) self.head_end.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) @@ -64,7 +68,7 @@ def __init__(self, name="Head", prefix=None, suffix=None): pos_jaw = Vector3(y=147.5, z=2.5) self.jaw.set_initial_position(xyz=pos_jaw) self.jaw.set_locator_scale(scale=1.5) - self.jaw.set_meta_type(value="jaw") + self.jaw.set_meta_purpose(value="jaw") self.jaw.set_parent_uuid(self.head.get_uuid()) # Jaw End @@ -72,7 +76,7 @@ def __init__(self, name="Head", prefix=None, suffix=None): pos_jaw_end = Vector3(y=142.5, z=11) self.jaw_end.set_initial_position(xyz=pos_jaw_end) self.jaw_end.set_locator_scale(scale=1) - self.jaw_end.set_meta_type(value="jawEnd") + self.jaw_end.set_meta_purpose(value="jawEnd") self.jaw_end.set_parent_uuid(self.jaw.get_uuid()) self.jaw_end.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) @@ -81,7 +85,7 @@ def __init__(self, name="Head", prefix=None, suffix=None): pos_lt_eye = Vector3(x=3.5, y=151, z=8.7) self.lt_eye.set_initial_position(xyz=pos_lt_eye) self.lt_eye.set_locator_scale(scale=2.5) - self.lt_eye.set_meta_type(value="eyeLeft") + self.lt_eye.set_meta_purpose(value="eyeLeft") self.lt_eye.set_parent_uuid(self.head.get_uuid()) # Right Eye @@ -89,7 +93,7 @@ def __init__(self, name="Head", prefix=None, suffix=None): pos_rt_eye = Vector3(x=-3.5, y=151, z=8.7) self.rt_eye.set_initial_position(xyz=pos_rt_eye) self.rt_eye.set_locator_scale(scale=2.5) - self.rt_eye.set_meta_type(value="eyeRight") + self.rt_eye.set_meta_purpose(value="eyeRight") self.rt_eye.set_parent_uuid(self.head.get_uuid()) # Neck Mid (In-between) @@ -120,8 +124,8 @@ def set_mid_neck_num(self, neck_mid_num): new_neck_mid = Proxy(name=_neck_mid_name) new_neck_mid.set_locator_scale(scale=1) new_neck_mid.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) - new_neck_mid.set_meta_type(value=_neck_mid_name) - new_neck_mid.add_meta_parent(line_parent=_parent_uuid) + new_neck_mid.set_meta_purpose(value=_neck_mid_name) + new_neck_mid.add_line_parent(line_parent=_parent_uuid) new_neck_mid.set_parent_uuid(uuid=_parent_uuid) _parent_uuid = new_neck_mid.get_uuid() self.neck_mid_list.append(new_neck_mid) @@ -130,9 +134,9 @@ def set_mid_neck_num(self, neck_mid_num): self.neck_mid_list = self.neck_mid_list[:neck_mid_num] # Truncate the list if self.neck_mid_list: - self.head.add_meta_parent(line_parent=self.neck_mid_list[-1].get_uuid()) + self.head.add_line_parent(line_parent=self.neck_mid_list[-1].get_uuid()) else: - self.head.add_meta_parent(line_parent=self.neck_base.get_uuid()) + self.head.add_line_parent(line_parent=self.neck_base.get_uuid()) self.refresh_proxies_list() @@ -175,11 +179,11 @@ def read_proxies_from_dict(self, proxy_dict): for uuid, description in proxy_dict.items(): metadata = description.get("metadata") if metadata: - meta_type = metadata.get(RiggerConstants.PROXY_META_TYPE) + meta_type = metadata.get(RiggerConstants.META_PROXY_PURPOSE) if bool(re.match(neck_mid_pattern, meta_type)): _neck_mid_num += 1 self.set_mid_neck_num(_neck_mid_num) - self.read_type_matching_proxy_from_dict(proxy_dict) + self.read_purpose_matching_proxy_from_dict(proxy_dict) self.refresh_proxies_list() # --------------------------------------------------- Misc --------------------------------------------------- @@ -210,12 +214,12 @@ def build_proxy_setup(self): When in a project, this runs after the "build_proxy" is done in all modules. """ # Get Maya Elements - hip = find_proxy_node_from_uuid(self.neck_base.get_uuid()) - chest = find_proxy_node_from_uuid(self.head.get_uuid()) + hip = find_proxy_from_uuid(self.neck_base.get_uuid()) + chest = find_proxy_from_uuid(self.head.get_uuid()) neck_mid_list = [] for neck_mid in self.neck_mid_list: - neck_node = find_proxy_node_from_uuid(neck_mid.get_uuid()) + neck_node = find_proxy_from_uuid(neck_mid.get_uuid()) neck_mid_list.append(neck_node) self.neck_base.apply_offset_transform() self.head.apply_offset_transform() @@ -254,28 +258,67 @@ def build_skeleton_hierarchy(self): super().build_skeleton_hierarchy() # Passthrough self.head.clear_parent_uuid() - head_jnt = find_joint_node_from_uuid(self.head.get_uuid()) - head_end_jnt = find_joint_node_from_uuid(self.head_end.get_uuid()) - jaw_jnt = find_joint_node_from_uuid(self.jaw.get_uuid()) - jaw_end_jnt = find_joint_node_from_uuid(self.jaw_end.get_uuid()) - lt_eye = find_joint_node_from_uuid(self.lt_eye.get_uuid()) - rt_eye = find_joint_node_from_uuid(self.rt_eye.get_uuid()) + def build_rig(self, **kwargs): + # Get Elements + direction_crv = find_direction_curve() + neck_base_jnt = find_joint_from_uuid(self.neck_base.get_uuid()) + head_jnt = find_joint_from_uuid(self.head.get_uuid()) + head_end_jnt = find_joint_from_uuid(self.head_end.get_uuid()) + jaw_jnt = find_joint_from_uuid(self.jaw.get_uuid()) + jaw_end_jnt = find_joint_from_uuid(self.jaw_end.get_uuid()) + lt_eye = find_joint_from_uuid(self.lt_eye.get_uuid()) + rt_eye = find_joint_from_uuid(self.rt_eye.get_uuid()) copy_parent_orients(joint_list=[head_jnt, head_end_jnt]) reset_orients(joint_list=[lt_eye, rt_eye], verbose=True) set_color_viewport(obj_list=[head_end_jnt, jaw_end_jnt], rgb_color=ColorConstants.RigJoint.END) set_color_viewport(obj_list=[lt_eye, rt_eye], rgb_color=ColorConstants.RigJoint.UNIQUE) + # Get Scale + head_scale = dist_center_to_center(neck_base_jnt, head_jnt) + head_scale += dist_center_to_center(head_jnt, head_end_jnt) + + neck_base_ctrl = self._assemble_ctrl_name(name=self.neck_base.get_name()) + neck_base_ctrl = create_ctrl_curve(name=neck_base_ctrl, curve_file_name="_pin_neg_z") + self.add_driver_uuid_attr(target=neck_base_ctrl, + driver_type=RiggerDriverTypes.FK, + proxy_purpose=self.neck_base) + neck_base_offset = add_offset_transform(target_list=neck_base_ctrl)[0] + neck_base_offset = Node(neck_base_offset) + match_transform(source=neck_base_jnt, target_list=neck_base_offset) + hierarchy_utils.parent(source_objects=neck_base_offset, target_parent=direction_crv) + + self.module_children_drivers = [neck_base_offset] + + def build_rig_post(self): + """ + Runs post rig creation script. + This step runs after the execution of "build_rig" is complete in all modules. + Used to define automation or connections that require external elements to exist. + TODO @@@ ADD TO BASE OBJECT??? + """ + module_parent_jnt = find_joint_from_uuid(self.get_parent_uuid()) + if module_parent_jnt: + drivers = find_drivers_from_joint(module_parent_jnt, as_list=True) + if drivers: + hierarchy_utils.parent(source_objects=self.module_children_drivers, target_parent=drivers[0]) + if __name__ == "__main__": logger.setLevel(logging.DEBUG) + # Auto Reload Script - Must have been initialized using "Run-Only" mode. + from gt.utils.session_utils import remove_modules_startswith + remove_modules_startswith("gt.tools.auto_rigger.rig") cmds.file(new=True, force=True) from gt.tools.auto_rigger.rig_framework import RigProject + from gt.tools.auto_rigger.rig_module_spine import ModuleSpine + a_spine = ModuleSpine() a_head = ModuleHead() - # a_head.set_mid_neck_num(0) - # a_head.set_mid_neck_num(6) + spine_chest_uuid = a_spine.chest.get_uuid() + a_head.set_parent_uuid(spine_chest_uuid) a_project = RigProject() + a_project.add_to_modules(a_spine) a_project.add_to_modules(a_head) a_project.build_proxy() a_project.build_rig() diff --git a/gt/tools/auto_rigger/rig_module_root.py b/gt/tools/auto_rigger/rig_module_root.py index 3861772d..cccd9c53 100644 --- a/gt/tools/auto_rigger/rig_module_root.py +++ b/gt/tools/auto_rigger/rig_module_root.py @@ -2,9 +2,9 @@ Auto Rigger Root Module github.com/TrevisanGMW/gt-tools """ -from gt.tools.auto_rigger.rig_utils import find_proxy_root_curve_node, find_control_root_curve_node -from gt.tools.auto_rigger.rig_utils import find_proxy_node_from_uuid, find_vis_lines_from_uuid -from gt.tools.auto_rigger.rig_utils import find_joint_node_from_uuid +from gt.tools.auto_rigger.rig_utils import find_proxy_root_curve, find_control_root_curve +from gt.tools.auto_rigger.rig_utils import find_proxy_from_uuid, find_vis_lines_from_uuid +from gt.tools.auto_rigger.rig_utils import find_joint_from_uuid from gt.tools.auto_rigger.rig_framework import Proxy, ModuleGeneric from gt.utils.color_utils import ColorConstants, set_color_viewport from gt.utils.attr_utils import set_attr, add_attr @@ -36,7 +36,7 @@ def __init__(self, name="Root", prefix=None, suffix=None): # Default Proxies self.root = Proxy(name="root") self.root.set_locator_scale(scale=1) - self.root.set_meta_type(value="root") + self.root.set_meta_purpose(value="root") self.root.add_color(ColorConstants.RigProxy.TWEAK) self.proxies = [self.root] @@ -60,7 +60,7 @@ def read_proxies_from_dict(self, proxy_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.read_type_matching_proxy_from_dict(proxy_dict) + self.read_purpose_matching_proxy_from_dict(proxy_dict) # --------------------------------------------------- Misc --------------------------------------------------- def is_valid(self): @@ -92,8 +92,8 @@ def build_proxy_setup(self): """ super().build_proxy_setup() # Passthrough # Root Visibility Setup - proxy_root = find_proxy_root_curve_node() - root = find_proxy_node_from_uuid(self.root.get_uuid()) + proxy_root = find_proxy_root_curve() + root = find_proxy_from_uuid(self.root.get_uuid()) root_lines = find_vis_lines_from_uuid(parent_uuid=self.root.get_uuid()) metadata = self.get_metadata() @@ -110,13 +110,13 @@ def build_proxy_setup(self): if isinstance(hide_root, bool): set_attr(obj_list=proxy_root, attr_list="rootVisibility", value=hide_root) - def build_rig_post(self): + def build_rig(self, **kwargs): """ Runs post rig script. When in a project, this runs after the "build_rig" is done in all modules. """ - root_jnt = find_joint_node_from_uuid(self.root.get_uuid()) - root_ctrl = find_control_root_curve_node() + root_jnt = find_joint_from_uuid(self.root.get_uuid()) + root_ctrl = find_control_root_curve() cmds.parentConstraint(root_ctrl, root_jnt) set_color_viewport(obj_list=root_jnt, rgb_color=ColorConstants.RigJoint.ROOT) diff --git a/gt/tools/auto_rigger/rig_module_spine.py b/gt/tools/auto_rigger/rig_module_spine.py index 90efe083..d595f1f9 100644 --- a/gt/tools/auto_rigger/rig_module_spine.py +++ b/gt/tools/auto_rigger/rig_module_spine.py @@ -2,17 +2,20 @@ Auto Rigger Spine Modules github.com/TrevisanGMW/gt-tools """ -from gt.tools.auto_rigger.rig_utils import find_or_create_joint_automation_group, get_driven_joint -from gt.tools.auto_rigger.rig_utils import duplicate_joint_for_automation, rescale_joint_radius -from gt.tools.auto_rigger.rig_utils import find_proxy_node_from_uuid, find_direction_curve_node -from gt.tools.auto_rigger.rig_utils import find_joint_node_from_uuid +from gt.tools.auto_rigger.rig_utils import find_or_create_joint_automation_group, get_driven_joint, create_ctrl_curve +from gt.tools.auto_rigger.rig_utils import find_joint_from_uuid, expose_rotation_order, find_drivers_from_joint +from gt.tools.auto_rigger.rig_utils import find_proxy_from_uuid, find_direction_curve, rescale_joint_radius +from gt.tools.auto_rigger.rig_utils import duplicate_joint_for_automation, offset_control_orientation +from gt.utils.transform_utils import Vector3, scale_shapes, match_transform, translate_shapes from gt.utils.color_utils import ColorConstants, set_color_viewport, set_color_outliner from gt.tools.auto_rigger.rig_framework import Proxy, ModuleGeneric, OrientationData -from gt.tools.auto_rigger.rig_constants import RiggerConstants +from gt.tools.auto_rigger.rig_constants import RiggerConstants, RiggerDriverTypes +from gt.utils.attr_utils import add_separator_attr, set_attr_state from gt.utils.constraint_utils import equidistant_constraints from gt.tools.auto_rigger.rig_utils import get_proxy_offset +from gt.utils.hierarchy_utils import add_offset_transform from gt.utils.math_utils import dist_center_to_center -from gt.utils.transform_utils import Vector3 +from gt.utils.node_utils import Node from gt.utils import hierarchy_utils from gt.ui import resource_library import maya.cmds as cmds @@ -42,14 +45,16 @@ def __init__(self, name="Spine", prefix=None, suffix=None): pos_hip = Vector3(y=84.5) self.hip.set_initial_position(xyz=pos_hip) self.hip.set_locator_scale(scale=1.5) - self.hip.set_meta_type(value="hip") + self.hip.set_meta_purpose(value="hip") + self.hip.add_driver_type(driver_type=[RiggerDriverTypes.FK, RiggerDriverTypes.COG]) # Chest (End) self.chest = Proxy(name="chest") pos_chest = Vector3(y=114.5) self.chest.set_initial_position(xyz=pos_chest) self.chest.set_locator_scale(scale=1.5) - self.chest.set_meta_type(value="chest") + self.chest.set_meta_purpose(value="chest") + self.chest.add_driver_type(driver_type=["fk"]) # Spines (In-between) self.spines = [] @@ -78,9 +83,10 @@ def set_spine_num(self, spine_num): new_spine = Proxy(name=f'spine{str(num + 1).zfill(2)}') new_spine.set_locator_scale(scale=1) new_spine.add_color(rgb_color=ColorConstants.RigProxy.FOLLOWER) - new_spine.set_meta_type(value=f'spine{str(num + 1).zfill(2)}') - new_spine.add_meta_parent(line_parent=_parent_uuid) + new_spine.set_meta_purpose(value=f'spine{str(num + 1).zfill(2)}') + new_spine.add_line_parent(line_parent=_parent_uuid) new_spine.set_parent_uuid(uuid=_parent_uuid) + new_spine.add_driver_type(driver_type=[RiggerDriverTypes.FK]) _parent_uuid = new_spine.get_uuid() self.spines.append(new_spine) # New number lower than current - Remove unnecessary proxies @@ -88,9 +94,9 @@ def set_spine_num(self, spine_num): self.spines = self.spines[:spine_num] # Truncate the list if self.spines: - self.chest.add_meta_parent(line_parent=self.spines[-1].get_uuid()) + self.chest.add_line_parent(line_parent=self.spines[-1].get_uuid()) else: - self.chest.add_meta_parent(line_parent=self.hip.get_uuid()) + self.chest.add_line_parent(line_parent=self.hip.get_uuid()) self.refresh_proxies_list() @@ -128,11 +134,11 @@ def read_proxies_from_dict(self, proxy_dict): for uuid, description in proxy_dict.items(): metadata = description.get("metadata") if metadata: - meta_type = metadata.get(RiggerConstants.PROXY_META_TYPE) + meta_type = metadata.get(RiggerConstants.META_PROXY_PURPOSE) if bool(re.match(spine_pattern, meta_type)): _spine_num += 1 self.set_spine_num(_spine_num) - self.read_type_matching_proxy_from_dict(proxy_dict) + self.read_purpose_matching_proxy_from_dict(proxy_dict) self.refresh_proxies_list() # --------------------------------------------------- Misc --------------------------------------------------- @@ -163,12 +169,12 @@ def build_proxy_setup(self): When in a project, this runs after the "build_proxy" is done in all modules. """ # Get Maya Elements - hip = find_proxy_node_from_uuid(self.hip.get_uuid()) - chest = find_proxy_node_from_uuid(self.chest.get_uuid()) + hip = find_proxy_from_uuid(self.hip.get_uuid()) + chest = find_proxy_from_uuid(self.chest.get_uuid()) spines = [] for spine in self.spines: - spine_node = find_proxy_node_from_uuid(spine.get_uuid()) + spine_node = find_proxy_from_uuid(spine.get_uuid()) spines.append(spine_node) self.hip.apply_offset_transform() self.chest.apply_offset_transform() @@ -199,13 +205,13 @@ def build_skeleton_hierarchy(self): def build_rig(self, **kwargs): # Get Elements - direction_crv = find_direction_curve_node() - module_parent_jnt = find_joint_node_from_uuid(self.get_parent_uuid()) # TODO TEMP @@@ - hip_jnt = find_joint_node_from_uuid(self.hip.get_uuid()) - chest_jnt = find_joint_node_from_uuid(self.chest.get_uuid()) + direction_crv = find_direction_curve() + module_parent_jnt = find_joint_from_uuid(self.get_parent_uuid()) # TODO TEMP @@@ + hip_jnt = find_joint_from_uuid(self.hip.get_uuid()) + chest_jnt = find_joint_from_uuid(self.chest.get_uuid()) middle_jnt_list = [] for proxy in self.spines: - mid_jnt = find_joint_node_from_uuid(proxy.get_uuid()) + mid_jnt = find_joint_from_uuid(proxy.get_uuid()) if mid_jnt: middle_jnt_list.append(mid_jnt) @@ -216,7 +222,7 @@ def build_rig(self, **kwargs): # Set Colors set_color_viewport(obj_list=module_jnt_list, rgb_color=ColorConstants.RigJoint.GENERAL) - # Get Scale + # Get General Scale spine_scale = dist_center_to_center(hip_jnt, chest_jnt) joint_automation_grp = find_or_create_joint_automation_group() @@ -247,13 +253,119 @@ def build_rig(self, **kwargs): set_color_viewport(obj_list=fk_joints, rgb_color=ColorConstants.RigJoint.FK) set_color_outliner(obj_list=fk_joints, rgb_color=ColorConstants.RigOutliner.FK) + # COG Control + cog_ctrl = self._assemble_ctrl_name(name="cog") + cog_ctrl = create_ctrl_curve(name=cog_ctrl, curve_file_name="_circle_pos_x") + self.add_driver_uuid_attr(target=cog_ctrl, driver_type=RiggerDriverTypes.COG, proxy_purpose=self.hip) + cog_offset = Node(add_offset_transform(target_list=cog_ctrl)[0]) + match_transform(source=hip_jnt, target_list=cog_offset) + scale_shapes(obj_transform=cog_ctrl, offset=spine_scale / 4) + offset_control_orientation(ctrl=cog_ctrl, offset_transform=cog_offset, orient_tuple=(-90, -90, 0)) + hierarchy_utils.parent(source_objects=cog_offset, target_parent=direction_crv) + # Attributes + set_attr_state(attribute_path=f"{cog_ctrl}.v", locked=True, hidden=True) # Hide and Lock Visibility + add_separator_attr(target_object=cog_ctrl, attr_name=RiggerConstants.SEPARATOR_OPTIONS) + expose_rotation_order(cog_ctrl) + cmds.parentConstraint(cog_ctrl, hip_fk, maintainOffset=True) + + # Hip Control + hip_ctrl = self._assemble_ctrl_name(name=self.hip.get_name()) + hip_ctrl = create_ctrl_curve(name=hip_ctrl, curve_file_name="_wavy_circle_pos_x") + self.add_driver_uuid_attr(target=hip_ctrl, driver_type=RiggerDriverTypes.FK, proxy_purpose=self.hip) + hip_offset = Node(add_offset_transform(target_list=hip_ctrl)[0]) + match_transform(source=hip_jnt, target_list=hip_offset) + scale_shapes(obj_transform=hip_ctrl, offset=spine_scale / 6) + offset_control_orientation(ctrl=hip_ctrl, offset_transform=hip_offset, orient_tuple=(-90, -90, 0)) + hierarchy_utils.parent(source_objects=hip_offset, target_parent=cog_ctrl) + # Attributes + set_attr_state(attribute_path=f"{hip_ctrl}.v", locked=True, hidden=True) # Hide and Lock Visibility + add_separator_attr(target_object=hip_ctrl, attr_name=RiggerConstants.SEPARATOR_OPTIONS) + expose_rotation_order(hip_ctrl) + + # FK Controls + spine_ctrls = [] + last_mid_parent_ctrl = cog_ctrl + for spine_proxy, fk_jnt in zip(self.spines, mid_fk_list): + spine_ctrl = self._assemble_ctrl_name(name=spine_proxy.get_name()) + spine_ctrl = create_ctrl_curve(name=spine_ctrl, curve_file_name="_cube") + self.add_driver_uuid_attr(target=spine_ctrl, driver_type=RiggerDriverTypes.FK, proxy_purpose=spine_proxy) + spine_offset = Node(add_offset_transform(target_list=spine_ctrl)[0]) + # Move Pivot to Base + translate_shapes(obj_transform=spine_ctrl, offset=(1, 0, 0)) + # Define Shape Scale + _shape_scale = (spine_scale / 20, spine_scale / 4, spine_scale / 3) + child_joint = cmds.listRelatives(fk_jnt, fullPath=True, children=True, typ="joint") + if child_joint: + _distance = dist_center_to_center(obj_a=fk_jnt, obj_b=child_joint[0]) + _shape_height = _distance/4 + _shape_scale = _shape_height, _shape_scale[1], _shape_scale[2] + scale_shapes(obj_transform=spine_ctrl, offset=_shape_scale) + # Position and Constraint + match_transform(source=fk_jnt, target_list=spine_offset) + offset_control_orientation(ctrl=spine_ctrl, offset_transform=spine_offset, orient_tuple=(-90, -90, 0)) + hierarchy_utils.parent(source_objects=spine_offset, target_parent=last_mid_parent_ctrl) + # Attributes + set_attr_state(attribute_path=f"{spine_ctrl}.v", locked=True, hidden=True) # Hide and Lock Visibility + add_separator_attr(target_object=spine_ctrl, attr_name=RiggerConstants.SEPARATOR_OPTIONS) + expose_rotation_order(spine_ctrl) + spine_ctrls.append(spine_ctrl) + cmds.parentConstraint(spine_ctrl, fk_jnt, maintainOffset=True) + last_mid_parent_ctrl = spine_ctrl + + # Chest Control + chest_ctrl = self._assemble_ctrl_name(name=self.chest.get_name()) + chest_ctrl = create_ctrl_curve(name=chest_ctrl, curve_file_name="_cube") + self.add_driver_uuid_attr(target=chest_ctrl, driver_type=RiggerDriverTypes.FK, proxy_purpose=self.chest) + chest_offset = Node(add_offset_transform(target_list=chest_ctrl)[0]) + match_transform(source=chest_jnt, target_list=chest_offset) + translate_shapes(obj_transform=chest_ctrl, offset=(1, 0, 0)) # Move Pivot to Base + _shape_scale = (spine_scale / 4, spine_scale / 4, spine_scale / 3) + scale_shapes(obj_transform=chest_ctrl, offset=_shape_scale) + offset_control_orientation(ctrl=chest_ctrl, offset_transform=chest_offset, orient_tuple=(-90, -90, 0)) + chest_ctrl_parent = spine_ctrls[-1] if spine_ctrls else cog_ctrl + hierarchy_utils.parent(source_objects=chest_offset, target_parent=chest_ctrl_parent) + cmds.parentConstraint(chest_ctrl, chest_fk, maintainOffset=True) + # Attributes + set_attr_state(attribute_path=f"{chest_ctrl}.v", locked=True, hidden=True) # Hide and Lock Visibility + add_separator_attr(target_object=chest_ctrl, attr_name=RiggerConstants.SEPARATOR_OPTIONS) + expose_rotation_order(chest_ctrl) + + # Constraints FK -> Base + for fk_jnt_zip in zip(fk_joints, module_jnt_list): + cmds.parentConstraint(fk_jnt_zip[0], fk_jnt_zip[1]) + + # # TODO TEMP @@@ + # out_find_driver = self.find_driver(driver_type=RiggerDriverTypes.FK, proxy_purpose=self.hip) + # out_find_module_drivers = self.find_module_drivers() + # out_find_proxy_drivers = self.find_proxy_drivers(proxy=self.hip, as_dict=True) + # print(f"out_find_driver:{out_find_driver}") + # print(f"out_find_module_drivers:{out_find_module_drivers}") + # print(f"out_find_proxy_drivers:{out_find_proxy_drivers}") + + self.module_children_drivers = [cog_offset] + + def build_rig_post(self): + """ + Runs post rig creation script. + This step runs after the execution of "build_rig" is complete in all modules. + Used to define automation or connections that require external elements to exist. + """ + module_parent_jnt = find_joint_from_uuid(self.get_parent_uuid()) + if module_parent_jnt: + drivers = find_drivers_from_joint(module_parent_jnt, as_list=True) + if drivers: + hierarchy_utils.parent(source_objects=self.module_children_drivers, target_parent=drivers[0]) + if __name__ == "__main__": logger.setLevel(logging.DEBUG) + + # Auto Reload Script - Must have been initialized using "Run-Only" mode. + from gt.utils.session_utils import remove_modules_startswith + remove_modules_startswith("gt.tools.auto_rigger.rig") cmds.file(new=True, force=True) from gt.tools.auto_rigger.rig_framework import RigProject - a_spine = ModuleSpine() a_spine.set_spine_num(0) a_spine.set_spine_num(6) @@ -261,8 +373,8 @@ def build_rig(self, **kwargs): a_project.add_to_modules(a_spine) a_project.build_proxy() - cmds.setAttr(f'hip.tx', 10) - cmds.setAttr(f'spine02.tx', 10) + # cmds.setAttr(f'hip.tx', 10) + # cmds.setAttr(f'spine02.tx', 10) a_project.read_data_from_scene() dictionary = a_project.get_project_as_dict() diff --git a/gt/tools/auto_rigger/rig_utils.py b/gt/tools/auto_rigger/rig_utils.py index cb789984..4dfad195 100644 --- a/gt/tools/auto_rigger/rig_utils.py +++ b/gt/tools/auto_rigger/rig_utils.py @@ -4,16 +4,18 @@ """ from gt.utils.attr_utils import add_separator_attr, hide_lock_default_attrs, connect_attr, add_attr, set_attr, get_attr from gt.utils.attr_utils import set_attr_state, delete_user_defined_attrs +from gt.utils.transform_utils import get_component_positions_as_dict, set_component_positions_from_dict from gt.utils.color_utils import set_color_viewport, ColorConstants, set_color_outliner -from gt.utils.uuid_utils import get_object_from_uuid_attr, generate_uuid, is_uuid_valid from gt.utils.curve_utils import get_curve, set_curve_width, create_connection_line from gt.tools.auto_rigger.rig_constants import RiggerConstants +from gt.utils.uuid_utils import get_object_from_uuid_attr from gt.utils.hierarchy_utils import duplicate_as_node from gt.utils.naming_utils import NamingConstants from gt.utils import hierarchy_utils from gt.utils.node_utils import Node import maya.cmds as cmds import logging +import json # Logging Setup @@ -29,23 +31,11 @@ def find_proxy_from_uuid(uuid_string): Args: uuid_string (str): UUID to look for (if it matches, then the proxy is found) Returns: - str or None: If found, the proxy with the matching UUID, otherwise None + Node or None: If found, the proxy with the matching UUID, otherwise None """ proxy = get_object_from_uuid_attr(uuid_string=uuid_string, - attr_name=RiggerConstants.PROXY_ATTR_UUID, + attr_name=RiggerConstants.ATTR_PROXY_UUID, obj_type="transform") - return proxy - - -def find_proxy_node_from_uuid(uuid_string): - """ - Returns the found proxy as a "Node" object (gt.utils.node_utils) - Args: - uuid_string (str): UUID to look for (if it matches, then the proxy is found) - Returns: - Node or None: If found, the proxy (as a Node) with the matching UUID, otherwise None - """ - proxy = find_proxy_from_uuid(uuid_string) if proxy: return Node(proxy) @@ -56,52 +46,51 @@ def find_joint_from_uuid(uuid_string): Args: uuid_string (str): UUID to look for (if it matches, then the joint is found) Returns: - str or None: If found, the joint with the matching UUID, otherwise None + Node or None: If found, the joint with the matching UUID, otherwise None """ joint = get_object_from_uuid_attr(uuid_string=uuid_string, - attr_name=RiggerConstants.JOINT_ATTR_UUID, + attr_name=RiggerConstants.ATTR_JOINT_UUID, obj_type="joint") - return joint - - -def find_joint_node_from_uuid(uuid_string): - """ - Returns the found joint as a "Node" object (gt.utils.node_utils) - Args: - uuid_string (str): UUID to look for (if it matches, then the joint is found) - Returns: - Node or None: If found, the joint (as a Node) with the matching UUID, otherwise None - """ - proxy = find_joint_from_uuid(uuid_string) - if proxy: - return Node(proxy) + if joint: + return Node(joint) -def find_control_from_uuid(uuid_string): +def find_driver_from_uuid(uuid_string): """ - Return a joint if the provided UUID is present in the attribute RiggerConstants.JOINT_ATTR_UUID + Return a transform if the provided UUID matches the value of the attribute RiggerConstants.DRIVER_ATTR_UUID Args: - uuid_string (str): UUID to look for (if it matches, then the joint is found) + uuid_string (str): UUID to look for (if it matches, then the driver is found) Returns: - str or None: If found, the joint with the matching UUID, otherwise None + Node or None: If found, the joint with the matching UUID, otherwise None """ - ctrl = get_object_from_uuid_attr(uuid_string=uuid_string, - attr_name=RiggerConstants.CONTROL_ATTR_UUID, - obj_type="transform") - return ctrl + driver = get_object_from_uuid_attr(uuid_string=uuid_string, + attr_name=RiggerConstants.ATTR_DRIVER_UUID, + obj_type="transform") + if driver: + return Node(driver) -def find_control_node_from_uuid(uuid_string): +def find_drivers_from_joint(source_joint, as_list=False): """ - Returns the found joint as a "Node" object (gt.utils.node_utils) + Finds drivers according to the data described in the joint attributes. + It's expected that the joint has this data available as string attributes. Args: - uuid_string (str): UUID to look for (if it matches, then the joint is found) + source_joint (str, Node): The path to a joint. It's expected that this joint contains the drivers attribute. + as_list (bool, optional): If True, it will return a list of Node objects. + If False, a dictionary where the key is the driver name and the value its path (Node) Returns: - Node or None: If found, the joint (as a Node) with the matching UUID, otherwise None + dict or list: A dictionary where the key is the driver name and the value its path (Node) + If "as_list" is True, then a list of Nodes containing the path to the drivers is returned. """ - joint = find_control_from_uuid(uuid_string) - if joint: - return Node(joint) + driver_uuids = get_driver_uuids_from_joint(source_joint=source_joint, as_list=False) + found_drivers = {} + for driver, uuid in driver_uuids.items(): + _found_driver = find_driver_from_uuid(uuid_string=uuid) + if _found_driver: + found_drivers[driver] = _found_driver + if as_list: + return list(found_drivers.values()) + return found_drivers def find_objects_with_attr(attr_name, obj_type="transform", transform_lookup=True): @@ -112,7 +101,7 @@ def find_objects_with_attr(attr_name, obj_type="transform", transform_lookup=Tru obj_type (str, optional): Type of objects to look for (default is "transform") transform_lookup (bool, optional): When not a transform, it checks the item parent instead of the item itself. Returns: - str, None: If found, the object with a matching UUID, otherwise None + Node or None: If found, the object with a matching UUID, otherwise None """ obj_list = cmds.ls(typ=obj_type, long=True) or [] for obj in obj_list: @@ -124,23 +113,27 @@ def find_objects_with_attr(attr_name, obj_type="transform", transform_lookup=Tru return Node(obj) -def find_proxy_root_group_node(): +def find_proxy_root_group(): """ Looks for the proxy root transform (group) by searching for objects containing the expected lookup attribute. Not to be confused with the root curve. This is the parent TRANSFORM. + Returns: + Node or None: The existing root group (top proxy parent), otherwise None. """ - return find_objects_with_attr(RiggerConstants.REF_ROOT_PROXY_ATTR, obj_type="transform") + return find_objects_with_attr(RiggerConstants.REF_ATTR_ROOT_PROXY, obj_type="transform") -def find_rig_root_group_node(): +def find_rig_root_group(): """ Looks for the rig root transform (group) by searching for objects containing the expected lookup attribute. Not to be confused with the root control curve. This is the parent TRANSFORM. + Returns: + Node or None: The existing rig group (top rig parent), otherwise None. """ - return find_objects_with_attr(RiggerConstants.REF_ROOT_RIG_ATTR, obj_type="transform") + return find_objects_with_attr(RiggerConstants.REF_ATTR_ROOT_RIG, obj_type="transform") -def find_control_root_curve_node(use_transform=False): +def find_control_root_curve(use_transform=False): """ Looks for the control root curve by searching for objects containing the expected lookup attribute. Args: @@ -148,14 +141,16 @@ def find_control_root_curve_node(use_transform=False): This can potentially make the operation less efficient, but will run a more complete search as it will include curves that had their shapes deleted. + Returns: + Node or None: The existing control root curve (a.k.a. main control), otherwise None. """ obj_type = "nurbsCurve" if use_transform: obj_type = "transform" - return find_objects_with_attr(RiggerConstants.REF_ROOT_CONTROL_ATTR, obj_type=obj_type) + return find_objects_with_attr(RiggerConstants.REF_ATTR_ROOT_CONTROL, obj_type=obj_type) -def find_direction_curve_node(use_transform=False): +def find_direction_curve(use_transform=False): """ Looks for the direction curve by searching for objects containing the expected lookup attribute. Args: @@ -163,14 +158,16 @@ def find_direction_curve_node(use_transform=False): This can potentially make the operation less efficient, but will run a more complete search as it will include curves that had their shapes deleted. + Returns: + Node or None: The existing direction curve, otherwise None. """ obj_type = "nurbsCurve" if use_transform: obj_type = "transform" - return find_objects_with_attr(RiggerConstants.REF_DIR_CURVE_ATTR, obj_type=obj_type) + return find_objects_with_attr(RiggerConstants.REF_ATTR_DIR_CURVE, obj_type=obj_type) -def find_proxy_root_curve_node(use_transform=False): +def find_proxy_root_curve(use_transform=False): """ Looks for the proxy root curve by searching for objects containing the expected attribute. Args: @@ -178,25 +175,31 @@ def find_proxy_root_curve_node(use_transform=False): This can potentially make the operation less efficient, but will run a more complete search as it will include curves that had their shapes deleted. + Returns: + Node or None: The existing proxy root curve, otherwise None. """ obj_type = "nurbsCurve" if use_transform: obj_type = "transform" - return find_objects_with_attr(RiggerConstants.REF_ROOT_PROXY_ATTR, obj_type=obj_type) + return find_objects_with_attr(RiggerConstants.REF_ATTR_ROOT_PROXY, obj_type=obj_type) def find_skeleton_group(): """ Looks for the rig skeleton group (transform) by searching for objects containing the expected attribute. + Returns: + Node or None: The existing skeleton group, otherwise None. """ - return find_objects_with_attr(RiggerConstants.REF_SKELETON_ATTR, obj_type="transform") + return find_objects_with_attr(RiggerConstants.REF_ATTR_SKELETON, obj_type="transform") def find_setup_group(): """ Looks for the rig setup group (transform) by searching for objects containing the expected attribute. + Returns: + Node or None: The existing setup group, otherwise None. """ - return find_objects_with_attr(RiggerConstants.REF_SETUP_ATTR, obj_type="transform") + return find_objects_with_attr(RiggerConstants.REF_ATTR_SETUP, obj_type="transform") def find_vis_lines_from_uuid(parent_uuid=None, child_uuid=None): @@ -209,19 +212,19 @@ def find_vis_lines_from_uuid(parent_uuid=None, child_uuid=None): tuple: A tuple of detected lines containing the requested parent or child uuids. Empty tuple otherwise. """ # Try the group first to save time. - lines_grp = find_objects_with_attr(attr_name=RiggerConstants.REF_LINES_ATTR) + lines_grp = find_objects_with_attr(attr_name=RiggerConstants.REF_ATTR_LINES) _lines = set() if lines_grp: _children = cmds.listRelatives(str(lines_grp), children=True, fullPath=True) or [] for child in _children: - if not cmds.objExists(f'{child}.{RiggerConstants.LINE_ATTR_PARENT_UUID}'): + if not cmds.objExists(f'{child}.{RiggerConstants.ATTR_LINE_PARENT_UUID}'): continue if parent_uuid: - existing_uuid = cmds.getAttr(f'{child}.{RiggerConstants.LINE_ATTR_PARENT_UUID}') + existing_uuid = cmds.getAttr(f'{child}.{RiggerConstants.ATTR_LINE_PARENT_UUID}') if existing_uuid == parent_uuid: _lines.add(Node(child)) if child_uuid: - existing_uuid = cmds.getAttr(f'{child}.{RiggerConstants.LINE_ATTR_CHILD_UUID}') + existing_uuid = cmds.getAttr(f'{child}.{RiggerConstants.ATTR_LINE_CHILD_UUID}') if existing_uuid == child_uuid: _lines.add(Node(child)) if _lines: @@ -233,15 +236,15 @@ def find_vis_lines_from_uuid(parent_uuid=None, child_uuid=None): _parent = cmds.listRelatives(obj, parent=True, fullPath=True) or [] if _parent: obj = _parent[0] - if cmds.objExists(f'{obj}.{RiggerConstants.LINE_ATTR_PARENT_UUID}'): + if cmds.objExists(f'{obj}.{RiggerConstants.ATTR_LINE_PARENT_UUID}'): valid_items.add(Node(obj)) for item in valid_items: if parent_uuid: - existing_uuid = cmds.getAttr(f'{item}.{RiggerConstants.LINE_ATTR_PARENT_UUID}') + existing_uuid = cmds.getAttr(f'{item}.{RiggerConstants.ATTR_LINE_PARENT_UUID}') if existing_uuid == parent_uuid: _lines.add(Node(child)) if child_uuid: - existing_uuid = cmds.getAttr(f'{item}.{RiggerConstants.LINE_ATTR_CHILD_UUID}') + existing_uuid = cmds.getAttr(f'{item}.{RiggerConstants.ATTR_LINE_CHILD_UUID}') if existing_uuid == child_uuid: _lines.add(Node(child)) return tuple(_lines) @@ -279,9 +282,9 @@ def create_proxy_visualization_lines(proxy_list, lines_parent=None): # Check for Meta Parent - OVERWRITES parent! metadata = proxy.get_metadata() if metadata: - meta_parent = metadata.get(RiggerConstants.PROXY_META_PARENT, None) - if meta_parent: - parent_proxy = find_proxy_from_uuid(meta_parent) + line_parent = metadata.get(RiggerConstants.META_PROXY_LINE_PARENT, None) + if line_parent: + parent_proxy = find_proxy_from_uuid(line_parent) # Create Line if built_proxy and parent_proxy and cmds.objExists(built_proxy) and cmds.objExists(parent_proxy): @@ -293,14 +296,14 @@ def create_proxy_visualization_lines(proxy_list, lines_parent=None): if line_objects: line_crv = line_objects[0] add_attr(obj_list=line_crv, - attributes=RiggerConstants.LINE_ATTR_CHILD_UUID, + attributes=RiggerConstants.ATTR_LINE_CHILD_UUID, attr_type="string") - set_attr(attribute_path=f'{line_crv}.{RiggerConstants.LINE_ATTR_CHILD_UUID}', + set_attr(attribute_path=f'{line_crv}.{RiggerConstants.ATTR_LINE_CHILD_UUID}', value=proxy.get_uuid()) add_attr(obj_list=line_crv, - attributes=RiggerConstants.LINE_ATTR_PARENT_UUID, + attributes=RiggerConstants.ATTR_LINE_PARENT_UUID, attr_type="string") - set_attr(attribute_path=f'{line_crv}.{RiggerConstants.LINE_ATTR_PARENT_UUID}', + set_attr(attribute_path=f'{line_crv}.{RiggerConstants.ATTR_LINE_PARENT_UUID}', value=proxy.get_parent_uuid()) except Exception as e: logger.debug(f'Failed to create visualization line. Issue: {str(e)}') @@ -338,15 +341,15 @@ def create_root_group(is_proxy=False): is_proxy (bool, optional): If True, it will create the proxy group, instead of the main rig group """ _name = RiggerConstants.GRP_RIG_NAME - _attr = RiggerConstants.REF_ROOT_RIG_ATTR + _attr = RiggerConstants.REF_ATTR_ROOT_RIG _color = ColorConstants.RigOutliner.GRP_ROOT_RIG if is_proxy: _name = RiggerConstants.GRP_PROXY_NAME - _attr = RiggerConstants.REF_ROOT_PROXY_ATTR + _attr = RiggerConstants.REF_ATTR_ROOT_PROXY _color = ColorConstants.RigOutliner.GRP_ROOT_PROXY root_group = cmds.group(name=_name, empty=True, world=True) root_group = Node(root_group) - hide_lock_default_attrs(obj_list=root_group) + hide_lock_default_attrs(obj_list=root_group, translate=True, rotate=True, scale=True) add_attr(obj_list=root_group, attr_type="string", is_keyable=False, attributes=_attr, verbose=True) set_color_outliner(root_group, rgb_color=_color) @@ -360,10 +363,10 @@ def create_proxy_root_curve(): Node, str: A Node containing the generated root curve """ root_transform = create_root_curve(name="root_proxy") - hide_lock_default_attrs(obj_list=root_transform, scale=False) - add_separator_attr(target_object=root_transform, attr_name=f'proxy{RiggerConstants.SEPARATOR_STD_SUFFIX}') + hide_lock_default_attrs(obj_list=root_transform, translate=True, rotate=True) + add_separator_attr(target_object=root_transform, attr_name=f'proxy{RiggerConstants.SEPARATOR_OPTIONS.title()}') add_attr(obj_list=root_transform, attr_type="string", is_keyable=False, - attributes=RiggerConstants.REF_ROOT_PROXY_ATTR, verbose=True) + attributes=RiggerConstants.REF_ATTR_ROOT_PROXY, verbose=True) set_curve_width(obj_list=root_transform, line_width=2) return Node(root_transform) @@ -376,46 +379,30 @@ def create_control_root_curve(): Node, str: A Node containing the generated root curve """ root_transform = create_root_curve(name=f'root_{NamingConstants.Suffix.CTRL}') - add_separator_attr(target_object=root_transform, attr_name=f'rig{RiggerConstants.SEPARATOR_STD_SUFFIX}') + add_separator_attr(target_object=root_transform, attr_name=f'rig{RiggerConstants.SEPARATOR_OPTIONS.title()}') add_attr(obj_list=root_transform, attr_type="string", is_keyable=False, - attributes=RiggerConstants.REF_ROOT_CONTROL_ATTR, verbose=True) + attributes=RiggerConstants.REF_ATTR_ROOT_CONTROL, verbose=True) set_curve_width(obj_list=root_transform, line_width=3) set_color_viewport(obj_list=root_transform, rgb_color=ColorConstants.RigControl.ROOT) return Node(root_transform) -def create_ctrl_curve(name, curve_file_name=None, uuid=None): +def create_ctrl_curve(name, curve_file_name=None): """ - Creates a control with a control UUID attribute. + Creates a curve to be used as control within the auto rigger context. Args: name (str): Control name. curve_file_name (str, optional): Curve file name (from inside "gt/utils/data/curves") e.g. "circle" - uuid (str, optional): A defined UUID for the control. - In case this is not provided, one will be automatically generated. Returns: - str or None: Path to the generated control, otherwise None + Node or None: Node with the generated control, otherwise None """ - if uuid and not is_uuid_valid(uuid): - raise Exception("Failed to create control. Provided UUID is invalid.") if not curve_file_name: - curve_file_name = "_circle_pos_x" + curve_file_name = "_cube" crv_obj = get_curve(file_name=curve_file_name) crv_obj.set_name(name) crv = crv_obj.build() - uuid_attr = add_attr(obj_list=crv, attr_type="string", is_keyable=False, - attributes=RiggerConstants.CONTROL_ATTR_UUID, verbose=True) - if uuid_attr: - uuid_attr = uuid_attr[0] - else: - try: - cmds.delete(crv) - except Exception as e: - logger.debug(f'Unable to delete curve control. Issue: {e}') - return - if not uuid: - uuid = generate_uuid(remove_dashes=True) - set_attr(attribute_path=uuid_attr, value=str(uuid)) - return crv + if crv: + return Node(crv) def create_direction_curve(): @@ -427,9 +414,9 @@ def create_direction_curve(): direction_crv = cmds.circle(name=f'direction_{NamingConstants.Suffix.CTRL}', nr=(0, 1, 0), ch=False, radius=44.5)[0] cmds.rebuildCurve(direction_crv, ch=False, rpo=1, rt=0, end=1, kr=0, kcp=0, kep=1, kt=0, s=20, d=3, tol=0.01) - add_separator_attr(target_object=direction_crv, attr_name=f'rig{RiggerConstants.SEPARATOR_STD_SUFFIX}') + add_separator_attr(target_object=direction_crv, attr_name=f'rig{RiggerConstants.SEPARATOR_OPTIONS.title()}') add_attr(obj_list=direction_crv, attr_type="string", is_keyable=False, - attributes=RiggerConstants.REF_DIR_CURVE_ATTR, verbose=True) + attributes=RiggerConstants.REF_ATTR_DIR_CURVE, verbose=True) set_color_viewport(obj_list=direction_crv, rgb_color=ColorConstants.RigControl.CENTER) return Node(direction_crv) @@ -454,23 +441,23 @@ def create_utility_groups(geometry=False, skeleton=False, control=False, if geometry: _name = RiggerConstants.GRP_GEOMETRY_NAME _color = ColorConstants.RigOutliner.GRP_GEOMETRY - desired_groups[RiggerConstants.REF_GEOMETRY_ATTR] = (_name, _color) + desired_groups[RiggerConstants.REF_ATTR_GEOMETRY] = (_name, _color) if skeleton: _name = RiggerConstants.GRP_SKELETON_NAME _color = ColorConstants.RigOutliner.GRP_SKELETON - desired_groups[RiggerConstants.REF_SKELETON_ATTR] = (_name, _color) + desired_groups[RiggerConstants.REF_ATTR_SKELETON] = (_name, _color) if control: _name = RiggerConstants.GRP_CONTROL_NAME _color = ColorConstants.RigOutliner.GRP_CONTROL - desired_groups[RiggerConstants.REF_CONTROL_ATTR] = (_name, _color) + desired_groups[RiggerConstants.REF_ATTR_CONTROL] = (_name, _color) if setup: _name = RiggerConstants.GRP_SETUP_NAME _color = ColorConstants.RigOutliner.GRP_SETUP - desired_groups[RiggerConstants.REF_SETUP_ATTR] = (_name, _color) + desired_groups[RiggerConstants.REF_ATTR_SETUP] = (_name, _color) if line: _name = RiggerConstants.GRP_LINE_NAME _color = None - desired_groups[RiggerConstants.REF_LINES_ATTR] = (_name, _color) + desired_groups[RiggerConstants.REF_ATTR_LINES] = (_name, _color) group_dict = {} for attr, (name, color) in desired_groups.items(): @@ -521,7 +508,7 @@ def get_proxy_offset(proxy_name): return offset -def get_meta_type_from_dict(proxy_dict): +def get_meta_purpose_from_dict(proxy_dict): """ Gets the meta type of the proxy. A meta type helps identify the purpose of a proxy within a module. For example, a type "knee" proxy describes that it will be influenced by the "hip" and "ankle" in a leg. @@ -532,7 +519,7 @@ def get_meta_type_from_dict(proxy_dict): string or None: The meta type string or None when not detected/found. """ if proxy_dict: - meta_type = proxy_dict.get(RiggerConstants.PROXY_META_TYPE) + meta_type = proxy_dict.get(RiggerConstants.META_PROXY_PURPOSE) return meta_type @@ -628,19 +615,19 @@ def get_driven_joint(uuid_string, suffix=NamingConstants.Suffix.DRIVEN, constrai """ driven_jnt = get_object_from_uuid_attr(uuid_string=uuid_string, - attr_name=RiggerConstants.JOINT_ATTR_DRIVEN_UUID, + attr_name=RiggerConstants.ATTR_JOINT_DRIVEN_UUID, obj_type="joint") if not driven_jnt: - source_jnt = find_joint_node_from_uuid(uuid_string) + source_jnt = find_joint_from_uuid(uuid_string) if not source_jnt: return driven_jnt = duplicate_joint_for_automation(joint=source_jnt, suffix=suffix) delete_user_defined_attrs(obj_list=driven_jnt) - add_attr(obj_list=driven_jnt, attr_type="string", attributes=RiggerConstants.JOINT_ATTR_DRIVEN_UUID) - set_attr(attribute_path=f'{driven_jnt}.{RiggerConstants.JOINT_ATTR_DRIVEN_UUID}', value=uuid_string) + add_attr(obj_list=driven_jnt, attr_type="string", attributes=RiggerConstants.ATTR_JOINT_DRIVEN_UUID) + set_attr(attribute_path=f'{driven_jnt}.{RiggerConstants.ATTR_JOINT_DRIVEN_UUID}', value=uuid_string) if constraint_to_source: constraint = cmds.parentConstraint(source_jnt, driven_jnt) - cmds.setAttr(constraint[0] + '.interpType', 0) # Set to No Flip + cmds.setAttr(f'{constraint[0]}.interpType', 0) # Set to No Flip return driven_jnt @@ -661,8 +648,104 @@ def rescale_joint_radius(joint_list, multiplier): cmds.setAttr(f'{jnt}.radius', scaled_radius) +def get_drivers_list_from_joint(source_joint): + """ + Gets the list of drivers that are stored in a joint drivers attribute. + If missing the attribute, it will return an empty list. + If the string data stored in the attribute is corrupted, it will return an empty list. + """ + drivers = get_attr(obj_name=source_joint, attr_name=RiggerConstants.ATTR_JOINT_DRIVERS) + if drivers: + try: + drivers = eval(drivers) + if not isinstance(drivers, list): + logger.debug('Stored value was not a list.') + drivers = None + except Exception as e: + logger.debug(f'Unable to read joint drivers data. Values will be overwritten. Issue: {e}') + drivers = None + if not drivers: + return [] + return drivers + + +def add_driver_to_joint(target_joint, new_drivers): + """ + Adds a new driver to the driver list of the target joint. + The list is stored inside the drivers attribute of the joint. + If the expected "joint drivers" attribute is not found, the operation is ignored. + Args: + target_joint (str, Node): The path to a joint. It's expected that this joint contains the drivers attribute. + new_drivers (str, list): A new driver to be added to the drivers list. e.g. "fk". (Can be a list of drivers) + This will only be added to the list and will not overwrite the existing items. + The operation is ignored in case the item is already part of the list. + """ + drivers = get_drivers_list_from_joint(source_joint=target_joint) + for new_driver in new_drivers: + if new_driver not in drivers: + drivers.append(new_driver) + data = json.dumps(drivers) + set_attr(obj_list=target_joint, attr_list=RiggerConstants.ATTR_JOINT_DRIVERS, value=data) + + +def get_driver_uuids_from_joint(source_joint, as_list=False): + """ + Gets a dictionary or list of drivers uuids from joint. + It's expected that the joint has this data available as string attributes. + Args: + source_joint (str, Node): The path to a joint. It's expected that this joint contains the drivers attribute. + as_list (bool, optional): If True, it will return a list of uuids. if False, the standard dictionary. + Returns: + dict or list: A dictionary where the key is the driver name and the value its uuid, or a list of uuids. + """ + driver_uuids = {} + if source_joint and cmds.objExists(source_joint): + drivers = get_drivers_list_from_joint(source_joint=source_joint) + module_uuid = get_attr(obj_name=source_joint, attr_name=RiggerConstants.ATTR_MODULE_UUID) + joint_purpose = get_attr(obj_name=source_joint, attr_name=RiggerConstants.ATTR_JOINT_PURPOSE) + for driver in drivers: + _driver_uuid = f'{module_uuid}-{driver}' + if joint_purpose: + _driver_uuid = f'{_driver_uuid}-{joint_purpose}' + driver_uuids[driver] = _driver_uuid + if as_list: + return list(driver_uuids.values()) + return driver_uuids + + +def expose_rotation_order(target): + """ + Creates an attribute to control the rotation order of the target object and connects the attribute + to the hidden "rotationOrder" attribute. + Args: + target (str): Path to the target object (usually a control) + """ + cmds.addAttr(target, ln='rotationOrder', at='enum', keyable=True, + en=RiggerConstants.ENUM_ROTATE_ORDER, niceName='Rotate Order') + cmds.connectAttr(f'{target}.rotationOrder', f'{target}.rotateOrder', f=True) + + +def offset_control_orientation(ctrl, offset_transform, orient_tuple): + """ + Offsets orientation of the control offset transform, while maintaining the original curve shape point position. + Args: + ctrl (str, Node): Path to the control transform (with curve shapes) + offset_transform (str, Node): Path to the control offset transform. + orient_tuple (tuple): A tuple with X, Y and Z values used as offset. + e.g. (90, 0, 0) # offsets orientation 90 in X + """ + for obj in [ctrl, offset_transform]: + if not obj or not cmds.objExists(obj): + logger.debug(f'Unable to offset control orientation, not all objects were found in the scene. ' + f'Missing: {str(obj)}') + return + cv_pos_dict = get_component_positions_as_dict(obj_transform=ctrl, full_path=True, world_space=True) + cmds.rotate(*orient_tuple, offset_transform, relative=True, objectSpace=True) + set_component_positions_from_dict(component_pos_dict=cv_pos_dict) + + if __name__ == "__main__": logger.setLevel(logging.DEBUG) # cmds.file(new=True, force=True) # cmds.viewFit(all=True) - create_direction_curve() + # create_direction_curve() diff --git a/gt/tools/auto_rigger/rigger_attr_widget.py b/gt/tools/auto_rigger/rigger_attr_widget.py index e13adeb3..2bbd9a8f 100644 --- a/gt/tools/auto_rigger/rigger_attr_widget.py +++ b/gt/tools/auto_rigger/rigger_attr_widget.py @@ -23,7 +23,7 @@ logger.setLevel(logging.INFO) -class ModuleAttrWidget(QWidget): +class AttrWidget(QWidget): """ Base Widget for managing attributes of a module. """ @@ -268,7 +268,7 @@ def add_widget_action_buttons(self): # Utils ---------------------------------------------------------------------------------------------------- def refresh_current_widgets(self): """ - Refreshes available widgets. For example, tables, so they should the correct module name. + Refreshes available widgets. For example, tables, so they display the correct module name. """ if self.mod_name_field: _name = self.module.get_name() @@ -393,6 +393,7 @@ def update_proxy_from_raw_data(self, data_getter, proxy): def update_module_from_raw_data(self, data_getter, module): """ Updates a proxy description using raw string data. + Used with "on_button_edit_module_clicked" to update modules from raw data. Args: data_getter (callable): A function used to retrieve the data string module (ModuleGeneric): A module object to be updated using the data @@ -402,6 +403,7 @@ def update_module_from_raw_data(self, data_getter, module): _data_as_dict = ast.literal_eval(data) module.read_data_from_dict(_data_as_dict) self.refresh_current_widgets() + self.call_parent_refresh() except Exception as e: raise Exception(f'Unable to set module attributes from provided raw data. Issue: "{e}".') @@ -748,7 +750,7 @@ def set_refresh_parent_func(self, func): func (callable): The function to be set as the refresh table function. """ if not callable(func): - logger.warning(f'Unable to set refresh tree function. Provided argument is not a callable object.') + logger.warning(f'Unable to parent refresh function. Provided argument is not a callable object.') return self.refresh_parent_func = func @@ -772,7 +774,7 @@ def get_table_item_proxy_object(self, item): return item.data(self.PROXY_ROLE) -class ModuleGenericAttrWidget(ModuleAttrWidget): +class ModuleGenericAttrWidget(AttrWidget): def __init__(self, parent=None, *args, **kwargs): """ Initialize the ModuleGenericAttrWidget. @@ -794,7 +796,7 @@ def __init__(self, parent=None, *args, **kwargs): self.add_widget_action_buttons() -class ModuleSpineAttrWidget(ModuleAttrWidget): +class ModuleSpineAttrWidget(AttrWidget): def __init__(self, parent=None, *args, **kwargs): """ Initialize the ModuleSpineAttrWidget. @@ -808,45 +810,134 @@ def __init__(self, parent=None, *args, **kwargs): self.add_widget_proxy_basic_table() -class ProjectAttrWidget(QWidget): - def __init__(self, parent=None, project=None, *args, **kwargs): +class ProjectAttrWidget(AttrWidget): + def __init__(self, parent=None, project=None, refresh_parent_func=None, *args, **kwargs): super().__init__(parent, *args, **kwargs) + # Basic Variables self.project = project + self.project_name_field = None + self.project_prefix_field = None + self.refresh_parent_func = None + + if refresh_parent_func: + self.set_refresh_parent_func(refresh_parent_func) + + self.add_widget_project_header() - # Project Header (Icon, Type, Name, Buttons) ---------------------------------------------- - header_layout = QHBoxLayout() + # Parameter Widgets ---------------------------------------------------------------------------------------- + def add_widget_project_header(self): + """ + Adds the header for controlling a project. With Icon, Name and modify button. + """ + # Project Header (Icon, Name, Buttons) + _layout = QHBoxLayout() + _layout.setContentsMargins(0, 0, 0, 5) # L-T-R-B + _layout.setAlignment(Qt.AlignTop) # Icon - icon = QIcon(project.icon) + icon = QIcon(self.project.icon) icon_label = QLabel() icon_label.setPixmap(icon.pixmap(32, 32)) - header_layout.addWidget(icon_label) - - # Type (Project) - header_layout.addWidget(QLabel("RigProject")) - header_layout.setAlignment(Qt.AlignTop) + label_tooltip = "Rig Project" + icon_label.setToolTip(label_tooltip) + _layout.addWidget(icon_label) # Name (User Custom) - name = project.get_name() - self.name_text_field = ConfirmableQLineEdit() + name = self.project.get_name() + self.project_name_field = ConfirmableQLineEdit() + self.project_name_field.setFixedHeight(35) if name: - self.name_text_field.setText(name) - self.name_text_field.textChanged.connect(self.set_module_name) - header_layout.addWidget(self.name_text_field) + self.project_name_field.setText(name) + self.project_name_field.editingFinished.connect(self.set_project_name) + _layout.addWidget(self.project_name_field) # Edit Button - self.edit_btn = QPushButton() - self.edit_btn.setIcon(QIcon(resource_library.Icon.rigger_dict)) - header_layout.addWidget(self.edit_btn) + edit_project_btn = QPushButton() + edit_project_btn.setIcon(QIcon(resource_library.Icon.rigger_dict)) + edit_project_btn.setToolTip("Edit Raw Data") + edit_project_btn.clicked.connect(self.on_button_edit_project_clicked) + _layout.addWidget(edit_project_btn) + self.content_layout.addLayout(_layout) - # Create Layout - scroll_content_layout = QVBoxLayout(self) - scroll_content_layout.addLayout(header_layout) + # def add_widget_project_prefix(self): + # """ + # Adds widgets to control the prefix of the project + # """ + # _layout = QHBoxLayout() + # _layout.setContentsMargins(0, 0, 0, 5) # L-T-R-B + # # Prefix + # prefix_label = QLabel("Prefix:") + # prefix_label.setFixedWidth(50) + # self.project_prefix_field = ConfirmableQLineEdit() + # self.project_prefix_field.setFixedHeight(35) + # _layout.addWidget(prefix_label) + # _layout.addWidget(self.project_prefix_field) + # prefix = self.module.get_prefix() + # self.project_prefix_field.textChanged.connect(self.set_module_prefix) + # if prefix: + # self.project_prefix_field.setText(prefix) + # self.content_layout.addLayout(_layout) + + def on_button_edit_project_clicked(self, skip_modules=True, *args): + """ + Shows a text-editor window with the project converted to a dictionary (raw data) + If the user applies the changes, and they are considered valid, the module is updated with it. + Args: + skip_modules (bool, optional): If active, the "modules" key will be ignored. + *args: Variable-length argument list. - Here to avoid issues with the "skip_modules" argument. + """ + project_name = self.project.get_name() + param_win = InputWindowText(parent=self, + message=f'Editing Raw Data for the Project "{project_name}"', + window_title=f'Raw data for "{project_name}"', + image=resource_library.Icon.rigger_dict, + window_icon=resource_library.Icon.library_parameters, + image_scale_pct=10, + is_python_code=True) + param_win.set_confirm_button_text("Apply") + project_raw_data = self.project.get_project_as_dict() + if "modules" in project_raw_data and skip_modules: + project_raw_data.pop("modules") + formatted_dict = dict_as_formatted_str(project_raw_data, one_key_per_line=True) + param_win.set_text_field_text(formatted_dict) + confirm_button_func = partial(self.update_project_from_raw_data, + param_win.get_text_field_text, + self.project) + param_win.confirm_button.clicked.connect(confirm_button_func) + param_win.show() - def set_module_name(self): - new_name = self.name_text_field.text() or "" + # Setters -------------------------------------------------------------------------------------------------- + def set_project_name(self): + new_name = self.project_name_field.text() or "" self.project.set_name(new_name) + self.call_parent_refresh() + + def update_project_from_raw_data(self, data_getter, project): + """ + Updates a project description using raw string data. + Used with "on_button_edit_project_clicked" to update a project from raw data. + Args: + data_getter (callable): A function used to retrieve the data string. e.g. Get a string from a textfield. + project (RigProject): A rig project object to be updated using the provided data. + """ + data = data_getter() + try: + _data_as_dict = ast.literal_eval(data) + project.read_data_from_dict(module_dict=_data_as_dict, clear_modules=False) + self.refresh_current_widgets() + self.call_parent_refresh() + except Exception as e: + raise Exception(f'Unable to set project attributes from provided raw data. Issue: "{e}".') + + def refresh_current_widgets(self): + """ + Refreshes available widgets. For example, text-fields, so they display the correct data. + """ + if self.project_name_field: + _name = self.project.get_name() + if _name: + self.project_name_field.setText(_name) if __name__ == "__main__": diff --git a/gt/tools/auto_rigger/rigger_controller.py b/gt/tools/auto_rigger/rigger_controller.py index 5d953ca9..ea027165 100644 --- a/gt/tools/auto_rigger/rigger_controller.py +++ b/gt/tools/auto_rigger/rigger_controller.py @@ -1,7 +1,7 @@ """ Auto Rigger Controller """ -from gt.tools.auto_rigger.rig_utils import find_proxy_root_group_node, find_rig_root_group_node +from gt.tools.auto_rigger.rig_utils import find_proxy_root_group, find_rig_root_group from PySide2.QtWidgets import QTreeWidgetItem, QAction, QMessageBox from gt.utils.string_utils import camel_case_split, remove_prefix from gt.tools.auto_rigger.rig_constants import RiggerConstants @@ -319,7 +319,9 @@ def on_tree_item_clicked(self, item, *kwargs): return # Project --------------------------------------------------------------- if isinstance(data_obj, rig_framework.RigProject): - self.view.set_module_widget(rigger_attr_widget.ProjectAttrWidget(project=data_obj)) + widget_object = rigger_attr_widget.ProjectAttrWidget(project=data_obj, + refresh_parent_func=self.refresh_widgets) + self.view.set_module_widget(widget_object) return # Unknown --------------------------------------------------------------- self.view.clear_module_widget() @@ -332,7 +334,7 @@ def preprocessing_validation(self): False if operation is ready to proceed. """ # Existing Proxy ------------------------------------------------------------------------ - proxy_grp = find_proxy_root_group_node() + proxy_grp = find_proxy_root_group() if proxy_grp: message_box = QMessageBox(self.view) message_box.setWindowTitle(f'Proxy detected in the scene.') @@ -355,7 +357,7 @@ def preprocessing_validation(self): else: return True # Existing Rig ------------------------------------------------------------------------- - rig_grp = find_rig_root_group_node() + rig_grp = find_rig_root_group() if rig_grp: message_box = QMessageBox(self.view) message_box.setWindowTitle(f'Existing rig detected in the scene.') diff --git a/gt/tools/curve_to_python/curve_to_python_view.py b/gt/tools/curve_to_python/curve_to_python_view.py index 55c7ff6c..1c03bfcc 100644 --- a/gt/tools/curve_to_python/curve_to_python_view.py +++ b/gt/tools/curve_to_python/curve_to_python_view.py @@ -83,10 +83,10 @@ def create_widgets(self): self.output_python_box.setMinimumHeight(150) PythonSyntaxHighlighter(self.output_python_box.get_text_edit().document()) - # + self.output_python_label.setAlignment(QtCore.Qt.AlignCenter) self.output_python_label.setFont(qt_utils.get_font(resource_library.Font.roboto)) - # + self.output_python_box.setSizePolicy(self.output_python_box.sizePolicy().Expanding, self.output_python_box.sizePolicy().Expanding) diff --git a/gt/tools/package_setup/gt_tools_maya_menu.py b/gt/tools/package_setup/gt_tools_maya_menu.py index 9cc72609..30f5cdb6 100644 --- a/gt/tools/package_setup/gt_tools_maya_menu.py +++ b/gt/tools/package_setup/gt_tools_maya_menu.py @@ -221,6 +221,10 @@ def load_menu(*args): command=IMPORT_TOOL + 'initialize_tool("orient_joints")', tooltip='Orients Joint in a more predictable way.', icon=resource_library.Icon.tool_orient_joints) + menu.add_menu_item(label='Ribbon Tool', + command=IMPORT_TOOL + 'initialize_tool("ribbon_tool")', + tooltip='Create ribbon setups, using existing objects or by itself.', + icon=resource_library.Icon.tool_ribbon) menu.add_divider() # General Rigging Tools +++++++++++++++++++++++++++++++++ menu.add_menu_item(label='Rivet Locator', command=IMPORT_UTIL + 'initialize_utility("constraint_utils", "create_rivet")', diff --git a/gt/tools/ribbon_tool/__init__.py b/gt/tools/ribbon_tool/__init__.py new file mode 100644 index 00000000..31855b3a --- /dev/null +++ b/gt/tools/ribbon_tool/__init__.py @@ -0,0 +1,27 @@ +""" + Ribbon Tool + github.com/TrevisanGMW/gt-tools - 2024-02-17 +""" +from gt.tools.ribbon_tool import ribbon_tool_controller +from gt.tools.ribbon_tool import ribbon_tool_view +from gt.ui import qt_utils + +# Tool Version +__version_tuple__ = (1, 0, 0) +__version_suffix__ = '' +__version__ = '.'.join(str(n) for n in __version_tuple__) + __version_suffix__ + + +def launch_tool(): + """ + Launch user interface and create any necessary connections for the tool to function. + Entry point for when using this tool. + Creates Model, View and Controller and uses QtApplicationContext to determine context (inside of Maya or not?) + """ + with qt_utils.QtApplicationContext() as context: + _view = ribbon_tool_view.RibbonToolView(parent=context.get_parent(), version=__version__) + _controller = ribbon_tool_controller.RibbonToolController(view=_view) + + +if __name__ == "__main__": + launch_tool() diff --git a/gt/tools/ribbon_tool/ribbon_tool_controller.py b/gt/tools/ribbon_tool/ribbon_tool_controller.py new file mode 100644 index 00000000..c0663fcc --- /dev/null +++ b/gt/tools/ribbon_tool/ribbon_tool_controller.py @@ -0,0 +1,148 @@ +""" +Ribbon Tool Controller +""" +from gt.utils.iterable_utils import sanitize_maya_list +from gt.utils.naming_utils import get_short_name +from gt.utils.feedback_utils import FeedbackMessage +from gt.utils.surface_utils import Ribbon +import logging + +# Logging Setup +logging.basicConfig() +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class RibbonToolController: + def __init__(self, view, model=None): + """ + Initialize the RibbonToolController object. + + Args: + view: The view object to interact with the user interface. + model: The CurveToPythonModel object used for data manipulation. + """ + self.model = model + self.view = view + self.view.controller = self + + # Surface Data and Mode Variables + self.source_mode = 0 + self.source_data = None + + # Connections + self.view.help_btn.clicked.connect(self.open_help) + self.view.mode_combo_box.currentIndexChanged.connect(self.on_mode_change) + self.view.surface_data_set_btn.clicked.connect(self.set_source_data) + self.view.surface_data_clear_btn.clicked.connect(self.clear_source_data) + self.view.surface_data_content_btn.clicked.connect(self.select_source_data) + self.view.create_ribbon_btn.clicked.connect(self.create_ribbon) + self.view.show() + + def on_mode_change(self): + """ + Called when the mode combobox is updated. + It clears the source data value and stores the new source mode index. + """ + self.source_mode = self.view.get_mode_combobox_index() + self.source_data = None + + def set_source_data(self): + """ Sets the source data by using selection and updating the button view """ + if not self.source_mode: + return + if self.source_mode == 1: # Surface Input + selection = self.__get_selection_surface() + if selection: + self.source_data = selection[0] + short_name = get_short_name(long_name=selection[0]) + self.view.set_source_data_button_values(text=short_name) + if self.source_mode == 2: # List Input + selection = self.__get_selection_transform_list() + if selection: + self.source_data = selection + message = f"{len(selection)} objects" + self.view.set_source_data_button_values(text=message) + + def clear_source_data(self): + """ Clears source data button by changing its colors and text back to "No Data".""" + self.source_data = None + self.view.clear_source_data_button() + + def select_source_data(self): + """ Selects the items stored in the source data. This is the input surface or a transform list. """ + if self.source_data: + import maya.cmds as cmds + cmds.select(self.source_data) + + @staticmethod + def open_help(): + """ Opens package docs """ + from gt.utils.request_utils import open_package_docs_url_in_browser + open_package_docs_url_in_browser() + + @staticmethod + def __get_selection_surface(): + """ + Gets selection while warning the user in case nothing is elected + Returns: + list: Selection or empty list when nothing is selected. + """ + import maya.cmds as cmds + selection = cmds.ls(selection=True) or [] + if len(selection) == 0 or len(selection) > 1 : + cmds.warning(f'Please select one surface and try again.') + return [] + return selection + + @staticmethod + def __get_selection_transform_list(): + """ + Gets selection while warning the user in case nothing is elected + Returns: + list: Selection or empty list when nothing is selected. + """ + import maya.cmds as cmds + selection = cmds.ls(selection=True) or [] + if len(selection) <= 1: + cmds.warning(f'Please select two or more transforms and try again.') + return [] + return selection + + def create_ribbon(self): + """ + Create ribbon using view settings + """ + prefix = self.view.get_prefix() + num_ctrls = self.view.get_num_controls_value() + num_joints = self.view.get_num_joints_value() + dropoff_rate = self.view.get_dropoff_rate_value() + span_multiplier = self.view.get_span_multiplier_value() + equidistant = self.view.is_equidistant_checked() + add_fk = self.view.is_add_fk_checked() + parent_skin_joints = self.view.is_parent_skin_joints_checked() + constraint_source = self.view.is_constraint_source_checked() + # Create Ribbon Factory + ribbon_factory = Ribbon(prefix=prefix, + num_controls=num_ctrls, + num_joints=num_joints, + equidistant=equidistant, + add_fk=add_fk) + ribbon_factory.set_dropoff_rate(rate=dropoff_rate) + ribbon_factory.set_bind_joints_parenting(parenting=parent_skin_joints) + ribbon_factory.set_surface_span_multiplier(span_multiplier=span_multiplier) + if self.source_mode == 1 or self.source_mode == 2: + ribbon_factory.set_surface_data(surface_data=self.source_data, is_driven=constraint_source) + function_name = 'Build Ribbon' + import maya.cmds as cmds + cmds.undoInfo(openChunk=True, chunkName=function_name) + try: + ribbon_factory.build() + except Exception as e: + raise e + finally: + cmds.undoInfo(closeChunk=True, chunkName=function_name) + + +if __name__ == "__main__": + print('Run it from "__init__.py".') diff --git a/gt/tools/ribbon_tool/ribbon_tool_view.py b/gt/tools/ribbon_tool/ribbon_tool_view.py new file mode 100644 index 00000000..a6eef88d --- /dev/null +++ b/gt/tools/ribbon_tool/ribbon_tool_view.py @@ -0,0 +1,453 @@ +""" +Ribbon Tool View/Window/UI +""" +from PySide2.QtWidgets import QPushButton, QLabel, QVBoxLayout, QFrame, QSpinBox, QHBoxLayout, QCheckBox, QLineEdit +from PySide2.QtWidgets import QComboBox, QDoubleSpinBox +import gt.ui.resource_library as resource_library +from gt.ui.qt_utils import MayaWindowMeta +from PySide2 import QtWidgets, QtCore +import gt.ui.qt_utils as qt_utils +from PySide2.QtGui import QIcon + + +class RibbonToolView(metaclass=MayaWindowMeta): + def __init__(self, parent=None, controller=None, version=None): + """ + Initialize the RibbonToolView. + This window represents the main GUI window of the tool. + + Args: + parent (str): Parent for this window + controller (RibbonToolViewController): RibbonToolViewController, not to be used, here so + it's not deleted by the garbage collector. Defaults to None. + version (str, optional): If provided, it will be used to determine the window title. e.g. Title - (v1.2.3) + """ + super().__init__(parent=parent) + + self.controller = controller # Only here so it doesn't get deleted by the garbage collectors + + # Window Title + self.window_title = "GT Ribbon Tool" + _window_title = self.window_title + if version: + _window_title += f' - (v{str(version)})' + self.setWindowTitle(_window_title) + + # Title + self.title_label = QtWidgets.QLabel(self.window_title) + self.title_label.setStyleSheet('background-color: rgb(93, 93, 93); border: 0px solid rgb(93, 93, 93); \ + color: rgb(255, 255, 255); padding: 10px; margin-bottom: 0; text-align: left;') + self.title_label.setFont(qt_utils.get_font(resource_library.Font.roboto)) + self.help_btn = QPushButton('Help') + self.help_btn.setToolTip("Open Help Dialog.") + self.help_btn.setStyleSheet('color: rgb(255, 255, 255); padding: 10px; ' + 'padding-right: 15px; padding-left: 15px; margin: 0;') + self.help_btn.setFont(qt_utils.get_font(resource_library.Font.roboto)) + + # Prefix + self.prefix_label = QLabel("Prefix:") + self.prefix_label.setMinimumWidth(90) + self.prefix_content = QLineEdit() + self.prefix_content.setPlaceholderText("Enter prefix here...") + self.prefix_clear_btn = QPushButton("Clear") + self.prefix_clear_btn.setStyleSheet("padding: 7; border-radius: 5px;") + self.prefix_clear_btn.clicked.connect(self.clear_prefix_content) + + # Num Controls + self.num_controls_label = QLabel("Number of Controls:") + self.num_controls_label.setMinimumWidth(170) + self.num_controls_content = QSpinBox() + self.num_controls_content.setMinimum(1) + self.num_controls_content.setSingleStep(1) + self.num_controls_content.setValue(6) + + # Num Joints + self.num_joints_label = QLabel("Number of Joints:") + self.num_joints_label.setMinimumWidth(170) + self.num_joints_content = QSpinBox() + self.num_joints_content.setMinimum(0) + self.num_joints_content.setSingleStep(1) + self.num_joints_content.setValue(8) + + # Num Joints + self.num_joints_label = QLabel("Number of Joints:") + self.num_joints_label.setMinimumWidth(170) + self.num_joints_content = QSpinBox() + self.num_joints_content.setMinimum(0) + self.num_joints_content.setSingleStep(1) + self.num_joints_content.setValue(8) + + # Drop Off Rate + self.dropoff_label = QLabel("Dropoff Rate:") + self.dropoff_label.setMinimumWidth(170) + self.dropoff_content = QDoubleSpinBox() + self.dropoff_content.setMinimum(0) + self.dropoff_content.setMaximum(10) + self.dropoff_content.setSingleStep(0.1) + self.dropoff_content.setValue(2) + + # Span Multiplier + self.span_multiplier_label = QLabel("Span Multiplier:") + self.span_multiplier_label.setMinimumWidth(170) + self.span_multiplier_content = QSpinBox() + self.span_multiplier_content.setMinimum(0) + self.span_multiplier_content.setSingleStep(1) + self.span_multiplier_content.setValue(0) + + # Checkboxes + self.equidistant_label = QLabel("Equidistant:") + self.equidistant_label.setToolTip("Ensures equidistant calculation between the distance of every follicle.") + self.equidistant_label.setMinimumWidth(100) + self.equidistant_checkbox = QCheckBox() + self.equidistant_checkbox.setChecked(True) + self.add_fk_label = QLabel("Add FK:") + self.add_fk_label.setToolTip("Creates extra forward-kinematics controls to drive ribbon controls.") + self.add_fk_label.setMinimumWidth(100) + self.add_fk_checkbox = QCheckBox() + self.add_fk_checkbox.setChecked(True) + self.constraint_source_label = QLabel("Constraint Source:") + self.constraint_source_label.setToolTip("Constraint source transforms to follow the ribbon. " + "(This skips joint creation)") + self.constraint_source_label.setMinimumWidth(100) + self.constraint_source_checkbox = QCheckBox() + self.constraint_source_checkbox.setChecked(True) + self.parent_jnt_label = QLabel("Parent Skin Joints:") + self.parent_jnt_label.setToolTip("Creates a hierarchy with the generated driven joints.") + self.parent_jnt_label.setMinimumWidth(100) + self.parent_jnt_checkbox = QCheckBox() + self.parent_jnt_checkbox.setChecked(True) + + # Surface Data / Mode + self.mode_label = QLabel("Source Mode:") + self.mode_label.setToolTip("No Source: Creates a simple ribbon.\n" + "Surface: Uses provided surface as input.\n" + "Transform List: Creates ribbon using a provided transform list.") + self.mode_combo_box = QComboBox() + self.mode_combo_box.addItems(["No Source", "Surface", "Transform List"]) + self.mode_combo_box.setStyleSheet("padding: 5;") + + self.surface_data_set_btn = QPushButton('Set') + self.surface_data_set_btn.setToolTip("Uses selection to determine source data.") + self.surface_data_clear_btn = QPushButton('Clear') + self.surface_data_clear_btn.setToolTip("Clears source data.") + self.surface_data_set_btn.setStyleSheet("padding: 5;") + self.surface_data_content_btn = QPushButton("No Data") + self.surface_data_content_btn.setToolTip('Current Surface Data (Click to Select It)') + + # Create Button + self.create_ribbon_btn = QPushButton("Create Ribbon") + self.create_ribbon_btn.setStyleSheet("padding: 10;") + self.create_ribbon_btn.setSizePolicy(self.create_ribbon_btn.sizePolicy().Expanding, + self.create_ribbon_btn.sizePolicy().Expanding) + + # Window Setup ------------------------------------------------------------------------------------ + self.create_layout() + self.mode_combo_box.currentIndexChanged.connect(self.update_ui_from_mode) + + self.setWindowFlags(self.windowFlags() | + QtCore.Qt.WindowMaximizeButtonHint | + QtCore.Qt.WindowMinimizeButtonHint) + self.setWindowIcon(QIcon(resource_library.Icon.tool_ribbon)) + + stylesheet = resource_library.Stylesheet.scroll_bar_base + stylesheet += resource_library.Stylesheet.maya_dialog_base + stylesheet += resource_library.Stylesheet.list_widget_base + stylesheet += resource_library.Stylesheet.spin_box_base + stylesheet += resource_library.Stylesheet.checkbox_base + stylesheet += resource_library.Stylesheet.line_edit_base + stylesheet += resource_library.Stylesheet.combobox_rounded + self.setStyleSheet(stylesheet) + self.create_ribbon_btn.setStyleSheet(resource_library.Stylesheet.btn_push_bright) + qt_utils.center_window(self) + self.update_ui_from_mode(0) # No Source + width = 400 # Initial width + self.resize(width, self.height()) + + def create_layout(self): + """Create the layout for the window.""" + # Top Layout ------------------------------------------------------------------------- + title_layout = QtWidgets.QHBoxLayout() + title_layout.setSpacing(0) + title_layout.addWidget(self.title_label, 5) + title_layout.addWidget(self.help_btn) + + # Body Layout ------------------------------------------------------------------------- + body_layout = QVBoxLayout() + body_layout.setContentsMargins(15, 0, 15, 5) # L-T-R-B + + prefix_layout = QHBoxLayout() + prefix_layout.addWidget(self.prefix_label) + prefix_layout.addWidget(self.prefix_content) + prefix_layout.addWidget(self.prefix_clear_btn) + prefix_layout.setContentsMargins(0, 0, 0, 5) # L-T-R-B + body_layout.addLayout(prefix_layout) + + mode_layout = QHBoxLayout() + mode_layout.addWidget(self.mode_label) + mode_layout.addWidget(self.mode_combo_box) + mode_layout.setContentsMargins(0, 0, 0, 5) # L-T-R-B + body_layout.addLayout(mode_layout) + + num_controls_layout = QHBoxLayout() + num_controls_layout.addWidget(self.num_controls_label) + num_controls_layout.addWidget(self.num_controls_content) + body_layout.addLayout(num_controls_layout) + + num_joints_layout = QHBoxLayout() + num_joints_layout.addWidget(self.num_joints_label) + num_joints_layout.addWidget(self.num_joints_content) + body_layout.addLayout(num_joints_layout) + + drop_off_layout = QHBoxLayout() + drop_off_layout.addWidget(self.dropoff_label) + drop_off_layout.addWidget(self.dropoff_content) + body_layout.addLayout(drop_off_layout) + + span_multiplier_layout = QHBoxLayout() + span_multiplier_layout.addWidget(self.span_multiplier_label) + span_multiplier_layout.addWidget(self.span_multiplier_content) + body_layout.addLayout(span_multiplier_layout) + + checkboxes_one_layout = QHBoxLayout() + checkboxes_one_layout.addWidget(self.equidistant_label) + checkboxes_one_layout.addWidget(self.equidistant_checkbox) + checkboxes_one_layout.addWidget(self.parent_jnt_label) + checkboxes_one_layout.addWidget(self.parent_jnt_checkbox) + body_layout.addLayout(checkboxes_one_layout) + + checkboxes_two_layout = QHBoxLayout() + checkboxes_two_layout.addWidget(self.add_fk_label) + checkboxes_two_layout.addWidget(self.add_fk_checkbox) + checkboxes_two_layout.addWidget(self.constraint_source_label) + checkboxes_two_layout.addWidget(self.constraint_source_checkbox) + body_layout.addLayout(checkboxes_two_layout) + + surface_data_layout = QtWidgets.QVBoxLayout() + sur_data_label_content_layout = QtWidgets.QHBoxLayout() + set_clear_layout = QtWidgets.QHBoxLayout() + content_layout = QtWidgets.QHBoxLayout() + set_clear_layout.addWidget(self.surface_data_set_btn) + set_clear_layout.addWidget(self.surface_data_clear_btn) + content_layout.addWidget(self.surface_data_content_btn) + sur_data_label_content_layout.addLayout(set_clear_layout) + sur_data_label_content_layout.addLayout(content_layout) + set_clear_layout.setSpacing(2) + content_layout.setSpacing(0) + source_data_label = QLabel("Source Surface/Transform List:") + source_data_label.setStyleSheet(f"font-weight: bold; font-size: 8; margin-top: 0; " + f"color: {resource_library.Color.RGB.gray_lighter};") + source_data_label.setAlignment(QtCore.Qt.AlignCenter) + source_data_font = qt_utils.get_font(resource_library.Font.roboto) + source_data_font.setPointSize(6) + source_data_label.setFont(source_data_font) + surface_data_layout.addWidget(source_data_label) + surface_data_layout.addLayout(sur_data_label_content_layout) + + source_layout = QVBoxLayout() + source_layout.setContentsMargins(15, 0, 15, 5) # L-T-R-B + source_layout.addLayout(surface_data_layout) + + bottom_main_button_layout = QVBoxLayout() + bottom_main_button_layout.addWidget(self.create_ribbon_btn) + + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Sunken) + + separator_two = QFrame() + separator_two.setFrameShape(QFrame.HLine) + separator_two.setFrameShadow(QFrame.Sunken) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + top_layout = QtWidgets.QVBoxLayout() + bottom_layout = QtWidgets.QVBoxLayout() + top_layout.addLayout(title_layout) + top_layout.setContentsMargins(15, 15, 15, 10) # L-T-R-B + main_layout.addLayout(top_layout) + main_layout.addLayout(body_layout) + main_layout.addWidget(separator) + main_layout.addLayout(source_layout) + main_layout.addWidget(separator_two) + bottom_layout.addLayout(bottom_main_button_layout) + bottom_layout.setContentsMargins(15, 0, 15, 15) # L-T-R-B + main_layout.addLayout(bottom_layout) + + def update_ui_from_mode(self, index): + """ + Updates UI according to the selected mode. + Args: + index (int) The index of the combobox. + """ + if index == 0: # No Source + self.surface_data_set_btn.setEnabled(False) + self.surface_data_clear_btn.setEnabled(False) + self.surface_data_content_btn.setEnabled(False) + self.constraint_source_label.setEnabled(False) + self.constraint_source_checkbox.setEnabled(False) + self.clear_source_data_button() + elif index == 1: # From Surface + self.surface_data_set_btn.setEnabled(True) + self.surface_data_clear_btn.setEnabled(True) + self.surface_data_content_btn.setEnabled(True) + self.constraint_source_label.setEnabled(False) + self.constraint_source_checkbox.setEnabled(False) + self.clear_source_data_button() + elif index == 2: # From Transform List + self.surface_data_set_btn.setEnabled(True) + self.surface_data_clear_btn.setEnabled(True) + self.surface_data_content_btn.setEnabled(True) + self.constraint_source_label.setEnabled(True) + self.constraint_source_checkbox.setEnabled(True) + self.clear_source_data_button() + + def close_window(self): + """ Closes this window """ + self.close() + + # Setters -------------------------------------------------------------------------------------------------- + def clear_source_data_button(self): + """ Clears the source data content button by changing its colors and text. """ + self.set_source_data_button_values(text="No Data", + color_text=resource_library.Color.RGB.gray_light, + color_text_disabled=resource_library.Color.RGB.gray_mid_light, + color_btn=resource_library.Color.RGB.gray_darker, + color_btn_hover=resource_library.Color.RGB.gray_mid_light, + color_btn_pressed=resource_library.Color.RGB.gray_mid_lighter, + color_btn_disabled=resource_library.Color.RGB.gray_darker) + + def clear_prefix_content(self): + """ + Clears the text in the QLineEdit used for prefix. + """ + self.prefix_content.setText("") + + def set_source_data_button_values(self, text=None, color_text=None, color_text_disabled=None, color_btn=None, + color_btn_hover=None, color_btn_pressed=None, color_btn_disabled=None): + """ + Updates the source data button color, text and button color (background color) + Args: + text (str, optional): New button text. + color_btn (str, optional): HEX or RGB string to be used as background color. + color_text (str, optional): HEX or RGB string to be used as text color. + color_text_disabled (str, optional): HEX or RGB string to be used as disabled text color. + color_btn_hover (str, optional): HEX or RGB string to be used as button hover color. + color_btn_pressed (str, optional): HEX or RGB string to be used as button pressed color. + color_btn_disabled (str, optional): HEX or RGB string to be used as button disabled color. + """ + if text is not None: + self.surface_data_content_btn.setText(text) + # Base + new_stylesheet = "QPushButton {" + if color_text: + new_stylesheet += f"color: {color_text}; " + if color_btn: + new_stylesheet += f"background-color: {color_btn}; " + new_stylesheet += "}" + # Hover + new_stylesheet += "\nQPushButton:hover {" + if color_btn_hover: + new_stylesheet += f"background-color: {color_btn_hover}; " + new_stylesheet += "}" + # Pressed + new_stylesheet += "\nQPushButton:pressed {" + if color_btn_pressed: + new_stylesheet += f"background-color: {color_btn_pressed}; " + new_stylesheet += "}" + # Disabled + new_stylesheet += "\nQPushButton:disabled {" + if color_btn_disabled: + new_stylesheet += f"background-color: {color_btn_disabled}; " + if color_text_disabled: + new_stylesheet += f"color: {color_text_disabled}; " + new_stylesheet += "}" + self.surface_data_content_btn.setStyleSheet(new_stylesheet) + + # Getters -------------------------------------------------------------------------------------------------- + def get_prefix(self): + """ + Gets the current text of the prefix line edit. + Returns: + str: Text found in the prefix text box + """ + return self.prefix_content.text() + + def get_mode_combobox_index(self): + """ + Gets the current index of the mode combobox. + 0 = No Source, 1 = Surface Input, 2 = Transform List Input + Returns: + int: Index of the mode combobox + """ + return self.mode_combo_box.currentIndex() + + def get_num_controls_value(self): + """ + Gets the current value of the number of controls spin box + Returns: + int: Number of controls value. + """ + return self.num_controls_content.value() + + def get_num_joints_value(self): + """ + Gets the current value of the number of joints spin box + Returns: + int: Number of joints value. + """ + return self.num_joints_content.value() + + def get_dropoff_rate_value(self): + """ + Gets the current value of the dropoff rate spin box + Returns: + double: Dropoff rate value. + """ + return self.dropoff_content.value() + + def get_span_multiplier_value(self): + """ + Gets the current value of the span multiplier spin box + Returns: + int: span multiplier value. + """ + return self.span_multiplier_content.value() + + def is_equidistant_checked(self): + """ + Gets the current value of the equidistant checkbox + Returns: + bool: True if checked, False otherwise. + """ + return self.equidistant_checkbox.isChecked() + + def is_add_fk_checked(self): + """ + Gets the current value of the add FK checkbox + Returns: + bool: True if checked, False otherwise. + """ + return self.add_fk_checkbox.isChecked() + + def is_parent_skin_joints_checked(self): + """ + Gets the current value of the parent skin joints checkbox + Returns: + bool: True if checked, False otherwise. + """ + return self.parent_jnt_checkbox.isChecked() + + def is_constraint_source_checked(self): + """ + Gets the current value of the parent skin joints checkbox + Returns: + bool: True if checked, False otherwise. + """ + return self.constraint_source_checkbox.isChecked() + + +if __name__ == "__main__": + with qt_utils.QtApplicationContext(): + window = RibbonToolView(version="1.2.3") # View + window.set_source_data_button_values(text="Some Data", color_btn="#333333", color_text="#FFFFFF") + window.show() diff --git a/gt/ui/resource_library.py b/gt/ui/resource_library.py index c93f4819..dd01c79c 100644 --- a/gt/ui/resource_library.py +++ b/gt/ui/resource_library.py @@ -279,6 +279,7 @@ def __init__(self): tool_morphing_attributes = get_icon_path(r"tool_morphing_attributes.svg") tool_morphing_utils = get_icon_path(r"tool_morphing_utils.svg") tool_orient_joints = get_icon_path(r"tool_orient_joints.svg") + tool_ribbon = get_icon_path(r"tool_ribbon.svg") # Utils util_reload_file = get_icon_path(r"util_reload_file.svg") util_open_dir = get_icon_path(r"util_open_dir.svg") @@ -381,8 +382,10 @@ def __init__(self): ui_arrow_left = get_icon_path(r"ui_arrow_left.svg") ui_arrow_right = get_icon_path(r"ui_arrow_right.svg") ui_exclamation = get_icon_path(r"ui_exclamation.svg") - ui_checkbox_enabled = get_icon_path(r"ui_checkbox_enabled.svg") - ui_checkbox_disabled = get_icon_path(r"ui_checkbox_disabled.svg") + ui_checkbox_checked = get_icon_path(r"ui_checkbox_checked.svg") + ui_checkbox_unchecked = get_icon_path(r"ui_checkbox_unchecked.svg") + ui_checkbox_checked_disabled = get_icon_path(r"ui_checkbox_checked_disabled.svg") + ui_checkbox_unchecked_disabled = get_icon_path(r"ui_checkbox_unchecked_disabled.svg") ui_toggle_enabled = get_icon_path(r"ui_toggle_enabled.svg") ui_toggle_disabled = get_icon_path(r"ui_toggle_disabled.svg") ui_edit = get_icon_path(r"ui_edit.svg") @@ -811,6 +814,7 @@ def __init__(self): "@maya_button_clicked;": Color.RGB.gray_much_darker, "@maya_selection;": Color.RGB.blue_pastel, "@maya_text;": Color.RGB.white_smoke_darker, + "@maya_text_disabled;": Color.RGB.gray_dim, "@background_disabled_color;": Color.RGB.gray_mid_light, "@disabled_text_color;": Color.RGB.gray_mid_much_lighter, "@text_edit_border;": Color.RGB.gray_mid_dark, @@ -895,20 +899,26 @@ def __init__(self): "@border_color;": Color.RGB.gray_much_darker, "@selection_background;": Color.RGB.blue_pastel, "@left_border_bg;": Color.RGB.gray_darker_mid, + # Style + "@border-radius;": "0", # Icons "@image_arrow_down;": f"url({Icon.ui_arrow_down})".replace("\\", "/"), "@image_arrow_down_width;": 12, "@image_arrow_down_height;": 12, } + combobox_rounded = deepcopy(combobox_base) + combobox_rounded["@border-radius;"] = "5" checkbox_base = { # Colors "@text_color;": Color.RGB.gray_dark_silver, - # Icons - "@image_checked;": f"url({Icon.ui_checkbox_enabled})".replace("\\", "/"), + # Checked + "@image_checked;": f"url({Icon.ui_checkbox_checked})".replace("\\", "/"), + "@image_checked_disabled;": f"url({Icon.ui_checkbox_checked_disabled})".replace("\\", "/"), "@image_checked_width;": 32, "@image_checked_height;": 32, - # Icons - "@image_unchecked;": f"url({Icon.ui_checkbox_disabled})".replace("\\", "/"), + # Unchecked + "@image_unchecked;": f"url({Icon.ui_checkbox_unchecked})".replace("\\", "/"), + "@image_unchecked_disabled;": f"url({Icon.ui_checkbox_unchecked_disabled})".replace("\\", "/"), "@image_unchecked_width;": 32, "@image_unchecked_height;": 32, } @@ -966,6 +976,18 @@ def __init__(self): # Formatting "@border_radius;": "5", } + spin_box_base = { + # Colors + "@text_color;": Color.RGB.white, + "@background_color;": Color.RGB.gray_darker, + "@background_color_pressed;": Color.RGB.gray_mid_dark, + "@background_color_buttons;": Color.RGB.gray_mid_light, + "@border_color;": Color.RGB.gray_mid_much_lighter, + "@up_down_hover_color;": Color.RGB.gray_mid_much_lighter, + # Images + "@image_arrow_up;": f"url({Icon.ui_arrow_up})".replace("\\", "/"), + "@image_arrow_down;": f"url({Icon.ui_arrow_down})".replace("\\", "/"), + } # Metro QToolButton Start ---------------------------------------------------------------- btn_tool_metro_base = { # Colors @@ -1008,6 +1030,8 @@ def __init__(self): stylesheet_variables=StylesheetVariables.text_edit_base) combobox_base = get_stylesheet_content(stylesheet_name="combobox_base", stylesheet_variables=StylesheetVariables.combobox_base) + combobox_rounded = get_stylesheet_content(stylesheet_name="combobox_base", + stylesheet_variables=StylesheetVariables.combobox_rounded) checkbox_base = get_stylesheet_content(stylesheet_name="checkbox_base", stylesheet_variables=StylesheetVariables.checkbox_base) tree_widget_base = get_stylesheet_content(stylesheet_name="tree_widget_base", @@ -1022,6 +1046,8 @@ def __init__(self): stylesheet_variables=StylesheetVariables.group_box_base) scroll_area_base = get_stylesheet_content(stylesheet_name="scroll_area_base", stylesheet_variables=StylesheetVariables.scroll_area_base) + spin_box_base = get_stylesheet_content(stylesheet_name="spin_box_base", + stylesheet_variables=StylesheetVariables.spin_box_base) # --------------------------------------------- Buttons --------------------------------------------- btn_push_base = get_stylesheet_content(stylesheet_name="btn_push_base", diff --git a/gt/ui/resources/icons/tool_ribbon.svg b/gt/ui/resources/icons/tool_ribbon.svg new file mode 100644 index 00000000..913809a5 --- /dev/null +++ b/gt/ui/resources/icons/tool_ribbon.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gt/ui/resources/icons/ui_checkbox_enabled.svg b/gt/ui/resources/icons/ui_checkbox_checked.svg similarity index 100% rename from gt/ui/resources/icons/ui_checkbox_enabled.svg rename to gt/ui/resources/icons/ui_checkbox_checked.svg diff --git a/gt/ui/resources/icons/ui_checkbox_checked_disabled.svg b/gt/ui/resources/icons/ui_checkbox_checked_disabled.svg new file mode 100644 index 00000000..015bae70 --- /dev/null +++ b/gt/ui/resources/icons/ui_checkbox_checked_disabled.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/gt/ui/resources/icons/ui_checkbox_disabled.svg b/gt/ui/resources/icons/ui_checkbox_unchecked.svg similarity index 100% rename from gt/ui/resources/icons/ui_checkbox_disabled.svg rename to gt/ui/resources/icons/ui_checkbox_unchecked.svg diff --git a/gt/ui/resources/icons/ui_checkbox_unchecked_disabled.svg b/gt/ui/resources/icons/ui_checkbox_unchecked_disabled.svg new file mode 100644 index 00000000..cabc984d --- /dev/null +++ b/gt/ui/resources/icons/ui_checkbox_unchecked_disabled.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/gt/ui/resources/stylesheets/btn_push_base.qss b/gt/ui/resources/stylesheets/btn_push_base.qss index ae8558f4..92445193 100644 --- a/gt/ui/resources/stylesheets/btn_push_base.qss +++ b/gt/ui/resources/stylesheets/btn_push_base.qss @@ -1,9 +1,9 @@ QPushButton { - background-color: @background_color; - color: @text_color; - border: none; - padding: @button_padding; - } + background-color: @background_color; + color: @text_color; + border: none; + padding: @button_padding; +} QPushButton:hover { background-color: @background_hover_color; diff --git a/gt/ui/resources/stylesheets/checkbox_base.qss b/gt/ui/resources/stylesheets/checkbox_base.qss index 16cec284..c1f6b0c0 100644 --- a/gt/ui/resources/stylesheets/checkbox_base.qss +++ b/gt/ui/resources/stylesheets/checkbox_base.qss @@ -17,4 +17,16 @@ QCheckBox::indicator:unchecked { image: @image_unchecked; width: @image_unchecked_width; height: @image_unchecked_height; +} + +QCheckBox::indicator:checked:disabled { + image: @image_checked_disabled; + width: @image_checked_width; + height: @image_checked_height; +} + +QCheckBox::indicator:unchecked:disabled { + image: @image_unchecked_disabled; + width: @image_unchecked_width; + height: @image_unchecked_height; } \ No newline at end of file diff --git a/gt/ui/resources/stylesheets/combobox_base.qss b/gt/ui/resources/stylesheets/combobox_base.qss index 718ea601..b46b3547 100644 --- a/gt/ui/resources/stylesheets/combobox_base.qss +++ b/gt/ui/resources/stylesheets/combobox_base.qss @@ -1,6 +1,7 @@ QComboBox { color: white; background-color: @background_color; + border-radius: @border-radius; border: 1px solid @border_color; selection-background-color: @selection_background; selection-color: white; diff --git a/gt/ui/resources/stylesheets/maya_dialog_base.qss b/gt/ui/resources/stylesheets/maya_dialog_base.qss index f14d4656..84466fc6 100644 --- a/gt/ui/resources/stylesheets/maya_dialog_base.qss +++ b/gt/ui/resources/stylesheets/maya_dialog_base.qss @@ -31,6 +31,10 @@ QLabel{ color: @maya_text; } +QLabel:disabled{ + color: @maya_text_disabled; +} + QLineEdit{ background-color: @maya_background_dark; selection-background-color: @maya_selection; diff --git a/gt/ui/resources/stylesheets/spin_box_base.qss b/gt/ui/resources/stylesheets/spin_box_base.qss new file mode 100644 index 00000000..5392ecda --- /dev/null +++ b/gt/ui/resources/stylesheets/spin_box_base.qss @@ -0,0 +1,40 @@ +QSpinBox, +QDoubleSpinBox { + background-color: @background_color; + color: @text_color; + border: 1px solid @border_color; + border-radius: 5px; + padding: 5px; +} +QSpinBox::up-button, +QDoubleSpinBox::up-button { + subcontrol-origin: border; + subcontrol-position: top right; + border-style: none; + width: 20px; + background-color: @background_color_buttons; + image: @image_arrow_up; + border-top-right-radius: 5px; +} +QSpinBox::down-button, +QDoubleSpinBox::down-button { + subcontrol-origin: border; + subcontrol-position: bottom right; + border-style: none; + width: 20px; + background-color: @background_color_buttons; + image: @image_arrow_down; + border-bottom-right-radius: 5px; +} +QSpinBox::up-button:hover, +QDoubleSpinBox::up-button:hover, +QSpinBox::down-button:hover, +QDoubleSpinBox::down-button:hover { + background-color: @up_down_hover_color; +} +QSpinBox::up-button:pressed, +QDoubleSpinBox::up-button:pressed, +QSpinBox::down-button:pressed, +QDoubleSpinBox::down-button:pressed { + background-color: @background_color_pressed; +} diff --git a/gt/utils/attr_utils.py b/gt/utils/attr_utils.py index 3ded72cf..2e723977 100644 --- a/gt/utils/attr_utils.py +++ b/gt/utils/attr_utils.py @@ -196,15 +196,15 @@ def set_trs_attr(target_obj, value_tuple, translate=False, rotate=False, scale=F log_when_true(logger, message, do_log=verbose, level=log_level) -def hide_lock_default_attrs(obj_list, translate=True, rotate=True, scale=True, visibility=False): +def hide_lock_default_attrs(obj_list, translate=False, rotate=False, scale=False, visibility=False): """ Locks default TRS+V channels Args: obj_list (str, list): Name of the object(s) to lock TRS+V attributes - translate (bool, optional): If active, translate (position) will be included. (locked, hidden) - rotate (bool, optional): If active, rotate (rotation) will be included. (locked, hidden) - scale (bool, optional): If active, scale will be included. (locked, hidden) - visibility (bool, optional): If active, also locks and hides visibility + translate (bool, optional): If active, translate (position) will be affected. (locked, hidden) + rotate (bool, optional): If active, rotate (rotation) will be affected. (locked, hidden) + scale (bool, optional): If active, scale will be affected. (locked, hidden) + visibility (bool, optional): If active, function also locks and hides visibility """ channels = [] if not obj_list: diff --git a/gt/utils/curve_utils.py b/gt/utils/curve_utils.py index 76a1d86d..949d0b5d 100644 --- a/gt/utils/curve_utils.py +++ b/gt/utils/curve_utils.py @@ -1842,7 +1842,7 @@ def set_curve_width(obj_list, line_width=-1): return affected_shapes -def create_connection_line(object_a, object_b, constraint=True): +def create_connection_line(object_a, object_b, constraint=True, line_width=3): """ Creates a curve attached to two objects, often used to better visualize hierarchies @@ -1850,14 +1850,14 @@ def create_connection_line(object_a, object_b, constraint=True): object_a (str): Name of the object driving the start of the curve object_b (str): Name of the object driving end of the curve (usually a child of object_a) constraint (bool, optional): If True, it will constrain the clusters to "object_a" and "object_b". + line_width (float, optional): Width of the connection line. (Default is 3) Returns: tuple: A list with the generated curve, cluster_a, and cluster_b - """ crv = cmds.curve(p=[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], d=1) - cluster_a = cmds.cluster(crv + '.cv[0]') - cluster_b = cmds.cluster(crv + '.cv[1]') + cluster_a = cmds.cluster(f'{crv}.cv[0]') + cluster_b = cmds.cluster(f'{crv}.cv[1]') if cmds.objExists(object_a): cmds.pointConstraint(object_a, cluster_a[1]) @@ -1865,13 +1865,13 @@ def create_connection_line(object_a, object_b, constraint=True): if cmds.objExists(object_a): cmds.pointConstraint(object_b, cluster_b[1]) - object_a_short = object_a.split('|')[-1] - object_b_short = object_b.split('|')[-1] - crv = cmds.rename(crv, object_a_short + '_to_' + object_b_short) - cluster_a = cmds.rename(cluster_a[1], object_a_short + '_cluster') - cluster_b = cmds.rename(cluster_b[1], object_b_short + '_cluster') - cmds.setAttr(cluster_a + '.v', 0) - cmds.setAttr(cluster_b + '.v', 0) + object_a_short = get_short_name(object_a) + object_b_short = get_short_name(object_b) + crv = cmds.rename(crv, f'{object_a_short}_to_{object_b_short}') + cluster_a = cmds.rename(cluster_a[1], f'{object_a_short}_cluster') + cluster_b = cmds.rename(cluster_b[1], f'{object_b_short}_cluster') + cmds.setAttr(f'{cluster_a}.v', 0) + cmds.setAttr(f'{cluster_b}.v', 0) if constraint and cmds.objExists(object_a): cmds.pointConstraint(object_a, cluster_a) @@ -1879,8 +1879,8 @@ def create_connection_line(object_a, object_b, constraint=True): if constraint and cmds.objExists(object_b): cmds.pointConstraint(object_b, cluster_b) - shapes = cmds.listRelatives(crv, s=True, f=True) or [] - cmds.setAttr(shapes[0] + ".lineWidth", 3) + shapes = cmds.listRelatives(crv, shapes=True, fullPath=True) or [] + cmds.setAttr(f"{shapes[0]}.lineWidth", line_width) return crv, cluster_a, cluster_b @@ -1929,8 +1929,8 @@ def get_positions_from_curve(curve, count, periodic=True, space="uv", normalized crv_fn.getPointAtParam(pos, point, space) output_list.append([point[0], point[1], point[2]]) # X, Y, Z elif normalized is True: - max_v = cmds.getAttr(curve + ".minMaxValue.maxValue") - min_v = cmds.getAttr(curve + ".minMaxValue.minValue") + max_v = cmds.getAttr(f"{curve}.minMaxValue.maxValue") + min_v = cmds.getAttr(f"{curve}.minMaxValue.minValue") output_list = [remap_value(value=pos, old_range=[min_v, max_v], new_range=[0, 1]) for pos in pos_list] else: output_list = pos_list @@ -1945,7 +1945,6 @@ def rescale_curve(curve_transform, scale): curve_transform (str): The name of the curve transform to be rescaled. scale (float, tuple): The scaling factor to be applied uniformly to the control points. It can also be a tuple, e.g. (1, 2, 1) - Example: rescale_curve("myCurve", 2.0) """ diff --git a/gt/utils/data/curves/_circle_pos_y.crv b/gt/utils/data/curves/_circle_pos_y.crv new file mode 100644 index 00000000..01923325 --- /dev/null +++ b/gt/utils/data/curves/_circle_pos_y.crv @@ -0,0 +1,89 @@ +{ + "name": "circle_pos_y", + "transform": null, + "shapes": [ + { + "name": "circleShape", + "points": [ + [ + -1.697, + -0.0, + -1.697 + ], + [ + -2.4, + -0.0, + 0.0 + ], + [ + -1.697, + -0.0, + 1.697 + ], + [ + 0.0, + 0.0, + 2.4 + ], + [ + 1.697, + 0.0, + 1.697 + ], + [ + 2.4, + 0.0, + 0.0 + ], + [ + 1.697, + 0.0, + -1.697 + ], + [ + 0.0, + 0.0, + -2.4 + ], + [ + -1.697, + -0.0, + -1.697 + ], + [ + -2.4, + -0.0, + 0.0 + ], + [ + -1.697, + -0.0, + 1.697 + ] + ], + "degree": 3, + "knot": [ + -2.0, + -1.0, + 0.0, + 1.0, + 2.0, + 3.0, + 4.0, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0 + ], + "periodic": 2, + "is_bezier": false + } + ], + "metadata": { + "projectionAxis": "persp", + "projectionScale": 5, + "projectionFit": null + } +} \ No newline at end of file diff --git a/gt/utils/data/curves/_pin_neg_z.crv b/gt/utils/data/curves/_pin_neg_z.crv new file mode 100644 index 00000000..030e6875 --- /dev/null +++ b/gt/utils/data/curves/_pin_neg_z.crv @@ -0,0 +1,175 @@ +{ + "name": "pin", + "transform": null, + "shapes": [ + { + "name": "pinShape", + "points": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + -0.001, + -2.187 + ], + [ + 0.0, + -0.082, + -2.198 + ], + [ + 0.0, + -0.157, + -2.23 + ], + [ + 0.0, + -0.221, + -2.279 + ], + [ + 0.0, + -0.271, + -2.343 + ], + [ + 0.0, + -0.302, + -2.419 + ], + [ + 0.0, + -0.313, + -2.499 + ], + [ + 0.0, + -0.001, + -2.501 + ], + [ + 0.0, + -0.001, + -2.187 + ], + [ + -0.0, + 0.081, + -2.198 + ], + [ + -0.0, + 0.157, + -2.23 + ], + [ + -0.0, + 0.22, + -2.279 + ], + [ + -0.0, + 0.27, + -2.344 + ], + [ + -0.0, + 0.301, + -2.419 + ], + [ + -0.0, + 0.313, + -2.499 + ], + [ + -0.0, + 0.302, + -2.581 + ], + [ + -0.0, + 0.27, + -2.656 + ], + [ + -0.0, + 0.22, + -2.72 + ], + [ + -0.0, + 0.156, + -2.77 + ], + [ + -0.0, + 0.081, + -2.801 + ], + [ + 0.0, + -0.001, + -2.813 + ], + [ + 0.0, + -0.081, + -2.802 + ], + [ + 0.0, + -0.157, + -2.77 + ], + [ + 0.0, + -0.221, + -2.721 + ], + [ + 0.0, + -0.27, + -2.657 + ], + [ + 0.0, + -0.302, + -2.58 + ], + [ + 0.0, + -0.313, + -2.499 + ], + [ + -0.0, + 0.313, + -2.499 + ], + [ + 0.0, + -0.001, + -2.501 + ], + [ + 0.0, + -0.001, + -2.813 + ] + ], + "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/curves/_wavy_circle_pos_x.crv b/gt/utils/data/curves/_wavy_circle_pos_x.crv new file mode 100644 index 00000000..5e375566 --- /dev/null +++ b/gt/utils/data/curves/_wavy_circle_pos_x.crv @@ -0,0 +1,161 @@ +{ + "name": "wavy_circle_pos_x", + "transform": null, + "shapes": [ + { + "name": "wavy_circle_pos_yShape", + "points": [ + [ + -0.207, + 1.202, + 2.135 + ], + [ + -0.701, + -0.0, + 2.633 + ], + [ + -0.207, + -1.202, + 2.135 + ], + [ + -0.279, + -1.552, + 1.724 + ], + [ + -0.706, + -1.966, + 1.241 + ], + [ + -0.979, + -2.121, + 0.679 + ], + [ + -1.169, + -2.236, + 0.0 + ], + [ + -0.979, + -2.121, + -0.679 + ], + [ + -0.707, + -1.966, + -1.243 + ], + [ + -0.279, + -1.552, + -1.726 + ], + [ + -0.208, + -1.202, + -2.137 + ], + [ + -0.7, + -0.0, + -2.634 + ], + [ + -0.208, + 1.202, + -2.137 + ], + [ + -0.279, + 1.552, + -1.726 + ], + [ + -0.707, + 1.966, + -1.243 + ], + [ + -0.979, + 2.121, + -0.679 + ], + [ + -1.169, + 2.236, + 0.0 + ], + [ + -0.979, + 2.121, + 0.679 + ], + [ + -0.706, + 1.966, + 1.241 + ], + [ + -0.279, + 1.552, + 1.724 + ], + [ + -0.207, + 1.202, + 2.135 + ], + [ + -0.701, + -0.0, + 2.633 + ], + [ + -0.207, + -1.202, + 2.135 + ] + ], + "degree": 3, + "knot": [ + -0.1, + -0.05, + 0.0, + 0.05, + 0.1, + 0.15, + 0.2, + 0.25, + 0.3, + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + 1.05, + 1.1 + ], + "periodic": 2, + "is_bezier": false + } + ], + "metadata": { + "projectionAxis": "persp", + "projectionScale": 5, + "projectionFit": null + } +} \ No newline at end of file diff --git a/gt/utils/data/curves/_wavy_circle_pos_y.crv b/gt/utils/data/curves/_wavy_circle_pos_y.crv new file mode 100644 index 00000000..fb6a7ec0 --- /dev/null +++ b/gt/utils/data/curves/_wavy_circle_pos_y.crv @@ -0,0 +1,161 @@ +{ + "name": "wavy_circle_pos_y", + "transform": null, + "shapes": [ + { + "name": "wavy_circle_pos_yShape", + "points": [ + [ + -2.136, + 0.316, + -1.202 + ], + [ + -2.633, + -0.177, + 0.0 + ], + [ + -2.136, + 0.316, + 1.202 + ], + [ + -1.725, + 0.245, + 1.552 + ], + [ + -1.242, + -0.183, + 1.966 + ], + [ + -0.679, + -0.455, + 2.121 + ], + [ + 0.0, + -0.645, + 2.236 + ], + [ + 0.679, + -0.455, + 2.121 + ], + [ + 1.242, + -0.183, + 1.966 + ], + [ + 1.725, + 0.245, + 1.552 + ], + [ + 2.136, + 0.316, + 1.202 + ], + [ + 2.633, + -0.177, + 0.0 + ], + [ + 2.136, + 0.316, + -1.202 + ], + [ + 1.725, + 0.245, + -1.552 + ], + [ + 1.242, + -0.183, + -1.966 + ], + [ + 0.679, + -0.455, + -2.121 + ], + [ + 0.0, + -0.645, + -2.236 + ], + [ + -0.679, + -0.455, + -2.121 + ], + [ + -1.242, + -0.183, + -1.966 + ], + [ + -1.725, + 0.245, + -1.552 + ], + [ + -2.136, + 0.316, + -1.202 + ], + [ + -2.633, + -0.177, + 0.0 + ], + [ + -2.136, + 0.316, + 1.202 + ] + ], + "degree": 3, + "knot": [ + -0.1, + -0.05, + 0.0, + 0.05, + 0.1, + 0.15, + 0.2, + 0.25, + 0.3, + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + 1.05, + 1.1 + ], + "periodic": 2, + "is_bezier": false + } + ], + "metadata": { + "projectionAxis": "persp", + "projectionScale": 5, + "projectionFit": null + } +} \ No newline at end of file diff --git a/gt/utils/hierarchy_utils.py b/gt/utils/hierarchy_utils.py index 9397f0b2..363897b3 100644 --- a/gt/utils/hierarchy_utils.py +++ b/gt/utils/hierarchy_utils.py @@ -133,7 +133,7 @@ def duplicate_as_node(to_duplicate, name=None, input_connections=False, """ Duplicates the input object with a few extra options, then return a "Node" version of it. Args: - to_duplicate (str): Object to duplicate (must exist in the scene) + to_duplicate (str, Node): Object to duplicate (must exist in the scene) name (str, optional): A name for the duplicated object input_connections (bool, optional): parent_only (bool, optional): If True, only the parent is duplicate. (Will exclude shapes) @@ -162,7 +162,45 @@ def duplicate_as_node(to_duplicate, name=None, input_connections=False, return new_obj +def get_shape_components(shape, mesh_component_type="vertices", full_path=False): + """ + Get all components of a shape. + Args: + shape (str): The shape node. + mesh_component_type (str, optional): The type of component to return when the shape is of the type "mesh". + Can be: "vertices"/"vtx", "edges"/"e", "faces"/"f", or "all". + If the type is unrecognized, it will return an empty list. e.g. [] + full_path (bool, optional): when True, returns the full path to the components instead of their short name. + Returns: + List[str]: List of all components for the given shape. + Example: + out = get_shape_components(shape=transform, mesh_component_type="faces") + print (out) # ['cube_one.f[0]', 'cube_one.f[1]'] + """ + if not shape or not cmds.objExists(shape): + return [] + if cmds.nodeType(shape) == "mesh": + if mesh_component_type == 'vertices' or mesh_component_type == 'vtx': + return cmds.ls(f"{shape}.vtx[*]", flatten=True, long=full_path) + elif mesh_component_type == 'edges' or mesh_component_type == 'e': + return cmds.ls(f"{shape}.e[*]", flatten=True, long=full_path) + elif mesh_component_type == 'faces' or mesh_component_type == 'f': + return cmds.ls(f"{shape}.f[*]", flatten=True, long=full_path) + elif mesh_component_type == 'all': + components = cmds.ls(f"{shape}.vtx[*]", flatten=True, long=full_path) + components += cmds.ls(f"{shape}.e[*]", flatten=True, long=full_path) + components += cmds.ls(f"{shape}.f[*]", flatten=True, long=full_path) + return components + return [] + elif cmds.nodeType(shape) == "nurbsSurface": + return cmds.ls(f"{shape}.cv[*][*]", flatten=True, long=full_path) + elif cmds.nodeType(shape) == "nurbsCurve": + return cmds.ls(f"{shape}.cv[*]", flatten=True, long=full_path) + else: + return [] + + if __name__ == "__main__": logger.setLevel(logging.DEBUG) - out = duplicate_as_node(to_duplicate="pSphere1") + out = get_shape_components("pConeShape1") print(out) diff --git a/gt/utils/iterable_utils.py b/gt/utils/iterable_utils.py index 4b6e6f9a..77858b0c 100644 --- a/gt/utils/iterable_utils.py +++ b/gt/utils/iterable_utils.py @@ -338,6 +338,25 @@ def sanitize_maya_list(input_list, return _output +def filter_list_by_type(input_list, data_type, num_items=None): + """ + Filters a list to include only elements of a specified data type. + + Args: + input_list (list): The input list containing elements of various data types. + data_type (type, tuple): The desired data type to filter the list. E.g., str, int, float, etc. + num_items (int, optional): If provided, filters by the specified number of items in iterable elements. + + Returns: + list: A new list containing only elements of the specified data type. + """ + result_list = [item for item in input_list if isinstance(item, data_type)] + if num_items is not None: + _iterables = (str, list, tuple, dict) + result_list = [item for item in result_list if isinstance(item, _iterables) and len(item) == num_items] + return result_list + + if __name__ == "__main__": logger.setLevel(logging.DEBUG) a_list = ['|joint1', '|joint1|joint2', '|joint1|joint2|joint3', '|joint1|joint2|joint3|joint4', diff --git a/gt/utils/session_utils.py b/gt/utils/session_utils.py index 094bc348..65676dad 100644 --- a/gt/utils/session_utils.py +++ b/gt/utils/session_utils.py @@ -161,7 +161,7 @@ def filter_loaded_modules_path_containing(filter_strings, return_module=True): """ Looks through loaded modules and returns the ones containing the provided string under their path Args: - filter_strings (list): A list of strings used to filter modules. If any are found under the module path, + filter_strings (list, str): A list of strings used to filter modules. If any are found under the module path, then they will be included in the return list. return_module (bool, optional): If active, it will return the module. If inactive, it will return the module name. diff --git a/gt/utils/setup_utils.py b/gt/utils/setup_utils.py index 2bc0955a..03437b3e 100644 --- a/gt/utils/setup_utils.py +++ b/gt/utils/setup_utils.py @@ -345,7 +345,6 @@ def remove_package_loader_from_maya_installs(): e.g. Windows: "Documents/scripts/gt_tools_loader.py" - If it exists, it will get deleted """ to_remove_list = generate_scripts_dir_list(file_name="gt_tools_loader.py", only_existing=True) - for file in to_remove_list: if os.path.exists(file): logger.debug(f'Removing loader script: "{file}"') diff --git a/gt/utils/skin_utils.py b/gt/utils/skin_utils.py index 35bc8dcb..54ac0e24 100644 --- a/gt/utils/skin_utils.py +++ b/gt/utils/skin_utils.py @@ -312,7 +312,7 @@ def get_python_influences_code(obj_list, include_bound_mesh=True, include_existi obj_list = [obj_list] valid_nodes = [] for obj in obj_list: - shapes = cmds.listRelatives(obj, shapes=True, children=False) or [] + shapes = cmds.listRelatives(obj, shapes=True, children=False, fullPath=True) or [] if shapes: if cmds.objectType(shapes[0]) == 'mesh' or cmds.objectType(shapes[0]) == 'nurbsSurface': valid_nodes.append(obj) diff --git a/gt/utils/string_utils.py b/gt/utils/string_utils.py index fb01d889..1ff29989 100644 --- a/gt/utils/string_utils.py +++ b/gt/utils/string_utils.py @@ -230,6 +230,42 @@ def extract_digits_as_int(input_string, only_first_match=True, can_be_negative=F return result +def get_int_as_rank(number): + """ + Converts an integer to its corresponding rank string. + + Args: + number (int): The input integer. + + Returns: + str: The rank string. + + Example: + get_int_as_rank(1) + '1st' + get_int_as_rank(5) + '5th' + get_int_as_rank(11) + '11th' + """ + # Handle special cases for 11, 12, and 13 since they end in "th" + if 10 < number % 100 < 20: + suffix = "th" + else: + # Use modulo 10 to determine the last digit + last_digit = number % 10 + if last_digit == 1: + suffix = "st" + elif last_digit == 2: + suffix = "nd" + elif last_digit == 3: + suffix = "rd" + else: + suffix = "th" + + return str(number) + suffix + + if __name__ == "__main__": from pprint import pprint out = None diff --git a/gt/utils/surface_utils.py b/gt/utils/surface_utils.py index 0f28610c..5aca0339 100644 --- a/gt/utils/surface_utils.py +++ b/gt/utils/surface_utils.py @@ -3,6 +3,7 @@ github.com/TrevisanGMW/gt-tools """ from gt.utils.curve_utils import get_curve, get_positions_from_curve, rescale_curve +from gt.utils.iterable_utils import sanitize_maya_list, filter_list_by_type from gt.utils.attr_utils import hide_lock_default_attrs, set_trs_attr from gt.utils.color_utils import set_color_viewport, ColorConstants from gt.utils.transform_utils import match_transform @@ -10,9 +11,11 @@ from gt.utils.naming_utils import NamingConstants from gt.utils.node_utils import Node from gt.utils import hierarchy_utils +import maya.OpenMaya as OpenMaya import maya.cmds as cmds import logging + # Logging Setup logging.basicConfig() logger = logging.getLogger(__name__) @@ -21,6 +24,26 @@ SURFACE_TYPE = "nurbsSurface" +def is_surface(surface, accept_transform_parent=True): + """ + Check if the provided object is a NURBS surface or transform parent of a surface. + + Args: + surface (str): Object to check. + accept_transform_parent (bool, optional): If True, accepts transform parent as surface + in case it has a surface shapes as its child. + Returns: + bool: True if the object is a NURBS surface or transform parent of a surface, False otherwise. + """ + if not cmds.objExists(surface): + return False + + # Check shape + if cmds.objectType(surface) == 'transform' and accept_transform_parent: + surface = cmds.listRelatives(surface, shapes=True, noIntermediate=True, path=True)[0] + + return cmds.objectType(surface) == SURFACE_TYPE + def is_surface_periodic(surface_shape): """ Determine if a surface is periodic. @@ -37,54 +60,253 @@ def is_surface_periodic(surface_shape): return False +def get_surface_function_set(surface): + """ + Creates MFnNurbsSurface class object from the provided NURBS surface. + + Args: + surface (str): Surface to create function class for. + + Returns: + OpenMaya.MFnNurbsSurface: The MFnNurbsSurface class object. + """ + if not is_surface(surface): + raise ValueError(f'Unable to get MFnNurbsSurface from "{surface}". Provided object is not a surface.') + + if cmds.objectType(surface) == 'transform': + surface = cmds.listRelatives(surface, shapes=True, noIntermediate=True, path=True)[0] + + # Retrieve MFnNurbsSurface + selection = OpenMaya.MSelectionList() + OpenMaya.MGlobal.getSelectionListByName(surface, selection) + surface_path = OpenMaya.MDagPath() + selection.getDagPath(0, surface_path) + surface_fn = OpenMaya.MFnNurbsSurface() + surface_fn.setObject(surface_path) + return surface_fn + + +def create_surface_from_object_list(obj_list, surface_name=None, degree=3): + """ + Creates a surface from a list of objects (according to list order) + The surface is created using curve offsets. + 1. A curve is created using the position of the objects from the list. + 2. Two offset curves are created from the initial curve. + 3. A loft surface is created out of the two offset curves. + Args: + obj_list (list): List of objects used to generate the surface (order matters) + surface_name (str, optional): Name of the generated surface. + degree (int, optional): The degree of the generated lofted surface. Default is cubic (3) + Returns: + str or None: Generated surface (loft) object, otherwise None. + """ + # Check if there are at least two objects in the list + if len(obj_list) < 2: + cmds.warning("At least two objects are required to create a surface.") + return + + # Get positions of the objects + positions = [cmds.xform(obj, query=True, translation=True, worldSpace=True) for obj in obj_list] + + # Create a curve with the given positions as control vertices (CVs) + crv_mid = cmds.curve(d=1, p=positions, n=f'{surface_name}_curveFromList') + crv_mid = Node(crv_mid) + + # Offset the duplicated curve positively + offset_distance = 1 + crv_pos = cmds.offsetCurve(crv_mid, name=f'{crv_mid}PositiveOffset', + distance=offset_distance, constructionHistory=False)[0] + crv_neg = cmds.offsetCurve(crv_mid, name=f'{crv_mid}NegativeOffset', + distance=-offset_distance, constructionHistory=False)[0] + crv_pos = Node(crv_pos) + crv_neg = Node(crv_neg) + + loft_parameters = {} + if surface_name and isinstance(surface_name, str): + loft_parameters["name"] = surface_name + + lofted_surface = cmds.loft(crv_pos, crv_neg, + range=True, + autoReverse=True, + degree=degree, + uniform=True, + constructionHistory=False, + **loft_parameters)[0] + cmds.delete([crv_mid, crv_pos, crv_neg]) + return lofted_surface + + +def multiply_surface_spans(input_surface, u_multiplier=0, v_multiplier=0, u_degree=None, v_degree=None): + """ + Multiplies the number of spans in the U and V directions of a NURBS surface. + This operation deletes the history of the object before running. + + Args: + input_surface (str, Node): The name of the NURBS surface to be modified. (can be the shape or its transform) + u_multiplier (int): Multiplier for the number of spans in the U direction. Default is 0. + v_multiplier (int): Multiplier for the number of spans in the V direction. Default is 0. + u_degree (int): Degree of the surface in the U direction. + v_degree (int): Degree of the surface in the V direction. + + Returns: + str or None: The name of the affected surface, otherwise None. + + Example: + multiply_surface_spans("myNurbsSurface", u_multiplier=0, v_multiplier=3, u_degree=3, v_degree=3) + """ + # Check existence + if not input_surface or not cmds.objExists(input_surface): + logger.debug(f'Unable to multiply surface division. Missing provided surface.') + return + # Check if the provided surface is a transform + if cmds.objectType(input_surface) == 'transform': + # If it's a transform, get the associated shape node + shapes = cmds.listRelatives(input_surface, shapes=True, typ=SURFACE_TYPE) + if shapes: + input_surface = shapes[0] + else: + logger.debug(f'Unable to multiply surface division. ' + f'No "nurbsSurface" found in the provided transform.') + return + + # Get the number of spans in the U and V directions. 0 is ignored. + num_spans_u = cmds.getAttr(f"{input_surface}.spansU")*u_multiplier + num_spans_v = cmds.getAttr(f"{input_surface}.spansV")*v_multiplier + + # Prepare parameters and rebuild + degree_params = {} + if u_degree and isinstance(u_degree, int): + degree_params["degreeU"] = u_degree + if v_degree and isinstance(v_degree, int): + degree_params["degreeV"] = v_degree + surface = cmds.rebuildSurface(input_surface, spansU=num_spans_u, spansV=num_spans_v, **degree_params) + if surface: + return surface[0] + + +def create_follicle(input_surface, uv_position=(0.5, 0.5), name=None): + """ + Creates a follicle and attaches it to a surface. + Args: + input_surface (str): A path to a surface transform or shape. + uv_position (tuple, optional): A UV values to determine where to initially position the follicle. + Default is (0.5, 0.5), which is the center of the surface. + name (str, optional): Follicle name. If not provided it will be named "follicle" + Returns: + tuple: A tuple where the first object is the follicle transform and the second the follicle shape. + """ + # If it's a transform, get the associated shape node + if cmds.objectType(input_surface) == 'transform': + shapes = cmds.listRelatives(input_surface, shapes=True, typ=SURFACE_TYPE) + if shapes: + input_surface = shapes[0] + else: + logger.debug(f'Unable create follicle. ' + f'No "nurbsSurface" found in the provided transform.') + return + if cmds.objectType(input_surface) != SURFACE_TYPE: + logger.debug(f'Unable create follicle. ' + f'The provided input surface is not a {SURFACE_TYPE}.') + return + if not name: + name = "follicle" + _follicle = Node(cmds.createNode("follicle")) + _follicle_transform = Node(cmds.listRelatives(_follicle, p=True, fullPath=True)[0]) + _follicle_transform.rename(name) + + # Connect Follicle to Transforms + cmds.connectAttr(f"{_follicle}.outTranslate", f"{_follicle_transform}.translate") + cmds.connectAttr(f"{_follicle}.outRotate", f"{_follicle_transform}.rotate") + + # Attach Follicle to Surface + cmds.connectAttr(f"{input_surface}.worldMatrix[0]", f"{_follicle}.inputWorldMatrix") + cmds.connectAttr(f"{input_surface}.local", f"{_follicle}.inputSurface") + + cmds.setAttr(f'{_follicle}.parameterU', uv_position[0]) + cmds.setAttr(f'{_follicle}.parameterV', uv_position[1]) + + return _follicle_transform, _follicle + + +def get_closest_uv_point(surface, xyz_pos=(0, 0, 0)): + """ + Returns UV coordinates of the closest point on surface according to provided XYZ position. + + Args: + surface (str): Surface to get the closest point. + xyz_pos (optional, tuple, list): World Position to check against surface. Defaults is origin (0,0,0) + Returns: + tuple: The (u, v) coordinates of the closest point on the surface. + """ + # Get MPoint world position + point = OpenMaya.MPoint(xyz_pos[0], xyz_pos[1], xyz_pos[2], 1.0) + + # Get Surface Fn + surf_fn = get_surface_function_set(surface) + + # Get uCoord and vCoord pointer objects + u_coord = OpenMaya.MScriptUtil() + u_coord.createFromDouble(0.0) + u_coord_ptr = u_coord.asDoublePtr() + v_coord = OpenMaya.MScriptUtil() + v_coord.createFromDouble(0.0) + v_coord_ptr = v_coord.asDoublePtr() + + # Get the closest coordinate to edit point position + # Parameters: toThisPoint, paramU, paramV, ignoreTrimBoundaries, tolerance, space + surf_fn.closestPoint(point, u_coord_ptr, v_coord_ptr, True, 0.0001, OpenMaya.MSpace.kWorld) + return OpenMaya.MScriptUtil(u_coord_ptr).asDouble(), OpenMaya.MScriptUtil(v_coord_ptr).asDouble() + + class Ribbon: def __init__(self, prefix=None, - surface=None, + surface_data=None, equidistant=True, num_controls=5, num_joints=20, - add_fk=False, - bind_joint_orient_offset=(90, 0, 0), - bind_joint_parenting=True + add_fk=True, ): """ Args: prefix (str): The system name to be added as a prefix to the created nodes. If not provided, the name of the surface is used. - surface (str, optional): The name of the surface to be used as a ribbon. (Can be its transform or shape) + surface_data (str, optional): The name of the surface to be used as a ribbon. (Can be its transform or shape) If not provided one will be created automatically. equidistant (int, optional): Determine if the controls should be equally spaced (True) or not (False). num_controls (int, optional): The number of controls to create. num_joints (int, optional): The number of joints to create on the ribbon. add_fk (int): Flag to add FK controls. - bind_joint_orient_offset (tuple): An offset tuple with the X, Y, and Z rotation values. - bind_joint_parenting (bool, optional): Define if bind joints will form a hierarchy (True) or not (False) """ self.prefix = None - self.surface = None self.equidistant = True self.num_controls = 5 self.num_joints = 20 - self.fixed_radius = None self.add_fk = add_fk - self.bind_joint_offset = None - self.bind_joint_parenting = True + self.dropoff_rate = 2 + + # Surface Data + self.sur_data = None + self.sur_data_sanitized = None # Cached reference for bound objects - Internal Use Only + self.sur_data_length = None # When using a list, this is the length of the list. - Internal Use Only + self.sur_spans_multiplier = 0 + self.sur_data_is_driven = False + self.sur_data_maintain_offset = True + + # Bind Joint Data + self.bind_joints_orient_offset = None + self.bind_joints_parenting = True if prefix: self.set_prefix(prefix=prefix) - if surface: - self.set_surface(surface=surface) + if surface_data: + self.set_surface_data(surface_data=surface_data) if isinstance(equidistant, bool): - self.set_equidistant(is_activated=equidistant) + self.set_equidistant(equidistant=equidistant) if num_controls: self.set_num_controls(num_controls=num_controls) if num_joints: self.set_num_joints(num_joints=num_joints) - if bind_joint_orient_offset: - self.set_bind_joint_orient_offset(offset_tuple=bind_joint_orient_offset) - if isinstance(bind_joint_parenting, bool): - self.set_bind_joint_hierarchy(state=bind_joint_parenting) def set_prefix(self, prefix): """ @@ -98,111 +320,221 @@ def set_prefix(self, prefix): return self.prefix = prefix - def set_surface(self, surface): + def set_equidistant(self, equidistant): """ - Set the surface to be used as a ribbon of the object. + Set the equidistant attribute of the object. + Args: - surface (str): The name of the surface to be used as a ribbon. (Can be its transform or shape) - If not provided one will be created automatically. + equidistant (bool): Determine if the controls should be equally spaced (True) or not (False). """ - if not surface or not isinstance(surface, str): - logger.debug(f'Unable to set surface path. Input must be a non-empty string.') + if not isinstance(equidistant, bool): + logger.debug('Unable to set equidistant state. Input must be a bool (True or False)') return - self.surface = surface + self.equidistant = equidistant - def set_bind_joint_orient_offset(self, offset_tuple): + def set_num_controls(self, num_controls): """ - Sets an orientation offset (rotation) for the bind joints. Helpful for when matching orientation. + Set the number of controls attribute of the object. + Args: - offset_tuple (tuple): An offset tuple with the X, Y, and Z rotation values. + num_controls (int): The number of controls to create. """ - if not isinstance(offset_tuple, tuple) or len(offset_tuple) < 3: - logger.debug(f'Unable to set bind joint orient offset. ' - f'Invalid input. Must be a tuple with X, Y and Z values.') + if not isinstance(num_controls, int) or num_controls <= 1: + logger.debug('Unable to set number of controls. Input must be two or more.') return + self.num_controls = num_controls - if not all(isinstance(num, (int, float)) for num in offset_tuple): - logger.debug(f'Unable to set bind joint orient offset. ' - f'Input must contain only numbers.') + def set_num_joints(self, num_joints): + """ + Set the number of joints attribute of the object. + Args: + num_joints (int): The number of joints to be set. + """ + if not isinstance(num_joints, int) or num_joints <= 0: + logger.debug('Unable to set number of joints. Input must be a positive integer.') return + self.num_joints = num_joints - self.bind_joint_offset = offset_tuple - - def set_bind_joint_hierarchy(self, state): + def set_add_fk_state(self, state): """ - Sets Bind joint parenting (hierarchy) + Determines if the system will create FK controls when building or not. Args: - state (bool, optional): Define if bind joints will form a hierarchy (True) or not (False) + state (bool) If True, forward kinematics system will be added to the ribbon, otherwise it will be skipped. """ - if isinstance(state, bool): - self.bind_joint_parenting = state + if not isinstance(state, bool): + logger.debug(f'Unable to set FK creation state. Input must be a boolean.') + return + self.add_fk = state - def clear_surface(self): + def set_dropoff_rate(self, rate): """ - Removes/Clears the currently set surface so a new one is automatically created during the "build" process. + Sets the rate at which the influence of a transform drops as the distance from that transform increases. + The valid range is between 0.1 and 10.0. - In this context, it determines the dropoff influence of the controls + along the ribbon surface. + Args: + rate (int, float): Dropoff rate for the ribbon controls. Range 0.1 to 10.0 """ - self.surface = None + if not isinstance(rate, (int, float)): + logger.debug(f'Unable to set dropoff rate. Invalid data type provided.') + if 0.1 <= rate <= 10.0: + self.dropoff_rate = rate + else: + logger.debug("Invalid dropoff value. The valid range is between 0.1 and 10.0.") - def set_fixed_radius(self, radius): + def set_surface_data(self, surface_data=None, is_driven=None, maintain_driven_offset=None): """ - Sets a fixed radius values + Set the surface origin to be used as a ribbon of the object. Args: - radius (int, float): A radius value to be set when creating bind joints. - If not provided, one is calculated automatically. + surface_data (str, list): Data used to create or connect ribbon surface. + If a string is provided, it should be the transform or shape of a nurbs surface. + If a list of objects or positions is used, a surface will be created using this data. + The function "clear_surface_data" can be used to remove previous provided data. + is_driven (bool, optional): If True, it will use the provided surface_data object list as driven. + This means that the follicles will drive these objects directly. + Commonly used with existing influence joints. + e.g. follicle -> parent constraint > surface_data list item + maintain_driven_offset (bool, optional): When True, it constrains follicles with maintain offset active. + This option is only used when "is_driven" is True. """ - if not radius or not isinstance(radius, (int, float)): - logger.debug(f'Unable to set fixed radius. Input must be an integer or a float.') + if surface_data and not isinstance(surface_data, (str, list, tuple)): + logger.debug(f'Unable to set surface path. Invalid data was provided.') return - self.fixed_radius = radius + self.sur_data = surface_data + if isinstance(is_driven, bool): + self.sur_data_is_driven = is_driven + if isinstance(maintain_driven_offset, bool): + self.sur_data_maintain_offset = maintain_driven_offset - def clear_fixed_radius(self): + def clear_surface_data(self): """ - Removes/Clears the currently set fixed radius value. - This causes the radius to be automatically calculated when building. + Removes/Clears the currently surface data and its attributes. + This will cause the ribbon to create a new one during the "build" process. """ - self.fixed_radius = None + self.sur_data = None + _default_ribbon = Ribbon() # Temporary ribbon used to extract default values + self.sur_data_is_driven = _default_ribbon.sur_data_is_driven + self.sur_data_maintain_offset = _default_ribbon.sur_data_maintain_offset + self.sur_spans_multiplier = _default_ribbon.sur_spans_multiplier - def set_equidistant(self, is_activated): + def set_bind_joint_data(self, orient_offset=None, parenting=None): """ - Set the equidistant attribute of the object. + Sets data related to the bind joints. These are only applied when the ribbon is creating the joints. Args: - is_activated (bool): Determine if the controls should be equally spaced (True) or not (False). + orient_offset (tuple, optional): An offset tuple with the X, Y, and Z rotation values. + Sets an orientation offset (rotation) for the bind joints. + Helpful for when matching orientation. + e.g. (90, 0, 0) will use "Z" as primary rotation. + parenting (bool, optional): Determines if the joints should form a hierarchy by parenting them to one + another in the order of creation. """ - if not isinstance(is_activated, bool): - logger.debug('Unable to set equidistant state. Input must be a bool (True or False)') - return - self.equidistant = is_activated - - def set_num_controls(self, num_controls): + if isinstance(parenting, bool): + self.bind_joints_parenting = parenting + if orient_offset: + if not isinstance(orient_offset, tuple) or len(orient_offset) < 3: + logger.debug(f'Unable to set bind joint orient offset. ' + f'Invalid input. Must be a tuple with X, Y and Z values.') + return + + if not all(isinstance(num, (int, float)) for num in orient_offset): + logger.debug(f'Unable to set bind joint orient offset. ' + f'Input must contain only numbers.') + return + self.bind_joints_orient_offset = orient_offset + + def clear_bind_joint_data(self): """ - Set the number of controls attribute of the object. + Removes/Clears the bind data by reverting them back to the default values. + This will cause the ribbon to create a new one during the "build" process. + """ + _default_ribbon = Ribbon() # Temporary ribbon used to extract default values + self.bind_joints_orient_offset = _default_ribbon.bind_joints_orient_offset + self.bind_joints_parenting = _default_ribbon.bind_joints_parenting + def set_surface_span_multiplier(self, span_multiplier): + """ + Sets the span multiplier value. Args: - num_controls (int): The number of controls to create. + span_multiplier (int): New span multiplier value. Sets the span multiplier value of the generated surface. + That is, the number of divisions in between spans. For example, if a surface is created from + point A to point B and the multiplier is set to zero or one, the surface will not change, and be + composed only of the starting and ending spans. + Now if the multiplier is set to 2, the number of spans will double, essentially adding a span/edge + in between the initial spans. This can be seen as a subdivision value for surfaces. """ - if not isinstance(num_controls, int) or num_controls <= 1: - logger.debug('Unable to set number of controls. Input must be two or more.') - return - self.num_controls = num_controls + if isinstance(span_multiplier, int): + self.sur_spans_multiplier = span_multiplier - def set_num_joints(self, num_joints): + def set_bind_joints_parenting(self, parenting): """ - Set the number of joints attribute of the object. + Determines if the ribbon should create a hierarchy out of the skin joints. Args: - num_joints (int): The number of joints to be set. + parenting (bool): If True, it will create a hierarchy, otherwise it will not. """ - if not isinstance(num_joints, int) or num_joints <= 0: - logger.debug('Unable to set number of joints. Input must be a positive integer.') - return - self.num_joints = num_joints + if isinstance(parenting, bool): + self.bind_joints_parenting = parenting def _get_or_create_surface(self, prefix): - surface = self.surface - if not self.surface or not cmds.objExists(self.surface): - surface = cmds.nurbsPlane(axis=(0, 1, 0), width=1, lengthRatio=24, degree=3, - patchesU=1, patchesV=10, constructionHistory=False)[0] - surface = cmds.rename(surface, f"{prefix}ribbon_surface") + """ + Gets or creates the surface used for the ribbon. + The operation depends on the data stored in the "surface_data" variables. + If empty, it will create a simple 1x24 surface to match the default size of the grid. + If a path (string) is provided, it will use it as the surface, essentially using an existing surface. + If a list of paths (strings) is provided, it will use the position of the objects to create a surface. + If a list positions (3d tuples or lists) is provided, it will use the data to create a surface. + + This function will also update the "surface_data_length" according to the data found. + No data = None. + Path = None. + List of paths = Length of the existing objects. + List of positions = Length of the positions list. + + Args: + prefix (str): Prefix to be added in front of the generated surface. + If an existing surface is found, this value is ignored. + Returns: + str: The surface name (path) + """ + self.sur_data_length = None + if isinstance(self.sur_data, str) and cmds.objExists(self.sur_data): + return self.sur_data + if isinstance(self.sur_data, (list, tuple)): + # Object List + _filter_obj_list = filter_list_by_type(self.sur_data, data_type=(str, Node)) + if _filter_obj_list: + _obj_list = sanitize_maya_list(input_list=self.sur_data, sort_list=False, + filter_unique=False, reverse_list=True) + if not _obj_list or len(_obj_list) < 2: + logger.warning(f'Unable to create surface using object list. ' + f'At least two valid objects are necessary for this operation.') + else: + self.sur_data_length = len(_obj_list) + self.sur_data_sanitized = _obj_list[::-1] # Reversed + _sur = create_surface_from_object_list(obj_list=_obj_list, surface_name=f"{prefix}ribbon_sur") + multiply_surface_spans(input_surface=_sur, u_degree=1, v_degree=3, + v_multiplier=self.sur_spans_multiplier) + return _sur + # Position List + _filter_pos_list = filter_list_by_type(self.sur_data, data_type=(list, tuple), num_items=3) + if _filter_pos_list: + obj_list_locator = [] + for pos in self.sur_data: + locator_name = cmds.spaceLocator(name=f'{prefix}_temp_surface_assembly_locator')[0] + cmds.move(*pos, locator_name) + obj_list_locator.append(locator_name) + obj_list_locator.reverse() + self.sur_data_length = len(obj_list_locator) + _sur = create_surface_from_object_list(obj_list=obj_list_locator, + surface_name=f"{prefix}_ribbon_sur") + multiply_surface_spans(input_surface=_sur, v_multiplier=self.sur_spans_multiplier, v_degree=3) + cmds.delete(obj_list_locator) + return _sur + v_patches = 10 + if self.sur_spans_multiplier: + v_patches *= self.sur_spans_multiplier + surface = cmds.nurbsPlane(axis=(0, 1, 0), width=1, lengthRatio=24, degree=3, + patchesU=1, patchesV=v_patches, constructionHistory=False)[0] + surface = cmds.rename(surface, f"{prefix}ribbon_sur") return surface def build(self): @@ -227,12 +559,16 @@ def build(self): if cmds.objectType(input_surface) == "transform": surface_shape = cmds.listRelatives(input_surface, shapes=True, fullPath=True)[0] surface_shape = Node(surface_shape) - if cmds.objectType(input_surface) == "nurbsSurface": + if cmds.objectType(input_surface) == SURFACE_TYPE: surface_shape = Node(input_surface) input_surface = cmds.listRelatives(surface_shape, parent=True, fullPath=True)[0] input_surface = Node(input_surface) cmds.delete(input_surface, constructionHistory=True) + if not surface_shape: + logger.warning(f'Unable to create ribbon. Failed to get or create surface.') + return + # Determine Direction ---------------------------------------------------------------------------- u_curve = cmds.duplicateCurve(f'{input_surface}.v[.5]', local=True, ch=0) # (.5 = center) v_curve = cmds.duplicateCurve(f'{input_surface}.u[.5]', local=True, ch=0) @@ -246,16 +582,31 @@ def build(self): u_curve_for_positions = cmds.duplicateCurve(f'{input_surface}.v[.5]', local=True, ch=0)[0] # U Positions - is_periodic = is_surface_periodic(surface_shape=surface_shape) + is_periodic = is_surface_periodic(surface_shape=str(surface_shape)) u_position_ctrls = get_positions_from_curve(curve=u_curve_for_positions, count=num_controls, periodic=is_periodic, space="uv") u_position_joints = get_positions_from_curve(curve=u_curve_for_positions, count=num_joints, periodic=is_periodic, space="uv") - length = cmds.arclen(u_curve_for_positions) cmds.delete(u_curve, v_curve, u_curve_for_positions) - # Organization ---------------------------------------------------------------------------------- + # Determine positions when using list as input + if self.sur_data_length: + _num_joints = self.sur_data_length - 1 + _num_joints_multiplied = 0 + if self.sur_spans_multiplier and not self.sur_data_is_driven: + _num_joints_multiplied = _num_joints*self.sur_spans_multiplier # Account for new spans + _num_joints = _num_joints # -1 to remove end span + u_pos_value = 1/_num_joints + + last_value = 0 + u_position_joints = [] + for index in range(_num_joints): + u_position_joints.append(last_value) + last_value = last_value+u_pos_value + u_position_joints.append(1) # End Position: 0=start, 1=end + + # Organization/Groups ---------------------------------------------------------------------------- grp_suffix = NamingConstants.Suffix.GRP parent_group = cmds.group(name=f"{prefix}ribbon_{grp_suffix}", empty=True) parent_group = Node(parent_group) @@ -273,7 +624,7 @@ def build(self): ribbon_crv.set_name(f"{prefix}base_{NamingConstants.Suffix.CTRL}") ribbon_ctrl = ribbon_crv.build() ribbon_ctrl = Node(ribbon_ctrl) - rescale_curve(curve_transform=ribbon_ctrl, scale=length/10) + rescale_curve(curve_transform=str(ribbon_ctrl), scale=length/10) ribbon_offset = cmds.group(name=f"{prefix}ctrl_main_offset", empty=True) ribbon_offset = Node(ribbon_offset) @@ -285,37 +636,31 @@ def build(self): target_parent=setup_grp) cmds.setAttr(f"{setup_grp}.visibility", 0) - # Follicles ----------------------------------------------------------------------------------- + # Follicles and Bind Joints ---------------------------------------------------------------------- follicle_nodes = [] follicle_transforms = [] bind_joints = [] - if self.fixed_radius is None: - bind_joint_radius = (length/60)/(float(num_joints)/40) - else: - bind_joint_radius = self.fixed_radius + bind_joint_radius = (length/60)/(float(num_joints)/40) - for index in range(num_joints): - _follicle = Node(cmds.createNode("follicle")) - _follicle_transform = Node(cmds.listRelatives(_follicle, p=True, fullPath=True)[0]) - _follicle_transform.rename(f"{prefix}follicle_{(index+1):02d}") + for index in range(len(u_position_joints)): + _fol_tuple = create_follicle(input_surface=str(surface_shape), + uv_position=(u_position_joints[index], 0.5), + name=f"{prefix}follicle_{(index+1):02d}") + _follicle_transform = _fol_tuple[0] + _follicle_shape = _fol_tuple[1] follicle_transforms.append(_follicle_transform) - follicle_nodes.append(_follicle) - - # Connect Follicle to Transforms - cmds.connectAttr(f"{_follicle}.outTranslate", f"{_follicle_transform}.translate") - cmds.connectAttr(f"{_follicle}.outRotate", f"{_follicle_transform}.rotate") - - # Attach Follicle to Surface - cmds.connectAttr(f"{surface_shape}.worldMatrix[0]", f"{_follicle}.inputWorldMatrix") - cmds.connectAttr(f"{surface_shape}.local", f"{_follicle}.inputSurface") - - cmds.setAttr(f'{_follicle}.parameterU', u_position_joints[index]) - cmds.setAttr(f'{_follicle}.parameterV', 0.5) + follicle_nodes.append(_follicle_shape) cmds.parent(_follicle_transform, follicles_grp) - # Bind Joint + # Driven List (Follicles drive surface_data source object list) + if self.sur_data_is_driven and isinstance(self.sur_data, list): + cmds.parentConstraint(_follicle_transform, self.sur_data_sanitized[index], + maintainOffset=self.sur_data_maintain_offset) + continue + + # Bind Joint (Creation) if prefix: joint_name = f"{prefix}{(index+1):02d}_{NamingConstants.Suffix.JNT}" else: @@ -324,9 +669,10 @@ def build(self): joint = Node(joint) bind_joints.append(joint) + # Constraint Joint match_transform(source=_follicle_transform, target_list=joint) - if self.bind_joint_offset: - cmds.rotate(*self.bind_joint_offset, joint, relative=True, os=True) + if self.bind_joints_orient_offset: + cmds.rotate(*self.bind_joints_orient_offset, joint, relative=True, os=True) cmds.parentConstraint(_follicle_transform, joint, maintainOffset=True) cmds.setAttr(f"{joint}.radius", bind_joint_radius) @@ -337,20 +683,20 @@ def build(self): set_trs_attr(target_obj=ribbon_offset, value_tuple=bbox_center, translate=True) hierarchy_utils.parent(source_objects=bind_joints, target_parent=bind_grp) - # Ribbon Controls ----------------------------------------------------------------------------------- + # Ribbon Controls --------------------------------------------------------------------------------- ctrl_ref_follicle_nodes = [] ctrl_ref_follicle_transforms = [] for index in range(num_controls): - _follicle = Node(cmds.createNode("follicle")) - _follicle_transform = cmds.listRelatives(_follicle, parent=True)[0] - ctrl_ref_follicle_nodes.append(_follicle) + _follicle_shape = Node(cmds.createNode("follicle")) + _follicle_transform = cmds.listRelatives(_follicle_shape, parent=True)[0] + ctrl_ref_follicle_nodes.append(_follicle_shape) ctrl_ref_follicle_transforms.append(_follicle_transform) - cmds.connectAttr(f"{_follicle}.outTranslate", f"{_follicle_transform}.translate") - cmds.connectAttr(f"{_follicle}.outRotate", f"{_follicle_transform}.rotate") - cmds.connectAttr(f"{surface_shape}.worldMatrix[0]", f"{_follicle}.inputWorldMatrix") - cmds.connectAttr(f"{surface_shape}.local", f"{_follicle}.inputSurface") + cmds.connectAttr(f"{_follicle_shape}.outTranslate", f"{_follicle_transform}.translate") + cmds.connectAttr(f"{_follicle_shape}.outRotate", f"{_follicle_transform}.rotate") + cmds.connectAttr(f"{surface_shape}.worldMatrix[0]", f"{_follicle_shape}.inputWorldMatrix") + cmds.connectAttr(f"{surface_shape}.local", f"{_follicle_shape}.inputSurface") divider_for_ctrls = num_controls if not is_periodic: @@ -372,7 +718,7 @@ def build(self): ctrl_offset_grps = [] ctrl_joints = [] ctrl_jnt_offset_grps = [] - ctrl_jnt_radius = bind_joint_radius * 2 + ctrl_jnt_radius = bind_joint_radius * 1 for index in range(num_controls): crv = get_curve("_cube") @@ -419,7 +765,7 @@ def build(self): # Bind the surface to driver joints nurbs_skin_cluster = cmds.skinCluster(ctrl_joints, input_surface, - dropoffRate=2, + dropoffRate=self.dropoff_rate, maximumInfluences=num_controls-1, nurbsSamples=num_controls*5, bindMethod=0, # Closest Distance @@ -459,78 +805,70 @@ def build(self): for fk_ctrl in fk_ctrls: rescale_curve(curve_transform=fk_ctrl, scale=fk_ctrl_scale) - ik_ctrl_offset_grps = [cmds.group(ctrl, - name=f"{ctrl.get_short_name()}_offset_grp") for ctrl in ribbon_ctrls] - [cmds.xform(ik_ctrl_offset_grp, piv=(0, 0, 0), os=True) for ik_ctrl_offset_grp in ik_ctrl_offset_grps] + ik_offset_grps = [cmds.group(ctrl, name=f"{ctrl.get_short_name()}_offset_grp") for ctrl in ribbon_ctrls] + [cmds.xform(ik_ctrl_offset_grp, piv=(0, 0, 0), os=True) for ik_ctrl_offset_grp in ik_offset_grps] for ik, fk in zip(ribbon_ctrls[:-1], fk_offset_groups): cmds.delete(cmds.parentConstraint(ik, fk)) - for fk, ik in zip(fk_ctrls, ik_ctrl_offset_grps[:-1]): + for fk, ik in zip(fk_ctrls, ik_offset_grps[:-1]): cmds.parentConstraint(fk, ik) # Constrain Last Ctrl - cmds.parentConstraint(fk_ctrls[-1], ik_ctrl_offset_grps[-1], mo=True) + cmds.parentConstraint(fk_ctrls[-1], ik_offset_grps[-1], mo=True) set_color_viewport(obj_list=fk_ctrls, rgb_color=ColorConstants.RigControl.TWEAK) - hide_lock_default_attrs(fk_offset_groups) + hide_lock_default_attrs(fk_offset_groups, translate=True, rotate=True, scale=True) cmds.select(cl=True) # Parenting Binding Joints - if self.bind_joint_parenting: + if self.bind_joints_parenting: for index in range(len(bind_joints) - 1): parent_joint = bind_joints[index] child_joint = bind_joints[index + 1] if cmds.objExists(parent_joint) and cmds.objExists(child_joint): cmds.parent(child_joint, parent_joint) - # Colors ---------------------------------------------------------------------------------------- + # Colors ------------------------------------------------------------------------------------------ set_color_viewport(obj_list=ribbon_ctrl, rgb_color=ColorConstants.RGB.WHITE) set_color_viewport(obj_list=fk_ctrls, rgb_color=ColorConstants.RGB.RED_INDIAN) set_color_viewport(obj_list=ribbon_ctrls, rgb_color=ColorConstants.RGB.BLUE_SKY) set_color_viewport(obj_list=bind_joints, rgb_color=ColorConstants.RGB.YELLOW) + # Clean-up ---------------------------------------------------------------------------------------- + # Delete Empty Bind Group + bind_grp_children = cmds.listRelatives(bind_grp, children=True) + if not bind_grp_children: + cmds.delete(bind_grp) # Clear selection cmds.select(cl=True) if __name__ == "__main__": - from pprint import pprint logger.setLevel(logging.DEBUG) - cmds.file(new=True, force=True) - # out = None - # # an_input_surface = cmds.nurbsPlane(axis=(0, 1, 0), width=1, lengthRatio=24, degree=3, - # # patchesU=1, patchesV=10, constructionHistory=False)[0] - # # cmds.setAttr(f'{an_input_surface}.tx', 5) - # try: - # cmds.parent("abc_ribbon_surface", world=True) - # cmds.delete("abc_ribbon_grp") - # except: - # pass - # try: - # cmds.parent("left_arm_sur", world=True) - # cmds.delete("left_arm_ribbon_grp") - # except: - # pass + # Clear Scene + # cmds.file(new=True, force=True) + # # Create Test Joints + # test_joints = [cmds.joint(p=(0, 0, 0)), + # cmds.joint(p=(-5, 0, 0)), + # cmds.joint(p=(-10, 2, 0)), + # cmds.joint(p=(-15, 6, 3)), + # cmds.joint(p=(-20, 10, 5)), + # cmds.joint(p=(-25, 15, 10)), + # cmds.joint(p=(-30, 15, 15))] + + # from gt.utils.control_utils import create_fk + # test_fk_ctrls = create_fk(target_list=test_joints, constraint_joint=False) + # Create Ribbon ribbon_factory = Ribbon(equidistant=True, num_controls=5, - num_joints=17, + num_joints=8, add_fk=True) - ribbon_factory.set_surface("left_arm_ribbon_sur") - ribbon_factory.set_prefix("left_arm") - # ribbon_factory.set_surface("abc_ribbon_surface") - # ribbon_factory.set_prefix("abc") - ribbon_factory.build() + ribbon_factory.set_surface_data("mocked_sur") + ribbon_factory.set_prefix("mocked") + # ribbon_factory.set_surface_data(surface_data=test_joints, is_driven=True) + # ribbon_factory.set_surface_data([(0, 0, 0), (5, 0, 0), (10, 0, 0)]) + # print(ribbon_factory._get_or_create_surface(prefix="test")) ribbon_factory.build() - # cylinder = cmds.polyCylinder(axis=(0, 0, 1), height=24, subdivisionsX=24, subdivisionsY=48, subdivisionsZ=1) - # jnt_list = [] - # for idx in range(0, 20): - # jnt_list.append(f'abc_{(idx + 1):02d}_jnt') - # for jnt in jnt_list: - # cmds.setAttr(f'{jnt}.displayLocalAxis', 1) - # - # from gt.utils.skin_utils import bind_skin - # bind_skin(joints=jnt_list, objects=cylinder[0]) - # cmds.select(clear=True) - \ No newline at end of file + cmds.viewFit(all=True) diff --git a/gt/utils/system_utils.py b/gt/utils/system_utils.py index 77285fd6..19a5ddd0 100644 --- a/gt/utils/system_utils.py +++ b/gt/utils/system_utils.py @@ -178,11 +178,11 @@ def open_file_dir(path): def get_maya_preferences_dir(system): """ - Get maya preferences folder (folder contains scripts, prefs, etc..) + Get maya preferences folder (folder contains scripts, prefs, etc...) Args: system (str): System string Returns: - str: Path to preferences folder (folder where you find scripts, prefs, etc..) + str: Path to preferences folder (folder where you find scripts, prefs, etc...) """ win_maya_preferences_dir = "" mac_maya_preferences_dir = "" @@ -192,10 +192,12 @@ def get_maya_preferences_dir(system): import maya.cmds as cmds if cmds.about(batch=True): raise + else: + win_maya_preferences_dir = cmds.internalVar(userAppDir=True) except Exception as e: win_maya_preferences_dir = os.path.join(win_maya_preferences_dir, "Documents") 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") + win_maya_preferences_dir = os.path.join(win_maya_preferences_dir, "maya") elif system == OS_MAC: mac_maya_preferences_dir = os.path.join(os.path.expanduser('~'), "Library", "Preferences", "Autodesk", "maya") @@ -877,11 +879,8 @@ def create_object(class_name, raise_errors=True, class_path=None, *args, **kwarg if __name__ == "__main__": - from pprint import pprint - out = None logger.setLevel(logging.DEBUG) # out = os.environ.keys() out = get_maya_preferences_dir(get_system()) - print(logger) # out = initialize_from_package() - pprint(out) + print(out) diff --git a/gt/utils/transform_utils.py b/gt/utils/transform_utils.py index b1c7eb2c..c03af80d 100644 --- a/gt/utils/transform_utils.py +++ b/gt/utils/transform_utils.py @@ -1151,8 +1151,132 @@ def set_equidistant_transforms(start, end, target_list, skip_start_end=True, con cmds.delete(constraints) +def translate_shapes(obj_transform, offset): + """ + Rotates the shape of an object without affecting its transform. + Args: + obj_transform (str): The transform node of the object. + offset (tuple): The rotation offset in degrees (X, Y, Z). + """ + shapes = cmds.listRelatives(obj_transform, shapes=True, fullPath=True) or [] + if not shapes: + logger.debug("No shapes found for the given object.") + return + for shape in shapes: + from gt.utils.hierarchy_utils import get_shape_components + components = get_shape_components(shape) + cmds.move(*offset, components, relative=True, objectSpace=True) + + +def rotate_shapes(obj_transform, offset): + """ + Rotates the shape of an object without affecting its transform. + Args: + obj_transform (str): The transform node of the object. + offset (tuple): The rotation offset in degrees (X, Y, Z). + """ + shapes = cmds.listRelatives(obj_transform, shapes=True, fullPath=True) or [] + if not shapes: + logger.debug("No shapes found for the given object.") + return + for shape in shapes: + from gt.utils.hierarchy_utils import get_shape_components + components = get_shape_components(shape) + cmds.rotate(*offset, components, relative=True, objectSpace=True) + + +def scale_shapes(obj_transform, offset): + """ + Rotates the shape of an object without affecting its transform. + Args: + obj_transform (str): The transform node of the object. + offset (tuple, float, int): The scale offset in degrees (X, Y, Z). + If a float or an integer is provided, it will be used as X, Y and Z. + e.g. 0.5 = (0.5, 0.5, 0.5) + """ + shapes = cmds.listRelatives(obj_transform, shapes=True, fullPath=True) or [] + if not shapes: + logger.debug("No shapes found for the given object.") + return + if offset and isinstance(offset, (int, float)): + offset = (offset, offset, offset) + for shape in shapes: + from gt.utils.hierarchy_utils import get_shape_components + components = get_shape_components(shape) + cmds.scale(*offset, components, relative=True, objectSpace=True) + + +def get_component_positions_as_dict(obj_transform, full_path=True, world_space=True): + """ + Retrieves the positions of components (e.g., vertices) of a given object in the specified space. + + Args: + obj_transform (str): The transform node of the object. + full_path (bool, optional): Flag indicating whether to use full path names for shapes. Defaults to True. + world_space (bool, optional): Flag indicating whether to retrieve positions in world space. Defaults to True. + If set to False, function will use object space. + + Raises: + Exception: If there is an issue getting the position, an exception is logged, and the operation continues. + + Returns: + dict: A dictionary where component names are keys, and their corresponding positions are values. + e.g. "{'|mesh.vtx[0]': [0.5, -1, 1]}" + """ + if not obj_transform or not cmds.objExists(obj_transform): + logger.warning(f'Unable to get component position dictionary. Missing object: {str(obj_transform)}') + return {} + shapes = cmds.listRelatives(obj_transform, shapes=True, fullPath=True) or [] + components = [] + for shape in shapes: + from gt.utils.hierarchy_utils import get_shape_components + components.extend(get_shape_components(shape=shape, mesh_component_type="vtx", full_path=full_path)) + component_pos_dict = {} + for cv in components: + try: + if world_space: + pos = cmds.xform(cv, query=True, worldSpace=True, translation=True) + else: + pos = cmds.xform(cv, query=True, objectSpace=True, translation=True) + component_pos_dict[cv] = pos + except Exception as e: + logger.debug(f'Unable to get CV position. Issue: {e}') + return component_pos_dict + + +def set_component_positions_from_dict(component_pos_dict, world_space=True): + """ + Sets the positions of components (e.g., vertices) based on a provided dictionary. + Provided dictionary should use the component path as keys and a list or tuple with X, Y, and Z floats as value. + + Args: + component_pos_dict (dict): A dictionary where component names are keys, and their new positions are values. + Use "get_component_positions_as_dict" to generate a dictionary from an existing object. + world_space (bool, optional): Flag indicating whether to set positions in world space. Defaults to True. + If set to False, function will use object space. + + Raises: + Exception: If there is an issue setting the position, an exception is logged, and the operation continues. + + Note: + This function utilizes the 'cmds.xform' function to set component positions. + """ + if not isinstance(component_pos_dict, dict): + logger.debug(f'Unable to set component positions. Invalid component position dictionary.') + return + for cv, pos in component_pos_dict.items(): + try: + if world_space: + cmds.xform(cv, worldSpace=True, translation=pos) + else: + cmds.xform(cv, objectSpace=True, translation=pos) + except Exception as e: + logger.debug(f'Unable to set CV position. Issue: {e}') + + if __name__ == "__main__": logger.setLevel(logging.DEBUG) - transform = Transform() - transform.set_position(0, 10, 0) - transform.apply_transform('pSphere1') + # transform = Transform() + # transform.set_position(0, 10, 0) + # transform.apply_transform('pSphere1') + rotate_shapes(cmds.ls(selection=True)[0], offset=(0, 0, -90)) diff --git a/tests/test_auto_rigger/test_rig_framework.py b/tests/test_auto_rigger/test_rig_framework.py index 6b3a539f..37741b0d 100644 --- a/tests/test_auto_rigger/test_rig_framework.py +++ b/tests/test_auto_rigger/test_rig_framework.py @@ -111,8 +111,8 @@ def test_proxy_build(self): self.assertEqual(expected_long_name, str(result)) self.assertEqual(expected_short_name, result.get_short_name()) self.assertTrue(isinstance(result, rig_framework.ProxyData)) - self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{rig_framework.RiggerConstants.PROXY_ATTR_UUID}')) - self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{rig_framework.RiggerConstants.PROXY_ATTR_UUID}')) + self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{rig_framework.RiggerConstants.ATTR_PROXY_UUID}')) + self.assertTrue(maya_test_tools.cmds.objExists(f'{result}.{rig_framework.RiggerConstants.ATTR_PROXY_UUID}')) def test_proxy_custom_curve(self): from gt.utils.curve_utils import Curves @@ -285,3 +285,13 @@ def test_proxy_get_metadata(self): self.proxy.set_metadata_dict(mocked_dict) result = self.proxy.get_metadata() self.assertEqual(mocked_dict, result) + + # Create find driver tests: + # out_find_driver = self.find_driver(driver_type=RiggerDriverTypes.FK, proxy_purpose=self.hip) + # out_find_module_drivers = self.find_module_drivers() + # out_get_meta_purpose = self.hip.get_meta_purpose() + # out_find_proxy_drivers = self.find_proxy_drivers(proxy=self.hip, as_dict=True) + # print(f"out_find_driver:{out_find_driver}") + # print(f"out_find_module_drivers:{out_find_module_drivers}") + # print(f"out_get_meta_purpose:{out_get_meta_purpose}") + # print(f"out_find_proxy_drivers:{out_find_proxy_drivers}") diff --git a/tests/test_utils/test_attr_utils.py b/tests/test_utils/test_attr_utils.py index c9693899..ad2e2761 100644 --- a/tests/test_utils/test_attr_utils.py +++ b/tests/test_utils/test_attr_utils.py @@ -593,7 +593,7 @@ def test_set_trs_attr_scale_object_space(self): def test_hide_lock_default_attributes_with_visibility(self): cube = maya_test_tools.create_poly_cube() - attr_utils.hide_lock_default_attrs(cube, visibility=True) + attr_utils.hide_lock_default_attrs(cube, translate=True, rotate=True, scale=True, visibility=True) attr_to_test = ['tx', 'ty', 'tz', 'rx', 'ty', 'rz', 'sx', 'sy', 'sz', 'v'] for attr in attr_to_test: @@ -606,7 +606,7 @@ def test_hide_lock_default_attributes_with_visibility(self): def test_hide_lock_default_attributes_without_visibility(self): cube = maya_test_tools.create_poly_cube() - attr_utils.hide_lock_default_attrs(cube, visibility=False) + attr_utils.hide_lock_default_attrs(cube, translate=True, rotate=True, scale=True, visibility=False) attr_to_test = ['tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz'] for attr in attr_to_test: @@ -624,7 +624,7 @@ def test_hide_lock_default_attributes_without_visibility(self): def test_hide_lock_default_attributes_no_translate(self): cube = maya_test_tools.create_poly_cube() - attr_utils.hide_lock_default_attrs(cube, translate=False, visibility=False) + attr_utils.hide_lock_default_attrs(cube, translate=False, rotate=True, scale=True, visibility=False) attr_to_test = ['rx', 'ry', 'rz', 'sx', 'sy', 'sz'] attr_to_test_inactive = ['tx', 'ty', 'tz'] @@ -648,7 +648,7 @@ def test_hide_lock_default_attributes_no_translate(self): def test_hide_lock_default_attributes_no_rotate(self): cube = maya_test_tools.create_poly_cube() - attr_utils.hide_lock_default_attrs(cube, rotate=False, visibility=False) + attr_utils.hide_lock_default_attrs(cube, translate=True, rotate=False, scale=True, visibility=False) attr_to_test = ['tx', 'ty', 'tz', 'sx', 'sy', 'sz'] attr_to_test_inactive = ['rx', 'ry', 'rz'] @@ -672,7 +672,7 @@ def test_hide_lock_default_attributes_no_rotate(self): def test_hide_lock_default_attributes_no_scale(self): cube = maya_test_tools.create_poly_cube() - attr_utils.hide_lock_default_attrs(cube, scale=False, visibility=False) + attr_utils.hide_lock_default_attrs(cube, translate=True, rotate=True, scale=False, visibility=False) attr_to_test = ['tx', 'ty', 'tz', 'rx', 'ry', 'rz'] attr_to_test_inactive = ['sx', 'sy', 'sz'] diff --git a/tests/test_utils/test_hierarchy_utils.py b/tests/test_utils/test_hierarchy_utils.py index 24b602d6..e4c3660a 100644 --- a/tests/test_utils/test_hierarchy_utils.py +++ b/tests/test_utils/test_hierarchy_utils.py @@ -285,4 +285,93 @@ def test_duplicate_as_node_keep_attrs(self): self.assertTrue(maya_test_tools.cmds.objExists(expected), "Missing duplicated object.") self.assertEqual(expected, str(duplicate)) self.assertTrue(maya_test_tools.cmds.objExists(f'|pCube2.mockedAttr'), - "Unexpected attr found in duplicated object.") \ No newline at end of file + "Unexpected attr found in duplicated object.") + + def test_get_shape_components_mesh_vtx(self): + cube = Node(self.cube_one) + cube_shape = maya_test_tools.cmds.listRelatives(cube, shapes=True) + components_vtx_a = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="vertices") + components_vtx_b = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="vtx") + + expected = ['cube_one.vtx[0]', 'cube_one.vtx[1]', 'cube_one.vtx[2]', 'cube_one.vtx[3]', + 'cube_one.vtx[4]', 'cube_one.vtx[5]', 'cube_one.vtx[6]', 'cube_one.vtx[7]'] + self.assertEqual(expected, components_vtx_a) + self.assertEqual(expected, components_vtx_b) + + def test_get_shape_components_mesh_edges(self): + cube = Node(self.cube_one) + cube_shape = maya_test_tools.cmds.listRelatives(cube, shapes=True) + components_edges_a = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="edges") + components_edges_b = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="e") + + expected = ['cube_one.e[0]', 'cube_one.e[1]', 'cube_one.e[2]', 'cube_one.e[3]', + 'cube_one.e[4]', 'cube_one.e[5]', 'cube_one.e[6]', 'cube_one.e[7]', + 'cube_one.e[8]', 'cube_one.e[9]', 'cube_one.e[10]', 'cube_one.e[11]'] + self.assertEqual(expected, components_edges_a) + self.assertEqual(expected, components_edges_b) + + def test_get_shape_components_mesh_faces(self): + cube = Node(self.cube_one) + cube_shape = maya_test_tools.cmds.listRelatives(cube, shapes=True) + components_faces_a = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="faces") + components_faces_b = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="f") + + expected = ['cube_one.f[0]', 'cube_one.f[1]', 'cube_one.f[2]', + 'cube_one.f[3]', 'cube_one.f[4]', 'cube_one.f[5]'] + self.assertEqual(expected, components_faces_a) + self.assertEqual(expected, components_faces_b) + + def test_get_shape_components_mesh_all(self): + cube = Node(self.cube_one) + cube_shape = maya_test_tools.cmds.listRelatives(cube, shapes=True) + components = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="all") + + expected = ['cube_one.vtx[0]', 'cube_one.vtx[1]', 'cube_one.vtx[2]', 'cube_one.vtx[3]', + 'cube_one.vtx[4]', 'cube_one.vtx[5]', 'cube_one.vtx[6]', 'cube_one.vtx[7]'] + expected += ['cube_one.e[0]', 'cube_one.e[1]', 'cube_one.e[2]', 'cube_one.e[3]', + 'cube_one.e[4]', 'cube_one.e[5]', 'cube_one.e[6]', 'cube_one.e[7]', + 'cube_one.e[8]', 'cube_one.e[9]', 'cube_one.e[10]', 'cube_one.e[11]'] + expected += ['cube_one.f[0]', 'cube_one.f[1]', 'cube_one.f[2]', + 'cube_one.f[3]', 'cube_one.f[4]', 'cube_one.f[5]'] + self.assertEqual(expected, components) + + def test_get_shape_components_mesh_unrecognized(self): + cube = Node(self.cube_one) + cube_shape = maya_test_tools.cmds.listRelatives(cube, shapes=True) + components = hierarchy_utils.get_shape_components(shape=cube_shape[0], mesh_component_type="nothing") + + expected = [] + self.assertEqual(expected, components) + + def test_get_shape_components_curve(self): + circle = maya_test_tools.cmds.circle(ch=False) + circle_shape = maya_test_tools.cmds.listRelatives(circle[0], shapes=True) + components = hierarchy_utils.get_shape_components(shape=circle_shape[0]) + + expected = ['nurbsCircle1.cv[0]', 'nurbsCircle1.cv[1]', 'nurbsCircle1.cv[2]', 'nurbsCircle1.cv[3]', + 'nurbsCircle1.cv[4]', 'nurbsCircle1.cv[5]', 'nurbsCircle1.cv[6]', 'nurbsCircle1.cv[7]'] + self.assertEqual(expected, components) + + def test_get_shape_components_surface(self): + surface = maya_test_tools.cmds.nurbsPlane(ch=False) + surface_shape = maya_test_tools.cmds.listRelatives(surface[0], shapes=True) + components = hierarchy_utils.get_shape_components(shape=surface_shape[0]) + + expected = ['nurbsPlane1.cv[0][0]', 'nurbsPlane1.cv[0][1]', 'nurbsPlane1.cv[0][2]', + 'nurbsPlane1.cv[0][3]', 'nurbsPlane1.cv[1][0]', 'nurbsPlane1.cv[1][1]', + 'nurbsPlane1.cv[1][2]', 'nurbsPlane1.cv[1][3]', 'nurbsPlane1.cv[2][0]', + 'nurbsPlane1.cv[2][1]', 'nurbsPlane1.cv[2][2]', 'nurbsPlane1.cv[2][3]', + 'nurbsPlane1.cv[3][0]', 'nurbsPlane1.cv[3][1]', 'nurbsPlane1.cv[3][2]', + 'nurbsPlane1.cv[3][3]'] + self.assertEqual(expected, components) + + def test_get_shape_components_mesh_vtx_full_path(self): + cube = Node(self.cube_one) + cube_shape = maya_test_tools.cmds.listRelatives(cube, shapes=True) + components_vtx_a = hierarchy_utils.get_shape_components(shape=cube_shape[0], + mesh_component_type="vertices", + full_path=True) + + expected = ['|cube_one.vtx[0]', '|cube_one.vtx[1]', '|cube_one.vtx[2]', '|cube_one.vtx[3]', + '|cube_one.vtx[4]', '|cube_one.vtx[5]', '|cube_one.vtx[6]', '|cube_one.vtx[7]'] + self.assertEqual(expected, components_vtx_a) diff --git a/tests/test_utils/test_iterable_utils.py b/tests/test_utils/test_iterable_utils.py index 6dabce3b..1ab10ee7 100644 --- a/tests/test_utils/test_iterable_utils.py +++ b/tests/test_utils/test_iterable_utils.py @@ -287,7 +287,7 @@ def test_sanitize_maya_list(self): short_names=False) expected = [f'|{cube}', f'|{sphere}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_filter_unique(self): cube = maya_test_tools.create_poly_cube(name='cube') @@ -305,7 +305,7 @@ def test_sanitize_maya_list_filter_unique(self): convert_to_nodes=False, short_names=False) expected = [f'|{cube}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_filter_string(self): cube = maya_test_tools.create_poly_cube(name='cube') @@ -324,7 +324,7 @@ def test_sanitize_maya_list_filter_string(self): convert_to_nodes=False, short_names=False) expected = [f'|{sphere}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_hierarchy(self): cube = maya_test_tools.create_poly_cube(name='cube') @@ -345,7 +345,7 @@ def test_sanitize_maya_list_hierarchy(self): short_names=False) expected = [f'|{cube}', f'|{cube}|{sphere}', f'|{cube}|{cube}Shape', f'|{cube}|{sphere}|{sphere}Shape'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_filter_type(self): cube = maya_test_tools.create_poly_cube(name='cube') @@ -361,7 +361,7 @@ def test_sanitize_maya_list_filter_type(self): convert_to_nodes=False, short_names=False) expected = [f'|{cube}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_filter_regex(self): cube = maya_test_tools.create_poly_cube(name='cube') @@ -378,7 +378,7 @@ def test_sanitize_maya_list_filter_regex(self): convert_to_nodes=False, short_names=False) expected = [f'|{cube}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_filter_func(self): # Define a custom filter function @@ -400,7 +400,7 @@ def custom_filter(item): short_names=False) expected = [f'|{sphere}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_convert_to_nodes(self): from gt.utils.node_utils import Node @@ -438,7 +438,7 @@ def test_sanitize_maya_list_sort_list(self): short_names=False) expected = [f'|{cube}', f'|{sphere}', f'|{cylinder}'] result_as_str = list(map(str, result)) - self.assertEqual(result_as_str, expected) + self.assertEqual(expected, result_as_str) def test_sanitize_maya_list_sort_list_reverse(self): cube = maya_test_tools.create_poly_cube(name='cube') @@ -456,7 +456,7 @@ def test_sanitize_maya_list_sort_list_reverse(self): convert_to_nodes=False, short_names=False) expected = [f'|{cylinder}', f'|{sphere}', f'|{cube}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) def test_sanitize_maya_list_short_names(self): cube = maya_test_tools.create_poly_cube(name='cube') @@ -476,4 +476,60 @@ def test_sanitize_maya_list_short_names(self): short_names=True) expected = [f'{cube}', f'{sphere}'] - self.assertEqual(result, expected) + self.assertEqual(expected, result) + + def test_filter_list_by_type_strings(self): + input_list = ["world", 2, 3.5, None, "hello", 42] + desired_data_type = str + result = iterable_utils.filter_list_by_type(input_list, desired_data_type) + expected = ["world", "hello"] + self.assertEqual(expected, result) + + def test_filter_list_by_type_integers(self): + input_list = ["world", 2, 3.5, None, "hello", 42] + desired_data_type = int + result = iterable_utils.filter_list_by_type(input_list, desired_data_type) + expected = [2, 42] + self.assertEqual(expected, result) + + def test_filter_list_by_type_floats(self): + input_list = ["world", 2, 3.5, None, "hello", 42] + desired_data_type = float + result = iterable_utils.filter_list_by_type(input_list, desired_data_type) + expected = [3.5] + self.assertEqual(expected, result) + + def test_filter_list_by_type_none(self): + input_list = ["world", 2, 3.5, None, "hello", 42] + desired_data_type = type(None) + result = iterable_utils.filter_list_by_type(input_list, desired_data_type) + expected = [None] + self.assertEqual(expected, result) + + def test_filter_list_by_type_none_with_num_items(self): + input_list = ["world", 2, 3.5, None, "hello", 42] + desired_data_type = type(None) + result = iterable_utils.filter_list_by_type(input_list, desired_data_type, num_items=4) + expected = [] + self.assertEqual(expected, result) + + def test_filter_list_by_type_lists_with_num_items(self): + input_list = ["world", [1, 2, 3], "hello", [4, 5]] + desired_data_type = list + result = iterable_utils.filter_list_by_type(input_list, desired_data_type, num_items=3) + expected = [[1, 2, 3]] + self.assertEqual(expected, result) + + def test_filter_list_by_type_tuples_with_num_items(self): + input_list = ["world", (1, 2, 3), "hello", (4, 5)] + desired_data_type = tuple + result = iterable_utils.filter_list_by_type(input_list, desired_data_type, num_items=3) + expected = [(1, 2, 3)] + self.assertEqual(expected, result) + + def test_filter_list_by_type_dicts_with_num_items(self): + input_list = ["world", {"a": 1, "b": 2}, "hello", {"x": 10, "y": 20, "z": 30}] + desired_data_type = dict + result = iterable_utils.filter_list_by_type(input_list, desired_data_type, num_items=2) + expected = [{"a": 1, "b": 2}] + self.assertEqual(expected, result) diff --git a/tests/test_utils/test_string_utils.py b/tests/test_utils/test_string_utils.py index 7112c01e..a8d5c074 100644 --- a/tests/test_utils/test_string_utils.py +++ b/tests/test_utils/test_string_utils.py @@ -262,3 +262,49 @@ def test_extract_digits_as_int_negative_number_all_digits(self): only_first_match=False, default=0) self.assertEqual(expected, result) + + def test_get_int_as_rank_first(self): + expected = '1st' + result = string_utils.get_int_as_rank(1) + self.assertEqual(expected, result) + + def test_get_int_as_rank_second(self): + expected = '2nd' + result = string_utils.get_int_as_rank(2) + self.assertEqual(expected, result) + + def test_get_int_as_rank_third(self): + expected = '3rd' + result = string_utils.get_int_as_rank(3) + self.assertEqual(expected, result) + + def test_get_int_as_rank_4th_to_10th(self): + for i in range(4, 11): + with self.subTest(i=i): + expected = f'{i}th' + result = string_utils.get_int_as_rank(i) + self.assertEqual(expected, result) + + def test_get_int_as_rank_11th_to_13th(self): + for i in range(11, 14): + with self.subTest(i=i): + expected = f'{i}th' + result = string_utils.get_int_as_rank(i) + self.assertEqual(expected, result) + + def test_get_int_as_rank_14th_to_20th(self): + for i in range(14, 21): + with self.subTest(i=i): + expected = f'{i}th' + result = string_utils.get_int_as_rank(i) + self.assertEqual(expected, result) + + def test_get_int_as_rank_21st_to_100th(self): + for i in range(21, 101): + with self.subTest(i=i): + last_digit = i % 10 + suffix_dict = {1: "st", 2: "nd", 3: "rd"} + expected_suffix = suffix_dict.get(last_digit, "th") + expected = f'{i}{expected_suffix}' + result = string_utils.get_int_as_rank(i) + self.assertEqual(expected, result) diff --git a/tests/test_utils/test_surface_utils.py b/tests/test_utils/test_surface_utils.py index a7cef2b3..b41a314c 100644 --- a/tests/test_utils/test_surface_utils.py +++ b/tests/test_utils/test_surface_utils.py @@ -27,6 +27,29 @@ def setUp(self): def setUpClass(cls): maya_test_tools.import_maya_standalone(initialize=True) # Start Maya Headless (mayapy.exe) + def test_is_surface_true(self): + sur = maya_test_tools.cmds.nurbsPlane(name="mocked_sur", constructionHistory=False)[0] + sur_shape = maya_test_tools.cmds.listRelatives(sur, shapes=True)[0] + result = surface_utils.is_surface(surface=sur, accept_transform_parent=True) + expected = True + self.assertEqual(expected, result) + result = surface_utils.is_surface(surface=sur_shape, accept_transform_parent=True) + expected = True + self.assertEqual(expected, result) + result = surface_utils.is_surface(surface=sur_shape, accept_transform_parent=False) + expected = True + self.assertEqual(expected, result) + + def test_is_surface_false(self): + sur = maya_test_tools.cmds.nurbsPlane(name="mocked_sur", constructionHistory=False)[0] + cube = maya_test_tools.create_poly_cube(name="mocked_polygon") + result = surface_utils.is_surface(surface=sur, accept_transform_parent=False) + expected = False + self.assertEqual(expected, result) + result = surface_utils.is_surface(surface=cube, accept_transform_parent=True) + expected = False + self.assertEqual(expected, result) + def test_is_surface_periodic_false(self): sur = maya_test_tools.cmds.nurbsPlane(name="mocked_sur", constructionHistory=False)[0] sur_shape = maya_test_tools.cmds.listRelatives(sur, shapes=True)[0] @@ -42,3 +65,122 @@ def test_is_surface_periodic_true(self): expected = True self.assertEqual(expected, result) + + def test_get_surface_function_set(self): + sur = maya_test_tools.cmds.nurbsPlane(name="mocked_sur", constructionHistory=False)[0] + surface_fn = surface_utils.get_surface_function_set(surface=sur) + import maya.OpenMaya as OpenMaya + self.assertIsInstance(surface_fn, OpenMaya.MFnNurbsSurface) + + def test_create_surface_from_object_list(self): + cube_one = maya_test_tools.create_poly_cube(name="cube_one") + cube_two = maya_test_tools.create_poly_cube(name="cube_two") + maya_test_tools.cmds.setAttr(f'{cube_two}.tx', 5) + obj_list = [cube_one, cube_two] + result = surface_utils.create_surface_from_object_list(obj_list=obj_list) + expected = "loftedSurface1" + self.assertEqual(expected, result) + result = surface_utils.create_surface_from_object_list(obj_list=obj_list, + surface_name="mocked_name") + expected = "mocked_name" + self.assertEqual(expected, result) + + def test_create_surface_from_object_list_degree(self): + cube_one = maya_test_tools.create_poly_cube(name="cube_one") + cube_two = maya_test_tools.create_poly_cube(name="cube_two") + maya_test_tools.cmds.setAttr(f'{cube_two}.tx', 5) + obj_list = [cube_one, cube_two] + surface = surface_utils.create_surface_from_object_list(obj_list=obj_list, degree=3, surface_name="cubic") + surface_shape = maya_test_tools.cmds.listRelatives(surface, shapes=True, typ="nurbsSurface") + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.degreeUV')[0] + expected = (3, 1) + self.assertEqual(expected, result) + surface = surface_utils.create_surface_from_object_list(obj_list=obj_list, degree=1, surface_name="linear") + surface_shape = maya_test_tools.cmds.listRelatives(surface, shapes=True, typ="nurbsSurface") + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.degreeUV')[0] + expected = (1, 1) + self.assertEqual(expected, result) + + def test_multiply_surface_spans(self): + surface = maya_test_tools.cmds.nurbsPlane(ch=False)[0] + surface_shape = maya_test_tools.cmds.listRelatives(surface, shapes=True, typ="nurbsSurface") + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.spansUV')[0] + expected = (1, 1) + self.assertEqual(expected, result) + surface_utils.multiply_surface_spans(input_surface=surface, u_multiplier=2, v_multiplier=2) + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.spansUV')[0] + expected = (2, 2) + self.assertEqual(expected, result) + surface_utils.multiply_surface_spans(input_surface=surface, u_multiplier=2, v_multiplier=2) + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.spansUV')[0] + expected = (4, 4) + self.assertEqual(expected, result) + + def test_multiply_surface_spans_degrees(self): + surface = maya_test_tools.cmds.nurbsPlane(ch=False)[0] + surface_shape = maya_test_tools.cmds.listRelatives(surface, shapes=True, typ="nurbsSurface") + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.degreeUV')[0] + expected = (3, 3) + self.assertEqual(expected, result) + surface_utils.multiply_surface_spans(input_surface=surface, + u_multiplier=2, v_multiplier=2, + u_degree=3, v_degree=3) + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.degreeUV')[0] + expected = (3, 3) + self.assertEqual(expected, result) + surface_utils.multiply_surface_spans(input_surface=surface, + u_multiplier=2, v_multiplier=2, + u_degree=1, v_degree=1) + result = maya_test_tools.cmds.getAttr(f'{surface_shape[0]}.degreeUV')[0] + expected = (1, 1) + self.assertEqual(expected, result) + + def test_create_follicle(self): + surface = maya_test_tools.cmds.nurbsPlane(ch=False)[0] + follicle_tuple = surface_utils.create_follicle(input_surface=surface, uv_position=(0.5, 0.5), name=None) + _transform = follicle_tuple[0] + _shape = follicle_tuple[1] + expected_transform = "|follicle" + expected_shape = "|follicle|follicleShape" + self.assertEqual(expected_transform, str(_transform)) + self.assertEqual(expected_shape, str(_shape)) + result_u_pos = maya_test_tools.cmds.getAttr(f'{_shape}.parameterU') + result_v_pos = maya_test_tools.cmds.getAttr(f'{_shape}.parameterV') + expected_u_pos = 0.5 + expected_v_pos = 0.5 + self.assertEqual(expected_u_pos, result_u_pos) + self.assertEqual(expected_v_pos, result_v_pos) + + def test_create_follicle_custom_uv_position_and_name(self): + surface = maya_test_tools.cmds.nurbsPlane(ch=False)[0] + surface_shape = maya_test_tools.cmds.listRelatives(surface, shapes=True, typ="nurbsSurface")[0] + follicle_tuple = surface_utils.create_follicle(input_surface=surface_shape, + uv_position=(0.3, 0.7), + name="mocked_follicle") + _transform = follicle_tuple[0] + _shape = follicle_tuple[1] + expected_transform = "|mocked_follicle" + expected_shape = "|mocked_follicle|mocked_follicleShape" + self.assertEqual(expected_transform, str(_transform)) + self.assertEqual(expected_shape, str(_shape)) + result_u_pos = maya_test_tools.cmds.getAttr(f'{_shape}.parameterU') + result_v_pos = maya_test_tools.cmds.getAttr(f'{_shape}.parameterV') + expected_u_pos = 0.3 + expected_v_pos = 0.7 + self.assertEqual(expected_u_pos, result_u_pos) + self.assertEqual(expected_v_pos, result_v_pos) + + def test_get_closest_uv_point(self): + surface = maya_test_tools.cmds.nurbsPlane(ch=False, axis=(0, 1, 0))[0] + surface_shape = maya_test_tools.cmds.listRelatives(surface, shapes=True, typ="nurbsSurface")[0] + uv_coordinates = surface_utils.get_closest_uv_point(surface=surface, xyz_pos=(0, 0, 0)) + expected = (0.5, 0.5) + self.assertEqual(expected, uv_coordinates) + + uv_coordinates = surface_utils.get_closest_uv_point(surface=surface_shape, xyz_pos=(0, 0, 0)) + expected = (0.5, 0.5) + self.assertEqual(expected, uv_coordinates) + + uv_coordinates = surface_utils.get_closest_uv_point(surface=surface_shape, xyz_pos=(0.1, 0, 0)) + expected = (0.6, 0.5) + self.assertEqual(expected, uv_coordinates) diff --git a/tests/test_utils/test_transform_utils.py b/tests/test_utils/test_transform_utils.py index a41b735b..a59c4fee 100644 --- a/tests/test_utils/test_transform_utils.py +++ b/tests/test_utils/test_transform_utils.py @@ -1352,3 +1352,250 @@ def test_set_equidistant_transforms_point_type(self): self.assertAlmostEqualSigFig(rx, expected[3]) self.assertAlmostEqualSigFig(ry, expected[4]) self.assertAlmostEqualSigFig(rz, expected[5]) + + def test_translate_shapes(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + + num_cvs = maya_test_tools.cmds.getAttr(f"{crv}.spans") + num_cvs += maya_test_tools.cmds.getAttr(f"{crv}.degree") + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]] + self.assertEqual(expected, cv_positions) + + transform_utils.translate_shapes(obj_transform=crv, offset=(1, 0, 0)) + + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[1.0, 0.0, 1.0], [1.0, 0.0, 0.667], [1.0, 0.0, 0.0], + [1.0, 0.0, -1.0], [1.0, 0.0, -1.667], [1.0, 0.0, -2.0]] + self.assertEqual(expected, cv_positions) + + def test_rotate_shapes(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + + num_cvs = maya_test_tools.cmds.getAttr(f"{crv}.spans") + num_cvs += maya_test_tools.cmds.getAttr(f"{crv}.degree") + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]] + self.assertEqual(expected, cv_positions) + + transform_utils.rotate_shapes(obj_transform=crv, offset=(90, 0, 0)) + + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[0.0, -1.0, 0.0], [0.0, -0.667, 0.0], [0.0, 0.0, 0.0], + [0.0, 1.0, 0.0], [0.0, 1.667, 0.0], [0.0, 2.0, 0.0]] + self.assertEqual(expected, cv_positions) + + def test_scale_shapes_integer(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + + num_cvs = maya_test_tools.cmds.getAttr(f"{crv}.spans") + num_cvs += maya_test_tools.cmds.getAttr(f"{crv}.degree") + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]] + self.assertEqual(expected, cv_positions) + + transform_utils.scale_shapes(obj_transform=crv, offset=2) + + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[0.0, 0.0, 2.0], [0.0, 0.0, 1.334], [0.0, 0.0, 0.0], + [0.0, 0.0, -2.0], [0.0, 0.0, -3.334], [0.0, 0.0, -4.0]] + self.assertEqual(expected, cv_positions) + + def test_scale_shapes_tuple(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + + num_cvs = maya_test_tools.cmds.getAttr(f"{crv}.spans") + num_cvs += maya_test_tools.cmds.getAttr(f"{crv}.degree") + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]] + self.assertEqual(expected, cv_positions) + + transform_utils.scale_shapes(obj_transform=crv, offset=(2, 1, 1)) + + cv_positions = [] + for i in range(num_cvs): + cv_position = maya_test_tools.cmds.pointPosition(f"{crv}.cv[{i}]", world=True) + cv_positions.append(cv_position) + + expected = [[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]] + self.assertEqual(expected, cv_positions) + + def test_get_component_positions_as_dict_world_space(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + maya_test_tools.cmds.move(0, 1, 0, crv) + result = transform_utils.get_component_positions_as_dict(obj_transform=crv, + full_path=True, + world_space=True) + + expected = {'|mocked_curve.cv[0]': [0.0, 1.0, 1.0], + '|mocked_curve.cv[1]': [0.0, 1.0, 0.667], + '|mocked_curve.cv[2]': [0.0, 1.0, 0.0], + '|mocked_curve.cv[3]': [0.0, 1.0, -1.0], + '|mocked_curve.cv[4]': [0.0, 1.0, -1.667], + '|mocked_curve.cv[5]': [0.0, 1.0, -2.0]} + self.assertEqual(expected, result) + + result = transform_utils.get_component_positions_as_dict(obj_transform=crv, + full_path=False, + world_space=True) + + expected = {'mocked_curve.cv[0]': [0.0, 1.0, 1.0], + 'mocked_curve.cv[1]': [0.0, 1.0, 0.667], + 'mocked_curve.cv[2]': [0.0, 1.0, 0.0], + 'mocked_curve.cv[3]': [0.0, 1.0, -1.0], + 'mocked_curve.cv[4]': [0.0, 1.0, -1.667], + 'mocked_curve.cv[5]': [0.0, 1.0, -2.0]} + self.assertEqual(expected, result) + + def test_get_component_positions_as_dict_object_space(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + maya_test_tools.cmds.move(0, 1, 0, crv) + result = transform_utils.get_component_positions_as_dict(obj_transform=crv, + full_path=True, + world_space=False) # False = Object Space + + expected = {'|mocked_curve.cv[0]': [0.0, 0.0, 1.0], + '|mocked_curve.cv[1]': [0.0, 0.0, 0.667], + '|mocked_curve.cv[2]': [0.0, 0.0, 0.0], + '|mocked_curve.cv[3]': [0.0, 0.0, -1.0], + '|mocked_curve.cv[4]': [0.0, 0.0, -1.667], + '|mocked_curve.cv[5]': [0.0, 0.0, -2.0]} + self.assertEqual(expected, result) + + result = transform_utils.get_component_positions_as_dict(obj_transform=crv, + full_path=False, + world_space=False) # False = Object Space + + expected = {'mocked_curve.cv[0]': [0.0, 0.0, 1.0], + 'mocked_curve.cv[1]': [0.0, 0.0, 0.667], + 'mocked_curve.cv[2]': [0.0, 0.0, 0.0], + 'mocked_curve.cv[3]': [0.0, 0.0, -1.0], + 'mocked_curve.cv[4]': [0.0, 0.0, -1.667], + 'mocked_curve.cv[5]': [0.0, 0.0, -2.0]} + self.assertEqual(expected, result) + + def test_get_component_positions_as_dict_cube(self): + cube = maya_test_tools.create_poly_cube(name="mocked_cube") + maya_test_tools.cmds.move(0, 1, 0, cube) + result = transform_utils.get_component_positions_as_dict(obj_transform=cube, + full_path=True, + world_space=True) + + expected = {'|mocked_cube.vtx[0]': [-0.5, 0.5, 0.5], + '|mocked_cube.vtx[1]': [0.5, 0.5, 0.5], + '|mocked_cube.vtx[2]': [-0.5, 1.5, 0.5], + '|mocked_cube.vtx[3]': [0.5, 1.5, 0.5], + '|mocked_cube.vtx[4]': [-0.5, 1.5, -0.5], + '|mocked_cube.vtx[5]': [0.5, 1.5, -0.5], + '|mocked_cube.vtx[6]': [-0.5, 0.5, -0.5], + '|mocked_cube.vtx[7]': [0.5, 0.5, -0.5]} + self.assertEqual(expected, result) + + result = transform_utils.get_component_positions_as_dict(obj_transform=cube, + full_path=False, + world_space=True) + + expected = {'mocked_cube.vtx[0]': [-0.5, 0.5, 0.5], + 'mocked_cube.vtx[1]': [0.5, 0.5, 0.5], + 'mocked_cube.vtx[2]': [-0.5, 1.5, 0.5], + 'mocked_cube.vtx[3]': [0.5, 1.5, 0.5], + 'mocked_cube.vtx[4]': [-0.5, 1.5, -0.5], + 'mocked_cube.vtx[5]': [0.5, 1.5, -0.5], + 'mocked_cube.vtx[6]': [-0.5, 0.5, -0.5], + 'mocked_cube.vtx[7]': [0.5, 0.5, -0.5]} + self.assertEqual(expected, result) + + def test_set_component_positions_from_dict_world_space(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + maya_test_tools.cmds.move(0, 1, 0, crv) + + component_dict = {'|mocked_curve.cv[0]': [0.0, 0.0, 2.0]} + + transform_utils.set_component_positions_from_dict(component_pos_dict=component_dict, world_space=True) + + result = maya_test_tools.cmds.xform('|mocked_curve.cv[0]', worldSpace=True, query=True, translation=True) + + expected = [0.0, 0.0, 2.0] + self.assertEqual(expected, result) + + component_dict = {'|mocked_curve.cv[0]': [0.0, 0.0, 3.0]} + + transform_utils.set_component_positions_from_dict(component_pos_dict=component_dict, world_space=True) + + result = maya_test_tools.cmds.xform('|mocked_curve.cv[0]', worldSpace=True, query=True, translation=True) + + expected = [0.0, 0.0, 3.0] + self.assertEqual(expected, result) + + def test_set_component_positions_from_dict_object_space(self): + crv = maya_test_tools.cmds.curve(point=[[0.0, 0.0, 1.0], [0.0, 0.0, 0.667], [0.0, 0.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, -1.667], [0.0, 0.0, -2.0]], + degree=3, name='mocked_curve') + maya_test_tools.cmds.move(0, 1, 0, crv) + + component_dict = {'|mocked_curve.cv[0]': [0.0, 0.0, 2.0]} + + transform_utils.set_component_positions_from_dict(component_pos_dict=component_dict, + world_space=False) # False = Object Space + + result = maya_test_tools.cmds.xform('|mocked_curve.cv[0]', worldSpace=True, query=True, translation=True) + + expected = [0.0, 1.0, 2.0] + self.assertEqual(expected, result) + + component_dict = {'|mocked_curve.cv[0]': [0.0, 0.0, 3.0]} + + transform_utils.set_component_positions_from_dict(component_pos_dict=component_dict, + world_space=False) # False = Object Space + + result = maya_test_tools.cmds.xform('|mocked_curve.cv[0]', worldSpace=True, query=True, translation=True) + + expected = [0.0, 1.0, 3.0] + self.assertEqual(expected, result)