417 lines
13 KiB
Python
417 lines
13 KiB
Python
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)
|