Update
This commit is contained in:
40
2023/scripts/rigging_tools/ngskintools2/api/__init__.py
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
|
||||
Reference in New Issue
Block a user