From b4caddd2f7814e96a5e7c3f9aebe517987448e61 Mon Sep 17 00:00:00 2001 From: David Cattermole Date: Sat, 30 Dec 2023 20:11:16 +1100 Subject: [PATCH] Add 'Surface Cluster' tool. This is based on my original "surfaceCluster.py" script from 2014, but re-written to be more modular and reusable and easier to understand. This new version has a lot of improvements, such as: - A tool to update the soft-selection weights for a surface cluster. - A UI enable/disable opening the paint-weights tool, after creation. - A tool to open the paint-weights tool for the selected surface cluster control node. - The ability to create multiple surface clusters from multiple selected mesh components. GitHub issue #111. --- docs/source/mmSolver.tools.rst | 1 + docs/source/mmSolver.tools.surfacecluster.rst | 42 ++ docs/source/tools_generaltools.rst | 118 ++++ python/CMakeLists.txt | 5 + .../mmSolver/tools/surfacecluster/constant.py | 42 ++ python/mmSolver/tools/surfacecluster/lib.py | 519 ++++++++++++++++++ python/mmSolver/tools/surfacecluster/tool.py | 282 ++++++++++ .../tools/surfacecluster/ui/__init__.py | 20 + .../ui/surfacecluster_layout.py | 97 ++++ .../ui/surfacecluster_layout.ui | 63 +++ .../ui/surfacecluster_window.py | 118 ++++ python/mmSolver/utils/rivet/meshtwoedge.py | 3 +- python/mmSolver/utils/rivet/pointonpoly.py | 58 +- python/mmSolver/utils/selection.py | 21 +- share/config/functions.json | 32 ++ share/config/menu.json | 4 + share/config/shelf.json | 4 + share/config/shelf_minimal.json | 4 + 18 files changed, 1411 insertions(+), 22 deletions(-) create mode 100644 docs/source/mmSolver.tools.surfacecluster.rst create mode 100644 python/mmSolver/tools/surfacecluster/constant.py create mode 100644 python/mmSolver/tools/surfacecluster/lib.py create mode 100644 python/mmSolver/tools/surfacecluster/tool.py create mode 100644 python/mmSolver/tools/surfacecluster/ui/__init__.py create mode 100644 python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.py create mode 100644 python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.ui create mode 100644 python/mmSolver/tools/surfacecluster/ui/surfacecluster_window.py diff --git a/docs/source/mmSolver.tools.rst b/docs/source/mmSolver.tools.rst index f24209d1..9dbd951c 100644 --- a/docs/source/mmSolver.tools.rst +++ b/docs/source/mmSolver.tools.rst @@ -65,6 +65,7 @@ mmSolver.tools mmSolver.tools.solver mmSolver.tools.sortoutlinernodes mmSolver.tools.subdivideline + mmSolver.tools.surfacecluster mmSolver.tools.sysinfowindow mmSolver.tools.togglebundlelock mmSolver.tools.togglecameradistort diff --git a/docs/source/mmSolver.tools.surfacecluster.rst b/docs/source/mmSolver.tools.surfacecluster.rst new file mode 100644 index 00000000..55f402a8 --- /dev/null +++ b/docs/source/mmSolver.tools.surfacecluster.rst @@ -0,0 +1,42 @@ +============================= +mmSolver.tools.surfacecluster +============================= + +.. automodule:: mmSolver.tools.surfacecluster + :members: + :undoc-members: + +Tools ++++++ + +.. automodule:: mmSolver.tools.surfacecluster.tool + :members: + :undoc-members: + +Library ++++++++ + +.. automodule:: mmSolver.tools.surfacecluster.lib + :members: + :undoc-members: + +UI - Layout ++++++++++++ + +.. automodule:: mmSolver.tools.surfacecluster.ui.surfacecluster_layout + :members: + :undoc-members: + +UI - Window ++++++++++++ + +.. automodule:: mmSolver.tools.surfacecluster.ui.surfacecluster_window + :members: + :undoc-members: + +Constants ++++++++++ + +.. automodule:: mmSolver.tools.surfacecluster.constant + :members: + :undoc-members: diff --git a/docs/source/tools_generaltools.rst b/docs/source/tools_generaltools.rst index a342e224..7c13e705 100644 --- a/docs/source/tools_generaltools.rst +++ b/docs/source/tools_generaltools.rst @@ -276,6 +276,124 @@ To run the tool, use this Python command: .. _rivet.mel: https://www.highend3d.com/maya/script/rivet-button-for-maya +.. _create-rivet-tool-ref: + +Surface Cluster +--------------- + +A Surface Cluster is a "cluster" deformer that will be riveted to the +surface of a mesh object. All movement of the underlying surface is +inherited by the Surface Cluster, so the cluster "sits on" the +surface, even if the underlying surface is animated/deformed. + +Surface Clusters can be very helpful for subtly adjusting the +silhouette of an object, or adding a bulge, especially when the change +needs to be animated. + +.. note:: This old `Surface Cluster YouTube Video`_ shows the general + usage of the tool, however the tool has been re-written and + improved with features for editing the deforming weights. + +.. _Surface Cluster YouTube Video: + https://youtu.be/7SFP4TgVbEI + +Create Single Surface Cluster +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a Surface Cluster on the selected Mesh component. + +Usage: + +1) Select 1 or more components (vertices, edges, faces, etc). + +2) Run this tool. + + - create a single surface cluster at the average position of all selected + components. + + - (Optionally) Use current Soft Selection as default weighting - the + same as the "update_weights_with_soft_selection" tool. + +To run the tool, use this Python command: + +.. code:: python + + import mmSolver.tools.surfacecluster.tool as tool + tool.create_single_surface_cluster() + + # Open the UI window change settings before creation. + tool.open_window() + +Create Multiple Surface Cluster +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create multiple surface clusters, one for each component selected. + +Usage: + +1) Select 1 or more components (vertices, edges, faces, etc). + +2) Run this tool. + + - For each component, create a surface cluster is created. + +To run the tool, use this Python command: + +.. code:: python + + import mmSolver.tools.surfacecluster.tool as tool + tool.create_multiple_surface_clusters() + + # Open the UI window change settings before creation. + tool.open_window() + +Update Weights With Soft-Selection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Update the cluster deformer weights using the current component +soft-selection. + + +Usage: +1) Enable Soft Selection ('b' hotkey) + +2) Select 1 or more components (vertices, edges, faces, etc). + +3) Select surface cluster control. + +4) Run this tool. + + - The weights of the surface cluster are updated with the soft + selection. + +To run the tool, use this Python command: + +.. code:: python + + import mmSolver.tools.surfacecluster.tool as tool + tool.update_weights_with_soft_selection() + +Open Surface Cluster Paint Weights +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Open the paint weights tool for the selected surface cluster Control. + +Usage: +1) Select a surface cluster control. + +2) Run tool. + + - The paint weights tool is opened. + +3) User paints weights. + +To run the tool, use this Python command: + +.. code:: python + + import mmSolver.tools.surfacecluster.tool as tool + tool.open_paint_weights_tool() + .. _marker-bundle-rename-tool-ref: Marker Bundle Rename diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index ee6ef25f..01538bc2 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -189,6 +189,11 @@ if (MMSOLVER_BUILD_QT_UI) ${CMAKE_CURRENT_BINARY_DIR}/mmSolver/tools/screenspacerigbake/ui/ui_screenspacerigbake_layout.py ) + compile_qt_ui_to_python_file("surfacecluster" + ${CMAKE_CURRENT_SOURCE_DIR}/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.ui + ${CMAKE_CURRENT_BINARY_DIR}/mmSolver/tools/surfacecluster/ui/ui_surfacecluster_layout.py + ) + # Install generated Python UI files install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/" DESTINATION "${MODULE_FULL_NAME}/python" diff --git a/python/mmSolver/tools/surfacecluster/constant.py b/python/mmSolver/tools/surfacecluster/constant.py new file mode 100644 index 00000000..6c6b5399 --- /dev/null +++ b/python/mmSolver/tools/surfacecluster/constant.py @@ -0,0 +1,42 @@ +# 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 . +# +""" +Surface Cluster constants. +""" + +WINDOW_TITLE = 'Create Surface Cluster' + +CONFIG_CREATE_MODE_KEY = 'mmSolver_surfacecluster_createMode' +CONFIG_OPEN_PAINT_WEIGHTS_KEY = 'mmSolver_surfacecluster_openPaintWeights' + +CREATE_MODE_SINGLE_VALUE = 'single_surface_cluster' +CREATE_MODE_MULTIPLE_VALUE = 'multiple_surface_cluster' +CREATE_MODE_VALUES = [ + CREATE_MODE_SINGLE_VALUE, + CREATE_MODE_MULTIPLE_VALUE, +] + +CREATE_MODE_SINGLE_LABEL = 'Create Single Cluster On Components' +CREATE_MODE_MULTIPLE_LABEL = 'Create Multiple Clusters On Each Component' +CREATE_MODE_LABELS = [ + CREATE_MODE_SINGLE_LABEL, + CREATE_MODE_MULTIPLE_LABEL, +] + +DEFAULT_CREATE_MODE = CREATE_MODE_SINGLE_VALUE +DEFAULT_OPEN_PAINT_WEIGHTS = True diff --git a/python/mmSolver/tools/surfacecluster/lib.py b/python/mmSolver/tools/surfacecluster/lib.py new file mode 100644 index 00000000..445fe676 --- /dev/null +++ b/python/mmSolver/tools/surfacecluster/lib.py @@ -0,0 +1,519 @@ +# 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 . +# +""" +Functions to create and edit Surface Clusters. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections + +import maya.cmds +import maya.mel + +import mmSolver.logger +import mmSolver.utils.node as node_utils +import mmSolver.utils.python_compat as pycompat +import mmSolver.utils.rivet.nearestpointonmesh as nearestpointonmesh +import mmSolver.utils.rivet.pointonpoly as rivet_pointonpoly +import mmSolver.utils.selection as selection_utils + + +LOG = mmSolver.logger.get_logger() +OPEN_PAINT_EDITOR_MEL_CMD = ( + 'artSetToolAndSelectAttr("artAttrCtx", "cluster.{node}.weights");' + # Show Widow + 'toolPropertyWindow;' + 'toolPropertyShow;' + # Set weight slider to 1.0. + 'artAttrPaintOperation artAttrCtx Replace;' + 'artAttrCtx -e -value 1.0 `currentCtx`;' + 'artAttrValues artAttrContext;' +) + + +def _lock_node_attr(nodeAttr, lock=True, keyable=False, channelBox=False): + node = nodeAttr.split('.')[0] + attr = nodeAttr[len(node) + 1 :] + if node_utils.attribute_exists(attr, node): + return maya.cmds.setAttr( + nodeAttr, lock=lock, keyable=keyable, channelBox=channelBox + ) + return False + + +def _lock_node_attrs(node, attrs, lock=True, keyable=False, channelBox=False): + for attr in attrs: + if node_utils.attribute_exists(attr, node): + maya.cmds.setAttr( + node + '.' + attr, lock=lock, keyable=keyable, channelBox=channelBox + ) + pass + return True + + +def _lock_transform_attrs(node, lock=True, keyable=False, channelBox=False): + return _lock_node_attrs( + node, + ['tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz', 'shxy', 'shxz', 'shyz'], + lock=lock, + keyable=keyable, + channelBox=channelBox, + ) + + +def _lock_translate_attrs(node, lock=True, keyable=False, channelBox=False): + return _lock_node_attrs( + node, ['tx', 'ty', 'tz'], lock=lock, keyable=keyable, channelBox=channelBox + ) + + +def _lock_rotate_attrs(node, lock=True, keyable=False, channelBox=False): + return _lock_node_attrs( + node, ['rx', 'ry', 'rz'], lock=lock, keyable=keyable, channelBox=channelBox + ) + + +def _lock_scale_attrs(node, lock=True, keyable=False, channelBox=False): + return _lock_node_attrs( + node, ['sx', 'sy', 'sz'], lock=lock, keyable=keyable, channelBox=channelBox + ) + + +def _lock_shear_attrs(node, lock=True, keyable=False, channelBox=False): + return _lock_node_attrs( + node, + ['shxy', 'shxz', 'shyz'], + lock=lock, + keyable=keyable, + channelBox=channelBox, + ) + + +def _lock_constraint_offset_attrs(node, lock=True, keyable=False, channelBox=False): + return _lock_node_attrs( + node, + ['ox', 'oy', 'oz', 'otx', 'oty', 'otz', 'orx', 'ory', 'orz'], + lock=lock, + keyable=keyable, + channelBox=channelBox, + ) + + +def _reset_transform_values(node): + maya.cmds.xform(node, objectSpace=True, translation=(0.0, 0.0, 0.0)) + maya.cmds.xform(node, objectSpace=True, rotation=(0.0, 0.0, 0.0)) + maya.cmds.xform(node, objectSpace=True, scale=(1.0, 1.0, 1.0)) + maya.cmds.xform(node, objectSpace=True, shear=(0.0, 0.0, 0.0)) + maya.cmds.xform(node, objectSpace=True, shear=(0.0, 0.0, 0.0)) + maya.cmds.xform(node, objectSpace=True, pivots=(0.0, 0.0, 0.0)) + return True + + +def _create_constraint_to( + targetTransform, + objectTransform, + lockAttrs=True, + lock=True, + keyable=False, + channelBox=False, +): + constPoint = maya.cmds.pointConstraint(targetTransform, objectTransform)[0] + constOrient = maya.cmds.orientConstraint(targetTransform, objectTransform)[0] + constScale = maya.cmds.scaleConstraint(targetTransform, objectTransform)[0] + if lockAttrs: + _lock_constraint_offset_attrs( + constPoint, lock=lock, keyable=keyable, channelBox=channelBox + ) + _lock_constraint_offset_attrs( + constOrient, lock=lock, keyable=keyable, channelBox=channelBox + ) + _lock_constraint_offset_attrs( + constScale, lock=lock, keyable=keyable, channelBox=channelBox + ) + return constPoint, constOrient, constScale + + +def _create_transform(name=None, parent=None): + assert isinstance(name, pycompat.TEXT_TYPE) + node = maya.cmds.createNode("transform", name=name, parent=parent) + node = node_utils.get_long_name(node) + return node + + +def _create_locator(name=None, parent=None): + assert isinstance(name, pycompat.TEXT_TYPE) + 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 _find_existing_rivet_shape(mesh_shape): + """ + Find a mesh shape node with the name 'RivetShape' that has + previously been created and can be reused. + + :returns: Mesh shape node, or None. + :rtype: None or str + """ + rivet_shape = None + nodes = maya.cmds.listHistory(mesh_shape, allConnections=True, future=False) or [] + for node in nodes: + conns = ( + maya.cmds.listConnections( + node, shapes=True, connections=False, destination=True + ) + or [] + ) + for conn in conns: + if conn.find('RivetShape') != -1: + rivet_shape = conn + break + return rivet_shape + + +def _create_rivet_shape(mesh_transform, mesh_shape): + """ + Creates a new 'RivetShape' mesh shape node on the mesh given. + """ + # Find attribute. + original_src_attr = maya.cmds.connectionInfo( + mesh_shape + '.inMesh', sourceFromDestination=True + ) + if len(original_src_attr) == 0: + attr = mesh_shape + '.inMesh' + LOG.error('No incoming connection to mesh "%s".', attr) + return None + + # If we could not find a mesh shape, make one. + duplicate_transform = maya.cmds.duplicate(mesh_shape, returnRootsOnly=True)[0] + duplicate_shapes = maya.cmds.listRelatives( + duplicate_transform, shapes=True, noIntermediate=True, fullPath=True + ) + duplicate_shape = duplicate_shapes[0] + parented_shape = maya.cmds.parent( + duplicate_shape, mesh_transform, addObject=True, shape=True + )[0] + maya.cmds.delete(duplicate_transform) + rivet_shape = maya.cmds.rename(parented_shape, mesh_transform + 'RivetShape') + + # Connect up to previous node. + maya.cmds.connectAttr(original_src_attr, rivet_shape + '.inMesh', force=True) + return rivet_shape + + +def _setup_control_attrs( + control_tfm, cluster_deformer, point_poly_rivet_transform, uv_coordinate +): + """ + Add attributes and create connections for locator control. + """ + assert maya.cmds.objExists(control_tfm) + assert maya.cmds.objExists(cluster_deformer) + assert maya.cmds.objExists(point_poly_rivet_transform) + assert len(uv_coordinate) >= 2 + + maya.cmds.addAttr( + control_tfm, + longName='deformerWeight', + shortName='dfmwgt', + at='double', + defaultValue=1.0, + keyable=True, + ) + maya.cmds.addAttr( + control_tfm, + longName='coordinateU', + shortName='crdu', + niceName='U', + at='double', + defaultValue=uv_coordinate[0], + keyable=True, + ) + maya.cmds.addAttr( + control_tfm, + longName='coordinateV', + shortName='crdv', + niceName='V', + at='double', + defaultValue=uv_coordinate[1], + keyable=True, + ) + + rivet_attr_u = point_poly_rivet_transform + '.coordinateU' + rivet_attr_v = point_poly_rivet_transform + '.coordinateV' + rivet_attr_weight = point_poly_rivet_transform + '.coordinateWeight' + maya.cmds.connectAttr( + control_tfm + '.deformerWeight', cluster_deformer + '.envelope' + ) + maya.cmds.connectAttr(control_tfm + '.coordinateU', rivet_attr_u) + maya.cmds.connectAttr(control_tfm + '.coordinateV', rivet_attr_v) + + _lock_node_attr(rivet_attr_u) + _lock_node_attr(rivet_attr_v) + _lock_node_attr(rivet_attr_weight) + return + + +def paint_cluster_weights_on_mesh(mesh_tfm, cluster_shp): + """ + Opens the Paint Editor on a mesh node to allow user painting + of weights. + """ + maya.cmds.select(mesh_tfm, replace=True) + mel_cmd = OPEN_PAINT_EDITOR_MEL_CMD.format(node=cluster_shp) + maya.mel.eval(mel_cmd) + return + + +SurfaceCluster = collections.namedtuple( + 'SurfaceCluster', + ['control_transform', 'mesh_transform', 'mesh_shape', 'cluster_deformer_node'], +) + + +def get_surface_cluster_from_control_transform(control_tfm): + assert maya.cmds.objExists(control_tfm) + assert maya.cmds.nodeType(control_tfm) == 'transform' + + cluster_deformers = ( + maya.cmds.listConnections( + control_tfm, + destination=True, + source=False, + skipConversionNodes=True, + shapes=True, + type='cluster', + exactType=True, + ) + or [] + ) + if len(cluster_deformers) == 0: + LOG.error( + 'Could not find Cluster Deformer node from Control transform: %r', + control_tfm, + ) + return + + mesh_shps = maya.cmds.listHistory(cluster_deformers[0], future=True) or [] + mesh_shps = [ + node_utils.get_long_name(x) + for x in mesh_shps + if maya.cmds.nodeType(x) == 'mesh' + ] + if len(mesh_shps) == 0: + LOG.error( + 'Could not find Mesh shape node from Control transform: %r', control_tfm + ) + return + + mesh_tfms = ( + maya.cmds.listRelatives( + mesh_shps[0], parent=True, type='transform', fullPath=True + ) + or [] + ) + if len(mesh_tfms) == 0: + LOG.error( + 'Could not find Mesh transform node from Control transform: %r', control_tfm + ) + return + + return SurfaceCluster( + control_transform=control_tfm, + mesh_transform=mesh_tfms[0], + mesh_shape=mesh_shps[0], + cluster_deformer_node=cluster_deformers[0], + ) + + +def create_surface_cluster_on_mesh_and_component( + mesh_tfm, mesh_shp, component, uv_coordinate +): + """ + Create a Surface Cluster on a mesh component (face, edge or + vertex), positioned at a UV coordinate on the mesh_shp. + + :rtype: None or SurfaceCluster + """ + assert len(uv_coordinate) >= 2 + + # Group all nodes under a single group. + main_group = _create_transform(name='mmSurfaceCluster1') + _lock_transform_attrs(main_group) + + # Duplicate mesh shape node, keep all incoming connections. + rivet_shp = _find_existing_rivet_shape(mesh_shp) + if rivet_shp is None: + rivet_shp = _create_rivet_shape(mesh_tfm, mesh_shp) + if rivet_shp is None: + LOG.error('Could not create rivet shape.') + return None + + # Set non-rivet mesh as intermediate, and set rivet mesh as normal. + maya.cmds.setAttr(rivet_shp + '.intermediateObject', 0) + maya.cmds.setAttr(mesh_shp + '.intermediateObject', 1) + + point_on_poly = rivet_pointonpoly.create( + mesh_tfm, + mesh_shp, + parent=main_group, + as_locator=False, + uv_coordinate=uv_coordinate, + ) + if point_on_poly is None: + LOG.error('Could not create point on poly constraint.') + return None + rivet_tfm = point_on_poly.rivet_transform + + # Set rivet mesh as intermediate, and set non-rivet mesh as normal. + maya.cmds.setAttr(rivet_shp + '.intermediateObject', 1) + maya.cmds.setAttr(mesh_shp + '.intermediateObject', 0) + + # Create locator controller. + control_tfm, _control_shp = _create_locator(name='mmControl1', parent=rivet_tfm) + _reset_transform_values(control_tfm) + + # Create cluster on mesh. + cluster_deformer, cluster_handle = maya.cmds.cluster( + mesh_tfm, name='mmSurfaceCluster1' + ) + maya.cmds.setAttr(cluster_deformer + '.relative', 1) + maya.cmds.setAttr(cluster_handle + '.visibility', 0) + cluster_handle = node_utils.get_long_name( + maya.cmds.parent(cluster_handle, rivet_tfm)[0] + ) + maya.cmds.disconnectAttr( + cluster_handle + '.worldMatrix[0]', cluster_deformer + '.matrix' + ) + _create_constraint_to(control_tfm, cluster_handle) + _reset_transform_values(cluster_handle) + _lock_transform_attrs(cluster_handle) + + # Create Object Null, + object_null = _create_transform(name='objectNull1', parent=main_group) + _create_constraint_to(mesh_tfm, object_null) + _lock_transform_attrs(object_null) + + # Create Object Rivet Nulls. + object_rivet_null = _create_transform(name='objectRivet1', parent=object_null) + _create_constraint_to(rivet_tfm, object_rivet_null) + _lock_transform_attrs(object_rivet_null) + + # Connect GeomMatrix Cluster attribute. + maya.cmds.connectAttr( + object_rivet_null + '.inverseMatrix', + cluster_deformer + '.geomMatrix[0]', + force=True, + ) + + _setup_control_attrs( + control_tfm, + cluster_deformer, + point_on_poly.rivet_transform, + uv_coordinate, + ) + + return SurfaceCluster(control_tfm, mesh_tfm, mesh_shp, cluster_deformer) + + +def set_cluster_deformer_weights(cluster_shp, mesh_shp, weights): + assert maya.cmds.objExists(cluster_shp) + assert maya.cmds.objExists(mesh_shp) + assert maya.cmds.nodeType(cluster_shp) == 'cluster' + assert maya.cmds.nodeType(mesh_shp) == 'mesh' + assert isinstance(weights, dict) + vertex_count = maya.cmds.polyEvaluate(mesh_shp, vertex=True) + for i in range(vertex_count): + weights_attr = '{}.weightList[0].weights[{}]'.format(cluster_shp, i) + if i in weights: + maya.cmds.setAttr(weights_attr, weights[i]) + else: + maya.cmds.setAttr(weights_attr, 0.0) + return + + +def create_surface_cluster_on_component(component): + """ + Create a Surface Cluster on a mesh component (face, edge or + vertex). + + :rtype: None or SurfaceCluster + """ + # Get selection for mesh and locators. + meshes = maya.cmds.listRelatives(component, parent=True, fullPath=True) or [] + if len(meshes) == 0: + msg = 'Could not find mesh shape; component=%r meshes=%r.' + LOG.warn(msg, component, meshes) + return + mesh = meshes[0] + + # Get the mesh transform and shape. + mesh_tfm = None + mesh_shp = None + tmp = maya.cmds.listRelatives(mesh, parent=True) + if len(tmp) > 0: + mesh_tfm = tmp[0] + mesh_shp = mesh + else: + LOG.warn( + 'Mesh transform and shape could not be found' + ' for surface cluster; component=%r', + component, + ) + return + + sel = maya.cmds.ls(selection=True, long=True) or [] + try: + # The weights are calculated for the single component only. + maya.cmds.select(component, replace=True) + soft_selection_weights = selection_utils.get_soft_selection_weights( + only_shape_node=mesh_shp + ) + if len(soft_selection_weights) > 0: + soft_selection_weights = soft_selection_weights[0] + + # Get position from the component. + in_position = maya.cmds.xform( + component, query=True, worldSpace=True, translation=True + ) + + point_data = nearestpointonmesh.get_nearest_point_on_mesh(mesh_shp, in_position) + uv_coordinate = point_data.coords + + surface_cluster = create_surface_cluster_on_mesh_and_component( + mesh_tfm, mesh_shp, component, uv_coordinate + ) + if surface_cluster is None: + LOG.error('Could not create surface cluster!') + return + + if len(soft_selection_weights) > 0: + set_cluster_deformer_weights( + surface_cluster.cluster_deformer_node, mesh_shp, soft_selection_weights + ) + finally: + maya.cmds.select(sel, replace=True) + + return surface_cluster diff --git a/python/mmSolver/tools/surfacecluster/tool.py b/python/mmSolver/tools/surfacecluster/tool.py new file mode 100644 index 00000000..9d255338 --- /dev/null +++ b/python/mmSolver/tools/surfacecluster/tool.py @@ -0,0 +1,282 @@ +# 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 (and Edit) Surface Clusters. + +Surface Clusters can be used to deform a surface, starting from the +surface position and orientation. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import maya.cmds +import maya.mel + +import mmSolver.logger +import mmSolver.tools.surfacecluster.constant as const +import mmSolver.tools.surfacecluster.lib as lib +import mmSolver.utils.configmaya as configmaya +import mmSolver.utils.selection as selection_utils + + +LOG = mmSolver.logger.get_logger() + + +def create_single_surface_cluster(): + """ + Create a Surface Cluster on the selected Mesh component. + + Usage: + 1) Select 1 or more components (vertices, edges, faces, etc). + 2) Run this tool. + - create a single surface cluster at the average position of all selected + components. + - (Optionally) Use current Soft Selection as default weighting - the + same as the "update_weights_with_soft_selection" tool. + + :returns: None or a SurfaceCluster object with the nodes making up + the setup. + :rtype: SurfaceCluster or None + """ + # Find the selected vertex/edge/face components. + verts = selection_utils.get_mesh_vertex_selection() + edges = selection_utils.get_mesh_edge_selection() + faces = selection_utils.get_mesh_face_selection() + component = None + if (verts is not None) and (len(verts) == 1): + component = verts[0] + elif (edges is not None) and (len(edges) == 1): + component = edges[0] + elif (faces is not None) and (len(faces) == 1): + component = faces[0] + else: + LOG.error( + 'Must have only one vertex, edge or face selected ' + '(and optionally a soft selection).' + ) + return + + surface_cluster = lib.create_surface_cluster_on_component(component) + if surface_cluster is None: + LOG.error('Could not create surface cluster!') + return + + maya.cmds.select(surface_cluster.control_transform, replace=True) + return surface_cluster + + +def create_multiple_surface_clusters(): + """ + Create multiple surface clusters, one for each component + selected. + + Usage: + 1) Select 1 or more components (vertices, edges, faces, etc). + 2) Run this tool. + - For each component, create a surface cluster is created. + + :returns: A list of SurfaceCluster objects with the nodes making up + the setup. The list may be empty. + :rtype: [SurfaceCluster, ..] or [] + """ + # Find the selected vertex/edge/face components. + verts = selection_utils.get_mesh_vertex_selection() + edges = selection_utils.get_mesh_edge_selection() + faces = selection_utils.get_mesh_face_selection() + components = [] + if (verts is not None) and (len(verts) > 0): + components += verts + elif (edges is not None) and (len(edges) > 0): + components += edges + elif (faces is not None) and (len(faces) > 0): + components += faces + else: + LOG.error('Please select at least one Mesh vertex, edge or face.') + return + + surface_clusters = [] + for component in components: + surface_cluster = lib.create_surface_cluster_on_component(component) + if isinstance(surface_cluster, lib.SurfaceCluster): + surface_clusters.append(surface_cluster) + + control_tfms = [x.control_transform for x in surface_clusters] + if len(control_tfms) > 0: + maya.cmds.select(control_tfms, replace=True) + return surface_clusters + + +def update_weights_with_soft_selection(): + """ + Update the cluster deformer weights using the current + component soft-selection. + + Usage: + 1) Enable Soft Selection. + 2) Select 1 or more components (vertices, edges, faces, etc). + 3) Select surface cluster control. + 4) Run this tool. + - The weights of the surface cluster are updated with the soft + selection. + + :rtype: None + """ + sel = maya.cmds.ls(selection=True, long=True) or [] + tfms = [x for x in sel if maya.cmds.nodeType(x) == 'transform'] + if len(sel) == 0 or len(tfms) != 1: + LOG.error( + 'Please select 1 Surface Cluster Control ' + 'and some mesh components (with soft-selection enabled).' + ) + return + control_tfm = tfms[0] + + # Ensure soft-selection is enabled, if not error. + soft_select_enabled = maya.cmds.softSelect(query=True, softSelectEnabled=True) + if soft_select_enabled is False: + LOG.warn( + 'Soft-Selection is not enabled, ' + 'please enable soft-selection to update surface cluster weights.' + ) + return + + # Get the selected surface cluster transform. + surface_cluster = lib.get_surface_cluster_from_control_transform(control_tfm) + if surface_cluster is None: + LOG.error( + 'Could not find Surface Cluster from Control transform: %r', control_tfm + ) + return + + # Get the selected vertex/edge/face components. + verts = selection_utils.get_mesh_vertex_selection() + edges = selection_utils.get_mesh_edge_selection() + faces = selection_utils.get_mesh_face_selection() + components = [] + if (verts is not None) and (len(verts) > 0): + components += verts + elif (edges is not None) and (len(edges) > 0): + components += edges + elif (faces is not None) and (len(faces) > 0): + components += faces + else: + LOG.error('Please select at least one Mesh vertex, edge or face.') + return + + try: + # Update the surface cluster weights with the soft-selection + # weights. + maya.cmds.select(components, replace=True) + soft_selection_weights = selection_utils.get_soft_selection_weights( + only_shape_node=surface_cluster.mesh_shape + ) + if len(soft_selection_weights) > 0: + soft_selection_weights = soft_selection_weights[0] + lib.set_cluster_deformer_weights( + surface_cluster.cluster_deformer_node, + surface_cluster.mesh_shape, + soft_selection_weights, + ) + finally: + maya.cmds.select(sel, replace=True) + return + + +def open_paint_weights_tool(): + """ + Open the paint weights tool for the selected surface cluster. + + Usage: + 1) Select a surface cluster control. + 2) Run tool. + - The paint weights tool is opened. + 3) User paints weights. + + :rtype: None + """ + sel = maya.cmds.ls(selection=True, long=True) or [] + tfms = [x for x in sel if maya.cmds.nodeType(x) == 'transform'] + if len(sel) == 0 or len(tfms) != 1: + LOG.error('Please select only 1 Surface Cluster Control.') + return + control_tfm = tfms[0] + + # Get the selected surface cluster transform. + surface_cluster = lib.get_surface_cluster_from_control_transform(control_tfm) + if surface_cluster is None: + LOG.error( + 'Could not find Surface Cluster from Control transform: %r', control_tfm + ) + return + + # Open paint weights tool. + lib.paint_cluster_weights_on_mesh( + surface_cluster.mesh_transform, surface_cluster.cluster_deformer_node + ) + return + + +def main(): + """ + Create surface clusters using the values defined in the UI. + + Use open_window() function to open the UI to edit the values as + needed. + + :rtype: None + """ + name = const.CONFIG_CREATE_MODE_KEY + create_mode = configmaya.get_scene_option(name, default=const.DEFAULT_CREATE_MODE) + LOG.debug('key=%r value=%r', name, create_mode) + + name = const.CONFIG_OPEN_PAINT_WEIGHTS_KEY + open_paint_weights = configmaya.get_scene_option( + name, default=const.DEFAULT_OPEN_PAINT_WEIGHTS + ) + LOG.debug('key=%r value=%r', name, open_paint_weights) + assert isinstance(open_paint_weights, bool) + + if create_mode == const.CREATE_MODE_SINGLE_VALUE: + surface_cluster = create_single_surface_cluster() + if surface_cluster is not None and open_paint_weights is True: + lib.paint_cluster_weights_on_mesh( + surface_cluster.mesh_transform, surface_cluster.cluster_deformer_node + ) + elif create_mode == const.CREATE_MODE_MULTIPLE_VALUE: + surface_clusters = create_multiple_surface_clusters() + if len(surface_clusters) > 0 and open_paint_weights is True: + surface_cluster = surface_cluster[0] + lib.paint_cluster_weights_on_mesh( + surface_cluster.mesh_transform, surface_cluster.cluster_deformer_node + ) + else: + LOG.error("Surface Cluster Create Mode is invalid; %r", create_mode) + return + + +def open_window(): + """ + Open the Surface Cluster UI. + + :rtype: None + """ + import mmSolver.tools.surfacecluster.ui.surfacecluster_window as window + + window.main() diff --git a/python/mmSolver/tools/surfacecluster/ui/__init__.py b/python/mmSolver/tools/surfacecluster/ui/__init__.py new file mode 100644 index 00000000..367d7b68 --- /dev/null +++ b/python/mmSolver/tools/surfacecluster/ui/__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 . +# +""" +Surface Cluster user interface. +""" diff --git a/python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.py b/python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.py new file mode 100644 index 00000000..c0a84618 --- /dev/null +++ b/python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.py @@ -0,0 +1,97 @@ +# 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 . +# +""" +The main component of the user interface for the surface cluster +window. +""" + +import mmSolver.ui.qtpyutils as qtpyutils + +qtpyutils.override_binding_order() + +import mmSolver.ui.Qt.QtWidgets as QtWidgets + +import mmSolver.logger +import mmSolver.tools.surfacecluster.ui.ui_surfacecluster_layout as ui_layout +import mmSolver.tools.surfacecluster.constant as const +import mmSolver.utils.configmaya as configmaya +import mmSolver.utils.constant as utils_const + + +LOG = mmSolver.logger.get_logger() + + +class SurfaceClusterLayout(QtWidgets.QWidget, ui_layout.Ui_Form): + def __init__(self, parent=None, *args, **kwargs): + super(SurfaceClusterLayout, self).__init__(*args, **kwargs) + self.setupUi(self) + + # Create Mode + self.createModeComboBox.addItems(const.CREATE_MODE_LABELS) + self.createModeComboBox.currentIndexChanged.connect(self.createModeIndexChanged) + + # Open_Paint_Weights + self.openPaintWeightsCheckBox.stateChanged.connect( + self.openPaintWeightsCheckedChanged + ) + + # Populate the UI with data + self.populateUi() + + def createModeIndexChanged(self, index): + name = const.CONFIG_CREATE_MODE_KEY + value = const.CREATE_MODE_VALUES[index] + configmaya.set_scene_option(name, value, add_attr=True) + LOG.debug('key=%r value=%r', name, value) + + def openPaintWeightsCheckedChanged(self, value): + name = const.CONFIG_OPEN_PAINT_WEIGHTS_KEY + value = bool(value) + configmaya.set_scene_option(name, value, add_attr=True) + LOG.debug('key=%r value=%r', name, value) + + def reset_options(self): + name = const.CONFIG_CREATE_MODE_KEY + value = const.DEFAULT_CREATE_MODE + configmaya.set_scene_option(name, value) + LOG.debug('key=%r value=%r', name, value) + + name = const.CONFIG_OPEN_PAINT_WEIGHTS_KEY + value = const.DEFAULT_OPEN_PAINT_WEIGHTS + configmaya.set_scene_option(name, value) + LOG.debug('key=%r value=%r', name, value) + self.populateUi() + + def populateUi(self): + """ + Update the UI for the first time the class is created. + """ + name = const.CONFIG_CREATE_MODE_KEY + value = configmaya.get_scene_option(name, default=const.DEFAULT_CREATE_MODE) + value_index = const.CREATE_MODE_VALUES.index(value) + label = const.CREATE_MODE_LABELS[value_index] + LOG.debug('key=%r value=%r', name, value) + self.createModeComboBox.setCurrentText(label) + + name = const.CONFIG_OPEN_PAINT_WEIGHTS_KEY + value = configmaya.get_scene_option( + name, default=const.DEFAULT_OPEN_PAINT_WEIGHTS + ) + LOG.debug('key=%r value=%r', name, value) + self.openPaintWeightsCheckBox.setChecked(value) + return diff --git a/python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.ui b/python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.ui new file mode 100644 index 00000000..1d5f52d0 --- /dev/null +++ b/python/mmSolver/tools/surfacecluster/ui/surfacecluster_layout.ui @@ -0,0 +1,63 @@ + + + Form + + + + 0 + 0 + 374 + 174 + + + + Form + + + + + + Options + + + + + + Create Mode + + + + + + + + + + Open Paint Weights + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/python/mmSolver/tools/surfacecluster/ui/surfacecluster_window.py b/python/mmSolver/tools/surfacecluster/ui/surfacecluster_window.py new file mode 100644 index 00000000..1608bc87 --- /dev/null +++ b/python/mmSolver/tools/surfacecluster/ui/surfacecluster_window.py @@ -0,0 +1,118 @@ +# 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 . +# +""" +Window for the Surface Cluster tool. + +Usage:: + + import mmSolver.tools.surfacecluster.ui.surfacecluster_window as window + window.main() + +""" + +import mmSolver.ui.qtpyutils as qtpyutils + +qtpyutils.override_binding_order() + +import mmSolver.ui.Qt.QtCore as QtCore +import mmSolver.ui.Qt.QtWidgets as QtWidgets + +import mmSolver.logger +import mmSolver.ui.uiutils as uiutils +import mmSolver.ui.helputils as helputils +import mmSolver.ui.commonmenus as commonmenus +import mmSolver.tools.surfacecluster.constant as const +import mmSolver.tools.surfacecluster.tool as tool +import mmSolver.tools.surfacecluster.ui.surfacecluster_layout as surfacecluster_layout + + +LOG = mmSolver.logger.get_logger() +baseModule, BaseWindow = uiutils.getBaseWindow() + + +def _open_help(): + src = helputils.get_help_source() + page = 'tools_generaltools.html#surface-cluster' + helputils.open_help_in_browser(page=page, help_source=src) + return + + +class SurfaceClusterWindow(BaseWindow): + name = 'SurfaceClusterWindow' + + def __init__(self, parent=None, name=None): + super(SurfaceClusterWindow, self).__init__(parent, name=name) + self.setupUi(self) + self.addSubForm(surfacecluster_layout.SurfaceClusterLayout) + + self.setWindowTitle(const.WINDOW_TITLE) + self.setWindowFlags(QtCore.Qt.Tool) + + # Standard Buttons + self.baseHideStandardButtons() + self.applyBtn.show() + self.closeBtn.show() + self.applyBtn.setText('Create') + + self.applyBtn.clicked.connect(tool.main) + + # Hide irrelevant stuff + self.baseHideProgressBar() + + self.add_menus(self.menubar) + self.menubar.show() + + def add_menus(self, menubar): + edit_menu = QtWidgets.QMenu('Edit', menubar) + commonmenus.create_edit_menu_items( + edit_menu, reset_settings_func=self.reset_options + ) + menubar.addMenu(edit_menu) + + help_menu = QtWidgets.QMenu('Help', menubar) + commonmenus.create_help_menu_items(help_menu, tool_help_func=_open_help) + menubar.addMenu(help_menu) + + def reset_options(self): + form = self.getSubForm() + form.reset_options() + return + + +def main(show=True, auto_raise=True, delete=False): + """ + Open the Surface Cluster UI window. + + :param show: Show the UI. + :type show: bool + + :param auto_raise: If the UI is open, raise it to the front? + :type auto_raise: bool + + :param delete: Delete the existing UI and rebuild it? Helpful when + developing the UI in Maya script editor. + :type delete: bool + + :returns: A new solver window, or None if the window cannot be + opened. + :rtype: SolverWindow or None. + """ + win = SurfaceClusterWindow.open_window( + show=show, auto_raise=auto_raise, delete=delete + ) + return win diff --git a/python/mmSolver/utils/rivet/meshtwoedge.py b/python/mmSolver/utils/rivet/meshtwoedge.py index 547d5402..c6e516a2 100644 --- a/python/mmSolver/utils/rivet/meshtwoedge.py +++ b/python/mmSolver/utils/rivet/meshtwoedge.py @@ -35,6 +35,7 @@ import maya.cmds import mmSolver.logger +import mmSolver.utils.python_compat as pycompat LOG = mmSolver.logger.get_logger() @@ -75,7 +76,7 @@ def create(mesh_shape, edge_a, edge_b, name=None): assert maya.cmds.objExists(edge_b) if name is None: name = 'mmRivetMeshTwoEdge1' - assert isinstance(name, str) + assert isinstance(name, pycompat.TEXT_TYPE) edge_index_a = edge_a.split('[')[-1] edge_index_b = edge_b.split('[')[-1] diff --git a/python/mmSolver/utils/rivet/pointonpoly.py b/python/mmSolver/utils/rivet/pointonpoly.py index 73e4dde7..b622fa99 100644 --- a/python/mmSolver/utils/rivet/pointonpoly.py +++ b/python/mmSolver/utils/rivet/pointonpoly.py @@ -30,10 +30,18 @@ import mmSolver.utils.node as node_utils import mmSolver.utils.rivet.nearestpointonmesh as nearestpointonmesh_utils import mmSolver.logger +import mmSolver.utils.python_compat as pycompat LOG = mmSolver.logger.get_logger() +def _create_transform(name=None, parent=None): + assert isinstance(name, pycompat.TEXT_TYPE) + node = maya.cmds.createNode("transform", name=name, parent=parent) + node = node_utils.get_long_name(node) + return node + + def _create_locator(name, parent=None): tfm = maya.cmds.createNode('transform', name=name, parent=parent) tfm = node_utils.get_long_name(tfm) @@ -147,7 +155,8 @@ def create( mesh_shape, name=None, parent=None, - coordinate_uv=None, + as_locator=None, + uv_coordinate=None, in_position=None, ): """ @@ -159,11 +168,15 @@ def create( :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 as_locator: Should the Point-on-Poly Rivet be created as a + locator, or just a transform? By default it a locator. + :type as_locator: bool + + :param uv_coordinate: The default UV Coordinate of the newly created Rivet. + :type uv_coordinate: None or (float, float) :param in_position: The world-space position of the created Rivet, - if None, fall back to the default coordinate_uv. + if None, fall back to the default uv_coordinate. :type in_position: None or (float, float, float) :returns: Rivet data structure containing all nodes for rivet. @@ -171,22 +184,29 @@ def create( """ if name is None: name = 'mmRivetPointOnPoly1' - assert isinstance(name, str) + if as_locator is None: + as_locator = True + assert isinstance(name, pycompat.TEXT_TYPE) + assert isinstance(as_locator, bool) 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 + if uv_coordinate is None: + uv_coordinate = (0.5, 0.5) + assert isinstance(uv_coordinate, (tuple, list)) and len(uv_coordinate) == 2 - rivet_tfm, rivet_shp = _create_locator(name, parent=parent) + if as_locator is True: + rivet_tfm, rivet_shp = _create_locator(name, parent=parent) + else: + rivet_tfm = _create_transform(name, parent=parent) + rivet_shp = None 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] + coordinate_u = uv_coordinate[0] + coordinate_v = uv_coordinate[1] if in_position is not None: nearest_point_data = nearestpointonmesh_utils.get_nearest_point_on_mesh( mesh_shape, in_position @@ -197,6 +217,7 @@ def create( # Add attributes and create connections for locator control. coord_u_attr_name = 'coordinateU' coord_v_attr_name = 'coordinateV' + coord_w_attr_name = 'coordinateWeight' maya.cmds.addAttr( rivet_tfm, longName=coord_u_attr_name, @@ -215,14 +236,27 @@ def create( defaultValue=coordinate_v, keyable=True, ) + maya.cmds.addAttr( + rivet_tfm, + longName=coord_w_attr_name, + shortName='crdwgt', + at='double', + defaultValue=1.0, + 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_attr_name_w = point_on_poly.get_attr_name_target_weight() rivet_tfm_coord_u_attr = '{}.{}'.format(rivet_tfm, coord_u_attr_name) rivet_tfm_coord_v_attr = '{}.{}'.format(rivet_tfm, coord_v_attr_name) + rivet_tfm_coord_w_attr = '{}.{}'.format(rivet_tfm, coord_w_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) + maya.cmds.connectAttr(rivet_tfm_coord_w_attr, rivet_attr_name_w) _lock_node_attrs( - point_on_poly.get_node(), [rivet_attr_name_u, rivet_attr_name_v], lock=True + point_on_poly.get_node(), + [rivet_attr_name_u, rivet_attr_name_v, rivet_attr_name_w], + lock=True, ) rivet = RivetPointOnPoly( diff --git a/python/mmSolver/utils/selection.py b/python/mmSolver/utils/selection.py index 1d4508ed..4a05da28 100644 --- a/python/mmSolver/utils/selection.py +++ b/python/mmSolver/utils/selection.py @@ -91,34 +91,37 @@ def get_mesh_face_selection(): return filter_mesh_face_selection(selection) -def get_soft_selection_weights(only_node=None): +def get_soft_selection_weights(only_shape_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 + If 'only_shape_node' is given, then soft selections on only that node will be returned, all else will be ignored. - """ - all_weights = [] + :returns: List of object mappings of soft selection vertex index + to weights, or an empty list. + :rtype: [] or [{int: float}, ..] + """ soft_select_enabled = maya.cmds.softSelect(query=True, softSelectEnabled=True) if not soft_select_enabled: - return all_weights + return [] rich_selection = OpenMaya.MRichSelection() try: - # get currently active soft selection + # 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 + return [] rich_selection_list = OpenMaya.MSelectionList() rich_selection.getSelection(rich_selection_list) selection_count = rich_selection_list.length() + all_weights = [] for i in range(selection_count): shape_dag_path = OpenMaya.MDagPath() shape_component = OpenMaya.MObject() @@ -127,8 +130,8 @@ def get_soft_selection_weights(only_node=None): except RuntimeError: continue - if only_node is not None: - if shape_dag_path.fullPathName() != only_node: + if only_shape_node is not None: + if shape_dag_path.fullPathName() != only_shape_node: continue # Get weight value. diff --git a/share/config/functions.json b/share/config/functions.json index 339f27b6..71c42796 100644 --- a/share/config/functions.json +++ b/share/config/functions.json @@ -726,6 +726,38 @@ "mmSolver.tools.createrivet.tool.main();" ] }, + "create_surface_cluster": { + "name": "Create Surface Cluster", + "name_shelf": "SrfClst", + "tooltip": "Create a new Surface Cluster on the selected mesh components.", + "command": [ + "import mmSolver.tools.surfacecluster.tool;", + "mmSolver.tools.surfacecluster.tool.main();" + ], + "option_box": true, + "command_option_box": [ + "import mmSolver.tools.surfacecluster.tool;", + "mmSolver.tools.surfacecluster.tool.open_window();" + ] + }, + "update_surface_cluster_weights_with_soft_selection": { + "name": "Update Surface Cluster Weights with Soft-Selection", + "name_shelf": "UpdWgts", + "tooltip": "", + "command": [ + "import mmSolver.tools.surfacecluster.tool;", + "mmSolver.tools.surfacecluster.tool.update_weights_with_soft_selection();" + ] + }, + "open_surface_cluster_paint_weights_tool": { + "name": "Open Cluster Paint Weights Tool...", + "name_shelf": "PntWgts", + "tooltip": "", + "command": [ + "import mmSolver.tools.surfacecluster.tool;", + "mmSolver.tools.surfacecluster.tool.open_paint_weights_tool();" + ] + }, "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 7995dc3e..c655ab6c 100644 --- a/share/config/menu.json +++ b/share/config/menu.json @@ -104,6 +104,10 @@ "general_tools/remove_controller2", "general_tools/---Rivets", "general_tools/create_rivet", + "general_tools/---Surface Cluster", + "general_tools/create_surface_cluster", + "general_tools/update_surface_cluster_weights_with_soft_selection", + "general_tools/open_surface_cluster_paint_weights_tool", "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 0fba5412..aa1b35c5 100644 --- a/share/config/shelf.json +++ b/share/config/shelf.json @@ -101,6 +101,10 @@ "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/---Surface Cluster", + "general_tools/gen_tools_popup/create_surface_cluster", + "general_tools/gen_tools_popup/update_surface_cluster_weights_with_soft_selection", + "general_tools/gen_tools_popup/open_surface_cluster_paint_weights_tool", "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 7b0fb711..c1218d33 100644 --- a/share/config/shelf_minimal.json +++ b/share/config/shelf_minimal.json @@ -112,6 +112,10 @@ "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/---Surface Cluster", + "general_tools/gen_tools_popup/create_surface_cluster", + "general_tools/gen_tools_popup/update_surface_cluster_weights_with_soft_selection", + "general_tools/gen_tools_popup/open_surface_cluster_paint_weights_tool", "general_tools/gen_tools_popup/---ML Tools", "general_tools/gen_tools_popup/ml_convert_rotation_order", "general_tools/gen_tools_popup/---Naming & Organisation",