Update
BIN
2023/icons/ngSkinTools2.ico
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
2023/icons/ngSkinTools2ShelfIcon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
51
2023/scripts/rigging_tools/ngskintools2/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# ngSkinTools2 模块
|
||||
|
||||
ngSkinTools2 是一个强大的 Maya 蒙皮权重编辑工具。
|
||||
|
||||
## 📁 文件夹结构
|
||||
|
||||
```
|
||||
ngskintools2/ # ngSkinTools2 核心模块
|
||||
├── __init__.py # 模块初始化(原始)
|
||||
├── launcher.py # 启动器脚本(新增)
|
||||
├── api/ # API 接口
|
||||
├── ui/ # 用户界面
|
||||
├── operations/ # 操作功能
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 从工具架启动
|
||||
|
||||
1. 打开 Maya
|
||||
2. 切换到 **Nexus_Rigging** 工具架
|
||||
3. 点击 **ngSkin** 按钮
|
||||
|
||||
### 从 Python 启动
|
||||
|
||||
```python
|
||||
from rigging_tools.ngskintools2 import launcher
|
||||
launcher.LaunchNgSkinTools()
|
||||
```
|
||||
|
||||
### 直接使用 ngSkinTools2 API
|
||||
|
||||
```python
|
||||
from rigging_tools.ngskintools2 import open_ui
|
||||
open_ui()
|
||||
```
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
- 高级权重绘制和编辑
|
||||
- 权重镜像和传递
|
||||
- 多层权重管理
|
||||
- 权重导入/导出
|
||||
- 影响对象管理
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
- 确保 Maya 版本兼容
|
||||
- 需要加载对应的插件
|
||||
- 建议在绑定工作流程中使用
|
||||
33
2023/scripts/rigging_tools/ngskintools2/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
|
||||
DEBUG_MODE = os.getenv("NGSKINTOOLS_DEBUG", 'false') == 'true'
|
||||
|
||||
try:
|
||||
from maya import cmds
|
||||
|
||||
BATCH_MODE = cmds.about(batch=True) == 1
|
||||
except:
|
||||
BATCH_MODE = True
|
||||
|
||||
|
||||
def open_ui():
|
||||
"""
|
||||
opens ngSkinTools2 main UI window. if the window is already open, brings that workspace
|
||||
window to front.
|
||||
"""
|
||||
|
||||
from ngSkinTools2.ui import mainwindow
|
||||
|
||||
mainwindow.open()
|
||||
|
||||
|
||||
def workspace_control_main_window():
|
||||
"""
|
||||
this function is used permanently by Maya's "workspace control", and acts as an alternative top-level entry point to open UI
|
||||
"""
|
||||
from ngSkinTools2.ui import mainwindow
|
||||
from ngSkinTools2.ui.paintContextCallbacks import definePaintContextCallbacks
|
||||
|
||||
definePaintContextCallbacks()
|
||||
|
||||
mainwindow.resume_in_workspace_control()
|
||||
40
2023/scripts/rigging_tools/ngskintools2/api/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from . import import_v1
|
||||
from .copy_paste_weights import PasteOperation, copy_weights, cut_weights, paste_weights
|
||||
from .import_export import FileFormat, export_json, import_json
|
||||
from .influenceMapping import InfluenceInfo, InfluenceMapping, InfluenceMappingConfig
|
||||
from .layers import (
|
||||
Layer,
|
||||
LayerEffects,
|
||||
Layers,
|
||||
NamedPaintTarget,
|
||||
get_layers_enabled,
|
||||
init_layers,
|
||||
)
|
||||
from .mirror import Mirror, MirrorOptions
|
||||
from .paint import (
|
||||
BrushProjectionMode,
|
||||
BrushShape,
|
||||
PaintMode,
|
||||
PaintModeSettings,
|
||||
PaintTool,
|
||||
TabletMode,
|
||||
WeightsDisplayMode,
|
||||
)
|
||||
from .suspend_updates import suspend_updates
|
||||
from .target_info import (
|
||||
add_influences,
|
||||
get_related_skin_cluster,
|
||||
is_slow_mode_skin_cluster,
|
||||
list_influences,
|
||||
)
|
||||
from .tools import (
|
||||
assign_from_closest_joint,
|
||||
copy_component_weights,
|
||||
duplicate_layer,
|
||||
fill_transparency,
|
||||
flood_weights,
|
||||
merge_layers,
|
||||
paste_average_component_weights,
|
||||
unify_weights,
|
||||
)
|
||||
from .transfer import VertexTransferMode, transfer_layers
|
||||
19
2023/scripts/rigging_tools/ngskintools2/api/cmd_wrappers.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from maya import cmds
|
||||
|
||||
|
||||
def as_comma_separated_list(l):
|
||||
return ",".join((str(i) for i in l))
|
||||
|
||||
|
||||
def get_source(plug, **kwargs):
|
||||
for i in cmds.listConnections(plug, source=True, **kwargs) or []:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def get_source_node(plug):
|
||||
return get_source(plug, plugs=False)
|
||||
|
||||
|
||||
def get_source_plug(plug):
|
||||
return get_source(plug, plugs=True)
|
||||
47
2023/scripts/rigging_tools/ngskintools2/api/config.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import json
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
log = getLogger("api/layers")
|
||||
|
||||
|
||||
def __define_property__(name, conversion, doc, refresh_on_write=True):
|
||||
return property(
|
||||
lambda self: conversion(self.__load__(name)), lambda self, val: self.__save__(name, conversion(val), refresh=refresh_on_write), doc=doc
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
class Config(Object):
|
||||
"""
|
||||
per-skin-cluster configuration
|
||||
"""
|
||||
|
||||
influence_colors = __define_property__(
|
||||
"influence_colors",
|
||||
lambda v: {int(k): tuple(float(v[i]) for i in range(3)) for k, v in v.items()},
|
||||
doc="Influence color map [logical index]->(r,g,b)",
|
||||
refresh_on_write=True,
|
||||
)
|
||||
|
||||
def __init__(self, data_node):
|
||||
self.data_node = data_node
|
||||
|
||||
def __load__(self, attr):
|
||||
try:
|
||||
return json.loads(cmds.getAttr(self.data_node + ".config_" + attr))
|
||||
except:
|
||||
return None
|
||||
|
||||
def __save__(self, attr, value, refresh=False):
|
||||
if not cmds.attributeQuery("config_" + attr, node=self.data_node, exists=True):
|
||||
cmds.addAttr(self.data_node, dt="string", longName="config_" + attr)
|
||||
cmds.setAttr(self.data_node + ".config_" + attr, json.dumps(value), type='string')
|
||||
|
||||
if refresh:
|
||||
from ngSkinTools2.api.tools import refresh_screen
|
||||
|
||||
refresh_screen(self.data_node)
|
||||
@@ -0,0 +1,37 @@
|
||||
from ngSkinTools2.api import plugin
|
||||
|
||||
|
||||
def __clipboard_operation__(layer, influences, operation):
|
||||
influences = "" if influences is None else ','.join([str(i) for i in influences])
|
||||
plugin.ngst2Layers(layer.mesh, e=True, id=layer.id, clipboard=operation, paintTarget=influences)
|
||||
|
||||
|
||||
def copy_weights(layer, influences):
|
||||
"""
|
||||
:type layer: ngSkinTools2.api.layers.Layer
|
||||
:type influences: list
|
||||
"""
|
||||
__clipboard_operation__(layer, influences, 'copy')
|
||||
|
||||
|
||||
def cut_weights(layer, influences):
|
||||
"""
|
||||
:type layer: ngSkinTools2.api.layers.Layer
|
||||
:type influences: list
|
||||
"""
|
||||
__clipboard_operation__(layer, influences, 'cut')
|
||||
|
||||
|
||||
class PasteOperation:
|
||||
replace = 'pasteReplace'
|
||||
add = 'pasteAdd'
|
||||
subtract = 'pasteSubtract'
|
||||
|
||||
|
||||
def paste_weights(layer, operation=PasteOperation.replace, influences=None):
|
||||
"""
|
||||
:type layer: ngSkinTools2.api.layers.Layer
|
||||
:param operation: one of paste_* constants
|
||||
:param influences: list of target influences
|
||||
"""
|
||||
__clipboard_operation__(layer, influences, operation)
|
||||
190
2023/scripts/rigging_tools/ngskintools2/api/events.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
|
||||
## Event handling brainstorm
|
||||
|
||||
# Usecase: when selection changes, handlers need to update to this.
|
||||
|
||||
Handlers are interested in same data (what's the selected mesh, are layers available, etc). When even is received
|
||||
by handler, all data to handle the even is there. Data is mostly pre-fetched (assuming that someone will eventually
|
||||
need it anyway), but for some events lazy-loading might be needed.
|
||||
|
||||
# Usecase: event handlers need to respond only when data actually changes (state goes from "layers available"
|
||||
to "layers unavailable")
|
||||
|
||||
Even handlers store that information on heir side. Signal has no way of knowing prevous state of the handler.
|
||||
|
||||
# Usecase: event can be fired as a source of multiple other events (layer availability changed: could come from
|
||||
data transformation or undo/redo event)
|
||||
|
||||
Events have their own hierarchy, "layers availability changed" signal stores information about it's previous state
|
||||
and emits if state changes.
|
||||
|
||||
|
||||
|
||||
|
||||
## Events hierarchy complexity
|
||||
|
||||
Whenever possible, keep event tree localized in single place for easier refactoring.
|
||||
"""
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2 import api, cleanup, signal
|
||||
from ngSkinTools2.api import target_info
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.signal import Signal
|
||||
|
||||
log = getLogger("events")
|
||||
|
||||
|
||||
class ConditionalEmit(Object):
|
||||
def __init__(self, name, check):
|
||||
self.signal = Signal(name)
|
||||
self.check = check
|
||||
|
||||
def emitIfChanged(self):
|
||||
if self.check():
|
||||
self.signal.emit()
|
||||
|
||||
def addHandler(self, handler, **kwargs):
|
||||
self.signal.addHandler(handler, **kwargs)
|
||||
|
||||
def removeHandler(self, handler):
|
||||
self.signal.removeHandler(handler)
|
||||
|
||||
|
||||
def script_job(*args, **kwargs):
|
||||
"""
|
||||
a proxy on top of cmds.scriptJob for scriptJob creation;
|
||||
will register an automatic cleanup procedure to kill the job
|
||||
"""
|
||||
job = cmds.scriptJob(*args, **kwargs)
|
||||
|
||||
def kill():
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
cmds.scriptJob(kill=job)
|
||||
except:
|
||||
# should be no issue if we cannot kill the job anymore (e.g., killing from the
|
||||
# import traceback; traceback.print_exc()
|
||||
pass
|
||||
|
||||
cleanup.registerCleanupHandler(kill)
|
||||
|
||||
return job
|
||||
|
||||
|
||||
class Events(Object):
|
||||
"""
|
||||
root tree of events signaling each other
|
||||
"""
|
||||
|
||||
def __init__(self, state):
|
||||
"""
|
||||
|
||||
:type state: ngSkinTools2.api.session.State
|
||||
"""
|
||||
|
||||
def script_job_signal(name):
|
||||
result = Signal(name + "_scriptJob")
|
||||
script_job(e=[name, result.emit])
|
||||
return result
|
||||
|
||||
self.mayaDeleteAll = script_job_signal('deleteAll')
|
||||
|
||||
self.nodeSelectionChanged = script_job_signal('SelectionChanged')
|
||||
|
||||
self.undoExecuted = script_job_signal('Undo')
|
||||
self.redoExecuted = script_job_signal('Redo')
|
||||
self.undoRedoExecuted = Signal('undoRedoExecuted')
|
||||
self.undoExecuted.addHandler(self.undoRedoExecuted.emit)
|
||||
self.redoExecuted.addHandler(self.undoRedoExecuted.emit)
|
||||
|
||||
self.toolChanged = script_job_signal('ToolChanged')
|
||||
self.quitApplication = script_job_signal('quitApplication')
|
||||
|
||||
def check_target_changed():
|
||||
"""
|
||||
verify that currently selected mesh is changed, and this means a change in LayersManager.
|
||||
"""
|
||||
selection = cmds.ls(selection=True, objectsOnly=True) or []
|
||||
selected_skin_cluster = None if not selection else target_info.get_related_skin_cluster(selection[-1])
|
||||
|
||||
if selected_skin_cluster is not None:
|
||||
layers_available = api.get_layers_enabled(selected_skin_cluster)
|
||||
else:
|
||||
layers_available = False
|
||||
|
||||
if state.selectedSkinCluster == selected_skin_cluster and state.layersAvailable == layers_available:
|
||||
return False
|
||||
|
||||
state.selection = selection
|
||||
state.set_skin_cluster(selected_skin_cluster)
|
||||
state.skin_cluster_dq_channel_used = (
|
||||
False if selected_skin_cluster is None else cmds.getAttr(selected_skin_cluster + ".skinningMethod") == 2
|
||||
)
|
||||
state.layersAvailable = layers_available
|
||||
state.all_layers = [] # reset when target has actually changed
|
||||
log.info("target changed, layers available: %s", state.layersAvailable)
|
||||
|
||||
return True
|
||||
|
||||
self.targetChanged = event = ConditionalEmit("targetChanged", check_target_changed)
|
||||
|
||||
for source in [self.mayaDeleteAll, self.undoRedoExecuted, self.nodeSelectionChanged]:
|
||||
source.addHandler(event.emitIfChanged)
|
||||
|
||||
def check_layers_list_changed():
|
||||
state.all_layers = [] if not state.layersAvailable else api.Layers(state.selectedSkinCluster).list()
|
||||
return True
|
||||
|
||||
self.layerListChanged = ConditionalEmit("layerListChanged", check_layers_list_changed)
|
||||
signal.on(self.targetChanged, self.undoRedoExecuted)(self.layerListChanged.emitIfChanged)
|
||||
|
||||
def check_current_layer_changed():
|
||||
# current layer changed if current mesh changed,
|
||||
# or id within the mesh changed
|
||||
current_layer = None
|
||||
if state.selectedSkinCluster is not None and state.layersAvailable:
|
||||
current_layer = api.Layers(state.selectedSkinCluster).current_layer()
|
||||
|
||||
if state.selectedSkinCluster == state.currentLayer.selectedSkinCluster and state.currentLayer.layer == current_layer:
|
||||
return False
|
||||
|
||||
state.currentLayer.selectedSkinCluster = state.selectedSkinCluster
|
||||
state.currentLayer.layer = current_layer
|
||||
return True
|
||||
|
||||
self.currentLayerChanged = event = ConditionalEmit("currentLayerChanged", check_current_layer_changed)
|
||||
self.targetChanged.addHandler(event.emitIfChanged)
|
||||
self.undoRedoExecuted.addHandler(event.emitIfChanged)
|
||||
|
||||
def check_current_paint_target_changed():
|
||||
skin_cluster = state.selectedSkinCluster
|
||||
new_layer = state.currentLayer.layer
|
||||
new_targets = None
|
||||
if new_layer is not None:
|
||||
new_targets = new_layer.paint_targets
|
||||
|
||||
log.info("[%s] checking current influence changed to %s %r", skin_cluster, new_layer, new_targets)
|
||||
if (
|
||||
skin_cluster == state.currentInfluence.skinCluster
|
||||
and new_layer == state.currentInfluence.layer
|
||||
and new_targets == state.currentInfluence.targets
|
||||
):
|
||||
return False
|
||||
|
||||
log.info("[%s] current influence changed to %s %r", skin_cluster, new_layer, new_targets)
|
||||
|
||||
state.currentInfluence.skinCluster = skin_cluster
|
||||
state.currentInfluence.layer = new_layer
|
||||
state.currentInfluence.targets = new_targets
|
||||
return True
|
||||
|
||||
self.currentInfluenceChanged = event = ConditionalEmit("currentInfluenceChanged", check_current_paint_target_changed)
|
||||
self.currentLayerChanged.addHandler(event.emitIfChanged)
|
||||
|
||||
self.influencesListUpdated = Signal("influencesListUpdated")
|
||||
|
||||
# now get initial state
|
||||
self.targetChanged.emitIfChanged()
|
||||
@@ -0,0 +1,3 @@
|
||||
from ngSkinTools2.signal import Event
|
||||
|
||||
tool_settings_changed = Event('tool_settings_changed')
|
||||
11
2023/scripts/rigging_tools/ngskintools2/api/feedback.py
Normal file
@@ -0,0 +1,11 @@
|
||||
def display_error(message):
|
||||
from ngSkinTools2 import BATCH_MODE
|
||||
|
||||
if BATCH_MODE:
|
||||
import sys
|
||||
|
||||
sys.stdout.write(message + "\n")
|
||||
else:
|
||||
from ngSkinTools2.ui import dialogs
|
||||
|
||||
dialogs.displayError(message)
|
||||
46
2023/scripts/rigging_tools/ngskintools2/api/http_client.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
import threading
|
||||
|
||||
import maya.utils
|
||||
|
||||
from .python_compatibility import PY2
|
||||
|
||||
# HTTP library might not be available in batch mode
|
||||
available = True
|
||||
|
||||
try:
|
||||
# different ways to import urlopen
|
||||
if PY2:
|
||||
from urllib import urlencode
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from urllib2 import HTTPError, Request, urlopen
|
||||
else:
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
_ = urlencode
|
||||
_ = urlopen
|
||||
_ = Request
|
||||
_ = HTTPError
|
||||
except:
|
||||
available = False
|
||||
|
||||
|
||||
def encode_url(base_url, args):
|
||||
return base_url + "?" + urlencode(args)
|
||||
|
||||
|
||||
def get_async(url, success_callback, failure_callback):
|
||||
def runnerFunc():
|
||||
defer_func = maya.utils.executeDeferred
|
||||
try:
|
||||
result = urlopen(url).read()
|
||||
defer_func(success_callback, json.loads(result))
|
||||
except Exception as err:
|
||||
defer_func(failure_callback, str(err))
|
||||
|
||||
t = threading.Thread(target=runnerFunc)
|
||||
t.start()
|
||||
return t
|
||||
110
2023/scripts/rigging_tools/ngskintools2/api/import_export.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from os import unlink
|
||||
|
||||
from ngSkinTools2.api import plugin
|
||||
|
||||
from . import transfer
|
||||
from .influenceMapping import InfluenceMappingConfig
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class FileFormat:
|
||||
JSON = "json"
|
||||
CompressedJSON = "compressed json"
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def import_json(
|
||||
target,
|
||||
file,
|
||||
vertex_transfer_mode=transfer.VertexTransferMode.closestPoint,
|
||||
influences_mapping_config=InfluenceMappingConfig.transfer_defaults(),
|
||||
format=FileFormat.JSON,
|
||||
):
|
||||
"""
|
||||
Transfer layers from file into provided target mesh. Existing layers, if any, will be preserved
|
||||
|
||||
:param str target: destination mesh or skin cluster node name
|
||||
:param str file: file path to load json from
|
||||
:param vertex_transfer_mode: vertex mapping mode when matching imported file's vertices to the target mesh
|
||||
:param InfluenceMappingConfig influences_mapping_config:
|
||||
:param str format: expected file format, one of `FileFormat` values
|
||||
"""
|
||||
|
||||
importer = transfer.LayersTransfer()
|
||||
importer.vertex_transfer_mode = vertex_transfer_mode
|
||||
importer.influences_mapping.config = influences_mapping_config
|
||||
importer.load_source_from_file(file, format=format)
|
||||
importer.target = target
|
||||
importer.execute()
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def export_json(target, file, format=FileFormat.JSON):
|
||||
"""
|
||||
Save skinning layers to file in json format, to be later used in `import_json`
|
||||
|
||||
:param str target: source mesh or skin cluster node name
|
||||
:param str file: file path to save json to
|
||||
:param str format: exported file format, one of `FileFormat` values
|
||||
"""
|
||||
|
||||
with FileFormatWrapper(file, format=format, read_mode=False) as f:
|
||||
plugin.ngst2tools(
|
||||
tool="exportJsonFile",
|
||||
target=target,
|
||||
file=f.plain_file,
|
||||
)
|
||||
|
||||
|
||||
def compress_gzip(source, dest):
|
||||
import gzip
|
||||
import shutil
|
||||
|
||||
with open(source, 'rb') as f_in, gzip.open(dest, 'wb') as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
|
||||
def decompress_gzip(source, dest):
|
||||
import gzip
|
||||
import shutil
|
||||
|
||||
with gzip.open(source, 'rb') as f_in, open(dest, 'wb') as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
|
||||
class FileFormatWrapper:
|
||||
def __init__(self, target_file, format, read_mode=False):
|
||||
self.target_file = target_file
|
||||
self.format = format
|
||||
self.plain_file = target_file
|
||||
if self.using_temp_file():
|
||||
self.plain_file = target_file + "_temp"
|
||||
self.read_mode = read_mode
|
||||
|
||||
def using_temp_file(self):
|
||||
return self.format != FileFormat.JSON
|
||||
|
||||
def __compress__(self):
|
||||
if self.format == FileFormat.CompressedJSON:
|
||||
compress_gzip(self.plain_file, self.target_file)
|
||||
|
||||
def __decompress__(self):
|
||||
if self.format == FileFormat.CompressedJSON:
|
||||
decompress_gzip(self.target_file, self.plain_file)
|
||||
|
||||
def __enter__(self):
|
||||
if not self.using_temp_file():
|
||||
return self
|
||||
if self.read_mode:
|
||||
self.__decompress__()
|
||||
return self
|
||||
|
||||
def __exit__(self, _, value, traceback):
|
||||
if not self.using_temp_file():
|
||||
return self
|
||||
|
||||
try:
|
||||
if not self.read_mode:
|
||||
self.__compress__()
|
||||
finally:
|
||||
unlink(self.plain_file)
|
||||
103
2023/scripts/rigging_tools/ngskintools2/api/import_v1.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# for same mesh, convert from v1 layers to v2
|
||||
from maya import cmds, mel
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.decorators import undoable
|
||||
|
||||
logger = getLogger("import v1")
|
||||
|
||||
__has_v1 = None
|
||||
|
||||
|
||||
def has_v1():
|
||||
global __has_v1
|
||||
if __has_v1 is not None:
|
||||
return __has_v1
|
||||
|
||||
__has_v1 = False
|
||||
try:
|
||||
cmds.loadPlugin('ngSkinTools')
|
||||
__has_v1 = True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def can_import(target):
|
||||
if not has_v1():
|
||||
return False
|
||||
try:
|
||||
result = cmds.ngSkinLayer(target, q=True, lda=True)
|
||||
return result == 1
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
return False
|
||||
|
||||
|
||||
def cleanup(selection):
|
||||
"""
|
||||
Delete V1 data from provided list of nodes. Must be a v1 compatible target
|
||||
|
||||
|
||||
:type selection: list[string]
|
||||
"""
|
||||
for s in selection:
|
||||
hist = cmds.listHistory(s) or []
|
||||
skinClusters = [i for i in hist if cmds.nodeType(i) in ('skinCluster')]
|
||||
cmds.delete(
|
||||
[
|
||||
i
|
||||
for skinCluster in skinClusters
|
||||
for i in cmds.listHistory(skinCluster, future=True, levels=1)
|
||||
if cmds.nodeType(i) in ('ngSkinLayerDisplay', 'ngSkinLayerData')
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@undoable
|
||||
def import_layers(target):
|
||||
if not has_v1():
|
||||
return False
|
||||
|
||||
import ngSkinTools2.api as ngst_api
|
||||
|
||||
layers = ngst_api.init_layers(target)
|
||||
|
||||
layerList = cmds.ngSkinLayer(target, q=True, listLayers=True)
|
||||
layerList = [layerList[i : i + 3] for i in range(0, len(layerList), 3)]
|
||||
|
||||
layerIdMap = {0: None}
|
||||
|
||||
def get_layer_influences(layerId):
|
||||
influences = mel.eval("ngSkinLayer -id {0} -q -listLayerInfluences -activeInfluences {1}".format(layerId, target)) or []
|
||||
return zip(influences[0::2], map(int, influences[1::2]))
|
||||
|
||||
def get_layer_weights(layerId, infl):
|
||||
return mel.eval("ngSkinLayer -id {0:d} -paintTarget {1} -q -w {2:s}".format(layerId, infl, target))
|
||||
|
||||
def copy_layer(oldLayerId, newLayer):
|
||||
"""
|
||||
|
||||
:type oldLayerId: int
|
||||
:type newLayer: ngSkinTools2.api.layers.Layer
|
||||
"""
|
||||
|
||||
newLayer.opacity = mel.eval("ngSkinLayer -id {0:d} -q -opacity {1:s}".format(oldLayerId, target))
|
||||
newLayer.enabled = mel.eval("ngSkinLayer -id {0:d} -q -enabled {1:s}".format(oldLayerId, target))
|
||||
|
||||
newLayer.set_weights(ngst_api.NamedPaintTarget.MASK, get_layer_weights(oldLayerId, "mask"))
|
||||
newLayer.set_weights(ngst_api.NamedPaintTarget.DUAL_QUATERNION, get_layer_weights(oldLayerId, "dq"))
|
||||
|
||||
for inflPath, inflId in get_layer_influences(oldLayerId):
|
||||
logger.info("importing influence %s for layer %s", inflPath, oldLayerId)
|
||||
weights = get_layer_weights(oldLayerId, inflId)
|
||||
newLayer.set_weights(inflId, weights)
|
||||
|
||||
with ngst_api.suspend_updates(target):
|
||||
for layerId, layerName, layerParent in layerList:
|
||||
layerId = int(layerId)
|
||||
layerParent = int(layerParent)
|
||||
newLayer = layers.add(name=layerName, force_empty=True, parent=layerIdMap[layerParent])
|
||||
newLayer.index = 0
|
||||
layerIdMap[layerId] = newLayer.id
|
||||
|
||||
copy_layer(layerId, newLayer)
|
||||
545
2023/scripts/rigging_tools/ngskintools2/api/influenceMapping.py
Normal file
@@ -0,0 +1,545 @@
|
||||
"""
|
||||
Influence mapping process creates a "source->destination" list for two influence lists,
|
||||
describing how based on influence metadata (name, position) and mapping requirements
|
||||
(mirror mode, left/right side name prefixes, etc) a mapping is created,
|
||||
where "source->destination" in, say, weight transfer/mirror situation, describes that weights
|
||||
currently associated with source influence, should be transfered to destination influence.
|
||||
|
||||
for mirror mode, same influence can map to itself, which would generally mean "copy influence weights
|
||||
on one side to be the same on the other side".
|
||||
|
||||
mapping is returned as "logical index"->"logical index" map.
|
||||
|
||||
For usage details, see unit test examples.
|
||||
"""
|
||||
from __future__ import division
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object, is_string
|
||||
|
||||
log = getLogger("influenceMapping")
|
||||
|
||||
|
||||
def regexpMatchAny(fragments):
|
||||
return '(' + '|'.join(fragments) + ')'
|
||||
|
||||
|
||||
illegalCharactersRegexp = re.compile(r"[^\w_\*]")
|
||||
|
||||
|
||||
def validate_glob(glob):
|
||||
match = illegalCharactersRegexp.search(glob)
|
||||
if match is not None:
|
||||
raise Exception("invalid pattern '{}': character {} not allowed".format(glob, match.group(0)))
|
||||
|
||||
|
||||
def convertGlobToRegexp(glob):
|
||||
"""
|
||||
:type glob: str
|
||||
"""
|
||||
glob = illegalCharactersRegexp.sub("", glob)
|
||||
if "*" not in glob: # if no stars added, just add them on both sides, e.g. "_L_" is the same as "*_L_*"
|
||||
glob = "*" + glob + "*"
|
||||
return "^" + glob.replace("*", "(.*)") + "$"
|
||||
|
||||
|
||||
class InfluenceInfo(Object):
|
||||
"""
|
||||
Metadata about an influence in a skin cluster
|
||||
"""
|
||||
|
||||
SIDE_LEFT = "left"
|
||||
SIDE_RIGHT = "right"
|
||||
SIDE_CENTER = "center"
|
||||
SIDE_MAP = {
|
||||
0: SIDE_CENTER,
|
||||
1: SIDE_LEFT,
|
||||
2: SIDE_RIGHT,
|
||||
}
|
||||
|
||||
oppositeSides = {SIDE_LEFT: SIDE_RIGHT, SIDE_RIGHT: SIDE_LEFT}
|
||||
|
||||
def __init__(self, pivot=None, path=None, name=None, logicalIndex=None, labelSide=None, labelText=None):
|
||||
self.pivot = pivot #: influence pivot in world-space coordinates
|
||||
self.path = path #: influence node path
|
||||
self.name = name #: influence node name (if influence is not a DAG node, like )
|
||||
self.logicalIndex = logicalIndex #: influence logical index in the skin cluster.
|
||||
self.labelSide = labelSide #: joint label "side" attribute
|
||||
self.labelText = labelText #: joint label text
|
||||
|
||||
def path_name(self):
|
||||
"""
|
||||
returns path if it's not None, otherwise returns name
|
||||
:return:
|
||||
"""
|
||||
if self.path is not None:
|
||||
return self.path
|
||||
if self.name is None:
|
||||
raise Exception("both path and name is empty for InfluenceInfo")
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return "[InflInfo %r %r %r]" % (self.logicalIndex, self.path, self.pivot)
|
||||
|
||||
def as_json(self):
|
||||
return {
|
||||
"pivot": self.pivot,
|
||||
"path": self.path,
|
||||
"name": self.name,
|
||||
"logicalIndex": self.logicalIndex,
|
||||
"labelSide": self.labelSide,
|
||||
"labelText": self.labelText,
|
||||
}
|
||||
|
||||
def from_json(self, json):
|
||||
self.pivot = json["pivot"]
|
||||
self.path = json.get("path", "")
|
||||
self.name = json.get("name", "")
|
||||
self.logicalIndex = json["logicalIndex"]
|
||||
self.labelSide = json["labelSide"]
|
||||
self.labelText = json["labelText"]
|
||||
return self
|
||||
|
||||
|
||||
def calcShortestUniqueName(influences):
|
||||
"""
|
||||
calculates "uniquePath" for each influence - in a similar manner as Maya does, only in the context of single
|
||||
skincluster instead of the whole scene.
|
||||
:type influences: list[InfluenceInfo]
|
||||
"""
|
||||
|
||||
def commonOffset(original, sibling):
|
||||
i = 0
|
||||
for o, s in zip(original, sibling):
|
||||
if o != s:
|
||||
break
|
||||
i += 1
|
||||
|
||||
# extend to full name
|
||||
while i < len(original) and original[i] != '|':
|
||||
i += 1
|
||||
|
||||
return i
|
||||
|
||||
for infl in influences:
|
||||
if infl.path is None:
|
||||
infl.shortestPath = infl.name
|
||||
|
||||
reversedPaths = [{"path": infl.path[::-1], "infl": infl} for infl in influences if infl.path]
|
||||
|
||||
# sort by line ending
|
||||
reversedPaths = sorted(reversedPaths, key=lambda item: item["path"])
|
||||
|
||||
# compare path to siblings, find a shortest subpath that is different from nearest similar names
|
||||
for prev, curr, next in zip([None] + reversedPaths[:-1], reversedPaths, reversedPaths[1:] + [None]):
|
||||
minLength = curr['path'].find("|")
|
||||
if minLength < 0:
|
||||
minLength = len(curr['path'])
|
||||
|
||||
prevOffset = minLength if prev is None else commonOffset(curr['path'], prev['path'])
|
||||
nextOffset = minLength if next is None else commonOffset(curr['path'], next['path'])
|
||||
|
||||
curr['infl'].shortestPath = curr['path'][: max(prevOffset, nextOffset)][::-1]
|
||||
|
||||
|
||||
def nameMatches(globs, influences, destination_influences=None, mirror_mode=False):
|
||||
"""
|
||||
for each name pair, calculates a match score, and keeps matches that have highest score.
|
||||
|
||||
score calculation rules:
|
||||
* each name is broken down into sections, e.g. |root|L_shoulder|L_elbow -> root, L_shoulder, L_elbow
|
||||
* for each section, find glob match, e.g. L_elbow becomes : {withoutGlob: elbow, matchedRule=L_*, oppositeRule=R_*}
|
||||
* two names are matched from the end, section by section:
|
||||
* it is assumed that a section matches if "withoutGlob" part is identical, and section1.matchedRule==section2.oppositeRule
|
||||
|
||||
|
||||
matching of the name happens by finding highest score
|
||||
|
||||
returns map of source->destination matches
|
||||
:type globs: list[(string, string)]
|
||||
:type influences: list[InfluenceInfo]
|
||||
"""
|
||||
|
||||
if destination_influences is None:
|
||||
destination_influences = influences
|
||||
|
||||
# 1 each path element is calculated as glob value
|
||||
|
||||
globRegexps = [[re.compile(convertGlobToRegexp(i)) for i in g] for g in globs]
|
||||
|
||||
# join with reversed logic
|
||||
globRegexps = globRegexps + [tuple(reversed(ge)) for ge in globRegexps]
|
||||
|
||||
class GlobInfo(Object):
|
||||
def __init__(self):
|
||||
self.withoutGlob = ""
|
||||
self.matchedRule = None
|
||||
self.oppositeRule = None
|
||||
|
||||
def convertPathElementToGlobInfo(pathElement):
|
||||
result = GlobInfo()
|
||||
result.withoutGlob = pathElement
|
||||
|
||||
for expr, opposite in globRegexps:
|
||||
match = expr.match(pathElement)
|
||||
if match is not None:
|
||||
result.matchedRule = expr
|
||||
result.oppositeRule = opposite
|
||||
result.withoutGlob = "".join(match.groups())
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def calcMatchScore(info1, info2):
|
||||
"""
|
||||
|
||||
:type info1: list[GlobInfo]
|
||||
:type info2: list[GlobInfo]
|
||||
"""
|
||||
|
||||
# optimization - if there's no chance these two paths match,
|
||||
# cut away loop logic
|
||||
if info1[0].withoutGlob != info2[0].withoutGlob:
|
||||
return 0
|
||||
|
||||
score = 0
|
||||
|
||||
rules_matched = False
|
||||
|
||||
for e1, e2 in zip(info1, info2):
|
||||
if e1.withoutGlob != e2.withoutGlob or e1.matchedRule != e2.oppositeRule:
|
||||
break
|
||||
|
||||
if e1.matchedRule is not None:
|
||||
score += 10
|
||||
rules_matched = True
|
||||
|
||||
score += 1
|
||||
|
||||
# in mirror mode, it's important that at least rule is matched (e.g. L->R or similar)
|
||||
if mirror_mode and not rules_matched:
|
||||
score = 0
|
||||
|
||||
return score
|
||||
|
||||
class MatchData(Object):
|
||||
path_split = re.compile("[\\|\\:]")
|
||||
|
||||
def __init__(self, infl):
|
||||
"""
|
||||
:type infl: InfluenceInfo
|
||||
"""
|
||||
reversedPath = list(reversed(self.path_split.split(infl.path))) if infl.path else [infl.name]
|
||||
self.infl = infl
|
||||
self.score = 0
|
||||
self.match = None
|
||||
self.globInfo = [convertPathElementToGlobInfo(e) for e in reversedPath]
|
||||
|
||||
destination_matches = [MatchData(infl) for infl in destination_influences]
|
||||
if destination_influences == influences:
|
||||
source_data = destination_matches
|
||||
else:
|
||||
source_data = [MatchData(infl) for infl in influences]
|
||||
|
||||
# encapsulating for profiler
|
||||
def findBestMatches():
|
||||
for source in source_data:
|
||||
for destination in destination_matches:
|
||||
if source == destination:
|
||||
continue
|
||||
|
||||
score = calcMatchScore(source.globInfo, destination.globInfo)
|
||||
|
||||
if (not mirror_mode or score > source.score) and score > destination.score:
|
||||
destination.match = source
|
||||
destination.score = score
|
||||
if mirror_mode:
|
||||
source.match = destination
|
||||
source.score = score
|
||||
|
||||
findBestMatches()
|
||||
|
||||
return {md.match.infl: md.infl for md in destination_matches if md.match is not None}
|
||||
|
||||
|
||||
def exactNameMatches(influences, destination_influences=None):
|
||||
"""
|
||||
match influences by exact name
|
||||
:type influences: list[InfluenceInfo]
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def labelMatches(source_influences, destination_influences, mirror_mode=False):
|
||||
"""
|
||||
:type source_influences: list[InfluenceInfo]
|
||||
"""
|
||||
|
||||
def infl_key(i):
|
||||
if i.labelText is None:
|
||||
return ("", "")
|
||||
|
||||
return (i.labelText, i.labelSide)
|
||||
|
||||
def group_by_label_and_side(infl_list):
|
||||
return {k: list(v) for k, v in itertools.groupby(list(sorted(infl_list, key=infl_key)), key=infl_key)}
|
||||
|
||||
def as_unique_entries(l):
|
||||
return {k: v[0] for k, v in l.items() if len(v) == 1}
|
||||
|
||||
result = {}
|
||||
|
||||
# group by labelText+labelSide keys
|
||||
# it is essential that only unique keys are used; skip repeats of text+side on either list, as they are ambiguous
|
||||
grouped_sources = group_by_label_and_side(source_influences)
|
||||
unique_sources = as_unique_entries(grouped_sources)
|
||||
unique_destinations = unique_sources if mirror_mode else as_unique_entries(group_by_label_and_side(destination_influences))
|
||||
|
||||
# "center" treatment in mirror mode: sometimes users might not set left/right sides, and "center" is actually just an untouched default;
|
||||
# in that case, just favour other influences if there are multiple "center" with the same label
|
||||
if mirror_mode:
|
||||
for (label, side), src in grouped_sources.items():
|
||||
if label == "" or side != InfluenceInfo.SIDE_CENTER:
|
||||
continue
|
||||
|
||||
# only the cases of len==1 and len==2 are supported
|
||||
if len(src) == 1:
|
||||
result[src[0]] = src[0]
|
||||
else:
|
||||
result[src[0]] = src[1]
|
||||
result[src[1]] = src[0]
|
||||
|
||||
# find matching label+side pairs for all destinations; flip side for mirror mode
|
||||
for (label, side), src in unique_sources.items():
|
||||
if label == "":
|
||||
continue
|
||||
if mirror_mode:
|
||||
if side == InfluenceInfo.SIDE_CENTER:
|
||||
continue
|
||||
side = InfluenceInfo.oppositeSides[side]
|
||||
|
||||
dest = unique_destinations.get((label, side), None)
|
||||
if dest is not None:
|
||||
result[src] = dest
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def distanceMatches(source_influences, destination_influences, threshold, mirror_axis):
|
||||
"""
|
||||
:type source_influences: list[InfluenceInfo]
|
||||
:type destination_influences: list[InfluenceInfo]
|
||||
:type threshold: float
|
||||
:type mirror_axis: union(int, None)
|
||||
"""
|
||||
|
||||
def distance_squared(p1, p2):
|
||||
return (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2 + (p1[2] - p2[2]) ** 2
|
||||
|
||||
threshold_squared = threshold * threshold
|
||||
|
||||
mirror_mode = mirror_axis is not None
|
||||
|
||||
result = {}
|
||||
for source in source_influences:
|
||||
# if we're in mirror mode and near mirror axis, match self instead of other influence
|
||||
if mirror_mode and abs(source.pivot[mirror_axis]) < (threshold / 2.0):
|
||||
result[source] = source
|
||||
continue
|
||||
|
||||
source_pivot = list(source.pivot[:])
|
||||
if mirror_mode:
|
||||
source_pivot[mirror_axis] = -source_pivot[mirror_axis]
|
||||
|
||||
best_distance = None
|
||||
for destination in destination_influences:
|
||||
d = distance_squared(source_pivot, destination.pivot)
|
||||
if threshold_squared < d:
|
||||
continue
|
||||
|
||||
if best_distance is None or d < best_distance:
|
||||
best_distance = d
|
||||
result[source] = destination
|
||||
if mirror_mode:
|
||||
result[destination] = source
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def dg_matches(source_influences, destination_influences, link_resolver):
|
||||
"""
|
||||
:type source_influences: list[InfluenceInfo]
|
||||
:type destination_influences: list[InfluenceInfo]
|
||||
:type link_resolver:
|
||||
"""
|
||||
|
||||
result = {}
|
||||
|
||||
dest_by_path = {i.path: i for i in destination_influences}
|
||||
for i in source_influences:
|
||||
dest = link_resolver(i.path)
|
||||
dest = None if dest is None else dest_by_path.get(dest, None)
|
||||
if dest is not None:
|
||||
result[i] = dest
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class InfluenceMappingConfig(Object):
|
||||
"""
|
||||
This class represents a configuration for how influences are matched for weights mirroring or transfering
|
||||
between meshes.
|
||||
"""
|
||||
|
||||
globs = [
|
||||
("L_*", "R_*"),
|
||||
("l_*", "r_*"),
|
||||
("lf_*", "rt_*"),
|
||||
("*_lf", "*_rt"),
|
||||
] #: For mirrored influences matching, this specifies the globs that will be used for name substitution
|
||||
|
||||
use_dg_link_matching = True #: turn on to use dependency graph links
|
||||
use_name_matching = True #: should matching by name be used?
|
||||
use_label_matching = True #: should matching by label be used?
|
||||
use_distance_matching = True #: should matching by influence X,Y,Z coordinates be used?
|
||||
distance_threshold = 0.001 #: When matching by distance, if distance between two positions is greater than this threshold, that pair of influences is not considered as potential match.
|
||||
|
||||
__mirror_axis = None
|
||||
dg_destination_attribute = "oppositeInfluence" #: default attribute name
|
||||
|
||||
@property
|
||||
def mirror_axis(self):
|
||||
"""
|
||||
int: Mirror axis (0 - X, 1 - Y, 2 - Z)
|
||||
|
||||
When mirror axis is not None, matching is done in "mirror" mode:
|
||||
|
||||
* left/right side .globs are used;
|
||||
* matching by position uses mirrorAxis to invert positions first;
|
||||
|
||||
"""
|
||||
return self.__mirror_axis
|
||||
|
||||
@mirror_axis.setter
|
||||
def mirror_axis(self, axis):
|
||||
if is_string(axis):
|
||||
self.__mirror_axis = ['x', 'y', 'z'].index(axis)
|
||||
return
|
||||
|
||||
if axis is not None and not isinstance(axis, int):
|
||||
raise Exception("invalid axis type, need int")
|
||||
|
||||
self.__mirror_axis = axis
|
||||
|
||||
@classmethod
|
||||
def transfer_defaults(cls):
|
||||
"""
|
||||
Builds a mapping configuration that is suitable as default for transferring between meshes (or importing)
|
||||
|
||||
Returns:
|
||||
InfluenceMappingConfig: default transfer configuration
|
||||
"""
|
||||
result = InfluenceMappingConfig()
|
||||
result.mirror_axis = None
|
||||
result.globs = []
|
||||
return result
|
||||
|
||||
def as_json(self):
|
||||
"""
|
||||
serializes config as JSON string
|
||||
"""
|
||||
return json.dumps(self.__dict__)
|
||||
|
||||
def load_json(self, json_string):
|
||||
"""
|
||||
loads configuration from previously saved `as_json` output
|
||||
"""
|
||||
try:
|
||||
self.__dict__ = json.loads(json_string)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def default_dg_resolver(dg_attribute):
|
||||
from maya import cmds
|
||||
|
||||
def resolver(input_path):
|
||||
try:
|
||||
sources = cmds.listConnections(input_path + "." + dg_attribute, source=True)
|
||||
if sources:
|
||||
return cmds.ls(sources[0], long=True)[0]
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
return resolver
|
||||
|
||||
|
||||
class InfluenceMapping(Object):
|
||||
"""
|
||||
this class serves as a hub to calculate influences mapping, given a mapping config and source/destination influences
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = InfluenceMappingConfig() # type:InfluenceMappingConfig
|
||||
"assigned config"
|
||||
|
||||
self.influences = [] # type: list[InfluenceInfo]
|
||||
"Source influences list. Can be assigned to result of :py:meth:`Layers.list_influences`"
|
||||
|
||||
self.destinationInfluences = None
|
||||
self.calculatedMapping = None
|
||||
self.dg_resolver = lambda: default_dg_resolver(self.config.dg_destination_attribute)
|
||||
|
||||
def calculate(self):
|
||||
mirror_mode = self.config.mirror_axis is not None
|
||||
log.info("calculate influence mapping, mirror mode: %s", mirror_mode)
|
||||
if self.destinationInfluences is None:
|
||||
self.destinationInfluences = self.influences
|
||||
|
||||
results = []
|
||||
|
||||
if mirror_mode:
|
||||
results.append(({infl: infl for infl in self.destinationInfluences}, "fallback to self"))
|
||||
|
||||
if self.config.use_distance_matching:
|
||||
matches = distanceMatches(
|
||||
self.influences, self.destinationInfluences, self.config.distance_threshold, mirror_axis=self.config.mirror_axis
|
||||
)
|
||||
results.append((matches, "distance"))
|
||||
|
||||
if self.config.use_name_matching:
|
||||
results.append((nameMatches(self.config.globs, self.influences, self.destinationInfluences, mirror_mode=mirror_mode), "name"))
|
||||
|
||||
if self.config.use_label_matching:
|
||||
results.append((labelMatches(self.influences, self.destinationInfluences, mirror_mode=mirror_mode), "label"))
|
||||
|
||||
if self.config.use_dg_link_matching:
|
||||
matches = dg_matches(self.influences, self.destinationInfluences, self.dg_resolver())
|
||||
results.append((matches, "DG link"))
|
||||
|
||||
result = {}
|
||||
for mapping, matchedRule in results:
|
||||
for k, v in mapping.items():
|
||||
result[k] = {
|
||||
"matchedRule": matchedRule,
|
||||
"infl": v,
|
||||
}
|
||||
|
||||
self.calculatedMapping = result
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def asIntIntMapping(mapping):
|
||||
"""
|
||||
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return {k.logicalIndex: v['infl'].logicalIndex for k, v in mapping.items()}
|
||||
117
2023/scripts/rigging_tools/ngskintools2/api/influence_names.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import re
|
||||
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.signal import Signal
|
||||
|
||||
|
||||
class InfluenceNameFilter(Object):
|
||||
"""
|
||||
simple helper object to match against filter strings;
|
||||
accepts filter as a string, breaks it down into lowercase tokens, and
|
||||
matches values in non-case sensitive way
|
||||
|
||||
e.g. filter "leg arm spines" matches "leg", "left_leg",
|
||||
"R_arm", but does not match "spine"
|
||||
|
||||
in a special case of empty filter, returns true for isMatch
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.matchers = []
|
||||
self.changed = Signal("filter changed")
|
||||
self.currentFilterString = ""
|
||||
|
||||
def set_filter_string(self, filter_string):
|
||||
if self.currentFilterString == filter_string:
|
||||
# avoid emitting change events if there's no change
|
||||
return
|
||||
self.currentFilterString = filter_string
|
||||
|
||||
def create_pattern(expression):
|
||||
expression = "".join([char for char in expression if char.lower() in "abcdefghijklmnopqrstuvwxyz0123456789_*"])
|
||||
expression = expression.replace("*", ".*")
|
||||
return re.compile(expression, re.I)
|
||||
|
||||
self.matchers = [create_pattern(i.strip()) for i in filter_string.split() if i.strip() != '']
|
||||
self.changed.emit()
|
||||
return self
|
||||
|
||||
def short_name(self, name):
|
||||
try:
|
||||
return name[name.rindex("|") + 1 :]
|
||||
except Exception as err:
|
||||
return name
|
||||
|
||||
def is_match(self, value):
|
||||
if len(self.matchers) == 0:
|
||||
return True
|
||||
|
||||
value = self.short_name(str(value).lower())
|
||||
for pattern in self.matchers:
|
||||
if pattern.search(value) is not None:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def short_name(name, min_len=0):
|
||||
"""
|
||||
|
||||
:param str name:
|
||||
:param int min_len:
|
||||
"""
|
||||
idx = name.rfind("|", None, len(name) - min_len - 1)
|
||||
if idx < 0:
|
||||
return name
|
||||
return name[idx + 1 :]
|
||||
|
||||
|
||||
class IndexedName(object):
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.name = short_name(path)
|
||||
|
||||
def extend_short_name(self):
|
||||
self.name = short_name(self.path, len(self.name))
|
||||
|
||||
|
||||
def extend_unique_names(items, from_item):
|
||||
"""
|
||||
|
||||
:param int from_item:
|
||||
:param list[IndexedName] items:
|
||||
"""
|
||||
|
||||
if from_item >= len(items) - 1:
|
||||
return
|
||||
|
||||
curr = items[from_item]
|
||||
needs_more_iterations = True
|
||||
while needs_more_iterations:
|
||||
needs_more_iterations = False
|
||||
curr_name = curr.name
|
||||
|
||||
for item in items[from_item + 1 :]:
|
||||
if item.name == curr_name:
|
||||
if not needs_more_iterations:
|
||||
curr.extend_short_name()
|
||||
item.extend_short_name()
|
||||
needs_more_iterations = True
|
||||
|
||||
|
||||
def unique_names(name_list):
|
||||
"""
|
||||
returns a list of shortened names without duplicates. e.g ["|a|b", "|a|b|c", "b|b"] will become ["a|b", "c", "b|b"]
|
||||
:param name_list:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# assign index to each name to later restore the original order
|
||||
indexed_names = [IndexedName(i) for i in name_list]
|
||||
|
||||
sorted_by_reveresed_name = sorted(indexed_names, key=lambda x: x.path[::-1])
|
||||
|
||||
for i, _ in enumerate(sorted_by_reveresed_name):
|
||||
extend_unique_names(sorted_by_reveresed_name, i)
|
||||
|
||||
return [i.name for i in indexed_names]
|
||||
17
2023/scripts/rigging_tools/ngskintools2/api/internals.py
Normal file
@@ -0,0 +1,17 @@
|
||||
def make_editable_property(propertyName):
|
||||
return property(lambda self: self.__query__(**{propertyName: True}), lambda self, val: self.__edit__(**{propertyName: val}))
|
||||
|
||||
|
||||
def influences_map_to_list(influencesMapping):
|
||||
return ','.join(str(k) + "," + str(v) for (k, v) in list(influencesMapping.items()))
|
||||
|
||||
|
||||
def float_list_as_string(floatList):
|
||||
"""
|
||||
returns empty string for None and []
|
||||
otherwise, returns a list of floats, comma delimited
|
||||
"""
|
||||
if not floatList:
|
||||
return ""
|
||||
|
||||
return ",".join([str(i) for i in floatList])
|
||||
416
2023/scripts/rigging_tools/ngskintools2/api/layers.py
Normal file
@@ -0,0 +1,416 @@
|
||||
import json
|
||||
|
||||
from maya import mel
|
||||
|
||||
from ngSkinTools2.api import internals, plugin, target_info
|
||||
from ngSkinTools2.api.config import Config
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object, is_string
|
||||
from ngSkinTools2.api.suspend_updates import suspend_updates
|
||||
from ngSkinTools2.decorators import undoable
|
||||
|
||||
logger = getLogger("api/layers")
|
||||
|
||||
|
||||
class NamedPaintTarget(Object):
|
||||
MASK = "mask"
|
||||
DUAL_QUATERNION = "dq"
|
||||
|
||||
|
||||
class LayerEffects(Object):
|
||||
def __init__(self, layer, state=None):
|
||||
self.__layer = layer # type: Layer
|
||||
if state is not None:
|
||||
self.__set_state__(state)
|
||||
|
||||
def __set_state__(self, state):
|
||||
from ngSkinTools2.api import MirrorOptions
|
||||
|
||||
self.mirror_mask = state.get("mirrorMask", False)
|
||||
self.mirror_weights = state.get("mirrorWeights", False)
|
||||
self.mirror_dq = state.get("mirrorDq", False)
|
||||
self.mirror_direction = state.get("mirrorDirection", MirrorOptions.directionPositiveToNegative)
|
||||
|
||||
def configure_mirror(self, everything=None, mirror_mask=None, mirror_weights=None, mirror_dq=None, mirror_direction=None):
|
||||
"""
|
||||
Enable/disable components for mirror effect:
|
||||
|
||||
>>> layer.effects.configure_mirror(mirror_mask=True)
|
||||
>>> layer.effects.configure_mirror(mirror_dq=False)
|
||||
>>> # equivalent of setting all flags to False
|
||||
>>> layer.effects.configure_mirror(everything=False)
|
||||
|
||||
Mirroring direction must be set explicitly.
|
||||
|
||||
>>> from ngSkinTools2.api import MirrorOptions
|
||||
>>> layer.effects.configure_mirror(mirror_mask=True,mirror_direction=MirrorOptions.directionPositiveToNegative)
|
||||
|
||||
|
||||
:arg bool mirror_mask: should mask be mirrored with this effect?
|
||||
:arg bool mirror_weights: should influence weights be mirrored with this effect?
|
||||
:arg bool mirror_dq: should dq weights be mirrored with this effect?
|
||||
:arg int mirror_direction: mirroring direction. Use `MirrorOptions.directionPositiveToNegative`, `MirrorOptions.directionNegativeToPositive`
|
||||
or `MirrorOptions.directionFlip`
|
||||
"""
|
||||
if everything is not None:
|
||||
mirror_mask = mirror_dq = mirror_weights = everything
|
||||
|
||||
logger.info(
|
||||
"configure mirror: layer %s mask %r weights %r dq %r direction %r",
|
||||
self.__layer.name,
|
||||
mirror_mask,
|
||||
mirror_weights,
|
||||
mirror_dq,
|
||||
mirror_direction,
|
||||
)
|
||||
|
||||
args = {'mirrorLayerDq': mirror_dq, 'mirrorLayerMask': mirror_mask, 'mirrorLayerWeights': mirror_weights, "mirrorDirection": mirror_direction}
|
||||
|
||||
self.__layer.__edit__(configureMirrorEffect=True, **{k: v for k, v in list(args.items()) if v is not None})
|
||||
|
||||
|
||||
def _build_layer_property(name, doc, edit_name=None, default_value=None):
|
||||
if edit_name is None:
|
||||
edit_name = name
|
||||
|
||||
def getter(self):
|
||||
return self.__get_state__(name, default_value=default_value)
|
||||
|
||||
def setter(self, val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
val = ",".join([str(i) for i in val])
|
||||
self.__edit__(**{edit_name: val})
|
||||
|
||||
return property(getter, setter, doc=doc)
|
||||
|
||||
|
||||
class Layer(Object):
|
||||
""" """
|
||||
|
||||
name = _build_layer_property('name', "str: Layer name") # type: str
|
||||
enabled = _build_layer_property('enabled', "bool: is layer enabled or disabled") # type: bool
|
||||
opacity = _build_layer_property('opacity', "float: value between 1.0 and 0") # type: float
|
||||
paint_target = _build_layer_property(
|
||||
'paintTarget', "str or int: currently active paint target for this layer (either an influence or one of named targets)"
|
||||
) # type: Union(str, int)
|
||||
index = _build_layer_property('index', edit_name='layerIndex', doc="int: layer index in parent's child list; set to reorder") # type: int
|
||||
locked_influences = _build_layer_property(
|
||||
'lockedInfluences',
|
||||
doc="list[int]: list of locked influence indexes",
|
||||
default_value=[],
|
||||
) # type: list[int]
|
||||
|
||||
@classmethod
|
||||
def load(cls, mesh, layer_id):
|
||||
if layer_id < 0:
|
||||
raise Exception("invalid layer ID: %s" % layer_id)
|
||||
result = Layer(mesh, layer_id)
|
||||
result.reload()
|
||||
return result
|
||||
|
||||
def __init__(self, mesh, id, state=None):
|
||||
self.mesh = mesh
|
||||
self.id = id
|
||||
self.effects = LayerEffects(self) # type: LayerEffects
|
||||
"configure effects for this layer"
|
||||
|
||||
self.__state = None
|
||||
if state is not None:
|
||||
self.__set_state(state)
|
||||
|
||||
def __get_state__(self, k, default_value=None):
|
||||
return self.__state.get(k, default_value)
|
||||
|
||||
def __query__(self, arg, **kwargs):
|
||||
keys = " ".join(["-{k} {v}".format(k=k, v=v) for k, v in list(kwargs.items())])
|
||||
return mel.eval("ngst2Layers -id {id} {keys} -q -{arg} {mesh}".format(id=self.id, mesh=self.mesh, keys=keys, arg=arg))
|
||||
|
||||
def __edit__(self, **kwargs):
|
||||
self.__set_state(plugin.ngst2Layers(self.mesh, e=True, id=as_layer_id(self), **kwargs))
|
||||
|
||||
def __set_state(self, state):
|
||||
if state is None:
|
||||
# some plugin functions still return empty result after edits - nevermind those
|
||||
return
|
||||
if is_string(state):
|
||||
try:
|
||||
state = json.loads(state)
|
||||
except Exception as err:
|
||||
raise Exception(str(err) + "; input body was: " + repr(state))
|
||||
|
||||
self.__state = state
|
||||
|
||||
# logger.info("setting layer state %r: %r", self.id, state)
|
||||
|
||||
self.parent_id = state['parentId']
|
||||
self.__parent = None
|
||||
self.children_ids = state['children']
|
||||
self.__children = []
|
||||
|
||||
self.effects.__set_state__(state['effects'])
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
Refresh layer data from plugin.
|
||||
"""
|
||||
self.__set_state(self.__query__('layerAttributesJson'))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Layer):
|
||||
return False
|
||||
|
||||
return self.mesh == other.mesh and self.id == other.id and self.__state == other.__state
|
||||
|
||||
def __repr__(self):
|
||||
return "[Layer #{id} '#{name}']".format(id=self.id, name=self.name)
|
||||
|
||||
@property
|
||||
def paint_targets(self):
|
||||
"""
|
||||
list[str or int]: list of paint targets to be set as current for this layer
|
||||
"""
|
||||
return self.__get_state__("paintTargets")
|
||||
|
||||
@paint_targets.setter
|
||||
def paint_targets(self, targets):
|
||||
self.__edit__(**{"paintTarget": ",".join([str(target) for target in targets])})
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
"""
|
||||
Layer: layer parent, or None, if layer is at root level.
|
||||
"""
|
||||
if self.__parent is None:
|
||||
if self.parent_id is not None:
|
||||
self.__parent = Layer.load(self.mesh, self.parent_id)
|
||||
|
||||
return self.__parent
|
||||
|
||||
@parent.setter
|
||||
def parent(self, parent):
|
||||
if parent is None:
|
||||
parent = 0
|
||||
|
||||
self.__edit__(parent=as_layer_id(parent))
|
||||
|
||||
@property
|
||||
def num_children(self):
|
||||
"""
|
||||
int: a bit more lightweight method to count number of child layers than len(children()), as it does not
|
||||
prefetch children data.
|
||||
"""
|
||||
return len(self.children_ids)
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
"""
|
||||
list[Layer]: lazily load children if needed, and return as Layer objects
|
||||
"""
|
||||
if len(self.children_ids) != 0:
|
||||
if len(self.__children) == 0:
|
||||
self.__children = [Layer.load(self.mesh, i) for i in self.children_ids]
|
||||
|
||||
return self.__children
|
||||
|
||||
def set_current(self):
|
||||
"""
|
||||
|
||||
Set as "default" layer for other operations.
|
||||
|
||||
|
||||
.. warning::
|
||||
Scheduled for removal. API calls should specify target layer explicitly
|
||||
|
||||
|
||||
|
||||
"""
|
||||
plugin.ngst2Layers(self.mesh, currentLayer=self.id)
|
||||
|
||||
def set_weights(self, influence, weights_list, undo_enabled=True):
|
||||
"""
|
||||
Modify weights in the layer.
|
||||
|
||||
:arg int/str influence: either index of an influence, or named paint target (one of :py:class:`NamedPaintTarget` values)
|
||||
:arg list[int] weights_list: weights for each vertex (must match number of vertices in skin cluster)
|
||||
:arg bool undo_enabled: set to False if you don't need undo, for slight performance boost
|
||||
"""
|
||||
self.__edit__(paintTarget=influence, vertexWeights=internals.float_list_as_string(weights_list), undoEnabled=undo_enabled)
|
||||
|
||||
def get_weights(self, influence):
|
||||
"""
|
||||
get influence (or named paint target) weights for all vertices
|
||||
"""
|
||||
result = self.__query__('vertexWeights', paintTarget=influence)
|
||||
if result is None:
|
||||
return []
|
||||
return [float(i) for i in result]
|
||||
|
||||
def get_used_influences(self):
|
||||
"""
|
||||
|
||||
:rtype: list[int]
|
||||
"""
|
||||
result = self.__query__('usedInfluences')
|
||||
return result or []
|
||||
|
||||
|
||||
def as_layer_id(layer):
|
||||
"""
|
||||
converts given input to layer ID. If input is a Layer object, returns it's ID, otherwise assumes that input is already a layer ID
|
||||
|
||||
Returns:
|
||||
int: layer ID
|
||||
"""
|
||||
if isinstance(layer, Layer):
|
||||
return layer.id
|
||||
|
||||
return int(layer)
|
||||
|
||||
|
||||
def as_layer_id_list(layers):
|
||||
"""
|
||||
maps a given layer list with `as_layer_id`
|
||||
|
||||
Args:
|
||||
layers (list[Any]): objects representing a list of layers
|
||||
|
||||
:rtype: list[int]
|
||||
"""
|
||||
return (as_layer_id(i) for i in layers)
|
||||
|
||||
|
||||
def generate_layer_name(existing_layers, base_name):
|
||||
"""
|
||||
A little utility to generate a unique layer name. For example, if base_name="test", it will try to use values in sequence "test", "test (1)",
|
||||
"test (2)" and will return first value that is not a name for any layer in the given layers list.
|
||||
|
||||
:arg existing_layers Layer: whatever
|
||||
"""
|
||||
name = base_name
|
||||
currentLayerNames = [i.name for i in existing_layers]
|
||||
index = 1
|
||||
while name in currentLayerNames:
|
||||
index += 1
|
||||
name = base_name + " ({0})".format(index)
|
||||
|
||||
return name
|
||||
|
||||
|
||||
class Layers(Object):
|
||||
"""
|
||||
Layers manages skinning layers on provided target (skinCluster or a mesh)
|
||||
"""
|
||||
|
||||
prune_weights_filter_threshold = internals.make_editable_property('pruneWeightsFilterThreshold')
|
||||
influence_limit_per_vertex = internals.make_editable_property('influenceLimitPerVertex')
|
||||
|
||||
def __init__(self, target):
|
||||
"""
|
||||
|
||||
:param str target: name of skin cluster node or skinned mesh.
|
||||
"""
|
||||
if not target:
|
||||
raise Exception("target must be specified")
|
||||
|
||||
self.__target = target
|
||||
self.__cached_data_node = None
|
||||
|
||||
def add(self, name, force_empty=False, parent=None):
|
||||
"""
|
||||
creates new layer with given name and returns its ID; when force_empty flag is set to true,
|
||||
layer weights will not be populated from skin cluster.
|
||||
"""
|
||||
layer_id = plugin.ngst2Layers(self.mesh, name=name, add=True, forceEmpty=force_empty)
|
||||
result = Layer.load(self.mesh, layer_id)
|
||||
result.parent = parent
|
||||
return result
|
||||
|
||||
def delete(self, layer):
|
||||
plugin.ngst2Layers(self.mesh, removeLayer=True, id=as_layer_id(layer))
|
||||
|
||||
def list(self):
|
||||
"""
|
||||
|
||||
returns all layers as Layer objects.
|
||||
:rtype list[Layer]
|
||||
"""
|
||||
data = json.loads(plugin.ngst2Layers(self.mesh, q=True, listLayers=True))
|
||||
return [Layer(self.mesh, id=l['id'], state=l) for l in data]
|
||||
|
||||
@undoable
|
||||
def clear(self):
|
||||
"""
|
||||
delete all layers
|
||||
"""
|
||||
with suspend_updates(self.data_node):
|
||||
for i in self.list():
|
||||
if i.parent_id is None:
|
||||
self.delete(i)
|
||||
|
||||
def list_influences(self):
|
||||
"""
|
||||
Wraps :py:meth:`target_info.list_influences`
|
||||
"""
|
||||
return target_info.list_influences(self.mesh)
|
||||
|
||||
def current_layer(self):
|
||||
"""
|
||||
get current layer that was previously marked as current with :py:meth:`Layer.set_current`.
|
||||
|
||||
.. warning::
|
||||
Scheduled for removal. API calls should specify target layer explicitly
|
||||
|
||||
"""
|
||||
layer_id = plugin.ngst2Layers(self.mesh, q=True, currentLayer=True)
|
||||
if layer_id < 0:
|
||||
return None
|
||||
return Layer.load(self.mesh, layer_id)
|
||||
|
||||
def __edit__(self, **kwargs):
|
||||
plugin.ngst2Layers(self.mesh, e=True, **kwargs)
|
||||
|
||||
def __query__(self, **kwargs):
|
||||
return plugin.ngst2Layers(self.mesh, q=True, **kwargs)
|
||||
|
||||
def set_influences_mirror_mapping(self, influencesMapping):
|
||||
plugin.ngst2Layers(self.mesh, configureMirrorMapping=True, influencesMapping=internals.influences_map_to_list(influencesMapping))
|
||||
|
||||
@property
|
||||
def mesh(self):
|
||||
return self.__target
|
||||
|
||||
def is_enabled(self):
|
||||
"""
|
||||
returns true if skinning layers are enabled for the given mesh
|
||||
:return:
|
||||
"""
|
||||
return get_layers_enabled(self.mesh)
|
||||
|
||||
@property
|
||||
def data_node(self):
|
||||
if not self.__cached_data_node:
|
||||
self.__cached_data_node = target_info.get_related_data_node(self.mesh)
|
||||
return self.__cached_data_node
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return Config(self.data_node)
|
||||
|
||||
|
||||
def init_layers(target):
|
||||
"""Attach ngSkinTools data node to given target. Does nothing if layers are already attached.
|
||||
|
||||
|
||||
:arg str target: skin cluster or mesh node to attach layers to
|
||||
:rtype: Layers
|
||||
"""
|
||||
plugin.ngst2Layers(target, layerDataAttach=True)
|
||||
|
||||
return Layers(target)
|
||||
|
||||
|
||||
def get_layers_enabled(selection):
|
||||
"""
|
||||
return true if layers are enabled on this selection
|
||||
"""
|
||||
return plugin.ngst2Layers(selection, q=True, lda=True)
|
||||
76
2023/scripts/rigging_tools/ngskintools2/api/log.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
class DummyLogger(Object):
|
||||
def __getattr__(self, name):
|
||||
return self.doNothing
|
||||
|
||||
def doNothing(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def isEnabledFor(self, *args):
|
||||
return False
|
||||
|
||||
def getLogger(self, name):
|
||||
return self
|
||||
|
||||
|
||||
class LogLineCountFilter(logging.Filter):
|
||||
def __init__(self):
|
||||
self.count = 1
|
||||
|
||||
def filter(self, record):
|
||||
record.count = self.count
|
||||
self.count = (self.count + 1) % 1000
|
||||
return True
|
||||
|
||||
|
||||
class SimpleLoggerFactory(Object):
|
||||
ROOT_LOGGER_NAME = "ngSkinTools2"
|
||||
|
||||
def __init__(self, level=logging.DEBUG):
|
||||
self.level = level
|
||||
self.log = self.configureRootLogger()
|
||||
|
||||
def configureRootLogger(self):
|
||||
logger = logging.getLogger(self.ROOT_LOGGER_NAME)
|
||||
logger.setLevel(self.level)
|
||||
logger.addFilter(LogLineCountFilter())
|
||||
# logger.handlers = []
|
||||
|
||||
formatter = logging.Formatter("%(count)3d: [UI %(levelname)s %(filename)s:%(lineno)d] %(message)s")
|
||||
formatter.datefmt = '%H:%M:%S'
|
||||
|
||||
for i in logger.handlers[:]:
|
||||
logger.removeHandler(i)
|
||||
|
||||
ch = logging.StreamHandler(sys.__stdout__)
|
||||
ch.setLevel(self.level)
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
logger.propagate = False
|
||||
|
||||
return logger
|
||||
|
||||
def getLogger(self, name):
|
||||
return self.log
|
||||
|
||||
self.log.debug("creating logger '%s'" % name)
|
||||
|
||||
result = logging.getLogger(self.ROOT_LOGGER_NAME + "." + name)
|
||||
result.setLevel(self.level)
|
||||
|
||||
result.info("alive check")
|
||||
return result
|
||||
|
||||
|
||||
import ngSkinTools2
|
||||
|
||||
currentLoggerFactory = DummyLogger() if not ngSkinTools2.DEBUG_MODE else SimpleLoggerFactory(level=logging.DEBUG)
|
||||
|
||||
|
||||
def getLogger(name):
|
||||
return currentLoggerFactory.getLogger(name)
|
||||
184
2023/scripts/rigging_tools/ngskintools2/api/mirror.py
Normal file
@@ -0,0 +1,184 @@
|
||||
import itertools
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api import influenceMapping, internals, plugin, target_info
|
||||
from ngSkinTools2.api.cmd_wrappers import get_source_node
|
||||
from ngSkinTools2.api.layers import Layers
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
log = getLogger("mirror")
|
||||
|
||||
|
||||
class Mirror(Object):
|
||||
"""
|
||||
query and configure mirror options for provided target
|
||||
"""
|
||||
|
||||
axis = internals.make_editable_property('mirrorAxis')
|
||||
seam_width = internals.make_editable_property('mirrorWidth')
|
||||
vertex_transfer_mode = internals.make_editable_property('vertexTransferMode')
|
||||
|
||||
def __init__(self, target):
|
||||
"""
|
||||
:type target: skin target (skinCluster or mesh)
|
||||
"""
|
||||
self.target = target
|
||||
self.__skin_cluster__ = None
|
||||
self.__data_node__ = None
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def __query__(self, **kwargs):
|
||||
return plugin.ngst2Layers(self.target, q=True, **kwargs)
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def __edit__(self, **kwargs):
|
||||
plugin.ngst2Layers(self.target, configureMirrorMapping=True, **kwargs)
|
||||
self.recalculate_influences_mapping()
|
||||
|
||||
def __mapper_config_attr(self):
|
||||
return self.__get_data_node__() + ".influenceMappingOptions"
|
||||
|
||||
def build_influences_mapper(self, defaults=None):
|
||||
mapper = influenceMapping.InfluenceMapping()
|
||||
layers = Layers(self.target)
|
||||
mapper.influences = layers.list_influences()
|
||||
|
||||
mapper.config.load_json(cmds.getAttr(self.__mapper_config_attr()))
|
||||
mapper.config.mirror_axis = self.axis
|
||||
|
||||
return mapper
|
||||
|
||||
def save_influences_mapper(self, mapper):
|
||||
"""
|
||||
:type mapper: influenceMapping.InfluenceMapping
|
||||
"""
|
||||
self.set_mirror_config(mapper.config.as_json())
|
||||
|
||||
def set_mirror_config(self, config_as_json):
|
||||
cmds.setAttr(self.__mapper_config_attr(), config_as_json, type='string')
|
||||
|
||||
def set_influences_mapping(self, mapping):
|
||||
"""
|
||||
:type mapping: map[int] -> int
|
||||
"""
|
||||
log.info("mapping updated: %r", mapping)
|
||||
|
||||
mapping_as_string = ','.join(str(k) + "," + str(v) for (k, v) in list(mapping.items()))
|
||||
plugin.ngst2Layers(self.target, configureMirrorMapping=True, influencesMapping=mapping_as_string)
|
||||
|
||||
def recalculate_influences_mapping(self):
|
||||
"""
|
||||
loads current influence mapping settings, and update influences mapping with these values
|
||||
"""
|
||||
m = self.build_influences_mapper().calculate()
|
||||
self.set_influences_mapping(influenceMapping.InfluenceMapping.asIntIntMapping(m))
|
||||
|
||||
def mirror(self, options):
|
||||
"""
|
||||
:type options: MirrorOptions
|
||||
"""
|
||||
plugin.ngst2Layers(
|
||||
self.target,
|
||||
mirrorLayerWeights=options.mirrorWeights,
|
||||
mirrorLayerMask=options.mirrorMask,
|
||||
mirrorLayerDq=options.mirrorDq,
|
||||
mirrorDirection=options.direction,
|
||||
)
|
||||
|
||||
def set_reference_mesh(self, mesh_shape):
|
||||
dest = self.__get_data_node__() + ".mirrorMesh"
|
||||
if mesh_shape:
|
||||
cmds.connectAttr(mesh_shape + ".outMesh", dest)
|
||||
else:
|
||||
for i in cmds.listConnections(dest, source=True, plugs=True):
|
||||
cmds.disconnectAttr(i, dest)
|
||||
|
||||
def get_reference_mesh(self):
|
||||
return get_source_node(self.__get_data_node__() + '.mirrorMesh')
|
||||
|
||||
def __get_skin_cluster__(self):
|
||||
if self.__skin_cluster__ is None:
|
||||
self.__skin_cluster__ = target_info.get_related_skin_cluster(self.target)
|
||||
return self.__skin_cluster__
|
||||
|
||||
def __get_data_node__(self):
|
||||
if self.__data_node__ is None:
|
||||
self.__data_node__ = target_info.get_related_data_node(self.target)
|
||||
return self.__data_node__
|
||||
|
||||
# noinspection PyStatementEffect
|
||||
def build_reference_mesh(self):
|
||||
sc = self.__get_skin_cluster__()
|
||||
dn = self.__get_data_node__()
|
||||
|
||||
if sc is None or dn is None:
|
||||
return
|
||||
|
||||
existing_ref_mesh = self.get_reference_mesh()
|
||||
if existing_ref_mesh:
|
||||
cmds.select(existing_ref_mesh)
|
||||
raise Exception("symmetry mesh already configured for %s: %s" % (str(sc), existing_ref_mesh))
|
||||
|
||||
def get_shape(node):
|
||||
return cmds.listRelatives(node, shapes=True)[0]
|
||||
|
||||
result, _ = cmds.polyCube()
|
||||
g = cmds.group(empty=True, name="ngskintools_mirror_mesh_setup")
|
||||
cmds.parent(result, g)
|
||||
result = cmds.rename(g + "|" + result, "mirror_reference_mesh")
|
||||
|
||||
cmds.delete(result, ch=True)
|
||||
cmds.connectAttr(sc + ".input[0].inputGeometry", get_shape(result) + ".inMesh")
|
||||
cmds.delete(result, ch=True)
|
||||
|
||||
(mirrored,) = cmds.duplicate(result)
|
||||
mirrored = cmds.rename(g + "|" + mirrored, 'flipped_preview')
|
||||
mirrored_shape = get_shape(mirrored)
|
||||
|
||||
cmds.setAttr(mirrored + ".sx", -1)
|
||||
cmds.setAttr(mirrored + ".overrideEnabled", 1)
|
||||
cmds.setAttr(mirrored + ".overrideDisplayType", 2)
|
||||
cmds.setAttr(mirrored + ".overrideShading", 0)
|
||||
cmds.setAttr(mirrored + ".overrideTexturing", 1)
|
||||
|
||||
(blend,) = cmds.blendShape(result, mirrored_shape)
|
||||
cmds.setAttr(blend + ".weight[0]", 1.0)
|
||||
|
||||
# lock accidental transformations
|
||||
for c, t, m in itertools.product('xyz', 'trs', (result, mirrored)):
|
||||
cmds.setAttr(m + "." + t + c, lock=True)
|
||||
|
||||
# shift setup to the right by slightly more than bounding box width
|
||||
|
||||
bb = cmds.exactWorldBoundingBox(g)
|
||||
cmds.move((bb[3] - bb[0]) * 1.2, 0, 0, g, r=True)
|
||||
|
||||
self.set_reference_mesh(str(result))
|
||||
cmds.select(result)
|
||||
return result
|
||||
|
||||
|
||||
class MirrorOptions(Object):
|
||||
directionNegativeToPositive = 0
|
||||
directionPositiveToNegative = 1
|
||||
directionGuess = 2
|
||||
directionFlip = 3
|
||||
|
||||
def __init__(self):
|
||||
self.mirrorWeights = True
|
||||
self.mirrorMask = True
|
||||
self.mirrorDq = True
|
||||
self.direction = MirrorOptions.directionPositiveToNegative
|
||||
|
||||
|
||||
def set_reference_mesh_from_selection():
|
||||
selection = cmds.ls(sl=True, long=True)
|
||||
|
||||
if len(selection) != 2:
|
||||
log.debug("wrong selection size")
|
||||
return
|
||||
|
||||
m = Mirror(selection[1])
|
||||
m.set_reference_mesh(selection[0])
|
||||
466
2023/scripts/rigging_tools/ngskintools2/api/paint.py
Normal file
@@ -0,0 +1,466 @@
|
||||
import copy
|
||||
import json
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api import internals, plugin
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
log = getLogger("api/paint")
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class BrushProjectionMode(Object):
|
||||
surface = 0
|
||||
screen = 1
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class PaintMode(Object):
|
||||
"""
|
||||
Constants for paint mode
|
||||
"""
|
||||
|
||||
replace = 1
|
||||
add = 2
|
||||
scale = 3
|
||||
smooth = 4
|
||||
sharpen = 5
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
return cls.replace, cls.smooth, cls.add, cls.scale, cls.sharpen
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class TabletMode(Object):
|
||||
unused = 0
|
||||
multiplyIntensity = 1
|
||||
multiplyOpacity = 2
|
||||
multiplyRadius = 3
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class WeightsDisplayMode(Object):
|
||||
allInfluences = 0
|
||||
currentInfluence = 1
|
||||
currentInfluenceColored = 2
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class MaskDisplayMode(Object):
|
||||
default_ = 0
|
||||
color_ramp = 1
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class BrushShape(Object):
|
||||
solid = 0 # 1.0 for whole brush size
|
||||
smooth = 1 # feathered edges
|
||||
gaus = 2 # very smooth from center
|
||||
|
||||
|
||||
# noinspection PyClassHasNoInit
|
||||
class PaintModeSettings(Object):
|
||||
"""
|
||||
Brush/Flood settings
|
||||
"""
|
||||
|
||||
__property_map = {
|
||||
"brush_projection_mode": 'brushProjectionMode',
|
||||
"mode": 'paintMode',
|
||||
"brush_radius": 'brushRadius',
|
||||
"brush_shape": 'brushShape',
|
||||
"intensity": 'brushIntensity',
|
||||
"iterations": 'brushIterations',
|
||||
"tablet_mode": 'tabletMode',
|
||||
"mirror": 'interactiveMirror',
|
||||
"influences_limit": 'influencesLimit',
|
||||
"fixed_influences_per_vertex": 'fixedInfluencesPerVertex',
|
||||
"limit_to_component_selection": 'limitToComponentSelection',
|
||||
"use_volume_neighbours": 'useVolumeNeighbours',
|
||||
"distribute_to_other_influences": 'redistributeRemovedWeight',
|
||||
"sample_joint_on_stroke_start": 'sampleJointOnStrokeStart',
|
||||
}
|
||||
|
||||
mode = PaintMode.replace #: Tool mode. One of the :py:class:`PaintMode` values.
|
||||
|
||||
# varies by mode
|
||||
intensity = 1.0 #: tool intensity;
|
||||
iterations = 1
|
||||
"""
|
||||
iterations; repeats the same smooth operation given number of times -
|
||||
using this parameter instead of calling `flood_weights` multiple times.
|
||||
"""
|
||||
|
||||
brush_shape = BrushShape.solid
|
||||
|
||||
# varies by screen mode
|
||||
# can be modified inside the plugin
|
||||
brush_radius = 10
|
||||
|
||||
mirror = False #: is automatic mirroring on or off
|
||||
distribute_to_other_influences = False
|
||||
influences_limit = 0 #: influences limit per vertex to ensure while smoothing
|
||||
brush_projection_mode = BrushProjectionMode.surface #: brush projection mode, one of :py:class:`BrushProjectionMode` values.
|
||||
sample_joint_on_stroke_start = False
|
||||
tablet_mode = TabletMode.unused
|
||||
use_volume_neighbours = False
|
||||
limit_to_component_selection = False
|
||||
fixed_influences_per_vertex = False
|
||||
"""
|
||||
only applicable for smooth mode; when set to True, smoothing will not add additional influences to a vertex.
|
||||
"""
|
||||
|
||||
def apply_primary_brush(self):
|
||||
self.__apply(1)
|
||||
|
||||
def apply_alternative_brush(self):
|
||||
self.__apply(2)
|
||||
|
||||
def apply_inverted_brush(self):
|
||||
self.__apply(3)
|
||||
|
||||
def __apply(self, settings_type):
|
||||
"""
|
||||
apply settings to C++ plugin side.
|
||||
:param settings_type:
|
||||
"""
|
||||
|
||||
kwargs = {v: getattr(self, k) for k, v in self.__property_map.items()}
|
||||
kwargs['paintType'] = settings_type
|
||||
plugin.ngst2PaintSettingsCmd(**kwargs)
|
||||
|
||||
def from_dict(self, values):
|
||||
# type: (dict) -> PaintModeSettings
|
||||
|
||||
for k in self.__property_map.keys():
|
||||
if k in values:
|
||||
setattr(self, k, values[k])
|
||||
|
||||
return self
|
||||
|
||||
def to_dict(self):
|
||||
# type: () -> dict
|
||||
return {k: getattr(self, k) for k in self.__property_map.keys()}
|
||||
|
||||
|
||||
def __make_common_property__(property_name):
|
||||
def setval(self, val):
|
||||
setattr(self.primary_settings, property_name, val)
|
||||
self.apply_settings()
|
||||
|
||||
return property(lambda self: getattr(self.primary_settings, property_name), setval)
|
||||
|
||||
|
||||
def __make_mode_property__(property_name):
|
||||
return __make_dimensional_property__(lambda self: self.mode, lambda self: self.mode_settings[self.mode], property_name)
|
||||
|
||||
|
||||
def __make_projection_property__(property_name):
|
||||
return __make_dimensional_property__(
|
||||
lambda self: self.brush_projection_mode, lambda self: self.projection_settings[self.brush_projection_mode], property_name
|
||||
)
|
||||
|
||||
|
||||
def __make_dimensional_property__(name, get_dimension_func, property_name):
|
||||
def setval(self, val):
|
||||
log.debug("setting dimensional property %s/%s: %r", name(self), property_name, val)
|
||||
dimension = get_dimension_func(self)
|
||||
dimension[property_name] = val
|
||||
setattr(self.primary_settings, property_name, val)
|
||||
self.apply_settings()
|
||||
|
||||
def getval(self):
|
||||
return get_dimension_func(self).get(property_name, getattr(self.primary_settings, property_name))
|
||||
|
||||
return property(getval, setval)
|
||||
|
||||
|
||||
class PaintSettingsModel(Object):
|
||||
"""
|
||||
Paint settings model manages paint settings persistence and storage on CPP side;
|
||||
in CPP side three states need to be maintained:
|
||||
* primary settings
|
||||
* alternative settings (used when shift is pressed)
|
||||
* inverse settings (used when ctrl is pressed)
|
||||
"""
|
||||
|
||||
projection_settings = None # type: dict
|
||||
mode_settings = None # type: dict
|
||||
primary_settings = None # type: PaintModeSettings
|
||||
|
||||
paint_mode = __make_common_property__("mode")
|
||||
mode = __make_common_property__("mode")
|
||||
|
||||
intensity = __make_mode_property__("intensity")
|
||||
iterations = __make_mode_property__("iterations")
|
||||
brush_shape = __make_mode_property__("brush_shape")
|
||||
|
||||
brush_radius = __make_projection_property__("brush_radius")
|
||||
|
||||
mirror = __make_common_property__("mirror")
|
||||
distribute_to_other_influences = __make_common_property__("distribute_to_other_influences")
|
||||
influences_limit = __make_common_property__("influences_limit")
|
||||
brush_projection_mode = __make_common_property__("brush_projection_mode")
|
||||
sample_joint_on_stroke_start = __make_common_property__("sample_joint_on_stroke_start")
|
||||
tablet_mode = __make_common_property__("tablet_mode")
|
||||
use_volume_neighbours = __make_common_property__("use_volume_neighbours")
|
||||
limit_to_component_selection = __make_common_property__("limit_to_component_selection")
|
||||
fixed_influences_per_vertex = __make_common_property__("fixed_influences_per_vertex")
|
||||
|
||||
def __init__(self):
|
||||
self.projection_settings = None
|
||||
self.mode_settings = None
|
||||
self.primary_settings = None
|
||||
self.storage_func_save = lambda data: None
|
||||
self.storage_func_load = lambda: ""
|
||||
self.apply_settings_func = self.apply_plugin_settings
|
||||
|
||||
self.setup_maya_option_var_persistence()
|
||||
|
||||
def __save_settings(self):
|
||||
data = {
|
||||
"common": self.primary_settings.to_dict(),
|
||||
"mode_settings": self.mode_settings,
|
||||
"projection_settings": self.projection_settings,
|
||||
}
|
||||
serialized_data = json.dumps(data)
|
||||
log.info("saving brush settings: %s", serialized_data)
|
||||
self.storage_func_save(serialized_data)
|
||||
|
||||
def load_settings(self):
|
||||
def to_int_keys(d):
|
||||
return {int(k): v for k, v in d.items()}
|
||||
|
||||
try:
|
||||
saved_data = self.storage_func_load()
|
||||
log.info("loading brush settings from %s", saved_data)
|
||||
if saved_data is None:
|
||||
self.initialize_defaults()
|
||||
return
|
||||
data = json.loads(saved_data)
|
||||
self.primary_settings = PaintModeSettings().from_dict(data['common'])
|
||||
self.mode_settings = to_int_keys(data['mode_settings'])
|
||||
self.projection_settings = to_int_keys(data['projection_settings'])
|
||||
self.apply_settings()
|
||||
except Exception as err:
|
||||
log.info(err)
|
||||
|
||||
def setup_maya_option_var_persistence(self):
|
||||
from ngSkinTools2.ui import options
|
||||
|
||||
val = options.PersistentValue(options.VAR_OPTION_PREFIX + "_brush_settings")
|
||||
|
||||
self.storage_func_load = val.get
|
||||
self.storage_func_save = val.set
|
||||
self.load_settings()
|
||||
|
||||
def __bake_settings(self, mode):
|
||||
result = copy.copy(self.primary_settings)
|
||||
result.mode = mode
|
||||
|
||||
for k, v in self.mode_settings[mode].items():
|
||||
setattr(result, k, v)
|
||||
|
||||
for k, v in self.projection_settings[self.brush_projection_mode].items():
|
||||
setattr(result, k, v)
|
||||
|
||||
return result
|
||||
|
||||
def apply_settings(self):
|
||||
# TODO: very inelegant here; we should not have to re-bake and re-set all settings at once
|
||||
# if we're switching any of dimensional settings, we need to reflect this
|
||||
self.primary_settings = self.__bake_settings(self.mode)
|
||||
primary = self.primary_settings
|
||||
|
||||
inverted_modes = {
|
||||
PaintMode.replace: PaintMode.replace,
|
||||
PaintMode.add: PaintMode.scale,
|
||||
PaintMode.scale: PaintMode.add,
|
||||
PaintMode.smooth: PaintMode.sharpen,
|
||||
PaintMode.sharpen: PaintMode.smooth,
|
||||
}
|
||||
|
||||
alternative = self.__bake_settings(PaintMode.smooth)
|
||||
inverted = self.__bake_settings(inverted_modes.get(self.mode, PaintMode.replace))
|
||||
|
||||
if self.mode == PaintMode.replace:
|
||||
inverted.intensity = 0
|
||||
|
||||
log.debug("normal mode intensity: %r", primary.intensity)
|
||||
self.apply_settings_func(primary, alternative, inverted)
|
||||
self.__save_settings()
|
||||
|
||||
def initialize_defaults(self):
|
||||
self.primary_settings = PaintModeSettings()
|
||||
self.primary_settings.mode = PaintMode.replace
|
||||
self.primary_settings.brush_radius = 2
|
||||
self.primary_settings.intensity = 1.0
|
||||
self.primary_settings.tablet_mode = TabletMode.unused
|
||||
self.primary_settings.brush_shape = BrushShape.solid
|
||||
self.primary_settings.distribute_to_other_influences = False
|
||||
self.mode_settings = {
|
||||
PaintMode.replace: {
|
||||
"intensity": 1.0,
|
||||
"brush_shape": BrushShape.solid,
|
||||
},
|
||||
PaintMode.add: {
|
||||
"intensity": 0.1,
|
||||
"brush_shape": BrushShape.solid,
|
||||
},
|
||||
PaintMode.scale: {
|
||||
"intensity": 0.95,
|
||||
"brush_shape": BrushShape.solid,
|
||||
},
|
||||
PaintMode.smooth: {
|
||||
"intensity": 0.2,
|
||||
"iterations": 5,
|
||||
"brush_shape": BrushShape.smooth,
|
||||
},
|
||||
PaintMode.sharpen: {
|
||||
"intensity": 0.2,
|
||||
"iterations": 5,
|
||||
"brush_shape": BrushShape.smooth,
|
||||
},
|
||||
}
|
||||
self.projection_settings = {
|
||||
BrushProjectionMode.surface: {
|
||||
"brush_radius": 2,
|
||||
},
|
||||
BrushProjectionMode.screen: {
|
||||
"brush_radius": 100,
|
||||
},
|
||||
}
|
||||
self.apply_settings()
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def apply_plugin_settings(self, primary, alternative, inverted):
|
||||
# type: (PaintModeSettings, PaintModeSettings, PaintModeSettings) -> None
|
||||
primary.apply_primary_brush()
|
||||
alternative.apply_alternative_brush()
|
||||
inverted.apply_inverted_brush()
|
||||
|
||||
|
||||
class DisplaySettings(Object):
|
||||
def __init__(self):
|
||||
from ngSkinTools2.ui import options
|
||||
|
||||
self.persistence = options.PersistentDict("paint_display_settings")
|
||||
|
||||
weights_display_mode = internals.make_editable_property('weightsDisplayMode')
|
||||
mask_display_mode = internals.make_editable_property('maskDisplayMode')
|
||||
layer_effects_display = internals.make_editable_property('layerEffectsDisplay')
|
||||
display_masked = internals.make_editable_property('displayMasked')
|
||||
show_selected_verts_only = internals.make_editable_property('showSelectedVertsOnly')
|
||||
wireframe_color = internals.make_editable_property('wireframeColor')
|
||||
wireframe_color_single_influence = internals.make_editable_property('wireframeColorSingleInfluence')
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def __edit__(self, **kwargs):
|
||||
plugin.ngst2PaintSettingsCmd(**kwargs)
|
||||
for k, v in kwargs.items():
|
||||
self.persistence[k] = v
|
||||
|
||||
@property
|
||||
def display_node_visible(self):
|
||||
"""
|
||||
gets/sets visibility of temporary node that displays weight colors. when set to false, displays original mesh instead.
|
||||
"""
|
||||
return plugin.ngst2PaintSettingsCmd(q=True, displayNodeVisible=True)
|
||||
|
||||
@display_node_visible.setter
|
||||
def display_node_visible(self, value):
|
||||
plugin.ngst2PaintSettingsCmd(displayNodeVisible=value)
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def __query__(self, **kwargs):
|
||||
for k in kwargs:
|
||||
persisted = self.persistence[k]
|
||||
if persisted is not None:
|
||||
return persisted
|
||||
return plugin.ngst2PaintSettingsCmd(q=True, **kwargs)
|
||||
|
||||
|
||||
class PaintTool(PaintSettingsModel):
|
||||
__paint_context = None
|
||||
|
||||
def __init__(self):
|
||||
PaintSettingsModel.__init__(self)
|
||||
self.display_settings = DisplaySettings()
|
||||
|
||||
def update_plugin_brush_radius(self):
|
||||
new_value = plugin.ngst2PaintSettingsCmd(q=True, brushRadius=True)
|
||||
if self.brush_radius != new_value:
|
||||
self.brush_radius = new_value
|
||||
|
||||
def update_plugin_brush_intensity(self):
|
||||
new_value = plugin.ngst2PaintSettingsCmd(q=True, brushIntensity=True)
|
||||
if self.intensity != new_value:
|
||||
self.intensity = new_value
|
||||
|
||||
@classmethod
|
||||
def start(cls):
|
||||
if cls.__paint_context is None:
|
||||
cls.__paint_context = plugin.ngst2PaintContext()
|
||||
cmds.setToolTo(cls.__paint_context)
|
||||
|
||||
def flood(self, layer, influence=None, influences=None):
|
||||
from ngSkinTools2.api import tools
|
||||
|
||||
tools.flood_weights(target=layer, influence=influence, influences=influences, settings=self.primary_settings)
|
||||
|
||||
@classmethod
|
||||
def is_painting(cls):
|
||||
return cmds.contextInfo(cmds.currentCtx(), c=True) == 'ngst2PaintContext'
|
||||
|
||||
|
||||
class Popups(Object):
|
||||
def __init__(self):
|
||||
self.windows = []
|
||||
|
||||
def add(self, w):
|
||||
self.windows.append(w)
|
||||
w.destroyed.connect(lambda *args: self.remove(w))
|
||||
|
||||
def remove(self, w):
|
||||
self.windows = [i for i in self.windows if i != w]
|
||||
|
||||
def close_all(self):
|
||||
for i in self.windows:
|
||||
i.close()
|
||||
self.windows = []
|
||||
|
||||
|
||||
popups = Popups()
|
||||
|
||||
|
||||
class TabletEventFilter(QtCore.QObject):
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.pressure = 1.0
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() in [QtCore.QEvent.TabletPress, QtCore.QEvent.TabletMove]:
|
||||
self.pressure = event.pressure()
|
||||
# log.info("tablet pressure: %r", self.pressure)
|
||||
|
||||
return QtCore.QObject.eventFilter(self, obj, event)
|
||||
|
||||
def install(self):
|
||||
from ngSkinTools2.ui import qt
|
||||
|
||||
log.info("installing event filter...")
|
||||
qt.mainWindow.installEventFilter(self)
|
||||
log.info("...done")
|
||||
|
||||
def uninstall(self):
|
||||
from ngSkinTools2.ui import qt
|
||||
|
||||
qt.mainWindow.removeEventFilter(self)
|
||||
log.info("event filter uninstalled")
|
||||
|
||||
|
||||
tabletEventFilter = TabletEventFilter()
|
||||
70
2023/scripts/rigging_tools/ngskintools2/api/plugin.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import json
|
||||
|
||||
from maya import cmds, mel
|
||||
|
||||
from ngSkinTools2.api import feedback
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
|
||||
log = getLogger("plugin")
|
||||
|
||||
|
||||
def ngst2Layers(*args, **kwargs):
|
||||
log.debug("ngst2layers [%r] [%r]", args, kwargs)
|
||||
return cmds.ngst2Layers(*args, **kwargs)
|
||||
|
||||
|
||||
def ngst2LayersMel(cmd):
|
||||
cmd = "ngst2Layers " + cmd
|
||||
log.debug(cmd)
|
||||
return mel.eval(cmd)
|
||||
|
||||
|
||||
def ngst2tools(**kwargs):
|
||||
log.debug("ngst2tools [%r]", kwargs)
|
||||
result = cmds.ngst2Tools(json.dumps(kwargs))
|
||||
if result is not None:
|
||||
result = json.loads(result)
|
||||
return result
|
||||
|
||||
|
||||
def ngst2PaintContext():
|
||||
log.debug("ngst2PaintContext()")
|
||||
return cmds.ngst2PaintContext()
|
||||
|
||||
|
||||
def ngst2PaintSettingsCmd(**kwargs):
|
||||
log.debug("ngst2PaintSettingsCmd [%r]", kwargs)
|
||||
return cmds.ngst2PaintSettingsCmd(**kwargs)
|
||||
|
||||
|
||||
def ngst2_hotkey(**kwargs):
|
||||
log.debug("ngst2Hotkey [%r]", kwargs)
|
||||
return cmds.ngst2Hotkey(**kwargs)
|
||||
|
||||
|
||||
pluginBinary = 'ngSkinTools2'
|
||||
|
||||
|
||||
def is_plugin_loaded():
|
||||
return cmds.pluginInfo(pluginBinary, q=True, loaded=True)
|
||||
|
||||
|
||||
def load_plugin():
|
||||
from maya import cmds
|
||||
|
||||
if not is_plugin_loaded():
|
||||
cmds.loadPlugin(pluginBinary, quiet=True)
|
||||
|
||||
if not is_plugin_loaded():
|
||||
feedback.display_error("Failed to load the plugin. This is often a case-by-case issue - contact support.")
|
||||
return
|
||||
|
||||
from ngSkinTools2 import version
|
||||
|
||||
expected_version = version.pluginVersion()
|
||||
actual_version = cmds.pluginInfo(pluginBinary, q=True, version=True)
|
||||
if actual_version != expected_version:
|
||||
feedback.display_error(
|
||||
"Invalid plugin version detected: required '{expectedVersion}', "
|
||||
"but was '{actualVersion}'. Clean reinstall recommended.".format(expectedVersion=expected_version, actualVersion=actual_version)
|
||||
)
|
||||
29
2023/scripts/rigging_tools/ngskintools2/api/pyside.py
Normal file
@@ -0,0 +1,29 @@
|
||||
pyside6 = False
|
||||
try:
|
||||
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtSvgWidgets import QSvgWidget
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
pyside6 = True
|
||||
except:
|
||||
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtSvg import QSvgWidget
|
||||
from PySide2.QtWidgets import QAction, QActionGroup, QWidget
|
||||
|
||||
|
||||
def get_main_window():
|
||||
from maya import OpenMayaUI as omui
|
||||
|
||||
return wrap_instance(omui.MQtUtil.mainWindow(), QWidget)
|
||||
|
||||
|
||||
def wrap_instance(ptr, widget):
|
||||
if pyside6:
|
||||
from shiboken6 import wrapInstance
|
||||
else:
|
||||
from shiboken2 import wrapInstance
|
||||
|
||||
return wrapInstance(int(ptr), widget)
|
||||
@@ -0,0 +1,24 @@
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY3 = not PY2
|
||||
|
||||
|
||||
def is_string(obj):
|
||||
if PY2:
|
||||
# noinspection PyUnresolvedReferences
|
||||
return isinstance(obj, basestring)
|
||||
|
||||
return isinstance(obj, str)
|
||||
|
||||
|
||||
# need to use a new-style class in case of python2, or "normal" class otherwise
|
||||
if PY2:
|
||||
|
||||
class Object(object):
|
||||
pass
|
||||
|
||||
else:
|
||||
|
||||
class Object:
|
||||
pass
|
||||
162
2023/scripts/rigging_tools/ngskintools2/api/session.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
UI session is running as long as any of the ngSkinTools UI windows are open.
|
||||
|
||||
"""
|
||||
import functools
|
||||
|
||||
from ngSkinTools2 import cleanup, signal
|
||||
from ngSkinTools2.api import Layers, PaintTool, events, mirror, plugin
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.observableValue import ObservableValue
|
||||
from ngSkinTools2.signal import SignalHub
|
||||
|
||||
log = getLogger("events")
|
||||
|
||||
|
||||
class CurrentLayerState(Object):
|
||||
def __init__(self):
|
||||
self.selectedSkinCluster = None
|
||||
|
||||
# will be None, when no current layer is available
|
||||
self.layer = None # type: ngSkinTools2.api.Layer
|
||||
|
||||
|
||||
class CurrentPaintTargetState(Object):
|
||||
def __init__(self):
|
||||
self.skinCluster = None
|
||||
self.layerId = None
|
||||
self.targets = None
|
||||
|
||||
|
||||
class State(Object):
|
||||
def __init__(self):
|
||||
self.layersAvailable = False
|
||||
self.selection = []
|
||||
self.selectedSkinCluster = None
|
||||
self.skin_cluster_dq_channel_used = False
|
||||
|
||||
self.all_layers = [] # type: List[ngSkinTools2.api.Layer]
|
||||
self.currentLayer = CurrentLayerState()
|
||||
self.currentInfluence = CurrentPaintTargetState()
|
||||
|
||||
def set_skin_cluster(self, cluster):
|
||||
self.selectedSkinCluster = cluster
|
||||
self.layers = None if cluster is None else Layers(cluster)
|
||||
|
||||
def mirror(self):
|
||||
# type: () -> mirror.Mirror
|
||||
return mirror.Mirror(self.selectedSkinCluster)
|
||||
|
||||
|
||||
class Context(Object):
|
||||
def __init__(self):
|
||||
self.selected_layers = ObservableValue() # [] layerId
|
||||
|
||||
|
||||
class Session(Object):
|
||||
def __init__(self):
|
||||
# reference objects that are keeping the session
|
||||
self.references = set()
|
||||
self.state = None # type: State
|
||||
self.events = None # type: events.Events
|
||||
self.signal_hub = None # type: SignalHub
|
||||
self.context = None # type: Context
|
||||
|
||||
self.referenceId = 0
|
||||
|
||||
def active(self):
|
||||
return len(self.references) > 0
|
||||
|
||||
def start(self):
|
||||
log.info("STARTING SESSION")
|
||||
plugin.load_plugin()
|
||||
|
||||
self.paint_tool = PaintTool()
|
||||
|
||||
self.state = State()
|
||||
self.events = events.Events(self.state)
|
||||
self.signal_hub = SignalHub()
|
||||
self.signal_hub.activate()
|
||||
cleanup.registerCleanupHandler(self.signal_hub.deactivate)
|
||||
self.context = Context()
|
||||
|
||||
@signal.on(self.events.targetChanged)
|
||||
def on_target_change():
|
||||
log.info("target changed: clearing target context")
|
||||
self.context.selected_layers.set([])
|
||||
|
||||
self.events.nodeSelectionChanged.emit()
|
||||
|
||||
def end(self):
|
||||
log.info("ENDING SESSION")
|
||||
cleanup.cleanup()
|
||||
self.state = None
|
||||
self.events = None
|
||||
self.context = None
|
||||
self.signal_hub = None
|
||||
pass
|
||||
|
||||
def addReference(self):
|
||||
"""
|
||||
returns unique ID for this added reference; this value needs to be passed into removeReference();
|
||||
this ensures that reference holder does not remove other references rather than his own.
|
||||
:return:
|
||||
"""
|
||||
self.referenceId += 1
|
||||
if not self.active():
|
||||
self.start()
|
||||
|
||||
self.references.add(self.referenceId)
|
||||
return self.referenceId
|
||||
|
||||
def removeReference(self, referenceId):
|
||||
try:
|
||||
self.references.remove(referenceId)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not self.active():
|
||||
self.end()
|
||||
|
||||
def addQtWidgetReference(self, widget):
|
||||
ref = self.addReference()
|
||||
|
||||
widget.destroyed.connect(lambda: self.removeReference(ref))
|
||||
|
||||
def reference(self):
|
||||
class Context(Object):
|
||||
def __init__(self, session):
|
||||
"""
|
||||
:type session: Session
|
||||
:type refObj: object
|
||||
"""
|
||||
|
||||
self.session = session
|
||||
|
||||
def __enter__(self):
|
||||
self.ref = self.session.addReference()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.session.removeReference(self.ref)
|
||||
|
||||
return Context(self)
|
||||
|
||||
|
||||
session = Session()
|
||||
|
||||
|
||||
def withSession(func):
|
||||
"""
|
||||
decorator makes sure that single session is running throughout function's lifetime
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def result(*args, **kwargs):
|
||||
ref = session.addReference()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
session.removeReference(ref)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,17 @@
|
||||
from ngSkinTools2.api import plugin
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
def suspend_updates(target):
|
||||
return SuspendUpdatesContext(target)
|
||||
|
||||
|
||||
class SuspendUpdatesContext(Object):
|
||||
def __init__(self, target):
|
||||
self.target = target
|
||||
|
||||
def __enter__(self):
|
||||
plugin.ngst2Layers(self.target, suspendUpdates=True)
|
||||
|
||||
def __exit__(self, _type, value, traceback):
|
||||
plugin.ngst2Layers(self.target, suspendUpdates=False)
|
||||
86
2023/scripts/rigging_tools/ngskintools2/api/target_info.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api import plugin
|
||||
|
||||
from .influenceMapping import InfluenceInfo
|
||||
|
||||
|
||||
def get_related_skin_cluster(target):
|
||||
"""
|
||||
Returns skinCluster if provided node name represents skinned mesh. Returns true for shapes that have
|
||||
skinCLuster in their deformation stack, or it's a skinCluster node itself.
|
||||
|
||||
For invalid targets, returns None
|
||||
"""
|
||||
|
||||
return plugin.ngst2Layers(target, q=True, layerDataAttachTarget=True)
|
||||
|
||||
|
||||
def get_related_data_node(target):
|
||||
"""
|
||||
:returns: ngSkinTools data node name for this target
|
||||
"""
|
||||
return plugin.ngst2Layers(target, q=True, layerDataNode=True)
|
||||
|
||||
|
||||
def unserialize_influences_from_json_data(info):
|
||||
def as_influence_info(data):
|
||||
influence = InfluenceInfo()
|
||||
influence.pivot = data['pivot']
|
||||
influence.path = data.get('path', None)
|
||||
influence.name = data.get('name', None)
|
||||
influence.labelText = data['labelText']
|
||||
influence.labelSide = InfluenceInfo.SIDE_MAP[data['labelSide']]
|
||||
influence.logicalIndex = data['index']
|
||||
|
||||
return influence
|
||||
|
||||
if not info:
|
||||
return []
|
||||
|
||||
return [as_influence_info(i) for i in info]
|
||||
|
||||
|
||||
def list_influences(target):
|
||||
"""
|
||||
List influences in the given skin cluster as InfluenceInfo objects
|
||||
|
||||
:param str target: target mesh or skin cluster
|
||||
:rtype: list[InfluenceInfo]
|
||||
"""
|
||||
info = json.loads(plugin.ngst2Layers(target, q=True, influenceInfo=True))
|
||||
return unserialize_influences_from_json_data(info)
|
||||
|
||||
|
||||
def add_influences(influences, target):
|
||||
"""
|
||||
A shortcut for adding additional influences to a skincluster, without impacting existing weights
|
||||
|
||||
:param list[str] influences: list of influence paths
|
||||
:param str target: target mesh or skin cluster
|
||||
"""
|
||||
|
||||
skin_cluster = get_related_skin_cluster(target)
|
||||
|
||||
def long_names(names):
|
||||
result = set(cmds.ls(names, long=True))
|
||||
if len(result) != len(names):
|
||||
raise Exception("could not convert to a list of influences names: " + str(names))
|
||||
return result
|
||||
|
||||
existing = long_names([i.name if not i.path else i.path for i in list_influences(skin_cluster)])
|
||||
influences = long_names(influences)
|
||||
for i in influences - existing:
|
||||
cmds.skinCluster(skin_cluster, edit=True, addInfluence=i, weight=0)
|
||||
|
||||
|
||||
def is_slow_mode_skin_cluster(target):
|
||||
"""
|
||||
returns true, if ngSkinTools chose to use slow ngSkinTools api for this target. Right now this only happens when skinCluster has non-transform
|
||||
nodes as influences (e.g. inverseMatrix node).
|
||||
|
||||
:param str target: target mesh or skin cluster
|
||||
"""
|
||||
return plugin.ngst2Layers(target, q=True, skinClusterWriteMode=True) == "plug"
|
||||
189
2023/scripts/rigging_tools/ngskintools2/api/tools.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from ngSkinTools2.api import Layer, Layers
|
||||
from ngSkinTools2.api import layers as api_layers
|
||||
from ngSkinTools2.api import plugin
|
||||
from ngSkinTools2.api.layers import generate_layer_name
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.paint import PaintModeSettings
|
||||
from ngSkinTools2.api.target_info import list_influences
|
||||
from ngSkinTools2.decorators import undoable
|
||||
|
||||
log = getLogger("tools")
|
||||
|
||||
|
||||
def assign_from_closest_joint(target, layer, influences=None):
|
||||
# type: (str, Layer, List[int]) -> None
|
||||
"""
|
||||
For each selected vertex, picks a nearest joint and assigns 1.0 weight to that joint.
|
||||
|
||||
Operates on the currently active component selection, or whole mesh, depending on selection.
|
||||
|
||||
:param str target: skinned mesh or skin cluster node name;
|
||||
:param Layer layer: int or :py:class:`Layer` object to apply weights to;
|
||||
:param List[int] influences: selects only from provided subset of skinCluster influences.
|
||||
"""
|
||||
|
||||
if influences is None:
|
||||
influences = [i.logicalIndex for i in list_influences(target)]
|
||||
|
||||
if len(influences) == 0:
|
||||
# nothing to do?
|
||||
return
|
||||
|
||||
plugin.ngst2tools(
|
||||
tool="closestJoint",
|
||||
target=target,
|
||||
layer=api_layers.as_layer_id(layer),
|
||||
influences=[int(i) for i in influences],
|
||||
)
|
||||
|
||||
|
||||
def unify_weights(target, layer, overall_effect, single_cluster_mode):
|
||||
"""
|
||||
For all selected vertices, calculates average weights and assigns that value to each vertice. The effect is that all vertices end up having same weights.
|
||||
|
||||
Operates on the currently active component selection, or whole mesh, depending on selection.
|
||||
|
||||
:param str target: skinned mesh or skin cluster node name;
|
||||
:param Layer layer: int or :py:class:`Layer` object to apply weights to;
|
||||
:param float overall_effect: value between `0.0` and `1.0`, intensity of the operation. When applying newly calculated weights to the skin cluster,
|
||||
the formula is `weights = lerp(originalWeights, newWeights, overallEffect)`.
|
||||
:param bool single_cluster_mode: if `true`, all weights will receive the same average. If `false`, each connected mesh shell will be computed independently.
|
||||
"""
|
||||
plugin.ngst2tools(
|
||||
tool="unifyWeights",
|
||||
target=target,
|
||||
layer=api_layers.as_layer_id(layer),
|
||||
overallEffect=overall_effect,
|
||||
singleClusterMode=single_cluster_mode,
|
||||
)
|
||||
|
||||
|
||||
def flood_weights(target, influence=None, influences=None, settings=None):
|
||||
"""
|
||||
Apply paint tool in the layer with the given settings.
|
||||
|
||||
:param target: layer or mesh to set the weights in.
|
||||
:param influence: target influence: either an int for the logical index of the influence, or one of :py:class:`NamedPaintTarget` constants. Can be skipped if tool mode is Smooth or Sharpen.
|
||||
:param influences: if specified, overrides "influence" and allows passing multiple influences instead. Only supported by flood and sharpen at the moment.
|
||||
:type settings: PaintModeSettings
|
||||
"""
|
||||
|
||||
if settings is None:
|
||||
settings = PaintModeSettings() # just use default settings
|
||||
|
||||
args = {
|
||||
'tool': "floodWeights",
|
||||
'influences': influences if influences is not None else [influence],
|
||||
'mode': settings.mode,
|
||||
'intensity': settings.intensity,
|
||||
'iterations': int(settings.iterations),
|
||||
'influencesLimit': int(settings.influences_limit),
|
||||
'mirror': bool(settings.mirror),
|
||||
'distributeRemovedWeight': settings.distribute_to_other_influences,
|
||||
'limitToComponentSelection': settings.limit_to_component_selection,
|
||||
'useVolumeNeighbours': settings.use_volume_neighbours,
|
||||
'fixedInfluencesPerVertex': bool(settings.fixed_influences_per_vertex),
|
||||
}
|
||||
layer = None if not isinstance(target, Layer) else target # type: Layer
|
||||
if layer:
|
||||
args['layer'] = api_layers.as_layer_id(layer)
|
||||
|
||||
args['target'] = target if layer is None else layer.mesh
|
||||
|
||||
plugin.ngst2tools(**args)
|
||||
|
||||
|
||||
@undoable
|
||||
def merge_layers(layers):
|
||||
"""
|
||||
:type layers: list[Layer]
|
||||
:rtype: Layer
|
||||
"""
|
||||
if len(layers) > 1:
|
||||
# verify that all layers are from the same parent
|
||||
for i, j in zip(layers[:-1], layers[1:]):
|
||||
if i.mesh != j.mesh:
|
||||
raise Exception("layers are not from the same mesh")
|
||||
|
||||
result = plugin.ngst2tools(
|
||||
tool="mergeLayers",
|
||||
target=layers[0].mesh,
|
||||
layers=[api_layers.as_layer_id(i) for i in layers],
|
||||
)
|
||||
|
||||
target_layer = Layer.load(layers[0].mesh, result['layerId'])
|
||||
target_layer.set_current()
|
||||
|
||||
return target_layer
|
||||
|
||||
|
||||
@undoable
|
||||
def duplicate_layer(layer):
|
||||
"""
|
||||
|
||||
:type layer: Layer
|
||||
:rtype: Layer
|
||||
"""
|
||||
|
||||
result = plugin.ngst2tools(
|
||||
tool="duplicateLayer",
|
||||
target=layer.mesh,
|
||||
sourceLayer=layer.id,
|
||||
)
|
||||
|
||||
target_layer = Layer.load(layer.mesh, result['layerId'])
|
||||
|
||||
import re
|
||||
|
||||
base_name = re.sub(r"( \(copy\))?( \(\d+\))*", "", layer.name)
|
||||
other_layers = [l for l in Layers(target_layer.mesh).list() if l.id != target_layer.id]
|
||||
target_layer.name = generate_layer_name(other_layers, base_name + " (copy)")
|
||||
|
||||
target_layer.set_current()
|
||||
|
||||
return target_layer
|
||||
|
||||
|
||||
@undoable
|
||||
def fill_transparency(layer):
|
||||
"""
|
||||
|
||||
:type layer: Layer
|
||||
"""
|
||||
|
||||
plugin.ngst2tools(
|
||||
tool="fillLayerTransparency",
|
||||
target=layer.mesh,
|
||||
layer=layer.id,
|
||||
)
|
||||
|
||||
|
||||
def copy_component_weights(layer):
|
||||
"""
|
||||
:type layer: Layer
|
||||
"""
|
||||
|
||||
plugin.ngst2tools(
|
||||
tool="copyComponentWeights",
|
||||
target=layer.mesh,
|
||||
layer=layer.id,
|
||||
)
|
||||
|
||||
|
||||
def paste_average_component_weights(layer):
|
||||
"""
|
||||
:type layer: Layer
|
||||
"""
|
||||
|
||||
plugin.ngst2tools(
|
||||
tool="pasteAverageComponentWeights",
|
||||
target=layer.mesh,
|
||||
layer=layer.id,
|
||||
)
|
||||
|
||||
|
||||
def refresh_screen(target):
|
||||
plugin.ngst2tools(
|
||||
tool="refreshScreen",
|
||||
target=target,
|
||||
)
|
||||
110
2023/scripts/rigging_tools/ngskintools2/api/transfer.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import itertools
|
||||
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.decorators import undoable
|
||||
|
||||
from . import plugin
|
||||
from .influenceMapping import InfluenceMapping, InfluenceMappingConfig
|
||||
from .layers import init_layers, target_info
|
||||
from .mirror import Mirror
|
||||
from .suspend_updates import suspend_updates
|
||||
|
||||
|
||||
class VertexTransferMode(Object):
|
||||
"""
|
||||
Constants for vertex_transfer_mode argument
|
||||
"""
|
||||
|
||||
closestPoint = 'closestPoint' #: When vertices from two surface are matched, each destination mesh vertex finds a closest point on source mesh, and weights are calculated based on the triangle weights of that closest point.
|
||||
uvSpace = 'uvSpace' #: Similar to closestPoint strategy, but matching is done in UV space instead of XYZ space.
|
||||
vertexId = 'vertexId' #: Vertices are matched by ID. Not usable for mirroring; this is used for transfer/import cases where meshes are known to be identical
|
||||
|
||||
|
||||
class LayersTransfer(Object):
|
||||
def __init__(self):
|
||||
self.source = None
|
||||
self.target = None
|
||||
self.source_file = None
|
||||
self.vertex_transfer_mode = VertexTransferMode.closestPoint
|
||||
self.influences_mapping = InfluenceMapping()
|
||||
self.influences_mapping.config = InfluenceMappingConfig.transfer_defaults()
|
||||
self.keep_existing_layers = True
|
||||
self.customize_callback = None
|
||||
|
||||
def load_source_from_file(self, file, format):
|
||||
from .import_export import FileFormatWrapper
|
||||
|
||||
with FileFormatWrapper(file, format=format, read_mode=True) as f:
|
||||
data = plugin.ngst2tools(
|
||||
tool="importJsonFile",
|
||||
file=f.plain_file,
|
||||
)
|
||||
|
||||
self.source = "-reference-mesh-"
|
||||
self.source_file = file
|
||||
influences = target_info.unserialize_influences_from_json_data(data['influences'])
|
||||
|
||||
self.influences_mapping.influences = influences
|
||||
|
||||
def calc_influences_mapping_as_flat_list(self):
|
||||
mapping_pairs = list(self.influences_mapping.asIntIntMapping(self.influences_mapping.calculate()).items())
|
||||
if len(mapping_pairs) == 0:
|
||||
raise Exception("no mapping between source and destination influences")
|
||||
# convert dict to flat array
|
||||
return list(itertools.chain.from_iterable(mapping_pairs))
|
||||
|
||||
def execute(self):
|
||||
# sanity check: destination must be skinnable target
|
||||
if target_info.get_related_skin_cluster(self.target) is None:
|
||||
return False
|
||||
|
||||
if not self.influences_mapping.influences:
|
||||
if target_info.get_related_skin_cluster(self.source) is None:
|
||||
return False
|
||||
self.influences_mapping.influences = target_info.list_influences(self.source)
|
||||
self.influences_mapping.destinationInfluences = target_info.list_influences(self.target)
|
||||
|
||||
if self.customize_callback is None:
|
||||
self.complete_execution()
|
||||
else:
|
||||
self.customize_callback(self)
|
||||
|
||||
@undoable
|
||||
def complete_execution(self):
|
||||
l = init_layers(self.target)
|
||||
Mirror(self.target).recalculate_influences_mapping()
|
||||
|
||||
with suspend_updates(self.target):
|
||||
if not self.keep_existing_layers:
|
||||
l.clear()
|
||||
|
||||
plugin.ngst2tools(
|
||||
tool="transfer",
|
||||
source=self.source,
|
||||
target=self.target,
|
||||
vertexTransferMode=self.vertex_transfer_mode,
|
||||
influencesMapping=self.calc_influences_mapping_as_flat_list(),
|
||||
)
|
||||
|
||||
|
||||
def transfer_layers(
|
||||
source, destination, vertex_transfer_mode=VertexTransferMode.closestPoint, influences_mapping_config=InfluenceMappingConfig.transfer_defaults()
|
||||
):
|
||||
"""
|
||||
Transfer skinning layers from source to destination mesh.
|
||||
|
||||
:param str source: source mesh or skin cluster node name
|
||||
:param str destination: destination mesh or skin cluster node name
|
||||
:param str vertex_transfer_mode: describes how source mesh vertices are mapped to destination vertices. Defaults to `closestPoint`
|
||||
:param InfluenceMappingConfig influences_mapping_config: configuration for InfluenceMapping; supply this, or `influences_mapping` instance; default settings for transfer are used if this is not supplied.
|
||||
:param InfluenceMapping influences_mapping: mapper instance to use for matching influences; if this is provided, `influences_mapping_config` is ignored.
|
||||
:return:
|
||||
"""
|
||||
|
||||
t = LayersTransfer()
|
||||
t.source = source
|
||||
t.target = destination
|
||||
t.vertex_transfer_mode = vertex_transfer_mode
|
||||
t.influences_mapping.config = influences_mapping_config
|
||||
|
||||
t.execute()
|
||||
51
2023/scripts/rigging_tools/ngskintools2/api/versioncheck.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import datetime
|
||||
|
||||
from ngSkinTools2 import version
|
||||
from ngSkinTools2.api import http_client
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
class UpdateInfo(Object):
|
||||
def __init__(self):
|
||||
self.update_available = False
|
||||
self.update_date = ''
|
||||
self.latest_version = ''
|
||||
self.download_url = ''
|
||||
|
||||
|
||||
def download_update_info(success_callback, failure_callback):
|
||||
"""
|
||||
executes version info download in separate thread,
|
||||
then runs provided callbacks in main thread when download completes or fails
|
||||
|
||||
returns thread object that gets started.
|
||||
"""
|
||||
|
||||
import platform
|
||||
|
||||
from maya import cmds
|
||||
|
||||
os = platform.system()
|
||||
maya_version = str(int(cmds.about(api=True)))
|
||||
|
||||
url = http_client.encode_url(
|
||||
"https://versiondb.ngskintools.com/releases/ngSkinTools-v2-" + os + "-maya" + maya_version,
|
||||
{
|
||||
'currentVersion': version.pluginVersion(),
|
||||
'uniqueClientId': version.uniqueClientId(),
|
||||
},
|
||||
)
|
||||
|
||||
def on_success(response):
|
||||
try:
|
||||
info = UpdateInfo()
|
||||
info.update_date = datetime.datetime.strptime(response['dateReleased'], "%Y-%m-%d")
|
||||
info.latest_version = response['latestVersion']
|
||||
info.download_url = response['downloadUrl']
|
||||
info.update_available = version.compare_semver(version.pluginVersion(), info.latest_version) > 0
|
||||
|
||||
success_callback(info)
|
||||
except Exception as err:
|
||||
failure_callback(str(err))
|
||||
|
||||
return http_client.get_async(url, success_callback=on_success, failure_callback=failure_callback)
|
||||
23
2023/scripts/rigging_tools/ngskintools2/cleanup.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
The purpose of the module is mostly for testing: close everything and prepare for source reload
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
|
||||
handlers = []
|
||||
|
||||
log = getLogger("cleanup")
|
||||
|
||||
|
||||
def registerCleanupHandler(handler):
|
||||
handlers.append(handler)
|
||||
|
||||
|
||||
def cleanup():
|
||||
while len(handlers) > 0:
|
||||
handler = handlers.pop()
|
||||
try:
|
||||
handler()
|
||||
except Exception as err:
|
||||
log.error(err)
|
||||
78
2023/scripts/rigging_tools/ngskintools2/decorators.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from functools import wraps
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
log = getLogger("decorators")
|
||||
|
||||
|
||||
def preserve_selection(function):
|
||||
"""
|
||||
decorator for function;
|
||||
saves selection prior to execution and restores it
|
||||
after function finishes
|
||||
"""
|
||||
|
||||
@wraps(function)
|
||||
def undoable_wrapper(*args, **kargs):
|
||||
selection = cmds.ls(sl=True, fl=True)
|
||||
highlight = cmds.ls(hl=True, fl=True)
|
||||
try:
|
||||
return function(*args, **kargs)
|
||||
finally:
|
||||
if selection:
|
||||
cmds.select(selection)
|
||||
else:
|
||||
cmds.select(clear=True)
|
||||
if highlight:
|
||||
cmds.hilite(highlight)
|
||||
|
||||
return undoable_wrapper
|
||||
|
||||
|
||||
def undoable(function):
|
||||
"""
|
||||
groups function contents into one undo block
|
||||
"""
|
||||
|
||||
@wraps(function)
|
||||
def result(*args, **kargs):
|
||||
with Undo(name=function.__name__):
|
||||
return function(*args, **kargs)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Undo(Object):
|
||||
"""
|
||||
an undo context for use "with Undo():"
|
||||
"""
|
||||
|
||||
def __init__(self, name=None):
|
||||
self.name = name
|
||||
|
||||
def __enter__(self):
|
||||
log.debug("UNDO chunk %r: start", self.name)
|
||||
cmds.undoInfo(openChunk=True, chunkName=self.name)
|
||||
return self
|
||||
|
||||
def __exit__(self, _type, value, traceback):
|
||||
log.debug("UNDO chunk %r: end", self.name)
|
||||
cmds.undoInfo(closeChunk=True)
|
||||
|
||||
|
||||
def trace_exception(function):
|
||||
@wraps(function)
|
||||
def result(*args, **kwargs):
|
||||
try:
|
||||
return function(*args, **kwargs)
|
||||
except Exception:
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
traceback.print_exc(file=sys.__stderr__)
|
||||
raise
|
||||
|
||||
return result
|
||||
9
2023/scripts/rigging_tools/ngskintools2/docs/Resources/appstore-combined.min.css
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 28 KiB |
BIN
2023/scripts/rigging_tools/ngskintools2/docs/Resources/win64.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
353
2023/scripts/rigging_tools/ngskintools2/docs/index.html
Normal file
@@ -0,0 +1,353 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0106)https://apps.autodesk.com/RVT/en/Detail/HelpDoc?appId=2231912794620250494&appLang=en&os=Win64&mode=preview -->
|
||||
<html lang="en" class=" js canvas canvastext no-touch hashchange history draganddrop rgba multiplebgs borderimage boxshadow textshadow cssgradients csstransitions sessionstorage" data-lt-installed="true"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
<title>Autodesk App Store - help file</title><link rel="stylesheet" href="./Resources/appstore-combined.min.css"></head>
|
||||
<body>
|
||||
<div id="main" class="detail-page helpdoc-page clearfix" style="min-height: 619px;">
|
||||
<div id="content">
|
||||
<div id="helpdoc-head">
|
||||
<img id="helpdoc-head-icon" src="./Resources/resized_5ca803cc-8cd1-4cbe-8825-98efaeef156e_.png" alt="ngSkinTools 2">
|
||||
<div id="helpdoc-head-description">
|
||||
<h1 id="helpdoc-product-title">ngSkinTools 2</h1>
|
||||
|
||||
<div class="clear"></div>
|
||||
<hr>
|
||||
<div class="seller">Viktoras Makauskas</div>
|
||||
<div class="description">ngSkinTools is a skinning plugin for Autodesk® Maya®, introducing new concepts to character skinning such as layers, any-pose-mirroring, enhanced paint brushes, true smoothing, and more.</div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
<div id="helpdoc-tag"><a href="#description" class="helpdoc-breadcrumb">Description</a>
|
||||
|
||||
<a href="#generalinfo" class="helpdoc-breadcrumb">General Usage Instructions</a>
|
||||
<a href="#screensinfo" class="helpdoc-breadcrumb">Screenshots</a>
|
||||
<a href="#inunininfo" class="helpdoc-breadcrumb">Installation/Uninstallation</a>
|
||||
<a href="#knownissueinfo" class="helpdoc-breadcrumb">Known Issues</a>
|
||||
<a href="#contactinfo" class="helpdoc-breadcrumb">Contact</a>
|
||||
<a href="#versionhistoryinfo" class="">Version History</a>
|
||||
<div class="clear"></div>
|
||||
</div><div class="helpdoc-element description">
|
||||
<div class="clear"></div>
|
||||
<h1 id="description">Description</h1>
|
||||
<div class="helpdoc-text">
|
||||
|
||||
<h3 id="layers">Layers</h3>
|
||||
<p>Skinning
|
||||
layers are a central feature of ngSkinTools. With them, you break your rig down
|
||||
into easier manageable parts and edit them separately, then blend everything
|
||||
together through layer transparency.</p>
|
||||
<p>They’re
|
||||
not just a simple way to make your work more organized - they also physically
|
||||
isolate groups of influences from the rest of the rig, so paint and edit
|
||||
operations won’t mix-in influences you were not expecting. This also allows you
|
||||
to do things that were impossible before: per-layer mirroring, adjusting
|
||||
influence weight up/down through layer transparency, blend transferred weights
|
||||
with previous weights, to name a few.</p>
|
||||
|
||||
<h3 id="viewport-tools">Viewport
|
||||
tools</h3>
|
||||
<p>Just
|
||||
like in the previous version, ngSkinTools brings its own weight painting tools.
|
||||
Improving viewport experience is the main focus of V2, and it's complete revamp
|
||||
over the previous implementation.</p>
|
||||
<ul>
|
||||
<li>Selecting
|
||||
influences on screen, a #1 requested feature from users, is nowhere. Just hold
|
||||
“S” and drag over the surface to select dominant influence from that part of
|
||||
the mesh, or hover over a joint pivot to select precisely the joints you
|
||||
want;</li>
|
||||
<li>In
|
||||
addition to the usual surface projection mode for the brush, the new “screen”
|
||||
brush projection mode is useful when you want to quickly set weights for both
|
||||
sides of the mesh;</li>
|
||||
<li>Custom
|
||||
shortcuts while in paint mode allow for quick access to intensity
|
||||
presets;</li>
|
||||
<li>Color
|
||||
feedback is now provided through VP2 APIs, greatly improving the performance
|
||||
of displayed meshes.</li></ul><br>
|
||||
|
||||
<h3 id="smoothing">Smoothing</h3>
|
||||
<p>Keeping
|
||||
weights in harmony with each other is not easy. ngSkinTools help you smooth
|
||||
weights with the control you need, allowing you to control the intensity, number
|
||||
of iterations and effective radius. For very dense meshes, added “iterations”
|
||||
argument now allows for the quicker spread of smoothness over larger areas of
|
||||
the mesh.</p>
|
||||
<p>The
|
||||
“relax” tool from V1 is gone. With major performance rework, you’ll notice that
|
||||
simple flood-smoothing is now much faster and should be a near-instant operation
|
||||
even with large meshes.</p>
|
||||
<p>The
|
||||
opposite “brother” of smooth brush, “sharpen”, is also there - for cases where
|
||||
you want to just bring out the dominant influences</p>
|
||||
|
||||
<h3 id="mirroring">Mirroring</h3>
|
||||
<p>Mirroring
|
||||
is one of the most frequent automated tasks you might want from your skinning
|
||||
tool. With ngSkinTools, you’ll be able to:</p>
|
||||
<ul>
|
||||
<li>Mirror
|
||||
rigs in any pose; no need to switch to T-pose;</li>
|
||||
<li>Have
|
||||
granular control over left/right/center influences mapping, matching
|
||||
left/right joints by naming convention, joint labels, etc;</li>
|
||||
<li>Easily
|
||||
mirror parts of your rig by leveraging layers;</li>
|
||||
<li>Automatic
|
||||
mirroring of weights to the opposite side as you paint so that you don’t need
|
||||
to get distracted from painting while working on symmetrical layers.</li></ul><br>
|
||||
|
||||
<h3 id="layer-effects">Layer
|
||||
effects</h3>
|
||||
<p>With
|
||||
the “mirror as a layer effect” feature, ngSkinTools introduce a new concept to
|
||||
ngSkinTools - layer effects. This differs from automatic mirroring of weights as
|
||||
it’s not directly modifying your layer weights; instead, it’s a post-effect that
|
||||
happens in the background buffer. This has multiple benefits, like a much
|
||||
cleaner seamline of left/right sides, the ability to tweak mirroring settings
|
||||
AFTER weights are painted, etc.</p>
|
||||
|
||||
<h3 id="compatibility">Compatibility</h3>
|
||||
<p>As
|
||||
it's predecessor, ngSkinTools2 operates on standard Maya skinCluster (also known
|
||||
as “smooth skin”), so no custom nodes will be required to use your rig. The
|
||||
plugin has a couple of custom nodes, but they’re only required while you work on
|
||||
setting up your skin weights and can be deleted after, so your work should stay
|
||||
compatible with most pipelines out there.</p>
|
||||
|
||||
<h3 id="performance">Performance</h3>
|
||||
<p>A
|
||||
lot of speed improvements have been made since V2, like improving the
|
||||
utilization of modern multi-core processors, or eliminating bottlenecks through
|
||||
much heavier use of performance profiling. Having a responsive, snappy tool is
|
||||
always a pleasure to work with.</p></div></div>
|
||||
|
||||
|
||||
<div class="helpdoc-element ">
|
||||
<div class="clear"></div>
|
||||
<h1 id="generalinfo">General Usage Instructions</h1>
|
||||
<div class="helpdoc-text"><p>The installer from Autodesk App Store loads the application under the Custom shelf.</p>
|
||||
<p><a href="http://www.ngskintools.com/docs"target="_blank">http://www.ngskintools.com/docs/</a></p></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="helpdoc-element-screenshot" class="helpdoc-element ">
|
||||
<div class="clear"></div>
|
||||
<h1 id="screensinfo">Screenshots</h1>
|
||||
<div>
|
||||
<div class="helpdoc-screenshot">
|
||||
<a href="./Resources/original_89910fa8-2c30-4376-a905-12c8a003d16b_.png" title="" data-mime="Image" rel="gallery">
|
||||
<div class="helpdoc-img-container">
|
||||
<span class="helper"></span>
|
||||
<img class="helpdoc-screenshot-img" src="./Resources/original_89910fa8-2c30-4376-a905-12c8a003d16b_.png">
|
||||
<span class="helper"></span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="helpdoc-command helpdoc-element helpdoc-element-hidden">
|
||||
<div class="clear"></div>
|
||||
<h1 id="commandinfo">Commands</h1>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="helpdoc-element ">
|
||||
<div class="clear"></div>
|
||||
<h1 id="inunininfo">Installation/Uninstallation</h1>
|
||||
<div class="helpdoc-text"><div id="pills-tabContent">
|
||||
<div id="pills-windows">
|
||||
<p>The installer that ran when you downloaded this plug-in from Autodesk App Store has already installed the plug-in. Windows only: To uninstall this plug-in, simply rerun the installer downloaded, and select the 'Uninstall' button, or you can uninstall it from 'Control Panel\Programs\Programs and Features', just as you would uninstall any other application from your system. The panel on the Plug-ins tab will not be removed until Maya is restarted.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>Linux and OSX: To uninstall this plug-in, simply delete the module directory from your system. The panel on the Plug-ins tab will not be removed until Maya is restarted.</p>
|
||||
<div id="pills-tabContent">
|
||||
<div id="pills-windows">
|
||||
<p>Download .msi and run it on your computer. The installation will place files in <code>C:\ProgramData\Autodesk\ApplicationPlugins\ngskintools2</code> (unless your <code>%ProgramData%</code> the environment variable is different).</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>Using an autoloader system, nothing needs to be configured additionally. Maya scans autoloader locations for plugins at startup and configures each discovered plugin automatically. The autoloader will create a “ngSkinTools2” shelf with a button to open UI.</p>
|
||||
<p>Now, restart Maya and a new tab ngSkinTools2 should appear on your shelf.</p></div>
|
||||
</div>
|
||||
|
||||
<h3>Available on</h3>
|
||||
<table>
|
||||
<tr><td><img height=48 src="./Resources/win64.png" /></td><td><img height=48 src="./Resources/macos64.png" /></td><td><img height=48 src="./Resources/linux64.png" /></td></tr>
|
||||
<tr><td align="center">Windows</td><td align="center">Mac OSX</td><td align="center">Linux</td></tr>
|
||||
</table>
|
||||
|
||||
<div class="helpdoc-element helpdoc-element-hidden">
|
||||
<div class="clear"></div>
|
||||
<h1 id="addinfo">Additional Information</h1>
|
||||
<div class="helpdoc-text"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="helpdoc-element ">
|
||||
<div class="clear"></div>
|
||||
<h1 id="knownissueinfo">Known Issues</h1>
|
||||
<div class="helpdoc-text"><p>For upcoming features and bugfixes, visit <a href="http://ngskintools.com/v2/roadmap"target="_blank">ngSkinTools v2 public roadmap</a>.</p></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="helpdoc-element ">
|
||||
<div class="clear"></div>
|
||||
<h1 id="contactinfo">Contact</h1>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
<div class="">Company Name: Viktoras Makauskas</div>
|
||||
<div class="">Company URL: <a href="http://www.ngskintools.com/" target="_blank">http://www.ngskintools.com</a></div>
|
||||
<div class="">Support Contact: <a href="mailto:support@ngskintools.com">support@ngskintools.com</a></div>
|
||||
</div>
|
||||
|
||||
<div class="helpdoc-block ">
|
||||
<div class="clear"></div>
|
||||
<h1 id="authorinfo">Author/Company Information</h1>
|
||||
<div class="helpdoc-text">Viktoras Makauskas</div>
|
||||
</div>
|
||||
|
||||
<div class="helpdoc-block ">
|
||||
<div class="clear"></div>
|
||||
<h1 id="supportinfo">Support Information</h1>
|
||||
<div class="helpdoc-text"><p>If you have a problem, a question, a feature request, let me know. Your feedback is what drives the project forward!<strong> </strong></p>
|
||||
<p><strong>Documentation</strong></p>
|
||||
<p>Features, tutorials & FAQ available at <a href="http://ngskintools.com/v2/" target="_blank">ngSkinTools official website</a>. </p>
|
||||
<p><strong>Contact in private</strong></p>
|
||||
<p>Use the <a href="http://ngskintools.com/contact/" target="_blank">online contact form</a> or email to <a href="mailto:support@ngskintools.com">support@ngskintools.com</a>.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="helpdoc-version helpdoc-element">
|
||||
<div class="clear"></div>
|
||||
<h1 id="versionhistoryinfo">Version History</h1>
|
||||
<div>
|
||||
<table class="helpdoc-table" id="helpdoc-table-version">
|
||||
<colgroup>
|
||||
<col style="width:150px">
|
||||
<col style="width:680px">
|
||||
</colgroup>
|
||||
<tbody><tr>
|
||||
<th>Version Number</th>
|
||||
<th>Version Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>
|
||||
2.0.24
|
||||
</p>
|
||||
</td>
|
||||
<td style="white-space: pre-wrap;">2.0.23 (2021-Mar-12)
|
||||
* Fixed: stylus pressure is not updated during the stroke;
|
||||
* Fixed: Maya crashes when smoothing an empty layer with "adjust existing influences only";
|
||||
* Fixed: (regression) UI is not opening on macOS;
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>
|
||||
2.0.23
|
||||
</p>
|
||||
</td>
|
||||
<td style="white-space: pre-wrap;">2.0.23 (2021-Mar-10)
|
||||
* Added: adjustable brush size: don't reset to zero when changing brush size in the viewport.
|
||||
* Added: randomize influence colors in paint/display settings;
|
||||
* Added: layers on/off "eye" button in layers tree UI;
|
||||
* Fixed: ngSkinTools will not modify skinCluster's `normalizeWeights` value anymore; for performance boost, you still
|
||||
can disable skinCluster's normalization by setting `normalizeWeights=None`. In "normalizeWeights:interactive"
|
||||
skinCluster mode, Maya will no longer complain that "The weight total would have exceeded 1.0". ngSkinTools will try
|
||||
extra hard to normalize each vertex to a perfect 1.0;
|
||||
* Fixed: UI is not displayed correctly on high DPI displays when UI scaling is enabled in Maya;
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>
|
||||
2.0.22
|
||||
</p>
|
||||
</td>
|
||||
<td style="white-space: pre-wrap;">2.0.22 (2021-Feb-06)
|
||||
Added: convenience tool for adding influences to existing skin clusters. Select influences, target mesh and select “Tools | Add Influence”;
|
||||
Fixed: weights will now display properly when viewport option “use default material” is turned on;
|
||||
Added: (v1 feature) Limit max influences per vertex before writing to skin cluster;
|
||||
Added: (v1 feature) Prune small weights before writing to skin cluster.
|
||||
|
||||
2.0.21 (2021-Jan-11)
|
||||
Added: copy/paste vertex weights between different selections (tab “tools” - “copy component weights/paste average component weights”);
|
||||
Added: tool “fill transparency” - for all empty vertices in a layer, assign weights from closest non-empty vertex;
|
||||
Added: “duplicate layer” operation;
|
||||
Added: “Merge layers” operation: combine selected layers into one.
|
||||
|
||||
2.0.20 (2020-Dec-02)
|
||||
Fixed: Linux: crashing on startup
|
||||
|
||||
2.0.19 (2020-Nov-28)
|
||||
Added: influences mapping in mirror screen will now allow matching joints by DG connections; if symmetrical joints are linked between themselves with message connections, ngSkinTools will be able to leverage that information when mirroring weights;
|
||||
Added: symmetry mesh: an option to provide an alternative mesh for calculating vertex mapping for mirroring;
|
||||
Fixed: deleting visibility node can crash Maya sometimes, e.g. switching to component mode (F8) while paint tool is active
|
||||
|
||||
2.0.18 (2020-Nov-20)
|
||||
Added: “use all joints” option for “weights from closest joint” tool; few internal optimizations to speedup operation;
|
||||
Added: “weights from closest joint” option: create a new layer
|
||||
Fixed: “weights from closest joint”: the tool is only using joints as spots, but not as segments;
|
||||
Fixed: after “weights from closest joint” operation influences list is not refreshed;
|
||||
Fixed: “weights from closest joint”: “assign” button sometimes disabled;
|
||||
|
||||
2.0.17 (2020-Nov-19)
|
||||
Fixed: “resume in workspace” error while opening UI
|
||||
|
||||
2.0.16 (2020-Nov-15)
|
||||
Added: skin data will be compressed for ngSkinTools data nodes, which should substantially reduce file size for scenes with lots of skinning layers;
|
||||
Added: paint mode intensity sliders are now exponential: “smooth”, “add” and “sharpen” sliders will now be more precise for lower values, and “scale” mode will allow for more precision when setting high values.
|
||||
|
||||
2.0.15 (2020-Nov-10)
|
||||
Added: new “Set Weights” tab contains tools to apply weights to vertex/edge/polygon selection instead of painting.
|
||||
Added: a new option for smooth tool - “only adjust existing vertex influences”; when this is turned on, the smooth tool will prevent influences weights spreading across the surface
|
||||
Fixed: layer mirror effects correctly saved/loaded in files;
|
||||
Fixed: mask mirror effect was not correctly used by layer blending engine
|
||||
|
||||
2.0.14 (2020-Oct-04)
|
||||
Fixed: occasional crashes when using mirror effect on layers;
|
||||
Additional stability fixes.
|
||||
|
||||
2.0.13 (2020-Oct-04)
|
||||
Fixed: minor bug in 2.0.12 blocks UI from opening;
|
||||
|
||||
2.0.12 (2020-Oct-03)
|
||||
Added: option to view used influences in influences list;
|
||||
Added: hide “DQ weights” channel in influences list if skin cluster skinning method is not set to “Weight Blended”;
|
||||
|
||||
2.0.11 (2020-Oct-01)
|
||||
Fixed: broken Linux builds
|
||||
|
||||
2.0.10 (2020-Sep-26)
|
||||
Added: pressing “f” while painting focuses viewport camera to current paint target; for joints and other influences, the current joint pivot is used as camera interest point; when current paint target is a mask, viewport centers around painted values;
|
||||
Fixed: influence mapping UI error if some influences are no joints;
|
||||
|
||||
2.0.9 (2020-Sep-11)
|
||||
Fixed: undo paint crashing Maya;
|
||||
Fixed: incorrect brush behavior with multiple viewports open;
|
||||
Fixed: incorrect mesh display / VP2 transparency setting sensitive;
|
||||
Fixed: clearing selection while painting does not update the display of current mesh;</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div></body></html>
|
||||
67
2023/scripts/rigging_tools/ngskintools2/launcher.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
ngSkinTools2 启动器
|
||||
用于从工具架快速启动 ngSkinTools2
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def LaunchNgSkinTools():
|
||||
"""
|
||||
启动 ngSkinTools2 主界面
|
||||
"""
|
||||
try:
|
||||
from maya import cmds, mel
|
||||
|
||||
# 将当前目录添加到 Python 路径,并使用别名
|
||||
# 这样 ngSkinTools2 内部的 "from ngSkinTools2.ui" 就能找到模块
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
|
||||
# 添加父目录到路径
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
# 添加当前目录到路径,作为 ngSkinTools2 模块
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
# 在 sys.modules 中创建别名,让 ngSkinTools2 指向 ngskintools2
|
||||
import rigging_tools.ngskintools2 as ngskintools2_module
|
||||
sys.modules['ngSkinTools2'] = ngskintools2_module
|
||||
|
||||
# 查找并添加插件路径
|
||||
# ngSkinTools2 插件在 ngskintools2/plug-ins/2023/ 下
|
||||
plugin_dir = os.path.join(current_dir, 'plug-ins', '2023')
|
||||
|
||||
if os.path.exists(plugin_dir):
|
||||
# 添加插件路径
|
||||
current_plugin_path = os.environ.get('MAYA_PLUG_IN_PATH', '')
|
||||
if plugin_dir not in current_plugin_path:
|
||||
os.environ['MAYA_PLUG_IN_PATH'] = plugin_dir + os.pathsep + current_plugin_path
|
||||
|
||||
# 加载插件
|
||||
if not cmds.pluginInfo('ngSkinTools2', query=True, loaded=True):
|
||||
try:
|
||||
cmds.loadPlugin(os.path.join(plugin_dir, 'ngSkinTools2.mll'))
|
||||
print("ngSkinTools2 plugin loaded successfully")
|
||||
except Exception as plugin_error:
|
||||
print(f"Warning: Could not load ngSkinTools2 plugin: {plugin_error}")
|
||||
else:
|
||||
print(f"Warning: Plugin directory not found: {plugin_dir}")
|
||||
|
||||
# 现在可以导入并打开 UI
|
||||
from rigging_tools.ngskintools2 import open_ui
|
||||
open_ui()
|
||||
print("ngSkinTools2 UI opened successfully")
|
||||
except Exception as e:
|
||||
print(f"Failed to open ngSkinTools2: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
LaunchNgSkinTools()
|
||||
32
2023/scripts/rigging_tools/ngskintools2/license.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
ngSkinTools Software License Agreement.
|
||||
|
||||
|
||||
This is a legal agreement between you and ngSkinTools author (Viktoras Makauskas) covering your use of
|
||||
ngSkinTools (the "Software").
|
||||
|
||||
1) ngSkinTools is provided as freeware.
|
||||
|
||||
2) ngSkinTools Software is owned by Viktoras Makauskas and is protected by copyright laws and international
|
||||
treaty provisions. Therefore, you must treat the Software like any other copyrighted material.
|
||||
|
||||
3) You may not distribute, rent, sub-license or otherwise make available to others the Software or
|
||||
documentation or copies thereof, except as expressly permitted in this License without prior written consent
|
||||
from ngSkinTools author (Viktoras Makauskas). In the case of an authorized transfer, the transferee must agree
|
||||
to be bound by the terms and conditions of this License Agreement.
|
||||
|
||||
4) You may not remove any proprietary notices, labels, trademarks on the Software or documentation. You may not
|
||||
modify, de-compile, disassemble or reverse engineer the Software.
|
||||
|
||||
5) Limited warranty: ngSkinTools software and documentation are "as is" without any warranty as to their
|
||||
performance, merchantability or fitness for any particular purpose. The licensee assumes the entire risk as to
|
||||
the quality and performance of the software. In no event shall ngSkinTools author or anyone else who has been
|
||||
involved in the creation, development, production, or delivery of this software be liable for any direct,
|
||||
incidental or consequential damages, such as, but not limited to, loss of anticipated profits, benefits, use,
|
||||
or data resulting from the use of this software, or arising out of any breach of warranty.
|
||||
|
||||
Copyright (C) 2009-2020 Viktoras Makauskas
|
||||
|
||||
http://www.ngskintools.com
|
||||
support@ngskintools.com
|
||||
|
||||
All rights reserved.
|
||||
10
2023/scripts/rigging_tools/ngskintools2/maya_version.py
Normal file
@@ -0,0 +1,10 @@
|
||||
MAYA_2018 = 20180000
|
||||
MAYA_2019 = 20190000
|
||||
MAYA_2020 = 20200000
|
||||
MAYA_2021 = 20210000
|
||||
|
||||
|
||||
def at_least(version):
|
||||
from maya import cmds
|
||||
|
||||
return cmds.about(api=True) >= version
|
||||
25
2023/scripts/rigging_tools/ngskintools2/observableValue.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.signal import Signal
|
||||
|
||||
|
||||
class Undefined(Object):
|
||||
pass
|
||||
|
||||
|
||||
class ObservableValue(Object):
|
||||
def __init__(self, default_value=Undefined):
|
||||
self.value = default_value
|
||||
self.changed = Signal("observable value")
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
self.changed.emit()
|
||||
|
||||
def __call__(self, default=Undefined, *args, **kwargs):
|
||||
if self.value != Undefined:
|
||||
return self.value
|
||||
|
||||
if default != Undefined:
|
||||
return default
|
||||
|
||||
raise Exception("using observable value before setting it")
|
||||
@@ -0,0 +1,64 @@
|
||||
from ngSkinTools2 import api, signal
|
||||
from ngSkinTools2.api import Layer, PasteOperation
|
||||
from ngSkinTools2.api.session import Session
|
||||
|
||||
|
||||
def action_copy_cut(session, parent, cut):
|
||||
"""
|
||||
:type session: Session
|
||||
:type parent: PySide2.QtWidgets.QWidget
|
||||
:type cut: bool
|
||||
"""
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def cut_copy_callback():
|
||||
if session.state.selectedSkinCluster is None:
|
||||
return
|
||||
if session.state.currentLayer.layer is None:
|
||||
return
|
||||
influences = session.state.currentLayer.layer.paint_targets
|
||||
operation = api.copy_weights # type: Callable[[Layer, list], None]
|
||||
if cut:
|
||||
operation = api.cut_weights
|
||||
|
||||
operation(session.state.currentLayer.layer, influences)
|
||||
|
||||
operation_name = "Cut" if cut else "Copy"
|
||||
result = actions.define_action(parent, operation_name + " weights to clipboard", callback=cut_copy_callback)
|
||||
|
||||
@signal.on(session.events.currentLayerChanged, session.events.currentInfluenceChanged, qtParent=parent)
|
||||
def on_selection_changed():
|
||||
layer = session.state.currentLayer.layer
|
||||
result.setEnabled(layer is not None and len(layer.paint_targets) > 0)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def action_paste(session, parent, operation):
|
||||
"""
|
||||
:type session: Session
|
||||
:type parent: PySide2.QtWidgets.QWidget
|
||||
:type cut: bool
|
||||
"""
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def paste_callback():
|
||||
if session.state.currentLayer.layer is None:
|
||||
return
|
||||
influences = session.state.currentLayer.layer.paint_targets
|
||||
api.paste_weights(session.state.currentLayer.layer, operation, influences=influences)
|
||||
|
||||
labels = {
|
||||
PasteOperation.add: 'Paste weights (add to existing)',
|
||||
PasteOperation.subtract: 'Paste weight (subtract from existing)',
|
||||
PasteOperation.replace: 'Paste weights (replace existing)',
|
||||
}
|
||||
|
||||
result = actions.define_action(parent, labels[operation], callback=paste_callback)
|
||||
result.setToolTip("Paste previously copied weights from clipboard")
|
||||
|
||||
@signal.on(session.events.currentLayerChanged)
|
||||
def on_selection_changed():
|
||||
result.setEnabled(session.state.currentLayer.layer is not None)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,90 @@
|
||||
from ngSkinTools2 import api, signal
|
||||
from ngSkinTools2.api.pyside import QtWidgets
|
||||
from ngSkinTools2.ui.options import PersistentValue
|
||||
|
||||
filter_normal_json = 'JSON files(*.json)'
|
||||
filter_compressed = 'Compressed JSON(*.json.gz)'
|
||||
file_dialog_filters = ";;".join([filter_normal_json, filter_compressed])
|
||||
|
||||
format_map = {
|
||||
filter_normal_json: api.FileFormat.JSON,
|
||||
filter_compressed: api.FileFormat.CompressedJSON,
|
||||
}
|
||||
|
||||
default_filter = PersistentValue("default_import_filter", default_value=api.FileFormat.JSON)
|
||||
|
||||
|
||||
def buildAction_export(session, parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def export_callback():
|
||||
file_name, selected_filter = QtWidgets.QFileDialog.getSaveFileName(
|
||||
parent, "Export to Json", filter=file_dialog_filters, selectedFilter=default_filter.get()
|
||||
)
|
||||
if not file_name:
|
||||
return
|
||||
|
||||
default_filter.set(selected_filter)
|
||||
|
||||
if session.state.layersAvailable:
|
||||
api.export_json(session.state.selectedSkinCluster, file_name, format=format_map[selected_filter])
|
||||
|
||||
result = actions.define_action(
|
||||
parent,
|
||||
"Export Layers to Json...",
|
||||
callback=export_callback,
|
||||
tooltip="Save layer info to external file, suitable for importing weights to different scene/mesh",
|
||||
)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=parent)
|
||||
def update_to_target():
|
||||
result.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_to_target()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def buildAction_import(session, parent, file_dialog_func=None):
|
||||
from ngSkinTools2.ui import actions
|
||||
from ngSkinTools2.ui.transferDialog import LayersTransfer, UiModel, open
|
||||
|
||||
def default_file_dialog_func():
|
||||
file_name, selected_filter = QtWidgets.QFileDialog.getOpenFileName(
|
||||
parent, "Import from Json", filter=file_dialog_filters, selectedFilter=default_filter.get()
|
||||
)
|
||||
if file_name:
|
||||
default_filter.set(selected_filter)
|
||||
return file_name, selected_filter
|
||||
|
||||
if file_dialog_func is None:
|
||||
file_dialog_func = default_file_dialog_func
|
||||
|
||||
def transfer_dialog(transfer):
|
||||
model = UiModel()
|
||||
model.transfer = transfer
|
||||
open(parent, model)
|
||||
|
||||
def import_callback():
|
||||
if session.state.selectedSkinCluster is None:
|
||||
return
|
||||
|
||||
file_name, selected_format = file_dialog_func()
|
||||
if not file_name:
|
||||
return
|
||||
|
||||
t = LayersTransfer()
|
||||
t.load_source_from_file(file_name, format=format_map[selected_format])
|
||||
t.target = session.state.selectedSkinCluster
|
||||
t.customize_callback = transfer_dialog
|
||||
t.execute()
|
||||
|
||||
result = actions.define_action(parent, "Import Layers from Json...", callback=import_callback, tooltip="Load previously exported weights")
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=parent)
|
||||
def update():
|
||||
result.setEnabled(session.state.selectedSkinCluster is not None)
|
||||
|
||||
update()
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,40 @@
|
||||
from ngSkinTools2 import api, signal
|
||||
|
||||
|
||||
def can_import(session):
|
||||
"""
|
||||
:type session: ngSkinTools2.api.session.Session
|
||||
"""
|
||||
|
||||
if not session.state.selection:
|
||||
return False
|
||||
|
||||
if session.state.layersAvailable:
|
||||
return False
|
||||
|
||||
return api.import_v1.can_import(session.state.selection[-1])
|
||||
|
||||
|
||||
def build_action_import_v1(session, parent):
|
||||
"""
|
||||
:type parent: PySide2.QtWidgets.QWidget
|
||||
:type session: ngSkinTools2.api.session.Session
|
||||
"""
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def do_convert():
|
||||
api.import_v1.import_layers(session.state.selection[-1])
|
||||
api.import_v1.cleanup(session.state.selection[-1:])
|
||||
update_state()
|
||||
session.events.targetChanged.emitIfChanged()
|
||||
|
||||
result = actions.define_action(parent, "Convert From v1.0 Layers", callback=do_convert)
|
||||
result.setToolTip("Convert skinning layers from previous version of ngSkinTools; after completing this action, v1 nodes will be deleted.")
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def update_state():
|
||||
result.setVisible(can_import(session))
|
||||
|
||||
update_state()
|
||||
|
||||
return result
|
||||
229
2023/scripts/rigging_tools/ngskintools2/operations/layers.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import random
|
||||
|
||||
from maya import cmds
|
||||
|
||||
import ngSkinTools2.api
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import Mirror
|
||||
from ngSkinTools2.api.layers import generate_layer_name
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QAction, QtCore
|
||||
from ngSkinTools2.api.session import session, withSession
|
||||
from ngSkinTools2.decorators import undoable
|
||||
from ngSkinTools2.ui import dialogs, qt
|
||||
from ngSkinTools2.ui.action import Action
|
||||
from ngSkinTools2.ui.options import config
|
||||
|
||||
logger = getLogger("layer operations")
|
||||
|
||||
|
||||
@withSession
|
||||
@undoable
|
||||
def initializeLayers(createFirstLayer=True):
|
||||
if session.state.layersAvailable:
|
||||
return # ignore
|
||||
|
||||
target = session.state.selectedSkinCluster
|
||||
|
||||
layers = ngSkinTools2.api.init_layers(target)
|
||||
with ngSkinTools2.api.suspend_updates(target):
|
||||
if createFirstLayer:
|
||||
layer = layers.add("Base weights")
|
||||
layer.set_current()
|
||||
Mirror(target).set_mirror_config(config.mirrorInfluencesDefaults)
|
||||
|
||||
session.events.targetChanged.emitIfChanged()
|
||||
|
||||
if ngSkinTools2.api.is_slow_mode_skin_cluster(target):
|
||||
dialogs.info(
|
||||
"ngSkinTools switched to slow maya API for setting skin cluster weights for this skinCluster, to workaround a Maya bug when skinCluster uses dg nodes as inputs"
|
||||
)
|
||||
|
||||
|
||||
@undoable
|
||||
def addLayer():
|
||||
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
|
||||
|
||||
def guessParent():
|
||||
currentLayer = layers.current_layer()
|
||||
if currentLayer is None:
|
||||
return None
|
||||
|
||||
# current layer is a parent?
|
||||
if currentLayer.num_children > 0:
|
||||
return currentLayer
|
||||
|
||||
return currentLayer.parent
|
||||
|
||||
with ngSkinTools2.api.suspend_updates(layers.mesh):
|
||||
new_layer = layers.add(generate_layer_name(session.state.all_layers, "New Layer"))
|
||||
new_layer.parent = guessParent()
|
||||
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
setCurrentLayer(new_layer)
|
||||
|
||||
return new_layer
|
||||
|
||||
|
||||
def build_action_initialize_layers(session, parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
from . import import_v1_actions
|
||||
|
||||
def do_initialize():
|
||||
if import_v1_actions.can_import(session):
|
||||
q = (
|
||||
"Skinning layers from previous version of ngSkinTools are present on this mesh. This operation will initialize "
|
||||
"skinning layers from scratch, discarding previous layers information. Do you want to continue?"
|
||||
)
|
||||
if not dialogs.yesNo(q):
|
||||
return
|
||||
|
||||
initializeLayers()
|
||||
|
||||
result = actions.define_action(parent, "Initialize Skinning Layers", callback=do_initialize)
|
||||
|
||||
@signal.on(session.events.nodeSelectionChanged)
|
||||
def update():
|
||||
result.setEnabled(session.state.selectedSkinCluster is not None)
|
||||
|
||||
update()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def buildAction_createLayer(session, parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
result = actions.define_action(parent, "Create Layer", callback=addLayer, icon=":/newLayerEmpty.png", shortcut=QtCore.Qt.Key_Insert)
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def update_to_target():
|
||||
result.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_to_target()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def buildAction_deleteLayer(session, parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
result = actions.define_action(parent, "Delete Layer", callback=deleteSelectedLayers, shortcut=QtCore.Qt.Key_Delete)
|
||||
|
||||
@signal.on(session.context.selected_layers.changed, session.events.targetChanged, qtParent=parent)
|
||||
def update_to_target():
|
||||
result.setEnabled(session.state.layersAvailable and bool(session.context.selected_layers(default=[])))
|
||||
|
||||
update_to_target()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@undoable
|
||||
def setCurrentLayer(layer):
|
||||
"""
|
||||
:type layer: ngSkinTools2.api.layers.Layer
|
||||
"""
|
||||
if not session.active():
|
||||
logger.info("didn't set current layer: no session")
|
||||
|
||||
if not session.state.layersAvailable:
|
||||
logger.info("didn't set current layer: layers not enabled")
|
||||
|
||||
logger.info("setting current layer to %r on %r", layer, session.state.selectedSkinCluster)
|
||||
layer.set_current()
|
||||
session.events.currentLayerChanged.emitIfChanged()
|
||||
|
||||
|
||||
def getCurrentLayer():
|
||||
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
|
||||
return layers.current_layer()
|
||||
|
||||
|
||||
@undoable
|
||||
def renameLayer(layer, newName):
|
||||
layer.name = newName
|
||||
cmds.evalDeferred(session.events.layerListChanged.emitIfChanged)
|
||||
|
||||
|
||||
@undoable
|
||||
def deleteSelectedLayers():
|
||||
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
|
||||
for i in session.context.selected_layers(default=[]):
|
||||
layers.delete(i)
|
||||
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
session.events.currentLayerChanged.emitIfChanged()
|
||||
|
||||
|
||||
class ToggleEnabledAction(Action):
|
||||
name = "Enabled"
|
||||
checkable = True
|
||||
|
||||
def __init__(self, session):
|
||||
Action.__init__(self, session)
|
||||
|
||||
def checked(self):
|
||||
"""
|
||||
return true if most of selected layers are enabled
|
||||
:return:
|
||||
"""
|
||||
layers = session.context.selected_layers(default=[])
|
||||
if not layers:
|
||||
return True
|
||||
|
||||
enabled_disabled_balance = 0
|
||||
for layer in layers:
|
||||
try:
|
||||
# eat up the exception if layer id is invalid
|
||||
enabled_disabled_balance += 1 if layer.enabled else -1
|
||||
except:
|
||||
pass
|
||||
|
||||
return enabled_disabled_balance >= 0
|
||||
|
||||
def run(self):
|
||||
enabled = not self.checked()
|
||||
selected_layers = session.context.selected_layers()
|
||||
if not selected_layers:
|
||||
return
|
||||
|
||||
for i in selected_layers:
|
||||
i.enabled = enabled
|
||||
|
||||
logger.info("layers toggled: %r", selected_layers)
|
||||
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
|
||||
def enabled(self):
|
||||
return session.state.layersAvailable and bool(session.context.selected_layers(default=[]))
|
||||
|
||||
def update_on_signals(self):
|
||||
return [session.context.selected_layers.changed, session.events.layerListChanged, session.events.targetChanged]
|
||||
|
||||
|
||||
def build_action_randomize_influences_colors(session, parent):
|
||||
"""
|
||||
builds a UI action for randomly choosing new colors for influences
|
||||
:type session: ngSkinTools2.api.session.Session
|
||||
"""
|
||||
|
||||
result = QAction("Randomize colors", parent)
|
||||
result.setToolTip("Choose random colors for each influence, selecting from Maya's pallete of indexed colors")
|
||||
|
||||
def color_filter(c):
|
||||
brightness = c[0] * c[0] + c[1] * c[1] + c[2] * c[2]
|
||||
return brightness > 0.001 and brightness < 0.99 # just a fancy way to skip white and black
|
||||
|
||||
colors = set([tuple(cmds.colorIndex(i, q=True)) for i in range(1, 30)])
|
||||
colors = [c for c in colors if color_filter(c)]
|
||||
|
||||
@qt.on(result.triggered)
|
||||
def triggered():
|
||||
if session.state.selectedSkinCluster is None:
|
||||
return
|
||||
layers = ngSkinTools2.api.Layers(session.state.selectedSkinCluster)
|
||||
layers.config.influence_colors = {i.logicalIndex: random.choice(colors) for i in layers.list_influences()}
|
||||
|
||||
return result
|
||||
45
2023/scripts/rigging_tools/ngskintools2/operations/paint.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api import PaintTool
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui.action import Action
|
||||
|
||||
log = getLogger("operations/paint")
|
||||
|
||||
|
||||
class FloodAction(Action):
|
||||
name = "Flood"
|
||||
tooltip = "Apply current brush to whole selection"
|
||||
|
||||
def run(self):
|
||||
session.paint_tool.flood(self.session.state.currentLayer.layer, influences=self.session.state.currentLayer.layer.paint_targets)
|
||||
|
||||
def enabled(self):
|
||||
return PaintTool.is_painting() and self.session.state.selectedSkinCluster is not None and self.session.state.currentLayer.layer is not None
|
||||
|
||||
def update_on_signals(self):
|
||||
return [
|
||||
self.session.events.toolChanged,
|
||||
self.session.events.currentLayerChanged,
|
||||
self.session.events.currentInfluenceChanged,
|
||||
self.session.events.targetChanged,
|
||||
]
|
||||
|
||||
|
||||
class PaintAction(Action):
|
||||
name = "Paint"
|
||||
tooltip = "Toggle paint tool"
|
||||
checkable = True
|
||||
|
||||
def run(self):
|
||||
if self.checked():
|
||||
cmds.setToolTo("moveSuperContext")
|
||||
else:
|
||||
self.session.paint_tool.start()
|
||||
|
||||
def update_on_signals(self):
|
||||
return [self.session.events.toolChanged]
|
||||
|
||||
def checked(self):
|
||||
return PaintTool.is_painting()
|
||||
@@ -0,0 +1,101 @@
|
||||
import itertools
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api import PaintTool, target_info
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.decorators import undoable
|
||||
|
||||
|
||||
def as_list(arg):
|
||||
return [] if arg is None else arg
|
||||
|
||||
|
||||
customNodeTypes = ['ngst2MeshDisplay', 'ngst2SkinLayerData']
|
||||
|
||||
|
||||
def list_custom_nodes():
|
||||
"""
|
||||
list all custom nodes in the scene
|
||||
"""
|
||||
|
||||
result = []
|
||||
for nodeType in customNodeTypes:
|
||||
result.extend(as_list(cmds.ls(type=nodeType)))
|
||||
return result
|
||||
|
||||
|
||||
def list_custom_nodes_for_mesh(mesh=None):
|
||||
"""
|
||||
list custom nodes only related to provided mesh. None means current selection
|
||||
"""
|
||||
|
||||
skin_cluster = target_info.get_related_skin_cluster(mesh)
|
||||
if skin_cluster is None:
|
||||
return []
|
||||
|
||||
# delete any ngSkinTools deformers from the history, and find upstream stuff from given skinCluster.
|
||||
hist = as_list(cmds.listHistory(skin_cluster, future=True, levels=1))
|
||||
return [i for i in hist if cmds.nodeType(i) in customNodeTypes]
|
||||
|
||||
|
||||
def list_custom_nodes_for_meshes(meshes):
|
||||
return list(itertools.chain.from_iterable([list_custom_nodes_for_mesh(i) for i in meshes]))
|
||||
|
||||
|
||||
message_scene_noCustomNodes = 'Scene does not contain any custom ngSkinTools nodes.'
|
||||
message_selection_noCustomNodes = 'Selection does not contain any custom ngSkinTools nodes.'
|
||||
message_scene_warning = (
|
||||
'This command deletes all custom ngSkinTools nodes. Skin weights ' 'will be preserved, but all layer data will be lost. Do you want to continue?'
|
||||
)
|
||||
message_selection_warning = (
|
||||
'This command deletes custom ngSkinTools nodes for selection. Skin weights '
|
||||
'will be preserved, but all layer data will be lost. Do you want to continue?'
|
||||
)
|
||||
|
||||
|
||||
@undoable
|
||||
def remove_custom_nodes(interactive=False, session=None, meshes=None):
|
||||
"""
|
||||
Removes custom ngSkinTools2 nodes from the scene or selection.
|
||||
|
||||
:type meshes: list[str]
|
||||
:param meshes: list of node names; if empty, operation will be scene-wide.
|
||||
:type session: Session
|
||||
:param session: optional; if specified, will fire events for current session about changed status of selected mesh
|
||||
:type interactive: bool
|
||||
:param interactive: if True, user warnings will be emited
|
||||
"""
|
||||
from ngSkinTools2.ui import dialogs
|
||||
|
||||
if meshes is None:
|
||||
meshes = []
|
||||
|
||||
is_selection_mode = len(meshes) > 0
|
||||
|
||||
custom_nodes = list_custom_nodes() if not is_selection_mode else list_custom_nodes_for_meshes(meshes)
|
||||
|
||||
if not custom_nodes:
|
||||
if interactive:
|
||||
dialogs.info(message_selection_noCustomNodes if is_selection_mode else message_scene_noCustomNodes)
|
||||
return
|
||||
|
||||
delete_confirmed = True
|
||||
if interactive:
|
||||
delete_confirmed = dialogs.yesNo(message_selection_warning if is_selection_mode else message_scene_warning)
|
||||
|
||||
if delete_confirmed:
|
||||
cmds.delete(custom_nodes)
|
||||
|
||||
if PaintTool.is_painting():
|
||||
# make sure that painting is canceled to restore mesh display etc
|
||||
cmds.setToolTo("Move")
|
||||
|
||||
if session is not None:
|
||||
session.events.targetChanged.emitIfChanged()
|
||||
|
||||
|
||||
@undoable
|
||||
def remove_custom_nodes_from_selection(interactive=False, session=None):
|
||||
selection = cmds.ls(sl=True)
|
||||
remove_custom_nodes(interactive=interactive, session=session, meshes=as_list(selection))
|
||||
295
2023/scripts/rigging_tools/ngskintools2/operations/tools.py
Normal file
@@ -0,0 +1,295 @@
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2 import api, signal
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.decorators import undoable
|
||||
from ngSkinTools2.observableValue import ObservableValue
|
||||
from ngSkinTools2.operations import layers
|
||||
from ngSkinTools2.ui import dialogs
|
||||
|
||||
logger = getLogger("operation/tools")
|
||||
|
||||
|
||||
def __create_tool_action__(parent, session, action_name, action_tooltip, exec_handler, enabled_handler=None):
|
||||
"""
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def execute():
|
||||
if not session.active():
|
||||
return
|
||||
|
||||
exec_handler()
|
||||
|
||||
result = actions.define_action(parent, action_name, callback=execute, tooltip=action_tooltip)
|
||||
|
||||
@signal.on(session.events.targetChanged, session.events.currentLayerChanged)
|
||||
def update_state():
|
||||
enabled = session.state.layersAvailable and session.state.currentLayer.layer is not None
|
||||
if enabled and enabled_handler is not None:
|
||||
enabled = enabled_handler(session.state.currentLayer.layer)
|
||||
result.setEnabled(enabled)
|
||||
|
||||
update_state()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ClosestJointOptions(Object):
|
||||
def __init__(self):
|
||||
self.create_new_layer = ObservableValue(False)
|
||||
self.all_influences = ObservableValue(True)
|
||||
|
||||
|
||||
def create_action__from_closest_joint(parent, session):
|
||||
options = ClosestJointOptions()
|
||||
|
||||
def exec_handler():
|
||||
layer = session.state.currentLayer.layer
|
||||
influences = None
|
||||
if not options.all_influences():
|
||||
influences = layer.paint_targets
|
||||
if not influences:
|
||||
dialogs.info("Select one or more influences in Influences list")
|
||||
return
|
||||
|
||||
if options.create_new_layer():
|
||||
layer = layers.addLayer()
|
||||
|
||||
api.assign_from_closest_joint(
|
||||
session.state.selectedSkinCluster,
|
||||
layer,
|
||||
influences=influences,
|
||||
)
|
||||
session.events.currentLayerChanged.emitIfChanged()
|
||||
session.events.influencesListUpdated.emit()
|
||||
|
||||
if layer.paint_target is None:
|
||||
used_influences = layer.get_used_influences()
|
||||
if used_influences:
|
||||
layer.paint_target = min(used_influences)
|
||||
|
||||
return (
|
||||
__create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Assign From Closest Joint",
|
||||
action_tooltip="Assign 1.0 weight for closest influence per each vertex in selected layer",
|
||||
exec_handler=exec_handler,
|
||||
),
|
||||
options,
|
||||
)
|
||||
|
||||
|
||||
class UnifyWeightsOptions(Object):
|
||||
overall_effect = ObservableValue(1.0)
|
||||
single_cluster_mode = ObservableValue(False)
|
||||
|
||||
|
||||
def create_action__unify_weights(parent, session):
|
||||
options = UnifyWeightsOptions()
|
||||
|
||||
def exec_handler():
|
||||
api.unify_weights(
|
||||
session.state.selectedSkinCluster,
|
||||
session.state.currentLayer.layer,
|
||||
overall_effect=options.overall_effect(),
|
||||
single_cluster_mode=options.single_cluster_mode(),
|
||||
)
|
||||
|
||||
return (
|
||||
__create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Unify Weights",
|
||||
action_tooltip="For selected vertices, make verts the same for all verts",
|
||||
exec_handler=exec_handler,
|
||||
),
|
||||
options,
|
||||
)
|
||||
|
||||
|
||||
def create_action__merge_layers(parent, session):
|
||||
"""
|
||||
:param parent: UI parent for this action
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
def exec_handler():
|
||||
api.merge_layers(layers=session.context.selected_layers(default=[]))
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
session.events.currentLayerChanged.emitIfChanged()
|
||||
|
||||
def enabled_handler(layer):
|
||||
return layer is not None and layer.index > 0
|
||||
|
||||
return __create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Merge",
|
||||
action_tooltip="Merge contents of this layer into underlying layer. Pre-effects weights will be used for this",
|
||||
exec_handler=exec_handler,
|
||||
enabled_handler=enabled_handler,
|
||||
)
|
||||
|
||||
|
||||
def create_action__duplicate_layer(parent, session):
|
||||
"""
|
||||
:param parent: UI parent for this action
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
@undoable
|
||||
def exec_handler():
|
||||
with api.suspend_updates(session.state.selectedSkinCluster):
|
||||
for source in session.context.selected_layers(default=[]):
|
||||
api.duplicate_layer(layer=source)
|
||||
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
session.events.currentLayerChanged.emitIfChanged()
|
||||
|
||||
return __create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Duplicate",
|
||||
action_tooltip="Duplicate selected layer(s)",
|
||||
exec_handler=exec_handler,
|
||||
)
|
||||
|
||||
|
||||
def create_action__fill_transparency(parent, session):
|
||||
"""
|
||||
:param parent: UI parent for this action
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
@undoable
|
||||
def exec_handler():
|
||||
with api.suspend_updates(session.state.selectedSkinCluster):
|
||||
for source in session.context.selected_layers(default=[]):
|
||||
api.fill_transparency(layer=source)
|
||||
|
||||
return __create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Fill Transparency",
|
||||
action_tooltip="All transparent vertices in the selected layer(s) receive weights from their closest non-empty neighbour vertex",
|
||||
exec_handler=exec_handler,
|
||||
)
|
||||
|
||||
|
||||
def create_action__copy_component_weights(parent, session):
|
||||
"""
|
||||
:param parent: UI parent for this action
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
def exec_handler():
|
||||
for source in session.context.selected_layers(default=[]):
|
||||
api.copy_component_weights(layer=source)
|
||||
|
||||
return __create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Copy Component Weights",
|
||||
action_tooltip="Store components weights in memory for further component-based paste actions",
|
||||
exec_handler=exec_handler,
|
||||
)
|
||||
|
||||
|
||||
def create_action__paste_average_component_weight(parent, session):
|
||||
"""
|
||||
:param parent: UI parent for this action
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
def exec_handler():
|
||||
for l in session.context.selected_layers(default=[]):
|
||||
api.paste_average_component_weights(layer=l)
|
||||
|
||||
return __create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Paste Average Component Weight",
|
||||
action_tooltip="Compute average of copied component weights and set that value to currently selected components",
|
||||
exec_handler=exec_handler,
|
||||
)
|
||||
|
||||
|
||||
def create_action__add_influences(parent, session):
|
||||
"""
|
||||
:param parent: UI parent for this action
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
def exec_handler():
|
||||
selection = cmds.ls(sl=True, l=True)
|
||||
if len(selection) < 2:
|
||||
logger.info("invalid selection: %s", selection)
|
||||
return
|
||||
api.add_influences(selection[:-1], selection[-1])
|
||||
cmds.select(selection[-1])
|
||||
session.events.influencesListUpdated.emit()
|
||||
|
||||
return __create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Add Influences",
|
||||
action_tooltip="Add selected influences to current skin cluster.",
|
||||
exec_handler=exec_handler,
|
||||
)
|
||||
|
||||
|
||||
def create_action__select_affected_vertices(parent, session):
|
||||
"""
|
||||
:param parent: UI parent for this action
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
def exec_handler():
|
||||
selected_layers = session.context.selected_layers(default=[])
|
||||
if not selected_layers:
|
||||
return
|
||||
|
||||
if not session.state.currentLayer.layer:
|
||||
return
|
||||
|
||||
influences = session.state.currentLayer.layer.paint_targets
|
||||
if not influences:
|
||||
return
|
||||
|
||||
non_zero_weights = []
|
||||
for layer in selected_layers:
|
||||
for i in influences:
|
||||
weights = layer.get_weights(i)
|
||||
if weights:
|
||||
non_zero_weights.append(weights)
|
||||
|
||||
if not non_zero_weights:
|
||||
return
|
||||
|
||||
current_selection = cmds.ls(sl=True, o=True, l=True)
|
||||
if len(current_selection) != 1:
|
||||
return
|
||||
|
||||
# we're not sure - this won't work if skin cluster is selected directly
|
||||
selected_mesh_probably = current_selection[0]
|
||||
|
||||
combined_weights = [sum(i) for i in zip(*non_zero_weights)]
|
||||
indexes = [selected_mesh_probably + ".vtx[%d]" % index for index, i in enumerate(combined_weights) if i > 0.00001]
|
||||
try:
|
||||
cmds.select(indexes)
|
||||
except:
|
||||
pass
|
||||
|
||||
return __create_tool_action__(
|
||||
parent,
|
||||
session,
|
||||
action_name=u"Select Affected Vertices",
|
||||
action_tooltip="Select vertices that have non-zero weight for current influence.",
|
||||
exec_handler=exec_handler,
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
def website_base_url():
|
||||
import ngSkinTools2
|
||||
|
||||
if ngSkinTools2.DEBUG_MODE:
|
||||
return "http://localhost:1313"
|
||||
|
||||
return "https://www.ngskintools.com"
|
||||
|
||||
|
||||
class WebsiteLinksActions(Object):
|
||||
def __init__(self, parent):
|
||||
self.api_root = make_documentation_action(parent, "API Documentation", "/v2/api")
|
||||
self.user_guide = make_documentation_action(parent, "User Guide", "/v2/")
|
||||
self.changelog = make_documentation_action(parent, "Change Log", "/v2/changelog", icon=None)
|
||||
self.contact = make_documentation_action(parent, "Contact", "/contact/", icon=None)
|
||||
|
||||
|
||||
def make_documentation_action(parent, title, url, icon=":/help.png"):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def handler():
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open_new(website_base_url() + url)
|
||||
|
||||
result = actions.define_action(parent, title, callback=handler, icon=icon)
|
||||
result.setToolTip("opens {0} in a browser".format(url))
|
||||
return result
|
||||
63
2023/scripts/rigging_tools/ngskintools2/pluginCallbacks.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
these are methods that are called by plugin when corresponding events happen
|
||||
"""
|
||||
from ngSkinTools2 import api
|
||||
from ngSkinTools2.api import eventtypes as et
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import hotkeys_setup
|
||||
|
||||
log = getLogger("plugin callbacks")
|
||||
|
||||
|
||||
def current_paint_target_changed():
|
||||
# log.info("current paint target changed")
|
||||
if session.active():
|
||||
if session.state.currentLayer.layer is not None:
|
||||
session.state.currentLayer.layer.reload()
|
||||
session.events.currentInfluenceChanged.emitIfChanged()
|
||||
|
||||
|
||||
def tool_settings_changed():
|
||||
et.tool_settings_changed.emit()
|
||||
|
||||
|
||||
def paint_tool_started():
|
||||
api.paint.tabletEventFilter.install()
|
||||
hotkeys_setup.toggle_paint_hotkey_set(enabled=True)
|
||||
|
||||
|
||||
def paint_tool_stopped():
|
||||
api.paint.tabletEventFilter.uninstall()
|
||||
hotkeys_setup.toggle_paint_hotkey_set(enabled=False)
|
||||
|
||||
|
||||
def get_stylus_intensity():
|
||||
return api.paint.tabletEventFilter.pressure
|
||||
|
||||
|
||||
def initialize_influences_mirror_mapping(mesh):
|
||||
"""
|
||||
gets called by plugin when influences mirror mapping is not set yet
|
||||
:return:
|
||||
"""
|
||||
from ngSkinTools2.api.mirror import Mirror
|
||||
|
||||
Mirror(mesh).recalculate_influences_mapping()
|
||||
|
||||
|
||||
def display_node_created(display_node):
|
||||
"""
|
||||
gets called when display node is created
|
||||
|
||||
:param display_node:
|
||||
:return:
|
||||
"""
|
||||
|
||||
from maya import cmds
|
||||
|
||||
# add node to isolated objects if we're currently in isolated mode
|
||||
current_view = cmds.paneLayout('viewPanes', q=True, pane1=True)
|
||||
is_isolated = cmds.isolateSelect(current_view, q=True, state=True)
|
||||
if is_isolated:
|
||||
cmds.isolateSelect(current_view, addDagObject=display_node)
|
||||
253
2023/scripts/rigging_tools/ngskintools2/signal.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Signal is the previous method of emiting/subscribing to signals.
|
||||
* Subscribers are dependant on emiter's instances
|
||||
* There's only one global queue
|
||||
|
||||
New system is changing to allow decoupling subscribers and receivers, and allowing the code to work even when there's no need to process signals
|
||||
* Subscribers need to be able to subsribe prior to instantiation of emitters
|
||||
|
||||
|
||||
"""
|
||||
from functools import partial
|
||||
|
||||
from ngSkinTools2 import cleanup
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
log = getLogger("signal")
|
||||
|
||||
|
||||
class SignalQueue(Object):
|
||||
def __init__(self):
|
||||
self.max_length = 100
|
||||
self.queue = []
|
||||
|
||||
def emit(self, handler):
|
||||
if len(self.queue) > self.max_length:
|
||||
log.error("queue max length reached: emitting too many events?")
|
||||
raise Exception("queue max length reached: emitting too many events?")
|
||||
|
||||
should_start = len(self.queue) == 0
|
||||
|
||||
self.queue.append(handler)
|
||||
if should_start:
|
||||
self.process()
|
||||
|
||||
def process(self):
|
||||
current_handler = 0
|
||||
queue = self.queue
|
||||
|
||||
while current_handler < len(queue):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
queue[current_handler]()
|
||||
except Exception:
|
||||
import ngSkinTools2
|
||||
|
||||
if ngSkinTools2.DEBUG_MODE:
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
traceback.print_exc(file=sys.__stderr__)
|
||||
|
||||
current_handler += 1
|
||||
|
||||
if len(self.queue) > 50:
|
||||
log.info("handler queue finished with %d items", len(self.queue))
|
||||
self.queue = []
|
||||
|
||||
|
||||
# noinspection PyBroadException
|
||||
class Signal(Object):
|
||||
"""
|
||||
Signal class collects observers, interested in some particular event,and handles
|
||||
signaling them all when some event occurs. Both handling and signaling happens outside
|
||||
of signal's own code
|
||||
|
||||
|
||||
Handlers are processed breath first, in a queue based system.
|
||||
1. root signal fires, adds all it's handlers to the queue;
|
||||
2. queue starts being processed
|
||||
3. handlers fire more signals, in turn adding more handlers to the end of the queue.
|
||||
"""
|
||||
|
||||
all = []
|
||||
|
||||
queue = SignalQueue()
|
||||
|
||||
def __init__(self, name):
|
||||
if name is None:
|
||||
raise Exception("need name for debug purposes later")
|
||||
self.name = name
|
||||
self.handlers = []
|
||||
self.executing = False
|
||||
self.enabled = True
|
||||
|
||||
self.reset()
|
||||
Signal.all.append(self)
|
||||
cleanup.registerCleanupHandler(self.reset)
|
||||
|
||||
def reset(self):
|
||||
self.handlers = []
|
||||
self.executing = False
|
||||
|
||||
def emit_deferred(self, *args):
|
||||
import maya.utils as mu
|
||||
|
||||
mu.executeDeferred(self.emit, *args)
|
||||
|
||||
def emit(self, *args):
|
||||
"""
|
||||
emit mostly just adds handlers to the processing queue,
|
||||
but if nobody is processing handlers at the emit time,
|
||||
it is started here as well.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
# log.info("emit: %s", self.name)
|
||||
if self.executing:
|
||||
raise Exception('Nested emit on %s detected' % self.name)
|
||||
|
||||
for i in self.handlers[:]:
|
||||
Signal.queue.emit(partial(i, *args))
|
||||
|
||||
def addHandler(self, handler, qtParent=None):
|
||||
if hasattr(handler, 'emit'):
|
||||
handler = handler.emit
|
||||
|
||||
self.handlers.append(handler)
|
||||
|
||||
def remove():
|
||||
return self.removeHandler(handler)
|
||||
|
||||
if qtParent is not None:
|
||||
qtParent.destroyed.connect(remove)
|
||||
|
||||
return remove
|
||||
|
||||
def removeHandler(self, handler):
|
||||
try:
|
||||
self.handlers.remove(handler)
|
||||
except ValueError:
|
||||
# not found in list? no biggie.
|
||||
pass
|
||||
|
||||
|
||||
def on(*signals, **kwargs):
|
||||
"""
|
||||
decorator for function: list signals that should fire for this function.
|
||||
|
||||
@signal.on(signalReference)
|
||||
def something():
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(fn):
|
||||
for i in signals:
|
||||
i.addHandler(fn, **kwargs)
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# --------------------------------------
|
||||
# rework:
|
||||
# * decoupled emitters and subscribers
|
||||
# * subscribers are resolved only at the time of event
|
||||
# * can emit events even when there's no active sessions (acts as no-op)
|
||||
|
||||
|
||||
class Event(Object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __or__(self, other):
|
||||
return EventList() | self | other
|
||||
|
||||
def __iter__(self):
|
||||
yield self
|
||||
|
||||
def emit(self, *args, **kwargs):
|
||||
for hub in SignalHub.active_hubs:
|
||||
hub.emit(self, *args, **kwargs)
|
||||
|
||||
|
||||
class EventList(Object):
|
||||
"""
|
||||
helper to build and iterate over a list of "or"-ed events
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
def __or__(self, other):
|
||||
self.items.append(other)
|
||||
return self
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.items)
|
||||
|
||||
|
||||
class SignalHub(Object):
|
||||
active_hubs = set()
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.queue = SignalQueue()
|
||||
|
||||
def activate(self):
|
||||
self.active_hubs.add(self)
|
||||
|
||||
def deactivate(self):
|
||||
self.active_hubs.remove(self)
|
||||
|
||||
def subscribe(self, event, handler):
|
||||
"""
|
||||
:param Event event: event to subscribe to
|
||||
:param Callable handler: callback which will be called when event is emitted
|
||||
:return: unsubscribe function: call it to terminate this subscription
|
||||
"""
|
||||
self.handlers.setdefault(event, []).append(handler)
|
||||
|
||||
def unsubscribe():
|
||||
try:
|
||||
self.handlers[event].remove(handler)
|
||||
except ValueError:
|
||||
# not found in list? no biggie.
|
||||
pass
|
||||
|
||||
return unsubscribe
|
||||
|
||||
def emit(self, event):
|
||||
if not event in self.handlers:
|
||||
return
|
||||
|
||||
for i in self.handlers[event]:
|
||||
self.queue.emit(i)
|
||||
|
||||
def on(self, events, scope=None):
|
||||
"""
|
||||
decorator for function: bind function to signals
|
||||
|
||||
@hub.on(event1 | event2, scope=qt_object)
|
||||
def something():
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(fn):
|
||||
unsubscribe_handlers = []
|
||||
try:
|
||||
unsubscribe_handlers.append(scope.destroyed.connect)
|
||||
except:
|
||||
pass
|
||||
|
||||
for e in events:
|
||||
unsub = self.subscribe(e, fn)
|
||||
if unsubscribe_handlers:
|
||||
for i in unsubscribe_handlers:
|
||||
i(unsub)
|
||||
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
91
2023/scripts/rigging_tools/ngskintools2/ui/aboutwindow.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
from xml.sax.saxutils import escape as escape
|
||||
|
||||
from ngSkinTools2 import cleanup, version
|
||||
from ngSkinTools2.api.pyside import Qt, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def show(parent):
|
||||
"""
|
||||
:type parent: QWidget
|
||||
"""
|
||||
|
||||
def header():
|
||||
# noinspection PyShadowingNames
|
||||
def leftSide():
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addStretch()
|
||||
layout.addWidget(QtWidgets.QLabel("<h1>ngSkinTools</h1>"))
|
||||
layout.addWidget(QtWidgets.QLabel("Version {0}".format(version.pluginVersion())))
|
||||
layout.addWidget(QtWidgets.QLabel(version.COPYRIGHT))
|
||||
|
||||
url = QtWidgets.QLabel('<a href="{0}" style="color: #007bff;">{0}</a>'.format(version.PRODUCT_URL))
|
||||
url.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
url.setOpenExternalLinks(True)
|
||||
layout.addWidget(url)
|
||||
layout.addStretch()
|
||||
return layout
|
||||
|
||||
def logo():
|
||||
from ngSkinTools2.api.pyside import QSvgWidget
|
||||
|
||||
w = QSvgWidget(os.path.join(os.path.dirname(__file__), "images", "logo.svg"))
|
||||
|
||||
w.setFixedSize(*((70 * scale_multiplier,) * 2))
|
||||
layout.addWidget(w)
|
||||
return w
|
||||
|
||||
result = QtWidgets.QWidget()
|
||||
result.setPalette(qt.alternative_palette_light())
|
||||
result.setAutoFillBackground(True)
|
||||
|
||||
hSplit = QtWidgets.QHBoxLayout()
|
||||
hSplit.setContentsMargins(30, 30, 30, 30)
|
||||
result.setLayout(hSplit)
|
||||
|
||||
hSplit.addLayout(leftSide())
|
||||
hSplit.addStretch()
|
||||
hSplit.addWidget(logo())
|
||||
|
||||
return result
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def body():
|
||||
result = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.setContentsMargins(30, 30, 30, 30)
|
||||
|
||||
return result
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def buttonsRow(window):
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.addStretch()
|
||||
btnClose = QtWidgets.QPushButton("Close")
|
||||
btnClose.setMinimumWidth(100 * scale_multiplier)
|
||||
layout.addWidget(btnClose)
|
||||
layout.setContentsMargins(20 * scale_multiplier, 15 * scale_multiplier, 20 * scale_multiplier, 15 * scale_multiplier)
|
||||
|
||||
btnClose.clicked.connect(lambda: window.close())
|
||||
return layout
|
||||
|
||||
window = QtWidgets.QWidget(parent, Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
|
||||
window.resize(600 * scale_multiplier, 500 * scale_multiplier)
|
||||
window.setAttribute(Qt.WA_DeleteOnClose)
|
||||
window.setWindowTitle("About ngSkinTools")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
window.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout.addWidget(header())
|
||||
layout.addWidget(body())
|
||||
layout.addStretch(2)
|
||||
layout.addLayout(buttonsRow(window))
|
||||
|
||||
window.show()
|
||||
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
57
2023/scripts/rigging_tools/ngskintools2/ui/action.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.api.session import Session
|
||||
|
||||
|
||||
class Action(Object):
|
||||
name = "Action"
|
||||
tooltip = ""
|
||||
checkable = False
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session # type: Session
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
def enabled(self):
|
||||
return True
|
||||
|
||||
def checked(self):
|
||||
return False
|
||||
|
||||
def run_if_enabled(self):
|
||||
if self.enabled():
|
||||
self.run()
|
||||
|
||||
def update_on_signals(self):
|
||||
return []
|
||||
|
||||
def as_qt_action(self, parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
result = actions.define_action(parent, self.name, callback=self.run_if_enabled, tooltip=self.tooltip)
|
||||
result.setCheckable(self.checkable)
|
||||
|
||||
def update():
|
||||
result.setEnabled(self.enabled())
|
||||
if self.checkable:
|
||||
result.setChecked(self.checked())
|
||||
|
||||
signal.on(*self.update_on_signals(), qtParent=parent)(update)
|
||||
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
def qt_action(action_class, session, parent):
|
||||
"""
|
||||
Wrap provided action_class into a QT action
|
||||
"""
|
||||
return action_class(session).as_qt_action(parent)
|
||||
|
||||
|
||||
def do_action_hotkey(action_class):
|
||||
from ngSkinTools2.api import session
|
||||
|
||||
action_class(session.session).run_if_enabled()
|
||||
146
2023/scripts/rigging_tools/ngskintools2/ui/actions.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import PasteOperation
|
||||
from ngSkinTools2.api.pyside import QAction, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.operations import import_export_actions, import_v1_actions
|
||||
from ngSkinTools2.operations.layers import (
|
||||
ToggleEnabledAction,
|
||||
build_action_initialize_layers,
|
||||
)
|
||||
from ngSkinTools2.operations.paint import FloodAction, PaintAction
|
||||
from ngSkinTools2.operations.website_links import WebsiteLinksActions
|
||||
from ngSkinTools2.ui import action
|
||||
from ngSkinTools2.ui.updatewindow import build_action_check_for_updates
|
||||
|
||||
|
||||
def define_action(parent, label, callback=None, icon=None, shortcut=None, tooltip=None):
|
||||
result = QAction(label, parent)
|
||||
if icon is not None:
|
||||
result.setIcon(QtGui.QIcon(icon))
|
||||
if callback is not None:
|
||||
result.triggered.connect(callback)
|
||||
if shortcut is not None:
|
||||
if not isinstance(shortcut, QtGui.QKeySequence):
|
||||
shortcut = QtGui.QKeySequence(shortcut)
|
||||
result.setShortcut(shortcut)
|
||||
if tooltip is not None:
|
||||
result.setToolTip(tooltip)
|
||||
result.setStatusTip(tooltip)
|
||||
return result
|
||||
|
||||
|
||||
def build_action_delete_custom_nodes_for_selection(parent, session):
|
||||
from ngSkinTools2.operations import removeLayerData
|
||||
|
||||
result = define_action(
|
||||
parent,
|
||||
"Delete Custom Nodes For Selection",
|
||||
callback=lambda: removeLayerData.remove_custom_nodes_from_selection(interactive=True, session=session),
|
||||
)
|
||||
|
||||
@signal.on(session.events.nodeSelectionChanged)
|
||||
def update():
|
||||
result.setEnabled(bool(session.state.selection))
|
||||
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
class Actions(Object):
|
||||
def separator(self, parent, label=""):
|
||||
separator = QAction(parent)
|
||||
separator.setText(label)
|
||||
separator.setSeparator(True)
|
||||
return separator
|
||||
|
||||
def __init__(self, parent, session):
|
||||
"""
|
||||
:type session: Session
|
||||
"""
|
||||
qt_action = lambda a: action.qt_action(a, session, parent)
|
||||
from ngSkinTools2.operations import layers, removeLayerData, tools
|
||||
from ngSkinTools2.ui.transferDialog import build_transfer_action
|
||||
|
||||
self.initialize = build_action_initialize_layers(session, parent)
|
||||
self.exportFile = import_export_actions.buildAction_export(session, parent)
|
||||
self.importFile = import_export_actions.buildAction_import(session, parent)
|
||||
self.import_v1 = import_v1_actions.build_action_import_v1(session, parent)
|
||||
|
||||
self.addLayer = layers.buildAction_createLayer(session, parent)
|
||||
self.deleteLayer = layers.buildAction_deleteLayer(session, parent)
|
||||
self.toggle_layer_enabled = qt_action(ToggleEnabledAction)
|
||||
|
||||
# self.moveLayerUp = defineCallbackAction(u"Move Layer Up", None, icon=":/moveLayerUp.png")
|
||||
# self.moveLayerDown = defineCallbackAction(u"Move Layer Down", None, icon=":/moveLayerDown.png")
|
||||
self.paint = qt_action(PaintAction)
|
||||
self.flood = qt_action(FloodAction)
|
||||
|
||||
self.toolsAssignFromClosestJoint, self.toolsAssignFromClosestJointOptions = tools.create_action__from_closest_joint(parent, session)
|
||||
(
|
||||
self.toolsAssignFromClosestJointSelectedInfluences,
|
||||
self.toolsAssignFromClosestJointOptionsSelectedInfluences,
|
||||
) = tools.create_action__from_closest_joint(parent, session)
|
||||
self.toolsAssignFromClosestJointOptionsSelectedInfluences.all_influences.set(False)
|
||||
self.toolsAssignFromClosestJointOptionsSelectedInfluences.create_new_layer.set(False)
|
||||
self.toolsUnifyWeights, self.toolsUnifyWeightsOptions = tools.create_action__unify_weights(parent, session)
|
||||
|
||||
self.toolsDeleteCustomNodes = define_action(
|
||||
parent, "Delete All Custom Nodes", callback=lambda: removeLayerData.remove_custom_nodes(interactive=True, session=session)
|
||||
)
|
||||
|
||||
self.toolsDeleteCustomNodesOnSelection = build_action_delete_custom_nodes_for_selection(parent, session)
|
||||
|
||||
self.transfer = build_transfer_action(session=session, parent=parent)
|
||||
|
||||
# self.setLayerMirrored = defineAction(u"Mirrored", icon=":/polyMirrorGeometry.png")
|
||||
# self.setLayerMirrored.setCheckable(True)
|
||||
|
||||
self.documentation = WebsiteLinksActions(parent=parent)
|
||||
|
||||
self.check_for_updates = build_action_check_for_updates(parent=parent)
|
||||
|
||||
from ngSkinTools2.operations import copy_paste_actions
|
||||
|
||||
self.cut_influences = copy_paste_actions.action_copy_cut(session, parent, True)
|
||||
self.copy_influences = copy_paste_actions.action_copy_cut(session, parent, False)
|
||||
self.paste_weights = copy_paste_actions.action_paste(session, parent, PasteOperation.replace)
|
||||
self.paste_weights_add = copy_paste_actions.action_paste(session, parent, PasteOperation.add)
|
||||
self.paste_weights_sub = copy_paste_actions.action_paste(session, parent, PasteOperation.subtract)
|
||||
|
||||
self.copy_components = tools.create_action__copy_component_weights(parent=parent, session=session)
|
||||
self.paste_component_average = tools.create_action__paste_average_component_weight(parent=parent, session=session)
|
||||
|
||||
self.merge_layer = tools.create_action__merge_layers(parent=parent, session=session)
|
||||
self.duplicate_layer = tools.create_action__duplicate_layer(parent=parent, session=session)
|
||||
self.fill_layer_transparency = tools.create_action__fill_transparency(parent=parent, session=session)
|
||||
|
||||
self.add_influences = tools.create_action__add_influences(parent=parent, session=session)
|
||||
from ngSkinTools2.ui import influencesview
|
||||
|
||||
self.show_used_influences_only = influencesview.build_used_influences_action(parent)
|
||||
self.set_influences_sorted = influencesview.build_set_influences_sorted_action(parent)
|
||||
self.randomize_influences_colors = layers.build_action_randomize_influences_colors(parent=parent, session=session)
|
||||
|
||||
self.select_affected_vertices = tools.create_action__select_affected_vertices(parent=parent, session=session)
|
||||
|
||||
def addLayersActions(self, context):
|
||||
context.addAction(self.addLayer)
|
||||
context.addAction(self.deleteLayer)
|
||||
context.addAction(self.separator(context))
|
||||
context.addAction(self.merge_layer)
|
||||
context.addAction(self.duplicate_layer)
|
||||
context.addAction(self.fill_layer_transparency)
|
||||
context.addAction(self.separator(context))
|
||||
context.addAction(self.toggle_layer_enabled)
|
||||
|
||||
def addInfluencesActions(self, context):
|
||||
context.addAction(self.separator(context, "Actions"))
|
||||
context.addAction(self.toolsAssignFromClosestJointSelectedInfluences)
|
||||
context.addAction(self.select_affected_vertices)
|
||||
context.addAction(self.separator(context, "Clipboard"))
|
||||
context.addAction(self.cut_influences)
|
||||
context.addAction(self.copy_influences)
|
||||
context.addAction(self.paste_weights)
|
||||
context.addAction(self.paste_weights_add)
|
||||
context.addAction(self.paste_weights_sub)
|
||||
@@ -0,0 +1,66 @@
|
||||
from ngSkinTools2.api import PaintTool
|
||||
from ngSkinTools2.api.paint import popups
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def brush_settings_popup(paint):
|
||||
"""
|
||||
|
||||
:type paint: PaintTool
|
||||
"""
|
||||
window = QtWidgets.QWidget(qt.mainWindow)
|
||||
window.setWindowFlags(QtCore.Qt.Popup | QtCore.Qt.FramelessWindowHint)
|
||||
window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
spacing = 5
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setSpacing(spacing)
|
||||
|
||||
intensity_slider = widgets.NumberSliderGroup()
|
||||
widgets.set_paint_expo(intensity_slider, paint.paint_mode)
|
||||
intensity_slider.set_value(paint.intensity)
|
||||
|
||||
@qt.on(intensity_slider.slider.sliderReleased, intensity_slider.spinner.editingFinished)
|
||||
def close_with_slider_intensity():
|
||||
close_with_intensity(intensity_slider.value())
|
||||
|
||||
def close_with_intensity(value):
|
||||
paint.intensity = value
|
||||
window.close()
|
||||
|
||||
def create_intensity_button(intensity):
|
||||
btn = QtWidgets.QPushButton("{0:.3g}".format(intensity))
|
||||
btn.clicked.connect(lambda: close_with_intensity(intensity))
|
||||
btn.setMinimumWidth(60 * scale_multiplier)
|
||||
btn.setMinimumHeight(30 * scale_multiplier)
|
||||
return btn
|
||||
|
||||
layout.addLayout(intensity_slider.layout())
|
||||
|
||||
for values in [(0.0, 1.0), (0.25, 0.5, 0.75), (0.025, 0.05, 0.075, 0.1, 0.125)]:
|
||||
row = QtWidgets.QHBoxLayout()
|
||||
row.setSpacing(spacing)
|
||||
for v in values:
|
||||
row.addWidget(create_intensity_button(v))
|
||||
layout.addLayout(row)
|
||||
|
||||
group = QtWidgets.QGroupBox("Brush Intensity")
|
||||
group.setLayout(layout)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(4 * scale_multiplier, 4 * scale_multiplier, 4 * scale_multiplier, 4 * scale_multiplier)
|
||||
layout.addWidget(group)
|
||||
|
||||
window.setLayout(layout)
|
||||
|
||||
window.show()
|
||||
mp = QtGui.QCursor.pos()
|
||||
window.move(mp.x() - window.size().width() / 2, mp.y() - window.size().height() / 2)
|
||||
|
||||
window.activateWindow()
|
||||
|
||||
popups.close_all()
|
||||
popups.add(window)
|
||||
57
2023/scripts/rigging_tools/ngskintools2/ui/dialogs.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from maya import OpenMaya as om
|
||||
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
|
||||
openDialogs = []
|
||||
messagesCallbacks = []
|
||||
|
||||
# main window will set itself here
|
||||
promptsParent = None
|
||||
|
||||
|
||||
def __baseMessageBox(message):
|
||||
msg = QtWidgets.QMessageBox(promptsParent)
|
||||
msg.setWindowTitle("ngSkinTools2")
|
||||
msg.setText(message)
|
||||
|
||||
for i in messagesCallbacks:
|
||||
i(message)
|
||||
|
||||
openDialogs.append(msg)
|
||||
return msg
|
||||
|
||||
|
||||
def displayError(message):
|
||||
"""
|
||||
displays error in script editor and in a dialog box
|
||||
"""
|
||||
|
||||
message = str(message)
|
||||
om.MGlobal.displayError('[ngSkinTools2] ' + message)
|
||||
|
||||
msg = __baseMessageBox(message)
|
||||
msg.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
msg.exec_()
|
||||
|
||||
|
||||
def info(message):
|
||||
msg = __baseMessageBox(message)
|
||||
msg.setIcon(QtWidgets.QMessageBox.Information)
|
||||
msg.exec_()
|
||||
|
||||
|
||||
def yesNo(message):
|
||||
msg = __baseMessageBox(message)
|
||||
msg.setIcon(QtWidgets.QMessageBox.Question)
|
||||
msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
|
||||
msg.setDefaultButton(QtWidgets.QMessageBox.Yes)
|
||||
return msg.exec_() == QtWidgets.QMessageBox.Yes
|
||||
|
||||
|
||||
def closeAllAfterTimeout(timeout, result=0):
|
||||
def closeAll():
|
||||
while openDialogs:
|
||||
msg = openDialogs.pop()
|
||||
msg.done(result)
|
||||
|
||||
QtCore.QTimer.singleShot(timeout, closeAll)
|
||||
86
2023/scripts/rigging_tools/ngskintools2/ui/hotkeys.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Global list of hotkey-able functions within the plugin.
|
||||
|
||||
|
||||
These functions will be embedded in end-user's hotkey setup by absolute path (package.function_name)
|
||||
so the names should not fluctuate.
|
||||
"""
|
||||
|
||||
from ngSkinTools2.api import NamedPaintTarget, PaintTool, WeightsDisplayMode, plugin
|
||||
from ngSkinTools2.api.paint import MaskDisplayMode
|
||||
from ngSkinTools2.api.session import session, withSession
|
||||
from ngSkinTools2.operations.paint import FloodAction, PaintAction
|
||||
from ngSkinTools2.ui.action import do_action_hotkey
|
||||
|
||||
|
||||
def paint_tool_start():
|
||||
do_action_hotkey(PaintAction)
|
||||
|
||||
|
||||
def paint_tool_toggle_help():
|
||||
plugin.ngst2_hotkey(paintContextToggleHelp=True)
|
||||
|
||||
|
||||
def paint_tool_flood():
|
||||
do_action_hotkey(FloodAction)
|
||||
|
||||
|
||||
def paint_tool_focus_current_influence():
|
||||
plugin.ngst2_hotkey(paintContextViewFit=True)
|
||||
|
||||
|
||||
def paint_tool_brush_size():
|
||||
plugin.ngst2_hotkey(paintContextBrushSize=True)
|
||||
|
||||
|
||||
def paint_tool_brush_size_release():
|
||||
plugin.ngst2_hotkey(paintContextBrushSize=False)
|
||||
|
||||
|
||||
def paint_tool_sample_influence():
|
||||
plugin.ngst2_hotkey(paintContextSampleInfluence=True)
|
||||
|
||||
|
||||
def paint_tool_sample_influence_release():
|
||||
plugin.ngst2_hotkey(paintContextSampleInfluence=False)
|
||||
|
||||
|
||||
@withSession
|
||||
def select_paint_brush_intensity():
|
||||
from ngSkinTools2.ui.brush_settings_popup import brush_settings_popup
|
||||
|
||||
brush_settings_popup(session.paint_tool)
|
||||
|
||||
|
||||
@withSession
|
||||
def paint_tool_toggle_original_mesh():
|
||||
paint = session.paint_tool
|
||||
paint.display_settings.display_node_visible = not paint.display_settings.display_node_visible
|
||||
session.events.toolChanged.emit()
|
||||
|
||||
|
||||
@withSession
|
||||
def paint_tool_cycle_weights_display_mode():
|
||||
"""
|
||||
cycle current display mode "all influences" -> "current influence" -> "current influence colored"
|
||||
:return:
|
||||
"""
|
||||
paint = session.paint_tool
|
||||
|
||||
targets = session.state.currentInfluence.targets
|
||||
is_mask_mode = targets is not None and len(targets) == 1 and targets[0] == NamedPaintTarget.MASK
|
||||
|
||||
settings = paint.display_settings
|
||||
if is_mask_mode:
|
||||
settings.mask_display_mode = {
|
||||
MaskDisplayMode.default_: MaskDisplayMode.color_ramp,
|
||||
MaskDisplayMode.color_ramp: MaskDisplayMode.default_,
|
||||
}.get(settings.mask_display_mode, MaskDisplayMode.default_)
|
||||
else:
|
||||
settings.weights_display_mode = {
|
||||
WeightsDisplayMode.allInfluences: WeightsDisplayMode.currentInfluence,
|
||||
WeightsDisplayMode.currentInfluence: WeightsDisplayMode.currentInfluenceColored,
|
||||
WeightsDisplayMode.currentInfluenceColored: WeightsDisplayMode.allInfluences,
|
||||
}.get(settings.weights_display_mode, WeightsDisplayMode.allInfluences)
|
||||
|
||||
session.events.toolChanged.emit()
|
||||
200
2023/scripts/rigging_tools/ngskintools2/ui/hotkeys_setup.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Maya internals dissection, comments are ours. similar example available in "command|hotkey code", see "Here's an example
|
||||
of how to create runtimeCommand with a certain hotkey context"
|
||||
|
||||
|
||||
```
|
||||
|
||||
// add new hotkey ctx
|
||||
// t: Specifies the context type. It's used together with the other flags such as "currentClient", "addClient",
|
||||
// "removeClient" and so on.
|
||||
// ac: Associates a client to the given hotkey context type. This flag needs to be used with the flag "type" which
|
||||
// specifies the context type.
|
||||
hotkeyCtx -t "Tool" -ac "sculptMeshCache";
|
||||
|
||||
// create new runtime command, associate with created context
|
||||
runTimeCommand -default true
|
||||
-annotation (uiRes("m_defaultRunTimeCommands.kModifySizePressAnnot"))
|
||||
-category ("Other items.Brush Tools")
|
||||
-command ("if ( `contextInfo -ex sculptMeshCacheContext`) sculptMeshCacheCtx -e -adjustSize 1 sculptMeshCacheContext;")
|
||||
-hotkeyCtx ("sculptMeshCache")
|
||||
SculptMeshActivateBrushSize;
|
||||
|
||||
// create named command for the runtime command
|
||||
nameCommand
|
||||
-annotation "Start adjust size"
|
||||
-command ("SculptMeshActivateBrushSize")
|
||||
SculptMeshActivateBrushSizeNameCommand;
|
||||
|
||||
// assign hotkey for name command
|
||||
hotkey -keyShortcut "b" -name ("SculptMeshActivateBrushSizeNameCommand") -releaseName ("SculptMeshDeactivateBrushSizeNameCommand");
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import is_string
|
||||
|
||||
from . import hotkeys
|
||||
|
||||
hotkeySetName = 'ngSkinTools2'
|
||||
context = 'ngst2PaintContext'
|
||||
command_prefix = "ngskintools_2_" # maya breaks command name on capital letters and before numbers, so this will ensure that all command names start with "ngskintools 2 "
|
||||
|
||||
log = getLogger("hotkeys setup")
|
||||
|
||||
|
||||
def uninstall_hotkeys():
|
||||
if cmds.hotkeySet(hotkeySetName, q=True, exists=True):
|
||||
cmds.hotkeySet(hotkeySetName, e=True, delete=True)
|
||||
|
||||
|
||||
def setup_named_commands():
|
||||
# "default" mode will force a read-only behavior for runTimCommands
|
||||
# only turn this on for production mode
|
||||
import ngSkinTools2
|
||||
|
||||
append_only_mode = not ngSkinTools2.DEBUG_MODE
|
||||
|
||||
def add_command(name, annotation, command, context=None):
|
||||
if not is_string(command):
|
||||
command = function_link(command)
|
||||
|
||||
runtime_command_name = command_prefix + name
|
||||
|
||||
# delete (if exists) and recreate runtime command
|
||||
if not append_only_mode and cmds.runTimeCommand(runtime_command_name, q=True, exists=True):
|
||||
cmds.runTimeCommand(runtime_command_name, e=True, delete=True)
|
||||
|
||||
if not cmds.runTimeCommand(runtime_command_name, q=True, exists=True):
|
||||
additional_args = {}
|
||||
if context is not None:
|
||||
additional_args['hotkeyCtx'] = context
|
||||
|
||||
cmds.runTimeCommand(
|
||||
runtime_command_name,
|
||||
category="Other items.ngSkinTools2",
|
||||
default=append_only_mode,
|
||||
annotation=annotation,
|
||||
command=command,
|
||||
commandLanguage="python",
|
||||
**additional_args
|
||||
)
|
||||
|
||||
cmds.nameCommand(
|
||||
command_prefix + name + "NameCommand",
|
||||
annotation=annotation + "-",
|
||||
sourceType="python",
|
||||
default=append_only_mode,
|
||||
command=runtime_command_name,
|
||||
)
|
||||
|
||||
def add_toggle(name, annotation, command_on, command_off, context=None):
|
||||
add_command(name + 'On', annotation=annotation, command=command_on, context=context)
|
||||
add_command(name + 'Off', annotation=annotation + "(release)", command=command_off, context=context)
|
||||
|
||||
add_toggle(
|
||||
'BrushSize',
|
||||
annotation='Toggle brush size mode',
|
||||
command_on=hotkeys.paint_tool_brush_size,
|
||||
command_off=hotkeys.paint_tool_brush_size_release,
|
||||
context=context,
|
||||
)
|
||||
|
||||
add_command('ToggleHelp', annotation='toggle help', command=hotkeys.paint_tool_toggle_help, context=context)
|
||||
add_command('ViewFitInfluence', annotation='fit influence in view', command=hotkeys.paint_tool_focus_current_influence, context=context)
|
||||
add_toggle(
|
||||
'SampleInfluence',
|
||||
annotation='Sample influence',
|
||||
command_on=hotkeys.paint_tool_sample_influence,
|
||||
command_off=hotkeys.paint_tool_sample_influence_release,
|
||||
context=context,
|
||||
)
|
||||
|
||||
add_command("SetBrushIntensity", annotation="set brush intensity", command=hotkeys.select_paint_brush_intensity, context=context)
|
||||
add_command("PaintFlood", annotation="apply current brush to all vertices", command=hotkeys.paint_tool_flood, context=context)
|
||||
|
||||
add_command("Paint", annotation="start paint tool", command=hotkeys.paint_tool_start)
|
||||
add_command(
|
||||
"ToggleOriginalMesh",
|
||||
annotation="toggle between weights display and original mesh while painting",
|
||||
command=hotkeys.paint_tool_toggle_original_mesh,
|
||||
)
|
||||
add_command(
|
||||
"CycleWeightsDisplayMode",
|
||||
annotation='Cycle weights display mode "all influences" -> "current influence" -> "current influence colored"',
|
||||
command=hotkeys.paint_tool_cycle_weights_display_mode,
|
||||
)
|
||||
|
||||
|
||||
def define_hotkeys():
|
||||
setup_named_commands()
|
||||
|
||||
def nc(name_command_short_name):
|
||||
return command_prefix + name_command_short_name + "NameCommand"
|
||||
|
||||
# cmds.hotkey(k="b", name=nc("BrushSizeOn"), releaseName=nc("BrushSizeOff"))
|
||||
cmds.hotkey(keyShortcut="b", name=nc("BrushSizeOn"), releaseName=nc("BrushSizeOff"))
|
||||
cmds.hotkey(keyShortcut="i", name=nc("SetBrushIntensity"))
|
||||
cmds.hotkey(keyShortcut="f", ctrlModifier=True, name=nc("PaintFlood"))
|
||||
cmds.hotkey(keyShortcut="f", name=nc("ViewFitInfluence"))
|
||||
cmds.hotkey(keyShortcut="h", name=nc("ToggleHelp"))
|
||||
|
||||
cmds.hotkey(keyShortcut="s", name=nc("SampleInfluenceOn"), releaseName=nc("SampleInfluenceOff"))
|
||||
cmds.hotkey(keyShortcut="d", name=nc("CycleWeightsDisplayMode"))
|
||||
cmds.hotkey(keyShortcut="t", name=nc("ToggleOriginalMesh"))
|
||||
|
||||
|
||||
def install_hotkeys():
|
||||
uninstall_hotkeys()
|
||||
|
||||
__hotkey_set_handler.remember()
|
||||
try:
|
||||
if cmds.hotkeySet(hotkeySetName, q=True, exists=True):
|
||||
cmds.hotkeySet(hotkeySetName, e=True, current=True)
|
||||
else:
|
||||
cmds.hotkeySet(hotkeySetName, current=True)
|
||||
|
||||
cmds.hotkeyCtx(addClient=context, type='Tool')
|
||||
|
||||
define_hotkeys()
|
||||
finally:
|
||||
__hotkey_set_handler.restore()
|
||||
|
||||
|
||||
def function_link(fun):
|
||||
return "import {module}; {module}.{fn}()".format(module=fun.__module__, fn=fun.__name__)
|
||||
|
||||
|
||||
class HotkeySetHandler:
|
||||
def __init__(self):
|
||||
log.info("initializing new hotkey set handler")
|
||||
self.prev_hotkey_set = None
|
||||
|
||||
def remember(self):
|
||||
if self.prev_hotkey_set is not None:
|
||||
return
|
||||
|
||||
log.info("remembering current hotkey set")
|
||||
self.prev_hotkey_set = cmds.hotkeySet(q=True, current=True)
|
||||
|
||||
def restore(self):
|
||||
if self.prev_hotkey_set is None:
|
||||
return
|
||||
|
||||
log.info("restoring previous hotkey set")
|
||||
cmds.hotkeySet(self.prev_hotkey_set, e=True, current=True)
|
||||
self.prev_hotkey_set = None
|
||||
|
||||
|
||||
__hotkey_set_handler = HotkeySetHandler()
|
||||
|
||||
|
||||
def toggle_paint_hotkey_set(enabled):
|
||||
if enabled:
|
||||
__hotkey_set_handler.remember()
|
||||
cmds.hotkeySet(hotkeySetName, e=True, current=True)
|
||||
else:
|
||||
__hotkey_set_handler.restore()
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-eye-fill"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1488"
|
||||
sodipodi:docname="eye-fill.svg"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
|
||||
<metadata
|
||||
id="metadata1494">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1492" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2080"
|
||||
id="namedview1490"
|
||||
showgrid="false"
|
||||
inkscape:zoom="63"
|
||||
inkscape:cx="8"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-x="3829"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1488" />
|
||||
<path
|
||||
d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"
|
||||
id="path1484"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"
|
||||
id="path1486"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-eye-slash-fill"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg2073"
|
||||
sodipodi:docname="eye-slash-fill.svg"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
|
||||
<metadata
|
||||
id="metadata2079">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2077" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2080"
|
||||
id="namedview2075"
|
||||
showgrid="false"
|
||||
inkscape:zoom="63"
|
||||
inkscape:cx="8"
|
||||
inkscape:cy="10.539683"
|
||||
inkscape:window-x="3829"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2073" />
|
||||
<path
|
||||
d="M10.79 12.912l-1.614-1.615a3.5 3.5 0 0 1-4.474-4.474l-2.06-2.06C.938 6.278 0 8 0 8s3 5.5 8 5.5a7.027 7.027 0 0 0 2.79-.588zM5.21 3.088A7.028 7.028 0 0 1 8 2.5c5 0 8 5.5 8 5.5s-.939 1.721-2.641 3.238l-2.062-2.062a3.5 3.5 0 0 0-4.474-4.474L5.21 3.088z"
|
||||
id="path2069"
|
||||
style="fill:#bababa;fill-opacity:1" />
|
||||
<path
|
||||
d="M5.525 7.646a2.5 2.5 0 0 0 2.829 2.829l-2.83-2.829zm4.95.708l-2.829-2.83a2.5 2.5 0 0 1 2.829 2.829zm3.171 6l-12-12 .708-.708 12 12-.708.707z"
|
||||
id="path2071"
|
||||
style="fill:#bababa;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16">
|
||||
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
||||
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299l.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884l-12-12 .708-.708 12 12-.708.708z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 893 B |
61
2023/scripts/rigging_tools/ngskintools2/ui/images/eye.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-eye"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg875"
|
||||
sodipodi:docname="eye.svg"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
|
||||
<metadata
|
||||
id="metadata881">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs879" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2133"
|
||||
inkscape:window-height="1414"
|
||||
id="namedview877"
|
||||
showgrid="false"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:zoom="63"
|
||||
inkscape:cx="8"
|
||||
inkscape:cy="9.2698413"
|
||||
inkscape:window-x="4867"
|
||||
inkscape:window-y="429"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg875" />
|
||||
<path
|
||||
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"
|
||||
id="path871"
|
||||
style="stroke-width:1.0015748;stroke-miterlimit:4;stroke-dasharray:none;stroke:none;stroke-opacity:1;fill:#ffffff;fill-opacity:1" />
|
||||
<path
|
||||
d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"
|
||||
id="path873"
|
||||
style="stroke-width:1.0015748;stroke-miterlimit:4;stroke-dasharray:none;fill:#ffffff;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 18 KiB |
BIN
2023/scripts/rigging_tools/ngskintools2/ui/images/logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
131
2023/scripts/rigging_tools/ngskintools2/ui/images/logo.svg
Normal file
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="42"
|
||||
height="42"
|
||||
viewBox="0 0 11.1125 11.1125"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
|
||||
sodipodi:docname="logo-colored2.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="10"
|
||||
inkscape:cx="57.337866"
|
||||
inkscape:cy="29.075965"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="g968"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2080"
|
||||
inkscape:window-x="3829"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:pagecheckerboard="false"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="g968"
|
||||
inkscape:label="main"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path1008"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#e6f7e1;fill-opacity:1;fill-rule:evenodd;stroke:#394241;stroke-width:0.381;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 10.815145,5.5547744 A 5.2636318,5.2687149 0 0 1 5.5515153,10.823488 5.2636318,5.2687149 0 0 1 0.28788462,5.5547744 5.2636318,5.2687149 0 0 1 5.5515153,0.2860595 5.2636318,5.2687149 0 0 1 10.815145,5.5547744 Z"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#8ed07d;fill-opacity:1;stroke:#394241;stroke-width:0.132292;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 10.071522,5.5547744 A 4.520009,4.5243745 0 0 1 5.5515153,10.079147 4.520009,4.5243745 0 0 1 1.0315065,5.5547744 4.520009,4.5243745 0 0 1 5.5515153,1.0303994 4.520009,4.5243745 0 0 1 10.071522,5.5547744 Z"
|
||||
id="path1010"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1012"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#e4f6de;fill-opacity:1;fill-rule:evenodd;stroke:#394241;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 8.2782313,3.4978104 a 0.99620533,0.99716729 0 0 1 -0.996205,0.997168 0.99620533,0.99716729 0 0 1 -0.996206,-0.997168 0.99620533,0.99716729 0 0 1 0.996206,-0.997167 0.99620533,0.99716729 0 0 1 0.996205,0.997167 z"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1014"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#394241;fill-opacity:1;stroke:#394241;stroke-width:0.132292;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 6.1665103,6.0291114 0.362793,-1.729342 m 0,0 -1.678701,0.935426 c 0,0 0.507188,0.0474 0.759757,0.206425 0.25257,0.159026 0.556151,0.587491 0.556151,0.587491"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1016"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:none;stroke:#394241;stroke-width:0.132;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2.8789345,7.9067184 -0.5782041,0.785102 m 5.8312779,-0.953399 0.673759,0.98138 m -3.200692,0.07241 0.02713,1.2964136 M 2.8293132,7.5401614 C 2.0841484,7.0595114 1.5609154,6.3432344 1.0846774,5.1276854 m 5.2510059,3.037117 c 1.565971,-0.02362 3.1253259,-1.1282499 3.7386737,-2.2919899"
|
||||
sodipodi:nodetypes="cccccccccc"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<g
|
||||
id="g1026"
|
||||
style="fill:#45954d;fill-opacity:1;stroke:none;stroke-opacity:1"
|
||||
transform="translate(0.00677962,-0.03127322)"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001">
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 8.505902,8.1900327 8.2143526,7.7654947 8.4591298,7.6195127 c 0.5506069,-0.328376 1.047841,-0.767506 1.4030352,-1.239084 l 0.07046,-0.09355 -0.0128,0.07484 c -0.082767,0.48376 -0.30625,1.076837 -0.5745653,1.524772 -0.1008621,0.168383 -0.3570904,0.517576 -0.4764757,0.64935 l -0.071327,0.07873 z"
|
||||
id="path1018"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 5.6947136,9.78319 C 5.6879536,9.6422617 5.6828286,9.3872697 5.6833236,9.2165407 l 8.996e-4,-0.310415 0.1047906,-0.06548 c 0.1368939,-0.08554 0.3882578,-0.323017 0.4904542,-0.463355 l 0.080931,-0.111135 0.2082474,-0.01182 c 0.3803844,-0.02159 0.8510395,-0.141515 1.297223,-0.330527 l 0.2096587,-0.08882 0.3001579,0.43557 c 0.1650868,0.239563 0.3001579,0.443507 0.3001579,0.453207 0,0.02468 -0.1876353,0.200193 -0.3598442,0.336599 C 7.8780736,9.4072457 7.3539626,9.68306 6.8315378,9.841564 6.5909701,9.914554 6.2021559,9.993084 6.000476,10.00942 5.923302,10.01572 5.8256991,10.02498 5.7835811,10.03011 l -0.076578,0.0093 z"
|
||||
id="path1020"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 5.1398679,10.017257 C 4.1884222,9.933694 3.2165381,9.4967897 2.5160663,8.8377427 l -0.1261678,-0.118706 0.21553,-0.289988 c 0.1185414,-0.159492 0.2216215,-0.291499 0.2290666,-0.293348 0.00745,-0.0019 0.059841,0.06838 0.1164354,0.156066 0.1213623,0.188035 0.3700815,0.435839 0.5621639,0.560097 0.5624248,0.36383 1.2599882,0.437436 1.8765803,0.198012 l 0.1430793,-0.05556 v 0.294403 c 0,0.161922 0.0055,0.3967533 0.012229,0.5218473 l 0.012229,0.227444 -0.1198055,-0.0026 c -0.065893,-0.0014 -0.1997861,-0.0096 -0.2975398,-0.01818 z"
|
||||
id="path1022"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
style="display:inline;fill:#cae5c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0269407;stroke-linecap:round;stroke-opacity:1"
|
||||
d="M 2.1635305,8.4698927 C 1.9097453,8.1663167 1.7204681,7.8762037 1.5570923,7.5403777 1.2671987,6.9444897 1.1262435,6.3732707 1.1069707,5.7162637 l -0.00851,-0.289987 0.041376,0.09354 c 0.097201,0.219754 0.3172379,0.649387 0.4168077,0.813835 0.2559593,0.422741 0.6076541,0.835074 0.9264318,1.086166 0.1353842,0.106639 0.1490128,0.123649 0.1696878,0.211798 0.012364,0.05272 0.0418,0.147838 0.065413,0.211382 l 0.042933,0.115534 -0.2344342,0.319447 c -0.1289388,0.175695 -0.2398767,0.318883 -0.2465287,0.318195 -0.00665,-6.88e-4 -0.059131,-0.05752 -0.1166208,-0.126285 z"
|
||||
id="path1024"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
</g>
|
||||
<path
|
||||
id="path1028"
|
||||
style="display:inline;opacity:1;mix-blend-mode:normal;fill:#e4f6de;fill-opacity:1;fill-rule:evenodd;stroke:#394241;stroke-width:0.27;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 6.5574843,7.1065184 A 1.9072757,1.9091177 0 0 1 4.6502091,9.0156364 1.9072757,1.9091177 0 0 1 2.7429334,7.1065184 1.9072757,1.9091177 0 0 1 4.6502091,5.1974004 1.9072757,1.9091177 0 0 1 6.5574843,7.1065184 Z"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
<path
|
||||
id="path1030"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:#000000;fill-opacity:0;stroke:#394241;stroke-width:0.132292;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 4.5859576,9.0166314 c -0.4550684,-0.740982 -0.536541,-2.94365 0.010119,-3.784058 m 1.9302377,1.867597 c -1.222413,0.573856 -2.9362113,0.537553 -3.865322,-0.01532"
|
||||
inkscape:export-xdpi="465.70001"
|
||||
inkscape:export-ydpi="465.70001" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
360
2023/scripts/rigging_tools/ngskintools2/ui/influenceMappingUI.py
Normal file
@@ -0,0 +1,360 @@
|
||||
from ngSkinTools2 import cleanup, signal
|
||||
from ngSkinTools2.api import influenceMapping, mirror
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.signal import Signal
|
||||
from ngSkinTools2.ui import dialogs, qt, widgets
|
||||
from ngSkinTools2.ui.dialogs import yesNo
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
from ngSkinTools2.ui.options import config
|
||||
from ngSkinTools2.ui.widgets import NumberSliderGroup
|
||||
|
||||
log = getLogger("influence mapping UI")
|
||||
|
||||
|
||||
def open_ui_for_mesh(ui_parent, mesh):
|
||||
m = mirror.Mirror(mesh)
|
||||
mapper = m.build_influences_mapper()
|
||||
|
||||
def do_apply(mapping):
|
||||
m.set_influences_mapping(mapping)
|
||||
m.save_influences_mapper(mapper)
|
||||
|
||||
return open_as_dialog(ui_parent, mapper, do_apply)
|
||||
|
||||
|
||||
def open_as_dialog(parent, matcher, result_callback):
|
||||
"""
|
||||
|
||||
:type matcher: ngSkinTools2.api.influenceMapping.InfluenceMapping
|
||||
"""
|
||||
main_layout, reload_ui, recalc_matches = build_ui(parent, matcher)
|
||||
|
||||
def button_row(window):
|
||||
def apply():
|
||||
result_callback(matcher.asIntIntMapping(matcher.calculatedMapping))
|
||||
window.close()
|
||||
|
||||
def save_defaults():
|
||||
if not yesNo("Save current settings as default?"):
|
||||
return
|
||||
config.mirrorInfluencesDefaults = matcher.config.as_json()
|
||||
|
||||
def load_defaults():
|
||||
matcher.config.load_json(config.mirrorInfluencesDefaults)
|
||||
reload_ui()
|
||||
recalc_matches()
|
||||
|
||||
return widgets.button_row(
|
||||
[
|
||||
("Apply", apply),
|
||||
("Cancel", window.close),
|
||||
],
|
||||
side_menu=[
|
||||
("Save As Default", save_defaults),
|
||||
("Load Defaults", load_defaults),
|
||||
],
|
||||
)
|
||||
|
||||
window = QtWidgets.QDialog(parent)
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
window.setWindowTitle("Influence Mirror Mapping")
|
||||
window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
window.resize(720 * scale_multiplier, 500 * scale_multiplier)
|
||||
window.setLayout(QtWidgets.QVBoxLayout())
|
||||
window.layout().addWidget(main_layout)
|
||||
window.layout().addLayout(button_row(window))
|
||||
|
||||
window.show()
|
||||
|
||||
recalc_matches()
|
||||
|
||||
return window
|
||||
|
||||
|
||||
def build_ui(parent, matcher):
|
||||
"""
|
||||
|
||||
:param parent: parent qt widget
|
||||
:type matcher: influenceMapping.InfluenceMapping
|
||||
"""
|
||||
|
||||
influence_data = matcher.influences
|
||||
|
||||
influenceMapping.calcShortestUniqueName(matcher.influences)
|
||||
if matcher.destinationInfluences is not None and matcher.destinationInfluences != matcher.influences:
|
||||
influenceMapping.calcShortestUniqueName(matcher.destinationInfluences)
|
||||
|
||||
update_globs = Signal("need recalc")
|
||||
reload_ui = Signal("reload_ui")
|
||||
|
||||
mirror_mode = matcher.config.mirror_axis is not None
|
||||
|
||||
def build_tree_hierarchy(tree_view):
|
||||
tree_items = {} # mapping of path->treeItem
|
||||
influence_items = {} # same as above, only includes non-intermediate items
|
||||
|
||||
def find_item(path, is_intermediate):
|
||||
result = tree_items.get(path, None)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
split_path = path.rsplit("|", 1)
|
||||
parent_path, name = split_path if len(split_path) == 2 else ["", split_path[0]]
|
||||
|
||||
item = QtWidgets.QTreeWidgetItem([name, '-', '(not in skin cluster)' if is_intermediate else '?'])
|
||||
tree_items[path] = item
|
||||
|
||||
parent_item = None if parent_path == "" else find_item(parent_path, True)
|
||||
|
||||
if parent_item is not None:
|
||||
parent_item.addChild(item)
|
||||
else:
|
||||
tree_view.addTopLevelItem(item)
|
||||
|
||||
item.setExpanded(True)
|
||||
|
||||
return item
|
||||
|
||||
for i in influence_data:
|
||||
influence_items[i.path_name()] = find_item(i.path_name(), False)
|
||||
|
||||
return influence_items
|
||||
|
||||
def tolerance():
|
||||
result = NumberSliderGroup(min_value=0.001, max_value=10)
|
||||
result.spinner.setDecimals(3)
|
||||
|
||||
@signal.on(reload_ui)
|
||||
def reload():
|
||||
with qt.signals_blocked(result):
|
||||
result.set_value(matcher.config.distance_threshold)
|
||||
|
||||
@signal.on(result.valueChanged)
|
||||
def changed():
|
||||
matcher.config.distance_threshold = result.value()
|
||||
recalcMatches()
|
||||
|
||||
reload()
|
||||
|
||||
return result
|
||||
|
||||
def pattern():
|
||||
result = QtWidgets.QTableWidget()
|
||||
result.setColumnCount(2)
|
||||
result.setHorizontalHeaderLabels(["Pattern", "Opposite"] if mirror_mode else ["Source", "Destination"])
|
||||
result.setEditTriggers(QtWidgets.QTableWidget.AllEditTriggers)
|
||||
|
||||
result.verticalHeader().setVisible(False)
|
||||
result.verticalHeader().setDefaultSectionSize(20)
|
||||
|
||||
item_font = QtGui.QFont("Courier New", 12)
|
||||
item_font.setStyleHint(QtGui.QFont.Monospace)
|
||||
|
||||
@signal.on(reload_ui)
|
||||
def reload_patterns():
|
||||
with qt.signals_blocked(result):
|
||||
result.setRowCount(len(matcher.config.globs) + 1)
|
||||
for rowIndex, patterns in enumerate(matcher.config.globs + [('', '')]):
|
||||
for colIndex, p in enumerate(patterns):
|
||||
item = QtWidgets.QTableWidgetItem(p)
|
||||
item.setFont(item_font)
|
||||
result.setItem(rowIndex, colIndex, item)
|
||||
|
||||
reload_patterns()
|
||||
|
||||
@signal.on(update_globs)
|
||||
def update_matcher_globs():
|
||||
globs = []
|
||||
|
||||
def text(r, c):
|
||||
item = result.item(r, c)
|
||||
if item is None:
|
||||
return ""
|
||||
return item.text().strip()
|
||||
|
||||
for row in range(result.rowCount()):
|
||||
v1 = text(row, 0)
|
||||
v2 = text(row, 1)
|
||||
if v1 != "" and v2 != "":
|
||||
globs.append((v1, v2))
|
||||
|
||||
matcher.config.globs = globs
|
||||
recalcMatches()
|
||||
|
||||
@qt.on(result.itemChanged)
|
||||
def item_changed(item):
|
||||
log.debug("item changed")
|
||||
item.setText(item.text().strip())
|
||||
|
||||
try:
|
||||
influenceMapping.validate_glob(item.text().strip())
|
||||
except Exception as err:
|
||||
dialogs.displayError(str(err))
|
||||
item.setText(influenceMapping.illegalCharactersRegexp.sub("", item.text()))
|
||||
|
||||
if item.row() != result.rowCount() - 1:
|
||||
if item.text().strip() == "":
|
||||
result.removeRow(item.row())
|
||||
|
||||
# ensure one empty line at the end
|
||||
rows = result.rowCount()
|
||||
last_item = result.item(rows - 1, 0)
|
||||
if last_item and last_item.text() != "":
|
||||
result.setRowCount(rows + 1)
|
||||
|
||||
update_matcher_globs()
|
||||
|
||||
return result
|
||||
|
||||
def automaticRules():
|
||||
form = QtWidgets.QFormLayout()
|
||||
use_joint_names = QtWidgets.QCheckBox("Match by joint name")
|
||||
naming_patterns = pattern()
|
||||
use_position = QtWidgets.QCheckBox("Match by position")
|
||||
tolerance_scroll = tolerance()
|
||||
use_joint_labels = QtWidgets.QCheckBox("Match by joint label")
|
||||
use_dg_links = QtWidgets.QCheckBox("Match by dependency graph links")
|
||||
|
||||
def update_enabled_disabled():
|
||||
def enable_form_row(form_item, e):
|
||||
form_item.setEnabled(e)
|
||||
form.labelForField(form_item).setEnabled(e)
|
||||
|
||||
checked = use_joint_names.isChecked()
|
||||
enable_form_row(naming_patterns, checked)
|
||||
|
||||
checked = use_position.isChecked()
|
||||
tolerance_scroll.set_enabled(checked)
|
||||
form.labelForField(tolerance_scroll.layout()).setEnabled(checked)
|
||||
|
||||
enable_form_row(dg_attribute, use_dg_links.isChecked())
|
||||
|
||||
@qt.on(use_joint_names.toggled, use_position.toggled, use_joint_labels.toggled, use_dg_links.toggled)
|
||||
def use_joint_names_toggled():
|
||||
update_enabled_disabled()
|
||||
matcher.config.use_name_matching = use_joint_names.isChecked()
|
||||
matcher.config.use_distance_matching = use_position.isChecked()
|
||||
matcher.config.use_label_matching = use_joint_labels.isChecked()
|
||||
matcher.config.use_dg_link_matching = use_dg_links.isChecked()
|
||||
recalcMatches()
|
||||
|
||||
dg_attribute = QtWidgets.QLineEdit()
|
||||
|
||||
@qt.on(dg_attribute.editingFinished)
|
||||
def use_joint_names_toggled():
|
||||
matcher.config.dg_destination_attribute = str(dg_attribute.text()).strip()
|
||||
recalcMatches()
|
||||
|
||||
@signal.on(reload_ui)
|
||||
def update_values():
|
||||
with qt.signals_blocked(dg_attribute):
|
||||
dg_attribute.setText(matcher.config.dg_destination_attribute)
|
||||
with qt.signals_blocked(use_joint_names):
|
||||
use_joint_names.setChecked(matcher.config.use_name_matching)
|
||||
with qt.signals_blocked(use_position):
|
||||
use_position.setChecked(matcher.config.use_distance_matching)
|
||||
with qt.signals_blocked(use_joint_labels):
|
||||
use_joint_labels.setChecked(matcher.config.use_label_matching)
|
||||
with qt.signals_blocked(use_dg_links):
|
||||
use_dg_links.setChecked(matcher.config.use_dg_link_matching)
|
||||
update_enabled_disabled()
|
||||
|
||||
g = QtWidgets.QGroupBox("Rules")
|
||||
g.setLayout(form)
|
||||
form.addRow(use_dg_links)
|
||||
form.addRow("Attribute name:", dg_attribute)
|
||||
form.addRow(use_joint_labels)
|
||||
form.addRow(use_joint_names)
|
||||
form.addRow("Naming scheme:", naming_patterns)
|
||||
form.addRow(use_position)
|
||||
form.addRow("Position tolerance:", tolerance_scroll.layout())
|
||||
|
||||
update_values()
|
||||
return g
|
||||
|
||||
def scriptedRules():
|
||||
g = QtWidgets.QGroupBox("Scripted rules")
|
||||
g.setLayout(QtWidgets.QVBoxLayout())
|
||||
g.layout().addWidget(QtWidgets.QLabel("TODO"))
|
||||
return g
|
||||
|
||||
def manualRules():
|
||||
g = QtWidgets.QGroupBox("Manual overrides")
|
||||
g.setLayout(QtWidgets.QVBoxLayout())
|
||||
g.layout().addWidget(QtWidgets.QLabel("TODO"))
|
||||
return g
|
||||
|
||||
leftSide = QtWidgets.QScrollArea()
|
||||
leftSide.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
leftSide.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
leftSide.setWidgetResizable(True)
|
||||
|
||||
l = QtWidgets.QVBoxLayout()
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
l.addWidget(automaticRules())
|
||||
# l.addWidget(scriptedRules())
|
||||
# l.addWidget(manualRules())
|
||||
# l.addStretch()
|
||||
|
||||
leftSide.setWidget(qt.wrap_layout_into_widget(l))
|
||||
|
||||
def createMappingView():
|
||||
view = QtWidgets.QTreeWidget()
|
||||
view.setColumnCount(3)
|
||||
view.setHeaderLabels(["Source", "Destination", "Matched by rule"])
|
||||
view.setIndentation(7)
|
||||
view.setExpandsOnDoubleClick(False)
|
||||
|
||||
usedItems = build_tree_hierarchy(view)
|
||||
|
||||
linkedItemRole = QtCore.Qt.UserRole + 1
|
||||
|
||||
def previewMapping(mapping):
|
||||
"""
|
||||
|
||||
:type mapping: dict[InfluenceInfo, InfluenceInfo]
|
||||
"""
|
||||
for treeItem in list(usedItems.values()):
|
||||
treeItem.setText(1, "(not matched)")
|
||||
treeItem.setText(2, "")
|
||||
|
||||
for k, v in list(mapping.items()):
|
||||
treeItem = usedItems.get(k.path_name(), None)
|
||||
if treeItem is None:
|
||||
continue
|
||||
treeItem.setText(1, "(self)" if k == v['infl'] else v["infl"].shortestPath)
|
||||
treeItem.setText(2, v["matchedRule"])
|
||||
treeItem.setData(1, linkedItemRole, v["infl"].path)
|
||||
|
||||
@qt.on(view.itemDoubleClicked)
|
||||
def itemDoubleClicked(item, column):
|
||||
item.setExpanded(True)
|
||||
|
||||
linkedItemPath = item.data(1, linkedItemRole)
|
||||
item = usedItems.get(linkedItemPath, None)
|
||||
if item is not None:
|
||||
item.setSelected(True)
|
||||
view.scrollToItem(item)
|
||||
|
||||
return view, previewMapping
|
||||
|
||||
def recalcMatches():
|
||||
matches = matcher.calculate()
|
||||
mappingView_updateMatches(matches)
|
||||
|
||||
g = QtWidgets.QGroupBox("Calculated mapping")
|
||||
g.setLayout(QtWidgets.QVBoxLayout())
|
||||
mappingView, mappingView_updateMatches = createMappingView()
|
||||
g.layout().addWidget(mappingView)
|
||||
|
||||
mainLayout = QtWidgets.QSplitter(orientation=QtCore.Qt.Horizontal, parent=parent)
|
||||
mainLayout.addWidget(leftSide)
|
||||
mainLayout.addWidget(g)
|
||||
|
||||
mainLayout.setStretchFactor(0, 10)
|
||||
mainLayout.setStretchFactor(1, 10)
|
||||
mainLayout.setCollapsible(0, True)
|
||||
mainLayout.setSizes([200] * 2)
|
||||
|
||||
return mainLayout, reload_ui.emit, recalcMatches
|
||||
315
2023/scripts/rigging_tools/ngskintools2/ui/influencesview.py
Normal file
@@ -0,0 +1,315 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import influence_names
|
||||
from ngSkinTools2.api.influenceMapping import InfluenceInfo
|
||||
from ngSkinTools2.api.layers import Layer
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.target_info import list_influences
|
||||
from ngSkinTools2.ui import actions, qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
from ngSkinTools2.ui.options import Config, config
|
||||
|
||||
log = getLogger("influencesView")
|
||||
_ = Layer # only imported for type reference
|
||||
|
||||
|
||||
def build_used_influences_action(parent):
|
||||
def toggle():
|
||||
config.influences_show_used_influences_only.set(not config.influences_show_used_influences_only())
|
||||
|
||||
result = actions.define_action(
|
||||
parent,
|
||||
"Used Influences Only",
|
||||
callback=toggle,
|
||||
tooltip="If enabled, influences view will only show influences that have weights on current layer",
|
||||
)
|
||||
|
||||
@signal.on(config.influences_show_used_influences_only.changed, qtParent=parent)
|
||||
def update():
|
||||
result.setChecked(config.influences_show_used_influences_only())
|
||||
|
||||
result.setCheckable(True)
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
def build_set_influences_sorted_action(parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
def toggle():
|
||||
new_value = Config.InfluencesSortDescending
|
||||
if config.influences_sort() == new_value:
|
||||
new_value = Config.InfluencesSortUnsorted
|
||||
config.influences_sort.set(new_value)
|
||||
|
||||
result = actions.define_action(
|
||||
parent,
|
||||
"Show influences sorted",
|
||||
callback=toggle,
|
||||
tooltip="Sort influences by name",
|
||||
)
|
||||
|
||||
@signal.on(config.influences_show_used_influences_only.changed, qtParent=parent)
|
||||
def update():
|
||||
result.setChecked(config.influences_sort() == Config.InfluencesSortDescending)
|
||||
|
||||
result.setCheckable(True)
|
||||
update()
|
||||
return result
|
||||
|
||||
|
||||
icon_mask = QtGui.QIcon(":/blendColors.svg")
|
||||
icon_dq = QtGui.QIcon(":/rotate_M.png")
|
||||
icon_joint = QtGui.QIcon(":/joint.svg")
|
||||
icon_joint_disabled = qt.image_icon("joint_disabled.png")
|
||||
icon_transform = QtGui.QIcon(":/cube.png")
|
||||
icon_transform_disabled = qt.image_icon("cube_disabled.png")
|
||||
|
||||
|
||||
def build_view(parent, actions, session, filter):
|
||||
"""
|
||||
:param parent: ui parent
|
||||
:type actions: ngSkinTools2.ui.actions.Actions
|
||||
:type session: ngSkinTools2.ui.session.Session
|
||||
:type filter: InfluenceNameFilter
|
||||
"""
|
||||
|
||||
icon_locked = QtGui.QIcon(":/Lock_ON.png")
|
||||
icon_unlocked = QtGui.QIcon(":/Lock_OFF_grey.png")
|
||||
|
||||
id_role = QtCore.Qt.UserRole + 1
|
||||
item_size_hint = QtCore.QSize(25 * scale_multiplier, 25 * scale_multiplier)
|
||||
|
||||
def get_item_id(item):
|
||||
if item is None:
|
||||
return None
|
||||
return item.data(0, id_role)
|
||||
|
||||
tree_items = {}
|
||||
|
||||
def build_items(view, items, layer):
|
||||
# type: (QtWidgets.QTreeWidget, list[InfluenceInfo], Layer) -> None
|
||||
is_group_layer = layer is not None and layer.num_children != 0
|
||||
|
||||
def rebuild_buttons(item, item_id, buttons):
|
||||
bar = QtWidgets.QToolBar(parent=parent)
|
||||
bar.setMovable(False)
|
||||
bar.setIconSize(QtCore.QSize(13 * scale_multiplier, 13 * scale_multiplier))
|
||||
|
||||
def add_or_remove(input_list, items, should_add):
|
||||
if should_add:
|
||||
return list(input_list) + list(items)
|
||||
return [i for i in input_list if i not in items]
|
||||
|
||||
def lock_unlock_handler(lock):
|
||||
def handler():
|
||||
targets = layer.paint_targets
|
||||
if item_id not in targets:
|
||||
targets = (item_id,)
|
||||
|
||||
layer.locked_influences = add_or_remove(layer.locked_influences, targets, lock)
|
||||
log.info("updated locked influences to %r", layer.locked_influences)
|
||||
session.events.influencesListUpdated.emit()
|
||||
|
||||
return handler
|
||||
|
||||
if "unlocked" in buttons:
|
||||
a = bar.addAction(icon_unlocked, "Toggle locked/unlocked")
|
||||
qt.on(a.triggered)(lock_unlock_handler(True))
|
||||
|
||||
if "locked" in buttons:
|
||||
a = bar.addAction(icon_locked, "Toggle locked/unlocked")
|
||||
qt.on(a.triggered)(lock_unlock_handler(False))
|
||||
|
||||
view.setItemWidget(item, 1, bar)
|
||||
|
||||
selected_ids = []
|
||||
if session.state.currentLayer.layer:
|
||||
selected_ids = session.state.currentLayer.layer.paint_targets
|
||||
current_id = None if not selected_ids else selected_ids[0]
|
||||
|
||||
with qt.signals_blocked(view):
|
||||
tree_items.clear()
|
||||
tree_root = view.invisibleRootItem()
|
||||
|
||||
item_index = 0
|
||||
for item_id, displayName, icon, buttons in wanted_tree_items(
|
||||
items=items,
|
||||
include_dq_item=session.state.skin_cluster_dq_channel_used,
|
||||
is_group_layer=is_group_layer,
|
||||
layer=layer,
|
||||
config=config,
|
||||
filter=filter,
|
||||
):
|
||||
if item_index >= tree_root.childCount():
|
||||
item = QtWidgets.QTreeWidgetItem([displayName])
|
||||
else:
|
||||
item = tree_root.child(item_index)
|
||||
item.setText(0, displayName)
|
||||
|
||||
item.setData(0, id_role, item_id)
|
||||
item.setIcon(0, icon)
|
||||
item.setSizeHint(0, item_size_hint)
|
||||
tree_root.addChild(item)
|
||||
|
||||
tree_items[item_id] = item
|
||||
if item_id == current_id:
|
||||
view.setCurrentItem(item, 0, QtCore.QItemSelectionModel.NoUpdate)
|
||||
item.setSelected(item_id in selected_ids)
|
||||
|
||||
rebuild_buttons(item, item_id, buttons)
|
||||
|
||||
item_index += 1
|
||||
|
||||
while item_index < tree_root.childCount():
|
||||
tree_root.removeChild(tree_root.child(item_index))
|
||||
|
||||
view = QtWidgets.QTreeWidget(parent)
|
||||
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
view.setUniformRowHeights(True)
|
||||
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||
actions.addInfluencesActions(view)
|
||||
view.addAction(actions.separator(parent, "View Options"))
|
||||
view.addAction(actions.show_used_influences_only)
|
||||
view.addAction(actions.set_influences_sorted)
|
||||
view.setIndentation(10 * scale_multiplier)
|
||||
view.header().setStretchLastSection(False)
|
||||
view.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
|
||||
view.setHeaderLabels(["Influences", ""])
|
||||
view.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
view.setColumnWidth(1, 25 * scale_multiplier)
|
||||
|
||||
# view.setHeaderHidden(True)
|
||||
def refresh_items():
|
||||
items = list_influences(session.state.currentLayer.selectedSkinCluster)
|
||||
|
||||
def sort_func(a):
|
||||
"""
|
||||
:type a: InfluenceInfo
|
||||
"""
|
||||
return a.name
|
||||
|
||||
# items = sorted(items, key=sort_func)
|
||||
build_items(view, items, session.state.currentLayer.layer)
|
||||
|
||||
@signal.on(
|
||||
filter.changed,
|
||||
config.influences_show_used_influences_only.changed,
|
||||
config.influences_sort.changed,
|
||||
session.events.influencesListUpdated,
|
||||
)
|
||||
def filter_changed():
|
||||
refresh_items()
|
||||
|
||||
@signal.on(session.events.currentLayerChanged, qtParent=view)
|
||||
def current_layer_changed():
|
||||
if not session.state.currentLayer.layer:
|
||||
build_items(view, [], None)
|
||||
else:
|
||||
log.info("current layer changed to %s", session.state.currentLayer.layer)
|
||||
refresh_items()
|
||||
current_influence_changed()
|
||||
|
||||
@signal.on(session.events.currentInfluenceChanged, qtParent=view)
|
||||
def current_influence_changed():
|
||||
if session.state.currentLayer.layer is None:
|
||||
return
|
||||
|
||||
log.info("current influence changed - updating item selection")
|
||||
with qt.signals_blocked(view):
|
||||
targets = session.state.currentLayer.layer.paint_targets
|
||||
first = True
|
||||
for tree_item in tree_items.values():
|
||||
selected = get_item_id(tree_item) in targets
|
||||
if selected and first:
|
||||
view.setCurrentItem(tree_item, 0, QtCore.QItemSelectionModel.NoUpdate)
|
||||
first = False
|
||||
tree_item.setSelected(selected)
|
||||
|
||||
@qt.on(view.currentItemChanged)
|
||||
def current_item_changed(curr, prev):
|
||||
if curr is None:
|
||||
return
|
||||
|
||||
if session.state.selectedSkinCluster is None:
|
||||
return
|
||||
|
||||
if not session.state.currentLayer.layer:
|
||||
return
|
||||
|
||||
log.info("focused item changed: %r", get_item_id(curr))
|
||||
sync_paint_targets_to_selection()
|
||||
|
||||
@qt.on(view.itemSelectionChanged)
|
||||
def sync_paint_targets_to_selection():
|
||||
log.info("syncing paint targets")
|
||||
selected_ids = [get_item_id(item) for item in view.selectedItems()]
|
||||
selected_ids = [i for i in selected_ids if i is not None]
|
||||
|
||||
current_item = view.currentItem()
|
||||
if current_item and current_item.isSelected():
|
||||
# move id of current item to front, if it's selected
|
||||
item_id = get_item_id(current_item)
|
||||
selected_ids.remove(item_id)
|
||||
selected_ids = [item_id] + selected_ids
|
||||
|
||||
if session.state.currentLayer.layer:
|
||||
session.state.currentLayer.layer.paint_targets = selected_ids
|
||||
|
||||
current_layer_changed()
|
||||
|
||||
return view
|
||||
|
||||
|
||||
def get_icon(influence, is_joint):
|
||||
if influence.used:
|
||||
return icon_joint if is_joint else icon_transform
|
||||
return icon_joint_disabled if is_joint else icon_transform_disabled
|
||||
|
||||
|
||||
def wanted_tree_items(
|
||||
layer,
|
||||
config,
|
||||
is_group_layer,
|
||||
include_dq_item,
|
||||
filter,
|
||||
items,
|
||||
):
|
||||
"""
|
||||
|
||||
:type items: list[InfluenceInfo]
|
||||
"""
|
||||
|
||||
if layer is None:
|
||||
return
|
||||
|
||||
# calculate "used" regardless as we're displaying it visually even if "show used influences only" is toggled off
|
||||
used = set((layer.get_used_influences() or []))
|
||||
locked = set((layer.locked_influences or []))
|
||||
for i in items:
|
||||
i.used = i.logicalIndex in used
|
||||
i.locked = i.logicalIndex in locked
|
||||
|
||||
if config.influences_show_used_influences_only() and layer is not None:
|
||||
items = [i for i in items if i.used]
|
||||
|
||||
if is_group_layer:
|
||||
items = []
|
||||
|
||||
yield "mask", "[Mask]", icon_mask, []
|
||||
if not is_group_layer and include_dq_item:
|
||||
yield "dq", "[DQ Weights]", icon_dq, []
|
||||
|
||||
names = influence_names.unique_names([i.path_name() for i in items])
|
||||
for i, name in zip(items, names):
|
||||
i.unique_name = name
|
||||
|
||||
if config.influences_sort() == Config.InfluencesSortDescending:
|
||||
items = list(sorted(items, key=lambda i: i.unique_name))
|
||||
|
||||
for i in items:
|
||||
is_joint = i.path is not None
|
||||
if filter.is_match(i.path_name()):
|
||||
yield i.logicalIndex, i.unique_name, get_icon(i, is_joint), ["locked" if i.locked else "unlocked"]
|
||||
225
2023/scripts/rigging_tools/ngskintools2/ui/layersview.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from ngSkinTools2 import api, signal
|
||||
from ngSkinTools2.api import python_compatibility
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
if python_compatibility.PY3:
|
||||
from typing import Union
|
||||
|
||||
|
||||
log = getLogger("layersView")
|
||||
|
||||
|
||||
def build_view(parent, actions):
|
||||
from ngSkinTools2.operations import layers
|
||||
|
||||
layer_icon_size = 20
|
||||
visibility_icon_size = 13
|
||||
|
||||
icon_layer = qt.scaled_icon(":/layeredTexture.svg", layer_icon_size, layer_icon_size)
|
||||
icon_layer_disabled = qt.scaled_icon(":/layerEditor.png", layer_icon_size, layer_icon_size)
|
||||
icon_visible = qt.scaled_icon("eye-fill.svg", visibility_icon_size, visibility_icon_size)
|
||||
icon_hidden = qt.scaled_icon("eye-slash-fill.svg", visibility_icon_size, visibility_icon_size)
|
||||
|
||||
layer_data_role = QtCore.Qt.UserRole + 1
|
||||
|
||||
def item_to_layer(item):
|
||||
# type: (QtWidgets.QTreeWidgetItem) -> Union[api.Layer, None]
|
||||
if item is None:
|
||||
return None
|
||||
return item.data(0, layer_data_role)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def sync_layer_parents_to_widget_items(view):
|
||||
"""
|
||||
after drag/drop tree reordering, just brute-force check
|
||||
that rearranged items match layers parents
|
||||
:return:
|
||||
"""
|
||||
|
||||
def sync_item(tree_item, parent_layer_id):
|
||||
for i in range(tree_item.childCount()):
|
||||
child = tree_item.child(i)
|
||||
rebuild_buttons(child)
|
||||
|
||||
child_layer = item_to_layer(child)
|
||||
|
||||
if child_layer.parent_id != parent_layer_id:
|
||||
log.info("changing layer parent: %r->%r (was %r)", parent_layer_id, child_layer, child_layer.parent_id)
|
||||
child_layer.parent = parent_layer_id
|
||||
|
||||
new_index = tree_item.childCount() - i - 1
|
||||
if child_layer.index != new_index:
|
||||
log.info("changing layer index: %r->%r (was %r)", child_layer, new_index, child_layer.index)
|
||||
child_layer.index = new_index
|
||||
|
||||
sync_item(child, child_layer.id)
|
||||
|
||||
with qt.signals_blocked(view):
|
||||
sync_item(view.invisibleRootItem(), None)
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class LayersWidget(QtWidgets.QTreeWidget):
|
||||
def dropEvent(self, event):
|
||||
QtWidgets.QTreeWidget.dropEvent(self, event)
|
||||
sync_layer_parents_to_widget_items(self)
|
||||
|
||||
view = LayersWidget(parent)
|
||||
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
view.setUniformRowHeights(True)
|
||||
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
# enable drag/drop
|
||||
view.setDragEnabled(True)
|
||||
view.viewport().setAcceptDrops(True)
|
||||
view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
|
||||
view.setDropIndicatorShown(True)
|
||||
|
||||
# add context menu
|
||||
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||
actions.addLayersActions(view)
|
||||
|
||||
view.setHeaderLabels(["Layers", ""])
|
||||
# view.setHeaderHidden(True)
|
||||
view.header().setMinimumSectionSize(1)
|
||||
view.header().setStretchLastSection(False)
|
||||
view.header().swapSections(0, 1)
|
||||
view.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
view.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
view.setColumnWidth(1, 25 * scale_multiplier)
|
||||
view.setIndentation(15 * scale_multiplier)
|
||||
view.setIconSize(QtCore.QSize(layer_icon_size * scale_multiplier, layer_icon_size * scale_multiplier))
|
||||
|
||||
tree_items = {}
|
||||
|
||||
def rebuild_buttons(item):
|
||||
layer = item_to_layer(item)
|
||||
bar = QtWidgets.QToolBar(parent=parent)
|
||||
bar.setMovable(False)
|
||||
bar.setIconSize(QtCore.QSize(visibility_icon_size * scale_multiplier, visibility_icon_size * scale_multiplier))
|
||||
a = bar.addAction(icon_visible if layer is None or layer.enabled else icon_hidden, "Toggle enabled/disabled")
|
||||
|
||||
@qt.on(a.triggered)
|
||||
def handler():
|
||||
layer.enabled = not layer.enabled
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
|
||||
view.setItemWidget(item, 1, bar)
|
||||
|
||||
def build_items(layer_infos):
|
||||
"""
|
||||
sync items in view with provided layer values, trying to delete as little items on the view as possible
|
||||
:type layer_infos: list[api.Layer]
|
||||
"""
|
||||
|
||||
# build map "parent id->list of children "
|
||||
|
||||
log.info("syncing items...")
|
||||
|
||||
# save selected layers IDs to restore item selection later
|
||||
selected_layer_ids = {item_to_layer(item).id for item in view.selectedItems()}
|
||||
log.info("selected layer IDs: %r", selected_layer_ids)
|
||||
current_item_id = None if view.currentItem() is None else item_to_layer(view.currentItem()).id
|
||||
|
||||
hierarchy = {}
|
||||
for child in layer_infos:
|
||||
if child.parent_id not in hierarchy:
|
||||
hierarchy[child.parent_id] = []
|
||||
hierarchy[child.parent_id].append(child)
|
||||
|
||||
def sync(parent_tree_item, children_list):
|
||||
while parent_tree_item.childCount() > len(children_list):
|
||||
parent_tree_item.removeChild(parent_tree_item.child(len(children_list)))
|
||||
|
||||
for index, child in enumerate(reversed(children_list)):
|
||||
if index >= parent_tree_item.childCount():
|
||||
item = QtWidgets.QTreeWidgetItem()
|
||||
item.setSizeHint(1, QtCore.QSize(1 * scale_multiplier, 25 * scale_multiplier))
|
||||
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
|
||||
parent_tree_item.addChild(item)
|
||||
else:
|
||||
item = parent_tree_item.child(index)
|
||||
|
||||
tree_items[child.id] = item
|
||||
|
||||
item.setData(0, layer_data_role, child)
|
||||
item.setText(0, child.name)
|
||||
item.setIcon(0, icon_layer if child.enabled else icon_layer_disabled)
|
||||
rebuild_buttons(item)
|
||||
|
||||
sync(item, hierarchy.get(child.id, []))
|
||||
|
||||
with qt.signals_blocked(view):
|
||||
tree_items.clear()
|
||||
sync(view.invisibleRootItem(), hierarchy.get(None, []))
|
||||
|
||||
current_item = tree_items.get(current_item_id, None)
|
||||
if current_item is not None:
|
||||
view.setCurrentItem(current_item, 0, QtCore.QItemSelectionModel.NoUpdate)
|
||||
|
||||
for i in selected_layer_ids:
|
||||
item = tree_items.get(i, None)
|
||||
if item is not None:
|
||||
item.setSelected(True)
|
||||
|
||||
@signal.on(session.events.layerListChanged, qtParent=view)
|
||||
def refresh_layer_list():
|
||||
log.info("event handler for layer list changed")
|
||||
if not session.state.layersAvailable:
|
||||
build_items([])
|
||||
else:
|
||||
build_items(session.state.all_layers)
|
||||
|
||||
update_selected_items()
|
||||
|
||||
@signal.on(session.events.currentLayerChanged, qtParent=view)
|
||||
def current_layer_changed():
|
||||
log.info("event handler for currentLayerChanged")
|
||||
layer = session.state.currentLayer.layer
|
||||
current_item = view.currentItem()
|
||||
if layer is None:
|
||||
view.setCurrentItem(None)
|
||||
return
|
||||
|
||||
prev_layer = None if current_item is None else item_to_layer(current_item)
|
||||
|
||||
if prev_layer is None or prev_layer.id != layer.id:
|
||||
item = tree_items.get(layer.id, None)
|
||||
if item is not None:
|
||||
log.info("setting current item to " + item.text(0))
|
||||
view.setCurrentItem(item, 0, QtCore.QItemSelectionModel.SelectCurrent | QtCore.QItemSelectionModel.ClearAndSelect)
|
||||
|
||||
item.setSelected(True)
|
||||
|
||||
@qt.on(view.currentItemChanged)
|
||||
def current_item_changed(curr, _):
|
||||
log.info("current item changed")
|
||||
if curr is None:
|
||||
return
|
||||
|
||||
selected_layer = item_to_layer(curr)
|
||||
|
||||
if layers.getCurrentLayer() == selected_layer:
|
||||
return
|
||||
|
||||
layers.setCurrentLayer(selected_layer)
|
||||
|
||||
@qt.on(view.itemChanged)
|
||||
def item_changed(item, column):
|
||||
log.info("item changed")
|
||||
layers.renameLayer(item_to_layer(item), item.text(column))
|
||||
|
||||
@qt.on(view.itemSelectionChanged)
|
||||
def update_selected_items():
|
||||
selection = [item_to_layer(item) for item in view.selectedItems()]
|
||||
|
||||
if selection != session.context.selected_layers(default=[]):
|
||||
log.info("new selected layers: %r", selection)
|
||||
session.context.selected_layers.set(selection)
|
||||
|
||||
refresh_layer_list()
|
||||
|
||||
return view
|
||||
50
2023/scripts/rigging_tools/ngskintools2/ui/layout.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.ui import qt
|
||||
|
||||
try:
|
||||
scale_multiplier = cmds.mayaDpiSetting(q=True, realScaleValue=True)
|
||||
except:
|
||||
# the command is not available on macos, using 1.0 for fallback
|
||||
scale_multiplier = 1
|
||||
|
||||
|
||||
def createTitledRow(title, contents, *additional_rows):
|
||||
row = QtWidgets.QFormLayout()
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
label = QtWidgets.QLabel(title)
|
||||
# label.setAlignment(QtCore.Qt.AlignRight |QtCore.Qt.)
|
||||
label.setFixedWidth(100 * scale_multiplier)
|
||||
|
||||
if contents is None:
|
||||
row.addRow(label, QtWidgets.QWidget())
|
||||
return row
|
||||
|
||||
row.addRow(label, contents)
|
||||
for i in additional_rows:
|
||||
row.addRow(None, i)
|
||||
return row
|
||||
|
||||
|
||||
class TabSetup(Object):
|
||||
def __init__(self):
|
||||
self.innerLayout = innerLayout = QtWidgets.QVBoxLayout()
|
||||
innerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
innerLayout.setSpacing(3 * scale_multiplier)
|
||||
|
||||
self.scrollArea = scrollArea = QtWidgets.QScrollArea()
|
||||
scrollArea.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
scrollArea.setWidget(qt.wrap_layout_into_widget(innerLayout))
|
||||
scrollArea.setWidgetResizable(True)
|
||||
|
||||
self.lowerButtonsRow = lowerButtonsRow = QtWidgets.QHBoxLayout()
|
||||
|
||||
self.mainLayout = mainLayout = QtWidgets.QVBoxLayout()
|
||||
mainLayout.addWidget(scrollArea)
|
||||
mainLayout.addLayout(lowerButtonsRow)
|
||||
mainLayout.setContentsMargins(7, 7, 7, 7)
|
||||
|
||||
self.tabContents = qt.wrap_layout_into_widget(mainLayout)
|
||||
223
2023/scripts/rigging_tools/ngskintools2/ui/mainwindow.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from maya import OpenMayaUI as omui
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets, QWidget, wrap_instance
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui.options import config
|
||||
|
||||
from .. import cleanup, signal, version
|
||||
from ..observableValue import ObservableValue
|
||||
from . import (
|
||||
aboutwindow,
|
||||
dialogs,
|
||||
hotkeys_setup,
|
||||
qt,
|
||||
tabLayerEffects,
|
||||
tabMirror,
|
||||
tabPaint,
|
||||
tabSetWeights,
|
||||
tabTools,
|
||||
targetui,
|
||||
updatewindow,
|
||||
)
|
||||
from .layout import scale_multiplier
|
||||
|
||||
log = getLogger("main window")
|
||||
|
||||
|
||||
def get_image_path(file_name):
|
||||
import os
|
||||
|
||||
for i in os.getenv("XBMLANGPATH", "").split(os.path.pathsep):
|
||||
result = os.path.join(i, file_name)
|
||||
if os.path.isfile(result):
|
||||
return result
|
||||
|
||||
return file_name
|
||||
|
||||
|
||||
def build_menu(parent, actions):
|
||||
menu = QtWidgets.QMenuBar(parent=parent)
|
||||
|
||||
def top_level_menu(label):
|
||||
sub_item = menu.addMenu(label)
|
||||
sub_item.setSeparatorsCollapsible(False)
|
||||
sub_item.setTearOffEnabled(True)
|
||||
return sub_item
|
||||
|
||||
sub = top_level_menu("File")
|
||||
sub.addSeparator().setText("Import/Export")
|
||||
sub.addAction(actions.importFile)
|
||||
sub.addAction(actions.exportFile)
|
||||
|
||||
sub = top_level_menu("Layers")
|
||||
sub.addSeparator().setText("Layer actions")
|
||||
sub.addAction(actions.initialize)
|
||||
sub.addAction(actions.import_v1)
|
||||
actions.addLayersActions(sub)
|
||||
sub.addSeparator().setText("Copy")
|
||||
sub.addAction(actions.transfer)
|
||||
|
||||
sub = top_level_menu("Tools")
|
||||
sub.addAction(actions.add_influences)
|
||||
sub.addAction(actions.toolsAssignFromClosestJoint)
|
||||
sub.addSeparator()
|
||||
sub.addAction(actions.transfer)
|
||||
sub.addSeparator()
|
||||
sub.addAction(actions.toolsDeleteCustomNodesOnSelection)
|
||||
sub.addAction(actions.toolsDeleteCustomNodes)
|
||||
|
||||
sub = top_level_menu("View")
|
||||
sub.addAction(actions.show_used_influences_only)
|
||||
|
||||
sub = top_level_menu("Help")
|
||||
sub.addAction(actions.documentation.user_guide)
|
||||
sub.addAction(actions.documentation.api_root)
|
||||
sub.addAction(actions.documentation.changelog)
|
||||
sub.addAction(actions.documentation.contact)
|
||||
sub.addSeparator()
|
||||
sub.addAction(actions.check_for_updates)
|
||||
sub.addAction("About...").triggered.connect(lambda: aboutwindow.show(parent))
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def build_rmb_menu_layers(view, actions):
|
||||
view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||
actions.addLayersActions(view)
|
||||
|
||||
|
||||
class MainWindowOptions:
|
||||
current_tab = ObservableValue(0)
|
||||
|
||||
|
||||
def build_ui(parent):
|
||||
"""
|
||||
:type parent: QWidget
|
||||
"""
|
||||
options = MainWindowOptions()
|
||||
window = QtWidgets.QWidget(parent)
|
||||
|
||||
session.addQtWidgetReference(window)
|
||||
|
||||
from ngSkinTools2.ui.actions import Actions
|
||||
|
||||
actions = Actions(parent=window, session=session)
|
||||
|
||||
tabs = QtWidgets.QTabWidget(window)
|
||||
|
||||
tabs.addTab(tabPaint.build_ui(tabs, actions), "Paint")
|
||||
tabs.addTab(tabSetWeights.build_ui(tabs), "Set Weights")
|
||||
tabs.addTab(tabMirror.build_ui(tabs), "Mirror")
|
||||
tabs.addTab(tabLayerEffects.build_ui(), "Effects")
|
||||
tabs.addTab(tabTools.build_ui(actions, session), "Tools")
|
||||
|
||||
@signal.on(options.current_tab.changed)
|
||||
def set_current_tab():
|
||||
tabs.setCurrentIndex(options.current_tab())
|
||||
|
||||
layers_toolbar = QtWidgets.QToolBar()
|
||||
layers_toolbar.addAction(actions.addLayer)
|
||||
layers_toolbar.setOrientation(QtCore.Qt.Vertical)
|
||||
|
||||
spacing_h = 5
|
||||
spacing_v = 5
|
||||
|
||||
layers_row = targetui.build_target_ui(window, actions, session)
|
||||
|
||||
split = QtWidgets.QSplitter(orientation=QtCore.Qt.Vertical, parent=window)
|
||||
split.addWidget(layers_row)
|
||||
split.addWidget(tabs)
|
||||
split.setStretchFactor(0, 2)
|
||||
split.setStretchFactor(1, 3)
|
||||
split.setContentsMargins(spacing_h, spacing_v, spacing_h, spacing_v)
|
||||
|
||||
def build_icon_label():
|
||||
w = QWidget()
|
||||
w.setStyleSheet("background-color: #dcce87;color: #373737;")
|
||||
l = QtWidgets.QHBoxLayout()
|
||||
icon = QtWidgets.QLabel()
|
||||
icon.setPixmap(QtGui.QIcon(":/error.png").pixmap(16 * scale_multiplier, 16 * scale_multiplier))
|
||||
icon.setFixedSize(16 * scale_multiplier, 16 * scale_multiplier)
|
||||
text = QtWidgets.QLabel("<placeholder>")
|
||||
text.setWordWrap(True)
|
||||
text.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||
|
||||
l.addWidget(icon)
|
||||
l.addWidget(text)
|
||||
w.setContentsMargins(0, 0, 0, 0)
|
||||
w.setLayout(l)
|
||||
|
||||
return w, text.setText
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(window)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(build_menu(window, actions))
|
||||
layout.addWidget(split)
|
||||
|
||||
window.setLayout(layout)
|
||||
|
||||
hotkeys_setup.install_hotkeys()
|
||||
|
||||
dialogs.promptsParent = window
|
||||
|
||||
if config.checkForUpdatesAtStartup():
|
||||
updatewindow.silent_check_and_show_if_available(qt.mainWindow)
|
||||
|
||||
return window, options
|
||||
|
||||
|
||||
DOCK_NAME = 'ngSkinTools2_mainWindow'
|
||||
|
||||
|
||||
def workspace_control_permanent_script():
|
||||
from ngSkinTools2 import workspace_control_main_window
|
||||
|
||||
return "import {f.__module__}; {f.__module__}.{f.__name__}()".format(f=workspace_control_main_window)
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def open():
|
||||
"""
|
||||
opens main window
|
||||
"""
|
||||
|
||||
if not cmds.workspaceControl(DOCK_NAME, q=True, exists=True):
|
||||
# build UI script in type-safe manner
|
||||
|
||||
cmds.workspaceControl(
|
||||
DOCK_NAME,
|
||||
retain=False,
|
||||
floating=True,
|
||||
# ttc=["AttributeEditor",-1],
|
||||
uiScript=workspace_control_permanent_script(),
|
||||
)
|
||||
|
||||
# bring tab to front
|
||||
cmds.evalDeferred(lambda *args: cmds.workspaceControl(DOCK_NAME, e=True, r=True))
|
||||
|
||||
def close():
|
||||
from maya import cmds
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
cmds.deleteUI(DOCK_NAME)
|
||||
except:
|
||||
pass
|
||||
pass
|
||||
|
||||
cleanup.registerCleanupHandler(close)
|
||||
|
||||
|
||||
def resume_in_workspace_control():
|
||||
"""
|
||||
this method is responsible for resuming workspace control when Maya is building/restoring UI as part of it's
|
||||
workspace management cycle (open UI for the first time, restart maya, change workspace, etc)
|
||||
"""
|
||||
|
||||
cmds.workspaceControl(DOCK_NAME, e=True, label="ngSkinTools " + version.pluginVersion())
|
||||
widget = wrap_instance(omui.MQtUtil.findControl(DOCK_NAME), QtWidgets.QWidget)
|
||||
|
||||
ui, _ = build_ui(widget)
|
||||
widget.layout().addWidget(ui)
|
||||
22
2023/scripts/rigging_tools/ngskintools2/ui/model_binds.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.pyside import QtWidgets
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
|
||||
|
||||
def bind(ui, model):
|
||||
if isinstance(ui, QtWidgets.QCheckBox):
|
||||
ui.setChecked(model())
|
||||
|
||||
@qt.on(ui.stateChanged)
|
||||
def update_model():
|
||||
model.set(ui.isChecked())
|
||||
|
||||
elif isinstance(ui, widgets.NumberSliderGroup):
|
||||
ui.set_value(model())
|
||||
|
||||
@signal.on(ui.valueChanged)
|
||||
def update_model():
|
||||
model.set(ui.value())
|
||||
|
||||
else:
|
||||
raise Exception("could not bind control to model")
|
||||
201
2023/scripts/rigging_tools/ngskintools2/ui/options.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import json
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.python_compatibility import Object, is_string
|
||||
from ngSkinTools2.observableValue import ObservableValue
|
||||
|
||||
log = getLogger("plugin")
|
||||
|
||||
|
||||
class Value(Object):
|
||||
def __init__(self, value=None):
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
def getInt(self):
|
||||
try:
|
||||
return int(self.get())
|
||||
except:
|
||||
return 0
|
||||
|
||||
|
||||
class PersistentValue(Value):
|
||||
"""
|
||||
persistent value can store itself into Maya's "option vars" array
|
||||
"""
|
||||
|
||||
def __init__(self, name, default_value=None, prefix=None):
|
||||
Value.__init__(self)
|
||||
|
||||
if prefix is None:
|
||||
prefix = VAR_OPTION_PREFIX
|
||||
self.name = prefix + name
|
||||
self.default_value = default_value
|
||||
self.value = load_option(self.name, self.default_value)
|
||||
|
||||
def set(self, value):
|
||||
Value.set(self, value)
|
||||
save_option(self.name, self.value)
|
||||
|
||||
|
||||
class PersistentDict(Object):
|
||||
def __init__(self, name, default_values=None):
|
||||
if default_values is None:
|
||||
default_values = {}
|
||||
self.persistence = PersistentValue(name=name, default_value=json.dumps(default_values))
|
||||
|
||||
def __get_values(self):
|
||||
# type: () -> dict
|
||||
return json.loads(self.persistence.get())
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.__get_values().get(item, None)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
v = self.__get_values()
|
||||
v[key] = value
|
||||
self.persistence.set(json.dumps(v))
|
||||
|
||||
|
||||
def load_option(var_name, default_value):
|
||||
"""
|
||||
loads value from optionVar
|
||||
"""
|
||||
|
||||
from ngSkinTools2 import BATCH_MODE
|
||||
|
||||
if BATCH_MODE:
|
||||
return default_value
|
||||
|
||||
if cmds.optionVar(exists=var_name):
|
||||
return cmds.optionVar(q=var_name)
|
||||
|
||||
return default_value
|
||||
|
||||
|
||||
def save_option(varName, value):
|
||||
"""
|
||||
saves option via optionVar
|
||||
"""
|
||||
from ngSkinTools2 import BATCH_MODE
|
||||
|
||||
if BATCH_MODE:
|
||||
return
|
||||
|
||||
# variable does not exist, attempt to save it
|
||||
key = None
|
||||
if isinstance(value, float):
|
||||
key = 'fv'
|
||||
elif isinstance(value, int):
|
||||
key = 'iv'
|
||||
elif is_string(value):
|
||||
key = 'sv'
|
||||
else:
|
||||
raise ValueError("could not save option %s: invalid value %r" % (varName, value))
|
||||
|
||||
kvargs = {key: (varName, value)}
|
||||
log.info("saving optionvar: %r", kvargs)
|
||||
cmds.optionVar(**kvargs)
|
||||
|
||||
|
||||
VAR_OPTION_PREFIX = 'ngSkinTools2_'
|
||||
|
||||
|
||||
def delete_custom_options():
|
||||
for varName in cmds.optionVar(list=True):
|
||||
if varName.startswith(VAR_OPTION_PREFIX):
|
||||
cmds.optionVar(remove=varName)
|
||||
|
||||
cmds.windowPref('MirrorWeightsWindow', ra=True)
|
||||
|
||||
|
||||
def build_config_property(name, default_value, doc=''):
|
||||
return property(lambda self: self.__get_value__(name, default_value), lambda self, val: self.__set_value__(name, val), doc=doc)
|
||||
|
||||
|
||||
class Config(Object):
|
||||
"""
|
||||
Maya-wide settings for ngSkinTools2
|
||||
"""
|
||||
|
||||
mirrorInfluencesDefaults = build_config_property('mirrorInfluencesDefaults', "{}") # type: string
|
||||
|
||||
InfluencesSortUnsorted = 'unsorted'
|
||||
InfluencesSortDescending = 'descending'
|
||||
|
||||
def __init__(self):
|
||||
from ngSkinTools2.api.mirror import MirrorOptions
|
||||
|
||||
self.__storage__ = PersistentValue("config", "{}")
|
||||
self.__state__ = self.load()
|
||||
|
||||
self.unique_client_id = PersistentValue('updateCheckUniqueClientId')
|
||||
|
||||
self.checkForUpdatesAtStartup = self.build_observable_value('checkForUpdatesAtStartup', True)
|
||||
self.influences_show_used_influences_only = self.build_observable_value("influencesViewShowUsedInfluencesOnly", False)
|
||||
|
||||
# influences sort is not a simple "true/false" flag to allow different sorting methods in the future.
|
||||
self.influences_sort = self.build_observable_value("influencesSort", Config.InfluencesSortUnsorted)
|
||||
|
||||
default_mirror_options = MirrorOptions()
|
||||
self.mirror_direction = self.build_observable_value("mirrorDirection", default_mirror_options.direction)
|
||||
self.mirror_dq = self.build_observable_value("mirrorDq", default_mirror_options.mirrorDq)
|
||||
self.mirror_mask = self.build_observable_value("mirrorMask", default_mirror_options.mirrorMask)
|
||||
self.mirror_weights = self.build_observable_value("mirrorWeights", default_mirror_options.mirrorWeights)
|
||||
|
||||
def __get_value__(self, name, default_value):
|
||||
result = self.__state__.get(name, default_value)
|
||||
log.info("config: return %s=%r", name, result)
|
||||
return result
|
||||
|
||||
def __set_value__(self, name, value):
|
||||
log.info("config: save %s=%r", name, value)
|
||||
self.__state__[name] = value
|
||||
self.save()
|
||||
|
||||
def build_observable_value(self, name, default_value):
|
||||
"""
|
||||
builds ObservableValue that is loaded and persisted into config when changed
|
||||
:type name: str
|
||||
:rtype: ngSkinTools2.observableValue.ObservableValue
|
||||
"""
|
||||
result = ObservableValue(self.__get_value__(name=name, default_value=default_value))
|
||||
|
||||
@signal.on(result.changed)
|
||||
def save():
|
||||
self.__set_value__(name, result())
|
||||
|
||||
return result
|
||||
|
||||
def load(self):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
return json.loads(self.__storage__.get())
|
||||
except:
|
||||
return {}
|
||||
|
||||
def save(self):
|
||||
self.__storage__.set(json.dumps(self.__state__))
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
|
||||
def bind_checkbox(cb, option):
|
||||
from ngSkinTools2.ui import qt
|
||||
|
||||
cb.setChecked(option())
|
||||
|
||||
@qt.on(cb.toggled)
|
||||
def update():
|
||||
option.set(cb.isChecked())
|
||||
|
||||
return cb
|
||||
@@ -0,0 +1,24 @@
|
||||
from maya import mel
|
||||
|
||||
|
||||
def definePaintContextCallbacks():
|
||||
"""
|
||||
Maya expects some mel procedures to be present for paint context metadata
|
||||
"""
|
||||
|
||||
mel.eval(
|
||||
"""
|
||||
global proc ngst2PaintContextProperties() {
|
||||
setUITemplate -pushTemplate DefaultTemplate;
|
||||
setUITemplate -popTemplate;
|
||||
|
||||
}
|
||||
|
||||
global proc ngst2PaintContextValues(string $toolName)
|
||||
{
|
||||
string $icon = "ngSkinToolsShelfIcon.png";
|
||||
string $help = "ngSkinTools2 - paint skin weights";
|
||||
toolPropertySetCommon $toolName $icon $help;
|
||||
}
|
||||
"""
|
||||
)
|
||||
39
2023/scripts/rigging_tools/ngskintools2/ui/parallel.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from threading import Thread
|
||||
|
||||
from maya import utils
|
||||
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
class ParallelTask(Object):
|
||||
def __init__(self):
|
||||
self.__run_handlers = []
|
||||
self.__done_handlers = []
|
||||
|
||||
def add_run_handler(self, handler):
|
||||
self.__run_handlers.append(handler)
|
||||
|
||||
def add_done_handler(self, handler):
|
||||
self.__done_handlers.append(handler)
|
||||
|
||||
def start(self, async_exec=True):
|
||||
def done():
|
||||
for i in self.__done_handlers:
|
||||
i(self)
|
||||
|
||||
def thread():
|
||||
for i in self.__run_handlers:
|
||||
i(self)
|
||||
if async_exec:
|
||||
utils.executeDeferred(done)
|
||||
else:
|
||||
done()
|
||||
|
||||
self.current_thread = Thread(target=thread)
|
||||
if async_exec:
|
||||
self.current_thread.start()
|
||||
else:
|
||||
self.current_thread.run()
|
||||
|
||||
def wait(self):
|
||||
self.current_thread.join()
|
||||
128
2023/scripts/rigging_tools/ngskintools2/ui/qt.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import os
|
||||
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets, get_main_window
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
def wrap_layout_into_widget(layout):
|
||||
w = QtWidgets.QWidget()
|
||||
w.setLayout(layout)
|
||||
return w
|
||||
|
||||
|
||||
def signals_blocked(widget):
|
||||
return SignalBlockContext(widget)
|
||||
|
||||
|
||||
class SignalBlockContext(Object):
|
||||
def __init__(self, widget):
|
||||
self.widget = widget
|
||||
|
||||
def __enter__(self):
|
||||
self.prevState = self.widget.blockSignals(True)
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.widget.blockSignals(self.prevState)
|
||||
|
||||
|
||||
def on(*signals):
|
||||
"""
|
||||
decorator for function: list signals that should fire for this function.
|
||||
|
||||
instead of:
|
||||
|
||||
def something():
|
||||
...
|
||||
btn.clicked.connect(something)
|
||||
|
||||
do:
|
||||
|
||||
@qt.on(btn.clicked)
|
||||
def something():
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(fn):
|
||||
for i in signals:
|
||||
i.connect(fn)
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class SingleWindowPolicy(Object):
|
||||
def __init__(self):
|
||||
self.lastWindow = None
|
||||
|
||||
def setCurrent(self, window):
|
||||
if self.lastWindow:
|
||||
self.lastWindow.close()
|
||||
self.lastWindow = window
|
||||
|
||||
on(window.finished)(self.cleanup)
|
||||
|
||||
def cleanup(self):
|
||||
self.lastWindow = None
|
||||
|
||||
|
||||
def alternative_palette_light():
|
||||
palette = QtGui.QPalette()
|
||||
palette.setColor(QtGui.QPalette.Window, QtGui.QColor(243, 244, 246))
|
||||
palette.setColor(QtGui.QPalette.WindowText, QtGui.QColor(33, 37, 41))
|
||||
return palette
|
||||
|
||||
|
||||
def bind_action_to_button(action, button):
|
||||
"""
|
||||
|
||||
:type button: PySide2.QtWidgets.QPushButton
|
||||
:type action: PySide2.QtWidgets.QAction
|
||||
"""
|
||||
|
||||
@on(action.changed)
|
||||
def update_state():
|
||||
button.setText(action.text())
|
||||
button.setEnabled(action.isEnabled())
|
||||
button.setToolTip(action.toolTip())
|
||||
button.setStatusTip(action.statusTip())
|
||||
button.setVisible(action.isVisible())
|
||||
if action.isCheckable():
|
||||
button.setChecked(action.isChecked())
|
||||
|
||||
button.setCheckable(action.isCheckable())
|
||||
|
||||
on(button.clicked)(action.trigger)
|
||||
update_state()
|
||||
|
||||
return button
|
||||
|
||||
|
||||
images_path = os.path.join(os.path.dirname(__file__), "images")
|
||||
|
||||
|
||||
def icon_path(path):
|
||||
if path.startswith(':'):
|
||||
return path
|
||||
return os.path.join(images_path, path)
|
||||
|
||||
|
||||
def scaled_icon(path, w, h):
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
return QtGui.QIcon(
|
||||
QtGui.QPixmap(icon_path(path)).scaled(w * scale_multiplier, h * scale_multiplier, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
)
|
||||
|
||||
|
||||
def image_icon(file_name):
|
||||
return QtGui.QIcon(icon_path(file_name))
|
||||
|
||||
|
||||
def select_data(combo, data):
|
||||
"""
|
||||
set combo box index to data index
|
||||
"""
|
||||
combo.setCurrentIndex(combo.findData(data))
|
||||
|
||||
|
||||
mainWindow = get_main_window()
|
||||
41
2023/scripts/rigging_tools/ngskintools2/ui/shelf.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from maya import cmds, mel
|
||||
|
||||
|
||||
def install_shelf():
|
||||
"""
|
||||
checks if there's ngSkintTools shelf installed, and if not, creates one.
|
||||
|
||||
this runs each time Maya starts (via Autoloader's ngSkinTools_load.mel) - avoid duplication, like creating things
|
||||
that already exist.
|
||||
"""
|
||||
|
||||
# don't do anything if we're in batch mode. UI commands are not available
|
||||
if cmds.about(batch=True) == 1:
|
||||
return
|
||||
|
||||
maya_shelf = mel.eval("$tempngSkinTools2Var=$gShelfTopLevel")
|
||||
existing_shelves = cmds.shelfTabLayout(maya_shelf, q=True, tabLabel=True)
|
||||
|
||||
parent_shelf = 'ngSkinTools2'
|
||||
|
||||
if parent_shelf in existing_shelves:
|
||||
return
|
||||
|
||||
mel.eval('addNewShelfTab ' + parent_shelf)
|
||||
cmds.shelfButton(
|
||||
parent=parent_shelf,
|
||||
enable=1,
|
||||
visible=1,
|
||||
preventOverride=0,
|
||||
label="ngst",
|
||||
annotation="opens ngSkinTools2 UI",
|
||||
image="ngSkinTools2ShelfIcon.png",
|
||||
style="iconOnly",
|
||||
noBackground=1,
|
||||
align="center",
|
||||
marginWidth=1,
|
||||
marginHeight=1,
|
||||
command="import ngSkinTools2; ngSkinTools2.open_ui()",
|
||||
sourceType="python",
|
||||
commandRepeatable=0,
|
||||
)
|
||||
200
2023/scripts/rigging_tools/ngskintools2/ui/tabLayerEffects.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.mirror import MirrorOptions
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
|
||||
log = getLogger("tab layer effects")
|
||||
|
||||
|
||||
def check_state_from_boolean_states(states):
|
||||
"""
|
||||
for a list of booleans, return checkbox check state - one of Qt.Checked, Qt.Unchecked and Qt.PartiallyChecked
|
||||
|
||||
:type states: list[bool]
|
||||
"""
|
||||
current_state = None
|
||||
for i in states:
|
||||
if current_state is None:
|
||||
current_state = i
|
||||
continue
|
||||
|
||||
if i != current_state:
|
||||
return QtCore.Qt.PartiallyChecked
|
||||
|
||||
if current_state:
|
||||
return QtCore.Qt.Checked
|
||||
|
||||
return QtCore.Qt.Unchecked
|
||||
|
||||
|
||||
def build_ui():
|
||||
def list_layers():
|
||||
return [] if not session.state.layersAvailable else session.context.selected_layers(default=[])
|
||||
|
||||
def build_properties():
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
opacity = widgets.NumberSliderGroup(tooltip="multiply layer mask to control overall transparency of the layer.")
|
||||
opacity.set_value(1.0)
|
||||
layout.addLayout(createTitledRow("Opacity:", opacity.layout()))
|
||||
|
||||
def default_selection_opacity(layers):
|
||||
if len(layers) > 0:
|
||||
return layers[0].opacity
|
||||
return 1.0
|
||||
|
||||
@signal.on(session.context.selected_layers.changed, session.events.currentLayerChanged, qtParent=tab.tabContents)
|
||||
def update_values():
|
||||
layers = list_layers()
|
||||
enabled = len(layers) > 0
|
||||
opacity.set_enabled(enabled)
|
||||
opacity.set_value(default_selection_opacity(layers))
|
||||
|
||||
@signal.on(opacity.valueChanged)
|
||||
def opacity_edited():
|
||||
layers = list_layers()
|
||||
# avoid changing opacity of all selected layers if we just changed slider value based on changed layer selection
|
||||
if opacity.value() == default_selection_opacity(layers):
|
||||
return
|
||||
val = opacity.value()
|
||||
for i in list_layers():
|
||||
if abs(i.opacity - val) > 0.00001:
|
||||
i.opacity = val
|
||||
|
||||
update_values()
|
||||
|
||||
group = QtWidgets.QGroupBox("Layer properties")
|
||||
group.setLayout(layout)
|
||||
|
||||
return group
|
||||
|
||||
def build_mirror_effect():
|
||||
def configure_mirror_all_layers(option, value):
|
||||
for i in list_layers():
|
||||
i.effects.configure_mirror(**{option: value})
|
||||
|
||||
mirror_direction = QtWidgets.QComboBox()
|
||||
mirror_direction.addItem("Positive to negative", MirrorOptions.directionPositiveToNegative)
|
||||
mirror_direction.addItem("Negative to positive", MirrorOptions.directionNegativeToPositive)
|
||||
mirror_direction.addItem("Flip", MirrorOptions.directionFlip)
|
||||
mirror_direction.setMinimumWidth(1)
|
||||
|
||||
@qt.on(mirror_direction.currentIndexChanged)
|
||||
def value_changed():
|
||||
configure_mirror_all_layers("mirror_direction", mirror_direction.currentData())
|
||||
|
||||
influences = QtWidgets.QCheckBox("Influence weights")
|
||||
mask = QtWidgets.QCheckBox("Layer mask")
|
||||
dq = QtWidgets.QCheckBox("Dual quaternion weights")
|
||||
|
||||
def configure_checkbox(checkbox, option):
|
||||
@qt.on(checkbox.stateChanged)
|
||||
def update_pref():
|
||||
if checkbox.checkState() == QtCore.Qt.PartiallyChecked:
|
||||
checkbox.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
enabled = checkbox.checkState() == QtCore.Qt.Checked
|
||||
configure_mirror_all_layers(option, enabled)
|
||||
|
||||
configure_checkbox(influences, 'mirror_weights')
|
||||
configure_checkbox(mask, 'mirror_mask')
|
||||
configure_checkbox(dq, 'mirror_dq')
|
||||
|
||||
@signal.on(session.context.selected_layers.changed, session.events.currentLayerChanged, qtParent=tab.tabContents)
|
||||
def update_values():
|
||||
layers = list_layers()
|
||||
with qt.signals_blocked(influences):
|
||||
influences.setCheckState(check_state_from_boolean_states([i.effects.mirror_weights for i in layers]))
|
||||
with qt.signals_blocked(mask):
|
||||
mask.setCheckState(check_state_from_boolean_states([i.effects.mirror_mask for i in layers]))
|
||||
with qt.signals_blocked(dq):
|
||||
dq.setCheckState(check_state_from_boolean_states([i.effects.mirror_dq for i in layers]))
|
||||
with qt.signals_blocked(mirror_direction):
|
||||
qt.select_data(mirror_direction, MirrorOptions.directionPositiveToNegative if not layers else layers[0].effects.mirror_direction)
|
||||
|
||||
update_values()
|
||||
|
||||
def elements():
|
||||
result = QtWidgets.QVBoxLayout()
|
||||
|
||||
for i in [influences, mask, dq]:
|
||||
i.setTristate(True)
|
||||
result.addWidget(i)
|
||||
return result
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Mirror effect on:", elements()))
|
||||
layout.addLayout(createTitledRow("Mirror direction:", mirror_direction))
|
||||
|
||||
group = QtWidgets.QGroupBox("Mirror")
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def build_skin_properties():
|
||||
use_max_influences = QtWidgets.QCheckBox("Limit max influences per vertex")
|
||||
max_influences = widgets.NumberSliderGroup(min_value=1, max_value=5, tooltip="", value_type=int)
|
||||
use_prune_weight = QtWidgets.QCheckBox("Prune small weights before writing to skin cluster")
|
||||
|
||||
prune_weight = widgets.NumberSliderGroup(decimals=6, min_value=0.000001, max_value=0.05, tooltip="")
|
||||
prune_weight.set_value(prune_weight.min_value)
|
||||
prune_weight.set_expo("start", 3)
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def update_ui():
|
||||
group.setEnabled(session.state.layersAvailable)
|
||||
|
||||
if session.state.layersAvailable:
|
||||
with qt.signals_blocked(use_max_influences):
|
||||
use_max_influences.setChecked(session.state.layers.influence_limit_per_vertex != 0)
|
||||
with qt.signals_blocked(max_influences):
|
||||
max_influences.set_value(session.state.layers.influence_limit_per_vertex if use_max_influences.isChecked() else 4)
|
||||
with qt.signals_blocked(use_prune_weight):
|
||||
use_prune_weight.setChecked(session.state.layers.prune_weights_filter_threshold != 0)
|
||||
with qt.signals_blocked(prune_weight):
|
||||
prune_weight.set_value(session.state.layers.prune_weights_filter_threshold if use_prune_weight.isChecked() else 0.0001)
|
||||
|
||||
update_ui_enabled()
|
||||
|
||||
def update_ui_enabled():
|
||||
max_influences.set_enabled(use_max_influences.isChecked())
|
||||
prune_weight.set_enabled(use_prune_weight.isChecked())
|
||||
|
||||
@qt.on(use_max_influences.stateChanged, use_prune_weight.stateChanged)
|
||||
@signal.on(max_influences.valueChanged, prune_weight.valueChanged)
|
||||
def update_values():
|
||||
log.info("updating effects tab")
|
||||
|
||||
if session.state.layersAvailable:
|
||||
session.state.layers.influence_limit_per_vertex = max_influences.value() if use_max_influences.isChecked() else 0
|
||||
session.state.layers.prune_weights_filter_threshold = 0 if not use_prune_weight.isChecked() else prune_weight.value_trimmed()
|
||||
|
||||
update_ui_enabled()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(use_max_influences)
|
||||
layout.addLayout(createTitledRow("Max influences:", max_influences.layout()))
|
||||
layout.addWidget(use_prune_weight)
|
||||
layout.addLayout(createTitledRow("Prune below:", prune_weight.layout()))
|
||||
|
||||
group = QtWidgets.QGroupBox("Skin Properties")
|
||||
group.setLayout(layout)
|
||||
|
||||
update_ui()
|
||||
|
||||
return group
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_properties())
|
||||
tab.innerLayout.addWidget(build_mirror_effect())
|
||||
tab.innerLayout.addWidget(build_skin_properties())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
215
2023/scripts/rigging_tools/ngskintools2/ui/tabMirror.py
Normal file
@@ -0,0 +1,215 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import Mirror, MirrorOptions, VertexTransferMode
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.mirror import set_reference_mesh_from_selection
|
||||
from ngSkinTools2.api.pyside import QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
from ngSkinTools2.ui.options import bind_checkbox, config
|
||||
from ngSkinTools2.ui.widgets import NumberSliderGroup
|
||||
|
||||
log = getLogger("tab paint")
|
||||
|
||||
|
||||
def build_ui(parent_window):
|
||||
def build_mirroring_options_group():
|
||||
def get_mirror_direction():
|
||||
mirror_direction = QtWidgets.QComboBox()
|
||||
mirror_direction.addItem("Guess from stroke", MirrorOptions.directionGuess)
|
||||
mirror_direction.addItem("Positive to negative", MirrorOptions.directionPositiveToNegative)
|
||||
mirror_direction.addItem("Negative to positive", MirrorOptions.directionNegativeToPositive)
|
||||
mirror_direction.addItem("Flip", MirrorOptions.directionFlip)
|
||||
mirror_direction.setMinimumWidth(1)
|
||||
qt.select_data(mirror_direction, config.mirror_direction())
|
||||
|
||||
@qt.on(mirror_direction.currentIndexChanged)
|
||||
def value_changed():
|
||||
config.mirror_direction.set(mirror_direction.currentData())
|
||||
|
||||
return mirror_direction
|
||||
|
||||
def axis():
|
||||
mirror_axis = QtWidgets.QComboBox()
|
||||
mirror_axis.addItem("X", 'x')
|
||||
mirror_axis.addItem("Y", 'y')
|
||||
mirror_axis.addItem("Z", 'z')
|
||||
|
||||
@qt.on(mirror_axis.currentIndexChanged)
|
||||
def value_changed():
|
||||
session.state.mirror().axis = mirror_axis.currentData()
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def target_changed():
|
||||
if session.state.layersAvailable:
|
||||
qt.select_data(mirror_axis, session.state.mirror().axis)
|
||||
|
||||
target_changed()
|
||||
|
||||
return mirror_axis
|
||||
|
||||
def mirror_seam_width():
|
||||
seam_width_ctrl = NumberSliderGroup(max_value=100)
|
||||
|
||||
@signal.on(seam_width_ctrl.valueChanged)
|
||||
def value_changed():
|
||||
session.state.mirror().seam_width = seam_width_ctrl.value()
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def update_values():
|
||||
if session.state.layersAvailable:
|
||||
seam_width_ctrl.set_value(session.state.mirror().seam_width)
|
||||
|
||||
update_values()
|
||||
|
||||
return seam_width_ctrl.layout()
|
||||
|
||||
def elements():
|
||||
influences = bind_checkbox(QtWidgets.QCheckBox("Influence weights"), config.mirror_weights)
|
||||
mask = bind_checkbox(QtWidgets.QCheckBox("Layer mask"), config.mirror_mask)
|
||||
dq = bind_checkbox(QtWidgets.QCheckBox("Dual quaternion weights"), config.mirror_dq)
|
||||
|
||||
return influences, mask, dq
|
||||
|
||||
result = QtWidgets.QGroupBox("Mirroring options")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addLayout(createTitledRow("Axis:", axis()))
|
||||
layout.addLayout(createTitledRow("Direction:", get_mirror_direction()))
|
||||
layout.addLayout(createTitledRow("Seam width:", mirror_seam_width()))
|
||||
layout.addLayout(createTitledRow("Elements to mirror:", *elements()))
|
||||
|
||||
return result
|
||||
|
||||
def vertex_mapping_group():
|
||||
# noinspection PyShadowingNames
|
||||
def mirror_mesh_group():
|
||||
mesh_name_edit = QtWidgets.QLineEdit("mesh1")
|
||||
mesh_name_edit.setReadOnly(True)
|
||||
select_button = QtWidgets.QPushButton("Select")
|
||||
create_button = QtWidgets.QPushButton("Create")
|
||||
set_button = QtWidgets.QPushButton("Set")
|
||||
set_button.setToolTip("Select symmetry mesh and a skinned target first")
|
||||
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.addWidget(mesh_name_edit)
|
||||
layout.addWidget(create_button)
|
||||
layout.addWidget(select_button)
|
||||
layout.addWidget(set_button)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_ui():
|
||||
if not session.state.layersAvailable:
|
||||
return
|
||||
|
||||
mesh = Mirror(session.state.selectedSkinCluster).get_reference_mesh()
|
||||
mesh_name_edit.setText(mesh or "")
|
||||
|
||||
def select_mesh(m):
|
||||
if m is None:
|
||||
return
|
||||
|
||||
from maya import cmds
|
||||
|
||||
cmds.setToolTo("moveSuperContext")
|
||||
cmds.selectMode(component=True)
|
||||
cmds.select(m + ".vtx[*]", r=True)
|
||||
cmds.hilite(m, replace=True)
|
||||
cmds.viewFit()
|
||||
|
||||
@qt.on(select_button.clicked)
|
||||
def select_handler():
|
||||
select_mesh(Mirror(session.state.selectedSkinCluster).get_reference_mesh())
|
||||
|
||||
@qt.on(create_button.clicked)
|
||||
def create():
|
||||
if not session.state.layersAvailable:
|
||||
return
|
||||
|
||||
m = Mirror(session.state.selectedSkinCluster)
|
||||
mesh = m.get_reference_mesh()
|
||||
if mesh is None:
|
||||
mesh = m.build_reference_mesh()
|
||||
|
||||
update_ui()
|
||||
select_mesh(mesh)
|
||||
|
||||
@qt.on(set_button.clicked)
|
||||
def set_clicked():
|
||||
set_reference_mesh_from_selection()
|
||||
update_ui()
|
||||
|
||||
update_ui()
|
||||
|
||||
return layout
|
||||
|
||||
vertex_mapping_mode = QtWidgets.QComboBox()
|
||||
vertex_mapping_mode.addItem("Closest point on surface", VertexTransferMode.closestPoint)
|
||||
vertex_mapping_mode.addItem("UV space", VertexTransferMode.uvSpace)
|
||||
|
||||
result = QtWidgets.QGroupBox("Vertex Mapping")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Mapping mode:", vertex_mapping_mode))
|
||||
layout.addLayout(createTitledRow("Symmetry mesh:", mirror_mesh_group()))
|
||||
result.setLayout(layout)
|
||||
|
||||
@qt.on(vertex_mapping_mode.currentIndexChanged)
|
||||
def value_changed():
|
||||
session.state.mirror().vertex_transfer_mode = vertex_mapping_mode.currentData()
|
||||
|
||||
@signal.on(session.events.targetChanged)
|
||||
def target_changed():
|
||||
if session.state.layersAvailable:
|
||||
qt.select_data(vertex_mapping_mode, session.state.mirror().vertex_transfer_mode)
|
||||
|
||||
return result
|
||||
|
||||
def influence_mapping_group():
|
||||
def edit_mapping():
|
||||
mapping = QtWidgets.QPushButton("Preview and edit mapping")
|
||||
|
||||
single_window_policy = qt.SingleWindowPolicy()
|
||||
|
||||
@qt.on(mapping.clicked)
|
||||
def edit():
|
||||
from ngSkinTools2.ui import influenceMappingUI
|
||||
|
||||
window = influenceMappingUI.open_ui_for_mesh(parent_window, session.state.selectedSkinCluster)
|
||||
single_window_policy.setCurrent(window)
|
||||
|
||||
return mapping
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(edit_mapping())
|
||||
|
||||
result = QtWidgets.QGroupBox("Influences mapping")
|
||||
result.setLayout(layout)
|
||||
return result
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_mirroring_options_group())
|
||||
tab.innerLayout.addWidget(vertex_mapping_group())
|
||||
tab.innerLayout.addWidget(influence_mapping_group())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
btn_mirror = QtWidgets.QPushButton("Mirror")
|
||||
tab.lowerButtonsRow.addWidget(btn_mirror)
|
||||
|
||||
@qt.on(btn_mirror.clicked)
|
||||
def mirror_clicked():
|
||||
if session.state.currentLayer.layer:
|
||||
mirror_options = MirrorOptions()
|
||||
mirror_options.direction = config.mirror_direction()
|
||||
mirror_options.mirrorDq = config.mirror_dq()
|
||||
mirror_options.mirrorMask = config.mirror_mask()
|
||||
mirror_options.mirrorWeights = config.mirror_weights()
|
||||
|
||||
session.state.mirror().mirror(mirror_options)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_ui():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_ui()
|
||||
|
||||
return tab.tabContents
|
||||
428
2023/scripts/rigging_tools/ngskintools2/ui/tabPaint.py
Normal file
@@ -0,0 +1,428 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import BrushShape, PaintMode, PaintTool, WeightsDisplayMode
|
||||
from ngSkinTools2.api import eventtypes as et
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.paint import BrushProjectionMode, MaskDisplayMode
|
||||
from ngSkinTools2.api.pyside import QAction, QActionGroup, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
from ngSkinTools2.ui.qt import bind_action_to_button
|
||||
|
||||
log = getLogger("tab paint")
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def build_ui(parent, global_actions):
|
||||
"""
|
||||
:type parent: PySide2.QtWidgets.QWidget
|
||||
:type global_actions: ngSkinTools2.ui.actions.Actions
|
||||
"""
|
||||
paint = session.paint_tool
|
||||
# TODO: move paint model to session maybe?
|
||||
|
||||
on_signal = session.signal_hub.on
|
||||
|
||||
def update_ui():
|
||||
pass # noop until it's defined
|
||||
|
||||
def build_brush_settings_group():
|
||||
def brush_mode_row3():
|
||||
row = QtWidgets.QVBoxLayout()
|
||||
|
||||
group = QActionGroup(parent)
|
||||
|
||||
actions = {}
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def create_brush_mode_button(t, mode, label, tooltip):
|
||||
a = QAction(label, parent)
|
||||
a.setToolTip(tooltip)
|
||||
a.setCheckable(True)
|
||||
actions[mode] = a
|
||||
group.addAction(a)
|
||||
|
||||
@qt.on(a.toggled)
|
||||
def toggled(checked):
|
||||
if checked and paint.paint_mode != mode:
|
||||
paint.paint_mode = mode
|
||||
|
||||
t.addAction(a)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_brush_mode_button(t, PaintMode.replace, "Replace", "Whatever")
|
||||
create_brush_mode_button(t, PaintMode.add, "Add", "")
|
||||
create_brush_mode_button(t, PaintMode.scale, "Scale", "")
|
||||
row.addWidget(t)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_brush_mode_button(t, PaintMode.smooth, "Smooth", "")
|
||||
create_brush_mode_button(t, PaintMode.sharpen, "Sharpen", "")
|
||||
row.addWidget(t)
|
||||
|
||||
@on_signal(et.tool_settings_changed, scope=row)
|
||||
def update_current_brush_mode():
|
||||
actions[paint.paint_mode].setChecked(True)
|
||||
|
||||
update_current_brush_mode()
|
||||
|
||||
return row
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def brush_shape_row():
|
||||
# noinspection PyShadowingNames
|
||||
result = QtWidgets.QToolBar()
|
||||
group = QActionGroup(parent)
|
||||
|
||||
def add_brush_shape_action(icon, title, shape, checked=False):
|
||||
a = QAction(title, parent)
|
||||
a.setCheckable(True)
|
||||
a.setIcon(QtGui.QIcon(icon))
|
||||
a.setChecked(checked)
|
||||
result.addAction(a)
|
||||
group.addAction(a)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def toggled(checked):
|
||||
if checked:
|
||||
paint.brush_shape = shape
|
||||
update_ui()
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@on_signal(et.tool_settings_changed, scope=a)
|
||||
def update_to_tool():
|
||||
a.setChecked(paint.brush_shape == shape)
|
||||
|
||||
update_to_tool()
|
||||
qt.on(a.toggled)(toggled)
|
||||
|
||||
add_brush_shape_action(':/circleSolid.png', 'Solid', BrushShape.solid, checked=True)
|
||||
add_brush_shape_action(':/circlePoly.png', 'Smooth', BrushShape.smooth)
|
||||
add_brush_shape_action(':/circleGaus.png', 'Gaus', BrushShape.gaus)
|
||||
|
||||
return result
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def brush_projection_mode_row():
|
||||
# noinspection PyShadowingNames
|
||||
result = QtWidgets.QToolBar()
|
||||
group = QActionGroup(parent)
|
||||
|
||||
def add(title, tooltip, mode, use_volume, checked):
|
||||
a = QAction(title, parent)
|
||||
a.setCheckable(True)
|
||||
a.setChecked(checked)
|
||||
a.setToolTip(tooltip)
|
||||
a.setStatusTip(tooltip)
|
||||
result.addAction(a)
|
||||
group.addAction(a)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@qt.on(a.toggled)
|
||||
def toggled(checked):
|
||||
if checked:
|
||||
paint.brush_projection_mode = mode
|
||||
paint.use_volume_neighbours = use_volume
|
||||
update_ui()
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@on_signal(et.tool_settings_changed, scope=a)
|
||||
def update_to_tool():
|
||||
a.setChecked(
|
||||
paint.brush_projection_mode == mode and (mode != BrushProjectionMode.surface or paint.use_volume_neighbours == use_volume)
|
||||
)
|
||||
|
||||
with qt.signals_blocked(a):
|
||||
update_to_tool()
|
||||
|
||||
add(
|
||||
'Surface',
|
||||
'Using first surface hit under the mouse, update all nearby vertices that are connected by surface to the hit location. '
|
||||
+ 'Only current shell will be updated.',
|
||||
BrushProjectionMode.surface,
|
||||
use_volume=False,
|
||||
checked=True,
|
||||
)
|
||||
add(
|
||||
'Volume',
|
||||
'Using first surface hit under the mouse, update all nearby vertices, including those from other shells.',
|
||||
BrushProjectionMode.surface,
|
||||
use_volume=True,
|
||||
checked=False,
|
||||
)
|
||||
add(
|
||||
'Screen',
|
||||
'Use screen projection of a brush, updating all vertices on all surfaces that are within the brush radius.',
|
||||
BrushProjectionMode.screen,
|
||||
use_volume=False,
|
||||
checked=False,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def stylus_pressure_selection():
|
||||
# noinspection PyShadowingNames
|
||||
result = QtWidgets.QComboBox()
|
||||
result.addItem("Unused")
|
||||
result.addItem("Multiply intensity")
|
||||
result.addItem("Multiply opacity")
|
||||
result.addItem("Multiply radius")
|
||||
return result
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Brush projection:", brush_projection_mode_row()))
|
||||
layout.addLayout(createTitledRow("Brush mode:", brush_mode_row3()))
|
||||
layout.addLayout(createTitledRow("Brush shape:", brush_shape_row()))
|
||||
intensity = widgets.NumberSliderGroup()
|
||||
radius = widgets.NumberSliderGroup(
|
||||
max_value=100, tooltip="You can also set brush radius by just holding <b>B</b> " "and mouse-dragging in the viewport"
|
||||
)
|
||||
iterations = widgets.NumberSliderGroup(value_type=int, min_value=1, max_value=100)
|
||||
layout.addLayout(createTitledRow("Intensity:", intensity.layout()))
|
||||
layout.addLayout(createTitledRow("Brush radius:", radius.layout()))
|
||||
layout.addLayout(createTitledRow("Brush iterations:", iterations.layout()))
|
||||
|
||||
influences_limit = widgets.NumberSliderGroup(value_type=int, min_value=0, max_value=10)
|
||||
layout.addLayout(createTitledRow("Influences limit:", influences_limit.layout()))
|
||||
|
||||
@signal.on(influences_limit.valueChanged)
|
||||
def influences_limit_changed():
|
||||
paint.influences_limit = influences_limit.value()
|
||||
update_ui()
|
||||
|
||||
fixed_influences = QtWidgets.QCheckBox("Only adjust existing vertex influences")
|
||||
fixed_influences.setToolTip(
|
||||
"When this option is enabled, smooth will only adjust existing influences per vertex, "
|
||||
"and won't include other influences from nearby vertices"
|
||||
)
|
||||
layout.addLayout(createTitledRow("Weight bleeding:", fixed_influences))
|
||||
|
||||
@qt.on(fixed_influences.stateChanged)
|
||||
def fixed_influences_changed():
|
||||
paint.fixed_influences_per_vertex = fixed_influences.isChecked()
|
||||
|
||||
limit_to_component_selection = QtWidgets.QCheckBox("Limit to component selection")
|
||||
limit_to_component_selection.setToolTip("When this option is enabled, smoothing will only happen between selected components")
|
||||
layout.addLayout(createTitledRow("Isolation:", limit_to_component_selection))
|
||||
|
||||
@qt.on(limit_to_component_selection.stateChanged)
|
||||
def limit_to_component_selection_changed():
|
||||
paint.limit_to_component_selection = limit_to_component_selection.isChecked()
|
||||
|
||||
interactive_mirror = QtWidgets.QCheckBox("Interactive mirror")
|
||||
layout.addLayout(createTitledRow("", interactive_mirror))
|
||||
|
||||
@qt.on(interactive_mirror.stateChanged)
|
||||
def interactive_mirror_changed():
|
||||
paint.mirror = interactive_mirror.isChecked()
|
||||
update_ui()
|
||||
|
||||
sample_joint_on_stroke_start = QtWidgets.QCheckBox("Sample current joint on stroke start")
|
||||
layout.addLayout(createTitledRow("", sample_joint_on_stroke_start))
|
||||
|
||||
@qt.on(sample_joint_on_stroke_start.stateChanged)
|
||||
def interactive_mirror_changed():
|
||||
paint.sample_joint_on_stroke_start = sample_joint_on_stroke_start.isChecked()
|
||||
update_ui()
|
||||
|
||||
redistribute_removed_weight = QtWidgets.QCheckBox("Distribute to other influences")
|
||||
layout.addLayout(createTitledRow("Removed weight:", redistribute_removed_weight))
|
||||
|
||||
@qt.on(redistribute_removed_weight.stateChanged)
|
||||
def redistribute_removed_weight_changed():
|
||||
paint.distribute_to_other_influences = redistribute_removed_weight.isChecked()
|
||||
update_ui()
|
||||
|
||||
stylus = stylus_pressure_selection()
|
||||
layout.addLayout(createTitledRow("Stylus pressure:", stylus))
|
||||
|
||||
@on_signal(et.tool_settings_changed, scope=layout)
|
||||
def update_ui():
|
||||
log.info("updating paint settings ui")
|
||||
log.info("brush mode:%s, brush shape: %s", paint.mode, paint.brush_shape)
|
||||
paint.update_plugin_brush_radius()
|
||||
paint.update_plugin_brush_intensity()
|
||||
|
||||
with qt.signals_blocked(intensity):
|
||||
intensity.set_value(paint.intensity)
|
||||
widgets.set_paint_expo(intensity, paint.paint_mode)
|
||||
|
||||
with qt.signals_blocked(radius):
|
||||
radius.set_range(0, 1000 if paint.brush_projection_mode == BrushProjectionMode.screen else 100, soft_max=True)
|
||||
radius.set_value(paint.brush_radius)
|
||||
|
||||
with qt.signals_blocked(iterations):
|
||||
iterations.set_value(paint.iterations)
|
||||
iterations.set_enabled(paint.paint_mode in [PaintMode.smooth, PaintMode.sharpen])
|
||||
|
||||
with qt.signals_blocked(stylus):
|
||||
stylus.setCurrentIndex(paint.tablet_mode)
|
||||
|
||||
with qt.signals_blocked(interactive_mirror):
|
||||
interactive_mirror.setChecked(paint.mirror)
|
||||
|
||||
with qt.signals_blocked(redistribute_removed_weight):
|
||||
redistribute_removed_weight.setChecked(paint.distribute_to_other_influences)
|
||||
|
||||
with qt.signals_blocked(influences_limit):
|
||||
influences_limit.set_value(paint.influences_limit)
|
||||
|
||||
with qt.signals_blocked(sample_joint_on_stroke_start):
|
||||
sample_joint_on_stroke_start.setChecked(paint.sample_joint_on_stroke_start)
|
||||
|
||||
with qt.signals_blocked(fixed_influences):
|
||||
fixed_influences.setChecked(paint.fixed_influences_per_vertex)
|
||||
fixed_influences.setEnabled(paint.paint_mode == PaintMode.smooth)
|
||||
|
||||
with qt.signals_blocked(limit_to_component_selection):
|
||||
limit_to_component_selection.setChecked(paint.limit_to_component_selection)
|
||||
limit_to_component_selection.setEnabled(fixed_influences.isEnabled())
|
||||
|
||||
@signal.on(radius.valueChanged, qtParent=layout)
|
||||
def radius_edited():
|
||||
log.info("updated brush radius")
|
||||
paint.brush_radius = radius.value()
|
||||
update_ui()
|
||||
|
||||
@signal.on(intensity.valueChanged, qtParent=layout)
|
||||
def intensity_edited():
|
||||
paint.intensity = intensity.value()
|
||||
update_ui()
|
||||
|
||||
@signal.on(iterations.valueChanged, qtParent=layout)
|
||||
def iterations_edited():
|
||||
paint.iterations = iterations.value()
|
||||
update_ui()
|
||||
|
||||
@qt.on(stylus.currentIndexChanged)
|
||||
def stylus_edited():
|
||||
paint.tablet_mode = stylus.currentIndex()
|
||||
update_ui()
|
||||
|
||||
update_ui()
|
||||
|
||||
result = QtWidgets.QGroupBox("Brush behavior")
|
||||
result.setLayout(layout)
|
||||
return result
|
||||
|
||||
def build_display_settings():
|
||||
result = QtWidgets.QGroupBox("Display settings")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
influences_display = QtWidgets.QComboBox()
|
||||
influences_display.addItem("All influences, multiple colors", WeightsDisplayMode.allInfluences)
|
||||
influences_display.addItem("Current influence, grayscale", WeightsDisplayMode.currentInfluence)
|
||||
influences_display.addItem("Current influence, colored", WeightsDisplayMode.currentInfluenceColored)
|
||||
influences_display.setMinimumWidth(1)
|
||||
influences_display.setCurrentIndex(paint.display_settings.weights_display_mode)
|
||||
|
||||
display_toolbar = QtWidgets.QToolBar()
|
||||
display_toolbar.addAction(global_actions.randomize_influences_colors)
|
||||
|
||||
@qt.on(influences_display.currentIndexChanged)
|
||||
def influences_display_changed():
|
||||
paint.display_settings.weights_display_mode = influences_display.currentData()
|
||||
update_ui_to_tool()
|
||||
|
||||
display_layout = QtWidgets.QVBoxLayout()
|
||||
display_layout.addWidget(influences_display)
|
||||
display_layout.addWidget(display_toolbar)
|
||||
layout.addLayout(createTitledRow("Influences display:", display_layout))
|
||||
|
||||
mask_display = QtWidgets.QComboBox()
|
||||
mask_display.addItem("Default", MaskDisplayMode.default_)
|
||||
mask_display.addItem("Color ramp", MaskDisplayMode.color_ramp)
|
||||
mask_display.setMinimumWidth(1)
|
||||
mask_display.setCurrentIndex(paint.display_settings.weights_display_mode)
|
||||
|
||||
@qt.on(mask_display.currentIndexChanged)
|
||||
def influences_display_changed():
|
||||
paint.display_settings.mask_display_mode = mask_display.currentData()
|
||||
update_ui_to_tool()
|
||||
|
||||
layout.addLayout(createTitledRow("Mask display:", mask_display))
|
||||
|
||||
show_effects = QtWidgets.QCheckBox("Show layer effects")
|
||||
layout.addLayout(createTitledRow("", show_effects))
|
||||
show_masked = QtWidgets.QCheckBox("Show masked weights")
|
||||
layout.addLayout(createTitledRow("", show_masked))
|
||||
|
||||
show_selected_verts_only = QtWidgets.QCheckBox("Hide unselected vertices")
|
||||
layout.addLayout(createTitledRow("", show_selected_verts_only))
|
||||
|
||||
@qt.on(show_effects.stateChanged)
|
||||
def show_effects_changed():
|
||||
paint.display_settings.layer_effects_display = show_effects.isChecked()
|
||||
|
||||
@qt.on(show_masked.stateChanged)
|
||||
def show_masked_changed():
|
||||
paint.display_settings.display_masked = show_masked.isChecked()
|
||||
|
||||
@qt.on(show_selected_verts_only.stateChanged)
|
||||
def show_selected_verts_changed():
|
||||
paint.display_settings.show_selected_verts_only = show_selected_verts_only.isChecked()
|
||||
|
||||
mesh_toolbar = QtWidgets.QToolBar()
|
||||
toggle_original_mesh = QAction("Show Original Mesh", mesh_toolbar)
|
||||
toggle_original_mesh.setCheckable(True)
|
||||
mesh_toolbar.addAction(toggle_original_mesh)
|
||||
layout.addLayout(createTitledRow("", mesh_toolbar))
|
||||
|
||||
@qt.on(toggle_original_mesh.triggered)
|
||||
def toggle_display_node_visible():
|
||||
paint.display_settings.display_node_visible = not toggle_original_mesh.isChecked()
|
||||
update_ui_to_tool()
|
||||
|
||||
wireframe_color_button = widgets.ColorButton()
|
||||
layout.addLayout(createTitledRow("Wireframe color:", wireframe_color_button))
|
||||
|
||||
@signal.on(wireframe_color_button.color_changed)
|
||||
def update_wireframe_color():
|
||||
if paint.display_settings.weights_display_mode == WeightsDisplayMode.allInfluences:
|
||||
paint.display_settings.wireframe_color = wireframe_color_button.get_color_3f()
|
||||
else:
|
||||
paint.display_settings.wireframe_color_single_influence = wireframe_color_button.get_color_3f()
|
||||
|
||||
@signal.on(session.events.toolChanged, qtParent=tab.tabContents)
|
||||
def update_ui_to_tool():
|
||||
ds = paint.display_settings
|
||||
toggle_original_mesh.setChecked(PaintTool.is_painting() and not ds.display_node_visible)
|
||||
|
||||
qt.select_data(influences_display, ds.weights_display_mode)
|
||||
qt.select_data(mask_display, ds.mask_display_mode)
|
||||
show_effects.setChecked(ds.layer_effects_display)
|
||||
show_masked.setChecked(ds.display_masked)
|
||||
show_selected_verts_only.setChecked(ds.show_selected_verts_only)
|
||||
global_actions.randomize_influences_colors.setEnabled(ds.weights_display_mode == WeightsDisplayMode.allInfluences)
|
||||
display_toolbar.setVisible(global_actions.randomize_influences_colors.isEnabled())
|
||||
|
||||
if ds.weights_display_mode == WeightsDisplayMode.allInfluences:
|
||||
wireframe_color_button.set_color(ds.wireframe_color)
|
||||
else:
|
||||
wireframe_color_button.set_color(ds.wireframe_color_single_influence)
|
||||
|
||||
update_ui_to_tool()
|
||||
|
||||
result.setLayout(layout)
|
||||
return result
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_brush_settings_group())
|
||||
tab.innerLayout.addWidget(build_display_settings())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
tab.lowerButtonsRow.addWidget(bind_action_to_button(global_actions.paint, QtWidgets.QPushButton()))
|
||||
tab.lowerButtonsRow.addWidget(bind_action_to_button(global_actions.flood, QtWidgets.QPushButton()))
|
||||
|
||||
@signal.on(session.events.toolChanged, qtParent=tab.tabContents)
|
||||
def update_to_tool():
|
||||
tab.scrollArea.setEnabled(PaintTool.is_painting())
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_to_tool()
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
228
2023/scripts/rigging_tools/ngskintools2/ui/tabSetWeights.py
Normal file
@@ -0,0 +1,228 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api import PaintMode, PaintModeSettings, flood_weights
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import QAction, QActionGroup, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.signal import Signal
|
||||
from ngSkinTools2.ui import qt, widgets
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
from ngSkinTools2.ui.ui_lock import UiLock
|
||||
|
||||
log = getLogger("tab set weights")
|
||||
|
||||
|
||||
def make_presets():
|
||||
presets = {m: PaintModeSettings() for m in PaintMode.all()}
|
||||
for k, v in presets.items():
|
||||
v.mode = k
|
||||
|
||||
presets[PaintMode.smooth].intensity = 0.3
|
||||
presets[PaintMode.scale].intensity = 0.3
|
||||
presets[PaintMode.add].intensity = 0.1
|
||||
presets[PaintMode.scale].intensity = 0.95
|
||||
|
||||
return presets
|
||||
|
||||
|
||||
class Model(Object):
|
||||
def __init__(self):
|
||||
self.mode_changed = Signal("mode changed")
|
||||
self.presets = make_presets()
|
||||
self.current_settings = None
|
||||
self.set_mode(PaintMode.replace)
|
||||
|
||||
def set_mode(self, mode):
|
||||
self.current_settings = self.presets[mode]
|
||||
self.mode_changed.emit()
|
||||
|
||||
def apply(self):
|
||||
flood_weights(session.state.currentLayer.layer, influences=session.state.currentLayer.layer.paint_targets, settings=self.current_settings)
|
||||
|
||||
|
||||
def build_ui(parent):
|
||||
model = Model()
|
||||
ui_lock = UiLock()
|
||||
|
||||
def build_mode_settings_group():
|
||||
def mode_row():
|
||||
row = QtWidgets.QVBoxLayout()
|
||||
|
||||
group = QActionGroup(parent)
|
||||
|
||||
actions = {}
|
||||
|
||||
def create_mode_button(toolbar, mode, label, tooltip):
|
||||
a = QAction(label, parent)
|
||||
a.setToolTip(tooltip)
|
||||
a.setStatusTip(tooltip)
|
||||
a.setCheckable(True)
|
||||
actions[mode] = a
|
||||
group.addAction(a)
|
||||
|
||||
@qt.on(a.toggled)
|
||||
@ui_lock.skip_if_updating
|
||||
def toggled(checked):
|
||||
if checked:
|
||||
model.set_mode(mode)
|
||||
update_ui()
|
||||
|
||||
toolbar.addAction(a)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_mode_button(t, PaintMode.replace, "Replace", "")
|
||||
create_mode_button(t, PaintMode.add, "Add", "")
|
||||
create_mode_button(t, PaintMode.scale, "Scale", "")
|
||||
row.addWidget(t)
|
||||
|
||||
t = QtWidgets.QToolBar()
|
||||
create_mode_button(t, PaintMode.smooth, "Smooth", "")
|
||||
create_mode_button(t, PaintMode.sharpen, "Sharpen", "")
|
||||
row.addWidget(t)
|
||||
|
||||
actions[model.current_settings.mode].setChecked(True)
|
||||
|
||||
return row
|
||||
|
||||
influences_limit = widgets.NumberSliderGroup(value_type=int, min_value=0, max_value=10)
|
||||
|
||||
@signal.on(influences_limit.valueChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def influences_limit_changed():
|
||||
for _, v in model.presets.items():
|
||||
v.influences_limit = influences_limit.value()
|
||||
update_ui()
|
||||
|
||||
intensity = widgets.NumberSliderGroup()
|
||||
|
||||
@signal.on(intensity.valueChanged, qtParent=parent)
|
||||
@ui_lock.skip_if_updating
|
||||
def intensity_edited():
|
||||
model.current_settings.intensity = intensity.value()
|
||||
update_ui()
|
||||
|
||||
iterations = widgets.NumberSliderGroup(value_type=int, min_value=1, max_value=100)
|
||||
|
||||
@signal.on(iterations.valueChanged, qtParent=parent)
|
||||
@ui_lock.skip_if_updating
|
||||
def iterations_edited():
|
||||
model.current_settings.iterations = iterations.value()
|
||||
update_ui()
|
||||
|
||||
fixed_influences = QtWidgets.QCheckBox("Only adjust existing vertex influences")
|
||||
fixed_influences.setToolTip(
|
||||
"When this option is enabled, smooth will only adjust existing influences per vertex, "
|
||||
"and won't include other influences from nearby vertices"
|
||||
)
|
||||
|
||||
volume_neighbours = QtWidgets.QCheckBox("Smooth across gaps and thin surfaces")
|
||||
volume_neighbours.setToolTip(
|
||||
"Use all nearby neighbours, regardless if they belong to same surface. "
|
||||
"This will allow for smoothing to happen across gaps and thin surfaces."
|
||||
)
|
||||
|
||||
limit_to_component_selection = QtWidgets.QCheckBox("Limit to component selection")
|
||||
limit_to_component_selection.setToolTip("When this option is enabled, smoothing will only happen between selected components")
|
||||
|
||||
@qt.on(fixed_influences.stateChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def fixed_influences_changed(*_):
|
||||
model.current_settings.fixed_influences_per_vertex = fixed_influences.isChecked()
|
||||
|
||||
@qt.on(limit_to_component_selection.stateChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def limit_to_component_selection_changed(*_):
|
||||
model.current_settings.limit_to_component_selection = limit_to_component_selection.isChecked()
|
||||
|
||||
def update_ui():
|
||||
with ui_lock:
|
||||
widgets.set_paint_expo(intensity, model.current_settings.mode)
|
||||
|
||||
intensity.set_value(model.current_settings.intensity)
|
||||
|
||||
iterations.set_value(model.current_settings.iterations)
|
||||
iterations.set_enabled(model.current_settings.mode in [PaintMode.smooth, PaintMode.sharpen])
|
||||
|
||||
fixed_influences.setEnabled(model.current_settings.mode in [PaintMode.smooth])
|
||||
fixed_influences.setChecked(model.current_settings.fixed_influences_per_vertex)
|
||||
|
||||
limit_to_component_selection.setChecked(model.current_settings.limit_to_component_selection)
|
||||
limit_to_component_selection.setEnabled(fixed_influences.isEnabled())
|
||||
|
||||
influences_limit.set_value(model.current_settings.influences_limit)
|
||||
|
||||
volume_neighbours.setChecked(model.current_settings.use_volume_neighbours)
|
||||
volume_neighbours.setEnabled(model.current_settings.mode == PaintMode.smooth)
|
||||
|
||||
settings_group = QtWidgets.QGroupBox("Mode Settings")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
layout.addLayout(createTitledRow("Mode:", mode_row()))
|
||||
layout.addLayout(createTitledRow("Intensity:", intensity.layout()))
|
||||
layout.addLayout(createTitledRow("Iterations:", iterations.layout()))
|
||||
layout.addLayout(createTitledRow("Influences limit:", influences_limit.layout()))
|
||||
layout.addLayout(createTitledRow("Weight bleeding:", fixed_influences))
|
||||
layout.addLayout(createTitledRow("Volume smoothing:", volume_neighbours))
|
||||
layout.addLayout(createTitledRow("Isolation:", limit_to_component_selection))
|
||||
settings_group.setLayout(layout)
|
||||
|
||||
update_ui()
|
||||
|
||||
return settings_group
|
||||
|
||||
def common_settings():
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
mirror = QtWidgets.QCheckBox("Mirror")
|
||||
layout.addLayout(createTitledRow("", mirror))
|
||||
|
||||
@qt.on(mirror.stateChanged)
|
||||
@ui_lock.skip_if_updating
|
||||
def mirror_changed(*_):
|
||||
for _, v in model.presets.items():
|
||||
v.mirror = mirror.isChecked()
|
||||
|
||||
redistribute_removed_weight = QtWidgets.QCheckBox("Distribute to other influences")
|
||||
layout.addLayout(createTitledRow("Removed weight:", redistribute_removed_weight))
|
||||
|
||||
@qt.on(redistribute_removed_weight.stateChanged)
|
||||
def redistribute_removed_weight_changed():
|
||||
for _, v in model.presets.items():
|
||||
v.distribute_to_other_influences = redistribute_removed_weight.isChecked()
|
||||
|
||||
@signal.on(model.mode_changed, qtParent=layout)
|
||||
def update_ui():
|
||||
mirror.setChecked(model.current_settings.mirror)
|
||||
redistribute_removed_weight.setChecked(model.current_settings.distribute_to_other_influences)
|
||||
|
||||
group = QtWidgets.QGroupBox("Common Settings")
|
||||
group.setLayout(layout)
|
||||
|
||||
update_ui()
|
||||
|
||||
return group
|
||||
|
||||
def apply_button():
|
||||
btn = QtWidgets.QPushButton("Apply")
|
||||
btn.setToolTip("Apply selected operation to vertex")
|
||||
|
||||
@qt.on(btn.clicked)
|
||||
def clicked():
|
||||
model.apply()
|
||||
|
||||
return btn
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(build_mode_settings_group())
|
||||
tab.innerLayout.addWidget(common_settings())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
tab.lowerButtonsRow.addWidget(apply_button())
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
117
2023/scripts/rigging_tools/ngskintools2/ui/tabTools.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.pyside import QtWidgets
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.ui import model_binds, qt, widgets
|
||||
from ngSkinTools2.ui.actions import Actions
|
||||
from ngSkinTools2.ui.layout import TabSetup, createTitledRow
|
||||
|
||||
|
||||
def build_ui(actions, session):
|
||||
"""
|
||||
|
||||
:type actions: Actions
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
def assign_weights_from_closest_joint_group():
|
||||
options = actions.toolsAssignFromClosestJointOptions
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def influences_options():
|
||||
result = QtWidgets.QVBoxLayout()
|
||||
button_group = QtWidgets.QButtonGroup()
|
||||
for index, i in enumerate(["Use all available influences", "Use selected influences"]):
|
||||
radio = QtWidgets.QRadioButton(i)
|
||||
button_group.addButton(radio, index)
|
||||
result.addWidget(radio)
|
||||
|
||||
@qt.on(radio.toggled)
|
||||
def update_value():
|
||||
options.all_influences.set(button_group.buttons()[0].isChecked())
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@signal.on(options.all_influences.changed, qtParent=result)
|
||||
def update_ui():
|
||||
button_group.buttons()[0 if options.all_influences() else 1].setChecked(True)
|
||||
|
||||
update_ui()
|
||||
|
||||
return result
|
||||
|
||||
new_layer = QtWidgets.QCheckBox("Create new layer")
|
||||
|
||||
@qt.on(new_layer.toggled)
|
||||
def update_new_layer():
|
||||
options.create_new_layer.set(new_layer.isChecked())
|
||||
|
||||
@signal.on(options.create_new_layer.changed)
|
||||
def update_ui():
|
||||
new_layer.setChecked(options.create_new_layer())
|
||||
|
||||
btn = QtWidgets.QPushButton()
|
||||
qt.bind_action_to_button(actions.toolsAssignFromClosestJoint, btn)
|
||||
|
||||
update_ui()
|
||||
|
||||
result = QtWidgets.QGroupBox("Assign weights from closest joint")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addLayout(createTitledRow("Target layer", new_layer))
|
||||
layout.addLayout(createTitledRow("Influences", influences_options()))
|
||||
layout.addWidget(btn)
|
||||
|
||||
return result
|
||||
|
||||
def unify_weights_group():
|
||||
options = actions.toolsUnifyWeightsOptions
|
||||
|
||||
intensity = widgets.NumberSliderGroup()
|
||||
model_binds.bind(intensity, options.overall_effect)
|
||||
|
||||
single_cluster_mode = QtWidgets.QCheckBox(
|
||||
"Single group mode",
|
||||
)
|
||||
single_cluster_mode.setToolTip("average weights across whole selection, ignoring separate shells or selection gaps")
|
||||
model_binds.bind(single_cluster_mode, options.single_cluster_mode)
|
||||
|
||||
btn = QtWidgets.QPushButton()
|
||||
qt.bind_action_to_button(actions.toolsUnifyWeights, btn)
|
||||
|
||||
result = QtWidgets.QGroupBox("Unify weights")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addLayout(createTitledRow("Intensity:", intensity.layout()))
|
||||
layout.addLayout(createTitledRow("Clustering:", single_cluster_mode))
|
||||
layout.addWidget(btn)
|
||||
|
||||
return result
|
||||
|
||||
def other_tools_group():
|
||||
result = QtWidgets.QGroupBox("Other")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.addWidget(to_button(actions.fill_layer_transparency))
|
||||
layout.addWidget(to_button(actions.copy_components))
|
||||
layout.addWidget(to_button(actions.paste_component_average))
|
||||
|
||||
return result
|
||||
|
||||
tab = TabSetup()
|
||||
tab.innerLayout.addWidget(assign_weights_from_closest_joint_group())
|
||||
tab.innerLayout.addWidget(unify_weights_group())
|
||||
tab.innerLayout.addWidget(other_tools_group())
|
||||
tab.innerLayout.addStretch()
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=tab.tabContents)
|
||||
def update_tab_enabled():
|
||||
tab.tabContents.setEnabled(session.state.layersAvailable)
|
||||
|
||||
update_tab_enabled()
|
||||
|
||||
return tab.tabContents
|
||||
|
||||
|
||||
def to_button(action):
|
||||
btn = QtWidgets.QPushButton()
|
||||
qt.bind_action_to_button(action, btn)
|
||||
return btn
|
||||
142
2023/scripts/rigging_tools/ngskintools2/ui/targetui.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from ngSkinTools2 import signal
|
||||
from ngSkinTools2.api.influence_names import InfluenceNameFilter
|
||||
from ngSkinTools2.api.pyside import QAction, QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import Session
|
||||
from ngSkinTools2.operations import import_v1_actions
|
||||
from ngSkinTools2.ui import influencesview, layersview, qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def build_layers_ui(parent, actions, session):
|
||||
"""
|
||||
|
||||
:type session: Session
|
||||
:type actions: ngSkinTools2.ui.actions.Actions
|
||||
:type parent: QWidget
|
||||
"""
|
||||
|
||||
influences_filter = InfluenceNameFilter()
|
||||
|
||||
def build_infl_filter():
|
||||
img = qt.image_icon("clear-input-white.png")
|
||||
|
||||
result = QtWidgets.QHBoxLayout()
|
||||
result.setSpacing(5)
|
||||
filter = QtWidgets.QComboBox()
|
||||
filter.setMinimumHeight(22 * scale_multiplier)
|
||||
filter.setEditable(True)
|
||||
filter.lineEdit().setPlaceholderText("Search...")
|
||||
result.addWidget(filter)
|
||||
# noinspection PyShadowingNames
|
||||
clear = QAction(result)
|
||||
clear.setIcon(img)
|
||||
filter.lineEdit().addAction(clear, QtWidgets.QLineEdit.TrailingPosition)
|
||||
|
||||
@qt.on(filter.editTextChanged)
|
||||
def filter_edited():
|
||||
influences_filter.set_filter_string(filter.currentText())
|
||||
|
||||
clear.setVisible(len(filter.currentText()) != 0)
|
||||
|
||||
@qt.on(clear.triggered)
|
||||
def clear_clicked():
|
||||
filter.clearEditText()
|
||||
|
||||
filter_edited()
|
||||
|
||||
return result
|
||||
|
||||
split = QtWidgets.QSplitter(orientation=QtCore.Qt.Horizontal, parent=parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(3)
|
||||
clear = QtWidgets.QPushButton()
|
||||
clear.setFixedSize(20, 20)
|
||||
# layout.addWidget(clear)
|
||||
|
||||
layers = layersview.build_view(parent, actions)
|
||||
layout.addWidget(layers)
|
||||
split.addWidget(qt.wrap_layout_into_widget(layout))
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(3)
|
||||
influences = influencesview.build_view(parent, actions, session, filter=influences_filter)
|
||||
layout.addWidget(influences)
|
||||
layout.addLayout(build_infl_filter())
|
||||
split.addWidget(qt.wrap_layout_into_widget(layout))
|
||||
|
||||
return split
|
||||
|
||||
|
||||
def build_no_layers_ui(parent, actions, session):
|
||||
"""
|
||||
:param parent: ui parent
|
||||
:type actions: ngSkinTools2.ui.actions.Actions
|
||||
:type session: Session
|
||||
"""
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(30, 30, 30, 30)
|
||||
|
||||
selection_display = QtWidgets.QLabel("pPlane1")
|
||||
selection_display.setStyleSheet("font-weight: bold")
|
||||
|
||||
selection_note = QtWidgets.QLabel("Skinning Layers cannot be attached to this object")
|
||||
selection_note.setWordWrap(True)
|
||||
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(selection_display)
|
||||
layout.addWidget(selection_note)
|
||||
layout.addWidget(qt.bind_action_to_button(actions.import_v1, QtWidgets.QPushButton()))
|
||||
layout.addWidget(qt.bind_action_to_button(actions.initialize, QtWidgets.QPushButton()))
|
||||
layout.addStretch(3)
|
||||
|
||||
layout_widget = qt.wrap_layout_into_widget(layout)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=parent)
|
||||
def handle_target_changed():
|
||||
if session.state.layersAvailable:
|
||||
return # no need to update
|
||||
|
||||
is_skinned = session.state.selectedSkinCluster is not None
|
||||
selection_display.setText(session.state.selectedSkinCluster)
|
||||
selection_display.setVisible(is_skinned)
|
||||
|
||||
note = "Select a mesh with a skin cluster attached."
|
||||
if is_skinned:
|
||||
note = "Skinning layers are not yet initialized for this mesh."
|
||||
if import_v1_actions.can_import(session):
|
||||
note = "Skinning layers from previous ngSkinTools version are initialized on this mesh."
|
||||
|
||||
selection_note.setText(note)
|
||||
|
||||
if session.active():
|
||||
handle_target_changed()
|
||||
|
||||
return layout_widget
|
||||
|
||||
|
||||
def build_target_ui(parent, actions, session):
|
||||
"""
|
||||
:param actions:
|
||||
:param parent:
|
||||
:type session: Session
|
||||
"""
|
||||
result = QtWidgets.QStackedWidget()
|
||||
result.addWidget(build_no_layers_ui(parent, actions, session))
|
||||
result.addWidget(build_layers_ui(parent, actions, session))
|
||||
result.setMinimumHeight(300 * scale_multiplier)
|
||||
|
||||
@signal.on(session.events.targetChanged, qtParent=parent)
|
||||
def handle_target_changed():
|
||||
if not session.state.layersAvailable:
|
||||
result.setCurrentIndex(0)
|
||||
else:
|
||||
result.setCurrentIndex(1)
|
||||
|
||||
if session.active():
|
||||
handle_target_changed()
|
||||
|
||||
return result
|
||||
207
2023/scripts/rigging_tools/ngskintools2/ui/transferDialog.py
Normal file
@@ -0,0 +1,207 @@
|
||||
from ngSkinTools2 import api, cleanup, signal
|
||||
from ngSkinTools2.api import VertexTransferMode
|
||||
from ngSkinTools2.api.pyside import QtCore, QtWidgets
|
||||
from ngSkinTools2.api.session import session
|
||||
from ngSkinTools2.api.transfer import LayersTransfer
|
||||
from ngSkinTools2.decorators import undoable
|
||||
from ngSkinTools2.ui import influenceMappingUI, qt, widgets
|
||||
from ngSkinTools2.ui.layout import createTitledRow, scale_multiplier
|
||||
|
||||
|
||||
class UiModel:
|
||||
def __init__(self):
|
||||
self.transfer = LayersTransfer()
|
||||
|
||||
def destination_has_layers(self):
|
||||
l = api.Layers(self.transfer.target)
|
||||
return l.is_enabled() and len(l.list()) > 0
|
||||
|
||||
@undoable
|
||||
def do_apply(self):
|
||||
self.transfer.complete_execution()
|
||||
from maya import cmds
|
||||
|
||||
cmds.select(self.transfer.target)
|
||||
if session.active():
|
||||
session.events.targetChanged.emitIfChanged()
|
||||
|
||||
|
||||
single_transfer_dialog_policy = qt.SingleWindowPolicy()
|
||||
|
||||
|
||||
def open(parent, model):
|
||||
"""
|
||||
|
||||
:type model: UiModel
|
||||
"""
|
||||
|
||||
def buttonRow(window):
|
||||
def apply():
|
||||
model.do_apply()
|
||||
session.events.layerListChanged.emitIfChanged()
|
||||
window.close()
|
||||
|
||||
return widgets.button_row(
|
||||
[
|
||||
("Transfer", apply),
|
||||
("Cancel", window.close),
|
||||
]
|
||||
)
|
||||
|
||||
def view_influences_settings():
|
||||
tabs.setCurrentIndex(1)
|
||||
|
||||
def build_settings():
|
||||
result = QtWidgets.QVBoxLayout()
|
||||
|
||||
vertexMappingMode = QtWidgets.QComboBox()
|
||||
vertexMappingMode.addItem("Closest point on surface", VertexTransferMode.closestPoint)
|
||||
vertexMappingMode.addItem("UV space", VertexTransferMode.uvSpace)
|
||||
vertexMappingMode.addItem("By vertex ID (source and destination vert count must match)", VertexTransferMode.vertexId)
|
||||
|
||||
g = QtWidgets.QGroupBox("Selection")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
g.setLayout(layout)
|
||||
|
||||
sourceLabel = QtWidgets.QLabel()
|
||||
layout.addLayout(createTitledRow("Source:", sourceLabel))
|
||||
|
||||
destinationLabel = QtWidgets.QLabel()
|
||||
layout.addLayout(createTitledRow("Destination:", destinationLabel))
|
||||
result.addWidget(g)
|
||||
|
||||
g = QtWidgets.QGroupBox("Vertex mapping")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(createTitledRow("Mapping mode:", vertexMappingMode))
|
||||
g.setLayout(layout)
|
||||
result.addWidget(g)
|
||||
|
||||
g = QtWidgets.QGroupBox("Influences mapping")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
g.setLayout(layout)
|
||||
|
||||
edit = QtWidgets.QPushButton("Configure")
|
||||
qt.on(edit.clicked)(view_influences_settings)
|
||||
|
||||
button_row = QtWidgets.QHBoxLayout()
|
||||
button_row.addWidget(edit)
|
||||
button_row.addStretch()
|
||||
layout.addLayout(button_row)
|
||||
|
||||
result.addWidget(g)
|
||||
|
||||
g = QtWidgets.QGroupBox("Other options")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
g.setLayout(layout)
|
||||
|
||||
keep_layers = QtWidgets.QCheckBox("Keep existing layers on destination")
|
||||
keep_layers_row = qt.wrap_layout_into_widget(createTitledRow("Destination layers:", keep_layers))
|
||||
layout.addWidget(keep_layers_row)
|
||||
|
||||
@qt.on(keep_layers.stateChanged)
|
||||
def checked():
|
||||
model.transfer.keep_existing_layers = keep_layers.isChecked()
|
||||
|
||||
result.addWidget(g)
|
||||
|
||||
result.addStretch()
|
||||
|
||||
def update_settings_to_model():
|
||||
keep_layers.setChecked(model.transfer.keep_existing_layers)
|
||||
qt.select_data(vertexMappingMode, model.transfer.vertex_transfer_mode)
|
||||
source_title = model.transfer.source
|
||||
if model.transfer.source_file is not None:
|
||||
source_title = 'file ' + model.transfer.source_file
|
||||
sourceLabel.setText("<strong>" + source_title + "</strong>")
|
||||
destinationLabel.setText("<strong>" + model.transfer.target + "</strong>")
|
||||
keep_layers_row.setEnabled(model.destination_has_layers())
|
||||
|
||||
@qt.on(vertexMappingMode.currentIndexChanged)
|
||||
def vertex_mapping_mode_changed():
|
||||
model.transfer.vertex_transfer_mode = vertexMappingMode.currentData()
|
||||
|
||||
update_settings_to_model()
|
||||
|
||||
return result
|
||||
|
||||
def build_influenes_tab():
|
||||
infl_ui, _, recalcMatches = influenceMappingUI.build_ui(parent, model.transfer.influences_mapping)
|
||||
|
||||
padding = QtWidgets.QVBoxLayout()
|
||||
padding.setContentsMargins(0, 20 * scale_multiplier, 0, 0)
|
||||
padding.addWidget(infl_ui)
|
||||
|
||||
recalcMatches()
|
||||
|
||||
return padding
|
||||
|
||||
tabs = QtWidgets.QTabWidget()
|
||||
|
||||
tabs.addTab(qt.wrap_layout_into_widget(build_settings()), "Settings")
|
||||
tabs.addTab(qt.wrap_layout_into_widget(build_influenes_tab()), "Influences mapping")
|
||||
|
||||
window = QtWidgets.QDialog(parent)
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
window.setWindowTitle("Transfer")
|
||||
window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
window.resize(720 * scale_multiplier, 500 * scale_multiplier)
|
||||
window.setLayout(QtWidgets.QVBoxLayout())
|
||||
|
||||
window.layout().addWidget(tabs)
|
||||
window.layout().addLayout(buttonRow(window))
|
||||
|
||||
if session.active():
|
||||
session.addQtWidgetReference(window)
|
||||
|
||||
single_transfer_dialog_policy.setCurrent(window)
|
||||
window.show()
|
||||
|
||||
|
||||
def build_transfer_action(session, parent):
|
||||
from maya import cmds
|
||||
|
||||
from .actions import define_action
|
||||
|
||||
targets = []
|
||||
|
||||
def detect_targets():
|
||||
targets[:] = []
|
||||
selection = cmds.ls(sl=True)
|
||||
if len(selection) != 2:
|
||||
return False
|
||||
|
||||
if not api.Layers(selection[0]).is_enabled():
|
||||
return False
|
||||
|
||||
targets[:] = selection
|
||||
|
||||
return True
|
||||
|
||||
def transfer_dialog(transfer):
|
||||
"""
|
||||
|
||||
:type transfer: LayersTransfer
|
||||
"""
|
||||
model = UiModel()
|
||||
model.transfer = transfer
|
||||
open(parent, model)
|
||||
|
||||
def handler():
|
||||
if not targets:
|
||||
return
|
||||
|
||||
t = LayersTransfer()
|
||||
t.source = targets[0]
|
||||
t.target = targets[1]
|
||||
t.customize_callback = transfer_dialog
|
||||
t.execute()
|
||||
|
||||
result = define_action(parent, "Transfer layers...", callback=handler)
|
||||
|
||||
@signal.on(session.events.nodeSelectionChanged)
|
||||
def on_selection_changed():
|
||||
result.setEnabled(detect_targets())
|
||||
|
||||
on_selection_changed()
|
||||
|
||||
return result
|
||||
23
2023/scripts/rigging_tools/ngskintools2/ui/ui_lock.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import functools
|
||||
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
|
||||
|
||||
class UiLock(Object):
|
||||
def __init__(self):
|
||||
self.updating = False
|
||||
|
||||
def __enter__(self):
|
||||
self.updating = True
|
||||
|
||||
def skip_if_updating(self, fn):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
if self.updating:
|
||||
return
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
def __exit__(self, _type, value, traceback):
|
||||
self.updating = False
|
||||
132
2023/scripts/rigging_tools/ngskintools2/ui/updatewindow.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import webbrowser
|
||||
|
||||
from ngSkinTools2.api import versioncheck
|
||||
from ngSkinTools2.api.log import getLogger
|
||||
from ngSkinTools2.api.pyside import Qt, QtWidgets
|
||||
from ngSkinTools2.ui.options import bind_checkbox, config
|
||||
|
||||
from .. import cleanup, signal, version
|
||||
from . import qt
|
||||
from .layout import scale_multiplier
|
||||
|
||||
log = getLogger("plugin")
|
||||
|
||||
|
||||
def show(parent, silent_mode):
|
||||
"""
|
||||
:type parent: QWidget
|
||||
"""
|
||||
|
||||
error_signal = signal.Signal("error")
|
||||
success_signal = signal.Signal("success")
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def body():
|
||||
result = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
result.setLayout(layout)
|
||||
layout.setContentsMargins(20, 30, 20, 20)
|
||||
|
||||
header = QtWidgets.QLabel("<strong>Checking for update...</strong>")
|
||||
result1 = QtWidgets.QLabel("Current version: <strong>2.0.0</strong>")
|
||||
result2 = QtWidgets.QLabel("Update available: 2.0.1")
|
||||
download = QtWidgets.QPushButton("Download ngSkinTools v2.0.1")
|
||||
# layout.addWidget(QtWidgets.QLabel("Checking for updates..."))
|
||||
layout.addWidget(header)
|
||||
layout.addWidget(result1)
|
||||
layout.addWidget(result2)
|
||||
layout.addWidget(download)
|
||||
|
||||
result1.setVisible(False)
|
||||
result2.setVisible(False)
|
||||
download.setVisible(False)
|
||||
|
||||
@signal.on(error_signal)
|
||||
def error_handler(error):
|
||||
header.setText("<strong>Error: {0}</strong>".format(error))
|
||||
|
||||
@signal.on(success_signal)
|
||||
def success_handler(info):
|
||||
"""
|
||||
|
||||
:type info: ngSkinTools2.api.versioncheck.
|
||||
"""
|
||||
|
||||
header.setText("<strong>{0}</strong>".format('Update available!' if info.update_available else 'ngSkinTools is up to date.'))
|
||||
result1.setVisible(True)
|
||||
result1.setText("Current version: <strong>{0}</strong>".format(version.pluginVersion()))
|
||||
if info.update_available:
|
||||
result2.setVisible(True)
|
||||
result2.setText(
|
||||
"Update available: <strong>{0}</strong>, released on {1}".format(info.latest_version, info.update_date.strftime("%d %B, %Y"))
|
||||
)
|
||||
download.setVisible(True)
|
||||
download.setText("Download ngSkinTools v" + info.latest_version)
|
||||
|
||||
@qt.on(download.clicked)
|
||||
def open_link():
|
||||
webbrowser.open_new(info.download_url)
|
||||
|
||||
return result
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def buttonsRow(window):
|
||||
btn_close = QtWidgets.QPushButton("Close")
|
||||
btn_close.setMinimumWidth(100 * scale_multiplier)
|
||||
|
||||
check_do_on_startup = bind_checkbox(QtWidgets.QCheckBox("Check for updates at startup"), config.checkForUpdatesAtStartup)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.addWidget(check_do_on_startup)
|
||||
layout.addStretch()
|
||||
layout.addWidget(btn_close)
|
||||
layout.setContentsMargins(20 * scale_multiplier, 15 * scale_multiplier, 20 * scale_multiplier, 15 * scale_multiplier)
|
||||
|
||||
btn_close.clicked.connect(lambda: window.close())
|
||||
return layout
|
||||
|
||||
window = QtWidgets.QWidget(parent, Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
|
||||
window.resize(400 * scale_multiplier, 200 * scale_multiplier)
|
||||
window.setAttribute(Qt.WA_DeleteOnClose)
|
||||
window.setWindowTitle("ngSkinTools2 version update")
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
window.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout.addWidget(body())
|
||||
layout.addStretch(2)
|
||||
layout.addLayout(buttonsRow(window))
|
||||
|
||||
if not silent_mode:
|
||||
window.show()
|
||||
|
||||
@signal.on(success_signal)
|
||||
def on_success(info):
|
||||
if silent_mode:
|
||||
if info.update_available:
|
||||
window.show()
|
||||
else:
|
||||
log.info("not showing the window")
|
||||
window.close()
|
||||
|
||||
cleanup.registerCleanupHandler(window.close)
|
||||
|
||||
@qt.on(window.destroyed)
|
||||
def closed():
|
||||
log.info("deleting update window")
|
||||
|
||||
versioncheck.download_update_info(success_callback=success_signal.emit, failure_callback=error_signal.emit)
|
||||
|
||||
|
||||
def silent_check_and_show_if_available(parent):
|
||||
show(parent, silent_mode=True)
|
||||
|
||||
|
||||
def show_and_start_update(parent):
|
||||
show(parent, silent_mode=False)
|
||||
|
||||
|
||||
def build_action_check_for_updates(parent):
|
||||
from ngSkinTools2.ui import actions
|
||||
|
||||
return actions.define_action(parent, "Check for Updates...", callback=lambda: show_and_start_update(parent))
|
||||