This commit is contained in:
2025-11-24 08:27:50 +08:00
parent 6940f17517
commit e76152945e
104 changed files with 10003 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
from ngSkinTools2 import api, signal
from ngSkinTools2.api import Layer, PasteOperation
from ngSkinTools2.api.session import Session
def action_copy_cut(session, parent, cut):
"""
:type session: Session
:type parent: PySide2.QtWidgets.QWidget
:type cut: bool
"""
from ngSkinTools2.ui import actions
def cut_copy_callback():
if session.state.selectedSkinCluster is None:
return
if session.state.currentLayer.layer is None:
return
influences = session.state.currentLayer.layer.paint_targets
operation = api.copy_weights # type: Callable[[Layer, list], None]
if cut:
operation = api.cut_weights
operation(session.state.currentLayer.layer, influences)
operation_name = "Cut" if cut else "Copy"
result = actions.define_action(parent, operation_name + " weights to clipboard", callback=cut_copy_callback)
@signal.on(session.events.currentLayerChanged, session.events.currentInfluenceChanged, qtParent=parent)
def on_selection_changed():
layer = session.state.currentLayer.layer
result.setEnabled(layer is not None and len(layer.paint_targets) > 0)
return result
def action_paste(session, parent, operation):
"""
:type session: Session
:type parent: PySide2.QtWidgets.QWidget
:type cut: bool
"""
from ngSkinTools2.ui import actions
def paste_callback():
if session.state.currentLayer.layer is None:
return
influences = session.state.currentLayer.layer.paint_targets
api.paste_weights(session.state.currentLayer.layer, operation, influences=influences)
labels = {
PasteOperation.add: 'Paste weights (add to existing)',
PasteOperation.subtract: 'Paste weight (subtract from existing)',
PasteOperation.replace: 'Paste weights (replace existing)',
}
result = actions.define_action(parent, labels[operation], callback=paste_callback)
result.setToolTip("Paste previously copied weights from clipboard")
@signal.on(session.events.currentLayerChanged)
def on_selection_changed():
result.setEnabled(session.state.currentLayer.layer is not None)
return result

View File

@@ -0,0 +1,90 @@
from ngSkinTools2 import api, signal
from ngSkinTools2.api.pyside import QtWidgets
from ngSkinTools2.ui.options import PersistentValue
filter_normal_json = 'JSON files(*.json)'
filter_compressed = 'Compressed JSON(*.json.gz)'
file_dialog_filters = ";;".join([filter_normal_json, filter_compressed])
format_map = {
filter_normal_json: api.FileFormat.JSON,
filter_compressed: api.FileFormat.CompressedJSON,
}
default_filter = PersistentValue("default_import_filter", default_value=api.FileFormat.JSON)
def buildAction_export(session, parent):
from ngSkinTools2.ui import actions
def export_callback():
file_name, selected_filter = QtWidgets.QFileDialog.getSaveFileName(
parent, "Export to Json", filter=file_dialog_filters, selectedFilter=default_filter.get()
)
if not file_name:
return
default_filter.set(selected_filter)
if session.state.layersAvailable:
api.export_json(session.state.selectedSkinCluster, file_name, format=format_map[selected_filter])
result = actions.define_action(
parent,
"Export Layers to Json...",
callback=export_callback,
tooltip="Save layer info to external file, suitable for importing weights to different scene/mesh",
)
@signal.on(session.events.targetChanged, qtParent=parent)
def update_to_target():
result.setEnabled(session.state.layersAvailable)
update_to_target()
return result
def buildAction_import(session, parent, file_dialog_func=None):
from ngSkinTools2.ui import actions
from ngSkinTools2.ui.transferDialog import LayersTransfer, UiModel, open
def default_file_dialog_func():
file_name, selected_filter = QtWidgets.QFileDialog.getOpenFileName(
parent, "Import from Json", filter=file_dialog_filters, selectedFilter=default_filter.get()
)
if file_name:
default_filter.set(selected_filter)
return file_name, selected_filter
if file_dialog_func is None:
file_dialog_func = default_file_dialog_func
def transfer_dialog(transfer):
model = UiModel()
model.transfer = transfer
open(parent, model)
def import_callback():
if session.state.selectedSkinCluster is None:
return
file_name, selected_format = file_dialog_func()
if not file_name:
return
t = LayersTransfer()
t.load_source_from_file(file_name, format=format_map[selected_format])
t.target = session.state.selectedSkinCluster
t.customize_callback = transfer_dialog
t.execute()
result = actions.define_action(parent, "Import Layers from Json...", callback=import_callback, tooltip="Load previously exported weights")
@signal.on(session.events.targetChanged, qtParent=parent)
def update():
result.setEnabled(session.state.selectedSkinCluster is not None)
update()
return result

