Update
91
2023/scripts/rigging_tools/ngskintools2/ui/aboutwindow.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
from xml.sax.saxutils import escape as escape
|
||||
|
||||
from ngSkinTools2 import cleanup, version
|
||||
from ngSkinTools2.api.pyside import Qt, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def show(parent):
|
||||
"""
|
||||
:type parent: QWidget
|
||||
"""
|
||||
|
||||
def header():
|
||||
# noinspection PyShadowingNames
|
||||
def leftSide():
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addStretch()
|
||||
layout.addWidget(QtWidgets.QLabel("<h1>ngSkinTools</h1>"))
|
||||
layout.addWidget(QtWidgets.QLabel("Version {0}".format(version.pluginVersion())))
|
||||
layout.addWidget(QtWidgets.QLabel(version.COPYRIGHT))
|
||||
|
||||
url = QtWidgets.QLabel('<a href="{0}" style="color: #007bff;">{0}</a>'.format(version.PRODUCT_URL))
|
||||
url.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
url.setOpenExternalLinks(True)
|
||||
layout.addWidget(url)
|
||||
layout.addStretch()
|
||||
return layout
|
||||
|
||||
def logo():
|
||||
from ngSkinTools2.api.pyside import QSvgWidget
|
||||
|
||||
w = QSvgWidget(os.path.join(os.path.dirname(__file__), "images", "logo.svg"))
|
||||
|
||||
w.setFixedSize(*((70 * scale_multiplier,) * 2))
|
||||
layout.addWidget(w)
|
||||
return w
|
||||
|
||||
result = QtWidgets.QWidget()
|
||||
result.setPalette(qt.alternative_palette_light())
|
||||
result.setAutoFillBackground(True)
|
||||
|
||||
hSplit = QtWidgets.QHBoxLayout()
|
||||
hSplit.setContentsMargins(30, 30, 30, 30)
|
||||
result.setLayout(hSplit)
|
||||
|
||||
hSplit.addLayout(leftSide())
|
||||
hSplit.addStretch()
|
||||
hSplit.addWidget(logo())
|
||||
|
||||
return result
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def body():
|
||||
result = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.setContentsMargins(30, 30, 30, 30)
|
||||
|
||||
return result
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def buttonsRow(window):
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.addStretch()
|
||||
btnClose = QtWidgets.QPushButton("Close")
|
||||
btnClose.setMinimumWidth(100 * scale_multiplier)
|
||||
layout.addWidget(btnClose)
|
||||
layout.setContentsMargins(20 * scale_multiplier, 15 * scale_multiplier, 20 * scale_multiplier, 15 * scale_multiplier)
|
||||
|
||||
btnClose.clicked.connect(lambda: window.close())
|
||||
return layout
|
||||
|
||||
window = QtWidgets.QWidget(parent, Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
|
||||
window.resize(600 * scale_multiplier, 500 * scale_multiplier)
|
||||
window.setAttribute(Qt.WA_DeleteOnClose)
|
||||
window.setWindowTitle("About ngSkinTools")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
window.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout.addWidget(header())
|
||||
layout.addWidget(body())
|
||||
layout.addStretch(2)
|
||||
layout.addLayout(buttonsRow(window))
|
||||
|
||||
window.show()
|
||||
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
57
2023/scripts/rigging_tools/ngskintools2/ui/action.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.api.session import Session
|
||||
|
||||
|
||||
class Action(Object):
|
||||
name = "Action"
|
||||
tooltip = ""
|
||||
checkable = False
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session # type: Session
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
def enabled(self):
|
||||
return True
|
||||
|
||||
def checked(self):
|
||||
return False
|
||||
|
||||
def run_if_enabled(self):
|
||||
if self.enabled():
|
||||
self.run()
|
||||
|
||||
def update_on_signals(self):
|
||||
return []
|
||||
|
||||
def as_qt_action(self, parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
result = actions.define_action(parent, self.name, callback=self.run_if_enabled, tooltip=self.tooltip)
|
||||
result.setCheckable(self.checkable)
|
||||
|
||||
def update():
|
||||
result.setEnabled(self.enabled())
|
||||
if self.checkable:
|
||||
result.setChecked(self.checked())
|
||||
|
||||
signal.on(*self.update_on_signals(), qtParent=parent)(update)
|
||||
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
def qt_action(action_class, session, parent):
|
||||
"""
|
||||
Wrap provided action_class into a QT action
|
||||
"""
|
||||
return action_class(session).as_qt_action(parent)
|
||||
|
||||
|
||||
def do_action_hotkey(action_class):
|
||||
from ngSkinTools2.api import session
|
||||
|
||||
action_class(session.session).run_if_enabled()
|
||||
146
2023/scripts/rigging_tools/ngskintools2/ui/actions.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import PasteOperation
|
||||
from ngSkinTools2.api.pyside import QAction, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.operations import import_export_actions, import_v1_actions
|
||||
from ngSkinTools2.operations.layers import (
|
||||
ToggleEnabledAction,
|
||||
build_action_initialize_layers,
|
||||
)
|
||||
from ngSkinTools2.operations.paint import FloodAction, PaintAction
|
||||
from ngSkinTools2.operations.website_links import WebsiteLinksActions
|
||||
from ngSkinTools2.ui import action
|
||||
from ngSkinTools2.ui.updatewindow import build_action_check_for_updates
|
||||
|
||||
|
||||
def define_action(parent, label, callback=None, icon=None, shortcut=None, tooltip=None):
|
||||
result = QAction(label, parent)
|
||||
if icon is not None:
|
||||
result.setIcon(QtGui.QIcon(icon))
|
||||
if callback is not None:
|
||||
result.triggered.connect(callback)
|
||||
if shortcut is not None:
|
||||
if not isinstance(shortcut, QtGui.QKeySequence):
|
||||
shortcut = QtGui.QKeySequence(shortcut)
|
||||
result.setShortcut(shortcut)
|
||||
if tooltip is not None:
|
||||
result.setToolTip(tooltip)
|
||||
result.setStatusTip(tooltip)
|
||||
return result
|
||||
|
||||
|
||||
def build_action_delete_custom_nodes_for_selection(parent, session):
|
||||
from ngSkinTools2.operations import removeLayerData
|
||||
|
||||
result = define_action(
|
||||
parent,
|
||||
"Delete Custom Nodes For Selection",
|
||||
callback=lambda: removeLayerData.remove_custom_nodes_from_selection(interactive=True, session=session),
|
||||
)
|
||||
|
||||
@signal.on(session.events.nodeSelectionChanged)
|
||||
def update():
|
||||
result.setEnabled(bool(session.state.selection))
|
||||
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
class Actions(Object):
|
||||
def separator(self, parent, label=""):
|
||||
separator = QAction(parent)
|
||||
separator.setText(label)
|
||||
separator.setSeparator(True)
|
||||
return separator
|
||||
|
||||
def __init__(self, parent, session):
|
||||
"""
|
||||
:type session: Session
|
||||
"""
|
||||
qt_action = lambda a: action.qt_action(a, session, parent)
|
||||
from ngSkinTools2.operations import layers, removeLayerData, tools
|
||||
from ngSkinTools2.ui.transferDialog import build_transfer_action
|
||||
|
||||
self.initialize = build_action_initialize_layers(session, parent)
|
||||
self.exportFile = import_export_actions.buildAction_export(session, parent)
|
||||
self.importFile = import_export_actions.buildAction_import(session, parent)
|
||||
self.import_v1 = import_v1_actions.build_action_import_v1(session, parent)
|
||||
|
||||
self.addLayer = layers.buildAction_createLayer(session, parent)
|
||||
self.deleteLayer = layers.buildAction_deleteLayer(session, parent)
|
||||
self.toggle_layer_enabled = qt_action(ToggleEnabledAction)
|
||||
|
||||
# self.moveLayerUp = defineCallbackAction(u"Move Layer Up", None, icon=":/moveLayerUp.png")
|
||||
# self.moveLayerDown = defineCallbackAction(u"Move Layer Down", None, icon=":/moveLayerDown.png")
|
||||
self.paint = qt_action(PaintAction)
|
||||
self.flood = qt_action(FloodAction)
|
||||
|
||||
self.toolsAssignFromClosestJoint, self.toolsAssignFromClosestJointOptions = tools.create_action__from_closest_joint(parent, session)
|
||||
(
|
||||
self.toolsAssignFromClosestJointSelectedInfluences,
|
||||
self.toolsAssignFromClosestJointOptionsSelectedInfluences,
|
||||
) = tools.create_action__from_closest_joint(parent, session)
|
||||
self.toolsAssignFromClosestJointOptionsSelectedInfluences.all_influences.set(False)
|
||||
self.toolsAssignFromClosestJointOptionsSelectedInfluences.create_new_layer.set(False)
|
||||
self.toolsUnifyWeights, self.toolsUnifyWeightsOptions = tools.create_action__unify_weights(parent, session)
|
||||
|
||||
self.toolsDeleteCustomNodes = define_action(
|
||||
parent, "Delete All Custom Nodes", callback=lambda: removeLayerData.remove_custom_nodes(interactive=True, session=session)
|
||||
)
|
||||
|
||||
self.toolsDeleteCustomNodesOnSelection = build_action_delete_custom_nodes_for_selection(parent, session)
|
||||
|
||||
self.transfer = build_transfer_action(session=session, parent=parent)
|
||||
|
||||
# self.setLayerMirrored = defineAction(u"Mirrored", icon=":/polyMirrorGeometry.png")
|
||||
# self.setLayerMirrored.setCheckable(True)
|
||||
|
||||
self.documentation = WebsiteLinksActions(parent=parent)
|
||||
|
||||
self.check_for_updates = build_action_check_for_updates(parent=parent)
|
||||
|
||||
from ngSkinTools2.operations import copy_paste_actions
|
||||
|
||||
self.cut_influences = copy_paste_actions.action_copy_cut(session, parent, True)
|
||||
self.copy_influences = copy_paste_actions.action_copy_cut(session, parent, False)
|
||||
self.paste_weights = copy_paste_actions.action_paste(session, parent, PasteOperation.replace)
|
||||
self.paste_weights_add = copy_paste_actions.action_paste(session, parent, PasteOperation.add)
|
||||
self.paste_weights_sub = copy_paste_actions.action_paste(session, parent, PasteOperation.subtract)
|
||||
|
||||
self.copy_components = tools.create_action__copy_component_weights(parent=parent, session=session)
|
||||
self.paste_component_average = tools.create_action__paste_average_component_weight(parent=parent, session=session)
|
||||
|
||||
self.merge_layer = tools.create_action__merge_layers(parent=parent, session=session)
|
||||
self.duplicate_layer = tools.create_action__duplicate_layer(parent=parent, session=session)
|
||||
self.fill_layer_transparency = tools.create_action__fill_transparency(parent=parent, session=session)
|
||||
|
||||
self.add_influences = tools.create_action__add_influences(parent=parent, session=session)
|
||||
from ngSkinTools2.ui import influencesview
|
||||
|
||||
self.show_used_influences_only = influencesview.build_used_influences_action(parent)
|
||||
self.set_influences_sorted = influencesview.build_set_influences_sorted_action(parent)
|
||||
self.randomize_influences_colors = layers.build_action_randomize_influences_colors(parent=parent, session=session)
|
||||
|
||||
self.select_affected_vertices = tools.create_action__select_affected_vertices(parent=parent, session=session)
|
||||
|
||||
def addLayersActions(self, context):
|
||||
context.addAction(self.addLayer)
|
||||
context.addAction(self.deleteLayer)
|
||||
context.addAction(self.separator(context))
|
||||
context.addAction(self.merge_layer)
|
||||
context.addAction(self.duplicate_layer)
|
||||
context.addAction(self.fill_layer_transparency)
|
||||
context.addAction(self.separator(context))
|
||||
context.addAction(self.toggle_layer_enabled)
|
||||
|
||||
def addInfluencesActions(self, context):
|
||||
context.addAction(self.separator(context, "Actions"))
|
||||
context.addAction(self.toolsAssignFromClosestJointSelectedInfluences)
|
||||
context.addAction(self.select_affected_vertices)
|
||||
context.addAction(self.separator(context, "Clipboard"))
|
||||
context.addAction(self.cut_influences)
|
||||
context.addAction(self.copy_influences)
|
||||
context.addAction(self.paste_weights)
|
||||
context.addAction(self.paste_weights_add)
|
||||
context.addAction(self.paste_weights_sub)
|
||||
@@ -0,0 +1,66 @@
|
||||
from ngSkinTools2.api import PaintTool
|
||||
from ngSkinTools2.api.paint import popups
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def brush_settings_popup(paint):
|
||||
"""
|
||||
|
||||
:type paint: PaintTool
|
||||
"""
|
||||
window = QtWidgets.QWidget(qt.mainWindow)
|
||||
window.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint)
|
||||
window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
spacing = 5
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setSpacing(spacing)
|
||||
|
||||
intensity_slider = widgets.NumberSliderGroup()
|
||||
widgets.set_paint_expo(intensity_slider, paint.paint_mode)
|
||||
intensity_slider.set_value(paint.intensity)
|
||||
|
||||
@qt.on(intensity_slider.slider.sliderReleased, intensity_slider.spinner.editingFinished)
|
||||
def close_with_slider_intensity():
|
||||
close_with_intensity(intensity_slider.value())
|
||||
|
||||
def close_with_intensity(value):
|
||||
paint.intensity = value
|
||||
window.close()
|
||||
|
||||
def create_intensity_button(intensity):
|
||||
btn = QtWidgets.QPushButton("{0:.3g}".format(intensity))
|
||||
btn.clicked.connect(lambda: close_with_intensity(intensity))
|
||||
btn.setMinimumWidth(60 * scale_multiplier)
|
||||
btn.setMinimumHeight(30 * scale_multiplier)
|
||||
return btn
|
||||
|
||||
layout.addLayout(intensity_slider.layout())
|
||||
|
||||
for values in [(0.0, 1.0), (0.25, 0.5, 0.75), (0.025, 0.05, 0.075, 0.1, 0.125)]:
|
||||
row = QtWidgets.QHBoxLayout()
|
||||
row.setSpacing(spacing)
|
||||
for v in values:
|
||||
row.addWidget(create_intensity_button(v))
|
||||
layout.addLayout(row)
|
||||
|
||||
group = QtWidgets.QGroupBox("Brush Intensity")
|
||||
group.setLayout(layout)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(4 * scale_multiplier, 4 * scale_multiplier, 4 * scale_multiplier, 4 * scale_multiplier)
|
||||
layout.addWidget(group)
|
||||
|
||||
window.setLayout(layout)
|
||||
|
||||
window.show()
|
||||
mp = QtGui.QCursor.pos()
|
||||
window.move(mp.x() - window.size().width() / 2, mp.y() - window.size().height() / 2)
|
||||
|
||||
window.activateWindow()
|
||||
|
||||
popups.close_all()
|
||||
popups.add(window)
|
||||
57
2023/scripts/rigging_tools/ngskintools2/ui/dialogs.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from maya import OpenMaya as om
|
||||
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
|
||||
openDialogs = []
|
||||
messagesCallbacks = []
|
||||
|
||||
# main window will set itself here
|
||||
promptsParent = None
|
||||
|
||||
|
||||
def __baseMessageBox(message):
|
||||
msg = QtWidgets.QMessageBox(promptsParent)
|
||||
msg.setWindowTitle("ngSkinTools2")
|
||||
msg.setText(message)
|
||||
|
||||
for i in messagesCallbacks:
|
||||
i(message)
|
||||
|
||||
openDialogs.append(msg)
|
||||
return msg
|
||||
|
||||
|
||||
def displayError(message):
|
||||
"""
|
||||
displays error in script editor and in a dialog box
|
||||
"""
|
||||
|
||||
message = str(message)
|
||||
om.MGlobal.displayError('[ngSkinTools2] ' + message)
|
||||
|
||||
msg = __baseMessageBox(message)
|
||||
msg.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
msg.exec_()
|
||||
|
||||
|
||||
def info(message):
|
||||
msg = __baseMessageBox(message)
|
||||
msg.setIcon(QtWidgets.QMessageBox.Information)
|
||||
msg.exec_()
|
||||
|
||||
|
||||
def yesNo(message):
|
||||
msg = __baseMessageBox(message)
|
||||
msg.setIcon(QtWidgets.QMessageBox.Question)
|
||||
msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
msg.setDefaultButton(QtWidgets.QMessageBox.Yes)
|
||||
return msg.exec_() == QtWidgets.QMessageBox.Yes
|
||||
|
||||
|
||||
def closeAllAfterTimeout(timeout, result=0):
|
||||
def closeAll():
|
||||
while openDialogs:
|
||||
msg = openDialogs.pop()
|
||||
msg.done(result)
|
||||
|
||||
QtCore.QTimer.singleShot(timeout, closeAll)
|
||||
86
2023/scripts/rigging_tools/ngskintools2/ui/hotkeys.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Global list of hotkey-able functions within the plugin.
|
||||
|
||||
|
||||
These functions will be embedded in end-user's hotkey setup by absolute path (package.function_name)
|
||||
so the names should not fluctuate.
|
||||
"""
|
||||
|
||||
from ngSkinTools2.api import NamedPaintTarget, PaintTool, WeightsDisplayMode, plugin
|
||||
from ngSkinTools2.api.paint import MaskDisplayMode
|
||||
from ngSkinTools2.api.session import session, withSession
|
||||
from ngSkinTools2.operations.paint import FloodAction, PaintAction
|
||||
from ngSkinTools2.ui.action import do_action_hotkey
|
||||
|
||||
|
||||
def paint_tool_start():
|
||||
do_action_hotkey(PaintAction)
|
||||
|
||||
|
||||
def paint_tool_toggle_help():
|
||||
plugin.ngst2_hotkey(paintContextToggleHelp=True)
|
||||
|
||||
|
||||
def paint_tool_flood():
|
||||
do_action_hotkey(FloodAction)
|
||||
|
||||
|
||||
def paint_tool_focus_current_influence():
|
||||
plugin.ngst2_hotkey(paintContextViewFit=True)
|
||||
|
||||
|
||||
def paint_tool_brush_size():
|
||||
plugin.ngst2_hotkey(paintContextBrushSize=True)
|
||||
|
||||
|
||||
def paint_tool_brush_size_release():
|
||||
plugin.ngst2_hotkey(paintContextBrushSize=False)
|
||||
|
||||
|
||||
def paint_tool_sample_influence():
|
||||
plugin.ngst2_hotkey(paintContextSampleInfluence=True)
|
||||
|
||||
|
||||
def paint_tool_sample_influence_release():
|
||||
plugin.ngst2_hotkey(paintContextSampleInfluence=False)
|
||||
|
||||
|
||||
@withSession
|
||||
def select_paint_brush_intensity():
|
||||
from ngSkinTools2.ui.brush_settings_popup import brush_settings_popup
|
||||
|
||||
brush_settings_popup(session.paint_tool)
|
||||
|
||||
|
||||
@withSession
|
||||
def paint_tool_toggle_original_mesh():
|
||||
paint = session.paint_tool
|
||||
paint.display_settings.display_node_visible = not paint.display_settings.display_node_visible
|
||||
session.events.toolChanged.emit()
|
||||
|
||||
|
||||
@withSession
|
||||
def paint_tool_cycle_weights_display_mode():
|
||||
"""
|
||||
cycle current display mode "all influences" -> "current influence" -> "current influence colored"
|
||||
:return:
|
||||
"""
|
||||
paint = session.paint_tool
|
||||
|
||||
targets = session.state.currentInfluence.targets
|
||||
is_mask_mode = targets is not None and len(targets) == 1 and targets[0] == NamedPaintTarget.MASK
|
||||
|
||||
settings = paint.display_settings
|
||||
if is_mask_mode:
|
||||
settings.mask_display_mode = {
|
||||
MaskDisplayMode.default_: MaskDisplayMode.color_ramp,
|
||||
MaskDisplayMode.color_ramp: MaskDisplayMode.default_,
|
||||
}.get(settings.mask_display_mode, MaskDisplayMode.default_)
|
||||
else:
|
||||
settings.weights_display_mode = {
|
||||
WeightsDisplayMode.allInfluences: WeightsDisplayMode.currentInfluence,
|
||||
WeightsDisplayMode.currentInfluence: WeightsDisplayMode.currentInfluenceColored,
|
||||
WeightsDisplayMode.currentInfluenceColored: WeightsDisplayMode.allInfluences,
|
||||
}.get(settings.weights_display_mode, WeightsDisplayMode.allInfluences)
|
||||
|
||||
session.events.toolChanged.emit()
|
||||
200
2023/scripts/rigging_tools/ngskintools2/ui/hotkeys_setup.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Maya internals dissection, comments are ours. similar example available in "command|hotkey code", see "Here's an example
|
||||
of how to create runtimeCommand with a certain hotkey context"
|
||||
|
||||
|
||||
```
|
||||
|
||||
// add new hotkey ctx
|
||||
// t: Specifies the context type. It's used together with the other flags such as "currentClient", "addClient",
|
||||
// "removeClient" and so on.
|
||||
// ac: Associates a client to the given hotkey context type. This flag needs to be used with the flag "type" which
|
||||
// specifies the context type.
|
||||
hotkeyCtx -t "Tool" -ac "sculptMeshCache";
|
||||
|
||||
// create new runtime command, associate with created context
|
||||
runTimeCommand -default true
|
||||
-annotation (uiRes("m_defaultRunTimeCommands.kModifySizePressAnnot"))
|
||||
-category ("Other items.Brush Tools")
|
||||
-command ("if ( `contextInfo -ex sculptMeshCacheContext`) sculptMeshCacheCtx -e -adjustSize 1 sculptMeshCacheContext;")
|
||||
-hotkeyCtx ("sculptMeshCache")
|
||||
SculptMeshActivateBrushSize;
|
||||
|
||||
// create named command for the runtime command
|
||||
nameCommand
|
||||
-annotation "Start adjust size"
|
||||
-command ("SculptMeshActivateBrushSize")
|
||||
SculptMeshActivateBrushSizeNameCommand;
|
||||
|
||||
// assign hotkey for name command
|
||||
hotkey -keyShortcut "b" -name ("SculptMeshActivateBrushSizeNameCommand") -releaseName ("SculptMeshDeactivateBrushSizeNameCommand");
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import is_string
|
||||
|
||||
from . import hotkeys
|
||||
|
||||
hotkeySetName = 'ngSkinTools2'
|
||||
context = 'ngst2PaintContext'
|
||||
command_prefix = "ngskintools_2_" # maya breaks command name on capital letters and before numbers, so this will ensure that all command names start with "ngskintools 2 "
|
||||
|
||||
log = getLogger("hotkeys setup")
|
||||
|
||||
|
||||
def uninstall_hotkeys():
|
||||
if cmds.hotkeySet(hotkeySetName, q=True, exists=True):
|
||||
cmds.hotkeySet(hotkeySetName, e=True, delete=True)
|
||||
|
||||
|
||||
def setup_named_commands():
|
||||
# "default" mode will force a read-only behavior for runTimCommands
|
||||
# only turn this on for production mode
|
||||
import ngSkinTools2
|
||||
|
||||
append_only_mode = not ngSkinTools2.DEBUG_MODE
|
||||
|
||||
def add_command(name, annotation, command, context=None):
|
||||
if not is_string(command):
|
||||
command = function_link(command)
|
||||
|
||||
runtime_command_name = command_prefix + name
|
||||
|
||||
# delete (if exists) and recreate runtime command
|
||||
if not append_only_mode and cmds.runTimeCommand(runtime_command_name, q=True, exists=True):
|
||||
cmds.runTimeCommand(runtime_command_name, e=True, delete=True)
|
||||
|
||||
if not cmds.runTimeCommand(runtime_command_name, q=True, exists=True):
|
||||
additional_args = {}
|
||||
if context is not None:
|
||||
additional_args['hotkeyCtx'] = context
|
||||
|
||||
cmds.runTimeCommand(
|
||||
runtime_command_name,
|
||||
category="Other items.ngSkinTools2",
|
||||
default=append_only_mode,
|
||||
annotation=annotation,
|
||||
command=command,
|
||||
commandLanguage="python",
|
||||
**additional_args
|
||||
)
|
||||
|
||||
cmds.nameCommand(
|
||||
command_prefix + name + "NameCommand",
|
||||
annotation=annotation + "-",
|
||||
sourceType="python",
|
||||
default=append_only_mode,
|
||||
command=runtime_command_name,
|
||||
)
|
||||
|
||||
def add_toggle(name, annotation, command_on, command_off, context=None):
|
||||
add_command(name + 'On', annotation=annotation, command=command_on, context=context)
|
||||
add_command(name + 'Off', annotation=annotation + "(release)", command=command_off, context=context)
|
||||
|
||||
add_toggle(
|
||||
'BrushSize',
|
||||
annotation='Toggle brush size mode',
|
||||
command_on=hotkeys.paint_tool_brush_size,
|
||||
command_off=hotkeys.paint_tool_brush_size_release,
|
||||
context=context,
|
||||
)
|
||||
|
||||
add_command('ToggleHelp', annotation='toggle help', command=hotkeys.paint_tool_toggle_help, context=context)
|
||||
add_command('ViewFitInfluence', annotation='fit influence in view', command=hotkeys.paint_tool_focus_current_influence, context=context)
|
||||
add_toggle(
|
||||
'SampleInfluence',
|
||||
annotation='Sample influence',
|
||||
command_on=hotkeys.paint_tool_sample_influence,
|
||||
command_off=hotkeys.paint_tool_sample_influence_release,
|
||||
context=context,
|
||||
)
|
||||
|
||||
add_command("SetBrushIntensity", annotation="set brush intensity", command=hotkeys.select_paint_brush_intensity, context=context)
|
||||
add_command("PaintFlood", annotation="apply current brush to all vertices", command=hotkeys.paint_tool_flood, context=context)
|
||||
|
||||
add_command("Paint", annotation="start paint tool", command=hotkeys.paint_tool_start)
|
||||
add_command(
|
||||
"ToggleOriginalMesh",
|
||||
annotation="toggle between weights display and original mesh while painting",
|
||||
command=hotkeys.paint_tool_toggle_original_mesh,
|
||||
)
|
||||
add_command(
|
||||
"CycleWeightsDisplayMode",
|
||||
annotation='Cycle weights display mode "all influences" -> "current influence" -> "current influence colored"',
|
||||
command=hotkeys.paint_tool_cycle_weights_display_mode,
|
||||
)
|
||||
|
||||
|
||||
def define_hotkeys():
|
||||
setup_named_commands()
|
||||
|
||||
def nc(name_command_short_name):
|
||||
return command_prefix + name_command_short_name + "NameCommand"
|
||||
|
||||
# cmds.hotkey(k="b", name=nc("BrushSizeOn"), releaseName=nc("BrushSizeOff"))
|
||||
cmds.hotkey(keyShortcut="b", name=nc("BrushSizeOn"), releaseName=nc("BrushSizeOff"))
|
||||
cmds.hotkey(keyShortcut="i", name=nc("SetBrushIntensity"))
|
||||
cmds.hotkey(keyShortcut="f", ctrlModifier=True, name=nc("PaintFlood"))
|
||||
cmds.hotkey(keyShortcut="f", name=nc("ViewFitInfluence"))
|
||||
cmds.hotkey(keyShortcut="h", name=nc("ToggleHelp"))
|
||||
|
||||
cmds.hotkey(keyShortcut="s", name=nc("SampleInfluenceOn"), releaseName=nc("SampleInfluenceOff"))
|
||||
cmds.hotkey(keyShortcut="d", name=nc("CycleWeightsDisplayMode"))
|
||||
cmds.hotkey(keyShortcut="t", name=nc("ToggleOriginalMesh"))
|
||||
|
||||
|
||||
def install_hotkeys():
|
||||
uninstall_hotkeys()
|
||||
|
||||
__hotkey_set_handler.remember()
|
||||
try:
|
||||
if cmds.hotkeySet(hotkeySetName, q=True, exists=True):
|
||||
cmds.hotkeySet(hotkeySetName, e=True, current=True)
|
||||
else:
|
||||
cmds.hotkeySet(hotkeySetName, current=True)
|
||||
|
||||
cmds.hotkeyCtx(addClient=context, type='Tool')
|
||||
|
||||
define_hotkeys()
|
||||
finally:
|
||||
__hotkey_set_handler.restore()
|
||||
|
||||
|
||||
def function_link(fun):
|
||||
return "import {module}; {module}.{fn}()".format(module=fun.__module__, fn=fun.__name__)
|
||||
|
||||
|
||||
class HotkeySetHandler:
|
||||
def __init__(self):
|
||||
log.info("initializing new hotkey set handler")
|
||||
self.prev_hotkey_set = None
|
||||
|
||||
def remember(self):
|
||||
if self.prev_hotkey_set is not None:
|
||||
return
|
||||
|
||||
log.info("remembering current hotkey set")
|
||||
self.prev_hotkey_set = cmds.hotkeySet(q=True, current=True)
|
||||
|
||||
def restore(self):
|
||||
if self.prev_hotkey_set is None:
|
||||
return
|
||||
|
||||
log.info("restoring previous hotkey set")
|
||||
cmds.hotkeySet(self.prev_hotkey_set, e=True, current=True)
|
||||
self.prev_hotkey_set = None
|
||||
|
||||
|
||||
__hotkey_set_handler = HotkeySetHandler()
|
||||
|
||||
|
||||
def toggle_paint_hotkey_set(enabled):
|
||||
if enabled:
|
||||
__hotkey_set_handler.remember()
|
||||
cmds.hotkeySet(hotkeySetName, e=True, current=True)
|
||||
else:
|
||||
__hotkey_set_handler.restore()
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-eye-fill"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1488"
|
||||
sodipodi:docname="eye-fill.svg"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
|
||||
<metadata
|
||||
id="metadata1494">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1492" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2080"
|
||||
id="namedview1490"
|
||||
showgrid="false"
|
||||
inkscape:zoom="63"
|
||||
inkscape:cx="8"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-x="3829"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1488" />
|
||||
<path
|
||||
d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"
|
||||
id="path1484"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"
|
||||
id="path1486"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-eye-slash-fill"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg2073"
|
||||
sodipodi:docname="eye-slash-fill.svg"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
|
||||
<metadata
|
||||
id="metadata2079">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2077" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2080"
|
||||
id="namedview2075"
|
||||
showgrid="false"
|
||||
inkscape:zoom="63"
|
||||
inkscape:cx="8"
|
||||
inkscape:cy="10.539683"
|
||||
inkscape:window-x="3829"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2073" />
|
||||
<path
|
||||
d="M10.79 12.912l-1.614-1.615a3.5 3.5 0 0 1-4.474-4.474l-2.06-2.06C.938 6.278 0 8 0 8s3 5.5 8 5.5a7.027 7.027 0 0 0 2.79-.588zM5.21 3.088A7.028 7.028 0 0 1 8 2.5c5 0 8 5.5 8 5.5s-.939 1.721-2.641 3.238l-2.062-2.062a3.5 3.5 0 0 0-4.474-4.474L5.21 3.088z"
|
||||
id="path2069"
|
||||
style="fill:#bababa;fill-opacity:1" />
|
||||
<path
|
||||
d="M5.525 7.646a2.5 2.5 0 0 0 2.829 2.829l-2.83-2.829zm4.95.708l-2.829-2.83a2.5 2.5 0 0 1 2.829 2.829zm3.171 6l-12-12 .708-.708 12 12-.708.707z"
|
||||
id="path2071"
|
||||
style="fill:#bababa;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16">
|
||||
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
||||
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299l.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884l-12-12 .708-.708 12 12-.708.708z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 893 B |
61
2023/scripts/rigging_tools/ngskintools2/ui/images/eye.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-eye"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg875"
|
||||
sodipodi:docname="eye.svg"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
|
||||
<metadata
|
||||
id="metadata881">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs879" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2133"
|
||||
inkscape:window-height="1414"
|
||||
id="namedview877"
|
||||
showgrid="false"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:zoom="63"
|
||||
inkscape:cx="8"
|
||||
inkscape:cy="9.2698413"
|
||||
inkscape:window-x="4867"
|
||||
inkscape:window-y="429"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg875" />
|
||||
<path
|
||||
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"
|
||||
id="path871"
|
||||
style="stroke-width:1.0015748;stroke-miterlimit:4;stroke-dasharray:none;stroke:none;stroke-opacity:1;fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"
|
||||
id="path873"
|
||||
style="stroke-width:1.0015748;stroke-miterlimit:4;stroke-dasharray:none;fill:#ffffff;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 18 KiB |
BIN
2023/scripts/rigging_tools/ngskintools2/ui/images/logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
131
2023/scripts/rigging_tools/ngskintools2/ui/images/logo.svg
Normal file
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="42"
|
||||
height="42"
|
||||
viewBox="0 0 11.1125 11.1125"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
|
||||
sodipodi:docname="logo-colored2.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="10"
|
||||
inkscape:cx="57.337866"
|
||||
inkscape:cy="29.075965"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="g968"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2080"
|
||||
inkscape:window-x="3829"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:pagecheckerboard="false"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="g968"
|
||||
inkscape:label="main"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path1008"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#e6f7e1;fill-opacity:1;fill-rule:evenodd;stroke:#394241;stroke-width:0.381;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 10.815145,5.5547744 A 5.2636318,5.2687149 0 0 1 5.5515153,10.823488 5.2636318,5.2687149 0 0 1 0.28788462,5.5547744 5.2636318,5.2687149 0 0 1 5.5515153,0.2860595 5.2636318,5.2687149 0 0 1 10.815145,5.5547744 Z"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#8ed07d;fill-opacity:1;stroke:#394241;stroke-width:0.132292;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 10.071522,5.5547744 A 4.520009,4.5243745 0 0 1 5.5515153,10.079147 4.520009,4.5243745 0 0 1 1.0315065,5.5547744 4.520009,4.5243745 0 0 1 5.5515153,1.0303994 4.520009,4.5243745 0 0 1 10.071522,5.5547744 Z"
|
||||
id="path1010"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1012"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#e4f6de;fill-opacity:1;fill-rule:evenodd;stroke:#394241;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 8.2782313,3.4978104 a 0.99620533,0.99716729 0 0 1 -0.996205,0.997168 0.99620533,0.99716729 0 0 1 -0.996206,-0.997168 0.99620533,0.99716729 0 0 1 0.996206,-0.997167 0.99620533,0.99716729 0 0 1 0.996205,0.997167 z"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1014"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#394241;fill-opacity:1;stroke:#394241;stroke-width:0.132292;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 6.1665103,6.0291114 0.362793,-1.729342 m 0,0 -1.678701,0.935426 c 0,0 0.507188,0.0474 0.759757,0.206425 0.25257,0.159026 0.556151,0.587491 0.556151,0.587491"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1016"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:none;stroke:#394241;stroke-width:0.132;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2.8789345,7.9067184 -0.5782041,0.785102 m 5.8312779,-0.953399 0.673759,0.98138 m -3.200692,0.07241 0.02713,1.2964136 M 2.8293132,7.5401614 C 2.0841484,7.0595114 1.5609154,6.3432344 1.0846774,5.1276854 m 5.2510059,3.037117 c 1.565971,-0.02362 3.1253259,-1.1282499 3.7386737,-2.2919899"
|
||||
sodipodi:nodetypes="cccccccccc"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<g
|
||||
id="g1026"
|
||||
style="fill:#45954d;fill-opacity:1;stroke:none;stroke-opacity:1"
|
||||
transform="translate(0.00677962,-0.03127322)"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001">
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 8.505902,8.1900327 8.2143526,7.7654947 8.4591298,7.6195127 c 0.5506069,-0.328376 1.047841,-0.767506 1.4030352,-1.239084 l 0.07046,-0.09355 -0.0128,0.07484 c -0.082767,0.48376 -0.30625,1.076837 -0.5745653,1.524772 -0.1008621,0.168383 -0.3570904,0.517576 -0.4764757,0.64935 l -0.071327,0.07873 z"
|
||||
id="path1018"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 5.6947136,9.78319 C 5.6879536,9.6422617 5.6828286,9.3872697 5.6833236,9.2165407 l 8.996e-4,-0.310415 0.1047906,-0.06548 c 0.1368939,-0.08554 0.3882578,-0.323017 0.4904542,-0.463355 l 0.080931,-0.111135 0.2082474,-0.01182 c 0.3803844,-0.02159 0.8510395,-0.141515 1.297223,-0.330527 l 0.2096587,-0.08882 0.3001579,0.43557 c 0.1650868,0.239563 0.3001579,0.443507 0.3001579,0.453207 0,0.02468 -0.1876353,0.200193 -0.3598442,0.336599 C 7.8780736,9.4072457 7.3539626,9.68306 6.8315378,9.841564 6.5909701,9.914554 6.2021559,9.993084 6.000476,10.00942 5.923302,10.01572 5.8256991,10.02498 5.7835811,10.03011 l -0.076578,0.0093 z"
|
||||
id="path1020"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 5.1398679,10.017257 C 4.1884222,9.933694 3.2165381,9.4967897 2.5160663,8.8377427 l -0.1261678,-0.118706 0.21553,-0.289988 c 0.1185414,-0.159492 0.2216215,-0.291499 0.2290666,-0.293348 0.00745,-0.0019 0.059841,0.06838 0.1164354,0.156066 0.1213623,0.188035 0.3700815,0.435839 0.5621639,0.560097 0.5624248,0.36383 1.2599882,0.437436 1.8765803,0.198012 l 0.1430793,-0.05556 v 0.294403 c 0,0.161922 0.0055,0.3967533 0.012229,0.5218473 l 0.012229,0.227444 -0.1198055,-0.0026 c -0.065893,-0.0014 -0.1997861,-0.0096 -0.2975398,-0.01818 z"
|
||||
id="path1022"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 2.1635305,8.4698927 C 1.9097453,8.1663167 1.7204681,7.8762037 1.5570923,7.5403777 1.2671987,6.9444897 1.1262435,6.3732707 1.1069707,5.7162637 l -0.00851,-0.289987 0.041376,0.09354 c 0.097201,0.219754 0.3172379,0.649387 0.4168077,0.813835 0.2559593,0.422741 0.6076541,0.835074 0.9264318,1.086166 0.1353842,0.106639 0.1490128,0.123649 0.1696878,0.211798 0.012364,0.05272 0.0418,0.147838 0.065413,0.211382 l 0.042933,0.115534 -0.2344342,0.319447 c -0.1289388,0.175695 -0.2398767,0.318883 -0.2465287,0.318195 -0.00665,-6.88e-4 -0.059131,-0.05752 -0.1166208,-0.126285 z"
|
||||
id="path1024"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
</g>
|
||||
<path
|
||||
id="path1028"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#e4f6de;fill-opacity:1;fill-rule:evenodd;stroke:#394241;stroke-width:0.27;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 6.5574843,7.1065184 A 1.9072757,1.9091177 0 0 1 4.6502091,9.0156364 1.9072757,1.9091177 0 0 1 2.7429334,7.1065184 1.9072757,1.9091177 0 0 1 4.6502091,5.1974004 1.9072757,1.9091177 0 0 1 6.5574843,7.1065184 Z"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1030"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:#000000;fill-opacity:0;stroke:#394241;stroke-width:0.132292;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 4.5859576,9.0166314 c -0.4550684,-0.740982 -0.536541,-2.94365 0.010119,-3.784058 m 1.9302377,1.867597 c -1.222413,0.573856 -2.9362113,0.537553 -3.865322,-0.01532"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
360
2023/scripts/rigging_tools/ngskintools2/ui/influenceMappingUI.py
Normal file
@@ -0,0 +1,360 @@
|
||||
from ngSkinTools2 import cleanup, signal
|
||||
from ngSkinTools2.api import influenceMapping, mirror
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.signal import Signal
|
||||
from ngSkinTools2.ui import dialogs, qt, widgets
|
||||
from ngSkinTools2.ui.dialogs import yesNo
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
from ngSkinTools2.ui.options import config
|
||||
from ngSkinTools2.ui.widgets import NumberSliderGroup
|
||||
|
||||
log = getLogger("influence mapping UI")
|
||||
|
||||
|
||||
def open_ui_for_mesh(ui_parent, mesh):
|
||||
m = mirror.Mirror(mesh)
|
||||
mapper = m.build_influences_mapper()
|
||||
|
||||
def do_apply(mapping):
|
||||
m.set_influences_mapping(mapping)
|
||||
m.save_influences_mapper(mapper)
|
||||
|
||||
return open_as_dialog(ui_parent, mapper, do_apply)
|
||||
|
||||
|
||||
def open_as_dialog(parent, matcher, result_callback):
|
||||
"""
|
||||
|
||||
:type matcher: ngSkinTools2.api.influenceMapping.InfluenceMapping
|
||||
"""
|
||||
main_layout, reload_ui, recalc_matches = build_ui(parent, matcher)
|
||||
|
||||
def button_row(window):
|
||||
def apply():
|
||||
result_callback(matcher.asIntIntMapping(matcher.calculatedMapping))
|
||||
window.close()
|
||||
|
||||
def save_defaults():
|
||||
if not yesNo("Save current settings as default?"):
|
||||
return
|
||||
config.mirrorInfluencesDefaults = matcher.config.as_json()
|
||||
|
||||
def load_defaults():
|
||||
matcher.config.load_json(config.mirrorInfluencesDefaults)
|
||||
reload_ui()
|
||||
recalc_matches()
|
||||
|
||||
return widgets.button_row(
|
||||
[
|
||||
("Apply", apply),
|
||||
("Cancel", window.close),
|
||||
],
|
||||
side_menu=[
|
||||
("Save As Default", save_defaults),
|
||||
("Load Defaults", load_defaults),
|
||||
],
|
||||
)
|
||||
|
||||
window = QtWidgets.QDialog(parent)
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
window.setWindowTitle("Influence Mirror Mapping")
|
||||
window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
window.resize(720 * scale_multiplier, 500 * scale_multiplier)
|
||||
window.setLayout(QtWidgets.QVBoxLayout())
|
||||
window.layout().addWidget(main_layout)
|
||||
window.layout().addLayout(button_row(window))
|
||||
|
||||
window.show()
|
||||
|
||||
recalc_matches()
|
||||
|
||||
return window
|
||||
|
||||
|
||||
def build_ui(parent, matcher):
|
||||
"""
|
||||
|
||||
:param parent: parent qt widget
|
||||
:type matcher: influenceMapping.InfluenceMapping
|
||||
"""
|
||||
|
||||
influence_data = matcher.influences
|
||||
|
||||
influenceMapping.calcShortestUniqueName(matcher.influences)
|
||||
if matcher.destinationInfluences is not None and matcher.destinationInfluences != matcher.influences:
|
||||
influenceMapping.calcShortestUniqueName(matcher.destinationInfluences)
|
||||
|
||||
update_globs = Signal("need recalc")
|
||||
reload_ui = Signal("reload_ui")
|
||||
|
||||
mirror_mode = matcher.config.mirror_axis is not None
|
||||
|
||||
def build_tree_hierarchy(tree_view):
|
||||
tree_items = {} # mapping of path->treeItem
|
||||
influence_items = {} # same as above, only includes non-intermediate items
|
||||
|
||||
def find_item(path, is_intermediate):
|
||||
result = tree_items.get(path, None)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
split_path = path.rsplit("|", 1)
|
||||
parent_path, name = split_path if len(split_path) == 2 else ["", split_path[0]]
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem([name, '-', '(not in skin cluster)' if is_intermediate else '?'])
|
||||
tree_items[path] = item
|
||||
|
||||
parent_item = None if parent_path == "" else find_item(parent_path, True)
|
||||
|
||||
if parent_item is not None:
|
||||
parent_item.addChild(item)
|
||||
else:
|
||||
tree_view.addTopLevelItem(item)
|
||||
|
||||
item.setExpanded(True)
|
||||
|
||||
return item
|
||||
|
||||
for i in influence_data:
|
||||
influence_items[i.path_name()] = find_item(i.path_name(), False)
|
||||
|
||||
return influence_items
|
||||
|
||||
def tolerance():
|
||||
result = NumberSliderGroup(min_value=0.001, max_value=10)
|
||||
result.spinner.setDecimals(3)
|
||||
|
||||
@signal.on(reload_ui)
|
||||
def reload():
|
||||
with qt.signals_blocked(result):
|
||||
result.set_value(matcher.config.distance_threshold)
|
||||
|
||||
@signal.on(result.valueChanged)
|
||||
def changed():
|
||||
matcher.config.distance_threshold = result.value()
|
||||
recalcMatches()
|
||||
|
||||
reload()
|
||||
|
||||
return result
|
||||
|
||||
def pattern():
|
||||
result = QtWidgets.QTableWidget()
|
||||
result.setColumnCount(2)
|
||||
result.setHorizontalHeaderLabels(["Pattern", "Opposite"] if mirror_mode else ["Source", "Destination"])
|
||||
result.setEditTriggers(QtWidgets.QTableWidget.AllEditTriggers)
|
||||
|
||||
result.verticalHeader().setVisible(False)
|
||||
result.verticalHeader().setDefaultSectionSize(20)
|
||||
|
||||
item_font = QtGui.QFont("Courier New", 12)
|
||||
item_font.setStyleHint(QtGui.QFont.Monospace)
|
||||
|
||||
@signal.on(reload_ui)
|
||||
def reload_patterns():
|
||||
with qt.signals_blocked(result):
|
||||
result.setRowCount(len(matcher.config.globs) + 1)
|
||||
for rowIndex, patterns in enumerate(matcher.config.globs + [('', '')]):
|
||||
for colIndex, p in enumerate(patterns):
|
||||
item = QtWidgets.QTableWidgetItem(p)
|
||||
item.setFont(item_font)
|
||||
result.setItem(rowIndex, colIndex, item)
|
||||
|
||||
reload_patterns()
|
||||
|
||||
@signal.on(update_globs)
|
||||
def update_matcher_globs():
|
||||
globs = []
|
||||
|
||||
def text(r, c):
|
||||
item = result.item(r, c)
|
||||
if item is None:
|
||||
return ""
|
||||
return item.text().strip()
|
||||
|
||||
for row in range(result.rowCount()):
|
||||
v1 = text(row, 0)
|
||||
v2 = text(row, 1)
|
||||
if v1 != "" and v2 != "":
|
||||
globs.append((v1, v2))
|
||||
|
||||
matcher.config.globs = globs
|
||||
recalcMatches()
|
||||
|
||||
@qt.on(result.itemChanged)
|
||||
def item_changed(item):
|
||||
log.debug("item changed")
|
||||
item.setText(item.text().strip())
|
||||
|
||||
try:
|
||||
influenceMapping.validate_glob(item.text().strip())
|
||||
except Exception as err:
|
||||
dialogs.displayError(str(err))
|
||||
item.setText(influenceMapping.illegalCharactersRegexp.sub("", item.text()))
|
||||
|
||||
if item.row() != result.rowCount() - 1:
|
||||
if item.text().strip() == "":
|
||||
result.removeRow(item.row())
|
||||
|
||||
# ensure one empty line at the end
|
||||
rows = result.rowCount()
|
||||
last_item = result.item(rows - 1, 0)
|
||||
if last_item and last_item.text() != "":
|
||||
result.setRowCount(rows + 1)
|
||||
|
||||
update_matcher_globs()
|
||||
|
||||
return result
|
||||
|
||||
def automaticRules():
|
||||
form = QtWidgets.QFormLayout()
|
||||
use_joint_names = QtWidgets.QCheckBox("Match by joint name")
|
||||
naming_patterns = pattern()
|
||||
use_position = QtWidgets.QCheckBox("Match by position")
|
||||
tolerance_scroll = tolerance()
|
||||
use_joint_labels = QtWidgets.QCheckBox("Match by joint label")
|
||||
use_dg_links = QtWidgets.QCheckBox("Match by dependency graph links")
|
||||
|
||||
def update_enabled_disabled():
|
||||
def enable_form_row(form_item, e):
|
||||
form_item.setEnabled(e)
|
||||
form.labelForField(form_item).setEnabled(e)
|
||||
|
||||
checked = use_joint_names.isChecked()
|
||||
enable_form_row(naming_patterns, checked)
|
||||
|
||||
checked = use_position.isChecked()
|
||||
tolerance_scroll.set_enabled(checked)
|
||||
form.labelForField(tolerance_scroll.layout()).setEnabled(checked)
|
||||
|
||||
enable_form_row(dg_attribute, use_dg_links.isChecked())
|
||||
|
||||
@qt.on(use_joint_names.toggled, use_position.toggled, use_joint_labels.toggled, use_dg_links.toggled)
|
||||
def use_joint_names_toggled():
|
||||
update_enabled_disabled()
|
||||
matcher.config.use_name_matching = use_joint_names.isChecked()
|
||||
matcher.config.use_distance_matching = use_position.isChecked()
|
||||
matcher.config.use_label_matching = use_joint_labels.isChecked()
|
||||
matcher.config.use_dg_link_matching = use_dg_links.isChecked()
|
||||
recalcMatches()
|
||||
|
||||
dg_attribute = QtWidgets.QLineEdit()
|
||||
|
||||
@qt.on(dg_attribute.editingFinished)
|
||||
def use_joint_names_toggled():
|
||||
matcher.config.dg_destination_attribute = str(dg_attribute.text()).strip()
|
||||
recalcMatches()
|
||||
|
||||
@signal.on(reload_ui)
|
||||
def update_values():
|
||||
with qt.signals_blocked(dg_attribute):
|
||||
dg_attribute.setText(matcher.config.dg_destination_attribute)
|
||||
with qt.signals_blocked(use_joint_names):
|
||||
use_joint_names.setChecked(matcher.config.use_name_matching)
|
||||
with qt.signals_blocked(use_position):
|
||||
use_position.setChecked(matcher.config.use_distance_matching)
|
||||
with qt.signals_blocked(use_joint_labels):
|
||||
use_joint_labels.setChecked(matcher.config.use_label_matching)
|
||||
with qt.signals_blocked(use_dg_links):
|
||||
use_dg_links.setChecked(matcher.config.use_dg_link_matching)
|
||||
update_enabled_disabled()
|
||||
|
||||
g = QtWidgets.QGroupBox("Rules")
|
||||
g.setLayout(form)
|
||||
form.addRow(use_dg_links)
|
||||
form.addRow("Attribute name:", dg_attribute)
|
||||
form.addRow(use_joint_labels)
|
||||
form.addRow(use_joint_names)
|
||||
form.addRow("Naming scheme:", naming_patterns)
|
||||
form.addRow(use_position)
|
||||
form.addRow("Position tolerance:", tolerance_scroll.layout())
|
||||
|
||||
update_values()
|
||||
return g
|
||||
|
||||
def scriptedRules():
|
||||
g = QtWidgets.QGroupBox("Scripted rules")
|
||||
g.setLayout(QtWidgets.QVBoxLayout())
|
||||
g.layout().addWidget(QtWidgets.QLabel("TODO"))
|
||||
return g
|
||||
|
||||
def manualRules():
|
||||
g = QtWidgets.QGroupBox("Manual overrides")
|
||||
g.setLayout(QtWidgets.QVBoxLayout())
|
||||
g.layout().addWidget(QtWidgets.QLabel("TODO"))
|
||||
return g
|
||||
|
||||
leftSide = QtWidgets.QScrollArea()
|
||||
leftSide.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
leftSide.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
leftSide.setWidgetResizable(True)
|
||||
|
||||
l = QtWidgets.QVBoxLayout()
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
l.addWidget(automaticRules())
|
||||
# l.addWidget(scriptedRules())
|
||||
# l.addWidget(manualRules())
|
||||
# l.addStretch()
|
||||
|
||||
leftSide.setWidget(qt.wrap_layout_into_widget(l))
|
||||
|
||||
def createMappingView():
|
||||
view = QtWidgets.QTreeWidget()
|
||||
view.setColumnCount(3)
|
||||
view.setHeaderLabels(["Source", "Destination", "Matched by rule"])
|
||||
view.setIndentation(7)
|
||||
view.setExpandsOnDoubleClick(False)
|
||||
|
||||
usedItems = build_tree_hierarchy(view)
|
||||
|
||||
linkedItemRole = QtCore.Qt.UserRole + 1
|
||||
|
||||
def previewMapping(mapping):
|
||||
"""
|
||||
|
||||
:type mapping: dict[InfluenceInfo, InfluenceInfo]
|
||||
"""
|
||||
for treeItem in list(usedItems.values()):
|
||||
treeItem.setText(1, "(not matched)")
|
||||
treeItem.setText(2, "")
|
||||
|
||||
for k, v in list(mapping.items()):
|
||||
treeItem = usedItems.get(k.path_name(), None)
|
||||
if treeItem is None:
|
||||
continue
|
||||
treeItem.setText(1, "(self)" if k == v['infl'] else v["infl"].shortestPath)
|
||||
treeItem.setText(2, v["matchedRule"])
|
||||
treeItem.setData(1, linkedItemRole, v["infl"].path)
|
||||
|
||||
@qt.on(view.itemDoubleClicked)
|
||||
def itemDoubleClicked(item, column):
|
||||
item.setExpanded(True)
|
||||
|
||||
linkedItemPath = item.data(1, linkedItemRole)
|
||||
item = usedItems.get(linkedItemPath, None)
|
||||
if item is not None:
|
||||
item.setSelected(True)
|
||||
view.scrollToItem(item)
|
||||
|
||||
return view, previewMapping
|
||||
|
||||
def recalcMatches():
|
||||
matches = matcher.calculate()
|
||||
mappingView_updateMatches(matches)
|
||||
|
||||
g = QtWidgets.QGroupBox("Calculated mapping")
|
||||
g.setLayout(QtWidgets.QVBoxLayout())
|
||||
mappingView, mappingView_updateMatches = createMappingView()
|
||||
g.layout().addWidget(mappingView)
|
||||
|
||||
mainLayout = QtWidgets.QSplitter(orientation=QtCore.Qt.Horizontal, parent=parent)
|
||||
mainLayout.addWidget(leftSide)
|
||||
mainLayout.addWidget(g)
|
||||
|
||||
mainLayout.setStretchFactor(0, 10)
|
||||
mainLayout.setStretchFactor(1, 10)
|
||||
mainLayout.setCollapsible(0, True)
|
||||
mainLayout.setSizes([200] * 2)
|
||||
|
||||
return mainLayout, reload_ui.emit, recalcMatches
|
||||
315
2023/scripts/rigging_tools/ngskintools2/ui/influencesview.py
Normal file
@@ -0,0 +1,315 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import influence_names
|
||||
from ngSkinTools2.api.influenceMapping import InfluenceInfo
|
||||
from ngSkinTools2.api.layers import Layer
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.target_info import list_influences
|
||||
from ngSkinTools2.ui import actions, qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
from ngSkinTools2.ui.options import Config, config
|
||||
|
||||
log = getLogger("influencesView")
|
||||
_ = Layer # only imported for type reference
|
||||
|
||||
|
||||
def build_used_influences_action(parent):
|
||||
def toggle():
|
||||
config.influences_show_used_influences_only.set(not config.influences_show_used_influences_only())
|
||||
|
||||
result = actions.define_action(
|
||||
parent,
|
||||
"Used Influences Only",
|
||||
callback=toggle,
|
||||
tooltip="If enabled, influences view will only show influences that have weights on current layer",
|
||||
)
|
||||
|
||||
@signal.on(config.influences_show_used_influences_only.changed, qtParent=parent)
|
||||
def update():
|
||||
result.setChecked(config.influences_show_used_influences_only())
|
||||
|
||||
result.setCheckable(True)
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
def build_set_influences_sorted_action(parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def toggle():
|
||||
new_value = Config.InfluencesSortDescending
|
||||
if config.influences_sort() == new_value:
|
||||
new_value = Config.InfluencesSortUnsorted
|
||||
config.influences_sort.set(new_value)
|
||||
|
||||
result = actions.define_action(
|
||||
parent,
|
||||
"Show influences sorted",
|
||||
callback=toggle,
|
||||
tooltip="Sort influences by name",
|
||||
)
|
||||
|
||||
@signal.on(config.influences_show_used_influences_only.changed, qtParent=parent)
|
||||
def update():
|
||||
result.setChecked(config.influences_sort() == Config.InfluencesSortDescending)
|
||||
|
||||
result.setCheckable(True)
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
icon_mask = QtGui.QIcon(":/blendColors.svg")
|
||||
icon_dq = QtGui.QIcon(":/rotate_M.png")
|
||||
icon_joint = QtGui.QIcon(":/joint.svg")
|
||||
icon_joint_disabled = qt.image_icon("joint_disabled.png")
|
||||
icon_transform = QtGui.QIcon(":/cube.png")
|
||||
icon_transform_disabled = qt.image_icon("cube_disabled.png")
|
||||
|
||||
|
||||
def build_view(parent, actions, session, filter):
|
||||
"""
|
||||
:param parent: ui parent
|
||||
:type actions: ngSkinTools2.ui.actions.Actions
|
||||
:type session: ngSkinTools2.ui.session.Session
|
||||
:type filter: InfluenceNameFilter
|
||||
"""
|
||||
|
||||
icon_locked = QtGui.QIcon(":/Lock_ON.png")
|
||||
icon_unlocked = QtGui.QIcon(":/Lock_OFF_grey.png")
|
||||
|
||||
id_role = QtCore.Qt.UserRole + 1
|
||||
item_size_hint = QtCore.QSize(25 * scale_multiplier, 25 * scale_multiplier)
|
||||
|
||||
def get_item_id(item):
|
||||
if item is None:
|
||||
return None
|
||||
return item.data(0, id_role)
|
||||
|
||||
tree_items = {}
|
||||
|
||||
def build_items(view, items, layer):
|
||||
# type: (QtWidgets.QTreeWidget, list[InfluenceInfo], Layer) -> None
|
||||
is_group_layer = layer is not None and layer.num_children != 0
|
||||
|
||||
def rebuild_buttons(item, item_id, buttons):
|
||||
bar = QtWidgets.QToolBar(parent=parent)
|
||||
bar.setMovable(False)
|
||||
bar.setIconSize(QtCore.QSize(13 * scale_multiplier, 13 * scale_multiplier))
|
||||
|
||||
def add_or_remove(input_list, items, should_add):
|
||||
if should_add:
|
||||
return list(input_list) + list(items)
|
||||
return [i for i in input_list if i not in items]
|
||||
|
||||
def lock_unlock_handler(lock):
|
||||
def handler():
|
||||
targets = layer.paint_targets
|
||||
if item_id not in targets:
|
||||
targets = (item_id,)
|
||||
|
||||
layer.locked_influences = add_or_remove(layer.locked_influences, targets, lock)
|
||||
log.info("updated locked influences to %r", layer.locked_influences)
|
||||
session.events.influencesListUpdated.emit()
|
||||
|
||||
return handler
|
||||
|
||||
if "unlocked" in buttons:
|
||||
a = bar.addAction(icon_unlocked, "Toggle locked/unlocked")
|
||||
qt.on(a.triggered)(lock_unlock_handler(True))
|
||||
|
||||
if "locked" in buttons:
|
||||
a = bar.addAction(icon_locked, "Toggle locked/unlocked")
|
||||
qt.on(a.triggered)(lock_unlock_handler(False))
|
||||
|
||||
view.setItemWidget(item, 1, bar)
|
||||
|
||||
selected_ids = []
|
||||
if session.state.currentLayer.layer:
|
||||
selected_ids = session.state.currentLayer.layer.paint_targets
|
||||
current_id = None if not selected_ids else selected_ids[0]
|
||||
|
||||
with qt.signals_blocked(view):
|
||||
tree_items.clear()
|
||||
tree_root = view.invisibleRootItem()
|
||||
|
||||
item_index = 0
|
||||
for item_id, displayName, icon, buttons in wanted_tree_items(
|
||||
items=items,
|
||||
include_dq_item=session.state.skin_cluster_dq_channel_used,
|
||||
is_group_layer=is_group_layer,
|
||||
layer=layer,
|
||||
config=config,
|
||||
filter=filter,
|
||||
):
|
||||
if item_index >= tree_root.childCount():
|
||||
item = QtWidgets.QTreeWidgetItem([displayName])
|
||||
else:
|
||||
item = tree_root.child(item_index)
|
||||
item.setText(0, displayName)
|
||||
|
||||
item.setData(0, id_role, item_id)
|
||||
item.setIcon(0, icon)
|
||||
item.setSizeHint(0, item_size_hint)
|
||||
tree_root.addChild(item)
|
||||
|
||||
tree_items[item_id] = item
|
||||
if item_id == current_id:
|
||||
view.setCurrentItem(item, 0, QtCore.QItemSelectionModel.NoUpdate)
|
||||
item.setSelected(item_id in selected_ids)
|
||||
|
||||
rebuild_buttons(item, item_id, buttons)
|
||||
|
||||
item_index += 1
|
||||
|
||||
while item_index < tree_root.childCount():
|
||||
tree_root.removeChild(tree_root.child(item_index))
|
||||
|
||||
view = QtWidgets.QTreeWidget(parent)
|
||||
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
view.setUniformRowHeights(True)
|
||||
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||
actions.addInfluencesActions(view)
|
||||
view.addAction(actions.separator(parent, "View Options"))
|
||||
view.addAction(actions.show_used_influences_only)
|
||||
view.addAction(actions.set_influences_sorted)
|
||||
view.setIndentation(10 * scale_multiplier)
|
||||
view.header().setStretchLastSection(False)
|
||||
view.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
|
||||
view.setHeaderLabels(["Influences", ""])
|
||||
view.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
view.setColumnWidth(1, 25 * scale_multiplier)
|
||||
|
||||
# view.setHeaderHidden(True)
|
||||
def refresh_items():
|
||||
items = list_influences(session.state.currentLayer.selectedSkinCluster)
|
||||
|
||||
def sort_func(a):
|
||||
"""
|
||||
:type a: InfluenceInfo
|
||||
"""
|
||||
return a.name
|
||||
|
||||
# items = sorted(items, key=sort_func)
|
||||
build_items(view, items, session.state.currentLayer.layer)
|
||||
|
||||
@signal.on(
|
||||
filter.changed,
|
||||
config.influences_show_used_influences_only.changed,
|
||||
config.influences_sort.changed,
|
||||
session.events.influencesListUpdated,
|
||||
)
|
||||
def filter_changed():
|
||||
refresh_items()
|
||||
|
||||
@signal.on(session.events.currentLayerChanged, qtParent=view)
|
||||
def current_layer_changed():
|
||||
if not session.state.currentLayer.layer:
|
||||
build_items(view, [], None)
|
||||
else:
|
||||
log.info("current layer changed to %s", session.state.currentLayer.layer)
|
||||
refresh_items()
|
||||
current_influence_changed()
|
||||
|
||||
@signal.on(session.events.currentInfluenceChanged, qtParent=view)
|
||||
def current_influence_changed():
|
||||
if session.state.currentLayer.layer is None:
|
||||
return
|
||||
|
||||
log.info("current influence changed - updating item selection")
|
||||
with qt.signals_blocked(view):
|
||||
targets = session.state.currentLayer.layer.paint_targets
|
||||
first = True
|
||||
for tree_item in tree_items.values():
|
||||
selected = get_item_id(tree_item) in targets
|
||||
if selected and first:
|
||||
view.setCurrentItem(tree_item, 0, QtCore.QItemSelectionModel.NoUpdate)
|
||||
first = False
|
||||
tree_item.setSelected(selected)
|
||||
|
||||
@qt.on(view.currentItemChanged)
|
||||
def current_item_changed(curr, prev):
|
||||
if curr is None:
|
||||
return
|
||||
|
||||
if session.state.selectedSkinCluster is None:
|
||||
return
|
||||
|
||||
if not session.state.currentLayer.layer:
|
||||
return
|
||||
|
||||
log.info("focused item changed: %r", get_item_id(curr))
|
||||
sync_paint_targets_to_selection()
|
||||
|
||||
@qt.on(view.itemSelectionChanged)
|
||||
def sync_paint_targets_to_selection():
|
||||
log.info("syncing paint targets")
|
||||
selected_ids = [get_item_id(item) for item in view.selectedItems()]
|
||||
selected_ids = [i for i in selected_ids if i is not None]
|
||||
|
||||
current_item = view.currentItem()
|
||||
if current_item and current_item.isSelected():
|
||||
# move id of current item to front, if it's selected
|
||||
item_id = get_item_id(current_item)
|
||||
selected_ids.remove(item_id)
|
||||
selected_ids = [item_id] + selected_ids
|
||||
|
||||
if session.state.currentLayer.layer:
|
||||
session.state.currentLayer.layer.paint_targets = selected_ids
|
||||
|
||||
current_layer_changed()
|
||||
|
||||
return view
|
||||
|
||||
|
||||
def get_icon(influence, is_joint):
|
||||
if influence.used:
|
||||
return icon_joint if is_joint else icon_transform
|
||||
return icon_joint_disabled if is_joint else icon_transform_disabled
|
||||
|
||||
|
||||
def wanted_tree_items(
|
||||
layer,
|
||||
config,
|
||||
is_group_layer,
|
||||
include_dq_item,
|
||||
filter,
|
||||
items,
|
||||
):
|
||||
"""
|
||||
|
||||
:type items: list[InfluenceInfo]
|
||||
"""
|
||||
|
||||
if layer is None:
|
||||
return
|
||||
|
||||
# calculate "used" regardless as we're displaying it visually even if "show used influences only" is toggled off
|
||||
used = set((layer.get_used_influences() or []))
|
||||
locked = set((layer.locked_influences or []))
|
||||
for i in items:
|
||||
i.used = i.logicalIndex in used
|
||||
i.locked = i.logicalIndex in locked
|
||||
|
||||
if config.influences_show_used_influences_only() and layer is not None:
|
||||
items = [i for i in items if i.used]
|
||||
|
||||
if is_group_layer:
|
||||
items = []
|
||||
|
||||
yield "mask", "[Mask]", icon_mask, []
|
||||
if not is_group_layer and include_dq_item:
|
||||
yield "dq", "[DQ Weights]", icon_dq, []
|
||||
|
||||
names = influence_names.unique_names([i.path_name() for i in items])
|
||||
for i, name in zip(items, names):
|
||||
i.unique_name = name
|
||||
|
||||
if config.influences_sort() == Config.InfluencesSortDescending:
|
||||
items = list(sorted(items, key=lambda i: i.unique_name))
|
||||
|
||||
for i in items:
|
||||
is_joint = i.path is not None
|
||||
if filter.is_match(i.path_name()):
|
||||
yield i.logicalIndex, i.unique_name, get_icon(i, is_joint), ["locked" if i.locked else "unlocked"]
|
||||
225
2023/scripts/rigging_tools/ngskintools2/ui/layersview.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from ngSkinTools2 import api, signal
|
||||
from ngSkinTools2.api import python_compatibility
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
if python_compatibility.PY3:
|
||||
from typing import Union
|
||||
|
||||
|
||||
log = getLogger("layersView")
|
||||
|
||||
|
||||
def build_view(parent, actions):
|
||||
from ngSkinTools2.operations import layers
|
||||
|
||||
layer_icon_size = 20
|
||||
visibility_icon_size = 13
|
||||
|
||||
icon_layer = qt.scaled_icon(":/layeredTexture.svg", layer_icon_size, layer_icon_size)
|
||||
icon_layer_disabled = qt.scaled_icon(":/layerEditor.png", layer_icon_size, layer_icon_size)
|
||||
icon_visible = qt.scaled_icon("eye-fill.svg", visibility_icon_size, visibility_icon_size)
|
||||
icon_hidden = qt.scaled_icon("eye-slash-fill.svg", visibility_icon_size, visibility_icon_size)
|
||||
|
||||
layer_data_role = QtCore.Qt.UserRole + 1
|
||||
|
||||
def item_to_layer(item):
|
||||
# type: (QtWidgets.QTreeWidgetItem) -> Union[api.Layer, None]
|
||||
if item is None:
|
||||
return None
|
||||
return item.data(0, layer_data_role)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def sync_layer_parents_to_widget_items(view):
|
||||
"""
|
||||
after drag/drop tree reordering, just brute-force check
|
||||
that rearranged items match layers parents
|
||||
:return:
|
||||
"""
|
||||
|
||||
def sync_item(tree_item, parent_layer_id):
|
||||
for i in range(tree_item.childCount()):
|
||||
child = tree_item.child(i)
|
||||
rebuild_buttons(child)
|
||||
|
||||
child_layer = item_to_layer(child)
|
||||
|
||||
if child_layer.parent_id != parent_layer_id:
|
||||
log.info("changing layer parent: %r->%r (was %r)", parent_layer_id, child_layer, child_layer.parent_id)
|
||||
child_layer.parent = parent_layer_id
|
||||
|
||||
new_index = tree_item.childCount() - i - 1
|
||||
if child_layer.index != new_index:
|
||||
log.info("changing layer index: %r->%r (was %r)", child_layer, new_index, child_layer.index)
|
||||
child_layer.index = new_index
|
||||
|
||||
sync_item(child, child_layer.id)
|
||||
|
||||
with qt.signals_blocked(view):
|
||||
sync_item(view.invisibleRootItem(), None)
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class LayersWidget(QtWidgets.QTreeWidget):
|
||||
def dropEvent(self, event):
|
||||
QtWidgets.QTreeWidget.dropEvent(self, event)
|
||||
sync_layer_parents_to_widget_items(self)
|
||||
|
||||
view = LayersWidget(parent)
|
||||
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
view.setUniformRowHeights(True)
|
||||
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
# enable drag/drop
|
||||
view.setDragEnabled(True)
|
||||
view.viewport().setAcceptDrops(True)
|
||||
view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
|
||||
view.setDropIndicatorShown(True)
|
||||
|
||||
# add context menu
|
||||
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||
actions.addLayersActions(view)
|
||||
|
||||
view.setHeaderLabels(["Layers", ""])
|
||||
# view.setHeaderHidden(True)
|
||||
view.header().setMinimumSectionSize(1)
|
||||
view.header().setStretchLastSection(False)
|
||||
view.header().swapSections(0, 1)
|
||||
view.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
view.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
view.setColumnWidth(1, 25 * scale_multiplier)
|
||||
view.setIndentation(15 * scale_multiplier)
|
||||
view.setIconSize(QtCore.QSize(layer_icon_size * scale_multiplier, layer_icon_size * scale_multiplier))
|
||||
|
||||
tree_items = {}
|
||||
|
||||
def rebuild_buttons(item):
|
||||
layer = item_to_layer(item)
|
||||
bar = QtWidgets.QToolBar(parent=parent)
|
||||
bar.setMovable(False)
|
||||
bar.setIconSize(QtCore.QSize(visibility_icon_size * scale_multiplier, visibility_icon_size * scale_multiplier))
|
||||
a = bar.addAction(icon_visible if layer is None or layer.enabled else icon_hidden, "Toggle enabled/disabled")
|
||||
|
||||
@qt.on(a.triggered)
|
||||
def handler():
|
||||
layer.enabled = not layer.enabled
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
|
||||
view.setItemWidget(item, 1, bar)
|
||||
|
||||
def build_items(layer_infos):
|
||||
"""
|
||||
sync items in view with provided layer values, trying to delete as little items on the view as possible
|
||||
:type layer_infos: list[api.Layer]
|
||||
"""
|
||||
|
||||
# build map "parent id->list of children "
|
||||
|
||||
log.info("syncing items...")
|
||||
|
||||
# save selected layers IDs to restore item selection later
|
||||
selected_layer_ids = {item_to_layer(item).id for item in view.selectedItems()}
|
||||
log.info("selected layer IDs: %r", selected_layer_ids)
|
||||
current_item_id = None if view.currentItem() is None else item_to_layer(view.currentItem()).id
|
||||
|
||||
hierarchy = {}
|
||||
for child in layer_infos:
|
||||
if child.parent_id not in hierarchy:
|
||||
hierarchy[child.parent_id] = []
|
||||
hierarchy[child.parent_id].append(child)
|
||||
|
||||
def sync(parent_tree_item, children_list):
|
||||
while parent_tree_item.childCount() > len(children_list):
|
||||
parent_tree_item.removeChild(parent_tree_item.child(len(children_list)))
|
||||
|
||||
for index, child in enumerate(reversed(children_list)):
|
||||
if index >= parent_tree_item.childCount():
|
||||
item = QtWidgets.QTreeWidgetItem()
|
||||
item.setSizeHint(1, QtCore.QSize(1 * scale_multiplier, 25 * scale_multiplier))
|
||||
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
|
||||
parent_tree_item.addChild(item)
|
||||
else:
|
||||
item = parent_tree_item.child(index)
|
||||
|
||||
tree_items[child.id] = item
|
||||
|
||||
item.setData(0, layer_data_role, child)
|
||||
item.setText(0, child.name)
|
||||
item.setIcon(0, icon_layer if child.enabled else icon_layer_disabled)
|
||||
rebuild_buttons(item)
|
||||
|
||||
sync(item, hierarchy.get(child.id, []))
|
||||
|
||||
with qt.signals_blocked(view):
|
||||
tree_items.clear()
|
||||
sync(view.invisibleRootItem(), hierarchy.get(None, []))
|
||||
|
||||
current_item = tree_items.get(current_item_id, None)
|
||||
if current_item is not None:
|
||||
view.setCurrentItem(current_item, 0, QtCore.QItemSelectionModel.NoUpdate)
|
||||
|
||||
for i in selected_layer_ids:
|
||||
item = tree_items.get(i, None)
|
||||
if item is not None:
|
||||
item.setSelected(True)
|
||||
|
||||
@signal.on(session.events.layerListChanged, qtParent=view)
|
||||
def refresh_layer_list():
|
||||
log.info("event handler for layer list changed")
|
||||
if not session.state.layersAvailable:
|
||||
build_items([])
|
||||
else:
|
||||
build_items(session.state.all_layers)
|
||||
|
||||
update_selected_items()
|
||||
|
||||
@signal.on(session.events.currentLayerChanged, qtParent=view)
|
||||
def current_layer_changed():
|
||||
log.info("event handler for currentLayerChanged")
|
||||
layer = session.state.currentLayer.layer
|
||||
current_item = view.currentItem()
|
||||
if layer is None:
|
||||
view.setCurrentItem(None)
|
||||
return
|
||||
|
||||
prev_layer = None if current_item is None else item_to_layer(current_item)
|
||||
|
||||
if prev_layer is None or prev_layer.id != layer.id:
|
||||
item = tree_items.get(layer.id, None)
|
||||
if item is not None:
|
||||
log.info("setting current item to " + item.text(0))
|
||||
view.setCurrentItem(item, 0, QtCore.QItemSelectionModel.SelectCurrent | QtCore.QItemSelectionModel.ClearAndSelect)
|
||||
|
||||
item.setSelected(True)
|
||||
|
||||
@qt.on(view.currentItemChanged)
|
||||
def current_item_changed(curr, _):
|
||||
log.info("current item changed")
|
||||
if curr is None:
|
||||
return
|
||||
|
||||
selected_layer = item_to_layer(curr)
|
||||
|
||||
if layers.getCurrentLayer() == selected_layer:
|
||||
return
|
||||
|
||||
layers.setCurrentLayer(selected_layer)
|
||||
|
||||
@qt.on(view.itemChanged)
|
||||
def item_changed(item, column):
|
||||
log.info("item changed")
|
||||
layers.renameLayer(item_to_layer(item), item.text(column))
|
||||
|
||||
@qt.on(view.itemSelectionChanged)
|
||||
def update_selected_items():
|
||||
selection = [item_to_layer(item) for item in view.selectedItems()]
|
||||
|
||||
if selection != session.context.selected_layers(default=[]):
|
||||
log.info("new selected layers: %r", selection)
|
||||
session.context.selected_layers.set(selection)
|
||||
|
||||
refresh_layer_list()
|
||||
|
||||
return view
|
||||
50
2023/scripts/rigging_tools/ngskintools2/ui/layout.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.ui import qt
|
||||
|
||||
try:
|
||||
scale_multiplier = cmds.mayaDpiSetting(q=True, realScaleValue=True)
|
||||
except:
|
||||
# the command is not available on macos, using 1.0 for fallback
|
||||
scale_multiplier = 1
|
||||
|
||||
|
||||
def createTitledRow(title, contents, *additional_rows):
|
||||
row = QtWidgets.QFormLayout()
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
label = QtWidgets.QLabel(title)
|
||||
# label.setAlignment(QtCore.Qt.AlignRight |QtCore.Qt.)
|
||||
label.setFixedWidth(100 * scale_multiplier)
|
||||
|
||||
if contents is None:
|
||||
row.addRow(label, QtWidgets.QWidget())
|
||||
return row
|
||||
|
||||
row.addRow(label, contents)
|
||||
for i in additional_rows:
|
||||
row.addRow(None, i)
|
||||
return row
|
||||
|
||||
|
||||
class TabSetup(Object):
|
||||
def __init__(self):
|
||||
self.innerLayout = innerLayout = QtWidgets.QVBoxLayout()
|
||||
innerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
innerLayout.setSpacing(3 * scale_multiplier)
|
||||
|
||||
self.scrollArea = scrollArea = QtWidgets.QScrollArea()
|
||||
scrollArea.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
scrollArea.setWidget(qt.wrap_layout_into_widget(innerLayout))
|
||||
scrollArea.setWidgetResizable(True)
|
||||
|
||||
self.lowerButtonsRow = lowerButtonsRow = QtWidgets.QHBoxLayout()
|
||||
|
||||
self.mainLayout = mainLayout = QtWidgets.QVBoxLayout()
|
||||
mainLayout.addWidget(scrollArea)
|
||||
mainLayout.addLayout(lowerButtonsRow)
|
||||
mainLayout.setContentsMargins(7, 7, 7, 7)
|
||||
|
||||
self.tabContents = qt.wrap_layout_into_widget(mainLayout)
|
||||
223
2023/scripts/rigging_tools/ngskintools2/ui/mainwindow.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from maya import OpenMayaUI as omui
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets, QWidget, wrap_instance
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui.options import config
|
||||
|
||||
from .. import cleanup, signal, version
|
||||
from ..observableValue import ObservableValue
|
||||
from . import (
|
||||
aboutwindow,
|
||||
dialogs,
|
||||
hotkeys_setup,
|
||||
qt,
|
||||
tabLayerEffects,
|
||||
tabMirror,
|
||||
tabPaint,
|
||||
tabSetWeights,
|
||||
tabTools,
|
||||
targetui,
|
||||
updatewindow,
|
||||
)
|
||||
from .layout import scale_multiplier
|
||||
|
||||
log = getLogger("main window")
|
||||
|
||||
|
||||
def get_image_path(file_name):
|
||||
import os
|
||||
|
||||
for i in os.getenv("XBMLANGPATH", "").split(os.path.pathsep):
|
||||
result = os.path.join(i, file_name)
|
||||
if os.path.isfile(result):
|
||||
return result
|
||||
|
||||
return file_name
|
||||
|
||||
|
||||
def build_menu(parent, actions):
|
||||
menu = QtWidgets.QMenuBar(parent=parent)
|
||||
|
||||
def top_level_menu(label):
|
||||
sub_item = menu.addMenu(label)
|
||||
sub_item.setSeparatorsCollapsible(False)
|
||||
sub_item.setTearOffEnabled(True)
|
||||
return sub_item
|
||||
|
||||
sub = top_level_menu("File")
|
||||
sub.addSeparator().setText("Import/Export")
|
||||
sub.addAction(actions.importFile)
|
||||
sub.addAction(actions.exportFile)
|
||||
|
||||
sub = top_level_menu("Layers")
|
||||
sub.addSeparator().setText("Layer actions")
|
||||
sub.addAction(actions.initialize)
|
||||
sub.addAction(actions.import_v1)
|
||||
actions.addLayersActions(sub)
|
||||
sub.addSeparator().setText("Copy")
|
||||
sub.addAction(actions.transfer)
|
||||
|
||||
sub = top_level_menu("Tools")
|
||||
sub.addAction(actions.add_influences)
|
||||
sub.addAction(actions.toolsAssignFromClosestJoint)
|
||||
sub.addSeparator()
|
||||
sub.addAction(actions.transfer)
|
||||
sub.addSeparator()
|
||||
sub.addAction(actions.toolsDeleteCustomNodesOnSelection)
|
||||
sub.addAction(actions.toolsDeleteCustomNodes)
|
||||
|
||||
sub = top_level_menu("View")
|
||||
sub.addAction(actions.show_used_influences_only)
|
||||
|
||||
sub = top_level_menu("Help")
|
||||
sub.addAction(actions.documentation.user_guide)
|
||||
sub.addAction(actions.documentation.api_root)
|
||||
sub.addAction(actions.documentation.changelog)
|
||||
sub.addAction(actions.documentation.contact)
|
||||
sub.addSeparator()
|
||||
sub.addAction(actions.check_for_updates)
|
||||
sub.addAction("About...").triggered.connect(lambda: aboutwindow.show(parent))
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def build_rmb_menu_layers(view, actions):
|
||||
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||
actions.addLayersActions(view)
|
||||
|
||||
|
||||
class MainWindowOptions:
|
||||
current_tab = ObservableValue(0)
|
||||
|
||||
|
||||
def build_ui(parent):
|
||||
"""
|
||||
:type parent: QWidget
|
||||
"""
|
||||
options = MainWindowOptions()
|
||||
window = QtWidgets.QWidget(parent)
|
||||
|
||||
session.addQtWidgetReference(window)
|
||||
|
||||
from ngSkinTools2.ui.actions import Actions
|
||||
|
||||
actions = Actions(parent=window, session=session)
|
||||
|
||||
tabs = QtWidgets.QTabWidget(window)
|
||||
|
||||
tabs.addTab(tabPaint.build_ui(tabs, actions), "Paint")
|
||||
tabs.addTab(tabSetWeights.build_ui(tabs), "Set Weights")
|
||||
tabs.addTab(tabMirror.build_ui(tabs), "Mirror")
|
||||
tabs.addTab(tabLayerEffects.build_ui(), "Effects")
|
||||
tabs.addTab(tabTools.build_ui(actions, session), "Tools")
|
||||
|
||||
@signal.on(options.current_tab.changed)
|
||||
def set_current_tab():
|
||||
tabs.setCurrentIndex(options.current_tab())
|
||||
|
||||
layers_toolbar = QtWidgets.QToolBar()
|
||||
layers_toolbar.addAction(actions.addLayer)
|
||||
layers_toolbar.setOrientation(QtCore.Qt.Vertical)
|
||||
|
||||
spacing_h = 5
|
||||
spacing_v = 5
|
||||
|
||||
layers_row = targetui.build_target_ui(window, actions, session)
|
||||
|
||||
split = QtWidgets.QSplitter(orientation=QtCore.Qt.Vertical, parent=window)
|
||||
split.addWidget(layers_row)
|
||||
split.addWidget(tabs)
|
||||
split.setStretchFactor(0, 2)
|
||||
split.setStretchFactor(1, 3)
|
||||
split.setContentsMargins(spacing_h, spacing_v, spacing_h, spacing_v)
|
||||
|
||||
def build_icon_label():
|
||||
w = QWidget()
|
||||
w.setStyleSheet("background-color: #dcce87;color: #373737;")
|
||||
l = QtWidgets.QHBoxLayout()
|
||||
icon = QtWidgets.QLabel()
|
||||
icon.setPixmap(QtGui.QIcon(":/error.png").pixmap(16 * scale_multiplier, 16 * scale_multiplier))
|
||||
icon.setFixedSize(16 * scale_multiplier, 16 * scale_multiplier)
|
||||
text = QtWidgets.QLabel("<placeholder>")
|
||||
text.setWordWrap(True)
|
||||
text.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||
|
||||
l.addWidget(icon)
|
||||
l.addWidget(text)
|
||||
w.setContentsMargins(0, 0, 0, 0)
|
||||
w.setLayout(l)
|
||||
|
||||
return w, text.setText
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(window)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(build_menu(window, actions))
|
||||
layout.addWidget(split)
|
||||
|
||||
window.setLayout(layout)
|
||||
|
||||
hotkeys_setup.install_hotkeys()
|
||||
|
||||
dialogs.promptsParent = window
|
||||
|
||||
if config.checkForUpdatesAtStartup():
|
||||
updatewindow.silent_check_and_show_if_available(qt.mainWindow)
|
||||
|
||||
return window, options
|
||||
|
||||
|
||||
DOCK_NAME = 'ngSkinTools2_mainWindow'
|
||||
|
||||
|
||||
def workspace_control_permanent_script():
|
||||
from ngSkinTools2 import workspace_control_main_window
|
||||
|
||||
return "import {f.__module__}; {f.__module__}.{f.__name__}()".format(f=workspace_control_main_window)
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def open():
|
||||
"""
|
||||
opens main window
|
||||
"""
|
||||
|
||||
if not cmds.workspaceControl(DOCK_NAME, q=True, exists=True):
|
||||
# build UI script in type-safe manner
|
||||
|
||||
cmds.workspaceControl(
|
||||
DOCK_NAME,
|
||||
retain=False,
|
||||
floating=True,
|
||||
# ttc=["AttributeEditor",-1],
|
||||
uiScript=workspace_control_permanent_script(),
|
||||
)
|
||||
|
||||
# bring tab to front
|
||||
cmds.evalDeferred(lambda *args: cmds.workspaceControl(DOCK_NAME, e=True, r=True))
|
||||
|
||||
def close():
|
||||
from maya import cmds
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
cmds.deleteUI(DOCK_NAME)
|
||||
except:
|
||||
pass
|
||||
pass
|
||||
|
||||
cleanup.registerCleanupHandler(close)
|
||||
|
||||
|
||||
def resume_in_workspace_control():
|
||||
"""
|
||||
this method is responsible for resuming workspace control when Maya is building/restoring UI as part of it's
|
||||
workspace management cycle (open UI for the first time, restart maya, change workspace, etc)
|
||||
"""
|
||||
|
||||
cmds.workspaceControl(DOCK_NAME, e=True, label="ngSkinTools " + version.pluginVersion())
|
||||
widget = wrap_instance(omui.MQtUtil.findControl(DOCK_NAME), QtWidgets.QWidget)
|
||||
|
||||
ui, _ = build_ui(widget)
|
||||
widget.layout().addWidget(ui)
|
||||
22
2023/scripts/rigging_tools/ngskintools2/ui/model_binds.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.pyside import QtWidgets
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
|
||||
|
||||
def bind(ui, model):
|
||||
if isinstance(ui, QtWidgets.QCheckBox):
|
||||
ui.setChecked(model())
|
||||
|
||||
@qt.on(ui.stateChanged)
|
||||
def update_model():
|
||||
model.set(ui.isChecked())
|
||||
|
||||
elif isinstance(ui, widgets.NumberSliderGroup):
|
||||
ui.set_value(model())
|
||||
|
||||
@signal.on(ui.valueChanged)
|
||||
def update_model():
|
||||
model.set(ui.value())
|
||||
|
||||
else:
|
||||
raise Exception("could not bind control to model")
|
||||
201
2023/scripts/rigging_tools/ngskintools2/ui/options.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import json
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object, is_string
|
||||
from ngSkinTools2.observableValue import ObservableValue
|
||||
|
||||
log = getLogger("plugin")
|
||||
|
||||
|
||||
class Value(Object):
|
||||
def __init__(self, value=None):
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
def getInt(self):
|
||||
try:
|
||||
return int(self.get())
|
||||
except:
|
||||
return 0
|
||||
|
||||
|
||||
class PersistentValue(Value):
|
||||
"""
|
||||
persistent value can store itself into Maya's "option vars" array
|
||||
"""
|
||||
|
||||
def __init__(self, name, default_value=None, prefix=None):
|
||||
Value.__init__(self)
|
||||
|
||||
if prefix is None:
|
||||
prefix = VAR_OPTION_PREFIX
|
||||
self.name = prefix + name
|
||||
self.default_value = default_value
|
||||
self.value = load_option(self.name, self.default_value)
|
||||
|
||||
def set(self, value):
|
||||
Value.set(self, value)
|
||||
save_option(self.name, self.value)
|
||||
|
||||
|
||||
class PersistentDict(Object):
|
||||
def __init__(self, name, default_values=None):
|
||||
if default_values is None:
|
||||
default_values = {}
|
||||
self.persistence = PersistentValue(name=name, default_value=json.dumps(default_values))
|
||||
|
||||
def __get_values(self):
|
||||
# type: () -> dict
|
||||
return json.loads(self.persistence.get())
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.__get_values().get(item, None)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
v = self.__get_values()
|
||||
v[key] = value
|
||||
self.persistence.set(json.dumps(v))
|
||||
|
||||
|
||||
def load_option(var_name, default_value):
|
||||
"""
|
||||
loads value from optionVar
|
||||
"""
|
||||
|
||||
from ngSkinTools2 import BATCH_MODE
|
||||
|
||||
if BATCH_MODE:
|
||||
return default_value
|
||||
|
||||
if cmds.optionVar(exists=var_name):
|
||||
return cmds.optionVar(q=var_name)
|
||||
|
||||
return default_value
|
||||
|
||||
|
||||
def save_option(varName, value):
|
||||
"""
|
||||
saves option via optionVar
|
||||
"""
|
||||
from ngSkinTools2 import BATCH_MODE
|
||||
|
||||
if BATCH_MODE:
|
||||
return
|
||||
|
||||
# variable does not exist, attempt to save it
|
||||
key = None
|
||||
if isinstance(value, float):
|
||||
key = 'fv'
|
||||
elif isinstance(value, int):
|
||||
key = 'iv'
|
||||
elif is_string(value):
|
||||
key = 'sv'
|
||||
else:
|
||||
raise ValueError("could not save option %s: invalid value %r" % (varName, value))
|
||||
|
||||
kvargs = {key: (varName, value)}
|
||||
log.info("saving optionvar: %r", kvargs)
|
||||
cmds.optionVar(**kvargs)
|
||||
|
||||
|
||||
VAR_OPTION_PREFIX = 'ngSkinTools2_'
|
||||
|
||||
|
||||
def delete_custom_options():
|
||||
for varName in cmds.optionVar(list=True):
|
||||
if varName.startswith(VAR_OPTION_PREFIX):
|
||||
cmds.optionVar(remove=varName)
|
||||
|
||||
cmds.windowPref('MirrorWeightsWindow', ra=True)
|
||||
|
||||
|
||||
def build_config_property(name, default_value, doc=''):
|
||||
return property(lambda self: self.__get_value__(name, default_value), lambda self, val: self.__set_value__(name, val), doc=doc)
|
||||
|
||||
|
||||
class Config(Object):
|
||||
"""
|
||||
Maya-wide settings for ngSkinTools2
|
||||
"""
|
||||
|
||||
mirrorInfluencesDefaults = build_config_property('mirrorInfluencesDefaults', "{}") # type: string
|
||||
|
||||
InfluencesSortUnsorted = 'unsorted'
|
||||
InfluencesSortDescending = 'descending'
|
||||
|
||||
def __init__(self):
|
||||
from ngSkinTools2.api.mirror import MirrorOptions
|
||||
|
||||
self.__storage__ = PersistentValue("config", "{}")
|
||||
self.__state__ = self.load()
|
||||
|
||||
self.unique_client_id = PersistentValue('updateCheckUniqueClientId')
|
||||
|
||||
self.checkForUpdatesAtStartup = self.build_observable_value('checkForUpdatesAtStartup', True)
|
||||
self.influences_show_used_influences_only = self.build_observable_value("influencesViewShowUsedInfluencesOnly", False)
|
||||
|
||||
# influences sort is not a simple "true/false" flag to allow different sorting methods in the future.
|
||||
self.influences_sort = self.build_observable_value("influencesSort", Config.InfluencesSortUnsorted)
|
||||
|
||||
default_mirror_options = MirrorOptions()
|
||||
self.mirror_direction = self.build_observable_value("mirrorDirection", default_mirror_options.direction)
|
||||
self.mirror_dq = self.build_observable_value("mirrorDq", default_mirror_options.mirrorDq)
|
||||
self.mirror_mask = self.build_observable_value("mirrorMask", default_mirror_options.mirrorMask)
|
||||
self.mirror_weights = self.build_observable_value("mirrorWeights", default_mirror_options.mirrorWeights)
|
||||
|
||||
def __get_value__(self, name, default_value):
|
||||
result = self.__state__.get(name, default_value)
|
||||
log.info("config: return %s=%r", name, result)
|
||||
return result
|
||||
|
||||
def __set_value__(self, name, value):
|
||||
log.info("config: save %s=%r", name, value)
|
||||
self.__state__[name] = value
|
||||
self.save()
|
||||
|
||||
def build_observable_value(self, name, default_value):
|
||||
"""
|
||||
builds ObservableValue that is loaded and persisted into config when changed
|
||||
:type name: str
|
||||
:rtype: ngSkinTools2.observableValue.ObservableValue
|
||||
"""
|
||||
result = ObservableValue(self.__get_value__(name=name, default_value=default_value))
|
||||
|
||||
@signal.on(result.changed)
|
||||
def save():
|
||||
self.__set_value__(name, result())
|
||||
|
||||
return result
|
||||
|
||||
def load(self):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
return json.loads(self.__storage__.get())
|
||||
except:
|
||||
return {}
|
||||
|
||||
def save(self):
|
||||
self.__storage__.set(json.dumps(self.__state__))
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
|
||||
def bind_checkbox(cb, option):
|
||||
from ngSkinTools2.ui import qt
|
||||
|
||||
cb.setChecked(option())
|
||||
|
||||
@qt.on(cb.toggled)
|
||||
def update():
|
||||
option.set(cb.isChecked())
|
||||
|
||||
return cb
|
||||
@@ -0,0 +1,24 @@
|
||||
from maya import mel
|
||||
|
||||
|
||||
def definePaintContextCallbacks():
|
||||
"""
|
||||
Maya expects some mel procedures to be present for paint context metadata
|
||||
"""
|
||||
|
||||
mel.eval(
|
||||
"""
|
||||
global proc ngst2PaintContextProperties() {
|
||||
setUITemplate -pushTemplate DefaultTemplate;
|
||||
setUITemplate -popTemplate;
|
||||
|
||||
}
|
||||
|
||||
global proc ngst2PaintContextValues(string $toolName)
|
||||
{
|
||||
string $icon = "ngSkinToolsShelfIcon.png";
|
||||
string $help = "ngSkinTools2 - paint skin weights";
|
||||
toolPropertySetCommon $toolName $icon $help;
|
||||
}
|
||||
"""
|
||||
)
|
||||
39
2023/scripts/rigging_tools/ngskintools2/ui/parallel.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from threading import Thread
|
||||
|
||||
from maya import utils
|
||||
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
class ParallelTask(Object):
|
||||
def __init__(self):
|
||||
self.__run_handlers = []
|
||||
self.__done_handlers = []
|
||||
|
||||
def add_run_handler(self, handler):
|
||||
self.__run_handlers.append(handler)
|
||||
|
||||
def add_done_handler(self, handler):
|
||||
self.__done_handlers.append(handler)
|
||||
|
||||
def start(self, async_exec=True):
|
||||
def done():
|
||||
for i in self.__done_handlers:
|
||||
i(self)
|
||||
|
||||
def thread():
|
||||
for i in self.__run_handlers:
|
||||
i(self)
|
||||
if async_exec:
|
||||
utils.executeDeferred(done)
|
||||
else:
|
||||
done()
|
||||
|
||||
self.current_thread = Thread(target=thread)
|
||||
if async_exec:
|
||||
self.current_thread.start()
|
||||
else:
|
||||
self.current_thread.run()
|
||||
|
||||
def wait(self):
|
||||
self.current_thread.join()
|
||||
128
2023/scripts/rigging_tools/ngskintools2/ui/qt.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import os
|
||||
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets, get_main_window
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
def wrap_layout_into_widget(layout):
|
||||
w = QtWidgets.QWidget()
|
||||
w.setLayout(layout)
|
||||
return w
|
||||
|
||||
|
||||
def signals_blocked(widget):
|
||||
return SignalBlockContext(widget)
|
||||
|
||||
|
||||
class SignalBlockContext(Object):
|
||||
def __init__(self, widget):
|
||||
self.widget = widget
|
||||
|
||||
def __enter__(self):
|
||||
self.prevState = self.widget.blockSignals(True)
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.widget.blockSignals(self.prevState)
|
||||
|
||||
|
||||
def on(*signals):
|
||||
"""
|
||||
decorator for function: list signals that should fire for this function.
|
||||
|
||||
instead of:
|
||||
|
||||
def something():
|
||||
...
|
||||
btn.clicked.connect(something)
|
||||
|
||||
do:
|
||||
|
||||
@qt.on(btn.clicked)
|
||||
def something():
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(fn):
|
||||
for i in signals:
|
||||
i.connect(fn)
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class SingleWindowPolicy(Object):
|
||||
def __init__(self):
|
||||
self.lastWindow = None
|
||||
|
||||
def setCurrent(self, window):
|
||||
if self.lastWindow:
|
||||
self.lastWindow.close()
|
||||
self.lastWindow = window
|
||||
|
||||
on(window.finished)(self.cleanup)
|
||||
|
||||
def cleanup(self):
|
||||
self.lastWindow = None
|
||||
|
||||
|
||||
def alternative_palette_light():
|
||||
palette = QtGui.QPalette()
|
||||
palette.setColor(QtGui.QPalette.Window, QtGui.QColor(243, 244, 246))
|
||||
palette.setColor(QtGui.QPalette.WindowText, QtGui.QColor(33, 37, 41))
|
||||
return palette
|
||||
|
||||
|
||||
def bind_action_to_button(action, button):
|
||||
"""
|
||||
|
||||
:type button: PySide2.QtWidgets.QPushButton
|
||||
:type action: PySide2.QtWidgets.QAction
|
||||
"""
|
||||
|
||||
@on(action.changed)
|
||||
def update_state():
|
||||
button.setText(action.text())
|
||||
button.setEnabled(action.isEnabled())
|
||||
button.setToolTip(action.toolTip())
|
||||
button.setStatusTip(action.statusTip())
|
||||
button.setVisible(action.isVisible())
|
||||
if action.isCheckable():
|
||||
button.setChecked(action.isChecked())
|
||||
|
||||
button.setCheckable(action.isCheckable())
|
||||
|
||||
on(button.clicked)(action.trigger)
|
||||
update_state()
|
||||
|
||||
return button
|
||||
|
||||
|
||||
images_path = os.path.join(os.path.dirname(__file__), "images")
|
||||
|
||||
|
||||
def icon_path(path):
|
||||
if path.startswith(':'):
|
||||
return path
|
||||
return os.path.join(images_path, path)
|
||||
|
||||
|
||||
def scaled_icon(path, w, h):
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
return QtGui.QIcon(
|
||||
QtGui.QPixmap(icon_path(path)).scaled(w * scale_multiplier, h * scale_multiplier, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
)
|
||||
|
||||
|
||||
def image_icon(file_name):
|
||||
return QtGui.QIcon(icon_path(file_name))
|
||||
|
||||
|
||||
def select_data(combo, data):
|
||||
"""
|
||||
set combo box index to data index
|
||||
"""
|
||||
combo.setCurrentIndex(combo.findData(data))
|
||||
|
||||
|
||||
mainWindow = get_main_window()
|
||||
41
2023/scripts/rigging_tools/ngskintools2/ui/shelf.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from maya import cmds, mel
|
||||
|
||||
|
||||
def install_shelf():
|
||||
"""
|
||||
checks if there's ngSkintTools shelf installed, and if not, creates one.
|
||||
|
||||
this runs each time Maya starts (via Autoloader's ngSkinTools_load.mel) - avoid duplication, like creating things
|
||||
that already exist.
|
||||
"""
|
||||
|
||||
# don't do anything if we're in batch mode. UI commands are not available
|
||||
if cmds.about(batch=True) == 1:
|
||||
return
|
||||
|
||||
maya_shelf = mel.eval("$tempngSkinTools2Var=$gShelfTopLevel")
|
||||
existing_shelves = cmds.shelfTabLayout(maya_shelf, q=True, tabLabel=True)
|
||||
|
||||
parent_shelf = 'ngSkinTools2'
|
||||
|
||||
if parent_shelf in existing_shelves:
|
||||
return
|
||||
|
||||
mel.eval('addNewShelfTab ' + parent_shelf)
|
||||
cmds.shelfButton(
|
||||
parent=parent_shelf,
|
||||
enable=1,
|
||||
visible=1,
|
||||
preventOverride=0,
|
||||
label="ngst",
|
||||
annotation="opens ngSkinTools2 UI",
|
||||
image="ngSkinTools2ShelfIcon.png",
|
||||
style="iconOnly",
|
||||
noBackground=1,
|
||||
align="center",
|
||||
marginWidth=1,
|
||||
marginHeight=1,
|
||||
command="import ngSkinTools2; ngSkinTools2.open_ui()",
|
||||
sourceType="python",
|
||||
commandRepeatable=0,
|
||||
)
|
||||
200
2023/scripts/rigging_tools/ngskintools2/ui/tabLayerEffects.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.mirror import MirrorOptions
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
|
||||
log = getLogger("tab layer effects")
|
||||
|
||||
|
||||
def check_state_from_boolean_states(states):
|
||||
"""
|
||||
for a list of booleans, return checkbox check state - one of Qt.Checked, Qt.Unchecked and Qt.PartiallyChecked
|
||||
|
||||
:type states: list[bool]
|
||||
"""
|
||||
current_state = None
|
||||
for i in states:
|
||||
if current_state is None:
|
||||
current_state = i
|
||||
continue
|
||||
|
||||
if i != current_state:
|
||||
return QtCore.Qt.PartiallyChecked
|
||||
|
||||
if current_state:
|
||||
return QtCore.Qt.Checked
|
||||
|
||||
return QtCore.Qt.Unchecked
|
||||
|
||||
|
||||
def build_ui():
|
||||
def list_layers():
|
||||
return [] if not session.state.layersAvailable else session.context.selected_layers(default=[])
|
||||
|
||||
def build_properties():
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
opacity = widgets.NumberSliderGroup(tooltip="multiply layer mask to control overall transparency of the layer.")
|
||||
opacity.set_value(1.0)
|
||||
layout.addLayout(createTitledRow("Opacity:", opacity.layout()))
|
||||
|
||||
def default_selection_opacity(layers):
|
||||
if len(layers) > 0:
|
||||
return layers[0].opacity
|
||||
return 1.0
|
||||
|
||||
@signal.on(session.context.selected_layers.changed, session.events.currentLayerChanged, qtParent=tab.tabContents)
|
||||
def update_values():
|
||||
layers = list_layers()
|
||||
enabled = len(layers) > 0
|
||||
opacity.set_enabled(enabled)
|
||||
opacity.set_value(default_selection_opacity(layers))
|
||||
|
||||
@signal.on(opacity.valueChanged)
|
||||
def opacity_edited():
|
||||
layers = list_layers()
|
||||
# avoid changing opacity of all selected layers if we just changed slider value based on changed layer selection
|
||||
if opacity.value() == default_selection_opacity(layers):
|
||||
return
|
||||
val = opacity.value()
|
||||
for i in list_layers():
|
||||
if abs(i.opacity - val) > 0.00001:
|
||||
i.opacity = val
|
||||
|
||||
update_values()
|
||||
|
||||
group = QtWidgets.QGroupBox("Layer properties")
|
||||
group.setLayout(layout)
|
||||
|
||||
return group
|
||||
|
||||
def build_mirror_effect():
|
||||
def configure_mirror_all_layers(option, value):
|
||||
for i in list_layers():
|
||||
i.effects.configure_mirror(**{option: value})
|
||||
|
||||
mirror_direction = QtWidgets.QComboBox()
|
||||
mirror_direction.addItem("Positive to negative", MirrorOptions.directionPositiveToNegative)
|
||||
mirror_direction.addItem("Negative to positive", MirrorOptions.directionNegativeToPositive)
|
||||
mirror_direction.addItem("Flip", MirrorOptions.directionFlip)
|
||||
mirror_direction.setMinimumWidth(1)
|
||||
|
||||
@qt.on(mirror_direction.currentIndexChanged)
|
||||
def value_changed():
|
||||
configure_mirror_all_layers("mirror_direction", mirror_direction.currentData())
|
||||
|
||||
influences = QtWidgets.QCheckBox("Influence weights")
|
||||
mask = QtWidgets.QCheckBox("Layer mask")
|
||||
dq = QtWidgets.QCheckBox("Dual quaternion weights")
|
||||
|
||||
def configure_checkbox(checkbox, option):
|
||||
@qt.on(checkbox.stateChanged)
|
||||
def update_pref():
|
||||
if checkbox.checkState() == QtCore.Qt.PartiallyChecked:
|
||||
checkbox.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
enabled = checkbox.checkState() == QtCore.Qt.Checked
|
||||
configure_mirror_all_layers(option, enabled)
|
||||
|
||||
configure_checkbox(influences, 'mirror_weights')
|
||||
configure_checkbox(mask, 'mirror_mask')
|
||||
configure_checkbox(dq, 'mirror_dq')
|
||||
|
||||
@signal.on(session.context.selected_layers.changed, session.events.currentLayerChanged, qtParent=tab.tabContents)
|
||||
def update_values():
|
||||
layers = list_layers()
|
||||
with qt.signals_blocked(influences):
|
||||
influences.setCheckState(check_state_from_boolean_states([i.effects.mirror_weights for i in layers]))
|
||||
with qt.signals_blocked(mask):
|
||||
mask.setCheckState(check_state_from_boolean_states([i.effects.mirror_mask for i in layers]))
|
||||
with qt.signals_blocked(dq):
|
||||
dq.setCheckState(check_state_from_boolean_states([i.effects.mirror_dq for i in layers]))
|
||||
with qt.signals_blocked(mirror_direction):
|
||||
qt.select_data(mirror_direction, MirrorOptions.directionPositiveToNegative if not layers else layers[0].effects.mirror_direction)
|
||||
|
||||
update_values()
|
||||
|
||||
def elements():
|
||||
result = QtWidgets.QVBoxLayout()
|
||||
|
||||
for i in [influences, mask, dq]:
|
||||
i.setTristate(True)
|
||||
result.addWidget(i)
|
||||
return result
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Mirror effect on:", elements()))
|
||||
layout.addLayout(createTitledRow("Mirror direction:", mirror_direction))
|
||||
|
||||
group = QtWidgets.QGroupBox("Mirror")
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def build_skin_properties():
|
||||
use_max_influences = QtWidgets.QCheckBox("Limit max influences per vertex")
|
||||
max_influences = widgets.NumberSliderGroup(min_value=1, max_value=5, tooltip="", value_type=int)
|
||||
use_prune_weight = QtWidgets.QCheckBox("Prune small weights before writing to skin cluster")
|
||||
|
||||
prune_weight = widgets.NumberSliderGroup(decimals=6, min_value=0.000001, max_value=0.05, tooltip="")
|
||||
prune_weight.set_value(prune_weight.min_value)
|
||||
prune_weight.set_expo("start", 3)
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def update_ui():
|
||||
group.setEnabled(session.state.layersAvailable)
|
||||
|
||||
if session.state.layersAvailable:
|
||||
with qt.signals_blocked(use_max_influences):
|
||||
use_max_influences.setChecked(session.state.layers.influence_limit_per_vertex != 0)
|
||||
with qt.signals_blocked(max_influences):
|
||||
max_influences.set_value(session.state.layers.influence_limit_per_vertex if use_max_influences.isChecked() else 4)
|
||||
with qt.signals_blocked(use_prune_weight):
|
||||
use_prune_weight.setChecked(session.state.layers.prune_weights_filter_threshold != 0)
|
||||
with qt.signals_blocked(prune_weight):
|
||||
prune_weight.set_value(session.state.layers.prune_weights_filter_threshold if use_prune_weight.isChecked() else 0.0001)
|
||||
|
||||
update_ui_enabled()
|
||||
|
||||
def update_ui_enabled():
|
||||
max_influences.set_enabled(use_max_influences.isChecked())
|
||||
prune_weight.set_enabled(use_prune_weight.isChecked())
|
||||
|
||||
@qt.on(use_max_influences.stateChanged, use_prune_weight.stateChanged)
|
||||
@signal.on(max_influences.valueChanged, prune_weight.valueChanged)
|
||||
def update_values():
|
||||
log.info("updating effects tab")
|
||||
|
||||
if session.state.layersAvailable:
|
||||
session.state.layers.influence_limit_per_vertex = max_influences.value() if use_max_influences.isChecked() else 0
|
||||
session.state.layers.prune_weights_filter_threshold = 0 if not use_prune_weight.isChecked() else prune_weight.value_trimmed()
|
||||
|
||||
update_ui_enabled()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(use_max_influences)
|
||||
layout.addLayout(createTitledRow("Max influences:", max_influences.layout()))
|
||||
layout.addWidget(use_prune_weight)
|
||||
layout.addLayout(createTitledRow("Prune below:", prune_weight.layout()))
|
||||
|
||||
group = QtWidgets.QGroupBox("Skin Properties")
|
||||
group.setLayout(layout)
|
||||
|
||||
update_ui()
|
||||
|
||||
return group
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_properties())
|
||||
tab.innerLayout.addWidget(build_mirror_effect())
|
||||
tab.innerLayout.addWidget(build_skin_properties())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
215
2023/scripts/rigging_tools/ngskintools2/ui/tabMirror.py
Normal file
@@ -0,0 +1,215 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import Mirror, MirrorOptions, VertexTransferMode
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.mirror import set_reference_mesh_from_selection
|
||||
from ngSkinTools2.api.pyside import QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
from ngSkinTools2.ui.options import bind_checkbox, config
|
||||
from ngSkinTools2.ui.widgets import NumberSliderGroup
|
||||
|
||||
log = getLogger("tab paint")
|
||||
|
||||
|
||||
def build_ui(parent_window):
|
||||
def build_mirroring_options_group():
|
||||
def get_mirror_direction():
|
||||
mirror_direction = QtWidgets.QComboBox()
|
||||
mirror_direction.addItem("Guess from stroke", MirrorOptions.directionGuess)
|
||||
mirror_direction.addItem("Positive to negative", MirrorOptions.directionPositiveToNegative)
|
||||
mirror_direction.addItem("Negative to positive", MirrorOptions.directionNegativeToPositive)
|
||||
mirror_direction.addItem("Flip", MirrorOptions.directionFlip)
|
||||
mirror_direction.setMinimumWidth(1)
|
||||
qt.select_data(mirror_direction, config.mirror_direction())
|
||||
|
||||
@qt.on(mirror_direction.currentIndexChanged)
|
||||
def value_changed():
|
||||
config.mirror_direction.set(mirror_direction.currentData())
|
||||
|
||||
return mirror_direction
|
||||
|
||||
def axis():
|
||||
mirror_axis = QtWidgets.QComboBox()
|
||||
mirror_axis.addItem("X", 'x')
|
||||
mirror_axis.addItem("Y", 'y')
|
||||
mirror_axis.addItem("Z", 'z')
|
||||
|
||||
@qt.on(mirror_axis.currentIndexChanged)
|
||||
def value_changed():
|
||||
session.state.mirror().axis = mirror_axis.currentData()
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def target_changed():
|
||||
if session.state.layersAvailable:
|
||||
qt.select_data(mirror_axis, session.state.mirror().axis)
|
||||
|
||||
target_changed()
|
||||
|
||||
return mirror_axis
|
||||
|
||||
def mirror_seam_width():
|
||||
seam_width_ctrl = NumberSliderGroup(max_value=100)
|
||||
|
||||
@signal.on(seam_width_ctrl.valueChanged)
|
||||
def value_changed():
|
||||
session.state.mirror().seam_width = seam_width_ctrl.value()
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def update_values():
|
||||
if session.state.layersAvailable:
|
||||
seam_width_ctrl.set_value(session.state.mirror().seam_width)
|
||||
|
||||
update_values()
|
||||
|
||||
return seam_width_ctrl.layout()
|
||||
|
||||
def elements():
|
||||
influences = bind_checkbox(QtWidgets.QCheckBox("Influence weights"), config.mirror_weights)
|
||||
mask = bind_checkbox(QtWidgets.QCheckBox("Layer mask"), config.mirror_mask)
|
||||
dq = bind_checkbox(QtWidgets.QCheckBox("Dual quaternion weights"), config.mirror_dq)
|
||||
|
||||
return influences, mask, dq
|
||||
|
||||
result = QtWidgets.QGroupBox("Mirroring options")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addLayout(createTitledRow("Axis:", axis()))
|
||||
layout.addLayout(createTitledRow("Direction:", get_mirror_direction()))
|
||||
layout.addLayout(createTitledRow("Seam width:", mirror_seam_width()))
|
||||
layout.addLayout(createTitledRow("Elements to mirror:", *elements()))
|
||||
|
||||
return result
|
||||
|
||||
def vertex_mapping_group():
|
||||
# noinspection PyShadowingNames
|
||||
def mirror_mesh_group():
|
||||
mesh_name_edit = QtWidgets.QLineEdit("mesh1")
|
||||
mesh_name_edit.setReadOnly(True)
|
||||
select_button = QtWidgets.QPushButton("Select")
|
||||
create_button = QtWidgets.QPushButton("Create")
|
||||
set_button = QtWidgets.QPushButton("Set")
|
||||
set_button.setToolTip("Select symmetry mesh and a skinned target first")
|
||||
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.addWidget(mesh_name_edit)
|
||||
layout.addWidget(create_button)
|
||||
layout.addWidget(select_button)
|
||||
layout.addWidget(set_button)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_ui():
|
||||
if not session.state.layersAvailable:
|
||||
return
|
||||
|
||||
mesh = Mirror(session.state.selectedSkinCluster).get_reference_mesh()
|
||||
mesh_name_edit.setText(mesh or "")
|
||||
|
||||
def select_mesh(m):
|
||||
if m is None:
|
||||
return
|
||||
|
||||
from maya import cmds
|
||||
|
||||
cmds.setToolTo("moveSuperContext")
|
||||
cmds.selectMode(component=True)
|
||||
cmds.select(m + ".vtx[*]", r=True)
|
||||
cmds.hilite(m, replace=True)
|
||||
cmds.viewFit()
|
||||
|
||||
@qt.on(select_button.clicked)
|
||||
def select_handler():
|
||||
select_mesh(Mirror(session.state.selectedSkinCluster).get_reference_mesh())
|
||||
|
||||
@qt.on(create_button.clicked)
|
||||
def create():
|
||||
if not session.state.layersAvailable:
|
||||
return
|
||||
|
||||
m = Mirror(session.state.selectedSkinCluster)
|
||||
mesh = m.get_reference_mesh()
|
||||
if mesh is None:
|
||||
mesh = m.build_reference_mesh()
|
||||
|
||||
update_ui()
|
||||
select_mesh(mesh)
|
||||
|
||||
@qt.on(set_button.clicked)
|
||||
def set_clicked():
|
||||
set_reference_mesh_from_selection()
|
||||
update_ui()
|
||||
|
||||
update_ui()
|
||||
|
||||
return layout
|
||||
|
||||
vertex_mapping_mode = QtWidgets.QComboBox()
|
||||
vertex_mapping_mode.addItem("Closest point on surface", VertexTransferMode.closestPoint)
|
||||
vertex_mapping_mode.addItem("UV space", VertexTransferMode.uvSpace)
|
||||
|
||||
result = QtWidgets.QGroupBox("Vertex Mapping")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Mapping mode:", vertex_mapping_mode))
|
||||
layout.addLayout(createTitledRow("Symmetry mesh:", mirror_mesh_group()))
|
||||
result.setLayout(layout)
|
||||
|
||||
@qt.on(vertex_mapping_mode.currentIndexChanged)
|
||||
def value_changed():
|
||||
session.state.mirror().vertex_transfer_mode = vertex_mapping_mode.currentData()
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def target_changed():
|
||||
if session.state.layersAvailable:
|
||||
qt.select_data(vertex_mapping_mode, session.state.mirror().vertex_transfer_mode)
|
||||
|
||||
return result
|
||||
|
||||
def influence_mapping_group():
|
||||
def edit_mapping():
|
||||
mapping = QtWidgets.QPushButton("Preview and edit mapping")
|
||||
|
||||
single_window_policy = qt.SingleWindowPolicy()
|
||||
|
||||
@qt.on(mapping.clicked)
|
||||
def edit():
|
||||
from ngSkinTools2.ui import influenceMappingUI
|
||||
|
||||
window = influenceMappingUI.open_ui_for_mesh(parent_window, session.state.selectedSkinCluster)
|
||||
single_window_policy.setCurrent(window)
|
||||
|
||||
return mapping
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(edit_mapping())
|
||||
|
||||
result = QtWidgets.QGroupBox("Influences mapping")
|
||||
result.setLayout(layout)
|
||||
return result
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_mirroring_options_group())
|
||||
tab.innerLayout.addWidget(vertex_mapping_group())
|
||||
tab.innerLayout.addWidget(influence_mapping_group())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
btn_mirror = QtWidgets.QPushButton("Mirror")
|
||||
tab.lowerButtonsRow.addWidget(btn_mirror)
|
||||
|
||||
@qt.on(btn_mirror.clicked)
|
||||
def mirror_clicked():
|
||||
if session.state.currentLayer.layer:
|
||||
mirror_options = MirrorOptions()
|
||||
mirror_options.direction = config.mirror_direction()
|
||||
mirror_options.mirrorDq = config.mirror_dq()
|
||||
mirror_options.mirrorMask = config.mirror_mask()
|
||||
mirror_options.mirrorWeights = config.mirror_weights()
|
||||
|
||||
session.state.mirror().mirror(mirror_options)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_ui():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_ui()
|
||||
|
||||
return tab.tabContents
|
||||
428
2023/scripts/rigging_tools/ngskintools2/ui/tabPaint.py
Normal file
@@ -0,0 +1,428 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import BrushShape, PaintMode, PaintTool, WeightsDisplayMode
|
||||
from ngSkinTools2.api import eventtypes as et
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.paint import BrushProjectionMode, MaskDisplayMode
|
||||
from ngSkinTools2.api.pyside import QAction, QActionGroup, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
from ngSkinTools2.ui.qt import bind_action_to_button
|
||||
|
||||
log = getLogger("tab paint")
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def build_ui(parent, global_actions):
|
||||
"""
|
||||
:type parent: PySide2.QtWidgets.QWidget
|
||||
:type global_actions: ngSkinTools2.ui.actions.Actions
|
||||
"""
|
||||
paint = session.paint_tool
|
||||
# TODO: move paint model to session maybe?
|
||||
|
||||
on_signal = session.signal_hub.on
|
||||
|
||||
def update_ui():
|
||||
pass # noop until it's defined
|
||||
|
||||
def build_brush_settings_group():
|
||||
def brush_mode_row3():
|
||||
row = QtWidgets.QVBoxLayout()
|
||||
|
||||
group = QActionGroup(parent)
|
||||
|
||||
actions = {}
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def create_brush_mode_button(t, mode, label, tooltip):
|
||||
a = QAction(label, parent)
|
||||
a.setToolTip(tooltip)
|
||||
a.setCheckable(True)
|
||||
actions[mode] = a
|
||||
group.addAction(a)
|
||||
|
||||
@qt.on(a.toggled)
|
||||
def toggled(checked):
|
||||
if checked and paint.paint_mode != mode:
|
||||
paint.paint_mode = mode
|
||||
|
||||
t.addAction(a)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_brush_mode_button(t, PaintMode.replace, "Replace", "Whatever")
|
||||
create_brush_mode_button(t, PaintMode.add, "Add", "")
|
||||
create_brush_mode_button(t, PaintMode.scale, "Scale", "")
|
||||
row.addWidget(t)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_brush_mode_button(t, PaintMode.smooth, "Smooth", "")
|
||||
create_brush_mode_button(t, PaintMode.sharpen, "Sharpen", "")
|
||||
row.addWidget(t)
|
||||
|
||||
@on_signal(et.tool_settings_changed, scope=row)
|
||||
def update_current_brush_mode():
|
||||
actions[paint.paint_mode].setChecked(True)
|
||||
|
||||
update_current_brush_mode()
|
||||
|
||||
return row
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def brush_shape_row():
|
||||
# noinspection PyShadowingNames
|
||||
result = QtWidgets.QToolBar()
|
||||
group = QActionGroup(parent)
|
||||
|
||||
def add_brush_shape_action(icon, title, shape, checked=False):
|
||||
a = QAction(title, parent)
|
||||
a.setCheckable(True)
|
||||
a.setIcon(QtGui.QIcon(icon))
|
||||
a.setChecked(checked)
|
||||
result.addAction(a)
|
||||
group.addAction(a)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def toggled(checked):
|
||||
if checked:
|
||||
paint.brush_shape = shape
|
||||
update_ui()
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@on_signal(et.tool_settings_changed, scope=a)
|
||||
def update_to_tool():
|
||||
a.setChecked(paint.brush_shape == shape)
|
||||
|
||||
update_to_tool()
|
||||
qt.on(a.toggled)(toggled)
|
||||
|
||||
add_brush_shape_action(':/circleSolid.png', 'Solid', BrushShape.solid, checked=True)
|
||||
add_brush_shape_action(':/circlePoly.png', 'Smooth', BrushShape.smooth)
|
||||
add_brush_shape_action(':/circleGaus.png', 'Gaus', BrushShape.gaus)
|
||||
|
||||
return result
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def brush_projection_mode_row():
|
||||
# noinspection PyShadowingNames
|
||||
result = QtWidgets.QToolBar()
|
||||
group = QActionGroup(parent)
|
||||
|
||||
def add(title, tooltip, mode, use_volume, checked):
|
||||
a = QAction(title, parent)
|
||||
a.setCheckable(True)
|
||||
a.setChecked(checked)
|
||||
a.setToolTip(tooltip)
|
||||
a.setStatusTip(tooltip)
|
||||
result.addAction(a)
|
||||
group.addAction(a)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@qt.on(a.toggled)
|
||||
def toggled(checked):
|
||||
if checked:
|
||||
paint.brush_projection_mode = mode
|
||||
paint.use_volume_neighbours = use_volume
|
||||
update_ui()
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@on_signal(et.tool_settings_changed, scope=a)
|
||||
def update_to_tool():
|
||||
a.setChecked(
|
||||
paint.brush_projection_mode == mode and (mode != BrushProjectionMode.surface or paint.use_volume_neighbours == use_volume)
|
||||
)
|
||||
|
||||
with qt.signals_blocked(a):
|
||||
update_to_tool()
|
||||
|
||||
add(
|
||||
'Surface',
|
||||
'Using first surface hit under the mouse, update all nearby vertices that are connected by surface to the hit location. '
|
||||
+ 'Only current shell will be updated.',
|
||||
BrushProjectionMode.surface,
|
||||
use_volume=False,
|
||||
checked=True,
|
||||
)
|
||||
add(
|
||||
'Volume',
|
||||
'Using first surface hit under the mouse, update all nearby vertices, including those from other shells.',
|
||||
BrushProjectionMode.surface,
|
||||
use_volume=True,
|
||||
checked=False,
|
||||
)
|
||||
add(
|
||||
'Screen',
|
||||
'Use screen projection of a brush, updating all vertices on all surfaces that are within the brush radius.',
|
||||
BrushProjectionMode.screen,
|
||||
use_volume=False,
|
||||
checked=False,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def stylus_pressure_selection():
|
||||
# noinspection PyShadowingNames
|
||||
result = QtWidgets.QComboBox()
|
||||
result.addItem("Unused")
|
||||
result.addItem("Multiply intensity")
|
||||
result.addItem("Multiply opacity")
|
||||
result.addItem("Multiply radius")
|
||||
return result
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Brush projection:", brush_projection_mode_row()))
|
||||
layout.addLayout(createTitledRow("Brush mode:", brush_mode_row3()))
|
||||
layout.addLayout(createTitledRow("Brush shape:", brush_shape_row()))
|
||||
intensity = widgets.NumberSliderGroup()
|
||||
radius = widgets.NumberSliderGroup(
|
||||
max_value=100, tooltip="You can also set brush radius by just holding <b>B</b> " "and mouse-dragging in the viewport"
|
||||
)
|
||||
iterations = widgets.NumberSliderGroup(value_type=int, min_value=1, max_value=100)
|
||||
layout.addLayout(createTitledRow("Intensity:", intensity.layout()))
|
||||
layout.addLayout(createTitledRow("Brush radius:", radius.layout()))
|
||||
layout.addLayout(createTitledRow("Brush iterations:", iterations.layout()))
|
||||
|
||||
influences_limit = widgets.NumberSliderGroup(value_type=int, min_value=0, max_value=10)
|
||||
layout.addLayout(createTitledRow("Influences limit:", influences_limit.layout()))
|
||||
|
||||
@signal.on(influences_limit.valueChanged)
|
||||
def influences_limit_changed():
|
||||
paint.influences_limit = influences_limit.value()
|
||||
update_ui()
|
||||
|
||||
fixed_influences = QtWidgets.QCheckBox("Only adjust existing vertex influences")
|
||||
fixed_influences.setToolTip(
|
||||
"When this option is enabled, smooth will only adjust existing influences per vertex, "
|
||||
"and won't include other influences from nearby vertices"
|
||||
)
|
||||
layout.addLayout(createTitledRow("Weight bleeding:", fixed_influences))
|
||||
|
||||
@qt.on(fixed_influences.stateChanged)
|
||||
def fixed_influences_changed():
|
||||
paint.fixed_influences_per_vertex = fixed_influences.isChecked()
|
||||
|
||||
limit_to_component_selection = QtWidgets.QCheckBox("Limit to component selection")
|
||||
limit_to_component_selection.setToolTip("When this option is enabled, smoothing will only happen between selected components")
|
||||
layout.addLayout(createTitledRow("Isolation:", limit_to_component_selection))
|
||||
|
||||
@qt.on(limit_to_component_selection.stateChanged)
|
||||
def limit_to_component_selection_changed():
|
||||
paint.limit_to_component_selection = limit_to_component_selection.isChecked()
|
||||
|
||||
interactive_mirror = QtWidgets.QCheckBox("Interactive mirror")
|
||||
layout.addLayout(createTitledRow("", interactive_mirror))
|
||||
|
||||
@qt.on(interactive_mirror.stateChanged)
|
||||
def interactive_mirror_changed():
|
||||
paint.mirror = interactive_mirror.isChecked()
|
||||
update_ui()
|
||||
|
||||
sample_joint_on_stroke_start = QtWidgets.QCheckBox("Sample current joint on stroke start")
|
||||
layout.addLayout(createTitledRow("", sample_joint_on_stroke_start))
|
||||
|
||||
@qt.on(sample_joint_on_stroke_start.stateChanged)
|
||||
def interactive_mirror_changed():
|
||||
paint.sample_joint_on_stroke_start = sample_joint_on_stroke_start.isChecked()
|
||||
update_ui()
|
||||
|
||||
redistribute_removed_weight = QtWidgets.QCheckBox("Distribute to other influences")
|
||||
layout.addLayout(createTitledRow("Removed weight:", redistribute_removed_weight))
|
||||
|
||||
@qt.on(redistribute_removed_weight.stateChanged)
|
||||
def redistribute_removed_weight_changed():
|
||||
paint.distribute_to_other_influences = redistribute_removed_weight.isChecked()
|
||||
update_ui()
|
||||
|
||||
stylus = stylus_pressure_selection()
|
||||
layout.addLayout(createTitledRow("Stylus pressure:", stylus))
|
||||
|
||||
@on_signal(et.tool_settings_changed, scope=layout)
|
||||
def update_ui():
|
||||
log.info("updating paint settings ui")
|
||||
log.info("brush mode:%s, brush shape: %s", paint.mode, paint.brush_shape)
|
||||
paint.update_plugin_brush_radius()
|
||||
paint.update_plugin_brush_intensity()
|
||||
|
||||
with qt.signals_blocked(intensity):
|
||||
intensity.set_value(paint.intensity)
|
||||
widgets.set_paint_expo(intensity, paint.paint_mode)
|
||||
|
||||
with qt.signals_blocked(radius):
|
||||
radius.set_range(0, 1000 if paint.brush_projection_mode == BrushProjectionMode.screen else 100, soft_max=True)
|
||||
radius.set_value(paint.brush_radius)
|
||||
|
||||
with qt.signals_blocked(iterations):
|
||||
iterations.set_value(paint.iterations)
|
||||
iterations.set_enabled(paint.paint_mode in [PaintMode.smooth, PaintMode.sharpen])
|
||||
|
||||
with qt.signals_blocked(stylus):
|
||||
stylus.setCurrentIndex(paint.tablet_mode)
|
||||
|
||||
with qt.signals_blocked(interactive_mirror):
|
||||
interactive_mirror.setChecked(paint.mirror)
|
||||
|
||||
with qt.signals_blocked(redistribute_removed_weight):
|
||||
redistribute_removed_weight.setChecked(paint.distribute_to_other_influences)
|
||||
|
||||
with qt.signals_blocked(influences_limit):
|
||||
influences_limit.set_value(paint.influences_limit)
|
||||
|
||||
with qt.signals_blocked(sample_joint_on_stroke_start):
|
||||
sample_joint_on_stroke_start.setChecked(paint.sample_joint_on_stroke_start)
|
||||
|
||||
with qt.signals_blocked(fixed_influences):
|
||||
fixed_influences.setChecked(paint.fixed_influences_per_vertex)
|
||||
fixed_influences.setEnabled(paint.paint_mode == PaintMode.smooth)
|
||||
|
||||
with qt.signals_blocked(limit_to_component_selection):
|
||||
limit_to_component_selection.setChecked(paint.limit_to_component_selection)
|
||||
limit_to_component_selection.setEnabled(fixed_influences.isEnabled())
|
||||
|
||||
@signal.on(radius.valueChanged, qtParent=layout)
|
||||
def radius_edited():
|
||||
log.info("updated brush radius")
|
||||
paint.brush_radius = radius.value()
|
||||
update_ui()
|
||||
|
||||
@signal.on(intensity.valueChanged, qtParent=layout)
|
||||
def intensity_edited():
|
||||
paint.intensity = intensity.value()
|
||||
update_ui()
|
||||
|
||||
@signal.on(iterations.valueChanged, qtParent=layout)
|
||||
def iterations_edited():
|
||||
paint.iterations = iterations.value()
|
||||
update_ui()
|
||||
|
||||
@qt.on(stylus.currentIndexChanged)
|
||||
def stylus_edited():
|
||||
paint.tablet_mode = stylus.currentIndex()
|
||||
update_ui()
|
||||
|
||||
update_ui()
|
||||
|
||||
result = QtWidgets.QGroupBox("Brush behavior")
|
||||
result.setLayout(layout)
|
||||
return result
|
||||
|
||||
def build_display_settings():
|
||||
result = QtWidgets.QGroupBox("Display settings")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
influences_display = QtWidgets.QComboBox()
|
||||
influences_display.addItem("All influences, multiple colors", WeightsDisplayMode.allInfluences)
|
||||
influences_display.addItem("Current influence, grayscale", WeightsDisplayMode.currentInfluence)
|
||||
influences_display.addItem("Current influence, colored", WeightsDisplayMode.currentInfluenceColored)
|
||||
influences_display.setMinimumWidth(1)
|
||||
influences_display.setCurrentIndex(paint.display_settings.weights_display_mode)
|
||||
|
||||
display_toolbar = QtWidgets.QToolBar()
|
||||
display_toolbar.addAction(global_actions.randomize_influences_colors)
|
||||
|
||||
@qt.on(influences_display.currentIndexChanged)
|
||||
def influences_display_changed():
|
||||
paint.display_settings.weights_display_mode = influences_display.currentData()
|
||||
update_ui_to_tool()
|
||||
|
||||
display_layout = QtWidgets.QVBoxLayout()
|
||||
display_layout.addWidget(influences_display)
|
||||
display_layout.addWidget(display_toolbar)
|
||||
layout.addLayout(createTitledRow("Influences display:", display_layout))
|
||||
|
||||
mask_display = QtWidgets.QComboBox()
|
||||
mask_display.addItem("Default", MaskDisplayMode.default_)
|
||||
mask_display.addItem("Color ramp", MaskDisplayMode.color_ramp)
|
||||
mask_display.setMinimumWidth(1)
|
||||
mask_display.setCurrentIndex(paint.display_settings.weights_display_mode)
|
||||
|
||||
@qt.on(mask_display.currentIndexChanged)
|
||||
def influences_display_changed():
|
||||
paint.display_settings.mask_display_mode = mask_display.currentData()
|
||||
update_ui_to_tool()
|
||||
|
||||
layout.addLayout(createTitledRow("Mask display:", mask_display))
|
||||
|
||||
show_effects = QtWidgets.QCheckBox("Show layer effects")
|
||||
layout.addLayout(createTitledRow("", show_effects))
|
||||
show_masked = QtWidgets.QCheckBox("Show masked weights")
|
||||
layout.addLayout(createTitledRow("", show_masked))
|
||||
|
||||
show_selected_verts_only = QtWidgets.QCheckBox("Hide unselected vertices")
|
||||
layout.addLayout(createTitledRow("", show_selected_verts_only))
|
||||
|
||||
@qt.on(show_effects.stateChanged)
|
||||
def show_effects_changed():
|
||||
paint.display_settings.layer_effects_display = show_effects.isChecked()
|
||||
|
||||
@qt.on(show_masked.stateChanged)
|
||||
def show_masked_changed():
|
||||
paint.display_settings.display_masked = show_masked.isChecked()
|
||||
|
||||
@qt.on(show_selected_verts_only.stateChanged)
|
||||
def show_selected_verts_changed():
|
||||
paint.display_settings.show_selected_verts_only = show_selected_verts_only.isChecked()
|
||||
|
||||
mesh_toolbar = QtWidgets.QToolBar()
|
||||
toggle_original_mesh = QAction("Show Original Mesh", mesh_toolbar)
|
||||
toggle_original_mesh.setCheckable(True)
|
||||
mesh_toolbar.addAction(toggle_original_mesh)
|
||||
layout.addLayout(createTitledRow("", mesh_toolbar))
|
||||
|
||||
@qt.on(toggle_original_mesh.triggered)
|
||||
def toggle_display_node_visible():
|
||||
paint.display_settings.display_node_visible = not toggle_original_mesh.isChecked()
|
||||
update_ui_to_tool()
|
||||
|
||||
wireframe_color_button = widgets.ColorButton()
|
||||
layout.addLayout(createTitledRow("Wireframe color:", wireframe_color_button))
|
||||
|
||||
@signal.on(wireframe_color_button.color_changed)
|
||||
def update_wireframe_color():
|
||||
if paint.display_settings.weights_display_mode == WeightsDisplayMode.allInfluences:
|
||||
paint.display_settings.wireframe_color = wireframe_color_button.get_color_3f()
|
||||
else:
|
||||
paint.display_settings.wireframe_color_single_influence = wireframe_color_button.get_color_3f()
|
||||
|
||||
@signal.on(session.events.toolChanged, qtParent=tab.tabContents)
|
||||
def update_ui_to_tool():
|
||||
ds = paint.display_settings
|
||||
toggle_original_mesh.setChecked(PaintTool.is_painting() and not ds.display_node_visible)
|
||||
|
||||
qt.select_data(influences_display, ds.weights_display_mode)
|
||||
qt.select_data(mask_display, ds.mask_display_mode)
|
||||
show_effects.setChecked(ds.layer_effects_display)
|
||||
show_masked.setChecked(ds.display_masked)
|
||||
show_selected_verts_only.setChecked(ds.show_selected_verts_only)
|
||||
global_actions.randomize_influences_colors.setEnabled(ds.weights_display_mode == WeightsDisplayMode.allInfluences)
|
||||
display_toolbar.setVisible(global_actions.randomize_influences_colors.isEnabled())
|
||||
|
||||
if ds.weights_display_mode == WeightsDisplayMode.allInfluences:
|
||||
wireframe_color_button.set_color(ds.wireframe_color)
|
||||
else:
|
||||
wireframe_color_button.set_color(ds.wireframe_color_single_influence)
|
||||
|
||||
update_ui_to_tool()
|
||||
|
||||
result.setLayout(layout)
|
||||
return result
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_brush_settings_group())
|
||||
tab.innerLayout.addWidget(build_display_settings())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
tab.lowerButtonsRow.addWidget(bind_action_to_button(global_actions.paint, QtWidgets.QPushButton()))
|
||||
tab.lowerButtonsRow.addWidget(bind_action_to_button(global_actions.flood, QtWidgets.QPushButton()))
|
||||
|
||||
@signal.on(session.events.toolChanged, qtParent=tab.tabContents)
|
||||
def update_to_tool():
|
||||
tab.scrollArea.setEnabled(PaintTool.is_painting())
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_to_tool()
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
228
2023/scripts/rigging_tools/ngskintools2/ui/tabSetWeights.py
Normal file
@@ -0,0 +1,228 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import PaintMode, PaintModeSettings, flood_weights
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QAction, QActionGroup, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.signal import Signal
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
from ngSkinTools2.ui.ui_lock import UiLock
|
||||
|
||||
log = getLogger("tab set weights")
|
||||
|
||||
|
||||
def make_presets():
|
||||
presets = {m: PaintModeSettings() for m in PaintMode.all()}
|
||||
for k, v in presets.items():
|
||||
v.mode = k
|
||||
|
||||
presets[PaintMode.smooth].intensity = 0.3
|
||||
presets[PaintMode.scale].intensity = 0.3
|
||||
presets[PaintMode.add].intensity = 0.1
|
||||
presets[PaintMode.scale].intensity = 0.95
|
||||
|
||||
return presets
|
||||
|
||||
|
||||
class Model(Object):
|
||||
def __init__(self):
|
||||
self.mode_changed = Signal("mode changed")
|
||||
self.presets = make_presets()
|
||||
self.current_settings = None
|
||||
self.set_mode(PaintMode.replace)
|
||||
|
||||
def set_mode(self, mode):
|
||||
self.current_settings = self.presets[mode]
|
||||
self.mode_changed.emit()
|
||||
|
||||
def apply(self):
|
||||
flood_weights(session.state.currentLayer.layer, influences=session.state.currentLayer.layer.paint_targets, settings=self.current_settings)
|
||||
|
||||
|
||||
def build_ui(parent):
|
||||
model = Model()
|
||||
ui_lock = UiLock()
|
||||
|
||||
def build_mode_settings_group():
|
||||
def mode_row():
|
||||
row = QtWidgets.QVBoxLayout()
|
||||
|
||||
group = QActionGroup(parent)
|
||||
|
||||
actions = {}
|
||||
|
||||
def create_mode_button(toolbar, mode, label, tooltip):
|
||||
a = QAction(label, parent)
|
||||
a.setToolTip(tooltip)
|
||||
a.setStatusTip(tooltip)
|
||||
a.setCheckable(True)
|
||||
actions[mode] = a
|
||||
group.addAction(a)
|
||||
|
||||
@qt.on(a.toggled)
|
||||
@ui_lock.skip_if_updating
|
||||
def toggled(checked):
|
||||
if checked:
|
||||
model.set_mode(mode)
|
||||
update_ui()
|
||||
|
||||
toolbar.addAction(a)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_mode_button(t, PaintMode.replace, "Replace", "")
|
||||
create_mode_button(t, PaintMode.add, "Add", "")
|
||||
create_mode_button(t, PaintMode.scale, "Scale", "")
|
||||
row.addWidget(t)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_mode_button(t, PaintMode.smooth, "Smooth", "")
|
||||
create_mode_button(t, PaintMode.sharpen, "Sharpen", "")
|
||||
row.addWidget(t)
|
||||
|
||||
actions[model.current_settings.mode].setChecked(True)
|
||||
|
||||
return row
|
||||
|
||||
influences_limit = widgets.NumberSliderGroup(value_type=int, min_value=0, max_value=10)
|
||||
|
||||
@signal.on(influences_limit.valueChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def influences_limit_changed():
|
||||
for _, v in model.presets.items():
|
||||
v.influences_limit = influences_limit.value()
|
||||
update_ui()
|
||||
|
||||
intensity = widgets.NumberSliderGroup()
|
||||
|
||||
@signal.on(intensity.valueChanged, qtParent=parent)
|
||||
@ui_lock.skip_if_updating
|
||||
def intensity_edited():
|
||||
model.current_settings.intensity = intensity.value()
|
||||
update_ui()
|
||||
|
||||
iterations = widgets.NumberSliderGroup(value_type=int, min_value=1, max_value=100)
|
||||
|
||||
@signal.on(iterations.valueChanged, qtParent=parent)
|
||||
@ui_lock.skip_if_updating
|
||||
def iterations_edited():
|
||||
model.current_settings.iterations = iterations.value()
|
||||
update_ui()
|
||||
|
||||
fixed_influences = QtWidgets.QCheckBox("Only adjust existing vertex influences")
|
||||
fixed_influences.setToolTip(
|
||||
"When this option is enabled, smooth will only adjust existing influences per vertex, "
|
||||
"and won't include other influences from nearby vertices"
|
||||
)
|
||||
|
||||
volume_neighbours = QtWidgets.QCheckBox("Smooth across gaps and thin surfaces")
|
||||
volume_neighbours.setToolTip(
|
||||
"Use all nearby neighbours, regardless if they belong to same surface. "
|
||||
"This will allow for smoothing to happen across gaps and thin surfaces."
|
||||
)
|
||||
|
||||
limit_to_component_selection = QtWidgets.QCheckBox("Limit to component selection")
|
||||
limit_to_component_selection.setToolTip("When this option is enabled, smoothing will only happen between selected components")
|
||||
|
||||
@qt.on(fixed_influences.stateChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def fixed_influences_changed(*_):
|
||||
model.current_settings.fixed_influences_per_vertex = fixed_influences.isChecked()
|
||||
|
||||
@qt.on(limit_to_component_selection.stateChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def limit_to_component_selection_changed(*_):
|
||||
model.current_settings.limit_to_component_selection = limit_to_component_selection.isChecked()
|
||||
|
||||
def update_ui():
|
||||
with ui_lock:
|
||||
widgets.set_paint_expo(intensity, model.current_settings.mode)
|
||||
|
||||
intensity.set_value(model.current_settings.intensity)
|
||||
|
||||
iterations.set_value(model.current_settings.iterations)
|
||||
iterations.set_enabled(model.current_settings.mode in [PaintMode.smooth, PaintMode.sharpen])
|
||||
|
||||
fixed_influences.setEnabled(model.current_settings.mode in [PaintMode.smooth])
|
||||
fixed_influences.setChecked(model.current_settings.fixed_influences_per_vertex)
|
||||
|
||||
limit_to_component_selection.setChecked(model.current_settings.limit_to_component_selection)
|
||||
limit_to_component_selection.setEnabled(fixed_influences.isEnabled())
|
||||
|
||||
influences_limit.set_value(model.current_settings.influences_limit)
|
||||
|
||||
volume_neighbours.setChecked(model.current_settings.use_volume_neighbours)
|
||||
volume_neighbours.setEnabled(model.current_settings.mode == PaintMode.smooth)
|
||||
|
||||
settings_group = QtWidgets.QGroupBox("Mode Settings")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
layout.addLayout(createTitledRow("Mode:", mode_row()))
|
||||
layout.addLayout(createTitledRow("Intensity:", intensity.layout()))
|
||||
layout.addLayout(createTitledRow("Iterations:", iterations.layout()))
|
||||
layout.addLayout(createTitledRow("Influences limit:", influences_limit.layout()))
|
||||
layout.addLayout(createTitledRow("Weight bleeding:", fixed_influences))
|
||||
layout.addLayout(createTitledRow("Volume smoothing:", volume_neighbours))
|
||||
layout.addLayout(createTitledRow("Isolation:", limit_to_component_selection))
|
||||
settings_group.setLayout(layout)
|
||||
|
||||
update_ui()
|
||||
|
||||
return settings_group
|
||||
|
||||
def common_settings():
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
mirror = QtWidgets.QCheckBox("Mirror")
|
||||
layout.addLayout(createTitledRow("", mirror))
|
||||
|
||||
@qt.on(mirror.stateChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def mirror_changed(*_):
|
||||
for _, v in model.presets.items():
|
||||
v.mirror = mirror.isChecked()
|
||||
|
||||
redistribute_removed_weight = QtWidgets.QCheckBox("Distribute to other influences")
|
||||
layout.addLayout(createTitledRow("Removed weight:", redistribute_removed_weight))
|
||||
|
||||
@qt.on(redistribute_removed_weight.stateChanged)
|
||||
def redistribute_removed_weight_changed():
|
||||
for _, v in model.presets.items():
|
||||
v.distribute_to_other_influences = redistribute_removed_weight.isChecked()
|
||||
|
||||
@signal.on(model.mode_changed, qtParent=layout)
|
||||
def update_ui():
|
||||
mirror.setChecked(model.current_settings.mirror)
|
||||
redistribute_removed_weight.setChecked(model.current_settings.distribute_to_other_influences)
|
||||
|
||||
group = QtWidgets.QGroupBox("Common Settings")
|
||||
group.setLayout(layout)
|
||||
|
||||
update_ui()
|
||||
|
||||
return group
|
||||
|
||||
def apply_button():
|
||||
btn = QtWidgets.QPushButton("Apply")
|
||||
btn.setToolTip("Apply selected operation to vertex")
|
||||
|
||||
@qt.on(btn.clicked)
|
||||
def clicked():
|
||||
model.apply()
|
||||
|
||||
return btn
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_mode_settings_group())
|
||||
tab.innerLayout.addWidget(common_settings())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
tab.lowerButtonsRow.addWidget(apply_button())
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
117
2023/scripts/rigging_tools/ngskintools2/ui/tabTools.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.pyside import QtWidgets
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.ui import model_binds, qt, widgets
|
||||
from ngSkinTools2.ui.actions import Actions
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
|
||||
|
||||
def build_ui(actions, session):
|
||||
"""
|
||||
|
||||
:type actions: Actions
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
def assign_weights_from_closest_joint_group():
|
||||
options = actions.toolsAssignFromClosestJointOptions
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def influences_options():
|
||||
result = QtWidgets.QVBoxLayout()
|
||||
button_group = QtWidgets.QButtonGroup()
|
||||
for index, i in enumerate(["Use all available influences", "Use selected influences"]):
|
||||
radio = QtWidgets.QRadioButton(i)
|
||||
button_group.addButton(radio, index)
|
||||
result.addWidget(radio)
|
||||
|
||||
@qt.on(radio.toggled)
|
||||
def update_value():
|
||||
options.all_influences.set(button_group.buttons()[0].isChecked())
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@signal.on(options.all_influences.changed, qtParent=result)
|
||||
def update_ui():
|
||||
button_group.buttons()[0 if options.all_influences() else 1].setChecked(True)
|
||||
|
||||
update_ui()
|
||||
|
||||
return result
|
||||
|
||||
new_layer = QtWidgets.QCheckBox("Create new layer")
|
||||
|
||||
@qt.on(new_layer.toggled)
|
||||
def update_new_layer():
|
||||
options.create_new_layer.set(new_layer.isChecked())
|
||||
|
||||
@signal.on(options.create_new_layer.changed)
|
||||
def update_ui():
|
||||
new_layer.setChecked(options.create_new_layer())
|
||||
|
||||
btn = QtWidgets.QPushButton()
|
||||
qt.bind_action_to_button(actions.toolsAssignFromClosestJoint, btn)
|
||||
|
||||
update_ui()
|
||||
|
||||
result = QtWidgets.QGroupBox("Assign weights from closest joint")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addLayout(createTitledRow("Target layer", new_layer))
|
||||
layout.addLayout(createTitledRow("Influences", influences_options()))
|
||||
layout.addWidget(btn)
|
||||
|
||||
return result
|
||||
|
||||
def unify_weights_group():
|
||||
options = actions.toolsUnifyWeightsOptions
|
||||
|
||||
intensity = widgets.NumberSliderGroup()
|
||||
model_binds.bind(intensity, options.overall_effect)
|
||||
|
||||
single_cluster_mode = QtWidgets.QCheckBox(
|
||||
"Single group mode",
|
||||
)
|
||||
single_cluster_mode.setToolTip("average weights across whole selection, ignoring separate shells or selection gaps")
|
||||
model_binds.bind(single_cluster_mode, options.single_cluster_mode)
|
||||
|
||||
btn = QtWidgets.QPushButton()
|
||||
qt.bind_action_to_button(actions.toolsUnifyWeights, btn)
|
||||
|
||||
result = QtWidgets.QGroupBox("Unify weights")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addLayout(createTitledRow("Intensity:", intensity.layout()))
|
||||
layout.addLayout(createTitledRow("Clustering:", single_cluster_mode))
|
||||
layout.addWidget(btn)
|
||||
|
||||
return result
|
||||
|
||||
def other_tools_group():
|
||||
result = QtWidgets.QGroupBox("Other")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addWidget(to_button(actions.fill_layer_transparency))
|
||||
layout.addWidget(to_button(actions.copy_components))
|
||||
layout.addWidget(to_button(actions.paste_component_average))
|
||||
|
||||
return result
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(assign_weights_from_closest_joint_group())
|
||||
tab.innerLayout.addWidget(unify_weights_group())
|
||||
tab.innerLayout.addWidget(other_tools_group())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
|
||||
|
||||
def to_button(action):
|
||||
btn = QtWidgets.QPushButton()
|
||||
qt.bind_action_to_button(action, btn)
|
||||
return btn
|
||||
142
2023/scripts/rigging_tools/ngskintools2/ui/targetui.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.influence_names import InfluenceNameFilter
|
||||
from ngSkinTools2.api.pyside import QAction, QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.operations import import_v1_actions
|
||||
from ngSkinTools2.ui import influencesview, layersview, qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def build_layers_ui(parent, actions, session):
|
||||
"""
|
||||
|
||||
:type session: Session
|
||||
:type actions: ngSkinTools2.ui.actions.Actions
|
||||
:type parent: QWidget
|
||||
"""
|
||||
|
||||
influences_filter = InfluenceNameFilter()
|
||||
|
||||
def build_infl_filter():
|
||||
img = qt.image_icon("clear-input-white.png")
|
||||
|
||||
result = QtWidgets.QHBoxLayout()
|
||||
result.setSpacing(5)
|
||||
filter = QtWidgets.QComboBox()
|
||||
filter.setMinimumHeight(22 * scale_multiplier)
|
||||
filter.setEditable(True)
|
||||
filter.lineEdit().setPlaceholderText("Search...")
|
||||
result.addWidget(filter)
|
||||
# noinspection PyShadowingNames
|
||||
clear = QAction(result)
|
||||
clear.setIcon(img)
|
||||
filter.lineEdit().addAction(clear, QtWidgets.QLineEdit.TrailingPosition)
|
||||
|
||||
@qt.on(filter.editTextChanged)
|
||||
def filter_edited():
|
||||
influences_filter.set_filter_string(filter.currentText())
|
||||
|
||||
clear.setVisible(len(filter.currentText()) != 0)
|
||||
|
||||
@qt.on(clear.triggered)
|
||||
def clear_clicked():
|
||||
filter.clearEditText()
|
||||
|
||||
filter_edited()
|
||||
|
||||
return result
|
||||
|
||||
split = QtWidgets.QSplitter(orientation=QtCore.Qt.Horizontal, parent=parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(3)
|
||||
clear = QtWidgets.QPushButton()
|
||||
clear.setFixedSize(20, 20)
|
||||
# layout.addWidget(clear)
|
||||
|
||||
layers = layersview.build_view(parent, actions)
|
||||
layout.addWidget(layers)
|
||||
split.addWidget(qt.wrap_layout_into_widget(layout))
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(3)
|
||||
influences = influencesview.build_view(parent, actions, session, filter=influences_filter)
|
||||
layout.addWidget(influences)
|
||||
layout.addLayout(build_infl_filter())
|
||||
split.addWidget(qt.wrap_layout_into_widget(layout))
|
||||
|
||||
return split
|
||||
|
||||
|
||||
def build_no_layers_ui(parent, actions, session):
|
||||
"""
|
||||
:param parent: ui parent
|
||||
:type actions: ngSkinTools2.ui.actions.Actions
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(30, 30, 30, 30)
|
||||
|
||||
selection_display = QtWidgets.QLabel("pPlane1")
|
||||
selection_display.setStyleSheet("font-weight: bold")
|
||||
|
||||
selection_note = QtWidgets.QLabel("Skinning Layers cannot be attached to this object")
|
||||
selection_note.setWordWrap(True)
|
||||
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(selection_display)
|
||||
layout.addWidget(selection_note)
|
||||
layout.addWidget(qt.bind_action_to_button(actions.import_v1, QtWidgets.QPushButton()))
|
||||
layout.addWidget(qt.bind_action_to_button(actions.initialize, QtWidgets.QPushButton()))
|
||||
layout.addStretch(3)
|
||||
|
||||
layout_widget = qt.wrap_layout_into_widget(layout)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=parent)
|
||||
def handle_target_changed():
|
||||
if session.state.layersAvailable:
|
||||
return # no need to update
|
||||
|
||||
is_skinned = session.state.selectedSkinCluster is not None
|
||||
selection_display.setText(session.state.selectedSkinCluster)
|
||||
selection_display.setVisible(is_skinned)
|
||||
|
||||
note = "Select a mesh with a skin cluster attached."
|
||||
if is_skinned:
|
||||
note = "Skinning layers are not yet initialized for this mesh."
|
||||
if import_v1_actions.can_import(session):
|
||||
note = "Skinning layers from previous ngSkinTools version are initialized on this mesh."
|
||||
|
||||
selection_note.setText(note)
|
||||
|
||||
if session.active():
|
||||
handle_target_changed()
|
||||
|
||||
return layout_widget
|
||||
|
||||
|
||||
def build_target_ui(parent, actions, session):
|
||||
"""
|
||||
:param actions:
|
||||
:param parent:
|
||||
:type session: Session
|
||||
"""
|
||||
result = QtWidgets.QStackedWidget()
|
||||
result.addWidget(build_no_layers_ui(parent, actions, session))
|
||||
result.addWidget(build_layers_ui(parent, actions, session))
|
||||
result.setMinimumHeight(300 * scale_multiplier)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=parent)
|
||||
def handle_target_changed():
|
||||
if not session.state.layersAvailable:
|
||||
result.setCurrentIndex(0)
|
||||
else:
|
||||
result.setCurrentIndex(1)
|
||||
|
||||
if session.active():
|
||||
handle_target_changed()
|
||||
|
||||
return result
|
||||
207
2023/scripts/rigging_tools/ngskintools2/ui/transferDialog.py
Normal file
@@ -0,0 +1,207 @@
|
||||
from ngSkinTools2 import api, cleanup, signal
|
||||
from ngSkinTools2.api import VertexTransferMode
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.api.transfer import LayersTransfer
|
||||
from ngSkinTools2.decorators import undoable
|
||||
from ngSkinTools2.ui import influenceMappingUI, qt, widgets
|
||||
from ngSkinTools2.ui.layout import createTitledRow, scale_multiplier
|
||||
|
||||
|
||||
class UiModel:
|
||||
def __init__(self):
|
||||
self.transfer = LayersTransfer()
|
||||
|
||||
def destination_has_layers(self):
|
||||
l = api.Layers(self.transfer.target)
|
||||
return l.is_enabled() and len(l.list()) > 0
|
||||
|
||||
@undoable
|
||||
def do_apply(self):
|
||||
self.transfer.complete_execution()
|
||||
from maya import cmds
|
||||
|
||||
cmds.select(self.transfer.target)
|
||||
if session.active():
|
||||
session.events.targetChanged.emitIfChanged()
|
||||
|
||||
|
||||
single_transfer_dialog_policy = qt.SingleWindowPolicy()
|
||||
|
||||
|
||||
def open(parent, model):
|
||||
"""
|
||||
|
||||
:type model: UiModel
|
||||
"""
|
||||
|
||||
def buttonRow(window):
|
||||
def apply():
|
||||
model.do_apply()
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
window.close()
|
||||
|
||||
return widgets.button_row(
|
||||
[
|
||||
("Transfer", apply),
|
||||
("Cancel", window.close),
|
||||
]
|
||||
)
|
||||
|
||||
def view_influences_settings():
|
||||
tabs.setCurrentIndex(1)
|
||||
|
||||
def build_settings():
|
||||
result = QtWidgets.QVBoxLayout()
|
||||
|
||||
vertexMappingMode = QtWidgets.QComboBox()
|
||||
vertexMappingMode.addItem("Closest point on surface", VertexTransferMode.closestPoint)
|
||||
vertexMappingMode.addItem("UV space", VertexTransferMode.uvSpace)
|
||||
vertexMappingMode.addItem("By vertex ID (source and destination vert count must match)", VertexTransferMode.vertexId)
|
||||
|
||||
g = QtWidgets.QGroupBox("Selection")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
g.setLayout(layout)
|
||||
|
||||
sourceLabel = QtWidgets.QLabel()
|
||||
layout.addLayout(createTitledRow("Source:", sourceLabel))
|
||||
|
||||
destinationLabel = QtWidgets.QLabel()
|
||||
layout.addLayout(createTitledRow("Destination:", destinationLabel))
|
||||
result.addWidget(g)
|
||||
|
||||
g = QtWidgets.QGroupBox("Vertex mapping")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Mapping mode:", vertexMappingMode))
|
||||
g.setLayout(layout)
|
||||
result.addWidget(g)
|
||||
|
||||
g = QtWidgets.QGroupBox("Influences mapping")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
g.setLayout(layout)
|
||||
|
||||
edit = QtWidgets.QPushButton("Configure")
|
||||
qt.on(edit.clicked)(view_influences_settings)
|
||||
|
||||
button_row = QtWidgets.QHBoxLayout()
|
||||
button_row.addWidget(edit)
|
||||
button_row.addStretch()
|
||||
layout.addLayout(button_row)
|
||||
|
||||
result.addWidget(g)
|
||||
|
||||
g = QtWidgets.QGroupBox("Other options")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
g.setLayout(layout)
|
||||
|
||||
keep_layers = QtWidgets.QCheckBox("Keep existing layers on destination")
|
||||
keep_layers_row = qt.wrap_layout_into_widget(createTitledRow("Destination layers:", keep_layers))
|
||||
layout.addWidget(keep_layers_row)
|
||||
|
||||
@qt.on(keep_layers.stateChanged)
|
||||
def checked():
|
||||
model.transfer.keep_existing_layers = keep_layers.isChecked()
|
||||
|
||||
result.addWidget(g)
|
||||
|
||||
result.addStretch()
|
||||
|
||||
def update_settings_to_model():
|
||||
keep_layers.setChecked(model.transfer.keep_existing_layers)
|
||||
qt.select_data(vertexMappingMode, model.transfer.vertex_transfer_mode)
|
||||
source_title = model.transfer.source
|
||||
if model.transfer.source_file is not None:
|
||||
source_title = 'file ' + model.transfer.source_file
|
||||
sourceLabel.setText("<strong>" + source_title + "</strong>")
|
||||
destinationLabel.setText("<strong>" + model.transfer.target + "</strong>")
|
||||
keep_layers_row.setEnabled(model.destination_has_layers())
|
||||
|
||||
@qt.on(vertexMappingMode.currentIndexChanged)
|
||||
def vertex_mapping_mode_changed():
|
||||
model.transfer.vertex_transfer_mode = vertexMappingMode.currentData()
|
||||
|
||||
update_settings_to_model()
|
||||
|
||||
return result
|
||||
|
||||
def build_influenes_tab():
|
||||
infl_ui, _, recalcMatches = influenceMappingUI.build_ui(parent, model.transfer.influences_mapping)
|
||||
|
||||
padding = QtWidgets.QVBoxLayout()
|
||||
padding.setContentsMargins(0, 20 * scale_multiplier, 0, 0)
|
||||
padding.addWidget(infl_ui)
|
||||
|
||||
recalcMatches()
|
||||
|
||||
return padding
|
||||
|
||||
tabs = QtWidgets.QTabWidget()
|
||||
|
||||
tabs.addTab(qt.wrap_layout_into_widget(build_settings()), "Settings")
|
||||
tabs.addTab(qt.wrap_layout_into_widget(build_influenes_tab()), "Influences mapping")
|
||||
|
||||
window = QtWidgets.QDialog(parent)
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
window.setWindowTitle("Transfer")
|
||||
window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
window.resize(720 * scale_multiplier, 500 * scale_multiplier)
|
||||
window.setLayout(QtWidgets.QVBoxLayout())
|
||||
|
||||
window.layout().addWidget(tabs)
|
||||
window.layout().addLayout(buttonRow(window))
|
||||
|
||||
if session.active():
|
||||
session.addQtWidgetReference(window)
|
||||
|
||||
single_transfer_dialog_policy.setCurrent(window)
|
||||
window.show()
|
||||
|
||||
|
||||
def build_transfer_action(session, parent):
|
||||
from maya import cmds
|
||||
|
||||
from .actions import define_action
|
||||
|
||||
targets = []
|
||||
|
||||
def detect_targets():
|
||||
targets[:] = []
|
||||
selection = cmds.ls(sl=True)
|
||||
if len(selection) != 2:
|
||||
return False
|
||||
|
||||
if not api.Layers(selection[0]).is_enabled():
|
||||
return False
|
||||
|
||||
targets[:] = selection
|
||||
|
||||
return True
|
||||
|
||||
def transfer_dialog(transfer):
|
||||
"""
|
||||
|
||||
:type transfer: LayersTransfer
|
||||
"""
|
||||
model = UiModel()
|
||||
model.transfer = transfer
|
||||
open(parent, model)
|
||||
|
||||
def handler():
|
||||
if not targets:
|
||||
return
|
||||
|
||||
t = LayersTransfer()
|
||||
t.source = targets[0]
|
||||
t.target = targets[1]
|
||||
t.customize_callback = transfer_dialog
|
||||
t.execute()
|
||||
|
||||
result = define_action(parent, "Transfer layers...", callback=handler)
|
||||
|
||||
@signal.on(session.events.nodeSelectionChanged)
|
||||
def on_selection_changed():
|
||||
result.setEnabled(detect_targets())
|
||||
|
||||
on_selection_changed()
|
||||
|
||||
return result
|
||||
23
2023/scripts/rigging_tools/ngskintools2/ui/ui_lock.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import functools
|
||||
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
class UiLock(Object):
|
||||
def __init__(self):
|
||||
self.updating = False
|
||||
|
||||
def __enter__(self):
|
||||
self.updating = True
|
||||
|
||||
def skip_if_updating(self, fn):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
if self.updating:
|
||||
return
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
def __exit__(self, _type, value, traceback):
|
||||
self.updating = False
|
||||
132
2023/scripts/rigging_tools/ngskintools2/ui/updatewindow.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import webbrowser
|
||||
|
||||
from ngSkinTools2.api import versioncheck
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import Qt, QtWidgets
|
||||
from ngSkinTools2.ui.options import bind_checkbox, config
|
||||
|
||||
from .. import cleanup, signal, version
|
||||
from . import qt
|
||||
from .layout import scale_multiplier
|
||||
|
||||
log = getLogger("plugin")
|
||||
|
||||
|
||||
def show(parent, silent_mode):
|
||||
"""
|
||||
:type parent: QWidget
|
||||
"""
|
||||
|
||||
error_signal = signal.Signal("error")
|
||||
success_signal = signal.Signal("success")
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def body():
|
||||
result = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.setContentsMargins(20, 30, 20, 20)
|
||||
|
||||
header = QtWidgets.QLabel("<strong>Checking for update...</strong>")
|
||||
result1 = QtWidgets.QLabel("Current version: <strong>2.0.0</strong>")
|
||||
result2 = QtWidgets.QLabel("Update available: 2.0.1")
|
||||
download = QtWidgets.QPushButton("Download ngSkinTools v2.0.1")
|
||||
# layout.addWidget(QtWidgets.QLabel("Checking for updates..."))
|
||||
layout.addWidget(header)
|
||||
layout.addWidget(result1)
|
||||
layout.addWidget(result2)
|
||||
layout.addWidget(download)
|
||||
|
||||
result1.setVisible(False)
|
||||
result2.setVisible(False)
|
||||
download.setVisible(False)
|
||||
|
||||
@signal.on(error_signal)
|
||||
def error_handler(error):
|
||||
header.setText("<strong>Error: {0}</strong>".format(error))
|
||||
|
||||
@signal.on(success_signal)
|
||||
def success_handler(info):
|
||||
"""
|
||||
|
||||
:type info: ngSkinTools2.api.versioncheck.
|
||||
"""
|
||||
|
||||
header.setText("<strong>{0}</strong>".format('Update available!' if info.update_available else 'ngSkinTools is up to date.'))
|
||||
result1.setVisible(True)
|
||||
result1.setText("Current version: <strong>{0}</strong>".format(version.pluginVersion()))
|
||||
if info.update_available:
|
||||
result2.setVisible(True)
|
||||
result2.setText(
|
||||
"Update available: <strong>{0}</strong>, released on {1}".format(info.latest_version, info.update_date.strftime("%d %B, %Y"))
|
||||
)
|
||||
download.setVisible(True)
|
||||
download.setText("Download ngSkinTools v" + info.latest_version)
|
||||
|
||||
@qt.on(download.clicked)
|
||||
def open_link():
|
||||
webbrowser.open_new(info.download_url)
|
||||
|
||||
return result
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def buttonsRow(window):
|
||||
btn_close = QtWidgets.QPushButton("Close")
|
||||
btn_close.setMinimumWidth(100 * scale_multiplier)
|
||||
|
||||
check_do_on_startup = bind_checkbox(QtWidgets.QCheckBox("Check for updates at startup"), config.checkForUpdatesAtStartup)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.addWidget(check_do_on_startup)
|
||||
layout.addStretch()
|
||||
layout.addWidget(btn_close)
|
||||
layout.setContentsMargins(20 * scale_multiplier, 15 * scale_multiplier, 20 * scale_multiplier, 15 * scale_multiplier)
|
||||
|
||||
btn_close.clicked.connect(lambda: window.close())
|
||||
return layout
|
||||
|
||||
window = QtWidgets.QWidget(parent, Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
|
||||
window.resize(400 * scale_multiplier, 200 * scale_multiplier)
|
||||
window.setAttribute(Qt.WA_DeleteOnClose)
|
||||
window.setWindowTitle("ngSkinTools2 version update")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
window.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout.addWidget(body())
|
||||
layout.addStretch(2)
|
||||
layout.addLayout(buttonsRow(window))
|
||||
|
||||
if not silent_mode:
|
||||
window.show()
|
||||
|
||||
@signal.on(success_signal)
|
||||
def on_success(info):
|
||||
if silent_mode:
|
||||
if info.update_available:
|
||||
window.show()
|
||||
else:
|
||||
log.info("not showing the window")
|
||||
window.close()
|
||||
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
|
||||
@qt.on(window.destroyed)
|
||||
def closed():
|
||||
log.info("deleting update window")
|
||||
|
||||
versioncheck.download_update_info(success_callback=success_signal.emit, failure_callback=error_signal.emit)
|
||||
|
||||
|
||||
def silent_check_and_show_if_available(parent):
|
||||
show(parent, silent_mode=True)
|
||||
|
||||
|
||||
def show_and_start_update(parent):
|
||||
show(parent, silent_mode=False)
|
||||
|
||||
|
||||
def build_action_check_for_updates(parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
return actions.define_action(parent, "Check for Updates...", callback=lambda: show_and_start_update(parent))
|
||||
219
2023/scripts/rigging_tools/ngskintools2/ui/widgets.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from ngSkinTools2.api import PaintMode
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.signal import Signal
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def curve_mapping(x, s, t):
|
||||
"""
|
||||
provides a linear-to smooth curve mapping
|
||||
|
||||
based on a paper https://arxiv.org/abs/2010.09714
|
||||
"""
|
||||
epsilon = 0.000001
|
||||
|
||||
if x < 0:
|
||||
return 0
|
||||
if x > 1:
|
||||
return 1
|
||||
if x < t:
|
||||
return (t * x) / (x + s * (t - x) + epsilon)
|
||||
|
||||
return ((1 - t) * (x - 1)) / (1 - x - s * (t - x) + epsilon) + 1
|
||||
|
||||
|
||||
class NumberSliderGroup(Object):
|
||||
"""
|
||||
float spinner is the "main control" while the slider acts as complementary way to change value
|
||||
"""
|
||||
|
||||
slider_resolution = 1000.0
|
||||
infinity_max = 65535
|
||||
|
||||
def __init__(self, value_type=float, min_value=0, max_value=1, soft_max=True, tooltip="", expo=None, decimals=3):
|
||||
self.value_range = 0
|
||||
self.min_value = 0
|
||||
self.max_value = 0
|
||||
|
||||
self.float_mode = value_type == float
|
||||
|
||||
self.__layout = layout = QtWidgets.QHBoxLayout()
|
||||
self.valueChanged = Signal("sliderGroupValueChanged")
|
||||
|
||||
self.spinner = spinner = QtWidgets.QDoubleSpinBox() if self.float_mode else QtWidgets.QSpinBox()
|
||||
spinner.setKeyboardTracking(False)
|
||||
|
||||
self.expo = expo
|
||||
self.expo_coefficient = 1.0
|
||||
|
||||
spinner.setFixedWidth(80 * scale_multiplier)
|
||||
if self.float_mode:
|
||||
spinner.setDecimals(decimals)
|
||||
|
||||
spinner.setToolTip(tooltip)
|
||||
|
||||
self.slider = slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
||||
slider.setMinimum(0)
|
||||
slider.setMaximum(self.slider_resolution)
|
||||
slider.setToolTip(tooltip)
|
||||
|
||||
layout.addWidget(spinner)
|
||||
layout.addWidget(slider)
|
||||
|
||||
self.set_range(min_value, max_value, soft_max=soft_max)
|
||||
|
||||
@qt.on(spinner.valueChanged)
|
||||
def update_slider():
|
||||
slider_value = self.__to_slider_value(spinner.value())
|
||||
if slider.value() == slider_value:
|
||||
return
|
||||
with qt.signals_blocked(slider):
|
||||
slider.setValue(slider_value)
|
||||
self.valueChanged.emit()
|
||||
|
||||
@qt.on(slider.valueChanged)
|
||||
def slider_dragging():
|
||||
slider_value = self.__from_slider_value(slider.value())
|
||||
with qt.signals_blocked(spinner):
|
||||
spinner.setValue(slider_value)
|
||||
|
||||
@qt.on(slider.sliderReleased)
|
||||
def slider_drag_finished():
|
||||
self.valueChanged.emit()
|
||||
|
||||
self.update_slider = update_slider
|
||||
|
||||
def set_range(self, min_value, max_value, soft_max=True):
|
||||
with qt.signals_blocked(self.spinner):
|
||||
self.spinner.setMaximum(self.infinity_max if soft_max else max_value)
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.value_range = max_value - min_value
|
||||
single_step = self.value_range / 100.0
|
||||
if not self.float_mode and single_step < 1:
|
||||
single_step = 1
|
||||
self.spinner.setSingleStep(single_step)
|
||||
|
||||
def __to_slider_value(self, v):
|
||||
# formulas: https://www.desmos.com/calculator/gjwk5t3wmn
|
||||
|
||||
x = float(v - self.min_value) / self.value_range
|
||||
|
||||
y = x
|
||||
if self.expo == 'start':
|
||||
y = curve_mapping(x, self.expo_coefficient, 0)
|
||||
if self.expo == 'end':
|
||||
y = curve_mapping(x, self.expo_coefficient, 1)
|
||||
|
||||
return y * self.slider_resolution
|
||||
|
||||
def __from_slider_value(self, v):
|
||||
x = v / self.slider_resolution
|
||||
if self.expo == 'start':
|
||||
x = curve_mapping(x, self.expo_coefficient, 1)
|
||||
if self.expo == 'end':
|
||||
x = curve_mapping(x, self.expo_coefficient, 0)
|
||||
|
||||
return self.min_value + self.value_range * x
|
||||
|
||||
def layout(self):
|
||||
return self.__layout
|
||||
|
||||
def value(self):
|
||||
return self.spinner.value()
|
||||
|
||||
def value_trimmed(self):
|
||||
value = self.value()
|
||||
if self.min_value is not None and value < self.min_value:
|
||||
return self.min_value
|
||||
if self.max_value is not None and value > self.max_value:
|
||||
return self.max_value
|
||||
return value
|
||||
|
||||
def set_value(self, value):
|
||||
if self.value == value:
|
||||
return
|
||||
self.spinner.setValue(value)
|
||||
self.update_slider()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self.spinner.setEnabled(enabled)
|
||||
self.slider.setEnabled(enabled)
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
def blockSignals(self, block):
|
||||
"""
|
||||
a mimic of qt's blockSignals for both inner widgets
|
||||
"""
|
||||
result = self.spinner.blockSignals(block)
|
||||
self.slider.blockSignals(block)
|
||||
self.valueChanged.enabled = not block
|
||||
return result
|
||||
|
||||
def set_expo(self, expo, coefficient=3):
|
||||
self.expo = expo
|
||||
self.expo_coefficient = coefficient
|
||||
self.update_slider()
|
||||
|
||||
|
||||
def set_paint_expo(number_group, paint_mode):
|
||||
"""
|
||||
Sets number slider group expo according to paint mode.
|
||||
|
||||
:type paint_mode: int
|
||||
:type number_group: NumberSliderGroup
|
||||
"""
|
||||
intensity_expo = {
|
||||
PaintMode.add: ("start", 3),
|
||||
PaintMode.scale: ("end", 8),
|
||||
PaintMode.smooth: ("start", 3),
|
||||
PaintMode.sharpen: ("start", 3),
|
||||
}
|
||||
expo, c = intensity_expo.get(paint_mode, (None, 1))
|
||||
if number_group.expo == expo and number_group.expo_coefficient == c:
|
||||
return
|
||||
|
||||
number_group.set_expo(expo=expo, coefficient=c)
|
||||
|
||||
|
||||
def button_row(button_defs, side_menu=None):
|
||||
result = QtWidgets.QHBoxLayout()
|
||||
|
||||
stretch_marker = "Marker"
|
||||
|
||||
for i in (side_menu or []) + [stretch_marker] + button_defs:
|
||||
if i == stretch_marker:
|
||||
result.addStretch()
|
||||
continue
|
||||
label, handler = i
|
||||
btn = QtWidgets.QPushButton(label, minimumWidth=100)
|
||||
qt.on(btn.clicked)(handler)
|
||||
result.addWidget(btn)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ColorButton(QtWidgets.QPushButton):
|
||||
def __init__(self):
|
||||
QtWidgets.QPushButton.__init__(self)
|
||||
self.color = None
|
||||
qt.on(self.clicked)(self.__pick_color)
|
||||
|
||||
self.color_changed = Signal("color changed")
|
||||
|
||||
def set_color(self, color):
|
||||
if isinstance(color, (list, tuple)):
|
||||
color = QtGui.QColor.fromRgb(color[0] * 255, color[1] * 255, color[2] * 255, 255)
|
||||
self.color = color
|
||||
self.setStyleSheet("background-color: %s;" % color.name())
|
||||
self.color_changed.emit()
|
||||
|
||||
def get_color_3f(self):
|
||||
return [i / 255.0 for i in self.color.getRgb()[:3]]
|
||||
|
||||
def __pick_color(self):
|
||||
color = QtWidgets.QColorDialog.getColor(initial=self.color, options=QtWidgets.QColorDialog.DontUseNativeDialog)
|
||||
if color.isValid():
|
||||
self.set_color(color)
|
||||