From 9e5a1ebd888b9aaf647bdf643dcfcf0de7568f3b Mon Sep 17 00:00:00 2001 From: David Cattermole Date: Fri, 29 Dec 2023 22:44:05 +1100 Subject: [PATCH] Add 'Create Rivet' tool. The Create Rivet tool supports both "mesh two edges" and "point on poly constraint" styles of rivets. --- docs/source/mmSolver.tools.createrivet.rst | 13 + docs/source/mmSolver.tools.rst | 1 + docs/source/mmSolver.utils.rst | 25 +- docs/source/tools_generaltools.rst | 48 ++++ python/mmSolver/tools/createrivet/__init__.py | 20 ++ python/mmSolver/tools/createrivet/tool.py | 92 +++++++ python/mmSolver/utils/rivet/__init__.py | 20 ++ python/mmSolver/utils/rivet/meshtwoedge.py | 185 ++++++++++++++ .../utils/rivet/nearestpointonmesh.py | 77 ++++++ python/mmSolver/utils/rivet/pointonpoly.py | 233 ++++++++++++++++++ python/mmSolver/utils/selection.py | 149 +++++++++++ share/config/functions.json | 9 + share/config/menu.json | 2 + share/config/shelf.json | 2 + share/config/shelf_minimal.json | 2 + 15 files changed, 876 insertions(+), 2 deletions(-) create mode 100644 docs/source/mmSolver.tools.createrivet.rst create mode 100644 python/mmSolver/tools/createrivet/__init__.py create mode 100644 python/mmSolver/tools/createrivet/tool.py create mode 100644 python/mmSolver/utils/rivet/__init__.py create mode 100644 python/mmSolver/utils/rivet/meshtwoedge.py create mode 100644 python/mmSolver/utils/rivet/nearestpointonmesh.py create mode 100644 python/mmSolver/utils/rivet/pointonpoly.py create mode 100644 python/mmSolver/utils/selection.py diff --git a/docs/source/mmSolver.tools.createrivet.rst b/docs/source/mmSolver.tools.createrivet.rst new file mode 100644 index 000000000..c109529e1 --- /dev/null +++ b/docs/source/mmSolver.tools.createrivet.rst @@ -0,0 +1,13 @@ +mmSolver.tools.createrivet +========================== + +.. automodule:: mmSolver.tools.createrivet + :members: + :undoc-members: + +Tools ++++++ + +.. automodule:: mmSolver.tools.createrivet.tool + :members: + :undoc-members: diff --git a/docs/source/mmSolver.tools.rst b/docs/source/mmSolver.tools.rst index 6654dca74..f24209d11 100644 --- a/docs/source/mmSolver.tools.rst +++ b/docs/source/mmSolver.tools.rst @@ -28,6 +28,7 @@ mmSolver.tools mmSolver.tools.createlens mmSolver.tools.createline mmSolver.tools.createmarker + mmSolver.tools.createrivet mmSolver.tools.createskydome mmSolver.tools.deformmarker mmSolver.tools.duplicatemarker diff --git a/docs/source/mmSolver.utils.rst b/docs/source/mmSolver.utils.rst index 476274458..bc458801c 100644 --- a/docs/source/mmSolver.utils.rst +++ b/docs/source/mmSolver.utils.rst @@ -89,7 +89,6 @@ Load File :members: :undoc-members: - Load Marker +++++++++++ @@ -131,7 +130,7 @@ Formats .. automodule:: mmSolver.utils.loadmarker.formats.uvtrack :members: :undoc-members: - + Node ++++ @@ -167,6 +166,28 @@ Re-Project :members: :undoc-members: +Rivet ++++++ + +.. automodule:: mmSolver.utils.rivet.meshtwoedge + :members: + :undoc-members: + +.. automodule:: mmSolver.utils.rivet.nearestpointonmesh + :members: + :undoc-members: + +.. automodule:: mmSolver.utils.rivet.pointonpoly + :members: + :undoc-members: + +Selection ++++++++++ + +.. automodule:: mmSolver.utils.selection + :members: + :undoc-members: + Smooth ++++++ diff --git a/docs/source/tools_generaltools.rst b/docs/source/tools_generaltools.rst index b4099a388..f62e73444 100644 --- a/docs/source/tools_generaltools.rst +++ b/docs/source/tools_generaltools.rst @@ -228,6 +228,54 @@ version 1, use this python code to run it. # Remove selected Controller tool.remove() +.. _create-rivet-tool-ref: + +Create Rivet +------------ + +Create a transform locator node that follows the surface of a Mesh, i.e. the +transform is 'riveted' to the mesh. + +There are two types of rivet types currently supported: + + - **Mesh Two Edges** rivets are created from 2 Mesh shape edge + components, the same as using the classic `rivet.mel`_ script. If + the topology of the underlying mesh changes, the rivet will move + across the surface. + + - **Point On Poly Constraint** rivets are created at selected + vertices and can be moved along the surface with the UV + coordinates. If the UV coordinates of the underlying mesh changes, + the rivet may move. This rivet-style will not work with UV + coordinates outside the regular 0.0 to 1.0 UV space as is commonly + used with texture UDIMs. + +Usage: + +1) Select a Maya Mesh components. + + - Select Mesh Vertices to create **Point On Poly Constraint** + rivets. + + - Select 2 Mesh Edges to create a single **Mesh Two Edges** rivet. + +2) Run tool. + + - A rivet locator will be created. + + - For a **Point On Poly Constraint** rivet, you can adjust the `U` + and `V` coordinates from the rivet locator, if needed. + +To run the tool, use this Python command: + +.. code:: python + + import mmSolver.tools.createrivet.tool as tool + tool.main() + +.. _rivet.mel: + https://www.highend3d.com/maya/script/rivet-button-for-maya + .. _marker-bundle-rename-tool-ref: Marker Bundle Rename diff --git a/python/mmSolver/tools/createrivet/__init__.py b/python/mmSolver/tools/createrivet/__init__.py new file mode 100644 index 000000000..63c3ea430 --- /dev/null +++ b/python/mmSolver/tools/createrivet/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2023 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Create a Rivet on a surface. +""" diff --git a/python/mmSolver/tools/createrivet/tool.py b/python/mmSolver/tools/createrivet/tool.py new file mode 100644 index 000000000..80b0d1449 --- /dev/null +++ b/python/mmSolver/tools/createrivet/tool.py @@ -0,0 +1,92 @@ +# Copyright (C) 2014, 2023 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +The Create Rivet tool. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import maya.cmds + +import mmSolver.logger +import mmSolver.utils.rivet.meshtwoedge as utils_rivet_meshtwoedge +import mmSolver.utils.rivet.pointonpoly as utils_rivet_pointonpoly +import mmSolver.utils.selection as utils_selection + + +LOG = mmSolver.logger.get_logger() + + +def _create_mesh_two_edge_rivet(edges): + assert len(edges) == 2 + edge_a = edges[0] + edge_b = edges[1] + mesh_shape = edges[0].partition('.')[0] + assert maya.cmds.objExists(mesh_shape) + rivet = utils_rivet_meshtwoedge.create(mesh_shape, edge_a, edge_b) + return [rivet.rivet_transform] + + +def _create_point_on_poly_rivets(vertices): + assert len(vertices) > 0 + select_nodes = [] + for vertex in vertices: + mesh_shape = vertex.partition('.')[0] + mesh_transform = maya.cmds.listRelatives( + mesh_shape, parent=True, fullPath=True, type='transform' + )[0] + vertex_world_position = maya.cmds.xform( + vertex, query=True, worldSpace=True, translation=True + ) + rivet = utils_rivet_pointonpoly.create( + mesh_transform, mesh_shape, in_position=vertex_world_position + ) + select_nodes.append(rivet.rivet_transform) + return select_nodes + + +def main(): + """ + Create Rivet(s) from the selected mesh components. + + If 2 edges are selected, this tool will create a two-edge rivet, + like the 'rivet.mel'. + """ + error_msg = 'Please select Mesh Vertices or 2 Mesh Edges.' + selection = maya.cmds.ls(selection=True, long=True) or [] + if len(selection) == 0: + LOG.error(error_msg) + return + + vertices = utils_selection.filter_mesh_vertex_selection(selection) + edges = utils_selection.filter_mesh_edge_selection(selection) + if len(vertices + edges) == 0: + LOG.error(error_msg) + return + + if len(vertices) > 0: + select_nodes = _create_point_on_poly_rivets(vertices) + maya.cmds.select(select_nodes, replace=True) + elif len(edges) == 2: + select_nodes = _create_mesh_two_edge_rivet(edges) + maya.cmds.select(select_nodes, replace=True) + else: + LOG.error(error_msg) + return diff --git a/python/mmSolver/utils/rivet/__init__.py b/python/mmSolver/utils/rivet/__init__.py new file mode 100644 index 000000000..2b9c87281 --- /dev/null +++ b/python/mmSolver/utils/rivet/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2023 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Rivet utilities. +""" diff --git a/python/mmSolver/utils/rivet/meshtwoedge.py b/python/mmSolver/utils/rivet/meshtwoedge.py new file mode 100644 index 000000000..547d54023 --- /dev/null +++ b/python/mmSolver/utils/rivet/meshtwoedge.py @@ -0,0 +1,185 @@ +# Copyright (C) 2022, 2023 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +The 'rivet.mel technique' for creating rivets. + +'rivet.mel' was written by Michael Bazhutkin in 2001. +https://www.highend3d.com/maya/script/rivet-button-for-maya + +This method uses polygon edges converted into NURBS curves, then +placing a point at the average of two different NURBS curves. + +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections + +import maya.cmds + +import mmSolver.logger + + +LOG = mmSolver.logger.get_logger() + + +RivetMeshTwoEdge = collections.namedtuple( + 'RivetMeshTwoEdge', + [ + 'rivet_transform', + 'rivet_shape', + 'constraint_node', + 'loft_node', + 'point_on_surface_info_node', + 'curve_from_mesh_edge_a_node', + 'curve_from_mesh_edge_b_node', + ], +) + + +def create(mesh_shape, edge_a, edge_b, name=None): + """ + Create a Rivet position between two edges. + + :param mesh_shape: Mesh shape node. + :type mesh_shape: str + + :param edge_a: First edge component path. + :type edge_a: str + + :param edge_b: Second edge component path. + :type edge_b: str + + :returns: A rivet object containing names to all the nodes. + :rtype: RivetMeshTwoEdge + """ + assert maya.cmds.objExists(mesh_shape) + assert maya.cmds.objExists(edge_a) + assert maya.cmds.objExists(edge_b) + if name is None: + name = 'mmRivetMeshTwoEdge1' + assert isinstance(name, str) + + edge_index_a = edge_a.split('[')[-1] + edge_index_b = edge_b.split('[')[-1] + edge_index_a = int(edge_index_a.split(']')[0]) + edge_index_b = int(edge_index_b.split(']')[0]) + + # Create Curve From Edge A. + curve_from_mesh_edge_a = maya.cmds.createNode( + 'curveFromMeshEdge', name='mmRivetCurveFromMeshEdge_a1' + ) + maya.cmds.setAttr(curve_from_mesh_edge_a + '.isHistoricallyInteresting', 1) + maya.cmds.setAttr(curve_from_mesh_edge_a + '.edgeIndex[0]', edge_index_a) + + # Create Curve From Edge B. + curve_from_mesh_edge_b = maya.cmds.createNode( + 'curveFromMeshEdge', name='mmRivetCurveFromMeshEdge_b1' + ) + maya.cmds.setAttr(curve_from_mesh_edge_b + '.isHistoricallyInteresting', 1) + maya.cmds.setAttr(curve_from_mesh_edge_b + '.edgeIndex[0]', edge_index_b) + + # Create Loft node. + loft_node = maya.cmds.createNode('loft', name='mmRivetLoft1') + maya.cmds.setAttr(loft_node + '.inputCurve', size=2) + maya.cmds.setAttr(loft_node + '.uniform', True) + maya.cmds.setAttr(loft_node + '.reverseSurfaceNormals', True) + + # Create Point On Surface Info. + surface_info = maya.cmds.createNode( + 'pointOnSurfaceInfo', name='mmRivetPointOnSurfaceInfo1' + ) + maya.cmds.setAttr(surface_info + '.turnOnPercentage', 1) + maya.cmds.setAttr(surface_info + '.parameterU', 0.5) + maya.cmds.setAttr(surface_info + '.parameterV', 0.5) + + maya.cmds.connectAttr( + loft_node + '.outputSurface', + surface_info + '.inputSurface', + force=True, + ) + output_surface_attr_a = curve_from_mesh_edge_a + '.outputCurve' + output_surface_attr_b = curve_from_mesh_edge_b + '.outputCurve' + maya.cmds.connectAttr(output_surface_attr_a, loft_node + '.inputCurve[0]') + maya.cmds.connectAttr(output_surface_attr_b, loft_node + '.inputCurve[1]') + mesh_world_mesh_attr = mesh_shape + '.worldMesh' + maya.cmds.connectAttr(mesh_world_mesh_attr, curve_from_mesh_edge_a + '.inputMesh') + maya.cmds.connectAttr(mesh_world_mesh_attr, curve_from_mesh_edge_b + '.inputMesh') + + # Create Locator. + rivet_tfm = maya.cmds.createNode('transform', name=name) + rivet_shp_name = rivet_tfm.split('|')[-1] + 'Shape' + rivet_shp = maya.cmds.createNode('locator', name=rivet_shp_name, parent=rivet_tfm) + + # Create Aim Constraint. + aim_constraint = maya.cmds.createNode( + 'aimConstraint', parent=rivet_tfm, name=(rivet_tfm + '_mmRivetAimConstraint1') + ) + maya.cmds.setAttr(aim_constraint + '.target[0].targetWeight', 1) + maya.cmds.setAttr(aim_constraint + '.aimVector', 0.0, 1.0, 0.0, typ='double3') + maya.cmds.setAttr(aim_constraint + '.upVector', 0.0, 0.0, 1.0, typ='double3') + # Don't show users the aim constraint internals they should not + # change anyway. + maya.cmds.setAttr(aim_constraint + '.v', keyable=False) + maya.cmds.setAttr(aim_constraint + '.tx', keyable=False) + maya.cmds.setAttr(aim_constraint + '.ty', keyable=False) + maya.cmds.setAttr(aim_constraint + '.tz', keyable=False) + maya.cmds.setAttr(aim_constraint + '.rx', keyable=False) + maya.cmds.setAttr(aim_constraint + '.ry', keyable=False) + maya.cmds.setAttr(aim_constraint + '.rz', keyable=False) + maya.cmds.setAttr(aim_constraint + '.sx', keyable=False) + maya.cmds.setAttr(aim_constraint + '.sy', keyable=False) + maya.cmds.setAttr(aim_constraint + '.sz', keyable=False) + + maya.cmds.connectAttr(surface_info + '.position', rivet_tfm + '.translate') + maya.cmds.connectAttr( + surface_info + '.normal', aim_constraint + '.target[0].targetTranslate' + ) + maya.cmds.connectAttr(surface_info + '.tangentV', aim_constraint + '.worldUpVector') + maya.cmds.connectAttr(aim_constraint + '.constraintRotateX', rivet_tfm + '.rotateX') + maya.cmds.connectAttr(aim_constraint + '.constraintRotateY', rivet_tfm + '.rotateY') + maya.cmds.connectAttr(aim_constraint + '.constraintRotateZ', rivet_tfm + '.rotateZ') + + # Do not allow users to disconnect or scale the rivet transform. + attr_names = [ + 'translateX', + 'translateY', + 'translateZ', + 'rotateX', + 'rotateY', + 'rotateZ', + 'scaleX', + 'scaleY', + 'scaleZ', + ] + for attr_name in attr_names: + attr = '{}.{}'.format(rivet_tfm, attr_name) + maya.cmds.setAttr(attr, lock=True) + + rivet = RivetMeshTwoEdge( + rivet_transform=rivet_tfm, + rivet_shape=rivet_shp, + constraint_node=aim_constraint, + loft_node=loft_node, + point_on_surface_info_node=surface_info, + curve_from_mesh_edge_a_node=curve_from_mesh_edge_a, + curve_from_mesh_edge_b_node=curve_from_mesh_edge_b, + ) + return rivet diff --git a/python/mmSolver/utils/rivet/nearestpointonmesh.py b/python/mmSolver/utils/rivet/nearestpointonmesh.py new file mode 100644 index 000000000..f0e32e4fd --- /dev/null +++ b/python/mmSolver/utils/rivet/nearestpointonmesh.py @@ -0,0 +1,77 @@ +# Copyright (C) 2014, 2022, 2023 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Query the Nearest Point On a Mesh surface. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections + +import maya.cmds + +import mmSolver.logger + + +LOG = mmSolver.logger.get_logger() + +NearestPointData = collections.namedtuple( + 'NearestPointData', ['position', 'normal', 'coords', 'face_index'] +) + + +def get_nearest_point_on_mesh(mesh_shape, in_position): + """ + Get data on the mesh surface, for the nearest position to + 'in_position'. + """ + assert maya.cmds.objExists(mesh_shape) + assert isinstance(in_position, (tuple, list)) and len(in_position) == 3 + + maya.cmds.loadPlugin('nearestPointOnMesh', quiet=True) + + tmp_node = maya.cmds.createNode('nearestPointOnMesh') + maya.cmds.connectAttr(mesh_shape + '.worldMesh[0]', tmp_node + '.inMesh') + + maya.cmds.setAttr(tmp_node + '.inPositionX', in_position[0]) + maya.cmds.setAttr(tmp_node + '.inPositionY', in_position[1]) + maya.cmds.setAttr(tmp_node + '.inPositionZ', in_position[2]) + + position = ( + maya.cmds.getAttr(tmp_node + '.positionX'), + maya.cmds.getAttr(tmp_node + '.positionY'), + maya.cmds.getAttr(tmp_node + '.positionZ'), + ) + normal = ( + maya.cmds.getAttr(tmp_node + '.normalX'), + maya.cmds.getAttr(tmp_node + '.normalY'), + maya.cmds.getAttr(tmp_node + '.normalZ'), + ) + coords = ( + maya.cmds.getAttr(tmp_node + '.parameterU'), + maya.cmds.getAttr(tmp_node + '.parameterV'), + ) + face_index = maya.cmds.getAttr(tmp_node + '.nearestFaceIndex') + result = NearestPointData( + position=position, normal=normal, coords=coords, face_index=face_index + ) + + maya.cmds.delete(tmp_node) + return result diff --git a/python/mmSolver/utils/rivet/pointonpoly.py b/python/mmSolver/utils/rivet/pointonpoly.py new file mode 100644 index 000000000..73e4dde7d --- /dev/null +++ b/python/mmSolver/utils/rivet/pointonpoly.py @@ -0,0 +1,233 @@ +# Copyright (C) 2014, 2022, 2023 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Create a Rivet using a Point-On-Poly Constraint. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections + +import maya.cmds + +import mmSolver.utils.node as node_utils +import mmSolver.utils.rivet.nearestpointonmesh as nearestpointonmesh_utils +import mmSolver.logger + +LOG = mmSolver.logger.get_logger() + + +def _create_locator(name, parent=None): + tfm = maya.cmds.createNode('transform', name=name, parent=parent) + tfm = node_utils.get_long_name(tfm) + + name_shp = tfm.split('|')[-1] + 'Shape' + shp = maya.cmds.createNode('locator', name=name_shp, parent=tfm) + shp = node_utils.get_long_name(shp) + return tfm, shp + + +def _lock_node_attrs(node, attrs, lock=None, keyable=None, channelBox=None): + assert lock is None or isinstance(lock, bool) + assert keyable is None or isinstance(keyable, bool) + assert channelBox is None or isinstance(channelBox, bool) + + kwargs = {} + if lock is not None: + kwargs['lock'] = lock + if keyable is not None: + kwargs['keyable'] = keyable + if channelBox is not None: + kwargs['channelBox'] = channelBox + + for attr in attrs: + if node_utils.attribute_exists(attr, node): + node_attr = node + '.' + attr + maya.cmds.setAttr(node_attr, **kwargs) + return + + +def _lock_constraint_offset_attrs(node, lock=None, keyable=None, channelBox=None): + attrs = ['ox', 'oy', 'oz', 'otx', 'oty', 'otz', 'orx', 'ory', 'orz'] + return _lock_node_attrs( + node, attrs, lock=lock, keyable=keyable, channelBox=channelBox + ) + + +def _lock_transform_attrs(node, lock=None, keyable=None, channelBox=None): + attrs = [ + 'tx', + 'ty', + 'tz', + 'rx', + 'ry', + 'rz', + 'sx', + 'sy', + 'sz', + 'shxy', + 'shxz', + 'shyz', + ] + return _lock_node_attrs( + node, attrs, lock=lock, keyable=keyable, channelBox=channelBox + ) + + +class PointOnPolyConstraintNode(object): + def __init__(self, node): + super(PointOnPolyConstraintNode, self).__init__() + self._node = node + + def get_node(self): + return self._node + + def get_attr_name_target_u(self): + attr_name = maya.cmds.connectionInfo( + self._node + '.target[0].targetU', sourceFromDestination=True + ) + return attr_name + + def get_attr_name_target_v(self): + attr_name = maya.cmds.connectionInfo( + self._node + '.target[0].targetV', sourceFromDestination=True + ) + return attr_name + + def get_attr_name_target_weight(self): + attr_name = maya.cmds.connectionInfo( + self._node + '.target[0].targetWeight', sourceFromDestination=True + ) + return attr_name + + +def _create_point_on_poly_constraint(mesh_tfm, rivet_tfm): + constraint = maya.cmds.pointOnPolyConstraint(mesh_tfm, rivet_tfm, weight=1) + if len(constraint) == 0: + msg = 'Could not create point on poly constraint.' + LOG.error(msg) + return None + + constraint = constraint[0] + point_on_poly = PointOnPolyConstraintNode(constraint) + + _lock_constraint_offset_attrs(constraint, lock=True) + node_attr = constraint + '.target[0].targetUseNormal' + maya.cmds.setAttr(node_attr, 1) + + _lock_transform_attrs(rivet_tfm, lock=True) + + return point_on_poly + + +RivetPointOnPoly = collections.namedtuple( + 'RivetPointOnPoly', ['rivet_transform', 'rivet_shape', 'point_on_poly_constraint'] +) + + +def create( + mesh_transform, + mesh_shape, + name=None, + parent=None, + coordinate_uv=None, + in_position=None, +): + """ + Create a Point-on-Poly Rivet. + + :param name: Name of the Rivet to create. + :type name: None or str + + :param parent: The parent node of the newly created Rivet. + :type parent: None or str + + :param coordinate_uv: The default UV Coordinate of the newly created Rivet. + :type coordinate_uv: None or (float, float) + + :param in_position: The world-space position of the created Rivet, + if None, fall back to the default coordinate_uv. + :type in_position: None or (float, float, float) + + :returns: Rivet data structure containing all nodes for rivet. + :rtype: RivetPointOnPoly + """ + if name is None: + name = 'mmRivetPointOnPoly1' + assert isinstance(name, str) + assert parent is None or maya.cmds.objExists(parent) + assert in_position is None or ( + isinstance(in_position, (tuple, list)) and len(in_position) == 3 + ) + if coordinate_uv is None: + coordinate_uv = (0.5, 0.5) + assert isinstance(in_position, (tuple, list)) and len(coordinate_uv) == 2 + + rivet_tfm, rivet_shp = _create_locator(name, parent=parent) + + point_on_poly = _create_point_on_poly_constraint(mesh_transform, rivet_tfm) + assert point_on_poly is not None + + coordinate_u = coordinate_uv[0] + coordinate_v = coordinate_uv[1] + if in_position is not None: + nearest_point_data = nearestpointonmesh_utils.get_nearest_point_on_mesh( + mesh_shape, in_position + ) + coordinate_u = nearest_point_data.coords[0] + coordinate_v = nearest_point_data.coords[1] + + # Add attributes and create connections for locator control. + coord_u_attr_name = 'coordinateU' + coord_v_attr_name = 'coordinateV' + maya.cmds.addAttr( + rivet_tfm, + longName=coord_u_attr_name, + shortName='crdu', + niceName='U', + at='double', + defaultValue=coordinate_u, + keyable=True, + ) + maya.cmds.addAttr( + rivet_tfm, + longName=coord_v_attr_name, + shortName='crdv', + niceName='V', + at='double', + defaultValue=coordinate_v, + keyable=True, + ) + rivet_attr_name_u = point_on_poly.get_attr_name_target_u() + rivet_attr_name_v = point_on_poly.get_attr_name_target_v() + rivet_tfm_coord_u_attr = '{}.{}'.format(rivet_tfm, coord_u_attr_name) + rivet_tfm_coord_v_attr = '{}.{}'.format(rivet_tfm, coord_v_attr_name) + maya.cmds.connectAttr(rivet_tfm_coord_u_attr, rivet_attr_name_u) + maya.cmds.connectAttr(rivet_tfm_coord_v_attr, rivet_attr_name_v) + _lock_node_attrs( + point_on_poly.get_node(), [rivet_attr_name_u, rivet_attr_name_v], lock=True + ) + + rivet = RivetPointOnPoly( + rivet_transform=rivet_tfm, + rivet_shape=rivet_shp, + point_on_poly_constraint=point_on_poly, + ) + return rivet diff --git a/python/mmSolver/utils/selection.py b/python/mmSolver/utils/selection.py new file mode 100644 index 000000000..1d4508ed1 --- /dev/null +++ b/python/mmSolver/utils/selection.py @@ -0,0 +1,149 @@ +# Copyright (C) 2014, 2022, 2023 David Cattermole. +# +# This file is part of mmSolver. +# +# mmSolver is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mmSolver is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with mmSolver. If not, see . +# +""" +Selection utilities. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import maya.cmds +import maya.OpenMaya as OpenMaya + +import mmSolver.logger + + +LOG = mmSolver.logger.get_logger() + +# Magic numbers for the component masks in Maya. +_COMPONENT_MASK_POLYGON_VERTEX = 31 +_COMPONENT_MASK_POLYGON_EDGE = 32 +_COMPONENT_MASK_POLYGON_FACE = 34 + + +def filter_mesh_vertex_selection(selection): + component_selection = ( + maya.cmds.filterExpand( + selection, + expand=True, + fullPath=True, + selectionMask=_COMPONENT_MASK_POLYGON_VERTEX, + ) + or [] + ) + return component_selection + + +def filter_mesh_edge_selection(selection): + component_selection = ( + maya.cmds.filterExpand( + selection, + expand=True, + fullPath=True, + selectionMask=_COMPONENT_MASK_POLYGON_EDGE, + ) + or [] + ) + return component_selection + + +def filter_mesh_face_selection(selection): + component_selection = ( + maya.cmds.filterExpand( + selection, + expand=True, + fullPath=True, + selectionMask=_COMPONENT_MASK_POLYGON_FACE, + ) + or [] + ) + return component_selection + + +def get_mesh_vertex_selection(): + selection = maya.cmds.ls(selection=True, long=True) or [] + return filter_mesh_vertex_selection(selection) + + +def get_mesh_edge_selection(): + selection = maya.cmds.ls(selection=True, long=True) or [] + return filter_mesh_edge_selection(selection) + + +def get_mesh_face_selection(): + selection = maya.cmds.ls(selection=True, long=True) or [] + return filter_mesh_face_selection(selection) + + +def get_soft_selection_weights(only_node=None): + """ + Get the currently 'soft' selected components. + + Soft selection may return a list for multiple different nodes. + + If 'only_node' is given, then soft selections on only that node + will be returned, all else will be ignored. + """ + all_weights = [] + + soft_select_enabled = maya.cmds.softSelect(query=True, softSelectEnabled=True) + if not soft_select_enabled: + return all_weights + + rich_selection = OpenMaya.MRichSelection() + try: + # get currently active soft selection + OpenMaya.MGlobal.getRichSelection(rich_selection) + except RuntimeError as e: + LOG.error(str(e)) + LOG.error('Error getting soft selection.') + return all_weights + + rich_selection_list = OpenMaya.MSelectionList() + rich_selection.getSelection(rich_selection_list) + selection_count = rich_selection_list.length() + + for i in range(selection_count): + shape_dag_path = OpenMaya.MDagPath() + shape_component = OpenMaya.MObject() + try: + rich_selection_list.getDagPath(i, shape_dag_path, shape_component) + except RuntimeError: + continue + + if only_node is not None: + if shape_dag_path.fullPathName() != only_node: + continue + + # Get weight value. + component_weights = {} + component_fn = OpenMaya.MFnSingleIndexedComponent(shape_component) + try: + for i in range(component_fn.elementCount()): + weight = component_fn.weight(i) + component_weights[component_fn.element(i)] = weight.influence() + except RuntimeError as e: + LOG.error(str(e)) + short_name = shape_dag_path.partialPathName() + msg = 'Soft selection appears invalid, skipping for shape "%s".' + LOG.error(msg, short_name) + + all_weights.append(component_weights) + + return all_weights diff --git a/share/config/functions.json b/share/config/functions.json index 52ebd07e1..339f27b61 100644 --- a/share/config/functions.json +++ b/share/config/functions.json @@ -717,6 +717,15 @@ "mmSolver.tools.removecontroller2.tool.main();" ] }, + "create_rivet": { + "name": "Create Rivet", + "name_shelf": "Rivet", + "tooltip": "Create a new Rivet on the selected mesh components.", + "command": [ + "import mmSolver.tools.createrivet.tool;", + "mmSolver.tools.createrivet.tool.main();" + ] + }, "camera_object_scale_adjust": { "name": "Adjust Camera/Object Scale...", "name_shelf": "CrSclRig", diff --git a/share/config/menu.json b/share/config/menu.json index 392a972dc..7995dc3e1 100644 --- a/share/config/menu.json +++ b/share/config/menu.json @@ -102,6 +102,8 @@ "general_tools/create_world_controllers2", "general_tools/create_controller2", "general_tools/remove_controller2", + "general_tools/---Rivets", + "general_tools/create_rivet", "general_tools/---ML Tools", "general_tools/ml_convert_rotation_order", "general_tools/---Naming & Organisation", diff --git a/share/config/shelf.json b/share/config/shelf.json index d4aa995b3..0fba54125 100644 --- a/share/config/shelf.json +++ b/share/config/shelf.json @@ -99,6 +99,8 @@ "general_tools/gen_tools_popup/create_world_controllers2", "general_tools/gen_tools_popup/create_controller2", "general_tools/gen_tools_popup/remove_controller2", + "general_tools/gen_tools_popup/---Rivets", + "general_tools/gen_tools_popup/create_rivet", "general_tools/gen_tools_popup/---ML Tools", "general_tools/gen_tools_popup/ml_convert_rotation_order", "general_tools/gen_tools_popup/---Naming & Organisation", diff --git a/share/config/shelf_minimal.json b/share/config/shelf_minimal.json index 538808ef1..7b0fb7117 100644 --- a/share/config/shelf_minimal.json +++ b/share/config/shelf_minimal.json @@ -110,6 +110,8 @@ "general_tools/gen_tools_popup/create_world_controllers2", "general_tools/gen_tools_popup/create_controller2", "general_tools/gen_tools_popup/remove_controller2", + "general_tools/gen_tools_popup/---Rivets", + "general_tools/gen_tools_popup/create_rivet", "general_tools/gen_tools_popup/---ML Tools", "general_tools/gen_tools_popup/ml_convert_rotation_order", "general_tools/gen_tools_popup/---Naming & Organisation",