View File

@@ -0,0 +1,40 @@
from ngSkinTools2 import api, signal
def can_import(session):
"""
:type session: ngSkinTools2.api.session.Session
"""
if not session.state.selection:
return False
if session.state.layersAvailable:
return False
return api.import_v1.can_import(session.state.selection[-1])
def build_action_import_v1(session, parent):
"""
:type parent: PySide2.QtWidgets.QWidget
:type session: ngSkinTools2.api.session.Session
"""
from ngSkinTools2.ui import actions
def do_convert():
api.import_v1.import_layers(session.state.selection[-1])
api.import_v1.cleanup(session.state.selection[-1:])
update_state()
session.events.targetChanged.emitIfChanged()
result = actions.define_action(parent, "Convert From v1.0 Layers", callback=do_convert)
result.setToolTip("Convert skinning layers from previous version of ngSkinTools; after completing this action, v1 nodes will be deleted.")
@signal.on(session.events.targetChanged)
def update_state():
result.setVisible(can_import(session))
update_state()
return result

View File

@@ -0,0 +1,229 @@
import random
from maya import cmds
import ngSkinTools2.api
from ngSkinTools2 import signal
from ngSkinTools2.api import Mirror
from ngSkinTools2.api.layers import generate_layer_name
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.pyside import QAction, QtCore
from ngSkinTools2.api.session import session, withSession
from ngSkinTools2.decorators import undoable
from ngSkinTools2.ui import dialogs, qt
from ngSkinTools2.ui.action import Action
from ngSkinTools2.ui.options import config
logger = getLogger("layer operations")
@withSession
@undoable
def initializeLayers(createFirstLayer=True):
if session.state.layersAvailable:
return # ignore
target = session.state.selectedSkinCluster
layers = ngSkinTools2.api.init_layers(target)
with ngSkinTools2.api.suspend_updates(target):
if createFirstLayer:
layer = layers.add("Base weights")
layer.set_current()
Mirror(target).set_mirror_config(config.mirrorInfluencesDefaults)
session.events.targetChanged.emitIfChanged()
if ngSkinTools2.api.is_slow_mode_skin_cluster(target):
dialogs.info(
"ngSkinTools switched to slow maya API for setting skin cluster weights for this skinCluster, to workaround a Maya bug when skinCluster uses dg nodes as inputs"
)
@undoable
def addLayer():
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
def guessParent():
currentLayer = layers.current_layer()
if currentLayer is None:
return None
# current layer is a parent?
if currentLayer.num_children > 0:
return currentLayer
return currentLayer.parent
with ngSkinTools2.api.suspend_updates(layers.mesh):
new_layer = layers.add(generate_layer_name(session.state.all_layers, "New Layer"))
new_layer.parent = guessParent()
session.events.layerListChanged.emitIfChanged()
setCurrentLayer(new_layer)
return new_layer
def build_action_initialize_layers(session, parent):
from ngSkinTools2.ui import actions
from . import import_v1_actions
def do_initialize():
if import_v1_actions.can_import(session):
q = (
"Skinning layers from previous version of ngSkinTools are present on this mesh. This operation will initialize "
"skinning layers from scratch, discarding previous layers information. Do you want to continue?"
)
if not dialogs.yesNo(q):
return
initializeLayers()
result = actions.define_action(parent, "Initialize Skinning Layers", callback=do_initialize)
@signal.on(session.events.nodeSelectionChanged)
def update():
result.setEnabled(session.state.selectedSkinCluster is not None)
update()
return result
def buildAction_createLayer(session, parent):
from ngSkinTools2.ui import actions
result = actions.define_action(parent, "Create Layer", callback=addLayer, icon=":/newLayerEmpty.png", shortcut=QtCore.Qt.Key_Insert)
@signal.on(session.events.targetChanged)
def update_to_target():
result.setEnabled(session.state.layersAvailable)
update_to_target()
return result
def buildAction_deleteLayer(session, parent):
from ngSkinTools2.ui import actions
result = actions.define_action(parent, "Delete Layer", callback=deleteSelectedLayers, shortcut=QtCore.Qt.Key_Delete)
@signal.on(session.context.selected_layers.changed, session.events.targetChanged, qtParent=parent)
def update_to_target():
result.setEnabled(session.state.layersAvailable and bool(session.context.selected_layers(default=[])))
update_to_target()
return result
@undoable
def setCurrentLayer(layer):
"""
:type layer: ngSkinTools2.api.layers.Layer
"""
if not session.active():
logger.info("didn't set current layer: no session")
if not session.state.layersAvailable:
logger.info("didn't set current layer: layers not enabled")
logger.info("setting current layer to %r on %r", layer, session.state.selectedSkinCluster)
layer.set_current()
session.events.currentLayerChanged.emitIfChanged()
def getCurrentLayer():
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
return layers.current_layer()
@undoable
def renameLayer(layer, newName):
layer.name = newName
cmds.evalDeferred(session.events.layerListChanged.emitIfChanged)
@undoable
def deleteSelectedLayers():
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
for i in session.context.selected_layers(default=[]):
layers.delete(i)
session.events.layerListChanged.emitIfChanged()
session.events.currentLayerChanged.emitIfChanged()
class ToggleEnabledAction(Action):
name = "Enabled"
checkable = True
def __init__(self, session):
Action.__init__(self, session)
def checked(self):
"""
return true if most of selected layers are enabled
:return:
"""
layers = session.context.selected_layers(default=[])
if not layers:
return True
enabled_disabled_balance = 0
for layer in layers:
try:
# eat up the exception if layer id is invalid
enabled_disabled_balance += 1 if layer.enabled else -1
except:
pass
return enabled_disabled_balance >= 0
def run(self):
enabled = not self.checked()
selected_layers = session.context.selected_layers()
if not selected_layers:
return
for i in selected_layers:
i.enabled = enabled
logger.info("layers toggled: %r", selected_layers)
session.events.layerListChanged.emitIfChanged()
def enabled(self):
return session.state.layersAvailable and bool(session.context.selected_layers(default=[]))
def update_on_signals(self):
return [session.context.selected_layers.changed, session.events.layerListChanged, session.events.targetChanged]
def build_action_randomize_influences_colors(session, parent):
"""
builds a UI action for randomly choosing new colors for influences
:type session: ngSkinTools2.api.session.Session
"""
result = QAction("Randomize colors", parent)
result.setToolTip("Choose random colors for each influence, selecting from Maya's pallete of indexed colors")
def color_filter(c):
brightness = c[0] * c[0] + c[1] * c[1] + c[2] * c[2]
return brightness > 0.001 and brightness < 0.99 # just a fancy way to skip white and black
colors = set([tuple(cmds.colorIndex(i, q=True)) for i in range(1, 30)])
colors = [c for c in colors if color_filter(c)]
@qt.on(result.triggered)
def triggered():
if session.state.selectedSkinCluster is None:
return
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
layers.config.influence_colors = {i.logicalIndex: random.choice(colors) for i in layers.list_influences()}
return result

