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

View File

@@ -0,0 +1,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)

View 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()

View 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)

View File

@@ -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)

View 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)

View 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()

View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View 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

View 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

View 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"]

View 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

View 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)

View 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)

View 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")

View 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

View File

@@ -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;
}
"""
)

View 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()

View 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()

View 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,
)

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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))

View 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)