View File

@@ -0,0 +1,45 @@
from maya import cmds
from ngSkinTools2.api import PaintTool
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.session import session
from ngSkinTools2.ui.action import Action
log = getLogger("operations/paint")
class FloodAction(Action):
name = "Flood"
tooltip = "Apply current brush to whole selection"
def run(self):
session.paint_tool.flood(self.session.state.currentLayer.layer, influences=self.session.state.currentLayer.layer.paint_targets)
def enabled(self):
return PaintTool.is_painting() and self.session.state.selectedSkinCluster is not None and self.session.state.currentLayer.layer is not None
def update_on_signals(self):
return [
self.session.events.toolChanged,
self.session.events.currentLayerChanged,
self.session.events.currentInfluenceChanged,
self.session.events.targetChanged,
]
class PaintAction(Action):
name = "Paint"
tooltip = "Toggle paint tool"
checkable = True
def run(self):
if self.checked():
cmds.setToolTo("moveSuperContext")
else:
self.session.paint_tool.start()
def update_on_signals(self):
return [self.session.events.toolChanged]
def checked(self):
return PaintTool.is_painting()

View File

@@ -0,0 +1,101 @@
import itertools
from maya import cmds
from ngSkinTools2.api import PaintTool, target_info
from ngSkinTools2.api.session import Session
from ngSkinTools2.decorators import undoable
def as_list(arg):
return [] if arg is None else arg
customNodeTypes = ['ngst2MeshDisplay', 'ngst2SkinLayerData']
def list_custom_nodes():
"""
list all custom nodes in the scene
"""
result = []
for nodeType in customNodeTypes:
result.extend(as_list(cmds.ls(type=nodeType)))
return result
def list_custom_nodes_for_mesh(mesh=None):
"""
list custom nodes only related to provided mesh. None means current selection
"""
skin_cluster = target_info.get_related_skin_cluster(mesh)
if skin_cluster is None:
return []
# delete any ngSkinTools deformers from the history, and find upstream stuff from given skinCluster.
hist = as_list(cmds.listHistory(skin_cluster, future=True, levels=1))
return [i for i in hist if cmds.nodeType(i) in customNodeTypes]
def list_custom_nodes_for_meshes(meshes):
return list(itertools.chain.from_iterable([list_custom_nodes_for_mesh(i) for i in meshes]))
message_scene_noCustomNodes = 'Scene does not contain any custom ngSkinTools nodes.'
message_selection_noCustomNodes = 'Selection does not contain any custom ngSkinTools nodes.'
message_scene_warning = (
'This command deletes all custom ngSkinTools nodes. Skin weights ' 'will be preserved, but all layer data will be lost. Do you want to continue?'
)
message_selection_warning = (
'This command deletes custom ngSkinTools nodes for selection. Skin weights '
'will be preserved, but all layer data will be lost. Do you want to continue?'
)
@undoable
def remove_custom_nodes(interactive=False, session=None, meshes=None):
"""
Removes custom ngSkinTools2 nodes from the scene or selection.
:type meshes: list[str]
:param meshes: list of node names; if empty, operation will be scene-wide.
:type session: Session
:param session: optional; if specified, will fire events for current session about changed status of selected mesh
:type interactive: bool
:param interactive: if True, user warnings will be emited
"""
from ngSkinTools2.ui import dialogs
if meshes is None:
meshes = []
is_selection_mode = len(meshes) > 0
custom_nodes = list_custom_nodes() if not is_selection_mode else list_custom_nodes_for_meshes(meshes)
if not custom_nodes:
if interactive:
dialogs.info(message_selection_noCustomNodes if is_selection_mode else message_scene_noCustomNodes)
return
delete_confirmed = True
if interactive:
delete_confirmed = dialogs.yesNo(message_selection_warning if is_selection_mode else message_scene_warning)
if delete_confirmed:
cmds.delete(custom_nodes)
if PaintTool.is_painting():
# make sure that painting is canceled to restore mesh display etc
cmds.setToolTo("Move")
if session is not None:
session.events.targetChanged.emitIfChanged()
@undoable
def remove_custom_nodes_from_selection(interactive=False, session=None):
selection = cmds.ls(sl=True)
remove_custom_nodes(interactive=interactive, session=session, meshes=as_list(selection))

View File

@@ -0,0 +1,295 @@
from maya import cmds
from ngSkinTools2 import api, signal
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.python_compatibility import Object
from ngSkinTools2.api.session import Session
from ngSkinTools2.decorators import undoable
from ngSkinTools2.observableValue import ObservableValue
from ngSkinTools2.operations import layers
from ngSkinTools2.ui import dialogs
logger = getLogger("operation/tools")
def __create_tool_action__(parent, session, action_name, action_tooltip, exec_handler, enabled_handler=None):
"""
:type session: Session
"""
from ngSkinTools2.ui import actions
def execute():
if not session.active():
return
exec_handler()
result = actions.define_action(parent, action_name, callback=execute, tooltip=action_tooltip)
@signal.on(session.events.targetChanged, session.events.currentLayerChanged)
def update_state():
enabled = session.state.layersAvailable and session.state.currentLayer.layer is not None
if enabled and enabled_handler is not None:
enabled = enabled_handler(session.state.currentLayer.layer)
result.setEnabled(enabled)
update_state()
return result
class ClosestJointOptions(Object):
def __init__(self):
self.create_new_layer = ObservableValue(False)
self.all_influences = ObservableValue(True)
def create_action__from_closest_joint(parent, session):
options = ClosestJointOptions()
def exec_handler():
layer = session.state.currentLayer.layer
influences = None
if not options.all_influences():
influences = layer.paint_targets
if not influences:
dialogs.info("Select one or more influences in Influences list")
return
if options.create_new_layer():
layer = layers.addLayer()
api.assign_from_closest_joint(
session.state.selectedSkinCluster,
layer,
influences=influences,
)
session.events.currentLayerChanged.emitIfChanged()
session.events.influencesListUpdated.emit()
if layer.paint_target is None:
used_influences = layer.get_used_influences()
if used_influences:
layer.paint_target = min(used_influences)
return (
__create_tool_action__(
parent,
session,
action_name=u"Assign From Closest Joint",
action_tooltip="Assign 1.0 weight for closest influence per each vertex in selected layer",
exec_handler=exec_handler,
),
options,
)
class UnifyWeightsOptions(Object):
overall_effect = ObservableValue(1.0)
single_cluster_mode = ObservableValue(False)
def create_action__unify_weights(parent, session):
options = UnifyWeightsOptions()
def exec_handler():
api.unify_weights(
session.state.selectedSkinCluster,
session.state.currentLayer.layer,
overall_effect=options.overall_effect(),
single_cluster_mode=options.single_cluster_mode(),
)
return (
__create_tool_action__(
parent,
session,
action_name=u"Unify Weights",
action_tooltip="For selected vertices, make verts the same for all verts",
exec_handler=exec_handler,
),
options,
)
def create_action__merge_layers(parent, session):
"""
:param parent: UI parent for this action
:type session: Session
"""
def exec_handler():
api.merge_layers(layers=session.context.selected_layers(default=[]))
session.events.layerListChanged.emitIfChanged()
session.events.currentLayerChanged.emitIfChanged()
def enabled_handler(layer):
return layer is not None and layer.index > 0
return __create_tool_action__(
parent,
session,
action_name=u"Merge",
action_tooltip="Merge contents of this layer into underlying layer. Pre-effects weights will be used for this",
exec_handler=exec_handler,
enabled_handler=enabled_handler,
)
def create_action__duplicate_layer(parent, session):
"""
:param parent: UI parent for this action
:type session: Session
"""
@undoable
def exec_handler():
with api.suspend_updates(session.state.selectedSkinCluster):
for source in session.context.selected_layers(default=[]):
api.duplicate_layer(layer=source)
session.events.layerListChanged.emitIfChanged()
session.events.currentLayerChanged.emitIfChanged()
return __create_tool_action__(
parent,
session,
action_name=u"Duplicate",
action_tooltip="Duplicate selected layer(s)",
exec_handler=exec_handler,
)
def create_action__fill_transparency(parent, session):
"""
:param parent: UI parent for this action
:type session: Session
"""
@undoable
def exec_handler():
with api.suspend_updates(session.state.selectedSkinCluster):
for source in session.context.selected_layers(default=[]):
api.fill_transparency(layer=source)
return __create_tool_action__(
parent,
session,
action_name=u"Fill Transparency",
action_tooltip="All transparent vertices in the selected layer(s) receive weights from their closest non-empty neighbour vertex",
exec_handler=exec_handler,
)
def create_action__copy_component_weights(parent, session):
"""
:param parent: UI parent for this action
:type session: Session
"""
def exec_handler():
for source in session.context.selected_layers(default=[]):
api.copy_component_weights(layer=source)
return __create_tool_action__(
parent,
session,
action_name=u"Copy Component Weights",
action_tooltip="Store components weights in memory for further component-based paste actions",
exec_handler=exec_handler,
)
def create_action__paste_average_component_weight(parent, session):
"""
:param parent: UI parent for this action
:type session: Session
"""
def exec_handler():
for l in session.context.selected_layers(default=[]):
api.paste_average_component_weights(layer=l)
return __create_tool_action__(
parent,
session,
action_name=u"Paste Average Component Weight",
action_tooltip="Compute average of copied component weights and set that value to currently selected components",
exec_handler=exec_handler,
)
def create_action__add_influences(parent, session):
"""
:param parent: UI parent for this action
:type session: Session
"""
def exec_handler():
selection = cmds.ls(sl=True, l=True)
if len(selection) < 2:
logger.info("invalid selection: %s", selection)
return
api.add_influences(selection[:-1], selection[-1])
cmds.select(selection[-1])
session.events.influencesListUpdated.emit()
return __create_tool_action__(
parent,
session,
action_name=u"Add Influences",
action_tooltip="Add selected influences to current skin cluster.",
exec_handler=exec_handler,
)
def create_action__select_affected_vertices(parent, session):
"""
:param parent: UI parent for this action
:type session: Session
"""
def exec_handler():
selected_layers = session.context.selected_layers(default=[])
if not selected_layers:
return
if not session.state.currentLayer.layer:
return
influences = session.state.currentLayer.layer.paint_targets
if not influences:
return
non_zero_weights = []
for layer in selected_layers:
for i in influences:
weights = layer.get_weights(i)
if weights:
non_zero_weights.append(weights)
if not non_zero_weights:
return
current_selection = cmds.ls(sl=True, o=True, l=True)
if len(current_selection) != 1:
return
# we're not sure - this won't work if skin cluster is selected directly
selected_mesh_probably = current_selection[0]
combined_weights = [sum(i) for i in zip(*non_zero_weights)]
indexes = [selected_mesh_probably + ".vtx[%d]" % index for index, i in enumerate(combined_weights) if i > 0.00001]
try:
cmds.select(indexes)
except:
pass
return __create_tool_action__(
parent,
session,
action_name=u"Select Affected Vertices",
action_tooltip="Select vertices that have non-zero weight for current influence.",
exec_handler=exec_handler,
)

View File

@@ -0,0 +1,31 @@
from ngSkinTools2.api.python_compatibility import Object
def website_base_url():
import ngSkinTools2
if ngSkinTools2.DEBUG_MODE:
return "http://localhost:1313"
return "https://www.ngskintools.com"
class WebsiteLinksActions(Object):
def __init__(self, parent):
self.api_root = make_documentation_action(parent, "API Documentation", "/v2/api")
self.user_guide = make_documentation_action(parent, "User Guide", "/v2/")
self.changelog = make_documentation_action(parent, "Change Log", "/v2/changelog", icon=None)
self.contact = make_documentation_action(parent, "Contact", "/contact/", icon=None)
def make_documentation_action(parent, title, url, icon=":/help.png"):
from ngSkinTools2.ui import actions
def handler():
import webbrowser
webbrowser.open_new(website_base_url() + url)
result = actions.define_action(parent, title, callback=handler, icon=icon)
result.setToolTip("opens {0} in a browser".format(url))
return result