Update
This commit is contained in:
parent
4e5c33f861
commit
5c8bc871af
@ -0,0 +1,99 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import traceback
|
||||
from functools import partial
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.v2.model import base_extension, pose_blender
|
||||
|
||||
|
||||
class BakePosesToTimeline(base_extension.PoseWranglerExtension):
|
||||
__category__ = "Core Extensions"
|
||||
|
||||
@property
|
||||
def view(self):
|
||||
if self._view is not None:
|
||||
return self._view
|
||||
from PySide2 import QtWidgets
|
||||
self._view = QtWidgets.QPushButton("Bake Poses To Timeline")
|
||||
self._view.clicked.connect(partial(self.execute, None))
|
||||
return self._view
|
||||
|
||||
def execute(self, context=None, **kwargs):
|
||||
if context is None:
|
||||
context = self.api.get_context()
|
||||
if context.current_solver is not None:
|
||||
bake_poses_to_timeline(solver=context.current_solver, view=self._display_view)
|
||||
|
||||
|
||||
def bake_poses_to_timeline(start_frame=0, anim_layer=None, solver=None, view=False):
|
||||
"""
|
||||
Bakes the poses to the timeline and sets the time range to the given animation.
|
||||
:param start_frame :type int: start frame of he baked animation
|
||||
:param anim_layer :type str: if given the animations will be baked on that layer or created if it doesn't exist
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
:param view :type bool: is the view present
|
||||
"""
|
||||
# Grab all the transforms for the solver
|
||||
transforms = solver.drivers()
|
||||
transforms.extend(solver.driven_nodes(pose_blender.UEPoseBlenderNode.node_type))
|
||||
|
||||
bake_enabled = True
|
||||
pose_list = []
|
||||
# Set default anim layer if one isn't specified
|
||||
if not anim_layer:
|
||||
anim_layer = "BaseAnimation"
|
||||
# If the layer doesnt exist, create it
|
||||
if not cmds.animLayer(anim_layer, query=True, exists=True):
|
||||
cmds.animLayer(anim_layer)
|
||||
try:
|
||||
cmds.autoKeyframe(e=1, st=0)
|
||||
# If we are running with the view, provide a popup
|
||||
if view:
|
||||
from PySide2 import QtWidgets
|
||||
msg = ("This will bake the poses to the timeline, change your time range, "
|
||||
"and delete inputs on driving and driven transforms.\n"
|
||||
"Do you want this to happen?")
|
||||
|
||||
ret = QtWidgets.QMessageBox.warning(
|
||||
None, "WARNING: DESTRUCTIVE FUNCTION", msg,
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||||
QtWidgets.QMessageBox.Cancel
|
||||
)
|
||||
if ret == QtWidgets.QMessageBox.StandardButton.Ok:
|
||||
bake_enabled = True
|
||||
cmds.undoInfo(openChunk=True, undoName='Bake poses to timeline')
|
||||
else:
|
||||
bake_enabled = False
|
||||
|
||||
# If we are baking, do the bake
|
||||
if bake_enabled:
|
||||
i = start_frame
|
||||
for pose_name in solver.poses():
|
||||
# let's key it on the previous and next frames before we pose it
|
||||
cmds.select(transforms)
|
||||
cmds.animLayer(anim_layer, addSelectedObjects=True, e=True)
|
||||
cmds.setKeyframe(transforms, t=[(i - 1), (i + 1)], animLayer=anim_layer)
|
||||
|
||||
# assume the pose
|
||||
solver.go_to_pose(pose_name)
|
||||
|
||||
cmds.setKeyframe(transforms, t=[i], animLayer=anim_layer)
|
||||
|
||||
pose_list.append(pose_name)
|
||||
|
||||
# increment to next keyframe
|
||||
i += 1
|
||||
|
||||
# set the range to the number of keyframes
|
||||
cmds.playbackOptions(minTime=0, maxTime=i, animationStartTime=0, animationEndTime=i - 1)
|
||||
cmds.dgdirty(a=1)
|
||||
return pose_list
|
||||
|
||||
cmds.dgdirty(a=1)
|
||||
except Exception as e:
|
||||
LOG.error(traceback.format_exc())
|
||||
finally:
|
||||
cmds.undoInfo(closeChunk=True)
|
@ -0,0 +1,224 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import traceback
|
||||
from functools import partial
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.v2.model import base_extension, exceptions, pose_blender
|
||||
|
||||
|
||||
class BakePosesToTimeline(base_extension.PoseWranglerExtension):
|
||||
__category__ = "Core Extensions"
|
||||
|
||||
@property
|
||||
def view(self):
|
||||
if self._view is not None:
|
||||
return self._view
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
self._view = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self._view.setLayout(layout)
|
||||
|
||||
label = QtWidgets.QLabel("Copy/Paste Driven Transforms")
|
||||
layout.addWidget(label)
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
copy_button = QtWidgets.QPushButton("Copy")
|
||||
copy_button.clicked.connect(partial(self.copy_driven_trs, None))
|
||||
button_layout.addWidget(copy_button)
|
||||
|
||||
paste_button = QtWidgets.QPushButton("Paste")
|
||||
button_layout.addWidget(paste_button)
|
||||
|
||||
spin_label = QtWidgets.QLabel("Multiplier: ")
|
||||
button_layout.addWidget(spin_label)
|
||||
|
||||
spin_box = QtWidgets.QDoubleSpinBox()
|
||||
spin_box.setMinimum(0.01)
|
||||
spin_box.setSingleStep(0.1)
|
||||
spin_box.setValue(1.0)
|
||||
button_layout.addWidget(spin_box)
|
||||
|
||||
paste_button.clicked.connect(partial(self.paste_driven_trs, lambda: spin_box.value(), None))
|
||||
|
||||
return self._view
|
||||
|
||||
def execute(self, context=None, **kwargs):
|
||||
copy = kwargs.get('copy', False)
|
||||
driver = kwargs.get('driver', True)
|
||||
multiplier = kwargs.get('multiplier', 1.0)
|
||||
if context is None:
|
||||
context = self.api.get_context()
|
||||
if context.current_solver is not None:
|
||||
if copy:
|
||||
if driver:
|
||||
self.copy_driver_trs()
|
||||
else:
|
||||
self.copy_driven_trs()
|
||||
else:
|
||||
if driver:
|
||||
self.paste_driver_trs(multiplier=multiplier)
|
||||
else:
|
||||
self.paste_driven_trs(multiplier=multiplier)
|
||||
|
||||
def copy_driven_trs(self, solver=None):
|
||||
"""
|
||||
Copy the driven transforms translate, rotate and scale for the specified solver
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
"""
|
||||
context = self.api.get_context()
|
||||
solver = solver or context.current_solver
|
||||
CopyPasteTRS.copy_driven(solver)
|
||||
|
||||
def paste_driven_trs(self, multiplier=1.0, solver=None):
|
||||
"""
|
||||
Paste the driven transforms translate, rotate and scale based on the multiplier specified
|
||||
:param multiplier :type float: scale multiplier
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
"""
|
||||
# Get the solver if it hasn't been specified
|
||||
if callable(multiplier):
|
||||
multiplier = multiplier()
|
||||
context = self.api.get_context()
|
||||
solver = solver or context.current_solver
|
||||
edit_status = self.api.get_solver_edit_status(solver=solver)
|
||||
if not edit_status:
|
||||
self.api.edit_solver(edit=True, solver=solver)
|
||||
CopyPasteTRS.paste_driven(multiplier=multiplier)
|
||||
|
||||
def copy_driver_trs(self, solver=None):
|
||||
"""
|
||||
Copy the driver transforms translate, rotate and scale for the specified solver
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
"""
|
||||
context = self.api.get_context()
|
||||
# Get the solver if it hasn't been specified
|
||||
solver = solver or context.current_solver
|
||||
# Copy the driver transforms
|
||||
CopyPasteTRS.copy_driver(solver)
|
||||
|
||||
def paste_driver_trs(self, multiplier=1.0):
|
||||
"""
|
||||
Paste the driver transforms rotate and scale based on the multiplier specified
|
||||
:param multiplier :type float: scale multiplier
|
||||
"""
|
||||
if callable(multiplier):
|
||||
multiplier = multiplier()
|
||||
CopyPasteTRS.paste_driver(multiplier=multiplier)
|
||||
|
||||
|
||||
class TRSError(exceptions.exceptions.PoseWranglerException):
|
||||
pass
|
||||
|
||||
|
||||
class CopyPasteTRS(object):
|
||||
"""
|
||||
Class wrapper for copying and pasting TRS data
|
||||
"""
|
||||
# Dicts to store the driver and driven data. Driver data is only used when auto generating poses
|
||||
TRS_DRIVEN_DATA = {}
|
||||
TRS_DRIVER_DATA = {}
|
||||
|
||||
@classmethod
|
||||
def copy_driven(cls, solver):
|
||||
"""
|
||||
Copy the driven transforms TRS data for the specified solver
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
"""
|
||||
cls._copy(
|
||||
transforms=solver.driven_nodes(pose_blender.UEPoseBlenderNode.node_type),
|
||||
target_datastore=cls.TRS_DRIVEN_DATA
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def paste_driven(cls, multiplier=1.0):
|
||||
"""
|
||||
Paste the copied driven values onto the driven with the specified modifier
|
||||
:param multiplier :type float: multiplier value
|
||||
"""
|
||||
cls._paste(multiplier=multiplier, target_datastore=cls.TRS_DRIVEN_DATA)
|
||||
|
||||
@classmethod
|
||||
def copy_driver(cls, solver):
|
||||
"""
|
||||
Copy the driver transforms TRS data for the specified solver
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
"""
|
||||
cls._copy(transforms=solver.drivers(), target_datastore=cls.TRS_DRIVER_DATA, attributes=['rotate', 'scale'])
|
||||
|
||||
@classmethod
|
||||
def paste_driver(cls, multiplier=1.0):
|
||||
"""
|
||||
Paste the copied driver values onto the driver with the specified modifier
|
||||
:param multiplier :type float: multiplier value
|
||||
"""
|
||||
cls._paste(multiplier=multiplier, target_datastore=cls.TRS_DRIVER_DATA)
|
||||
|
||||
@classmethod
|
||||
def _copy(cls, transforms, target_datastore, attributes=None):
|
||||
"""
|
||||
Copy the specified transforms attributes to the specified datastore
|
||||
:param transforms :type list: list of maya transform nodes
|
||||
:param target_datastore :type dict: reference to cls.TRS_DRIVER_DATA or cls.TRS_DRIVEN_DATA
|
||||
:param attributes :type list: list of attribute names to store
|
||||
"""
|
||||
# If no attributes are specified, use TRS
|
||||
if not attributes:
|
||||
attributes = ['translate', 'rotate', 'scale']
|
||||
# Clear the datastore before we copy
|
||||
target_datastore.clear()
|
||||
# For each transform, get the specified attributes and store them in a dictionary
|
||||
for transform in transforms:
|
||||
target_datastore[transform] = {attr: cmds.getAttr(
|
||||
"{transform}.{attr}".format(
|
||||
transform=transform,
|
||||
attr=attr
|
||||
)
|
||||
)[0]
|
||||
for attr in attributes}
|
||||
|
||||
LOG.info("Successfully copied TRS for {transforms}".format(transforms=transforms))
|
||||
|
||||
@classmethod
|
||||
def _paste(cls, multiplier, target_datastore):
|
||||
"""
|
||||
Paste the attributes from the target_datastore multiplied by the given multiplier
|
||||
:param multiplier :type float: multiplier value
|
||||
:param target_datastore : type dict: reference to cls.TRS_DRIVER_DATA or cls.TRS_DRIVEN_DATA
|
||||
"""
|
||||
# If the target datastore is empty, we can't paste
|
||||
if not target_datastore:
|
||||
raise TRSError("No data has been copied. Unable to paste")
|
||||
|
||||
# Iterate through the items in the datastore
|
||||
for transform, data in target_datastore.items():
|
||||
# Iterate through the attribute names and values in the datastore
|
||||
for attr, values in data.items():
|
||||
# Set the attribute to the multiplied value
|
||||
try:
|
||||
cmds.setAttr(
|
||||
"{transform}.{attr}".format(transform=transform, attr=attr), values[0] * multiplier,
|
||||
values[1] * multiplier,
|
||||
values[2] * multiplier
|
||||
)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
# Calculate and set the scale
|
||||
scale = [((s - 1.0) * multiplier) + 1 for s in data['scale']]
|
||||
try:
|
||||
cmds.setAttr("{driven}.scale".format(driven=transform), *scale)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
LOG.info(
|
||||
"Successfully pasted TRS with multiplier: {multiplier} for {transforms}".format(
|
||||
multiplier=multiplier,
|
||||
transforms=list(target_datastore.keys())
|
||||
)
|
||||
)
|
@ -0,0 +1,71 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from functools import partial
|
||||
|
||||
from epic_pose_wrangler.v2.model import base_extension
|
||||
from epic_pose_wrangler.v2.extensions import copy_paste_trs
|
||||
|
||||
|
||||
class GenerateInbetweens(base_extension.PoseWranglerExtension):
|
||||
__category__ = "Core Extensions"
|
||||
|
||||
@property
|
||||
def view(self):
|
||||
if self._view is not None:
|
||||
return self._view
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
self._view = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self._view.setLayout(layout)
|
||||
|
||||
label = QtWidgets.QLabel("Copy/Paste Driven Transforms")
|
||||
layout.addWidget(label)
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
generate_inbetweens_button = QtWidgets.QPushButton("Generate Inbetweens")
|
||||
button_layout.addWidget(generate_inbetweens_button)
|
||||
|
||||
spin_label = QtWidgets.QLabel("Pose Count: ")
|
||||
button_layout.addWidget(spin_label)
|
||||
|
||||
spin_box = QtWidgets.QSpinBox()
|
||||
spin_box.setMinimum(1)
|
||||
spin_box.setSingleStep(1)
|
||||
spin_box.setValue(1)
|
||||
button_layout.addWidget(spin_box)
|
||||
|
||||
generate_inbetweens_button.clicked.connect(partial(self.generate_inbetweens, lambda: spin_box.value()))
|
||||
|
||||
return self._view
|
||||
|
||||
def generate_inbetweens(self, count=1, pose_prefix="pose"):
|
||||
"""
|
||||
Generate inbetween poses between the current position and the default pose
|
||||
:param count :type int: number of poses to generate
|
||||
:param pose_prefix :type: name of the pose
|
||||
"""
|
||||
context = self.api.get_context()
|
||||
if callable(count):
|
||||
count = count()
|
||||
|
||||
solver = context.current_solver
|
||||
copy_paste_trs_action = self.api.get_extension_by_type(copy_paste_trs.BakePosesToTimeline)
|
||||
copy_paste_trs_action.copy_driven_trs(solver=solver)
|
||||
copy_paste_trs_action.copy_driver_trs(solver=solver)
|
||||
|
||||
# Calculate the multiplier increment based on the number of desired poses
|
||||
multiplier_increment = 1.0 / float((count + 1))
|
||||
# Set the start multiplier
|
||||
multiplier = 1.0
|
||||
# Iterate through the number of desired poses
|
||||
for i in range(count):
|
||||
# Generate the new multiplier
|
||||
multiplier -= multiplier_increment
|
||||
# Paste the driver and driven translate, rotate and scale based on the new multiplier
|
||||
# Note: Driver only gets rotate and scale applied to it.
|
||||
copy_paste_trs_action.paste_driven_trs(multiplier)
|
||||
copy_paste_trs_action.paste_driver_trs(multiplier)
|
||||
# Create a new pose at this position
|
||||
self.api.create_pose(pose_name="{pose_prefix}_{i}".format(pose_prefix=pose_prefix, i=i), solver=solver)
|
832
Scripts/Animation/epic_pose_wrangler/v2/main.py
Normal file
832
Scripts/Animation/epic_pose_wrangler/v2/main.py
Normal file
@ -0,0 +1,832 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from maya.api import OpenMaya as om
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.model import mirror_mapping, settings, exceptions
|
||||
from epic_pose_wrangler.model.api import RBFAPI
|
||||
from epic_pose_wrangler.v2.model import api, base_action, base_extension, pose_blender, context, utils
|
||||
from epic_pose_wrangler.v2.model import exceptions as api_exceptions
|
||||
|
||||
|
||||
class UERBFAPI(RBFAPI):
|
||||
"""
|
||||
Main entry point for interacting with the UERBFSolverNode and UEPoseBlenderNode
|
||||
|
||||
>>> from epic_pose_wrangler.v2 import main
|
||||
>>> rbf_api = main.UERBFAPI(view=False)
|
||||
>>> rbf_api.create_rbf_solver(solver_name="ExampleSolver", drivers=['leg_l'])
|
||||
"""
|
||||
VERSION = "2.0.0"
|
||||
|
||||
def __init__(self, view=False, parent=None, file_path=None):
|
||||
super(UERBFAPI, self).__init__(view=view, parent=parent)
|
||||
|
||||
self._view = None
|
||||
|
||||
# Set up a default mirror mapping
|
||||
self._mirror_mapping = mirror_mapping.MirrorMapping()
|
||||
# Instantiate a settings manager to restore settings from previous sessions
|
||||
self._settings_manager = settings.SettingsManager()
|
||||
# Empty var to store the current solver for convenience so that you don't have to pass the solver through
|
||||
# every time you call a function
|
||||
self._current_solver = None
|
||||
# Empty list to store any core and custom actions used to extend the functionality of PoseWrangler
|
||||
self._extensions = []
|
||||
|
||||
# If view is requested, import and build the UI. We use a local import so that any QtWidget dependencies
|
||||
# won't be loaded if the user is running from mayapy
|
||||
if view:
|
||||
from epic_pose_wrangler.v2.view import pose_wrangler_window
|
||||
self._view = pose_wrangler_window.PoseWranglerWindow()
|
||||
# Connect up the views events to the appropriate functions
|
||||
self._setup_view_events()
|
||||
|
||||
# Load the contents of the current scene
|
||||
LOG.info("Loading PoseWrangler...")
|
||||
self.load()
|
||||
# If a file path is specified, deserialize and load it
|
||||
if file_path:
|
||||
self.deserialize_from_file(file_path=file_path)
|
||||
|
||||
@property
|
||||
def extensions(self):
|
||||
"""
|
||||
:return: list of pose wrangler extensions currently loaded
|
||||
:rtype: list[pose_wrangler.v2.model.base_extension.PoseWranglerExtension]
|
||||
"""
|
||||
return self._extensions
|
||||
|
||||
@property
|
||||
def view(self):
|
||||
"""
|
||||
:return: reference to the ui QWidget
|
||||
:rtype: QtWidgets.QWidget or None
|
||||
"""
|
||||
return self._view
|
||||
|
||||
@property
|
||||
def current_solver(self):
|
||||
"""
|
||||
:return: reference to the current solver
|
||||
:rtype: api.RBFNode or None
|
||||
"""
|
||||
return self._current_solver
|
||||
|
||||
@current_solver.setter
|
||||
def current_solver(self, solver):
|
||||
# If the specified solver is not an RBFNode class, raise exception
|
||||
if not isinstance(solver, api.RBFNode):
|
||||
raise api_exceptions.InvalidSolverError(
|
||||
"Solver is not a valid {node_type} node".format(node_type=api.RBFNode)
|
||||
)
|
||||
# Set the current solver
|
||||
self._current_solver = solver
|
||||
solvers = self.rbf_solvers
|
||||
for action in self._extensions:
|
||||
action.on_context_changed(
|
||||
context.PoseWranglerContext(current_solver=solver, solvers=solvers)
|
||||
)
|
||||
# If we are displaying the view, update it with the current selection
|
||||
if self._view:
|
||||
# Generate a kwarg dict.
|
||||
kwargs = {
|
||||
"solver": solver,
|
||||
# We want to store a reference to the MObject in case the name of the node is changed and the
|
||||
# user doesn't refresh the UI
|
||||
"drivers": {driver: om.MGlobal.getSelectionListByName(driver).getDependNode(0) for driver in
|
||||
solver.drivers()},
|
||||
# We want to display both the driven transforms and connected blendshapes.
|
||||
# NOTE: Blendshapes will only be found if connected up via this API. For more info see:
|
||||
# self.create_blendshape or self.add_blendshape
|
||||
"driven_transforms": {
|
||||
'transform': solver.driven_nodes(pose_blender.UEPoseBlenderNode.node_type),
|
||||
'blendshape': {mesh: solver.get_pose_for_blendshape_mesh(mesh) for mesh in
|
||||
solver.driven_nodes(type='blendShape')}
|
||||
},
|
||||
"poses": solver.poses()
|
||||
}
|
||||
# Load the solver settings in the view with the given kwargs
|
||||
self._view.load_solver_settings(**kwargs)
|
||||
|
||||
@property
|
||||
def mirror_mapping(self):
|
||||
"""
|
||||
:return: reference to the currently loaded mirror mapping
|
||||
:rtype: mirror_mapping.MirrorMapping object
|
||||
"""
|
||||
return self._mirror_mapping
|
||||
|
||||
@property
|
||||
def rbf_solvers(self):
|
||||
"""
|
||||
:return: list of rbf solvers in the scene
|
||||
:rtype: list
|
||||
"""
|
||||
return api.RBFNode.find_all()
|
||||
|
||||
# ================================================ Solvers ======================================================= #
|
||||
|
||||
def create_rbf_solver(self, solver_name, drivers=None):
|
||||
"""
|
||||
Create an rbf solver node with the given name and the specified driver transforms
|
||||
|
||||
:param solver_name: name of the solver node
|
||||
:type solver_name: str
|
||||
:param drivers: list of driver transform node names
|
||||
:type drivers: list
|
||||
:return: RBFNode ref
|
||||
:rtype: api.RBFNode
|
||||
"""
|
||||
# If no drivers are specified, grab the current selection
|
||||
drivers = drivers or utils.get_selection(_type='transform')
|
||||
LOG.debug("Creating RBF solver '{name}' with drivers: {drivers}".format(name=solver_name, drivers=drivers))
|
||||
# Create the solver
|
||||
solver = api.RBFNode.create(solver_name)
|
||||
# If drivers have been specified, add them
|
||||
if drivers:
|
||||
self.add_drivers(drivers=drivers, solver=solver)
|
||||
# If we are in UI mode, add the solver to the view
|
||||
if self._view:
|
||||
self._view.add_rbf_solver(solver)
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
# Set the current solver to edit mode
|
||||
self.edit_solver(edit=True, solver=solver)
|
||||
# Return the new solver
|
||||
return solver
|
||||
|
||||
def delete_rbf_solver(self, solver=None):
|
||||
"""
|
||||
Delete the specified solver
|
||||
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# If no solver is specified, grab the current solver
|
||||
solver = solver or self._current_solver
|
||||
# Delete the solver
|
||||
solver.delete()
|
||||
# If we are using the UI, delete the solver
|
||||
if self._view:
|
||||
self._view.delete_solver(solver)
|
||||
# If the current solver is this solver, set current to None
|
||||
if self._current_solver == solver:
|
||||
self._current_solver = None
|
||||
|
||||
def edit_solver(self, edit=True, solver=None):
|
||||
"""
|
||||
Edit or finish editing the specified solver. Enables pose creation/driven node changes via the ui
|
||||
|
||||
:param edit: set edit mode on or off
|
||||
:type edit: bool
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# If no solver is specified, grab the current solver
|
||||
solver = solver or self._current_solver
|
||||
# Edit the solver
|
||||
solver.edit_solver(edit=edit)
|
||||
# If we have a ui, update the edit mode status
|
||||
if self._view:
|
||||
self._view.edit_solver(solver=solver, edit=edit)
|
||||
LOG.debug("Setting edit status: {status} for solver: {solver}".format(status=edit, solver=solver))
|
||||
self.current_solver = solver
|
||||
|
||||
def mirror_rbf_solver(self, solver=None):
|
||||
"""
|
||||
Mirror the current solver
|
||||
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
:return: mirrored solver reference
|
||||
:rtype: api.RBFNode
|
||||
"""
|
||||
# If no solver is specified, grab the current solver
|
||||
solver = solver or self._current_solver
|
||||
mirrored_solver = solver.mirror(self.mirror_mapping)
|
||||
if self._view:
|
||||
self._load_view()
|
||||
|
||||
# Set the current solver to the new solver
|
||||
self.current_solver = mirrored_solver
|
||||
# Return the new solver
|
||||
return mirrored_solver
|
||||
|
||||
def get_rbf_solver_by_name(self, solver_name):
|
||||
"""
|
||||
Searches the scene for an rbf solver with the given name. Case insensitive
|
||||
|
||||
:param solver_name: Solver node name
|
||||
:type solver_name: str
|
||||
:return: found node or None
|
||||
:rtype: api.RBFNode or None
|
||||
"""
|
||||
for solver in api.RBFNode.find_all():
|
||||
if str(solver).lower() == solver_name.lower():
|
||||
return solver
|
||||
|
||||
# ================================================ Drivers ======================================================= #
|
||||
|
||||
def add_drivers(self, drivers=None, solver=None):
|
||||
"""
|
||||
Add the specified drivers to the specified solver
|
||||
|
||||
:param drivers: list of transform nodes
|
||||
:type drivers: list
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# If no solver is specified, grab the current solver
|
||||
solver = solver or self._current_solver
|
||||
# If we already have more than one pose, we can't add a new driver (would require manually updating every pose)
|
||||
if solver.num_poses() > 1:
|
||||
raise api_exceptions.InvalidPoseIndex("Too many poses have been created, unable to add a new driver.")
|
||||
# Check if we have a default pose
|
||||
if solver.has_pose(pose_name='default'):
|
||||
# Delete the current rest pose if found
|
||||
solver.delete_pose(pose_name='default')
|
||||
# Add new drivers
|
||||
solver.add_driver(transform_nodes=drivers)
|
||||
# Create the new rest pose with all the drivers
|
||||
solver.add_pose_from_current(pose_name='default')
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def remove_drivers(self, drivers, solver=None):
|
||||
"""
|
||||
Remove the specified drivers from the specified solver
|
||||
|
||||
:param drivers: list of driver transform nodes
|
||||
:type drivers: list
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Get the solver if it hasn't been specified
|
||||
solver = solver or self._current_solver
|
||||
# Remove the drivers from the solver
|
||||
solver.remove_drivers(drivers)
|
||||
# Have to re-wrap solver due to a bug with targets array indexing
|
||||
self.current_solver = api.RBFNode(str(solver))
|
||||
|
||||
# ================================================ Driven ======================================================== #
|
||||
|
||||
def add_driven_transforms(self, driven_nodes=None, solver=None, edit=False):
|
||||
"""
|
||||
Add driven transforms to the specified solver
|
||||
|
||||
:param driven_nodes: list of transform nodes
|
||||
:type driven_nodes: list
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
:param edit: should this transform not be connected to the pose blender output upon creation
|
||||
:type edit: bool
|
||||
"""
|
||||
# Get the solver if it hasn't been specified
|
||||
solver = solver or self._current_solver
|
||||
solver.add_driven_transforms(driven_nodes, edit=edit)
|
||||
# Update the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def remove_driven(self, driven_nodes, solver=None):
|
||||
"""
|
||||
Remove driven transforms from the specified solver
|
||||
|
||||
:param driven_nodes: list of transform nodes
|
||||
:type driven_nodes: list
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Get the solver if it hasn't been specified
|
||||
solver = solver or self._current_solver
|
||||
solver.remove_driven_transforms(driven_nodes)
|
||||
# Update the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
# ============================================== Blendshapes ===================================================== #
|
||||
|
||||
def add_blendshape(self, pose_name, mesh_name, base_mesh, solver=None):
|
||||
"""
|
||||
Add an existing blendshape for the current pose
|
||||
|
||||
:param pose_name: name of the pose the blendshape is associated with
|
||||
:type pose_name: str
|
||||
:param mesh_name: name of the existing blendshape mesh
|
||||
:type mesh_name: str
|
||||
:param base_mesh: name of the mesh the blendshape mesh is derived from
|
||||
:type base_mesh: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Add the blendshape
|
||||
solver.add_existing_blendshape(pose_name, mesh_name, base_mesh)
|
||||
self.current_solver = solver
|
||||
|
||||
def create_blendshape(self, pose_name, mesh_name=None, edit=False, solver=None):
|
||||
"""
|
||||
Create a new blendshape for the given pose and mesh
|
||||
|
||||
:param pose_name: name of the pose to create this blendshape for
|
||||
:type pose_name: str
|
||||
:param mesh_name: name of the mesh to create the blendshape from
|
||||
:type mesh_name: str
|
||||
:param edit: should this blendshape be edited straight away
|
||||
:type edit: bool
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
:return: name of the newly created blendshape mesh
|
||||
:rtype: str
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Create a new blendshape mesh at the specified pose
|
||||
blendshape_mesh_name = solver.create_blendshape(pose_name, mesh_name)
|
||||
# If we are editing, change the mode
|
||||
if edit:
|
||||
self.edit_blendshape(pose_name=pose_name, edit=True, solver=solver)
|
||||
else:
|
||||
# If we aren't editing, update the current solver
|
||||
self.current_solver = solver
|
||||
# Return the name of the new blendshape mesh
|
||||
return blendshape_mesh_name
|
||||
|
||||
def delete_blendshape(self, pose_name, solver=None):
|
||||
"""
|
||||
Delete the blendshape associated with the specified pose
|
||||
|
||||
:param pose_name: name of the pose to delete blendshapes for
|
||||
:type pose_name: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Delete the blendshape mesh at the specified pose
|
||||
solver.delete_blendshape(pose_name, delete_mesh=True)
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def edit_blendshape(self, pose_name, edit=True, solver=None):
|
||||
"""
|
||||
Edit or finish editing the blendshape associated with the specified pose name
|
||||
|
||||
:param pose_name: name of the pose the blendshape is associated with
|
||||
:type pose_name: str
|
||||
:param edit: True = enable editing, False = finish editing
|
||||
:type edit: bool
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Edit or finish editing the blendshape
|
||||
solver.edit_blendshape(pose_name, edit=edit)
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
# If we have a ui, update the blendshapes status
|
||||
if self._view:
|
||||
self._view.edit_blendshape(pose_name, edit=edit)
|
||||
|
||||
def isolate_blendshape(self, pose_name, isolate=True, solver=None):
|
||||
"""
|
||||
Isolate the blendshape associated with the specified pose name, disabling all other blendshapes.
|
||||
|
||||
:param pose_name: name of the pose the blendshape is associated with
|
||||
:type pose_name: str
|
||||
:param isolate: True = isolate the blendshape, False = reconnect all disconnected blendshapes
|
||||
:type isolate: bool
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Isolate or un-isolate the blendshape
|
||||
solver.isolate_blendshape(pose_name=pose_name, isolate=isolate)
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
# ================================================= Poses ======================================================== #
|
||||
|
||||
def create_pose(self, pose_name, solver=None):
|
||||
"""
|
||||
Create a new pose for the specified solver
|
||||
|
||||
:param pose_name: name of the new pose
|
||||
:type pose_name: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Create a new pose from the current position
|
||||
solver.add_pose_from_current(pose_name=pose_name)
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def delete_pose(self, pose_name, solver=None):
|
||||
"""
|
||||
Remove a pose from the given solver
|
||||
|
||||
:param pose_name: name of the pose to remove
|
||||
:type pose_name: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Delete the pose
|
||||
solver.delete_pose(pose_name)
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def go_to_pose(self, pose_name, solver=None):
|
||||
"""
|
||||
Move the driver/driven transforms to the given pose
|
||||
|
||||
:param pose_name: name of the pose
|
||||
:type pose_name: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Go to the pose
|
||||
solver.go_to_pose(pose_name=pose_name)
|
||||
|
||||
def mirror_pose(self, pose_name, solver=None):
|
||||
"""
|
||||
Mirror a pose to the mirror of the current solver
|
||||
|
||||
:param pose_name: name of the pose
|
||||
:type pose_name: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Mirror the pose
|
||||
solver.mirror_pose(pose_name=pose_name, mirror_mapping=self.mirror_mapping)
|
||||
# Reload all the solvers
|
||||
self.load()
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def mute_pose(self, pose_name, mute=True, solver=None):
|
||||
"""
|
||||
Mute or unmute the specified pose, removing all influences of the pose from the solver.
|
||||
NOTE: This will affect the solver radius if automatic radius is enabled.
|
||||
|
||||
:param pose_name: name of the pose
|
||||
:type pose_name: str
|
||||
:param mute: mute or unmute the pose
|
||||
:type mute: bool
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Mute or unmute the pose
|
||||
solver.mute_pose(pose_name=pose_name, mute=mute)
|
||||
# Set the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def rename_pose(self, pose_name, new_pose_name, solver=None):
|
||||
"""
|
||||
Rename a pose on the given solver
|
||||
|
||||
:param pose_name: name of the pose
|
||||
:type pose_name: str
|
||||
:param new_pose_name: new name of the pose
|
||||
:type new_pose_name: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Get the current pose index
|
||||
pose_index = solver.pose_index(pose_name=pose_name)
|
||||
# Rename the pose at the given index
|
||||
solver.rename_pose(pose_index=pose_index, pose_name=new_pose_name)
|
||||
# Update the current solver
|
||||
self.current_solver = solver
|
||||
|
||||
def update_pose(self, pose_name, solver=None):
|
||||
"""
|
||||
Update the pose for the given solver
|
||||
|
||||
:param pose_name: name of the pose to update
|
||||
:type pose_name: str
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
"""
|
||||
# Ensure we have a solver
|
||||
solver = solver or self._current_solver
|
||||
# Update the pose with the current driver/driven positions
|
||||
solver.add_pose_from_current(pose_name, update=True)
|
||||
LOG.info("Updated {pose_name}".format(pose_name=pose_name))
|
||||
|
||||
self.current_solver = solver
|
||||
|
||||
# ================================================== IO ========================================================== #
|
||||
|
||||
def deserialize_from_file(self, file_path, solver_names=None):
|
||||
"""
|
||||
Deserialize solvers from a specific file.
|
||||
|
||||
:param file_path: json file to load
|
||||
:type file_path: str
|
||||
"""
|
||||
# Check the path exists
|
||||
if not os.path.exists(file_path):
|
||||
raise exceptions.PoseWranglerIOError(
|
||||
"Unable to deserialize from {file_path}, path does not exist".format(file_path=file_path)
|
||||
)
|
||||
if solver_names is None:
|
||||
solver_names = []
|
||||
# Load the json file and deserialize
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.loads(f.read())
|
||||
self.deserialize(data, solver_names=solver_names)
|
||||
LOG.debug("Successfully loaded solvers: {solvers}".format(solvers=solver_names or list(data.keys())))
|
||||
LOG.info(
|
||||
"Successfully loaded {num_solvers} solver(s) from {file_path}".format(
|
||||
num_solvers=len(data),
|
||||
file_path=file_path
|
||||
)
|
||||
)
|
||||
|
||||
def serialize_to_file(self, file_path, solvers=None):
|
||||
"""
|
||||
Serialize the specified solvers to a file
|
||||
|
||||
:param file_path: json file to serialize
|
||||
:type file_path: str
|
||||
:param solvers: list of api.RBFNode to serialize
|
||||
:type solvers: list
|
||||
"""
|
||||
# Check that the directory exists before writing to it
|
||||
if not os.path.exists(os.path.dirname(file_path)):
|
||||
os.makedirs(file_path)
|
||||
# Dump the serialized data to the json file
|
||||
with open(file_path, 'w') as f:
|
||||
data = self.serialize(solvers=solvers)
|
||||
LOG.debug("Successfully serialized solvers: {solvers}".format(solvers=list(data.keys())))
|
||||
f.write(json.dumps(data))
|
||||
LOG.info(
|
||||
"Successfully exported {num_solvers} solver(s) to {file_path}".format(
|
||||
num_solvers=len(data),
|
||||
file_path=file_path
|
||||
)
|
||||
)
|
||||
|
||||
def deserialize(self, data, solver_names=None):
|
||||
"""
|
||||
Deserialize and load the solvers from the data specified
|
||||
|
||||
:param data: serialized solver data
|
||||
:type data: dict
|
||||
:param solver_names: list of solver names to load from the data
|
||||
:type solver_names: list, optional
|
||||
"""
|
||||
for solver_name, solver_data in data.items():
|
||||
if solver_names and solver_name in solver_names or not solver_names:
|
||||
api.RBFNode.create_from_data(solver_data)
|
||||
self.load()
|
||||
|
||||
def serialize(self, solvers=None):
|
||||
"""
|
||||
Serialize the specified solvers
|
||||
|
||||
:param solvers: list of api.RBFNode to serialize
|
||||
:type solvers: list
|
||||
:return: serialized solver data
|
||||
:rtype: dict
|
||||
"""
|
||||
return {str(solver): solver.data() for solver in solvers or self.rbf_solvers}
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load the default pose wrangler settings
|
||||
"""
|
||||
# Set the mirror mapping from user settings
|
||||
self.set_mirror_mapping()
|
||||
self._load_extensions()
|
||||
|
||||
if self._view:
|
||||
self._load_view()
|
||||
|
||||
# =============================================== Utilities ====================================================== #
|
||||
|
||||
def get_context(self):
|
||||
"""
|
||||
Get the current solver context
|
||||
|
||||
:return: pose wrangler context containing the current solver and all rbf solvers
|
||||
:rtype: context.PoseWranglerContext
|
||||
"""
|
||||
return context.PoseWranglerContext(current_solver=self.current_solver, solvers=self.rbf_solvers)
|
||||
|
||||
def get_ui_context(self):
|
||||
"""
|
||||
If the ui is available, return the ui context
|
||||
|
||||
:return: ui context containing the current state of the ui
|
||||
:rtype: ui_context.PoseWranglerUIContext or None
|
||||
"""
|
||||
if self._view:
|
||||
return self._view.get_context()
|
||||
|
||||
def get_extension_by_type(self, class_ref):
|
||||
"""
|
||||
Get a reference to one of the loaded extensions from a class type
|
||||
|
||||
:param class_ref: reference to an extension class
|
||||
:type class_ref: base_extension.PoseWranglerExtension
|
||||
:return: reference to a loaded extension if one is loaded
|
||||
:rtype: base_extension.PoseWranglerExtension instance or None
|
||||
"""
|
||||
extensions = [extension for extension in self._extensions if isinstance(extension, class_ref)]
|
||||
if not extensions:
|
||||
return
|
||||
return extensions[0]
|
||||
|
||||
def set_mirror_mapping(self, path=None):
|
||||
"""
|
||||
Set the mirror mapping from a file
|
||||
|
||||
:param path: path to json mirror mapping file
|
||||
:type path: str
|
||||
"""
|
||||
# If no path is specified, get the path stored in the settings manager
|
||||
if path is None:
|
||||
path = self._settings_manager.get_setting("MirrorMappingFile")
|
||||
if path:
|
||||
# If the path doesnt exist, raise an exception
|
||||
if not os.path.exists(path):
|
||||
raise exceptions.InvalidMirrorMapping("Unable to find mapping file: {file}".format(file=path))
|
||||
# Set the new mirror mapping
|
||||
self._mirror_mapping = mirror_mapping.MirrorMapping(file_path=path)
|
||||
self._settings_manager.set_setting("MirrorMappingFile", path)
|
||||
# If we are using the UI, update the mirror mapping file path
|
||||
if self._view:
|
||||
self._view.update_mirror_mapping_file(self._mirror_mapping.file_path)
|
||||
|
||||
def get_solver_edit_status(self, solver):
|
||||
"""
|
||||
Check if the current solver is in 'Edit' mode
|
||||
|
||||
:param solver: solver reference
|
||||
:type solver: api.RBFNode
|
||||
:return: True = in edit mode, False = not in edit mode
|
||||
:rtype: bool
|
||||
"""
|
||||
# Get the solver if it hasn't been specified
|
||||
solver = solver or self._current_solver
|
||||
return solver.get_solver_edit_status()
|
||||
|
||||
def _get_valid_actions(self, ui_context):
|
||||
"""
|
||||
Gets the valid actions base on the ui context
|
||||
:param ui_context :type PoseWranglerUIContext: current selection state of the UI
|
||||
"""
|
||||
if not self._view:
|
||||
return
|
||||
# Find all the available actions
|
||||
found_actions = []
|
||||
# Iterate through all the modules currently loaded
|
||||
for module in list(sys.modules.values()):
|
||||
try:
|
||||
# Find all the classes in the module that inherit from BaseAction
|
||||
found_actions.extend(
|
||||
[obj for name, obj in inspect.getmembers(module, inspect.isclass) if
|
||||
issubclass(
|
||||
obj, base_action.BaseAction
|
||||
) and obj != base_action.BaseAction
|
||||
and obj not in found_actions]
|
||||
)
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
# Create the actions
|
||||
self._view.valid_actions.clear()
|
||||
self._view.valid_actions.extend([action(api=self) for action in found_actions])
|
||||
|
||||
def _load_extensions(self):
|
||||
"""
|
||||
Loads any core and custom extensions found in the sys.modules
|
||||
"""
|
||||
# Find all the available extensions
|
||||
found_extensions = []
|
||||
# Iterate through all the modules currently loaded
|
||||
for module in list(sys.modules.values()):
|
||||
try:
|
||||
# Find all the classes in the module that inherit from BaseAction
|
||||
found_extensions.extend(
|
||||
[obj for name, obj in inspect.getmembers(module, inspect.isclass) if
|
||||
issubclass(
|
||||
obj, base_extension.PoseWranglerExtension
|
||||
) and obj != base_extension.PoseWranglerExtension
|
||||
and obj not in found_extensions]
|
||||
)
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
# Create the extensions
|
||||
self._extensions = [extension(display_view=bool(self._view), api=self) for extension in found_extensions]
|
||||
|
||||
# If we are using the UI, show the actions
|
||||
if self._view:
|
||||
self._view.display_extensions(self._extensions)
|
||||
|
||||
def _load_view(self):
|
||||
"""
|
||||
Refresh the UI with the latest solvers
|
||||
"""
|
||||
if not self._view:
|
||||
return
|
||||
# If we have a ui, clear it before we load fresh data
|
||||
self._view.clear()
|
||||
# Bool to keep track if we have a solver that has edits to it
|
||||
existing_edit = None
|
||||
# Iterate through all the solvers in the scene
|
||||
for solver in self.rbf_solvers:
|
||||
# Get the solvers edit status
|
||||
edit = self.get_solver_edit_status(solver)
|
||||
# If we have an existing edit and this solver has also been edited
|
||||
if existing_edit is not None and edit:
|
||||
# Finish editing this solver
|
||||
self.edit_solver(edit=False, solver=solver)
|
||||
# Update the edit status accordingly
|
||||
edit = False
|
||||
# Otherwise if no existing edit and we have an edit
|
||||
elif edit:
|
||||
# Store the existing edit
|
||||
existing_edit = solver
|
||||
# Add the solver to the view with its edit status
|
||||
self._view.add_rbf_solver(solver, edit=edit)
|
||||
|
||||
def _set_current_solver(self, solver):
|
||||
"""
|
||||
UI Event to update the current solver
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
"""
|
||||
self.current_solver = solver
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def _setup_view_events(self):
|
||||
"""
|
||||
Connect up all of the ui signals to their corresponding functions
|
||||
"""
|
||||
# Only works if we have a view
|
||||
if not self._view:
|
||||
return
|
||||
# Connect up all the ui events
|
||||
# Solver Events
|
||||
self._view.event_create_solver.connect(self.create_rbf_solver)
|
||||
self._view.event_delete_solver.connect(self.delete_rbf_solver)
|
||||
self._view.event_edit_solver.connect(self.edit_solver)
|
||||
self._view.event_mirror_solver.connect(self.mirror_rbf_solver)
|
||||
self._view.event_refresh_solvers.connect(self._load_view)
|
||||
self._view.event_set_current_solver.connect(self._set_current_solver)
|
||||
# Driver Events
|
||||
self._view.event_add_drivers.connect(self.add_drivers)
|
||||
self._view.event_remove_drivers.connect(self.remove_drivers)
|
||||
self._view.event_export_drivers.connect(self.serialize_to_file)
|
||||
self._view.event_import_drivers.connect(self.deserialize_from_file)
|
||||
# Driven Events
|
||||
self._view.event_add_driven.connect(self.add_driven_transforms)
|
||||
self._view.event_remove_driven.connect(self.remove_driven)
|
||||
# Pose Events
|
||||
self._view.event_add_pose.connect(self.create_pose)
|
||||
self._view.event_delete_pose.connect(self.delete_pose)
|
||||
self._view.event_go_to_pose.connect(self.go_to_pose)
|
||||
self._view.event_mirror_pose.connect(self.mirror_pose)
|
||||
self._view.event_rename_pose.connect(self.rename_pose)
|
||||
self._view.event_update_pose.connect(self.update_pose)
|
||||
self._view.event_mute_pose.connect(self.mute_pose)
|
||||
# Blendshape Events
|
||||
self._view.event_create_blendshape.connect(self.create_blendshape)
|
||||
self._view.event_add_blendshape.connect(self.add_blendshape)
|
||||
self._view.event_edit_blendshape.connect(self.edit_blendshape)
|
||||
# Utility Events
|
||||
self._view.event_get_valid_actions.connect(self._get_valid_actions)
|
||||
self._view.event_select.connect(utils.set_selection)
|
||||
self._view.event_set_mirror_mapping.connect(self.set_mirror_mapping)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import ctypes
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
myappid = 'EpicGames.PoseWrangler'
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
tool = UERBFAPI(view=True)
|
||||
app.exec_()
|
@ -0,0 +1,5 @@
|
||||
from .actions import (
|
||||
selection,
|
||||
io,
|
||||
zero_pose
|
||||
)
|
71
Scripts/Animation/epic_pose_wrangler/v2/model/actions/io.py
Normal file
71
Scripts/Animation/epic_pose_wrangler/v2/model/actions/io.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from epic_pose_wrangler.v2.model import base_action
|
||||
|
||||
|
||||
class ExportSelectedAction(base_action.BaseAction):
|
||||
__display_name__ = "Export Selected Solvers"
|
||||
__tooltip__ = "Exports the currently selected solver nodes in the scene to a JSON file"
|
||||
__category__ = "IO"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, ui_context):
|
||||
return bool(ui_context.current_solvers)
|
||||
|
||||
def execute(self, ui_context=None, **kwargs):
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
if not ui_context:
|
||||
ui_context = self.api.get_ui_context()
|
||||
if not ui_context:
|
||||
return
|
||||
file_path = QtWidgets.QFileDialog.getSaveFileName(None, "Pose Wrangler File", "", "*.json")[0]
|
||||
# If no path is specified, exit early
|
||||
if file_path == "":
|
||||
return
|
||||
self.api.serialize_to_file(file_path, ui_context.current_solvers)
|
||||
|
||||
|
||||
class ExportAllAction(base_action.BaseAction):
|
||||
__display_name__ = "Export All Solvers"
|
||||
__tooltip__ = "Exports all solver nodes in the scene to a JSON file"
|
||||
__category__ = "IO"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, ui_context):
|
||||
return bool(ui_context.current_solvers)
|
||||
|
||||
def execute(self, ui_context=None, **kwargs):
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
if not ui_context:
|
||||
ui_context = self.api.get_ui_context()
|
||||
if not ui_context:
|
||||
return
|
||||
file_path = QtWidgets.QFileDialog.getSaveFileName(None, "Pose Wrangler File", "", "*.json")[0]
|
||||
# If no path is specified, exit early
|
||||
if file_path == "":
|
||||
return
|
||||
self.api.serialize_to_file(file_path, None)
|
||||
|
||||
|
||||
class ImportFromFileAction(base_action.BaseAction):
|
||||
__display_name__ = "Import Solvers"
|
||||
__tooltip__ = "Imports solver nodes into the scene from a JSON file"
|
||||
__category__ = "IO"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, ui_context):
|
||||
return bool(ui_context.current_solvers)
|
||||
|
||||
def execute(self, ui_context=None, **kwargs):
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
if not ui_context:
|
||||
ui_context = self.api.get_ui_context()
|
||||
if not ui_context:
|
||||
return
|
||||
file_path = QtWidgets.QFileDialog.getOpenFileName(None, "Pose Wrangler File", "", "*.json")[0]
|
||||
# If no path is specified, exit early
|
||||
if file_path == "":
|
||||
return
|
||||
self.api.deserialize_from_file(file_path)
|
@ -0,0 +1,55 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from maya import cmds
|
||||
|
||||
from epic_pose_wrangler.v2.model import base_action
|
||||
|
||||
|
||||
class SelectSolverAction(base_action.BaseAction):
|
||||
__display_name__ = "Select Solver Node(s)"
|
||||
__tooltip__ = "Selects the currently selected solver nodes in the scene"
|
||||
__category__ = "Select"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, ui_context):
|
||||
return bool(ui_context.current_solvers)
|
||||
|
||||
def execute(self, ui_context=None, **kwargs):
|
||||
if not ui_context:
|
||||
ui_context = self.api.get_ui_context()
|
||||
if not ui_context:
|
||||
return
|
||||
cmds.select(ui_context.current_solvers, replace=True)
|
||||
|
||||
|
||||
class SelectDriverAction(base_action.BaseAction):
|
||||
__display_name__ = "Select Driver Node(s)"
|
||||
__tooltip__ = "Selects the driver nodes in the scene"
|
||||
__category__ = "Select"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, ui_context):
|
||||
return bool(ui_context.drivers)
|
||||
|
||||
def execute(self, ui_context=None, **kwargs):
|
||||
if not ui_context:
|
||||
ui_context = self.api.get_ui_context()
|
||||
if not ui_context:
|
||||
return
|
||||
cmds.select(ui_context.drivers, replace=True)
|
||||
|
||||
|
||||
class SelectDrivenAction(base_action.BaseAction):
|
||||
__display_name__ = "Select Driven Node(s)"
|
||||
__tooltip__ = "Selects the driven nodes in the scene"
|
||||
__category__ = "Select"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, ui_context):
|
||||
return bool(ui_context.driven)
|
||||
|
||||
def execute(self, ui_context=None, **kwargs):
|
||||
if not ui_context:
|
||||
ui_context = self.api.get_ui_context()
|
||||
if not ui_context:
|
||||
return
|
||||
cmds.select(ui_context.driven, replace=True)
|
@ -0,0 +1,42 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from maya import cmds
|
||||
from epic_pose_wrangler.v2.model import base_action, pose_blender
|
||||
|
||||
|
||||
class ZeroDefaultPoseAction(base_action.BaseAction):
|
||||
__display_name__ = "Zero Default Pose Transforms"
|
||||
__tooltip__ = ""
|
||||
__category__ = "Utilities"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, ui_context):
|
||||
return bool(ui_context.current_solvers)
|
||||
|
||||
def execute(self, ui_context=None, solver=None, **kwargs):
|
||||
from PySide2 import QtWidgets
|
||||
if not ui_context:
|
||||
ui_context = self.api.get_ui_context()
|
||||
if ui_context:
|
||||
solver = self.api.get_rbf_solver_by_name(ui_context.current_solvers[-1])
|
||||
if not solver:
|
||||
solver = self.api.current_solver
|
||||
|
||||
# Go to the default pose
|
||||
solver.go_to_pose('default')
|
||||
# Assume edit is enabled
|
||||
edit = True
|
||||
if not self.api.get_solver_edit_status(solver):
|
||||
# If edit isn't enabled, store the current enabled state and enable editing
|
||||
edit = False
|
||||
self.api.edit_solver(edit=True, solver=solver)
|
||||
|
||||
# Reset the driven transforms
|
||||
for node in solver.driven_nodes(type=pose_blender.UEPoseBlenderNode.node_type):
|
||||
cmds.setAttr('{node}.translate'.format(node=node), 0.0, 0.0, 0.0)
|
||||
cmds.setAttr('{node}.rotate'.format(node=node), 0.0, 0.0, 0.0)
|
||||
cmds.setAttr('{node}.scale'.format(node=node), 1.0, 1.0, 1.0)
|
||||
|
||||
# Update the pose
|
||||
self.api.update_pose(pose_name='default', solver=solver)
|
||||
# Restore edit status
|
||||
self.api.edit_solver(edit=edit, solver=solver)
|
1965
Scripts/Animation/epic_pose_wrangler/v2/model/api.py
Normal file
1965
Scripts/Animation/epic_pose_wrangler/v2/model/api.py
Normal file
File diff suppressed because it is too large
Load Diff
24
Scripts/Animation/epic_pose_wrangler/v2/model/base_action.py
Normal file
24
Scripts/Animation/epic_pose_wrangler/v2/model/base_action.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
import abc
|
||||
|
||||
|
||||
class BaseAction(object):
|
||||
__display_name__ = "BaseAction"
|
||||
__tooltip__ = ""
|
||||
__category__ = ""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def validate(cls, ui_context):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(self, ui_context=None, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, api=None):
|
||||
self._api = api
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
return self._api
|
@ -0,0 +1,59 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from epic_pose_wrangler.model import exceptions
|
||||
|
||||
|
||||
class PoseWranglerExtension(object):
|
||||
"""
|
||||
Base class for extending pose wrangler with custom utilities that can be dynamically added to the UI
|
||||
"""
|
||||
__category__ = ""
|
||||
|
||||
def __init__(self, display_view=False, api=None):
|
||||
super(PoseWranglerExtension, self).__init__()
|
||||
self._display_view = display_view
|
||||
self._view = None
|
||||
self._api = api
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
"""
|
||||
Get the current API
|
||||
|
||||
:return: Reference to the main API interface
|
||||
:rtype: pose_wrangler.v2.main.UERBFAPI
|
||||
"""
|
||||
|
||||
return self._api
|
||||
|
||||
@property
|
||||
def view(self):
|
||||
"""
|
||||
Get the current view widget. This should be overridden by custom extensions if you wish to embed a UI for this
|
||||
extension into the main PoseWrangler UI.
|
||||
|
||||
:return: Reference to the PySide widget associated with this extension
|
||||
:rtype: QWidget or None
|
||||
"""
|
||||
return self._view
|
||||
|
||||
def execute(self, context=None, **kwargs):
|
||||
"""
|
||||
Generic entrypoint for executing the extension.
|
||||
|
||||
:param: context: pose wrangler context containing current solver and all solvers
|
||||
:type context: pose_wrangler.v2.model.context.PoseWranglerContext or None
|
||||
"""
|
||||
raise exceptions.PoseWranglerFunctionalityNotImplemented(
|
||||
"'execute' function has not been implemented for {class_name}".format(
|
||||
class_name=self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def on_context_changed(self, new_context):
|
||||
"""
|
||||
Context event called when the current solver is set via the API
|
||||
|
||||
:param new_context: pose wrangler context containing current solver and all solvers
|
||||
:type new_context: pose_wrangler.v2.model.context.PoseWranglerContext or None
|
||||
"""
|
||||
pass
|
13
Scripts/Animation/epic_pose_wrangler/v2/model/context.py
Normal file
13
Scripts/Animation/epic_pose_wrangler/v2/model/context.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
class PoseWranglerContext(object):
|
||||
def __init__(self, current_solver, solvers):
|
||||
self._current_solver = current_solver
|
||||
self._solvers = solvers
|
||||
|
||||
@property
|
||||
def current_solver(self):
|
||||
return self._current_solver
|
||||
|
||||
@property
|
||||
def solvers(self):
|
||||
return self._solvers
|
53
Scripts/Animation/epic_pose_wrangler/v2/model/exceptions.py
Normal file
53
Scripts/Animation/epic_pose_wrangler/v2/model/exceptions.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
"""
|
||||
API v2 Specific exceptions
|
||||
"""
|
||||
from epic_pose_wrangler.model import exceptions
|
||||
|
||||
|
||||
class MessageConnectionError(exceptions.PoseWranglerException):
|
||||
"""
|
||||
Raised when message connections fail.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidSolverError(exceptions.PoseWranglerException):
|
||||
"""
|
||||
Raised when the incorrect solver type is specified
|
||||
"""
|
||||
|
||||
|
||||
class InvalidNodeType(exceptions.PoseWranglerException, TypeError):
|
||||
"""
|
||||
Raised when the incorrect node type is specified
|
||||
"""
|
||||
|
||||
|
||||
class PoseWranglerAttributeError(exceptions.PoseWranglerException, AttributeError):
|
||||
"""
|
||||
Raised when there is an issue getting/setting an attribute
|
||||
"""
|
||||
|
||||
|
||||
class PoseBlenderPoseError(exceptions.PoseWranglerException):
|
||||
"""
|
||||
Generic error for issues with poses
|
||||
"""
|
||||
|
||||
|
||||
class InvalidPose(exceptions.PoseWranglerException):
|
||||
"""
|
||||
Generic error for incorrect poses
|
||||
"""
|
||||
|
||||
|
||||
class InvalidPoseIndex(exceptions.PoseWranglerException):
|
||||
"""
|
||||
Raised when issues arise surrounding the poses index
|
||||
"""
|
||||
|
||||
|
||||
class BlendshapeError(exceptions.PoseWranglerException):
|
||||
"""
|
||||
Generic error for blendshape issues
|
||||
"""
|
327
Scripts/Animation/epic_pose_wrangler/v2/model/export.py
Normal file
327
Scripts/Animation/epic_pose_wrangler/v2/model/export.py
Normal file
@ -0,0 +1,327 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
import os
|
||||
import json
|
||||
from maya import cmds
|
||||
|
||||
from special_projects.publish_tools.fbx_cmd import fbx_export
|
||||
from special_projects.publish_tools.utils import find_top_joints
|
||||
|
||||
from special_projects.rigging.rbf_node import RBFNode
|
||||
|
||||
|
||||
class RBFNodeExporter(object):
|
||||
"""
|
||||
Utility class to export a RBFsolver node, exports a JSON and FBX
|
||||
|
||||
>>> from special_projects.rigging.rbf_node.export import RBFNodeExporter
|
||||
>>> node = 'my_UE4RBFSolver_node'
|
||||
>>> asset_name = 'my_asset_name'
|
||||
>>> export_directory = 'my:/export/directory'
|
||||
>>> exporter = RBFNodeExporter(node, asset_name, export_directory)
|
||||
>>> exporter.export()
|
||||
|
||||
Result:
|
||||
export_directory/node.json
|
||||
export_directory/node.fbx
|
||||
"""
|
||||
|
||||
def __init__(self, node, asset_name, export_directory):
|
||||
"""
|
||||
Initialize RBF Exporter
|
||||
"""
|
||||
|
||||
self.set_node(node)
|
||||
self.set_asset_name(asset_name)
|
||||
self.set_export_directory(export_directory)
|
||||
self._set_fbx_export_path()
|
||||
self._set_json_export_path()
|
||||
|
||||
def node(self):
|
||||
return self._node
|
||||
|
||||
def set_node(self, node):
|
||||
|
||||
if not cmds.objectType(node, isAType=RBFNode.node_type):
|
||||
raise TypeError('Invalid "{}" node: "{}"'.format(RBFNode.node_type, node))
|
||||
|
||||
self._node = RBFNode(node)
|
||||
|
||||
def asset_name(self):
|
||||
return self._asset_name
|
||||
|
||||
def set_asset_name(self, asset_name):
|
||||
self._asset_name = asset_name
|
||||
|
||||
def export_directory(self):
|
||||
return self._export_directory
|
||||
|
||||
def set_export_directory(self, directory):
|
||||
if os.path.isdir(directory) and os.path.exists(directory):
|
||||
self._export_directory = directory
|
||||
else:
|
||||
raise IOError('Export directory "{}" does not exists'.format(directory))
|
||||
|
||||
def fbx_export_path(self):
|
||||
return self._fbx_export_path
|
||||
|
||||
def _set_fbx_export_path(self):
|
||||
self._fbx_export_path = os.path.join(self._export_directory, '{}.fbx'.format(self._node))
|
||||
|
||||
def json_export_path(self):
|
||||
return self._json_export_path
|
||||
|
||||
def _set_json_export_path(self):
|
||||
self._json_export_path = os.path.join(self._export_directory, '{}.json'.format(self._node))
|
||||
|
||||
# -------------------------------------------------------------------------------------
|
||||
|
||||
def export(self):
|
||||
"""
|
||||
Exports FBX and sidecar JSON file
|
||||
"""
|
||||
|
||||
self.fbx_export()
|
||||
self.json_export()
|
||||
|
||||
def json_export(self):
|
||||
"""
|
||||
Exports JSON sidecar
|
||||
"""
|
||||
|
||||
self.node().name_unnamed_poses()
|
||||
|
||||
config = self._node.data()
|
||||
config['export_fbx'] = self.fbx_export_path()
|
||||
config['asset_name'] = self.asset_name()
|
||||
|
||||
with open(self.json_export_path(), 'w') as outfile:
|
||||
json.dump(config, outfile, sort_keys=0, indent=4, separators=(",", ":"))
|
||||
|
||||
def fbx_export(self):
|
||||
"""
|
||||
Exports baked poses to FBX
|
||||
"""
|
||||
|
||||
self.node().name_unnamed_poses()
|
||||
|
||||
self.bake_poses()
|
||||
cmds.select(self.root_joint())
|
||||
fbx_export(self.fbx_export_path(),
|
||||
animation=True,
|
||||
bake_complex_animation=True,
|
||||
bake_complex_start=0,
|
||||
bake_complex_end=cmds.playbackOptions(q=True, maxTime=True),
|
||||
up_axis='z')
|
||||
|
||||
# -------------------------------------------------------------------------------------
|
||||
|
||||
def blendshape_nodes(self):
|
||||
"""
|
||||
Finds blendshape nodes from driven attributes
|
||||
"""
|
||||
|
||||
driven_attributes = self._node.driven_attributes(type='blendShape')
|
||||
|
||||
blendshape_nodes = list()
|
||||
for output in driven_attributes:
|
||||
for attribute in output:
|
||||
blendshape_nodes.append(attribute.split('.')[0])
|
||||
|
||||
return list(set(blendshape_nodes))
|
||||
|
||||
def meshes(self):
|
||||
"""
|
||||
Finds meshes from blendshape nodes
|
||||
"""
|
||||
|
||||
meshes = list()
|
||||
|
||||
blendShapes = self.blendshape_nodes()
|
||||
if blendShapes:
|
||||
for blendShape in blendShapes:
|
||||
meshes.extend(cmds.deformer(blendShape, q=True, geometry=True))
|
||||
|
||||
meshes = list(set(meshes))
|
||||
meshes = cmds.listRelatives(meshes, parent=True)
|
||||
|
||||
return meshes
|
||||
|
||||
def root_joint(self):
|
||||
"""
|
||||
Finds root joint from meshes
|
||||
"""
|
||||
|
||||
meshes = self.meshes()
|
||||
if meshes:
|
||||
skeleton = list()
|
||||
for mesh in meshes:
|
||||
skin_cluster = cmds.ls(cmds.findDeformers(mesh), type='skinCluster')
|
||||
if skin_cluster:
|
||||
skin_cluster = skin_cluster[0]
|
||||
influences = cmds.skinCluster(skin_cluster, q=True, inf=True)
|
||||
if influences:
|
||||
skeleton.extend(influences)
|
||||
else:
|
||||
raise RuntimeError('No influences found for "{}"'.format(skin_cluster))
|
||||
else:
|
||||
cmds.warning('No skinCluster found for "{}"'.format(mesh))
|
||||
|
||||
root_joints = find_top_joints(skeleton)
|
||||
|
||||
else:
|
||||
skeleton = self._node.drivers()
|
||||
# for driver in self._node.drivers():
|
||||
# driven = driver.replace('_drv', '')
|
||||
# if cmds.objExists(driven):
|
||||
# skeleton.append(driven)
|
||||
|
||||
root_joints = find_top_joints(skeleton)
|
||||
|
||||
if not root_joints:
|
||||
raise RuntimeError('No root joint found for "{}"'.format(self._node))
|
||||
|
||||
return root_joints[0]
|
||||
|
||||
def add_root_attributes(self, root_joint):
|
||||
"""
|
||||
Adds RBFNode driven attributes to root_joint
|
||||
"""
|
||||
|
||||
pose_root_attributes = dict()
|
||||
|
||||
poses = self._node.poses()
|
||||
for pose in poses:
|
||||
driven_attributes = self._node.pose_driven_attributes(pose) # add type flag!
|
||||
current_pose = list()
|
||||
for attribute in driven_attributes:
|
||||
node, target = attribute.split('.')
|
||||
root_attribute = '{}.{}'.format(root_joint, target)
|
||||
if root_attribute not in current_pose:
|
||||
if not cmds.objExists(root_attribute):
|
||||
cmds.addAttr(root_joint, ln=target, at='double', k=True)
|
||||
else:
|
||||
input_connection = cmds.listConnections(root_attribute, s=True, d=False, plugs=True)
|
||||
if input_connection:
|
||||
cmds.disconnectAttr(input_connection[0], root_attribute)
|
||||
|
||||
# cmds.connectAttr(attribute, root_attribute)
|
||||
current_pose.append(root_attribute)
|
||||
|
||||
pose_root_attributes[pose] = current_pose
|
||||
|
||||
return pose_root_attributes
|
||||
|
||||
def bake_poses(self):
|
||||
"""
|
||||
Bakes the RBFNode poses in the timeline for FBX export
|
||||
"""
|
||||
|
||||
for anim_curve_type in ['animCurveTL', 'animCurveTA', 'animCurveTU']:
|
||||
cmds.delete(cmds.ls(type=anim_curve_type))
|
||||
|
||||
pose_root_attributes = self.add_root_attributes(self.root_joint())
|
||||
|
||||
for frame, pose in enumerate(self._node.poses()):
|
||||
|
||||
# go to pose
|
||||
self._node.go_to_pose(pose)
|
||||
|
||||
# key controllers
|
||||
if self._node.num_controllers():
|
||||
for controller in self._node.controllers():
|
||||
cmds.setKeyframe(controller, t=frame, inTangentType='linear', outTangentType='step')
|
||||
|
||||
# or key drivers
|
||||
else:
|
||||
for driver in self._node.drivers():
|
||||
cmds.setKeyframe(driver, t=frame, inTangentType='linear', outTangentType='step')
|
||||
|
||||
root_attributes = pose_root_attributes.get(pose, [])
|
||||
|
||||
for root_attribute in root_attributes:
|
||||
|
||||
input_connection = cmds.listConnections(root_attribute, s=True, d=False, plugs=True)
|
||||
if input_connection:
|
||||
cmds.disconnectAttr(input_connection[0], root_attribute)
|
||||
|
||||
# Key Driven Before/After
|
||||
cmds.setAttr(root_attribute, 0)
|
||||
|
||||
if frame == len(self._node.poses()) - 1:
|
||||
cmds.setKeyframe(root_attribute, t=(frame - 1), inTangentType='linear', outTangentType='linear')
|
||||
else:
|
||||
cmds.setKeyframe(root_attribute, t=((frame - 1), (frame + 1)), inTangentType='linear',
|
||||
outTangentType='linear')
|
||||
|
||||
# Key Driven
|
||||
cmds.setAttr(root_attribute, 1)
|
||||
cmds.setKeyframe(root_attribute, t=frame, inTangentType='linear', outTangentType='linear')
|
||||
|
||||
# set start-end frames
|
||||
end_frame = len(self._node.poses()) - 1
|
||||
cmds.playbackOptions(minTime=0, maxTime=end_frame, animationStartTime=0, animationEndTime=end_frame)
|
||||
cmds.dgdirty(a=True)
|
||||
|
||||
|
||||
class RBFPoseExporterBatch(object):
|
||||
"""
|
||||
Utility class to export multiple RBFsolver nodes, exports a JSON and FBX for each solver
|
||||
|
||||
>>> solvers = cmds.ls(type='UE4RBFSolverNode')
|
||||
>>> rig_scene = r'D:\Build\UE5_Main\Collaboration\Frosty\ArtSource\Character\Hero\Kenny\Rig\Kenny_Rig.ma'
|
||||
>>> export_directory = r'D:\test\class'
|
||||
>>> asset_name = 'Kenny'
|
||||
>>> rbf_exporter_batch = RBFPoseExporterBatch(solvers, asset_name, export_directory, rig_scene)
|
||||
|
||||
Result:
|
||||
export_directory/node1.json
|
||||
export_directory/node1.fbx
|
||||
export_directory/node2.json
|
||||
export_directory/node2.fbx
|
||||
"""
|
||||
|
||||
def __init__(self, nodes, asset_name, export_directory, rig_scene):
|
||||
self.set_pose_exporter(nodes, asset_name, export_directory)
|
||||
self.set_rig_scene(rig_scene)
|
||||
|
||||
def pose_exporter(self):
|
||||
return self._poseExporter
|
||||
|
||||
def asset_name(self):
|
||||
return self._asset_name
|
||||
|
||||
def export_directory(self):
|
||||
return self._export_directory
|
||||
|
||||
def set_pose_exporter(self, nodes, asset_name, export_directory):
|
||||
if not hasattr(nodes, '__iter__'):
|
||||
nodes = [nodes]
|
||||
|
||||
exporters = list()
|
||||
for node in nodes:
|
||||
if cmds.objectType(node, isAType=RBFNodeExporter.node_type):
|
||||
exporters.append(RBFNodeExporter(node, asset_name, export_directory))
|
||||
|
||||
if not len(exporters):
|
||||
raise RuntimeError('No valid {} objects found'.format(RBFNodeExporter.node_type))
|
||||
|
||||
self._poseExporter = exporters
|
||||
self._asset_name = asset_name
|
||||
self._export_directory = export_directory
|
||||
|
||||
def rig_scene(self):
|
||||
return self._rig_scene
|
||||
|
||||
def set_rig_scene(self, rig_scene):
|
||||
if not os.path.exists(rig_scene):
|
||||
raise IOError('Rig scene "{}" does not exists'.format(rig_scene))
|
||||
|
||||
self._rig_scene = rig_scene
|
||||
|
||||
# -------------------------------------------------------------------------------------
|
||||
|
||||
def export(self, run_in_subprocess=True):
|
||||
# TO-DO: Implement run_in_subprocess
|
||||
for exporter in self.pose_exporter():
|
||||
cmds.file(self.rig_scene(), open=True, force=True)
|
||||
exporter.export()
|
502
Scripts/Animation/epic_pose_wrangler/v2/model/pose_blender.py
Normal file
502
Scripts/Animation/epic_pose_wrangler/v2/model/pose_blender.py
Normal file
@ -0,0 +1,502 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import copy
|
||||
|
||||
from maya import cmds
|
||||
from maya.api import OpenMaya as om
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.v2.model import exceptions, utils
|
||||
|
||||
|
||||
class UEPoseBlenderNode(object):
|
||||
"""
|
||||
Class wrapper for UEPoseBlenderNode
|
||||
"""
|
||||
node_type = 'UEPoseBlenderNode'
|
||||
|
||||
@classmethod
|
||||
def create(cls, driven_transform=None):
|
||||
"""
|
||||
Create Pose Blender Node
|
||||
>>> new_node = UEPoseBlenderNode.create()
|
||||
"""
|
||||
name = driven_transform
|
||||
# If no name is specified, generate unique name
|
||||
if name is None:
|
||||
name = '{name}#'.format(name=cls.node_type)
|
||||
name += "_{class_name}".format(class_name=cls.__name__)
|
||||
# Create node
|
||||
node = cmds.createNode(cls.node_type, name=name)
|
||||
node_ref = cls(node)
|
||||
# If a driving transform is specified, connect it via a message attribute and set the base pose
|
||||
if driven_transform is not None:
|
||||
utils.message_connect(
|
||||
from_attribute="{node}.drivenTransform".format(node=node),
|
||||
to_attribute="{transform}.poseBlender".format(transform=driven_transform)
|
||||
)
|
||||
# Set the base pose
|
||||
node_ref.base_pose = cmds.xform(driven_transform, query=True, objectSpace=True, matrix=True)
|
||||
# Return the new UEPoseBlenderNode reference
|
||||
return node_ref
|
||||
|
||||
@classmethod
|
||||
def find_all(cls):
|
||||
"""
|
||||
Returns all PoseBlender nodes in scene
|
||||
"""
|
||||
return [cls(node) for node in cmds.ls(type=cls.node_type)]
|
||||
|
||||
@classmethod
|
||||
def find_by_name(cls, name):
|
||||
"""
|
||||
Find a UEPoseBlenderNode class with the specified name
|
||||
:param name :type str: name of the node
|
||||
:return :type UEPoseBlenderNode or None:
|
||||
"""
|
||||
if not name.endswith(cls.node_type):
|
||||
name += "_{node_type}".format(node_type=cls.node_type)
|
||||
match = cmds.ls(name, type=cls.node_type)
|
||||
return cls(match[0]) if match else None
|
||||
|
||||
@classmethod
|
||||
def find_by_transform(cls, transform):
|
||||
"""
|
||||
Finds a UEPoseBlenderNode class connected to the specified joint
|
||||
:param transform :type str: name of the transform
|
||||
:return :type UEPoseBlenderNode or None
|
||||
"""
|
||||
if not cmds.attributeQuery('poseBlender', node=transform, exists=True):
|
||||
return
|
||||
connections = cmds.listConnections('{transform}.poseBlender'.format(transform=transform))
|
||||
if not connections:
|
||||
return
|
||||
return cls(connections[0])
|
||||
|
||||
def __init__(self, node):
|
||||
# If the node doesn't exist, raise an exception
|
||||
if not cmds.objectType(node, isAType=self.node_type):
|
||||
raise exceptions.InvalidNodeType(
|
||||
'Invalid "{node_type}" node: "{node_name}"'.format(
|
||||
node_type=self.node_type,
|
||||
node_name=node
|
||||
)
|
||||
)
|
||||
# Store a reference ot the MObject in case the name of the node is changed whilst this class is still in use
|
||||
self._node = om.MFnDependencyNode(om.MGlobal.getSelectionListByName(node).getDependNode(0))
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns class string representation
|
||||
"""
|
||||
return '<{node_type}>: {object}'.format(node_type=self.node_type, object=self)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Returns class as string
|
||||
"""
|
||||
return str(self.node)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Returns if two objects are the same, allows for comparing two different UEPoseBlenderNode references that wrap
|
||||
the same MObject
|
||||
"""
|
||||
return str(self) == str(other)
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Override the __enter__ to allow for this class to be used as a context manager to toggle edit mode
|
||||
>>> pose_blender = UEPoseBlenderNode('TestPoseBlenderNode')
|
||||
>>> with pose_blender:
|
||||
>>> # Edit mode enabled so changes can be made
|
||||
>>> pass
|
||||
>>> # Edit mode is now disabled
|
||||
"""
|
||||
self.edit = True
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
On exit, disable edit mode
|
||||
"""
|
||||
self.edit = not self.edit
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
return self._node.name()
|
||||
|
||||
@property
|
||||
def rbf_solver_attr(self):
|
||||
return '{node}.rbfSolver'.format(node=self.node)
|
||||
|
||||
@property
|
||||
def base_pose_attr(self):
|
||||
return '{node}.basePose'.format(node=self.node)
|
||||
|
||||
@property
|
||||
def envelope_attr(self):
|
||||
return '{node}.envelope'.format(node=self.node)
|
||||
|
||||
@property
|
||||
def in_matrix_attr(self):
|
||||
return '{node}.inMatrix'.format(node=self.node)
|
||||
|
||||
@property
|
||||
def out_matrix_attr(self):
|
||||
return '{node}.outMatrix'.format(node=self.node)
|
||||
|
||||
@property
|
||||
def poses_attr(self):
|
||||
return '{node}.poses'.format(node=self.node)
|
||||
|
||||
@property
|
||||
def weights_attr(self):
|
||||
return '{node}.weights'.format(node=self.node)
|
||||
|
||||
@property
|
||||
def driven_transform(self):
|
||||
"""
|
||||
Get the current driven transform
|
||||
:return :type str: transform node name
|
||||
"""
|
||||
if cmds.attributeQuery('drivenTransform', node=self.node, exists=True):
|
||||
transforms = cmds.listConnections("{node}.drivenTransform".format(node=self.node))
|
||||
if transforms:
|
||||
return transforms[0]
|
||||
|
||||
@property
|
||||
def edit(self):
|
||||
"""
|
||||
:return :type bool: are the driven transforms connected to this node editable?
|
||||
"""
|
||||
transform = self.driven_transform
|
||||
# If no transform, we can edit!
|
||||
if not transform:
|
||||
return True
|
||||
# False if we have any matrix connections, otherwise we have no connections and can edit
|
||||
return False if cmds.listConnections(self.out_matrix_attr) else True
|
||||
|
||||
@edit.setter
|
||||
def edit(self, value):
|
||||
"""
|
||||
Allow the driven transforms to be edited (or not)
|
||||
:param value :type bool: edit mode enabled True/False
|
||||
"""
|
||||
# If we don't have a driven transform we can't do anything
|
||||
transform = self.driven_transform
|
||||
if not transform:
|
||||
return
|
||||
# If we are in edit mode, disable out connections
|
||||
if bool(value):
|
||||
self.out_matrix = None
|
||||
# Otherwise connect up the transform
|
||||
else:
|
||||
self.out_matrix = transform
|
||||
|
||||
@property
|
||||
def rbf_solver(self):
|
||||
"""
|
||||
:return:The connected rbf solver node if it exists
|
||||
"""
|
||||
from epic_pose_wrangler.v2.model import api
|
||||
# Check the attr exists
|
||||
exists = cmds.attributeQuery(self.rbf_solver_attr.split('.')[-1], node=self.node, exists=True)
|
||||
# Query the connections
|
||||
connections = cmds.listConnections(self.rbf_solver_attr)
|
||||
# Return the node if the attr exists and a connection is found
|
||||
return api.RBFNode(connections[0]) if exists and connections else None
|
||||
|
||||
@rbf_solver.setter
|
||||
def rbf_solver(self, rbf_solver_attr):
|
||||
"""
|
||||
Connects up this node to an rbf solver node
|
||||
:param rbf_solver_attr: the attribute on the rbf solver to connect this to i.e my_rbf_solver.poseBlend_back_120
|
||||
"""
|
||||
utils.message_connect(rbf_solver_attr, self.rbf_solver_attr)
|
||||
|
||||
@property
|
||||
def base_pose(self):
|
||||
"""
|
||||
:return :type list: matrix
|
||||
"""
|
||||
return self.get_pose(index=0)
|
||||
|
||||
@base_pose.setter
|
||||
def base_pose(self, matrix):
|
||||
"""
|
||||
Set the base pose to a specific pose.PoseNode
|
||||
:param matrix :type list: Matrix to set as base pose
|
||||
"""
|
||||
# Connect up the pose's outputLocalMatrix to the basePose plug
|
||||
utils.set_attr_or_connect(source_attr_name=self.base_pose_attr, value=matrix, attr_type='matrix')
|
||||
# Set the inMatrix to the basePose plug
|
||||
self.in_matrix = matrix
|
||||
# Set the first pose in the pose list to the new base pose
|
||||
self.set_pose(index=0, overwrite=True, matrix=matrix)
|
||||
|
||||
@property
|
||||
def envelope(self):
|
||||
"""
|
||||
Get the value of the envelope attr from this node
|
||||
"""
|
||||
return utils.get_attr(self.envelope_attr, as_value=True)
|
||||
|
||||
@envelope.setter
|
||||
def envelope(self, value):
|
||||
"""
|
||||
Sets the envelope
|
||||
:param value: float, int or string (i.e node.attributeName). Float/Int will set the value, whilst
|
||||
passing an attribute will connect the plugs
|
||||
"""
|
||||
if isinstance(value, float) or isinstance(value, int) and 0 > value > 1:
|
||||
value = min(max(0.0, value), 1.0)
|
||||
utils.set_attr_or_connect(source_attr_name=self.envelope_attr, value=value)
|
||||
|
||||
@property
|
||||
def in_matrix(self):
|
||||
"""
|
||||
Get the inMatrix value
|
||||
:return: matrix or attributeName
|
||||
"""
|
||||
# Get the current value of the attribute
|
||||
return utils.get_attr(self.in_matrix_attr, as_value=True)
|
||||
|
||||
@in_matrix.setter
|
||||
def in_matrix(self, value):
|
||||
"""
|
||||
Sets the inMatrix attribute to a value or connects it to another matrix plug
|
||||
:param value: matrix (list) or string node.attributeName
|
||||
"""
|
||||
utils.set_attr_or_connect(source_attr_name=self.in_matrix_attr, value=value, attr_type='matrix')
|
||||
|
||||
@property
|
||||
def out_matrix(self):
|
||||
"""
|
||||
:return:
|
||||
"""
|
||||
return cmds.getAttr(self.out_matrix_attr)
|
||||
|
||||
@out_matrix.setter
|
||||
def out_matrix(self, transform_name=None):
|
||||
"""
|
||||
Connect the output matrix up to a given transform node
|
||||
:param transform_name: name of a transform node to connect to
|
||||
"""
|
||||
# If a transform is specified, connect it
|
||||
current_selection = cmds.ls(selection=True)
|
||||
if transform_name:
|
||||
# Check that the node type is a transform
|
||||
if not cmds.ls(transform_name, type='transform'):
|
||||
# If its not a transform raise an error
|
||||
raise exceptions.InvalidNodeType(
|
||||
"Invalid node type. Expected 'transform', received '{node_type}'".format(
|
||||
node_type=cmds.objectType(transform_name)
|
||||
)
|
||||
)
|
||||
# Create a new decomposeMatrix node to convert the outMatrix to T,R,S
|
||||
mx_decompose_node = cmds.createNode('decomposeMatrix', name="{node}_mx_decompose#".format(node=self.node))
|
||||
# Connect the outMatrix up to the decomposeMatrix's input
|
||||
cmds.connectAttr(self.out_matrix_attr, '{node}.inputMatrix'.format(node=mx_decompose_node))
|
||||
# Create an iterator with TRS attributes
|
||||
attributes = ('translate', 'rotate', 'scale')
|
||||
# Iterate through each attr and connect it
|
||||
for attr in attributes:
|
||||
out_attr = '{mx_decompose_node}.output{attr}'.format(
|
||||
mx_decompose_node=mx_decompose_node,
|
||||
attr=attr.capitalize()
|
||||
)
|
||||
in_attr = '{node_name}.{attr}'.format(node_name=transform_name, attr=attr)
|
||||
cmds.connectAttr(out_attr, in_attr, force=True)
|
||||
else:
|
||||
# No transform was specified, disconnect all existing connections
|
||||
connections = cmds.listConnections(self.out_matrix_attr, type='decomposeMatrix')
|
||||
current_matrix = cmds.xform(self.driven_transform, query=True, matrix=True, objectSpace=True)
|
||||
if connections:
|
||||
cmds.delete(connections)
|
||||
for connection in cmds.listConnections(self.out_matrix_attr, plugs=True) or []:
|
||||
cmds.disconnectAttr(self.out_matrix_attr, connection)
|
||||
cmds.xform(self.driven_transform, matrix=current_matrix, objectSpace=True)
|
||||
cmds.select(current_selection, replace=True)
|
||||
|
||||
def get_pose(self, index=-1):
|
||||
"""
|
||||
Get the pose at the specified index
|
||||
:param index :type int: index to query
|
||||
:return :type list matrix or None
|
||||
"""
|
||||
# Get the value of the pose_attr at the specified index
|
||||
return utils.get_attr(
|
||||
'{poses_attr}[{index}]'.format(poses_attr=self.poses_attr, index=index),
|
||||
as_value=True
|
||||
)
|
||||
|
||||
def get_poses(self):
|
||||
"""
|
||||
:return :type list of matrices
|
||||
"""
|
||||
# TODO update this when the weight + name get added
|
||||
return utils.get_attr_array(attr_name=self.poses_attr, as_value=True)
|
||||
|
||||
def add_pose_from_current(self, pose_name, index=-1):
|
||||
"""
|
||||
Add a pose from the current transforms positions
|
||||
:param pose_name :type str: name of the pose
|
||||
:param index :type int: target index for the pose
|
||||
"""
|
||||
# Find the next index if no index is specified
|
||||
if index < 0:
|
||||
# If the index is less than 0 find the next available index
|
||||
index = len(cmds.getAttr(self.poses_attr, multiIndices=True) or [0]) - 1
|
||||
# Store driven transform in var to reduce number of cmds calls
|
||||
driven_transform = self.driven_transform
|
||||
if not driven_transform:
|
||||
raise exceptions.PoseBlenderPoseError(
|
||||
"No driven transform associated with this node, "
|
||||
"unable to get the current matrix"
|
||||
)
|
||||
# Get the local matrix for the driven transform
|
||||
local_matrix = cmds.xform(driven_transform, query=True, objectSpace=True, matrix=True)
|
||||
# Set the pose
|
||||
self.set_pose(index=index, pose_name=pose_name, matrix=local_matrix)
|
||||
|
||||
def set_pose(self, index=-1, pose_name="", overwrite=True, matrix=None):
|
||||
"""
|
||||
Set a pose for the specified index
|
||||
:param index: int value of the index to set
|
||||
:param pose_name: name of the pose as a string
|
||||
:param overwrite: should it overwrite any existing pose at the index or insert
|
||||
:param matrix: matrix value to set
|
||||
"""
|
||||
# Find the next index if no index is specified
|
||||
if index < 0:
|
||||
if pose_name:
|
||||
LOG.warning("Set Pose has not been implemented to support a pose name. FIXME")
|
||||
return
|
||||
else:
|
||||
# If the index is less than 0 find the next available index
|
||||
index = len(cmds.getAttr(self.poses_attr, multiIndices=True) or [0]) - 1
|
||||
|
||||
# If no matrix is specified, grab the matrix for the driven transform
|
||||
if matrix is None:
|
||||
matrix = cmds.xform(self.driven_transform, query=True, matrix=True, objectSpace=True)
|
||||
|
||||
# TODO add in support for inserting into the pose list
|
||||
if not overwrite:
|
||||
pose_count = len(cmds.getAttr(self.poses_attr, multiIndices=True) or [])
|
||||
if pose_count - 1 > index:
|
||||
pass
|
||||
|
||||
# Generate the source attribute (the attribute on this node) from the default attr and given index
|
||||
source_attr_name = '{poses_attr}[{index}]'.format(poses_attr=self.poses_attr, index=index)
|
||||
# Set the pose
|
||||
utils.set_attr_or_connect(source_attr_name=source_attr_name, value=matrix, attr_type='matrix')
|
||||
|
||||
def go_to_pose(self, index=-1):
|
||||
"""
|
||||
Move the driven transform to the matrix stored in the pose list at the specified index
|
||||
:param index :type index: index to go to
|
||||
"""
|
||||
# Get the matrix at the specified index
|
||||
pose_matrix = self.get_pose(index=index)
|
||||
# Assume the position
|
||||
cmds.xform(self.driven_transform, matrix=pose_matrix)
|
||||
|
||||
def delete_pose(self, index=-1, pose_name=""):
|
||||
"""
|
||||
Delete a pose at the specified index or with the given name
|
||||
:param index :type int: index to delete or -1 to use pose_name
|
||||
:param pose_name :type str: pose name to delete
|
||||
"""
|
||||
# TODO Delete pose will need to remember enabled state + reconnect up pose_name connection when the changes are
|
||||
# made to the solver. Currently can't delete by pose name because there is nothing tying an index to a pose
|
||||
# name
|
||||
# If no index and no pose name, raise exception
|
||||
if index < 0 and not pose_name:
|
||||
raise exceptions.InvalidPoseIndex("Unable to delete pose")
|
||||
|
||||
# Copy a list of the poses
|
||||
poses = copy.deepcopy(self.get_poses())
|
||||
|
||||
# Iterate through the existing poses in reverse
|
||||
for i in reversed(range(len(poses))):
|
||||
# Remove the pose from the list of targets
|
||||
cmds.removeMultiInstance('{blender}.poses[{index}]'.format(blender=self, index=i), b=True)
|
||||
|
||||
# Remove the pose at the given index
|
||||
poses.pop(index)
|
||||
|
||||
# Re-add all the deleted poses (minus the one we popped)
|
||||
for pose_index, matrix in enumerate(poses):
|
||||
self.set_pose(index=pose_index, matrix=matrix)
|
||||
|
||||
def get_weight(self, index, as_float=True):
|
||||
"""
|
||||
Get the current weight for the specified index
|
||||
:param index :type int
|
||||
:param as_float :type bool: return as float or as attribute name of the connected plug
|
||||
:return: float or plug
|
||||
"""
|
||||
return utils.get_attr('{weights}[{index}]'.format(weights=self.weights_attr, index=index), as_value=as_float)
|
||||
|
||||
def get_weights(self, as_float=True):
|
||||
"""
|
||||
Get all the weights associated with this node
|
||||
:param as_float :type bool: as list of floats or list of plugs
|
||||
:return: list of floats or plugs
|
||||
"""
|
||||
return utils.get_attr_array(attr_name=self.weights_attr, as_value=as_float)
|
||||
|
||||
def set_weight(self, index=-1, in_float_attr="", float_value=0.0):
|
||||
"""
|
||||
Set the weight at a given index either by connecting an attribute or specifying a float value
|
||||
:param index :type int: index to set
|
||||
:param in_float_attr :type str: node.attributeName to connect to this plug
|
||||
:param float_value :type float: float value to set
|
||||
"""
|
||||
if index < 0:
|
||||
# If the index is less than 0 find the next available index
|
||||
index = len(cmds.getAttr(self.weights_attr, multiIndices=True) or [0]) - 1
|
||||
# Generate the correct source attr name for the index specified
|
||||
source_attr_name = '{weights}[{index}]'.format(weights=self.weights_attr, index=index)
|
||||
# Set the array element to either the attr or the float value
|
||||
utils.set_attr_or_connect(source_attr_name=source_attr_name, value=in_float_attr or float_value)
|
||||
|
||||
def set_weights(self, in_float_array_attr="", floats=None):
|
||||
"""
|
||||
Set multiple weights either by an attribute array string or a float list.Will overwrite existing values
|
||||
:param in_float_array_attr :type str: node.attributeName array attribute to connect all indices with
|
||||
:param floats :type list: list of float values to set for each corresponding index
|
||||
"""
|
||||
# Prioritize plugs over setting floats
|
||||
if in_float_array_attr:
|
||||
# Iterate through all the indices in the array
|
||||
for i in range(len(cmds.getAttr(in_float_array_attr, multiIndices=True) or [0])):
|
||||
# Generate the source attr name
|
||||
in_float_attr = "{array_attr}[{index}]".format(array_attr=in_float_array_attr, index=i)
|
||||
# Set the weight for the current index
|
||||
self.set_weight(index=i, in_float_attr=in_float_attr)
|
||||
elif floats:
|
||||
# Iterate through the floats
|
||||
for index, float_value in enumerate(floats):
|
||||
# Set the weight for the current index
|
||||
self.set_weight(index=index, float_value=float_value)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete the node associated with this wrapper
|
||||
"""
|
||||
# If we aren't in edit mode we still have connections made to decomposeMatrix nodes that we want to delete
|
||||
if not self.edit:
|
||||
# Enable edit mode to delete those decomposeMatrix nodes
|
||||
self.edit = True
|
||||
# Disconnect all attrs
|
||||
destination_conns = cmds.listConnections(self, plugs=True, connections=True, source=False) or []
|
||||
for i in range(0, len(destination_conns), 2):
|
||||
cmds.disconnectAttr(destination_conns[i], destination_conns[i + 1])
|
||||
source_conns = cmds.listConnections(self, plugs=True, connections=True, destination=False) or []
|
||||
|
||||
for i in range(0, len(source_conns), 2):
|
||||
# we have to flip these because the output is always node centric and not connection centric
|
||||
cmds.disconnectAttr(source_conns[i + 1], source_conns[i])
|
||||
# Delete the node
|
||||
cmds.delete(self.node)
|
311
Scripts/Animation/epic_pose_wrangler/v2/model/utils.py
Normal file
311
Scripts/Animation/epic_pose_wrangler/v2/model/utils.py
Normal file
@ -0,0 +1,311 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
import math
|
||||
import traceback
|
||||
|
||||
from maya import OpenMaya, cmds
|
||||
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.v2.model import exceptions
|
||||
|
||||
# NOTE: MTransformationMatrix & MEulerRotation have different values for the same axis.
|
||||
XFORM_ROTATION_ORDER = {
|
||||
'xyz': OpenMaya.MTransformationMatrix.kXYZ,
|
||||
'yzx': OpenMaya.MTransformationMatrix.kYZX,
|
||||
'zxy': OpenMaya.MTransformationMatrix.kZXY,
|
||||
'xzy': OpenMaya.MTransformationMatrix.kXZY,
|
||||
'yxz': OpenMaya.MTransformationMatrix.kYXZ,
|
||||
'zyx': OpenMaya.MTransformationMatrix.kZYX
|
||||
}
|
||||
|
||||
EULER_ROTATION_ORDER = {
|
||||
'xyz': OpenMaya.MEulerRotation.kXYZ,
|
||||
'yzx': OpenMaya.MEulerRotation.kYZX,
|
||||
'zxy': OpenMaya.MEulerRotation.kZXY,
|
||||
'xzy': OpenMaya.MEulerRotation.kXZY,
|
||||
'yxz': OpenMaya.MEulerRotation.kYXZ,
|
||||
'zyx': OpenMaya.MEulerRotation.kZYX
|
||||
}
|
||||
|
||||
|
||||
def compose_matrix(position, rotation, rotation_order='xyz'):
|
||||
"""
|
||||
Compose a 4x4 matrix with given transformation.
|
||||
|
||||
>>> compose_matrix((0.0, 0.0, 0.0), (90.0, 0.0, 0.0))
|
||||
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]
|
||||
"""
|
||||
|
||||
# create rotation ptr
|
||||
rot_script_util = OpenMaya.MScriptUtil()
|
||||
rot_script_util.createFromDouble(*[deg * math.pi / 180.0 for deg in rotation])
|
||||
rot_double_ptr = rot_script_util.asDoublePtr()
|
||||
|
||||
# construct transformation matrix
|
||||
xform_matrix = OpenMaya.MTransformationMatrix()
|
||||
xform_matrix.setTranslation(OpenMaya.MVector(*position), OpenMaya.MSpace.kTransform)
|
||||
xform_matrix.setRotation(rot_double_ptr, XFORM_ROTATION_ORDER[rotation_order], OpenMaya.MSpace.kTransform)
|
||||
|
||||
matrix = xform_matrix.asMatrix()
|
||||
return [matrix(m, n) for m in range(4) for n in range(4)]
|
||||
|
||||
|
||||
def decompose_matrix(matrix, rotation_order='xyz'):
|
||||
"""
|
||||
Decomposes a 4x4 matrix into translation and rotation.
|
||||
|
||||
>>> decompose_matrix([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0])
|
||||
((0.0, 0.0, 0.0), (90.0, 0.0, 0.0))
|
||||
"""
|
||||
|
||||
mmatrix = OpenMaya.MMatrix()
|
||||
OpenMaya.MScriptUtil.createMatrixFromList(matrix, mmatrix)
|
||||
|
||||
# create transformation matrix
|
||||
xform_matrix = OpenMaya.MTransformationMatrix(mmatrix)
|
||||
|
||||
# get translation
|
||||
translation = xform_matrix.getTranslation(OpenMaya.MSpace.kTransform)
|
||||
|
||||
# get rotation
|
||||
# @ref: https://github.com/LumaPictures/pymel/blob/master/pymel/core/datatypes.py
|
||||
# The apicls getRotation needs a "RotationOrder &" object, which is impossible to make in python...
|
||||
euler_rotation = xform_matrix.eulerRotation()
|
||||
euler_rotation.reorderIt(EULER_ROTATION_ORDER[rotation_order])
|
||||
rotation = euler_rotation.asVector()
|
||||
|
||||
return (
|
||||
(translation.x, translation.y, translation.z),
|
||||
(rotation.x * 180.0 / math.pi, rotation.y * 180.0 / math.pi, rotation.z * 180.0 / math.pi)
|
||||
)
|
||||
|
||||
|
||||
def euler_to_quaternion(rotation, rotation_order='xyz'):
|
||||
"""
|
||||
Returns Euler Rotation as Quaternion
|
||||
|
||||
>>> euler_to_quaternion((90, 0, 0))
|
||||
(0.7071, 0.0, 0.0, 0.70710))
|
||||
"""
|
||||
euler_rotation = OpenMaya.MEulerRotation(
|
||||
rotation[0] * math.pi / 180.0,
|
||||
rotation[1] * math.pi / 180.0,
|
||||
rotation[2] * math.pi / 180.0
|
||||
)
|
||||
euler_rotation.reorderIt(EULER_ROTATION_ORDER[rotation_order])
|
||||
|
||||
quat = euler_rotation.asQuaternion()
|
||||
return quat.x, quat.y, quat.z, quat.w
|
||||
|
||||
|
||||
def quaternion_to_euler(rotation, rotation_order='xyz'):
|
||||
"""
|
||||
Returns Quaternion Rotation as Euler
|
||||
|
||||
quaternion_to_euler((0.7071, 0.0, 0.0, 0.70710))
|
||||
(90, 0, 0)
|
||||
"""
|
||||
quat = OpenMaya.MQuaternion(*rotation)
|
||||
euler_rotation = quat.asEulerRotation()
|
||||
euler_rotation.reorderIt(EULER_ROTATION_ORDER[rotation_order])
|
||||
|
||||
return euler_rotation.x * 180.0 / math.pi, euler_rotation.y * 180.0 / math.pi, euler_rotation.z * 180.0 / math.pi
|
||||
|
||||
|
||||
def is_connected_to_array(attribute, array_attr):
|
||||
"""
|
||||
Check if the attribute is connected to the specified array
|
||||
:param attribute :type str: attribute
|
||||
:param array_attr :type str array attribute
|
||||
:return :type int or None: int of index in the array or None
|
||||
"""
|
||||
try:
|
||||
indices = cmds.getAttr(array_attr, multiIndices=True) or []
|
||||
except ValueError:
|
||||
return None
|
||||
for i in indices:
|
||||
attr = '{from_attr}[{index}]'.format(from_attr=array_attr, index=i)
|
||||
if attribute in (cmds.listConnections(attr, plugs=True) or []):
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def get_next_available_index_in_array(attribute):
|
||||
# Get the next available index
|
||||
indices = cmds.getAttr(attribute, multiIndices=True) or []
|
||||
i = 0
|
||||
for index in indices:
|
||||
if index != i and i not in indices:
|
||||
indices.append(i)
|
||||
i += 1
|
||||
indices.sort()
|
||||
attrs = ['{from_attr}[{index}]'.format(from_attr=attribute, index=i) for i in indices]
|
||||
connections = [cmds.listConnections(attr, plugs=True) or [] for attr in attrs]
|
||||
target_index = len(indices)
|
||||
for index, conn in enumerate(connections):
|
||||
if not conn:
|
||||
target_index = index
|
||||
break
|
||||
return target_index
|
||||
|
||||
|
||||
def message_connect(from_attribute, to_attribute, in_array=False, out_array=False):
|
||||
"""
|
||||
Create and connect a message attribute between two nodes
|
||||
"""
|
||||
# Generate the object and attr names
|
||||
from_object, from_attribute_name = from_attribute.split('.', 1)
|
||||
to_object, to_attribute_name = to_attribute.split('.', 1)
|
||||
|
||||
# If the attributes don't exist, create them
|
||||
if not cmds.attributeQuery(from_attribute_name, node=from_object, exists=True):
|
||||
cmds.addAttr(from_object, longName=from_attribute_name, attributeType='message', multi=in_array)
|
||||
if not cmds.attributeQuery(to_attribute_name, node=to_object, exists=True):
|
||||
cmds.addAttr(to_object, longName=to_attribute_name, attributeType='message', multi=out_array)
|
||||
# Check that both attributes, if existing are message attributes
|
||||
for a in (from_attribute, to_attribute):
|
||||
if cmds.getAttr(a, type=1) != 'message':
|
||||
raise exceptions.MessageConnectionError(
|
||||
'Message Connect: Attribute {attr} is not a message attribute. CONNECTION ABORTED.'.format(
|
||||
attr=a
|
||||
)
|
||||
)
|
||||
# Connect up the attributes
|
||||
try:
|
||||
if in_array:
|
||||
from_attribute = "{from_attribute}[{index}]".format(
|
||||
from_attribute=from_attribute,
|
||||
index=get_next_available_index_in_array(from_attribute)
|
||||
)
|
||||
|
||||
if out_array:
|
||||
to_attribute = "{to_attribute}[{index}]".format(
|
||||
to_attribute=to_attribute,
|
||||
index=get_next_available_index_in_array(to_attribute)
|
||||
)
|
||||
|
||||
return cmds.connectAttr(from_attribute, to_attribute, force=True)
|
||||
except Exception as e:
|
||||
LOG.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
def connect_attr(from_attr, to_attr):
|
||||
if not cmds.isConnected(from_attr, to_attr):
|
||||
cmds.connectAttr(from_attr, to_attr)
|
||||
|
||||
|
||||
def get_attr(attr_name, as_value=True):
|
||||
"""
|
||||
Get the specified attribute
|
||||
:param attr_name :type str: attribute name i.e node.translate
|
||||
:param as_value :type bool: return as value or connected plug name
|
||||
:return :type list or any: either returns a list of connections or the value of the attribute
|
||||
"""
|
||||
# Check if the attribute is connected
|
||||
connections = cmds.listConnections(attr_name, plugs=True)
|
||||
if connections and not as_value:
|
||||
# If the attribute is connected and we don't want the value, return the connections
|
||||
return connections
|
||||
elif as_value:
|
||||
# Return the value
|
||||
return cmds.getAttr(attr_name)
|
||||
|
||||
|
||||
def get_attr_array(attr_name, as_value=True):
|
||||
"""
|
||||
Get the specified array attr
|
||||
:param attr_name :type str: attribute name i.e node.translate
|
||||
:param as_value :type bool: return as value or connected plug name
|
||||
:return :type list or any: either returns a list of connections or the value of the attribute
|
||||
"""
|
||||
# Get the number of indices in the array
|
||||
indices = cmds.getAttr(attr_name, multiIndices=True) or []
|
||||
# Empty list to store the connected plugs
|
||||
connected_plugs = []
|
||||
# Empty list to store values
|
||||
values = []
|
||||
# Iterate through the indices
|
||||
for i in indices:
|
||||
# Get all the connected plugs for this index
|
||||
connections = cmds.listConnections('{attr_name}[{index}]'.format(attr_name=attr_name, index=i), plugs=True)
|
||||
# If we want the plugs and not values, store connections
|
||||
if connections and not as_value:
|
||||
connected_plugs.extend(connections)
|
||||
# If we want values, get the value at the index
|
||||
elif as_value:
|
||||
values.append(cmds.getAttr('{attr_name}[{index}]'.format(attr_name=attr_name, index=i)))
|
||||
# Return plugs or values, depending on which one has data
|
||||
return connected_plugs or values
|
||||
|
||||
|
||||
def set_attr_or_connect(source_attr_name, value=None, attr_type=None, output=False):
|
||||
"""
|
||||
Set an attribute or connect it to another attribute
|
||||
:param source_attr_name :type str: attribute name
|
||||
:param value : type any: value to set the attribute to
|
||||
:param attr_type :type str: name of the attribute type i.e matrix
|
||||
:param output :type bool: is this plug an output (True) or input (False)
|
||||
"""
|
||||
# Type conversion from maya: python
|
||||
attr_types = {
|
||||
'matrix': list
|
||||
}
|
||||
# Check if we have a matching type
|
||||
matching_type = attr_types.get(attr_type, None)
|
||||
# If we have a matching type and the value matches that type, set the attr
|
||||
if matching_type is not None and isinstance(value, matching_type):
|
||||
cmds.setAttr(source_attr_name, value, type=attr_type)
|
||||
# If the value is a string and no type is matched, we want to connect the attributes
|
||||
elif isinstance(value, str):
|
||||
try:
|
||||
# Connect from left->right depending on if the source is output or input
|
||||
if output:
|
||||
if not cmds.isConnected(source_attr_name, value):
|
||||
cmds.connectAttr(source_attr_name, value)
|
||||
else:
|
||||
if not cmds.isConnected(value, source_attr_name):
|
||||
cmds.connectAttr(value, source_attr_name)
|
||||
except Exception as e:
|
||||
raise exceptions.PoseWranglerAttributeError(
|
||||
"Unable to {direction} {input} to '{output}'".format(
|
||||
direction="connect" if value else "disconnect",
|
||||
input=source_attr_name if output else value,
|
||||
output=value if output else source_attr_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
cmds.setAttr(source_attr_name, value)
|
||||
|
||||
|
||||
def disconnect_attr(attr_name, array=False):
|
||||
"""
|
||||
Disconnect the specified attribute
|
||||
:param attr_name :type str: attribute name to disconnect
|
||||
:param array :type bool: is this attribute an array?
|
||||
"""
|
||||
attrs = []
|
||||
# If we are disconnecting an array, get the names of all the attributes
|
||||
if array:
|
||||
attrs.extend(cmds.getAttr(attr_name, multiIndices=True) or [])
|
||||
# Otherwise append the attr name specified
|
||||
else:
|
||||
attrs.append(attr_name)
|
||||
# Iterate through all the attrs listed
|
||||
for attr in attrs:
|
||||
# Find their connections and disconnect them
|
||||
for plug in cmds.listConnections(attr, plugs=True) or []:
|
||||
cmds.disconnectAttr(attr, plug)
|
||||
|
||||
|
||||
def get_selection(_type=""):
|
||||
"""
|
||||
Returns the current selection
|
||||
"""
|
||||
return cmds.ls(selection=True, type=_type)
|
||||
|
||||
|
||||
def set_selection(selection_list):
|
||||
"""
|
||||
Sets the active selection
|
||||
"""
|
||||
cmds.select(selection_list, replace=True)
|
BIN
Scripts/Animation/epic_pose_wrangler/v2/view/icons/main.png
Normal file
BIN
Scripts/Animation/epic_pose_wrangler/v2/view/icons/main.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
16
Scripts/Animation/epic_pose_wrangler/v2/view/maya_style.qss
Normal file
16
Scripts/Animation/epic_pose_wrangler/v2/view/maya_style.qss
Normal file
@ -0,0 +1,16 @@
|
||||
QListWidget[Log=true]{
|
||||
background-color: #2A2A2A;
|
||||
background-image: url("PoseWrangler:epic.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom right;
|
||||
}
|
||||
|
||||
QPushButton[Category=true] {
|
||||
text-align: left;
|
||||
background-color: #707070;
|
||||
}
|
||||
|
||||
QPushButton::hover[Category=true] {
|
||||
text-align: left;
|
||||
background-color: #707070;
|
||||
}
|
839
Scripts/Animation/epic_pose_wrangler/v2/view/pose_wrangler_ui.ui
Normal file
839
Scripts/Animation/epic_pose_wrangler/v2/view/pose_wrangler_ui.ui
Normal file
@ -0,0 +1,839 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>883</width>
|
||||
<height>693</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QSplitter" name="splitter_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>RBF SOLVERS:</string>
|
||||
</property>
|
||||
<property name="heading" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="solver_LIST">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>RMB Solver for options</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_14">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="toggle_edit_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Edit Selected Solver</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>2</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_13">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="create_solver_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Create Solver</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="refresh_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Refresh Solvers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>2</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="delete_solver_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Delete Solvers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="mirror_solver_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mirror Solver</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>2</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="verticalLayoutWidget_2">
|
||||
<layout class="QVBoxLayout" name="poses_layout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>POSES:</string>
|
||||
</property>
|
||||
<property name="heading" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="pose_LIST">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_pose_BTN">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add Pose</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="edit_pose_BTN">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Update Pose</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="delete_pose_BTN">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Delete Pose</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="mute_pose_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mute Pose</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="mirror_pose_BTN">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mirror Pose</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="rename_pose_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Rename Pose</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="driver_layout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>DRIVER TRANSFORMS:</string>
|
||||
</property>
|
||||
<property name="heading" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="driver_transforms_LIST">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_driver_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add Driver Transforms</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove_driver_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove Driver Transforms</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QVBoxLayout" name="driven_layout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>DRIVEN TRANSFORMS/BLENDSHAPES</string>
|
||||
</property>
|
||||
<property name="heading" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="driven_transforms_LIST">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_driven_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add Driven Transforms</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove_driven_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove Selected</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="create_blendshape_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Create Blendshape</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_existing_blendshape_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add Existing Blendshape</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="edit_blendshape_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Edit Blendshape</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="utilities_DOCK">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>306</width>
|
||||
<height>99</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="allowedAreas">
|
||||
<set>Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea</set>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Utilities</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QScrollArea" name="action_scroll_area">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>298</width>
|
||||
<height>642</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="0">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="extension_layout"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Settings:</string>
|
||||
</property>
|
||||
<property name="heading" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Mirror Mapping:</string>
|
||||
</property>
|
||||
<property name="heading" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="mirror_mapping_LINE">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Mirror Mapping File</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="mirror_mapping_BTN">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Select File</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>585</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>1</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menuBar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>883</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
<property name="title">
|
||||
<string>Help</string>
|
||||
</property>
|
||||
<addaction name="documentation_ACT"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="import_drivers_ACT"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="export_drivers_ACT"/>
|
||||
<addaction name="export_selected_drivers_ACT"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuEdit">
|
||||
<property name="title">
|
||||
<string>Edit</string>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuStyle">
|
||||
<property name="title">
|
||||
<string>Style</string>
|
||||
</property>
|
||||
<addaction name="use_maya_style_ACT"/>
|
||||
</widget>
|
||||
<addaction name="menuStyle"/>
|
||||
</widget>
|
||||
<addaction name="menuFile"/>
|
||||
<addaction name="menuEdit"/>
|
||||
<addaction name="menuHelp"/>
|
||||
</widget>
|
||||
<action name="connectSkinNode_action">
|
||||
<property name="text">
|
||||
<string>Connect Selected Skin Nodes</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="createGeometry_action">
|
||||
<property name="text">
|
||||
<string>Create Geometry From SkinNode</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="unbindGeometry_action">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Unbind Geometry When Saving</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="documentation_ACT">
|
||||
<property name="text">
|
||||
<string>Documentation</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionGithub">
|
||||
<property name="text">
|
||||
<string>Github</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="import_drivers_ACT">
|
||||
<property name="text">
|
||||
<string>Import Drivers</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="export_drivers_ACT">
|
||||
<property name="text">
|
||||
<string>Export All Drivers</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="export_selected_drivers_ACT">
|
||||
<property name="text">
|
||||
<string>Export Selected Drivers</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="use_maya_style_ACT">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use Maya Style</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -0,0 +1,978 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
import os
|
||||
import webbrowser
|
||||
from functools import partial
|
||||
|
||||
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
|
||||
|
||||
from PySide2 import QtWidgets, QtGui
|
||||
from PySide2 import QtCore
|
||||
from PySide2 import QtUiTools
|
||||
|
||||
from epic_pose_wrangler.v2.view import ui_context
|
||||
from epic_pose_wrangler.v2.view.widget import category
|
||||
from epic_pose_wrangler.log import LOG
|
||||
from epic_pose_wrangler.view import log_widget
|
||||
from epic_pose_wrangler.model import settings
|
||||
|
||||
|
||||
class PoseWranglerWindow(MayaQWidgetDockableMixin, QtWidgets.QMainWindow):
|
||||
"""
|
||||
class for the pose wranglerUI
|
||||
"""
|
||||
|
||||
# Solver Signals
|
||||
event_create_solver = QtCore.Signal(str)
|
||||
event_delete_solver = QtCore.Signal(str)
|
||||
event_edit_solver = QtCore.Signal(bool, object)
|
||||
event_mirror_solver = QtCore.Signal(object)
|
||||
event_refresh_solvers = QtCore.Signal()
|
||||
event_set_current_solver = QtCore.Signal(object)
|
||||
# Driver Signals
|
||||
event_add_drivers = QtCore.Signal()
|
||||
event_remove_drivers = QtCore.Signal(list)
|
||||
event_import_drivers = QtCore.Signal(str)
|
||||
event_export_drivers = QtCore.Signal(str, list)
|
||||
# Driven Signals
|
||||
event_add_driven = QtCore.Signal(list, object, bool)
|
||||
event_remove_driven = QtCore.Signal(list, object)
|
||||
# Pose Signals
|
||||
event_add_pose = QtCore.Signal(str, object)
|
||||
event_delete_pose = QtCore.Signal(str, object)
|
||||
event_go_to_pose = QtCore.Signal(str, object)
|
||||
event_mirror_pose = QtCore.Signal(str, object)
|
||||
event_rename_pose = QtCore.Signal(str, str, object)
|
||||
event_update_pose = QtCore.Signal(str, object)
|
||||
event_mute_pose = QtCore.Signal(str, object, object)
|
||||
# Blendshape Signals
|
||||
event_create_blendshape = QtCore.Signal(str, str, bool, object)
|
||||
event_add_blendshape = QtCore.Signal(str, str, str, object)
|
||||
event_edit_blendshape = QtCore.Signal(str, bool, object)
|
||||
# Utility Signals
|
||||
event_get_valid_actions = QtCore.Signal(object)
|
||||
event_select = QtCore.Signal(list)
|
||||
event_set_mirror_mapping = QtCore.Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super(PoseWranglerWindow, self).__init__()
|
||||
# Load the UI file
|
||||
file_path = os.path.dirname(__file__) + "/pose_wrangler_ui.ui"
|
||||
if os.path.exists(file_path):
|
||||
ui_file = QtCore.QFile(file_path)
|
||||
# Attempt to open and load the UI
|
||||
try:
|
||||
ui_file.open(QtCore.QFile.ReadOnly)
|
||||
loader = QtUiTools.QUiLoader()
|
||||
self.win = loader.load(ui_file)
|
||||
finally:
|
||||
# Always close the UI file regardless of loader result
|
||||
ui_file.close()
|
||||
else:
|
||||
raise ValueError('UI File does not exist on disk at path: {}'.format(file_path))
|
||||
icon_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'resources', 'icons')
|
||||
QtCore.QDir.setSearchPaths("PoseWrangler", [icon_folder])
|
||||
self.setWindowTitle("Pose Wrangler 2.0.0")
|
||||
|
||||
# Embed the UI window inside this window
|
||||
self.setCentralWidget(self.win)
|
||||
self.setWindowIcon(QtGui.QIcon(QtGui.QPixmap("PoseWrangler:unreal.png")))
|
||||
|
||||
# Solver Connections
|
||||
self.win.create_solver_BTN.pressed.connect(self._create_solver)
|
||||
self.win.delete_solver_BTN.pressed.connect(self._delete_solver)
|
||||
self.win.toggle_edit_BTN.clicked.connect(self._edit_solver_toggle)
|
||||
self.win.mirror_solver_BTN.pressed.connect(self._mirror_solver)
|
||||
self.win.refresh_BTN.clicked.connect(self._refresh_solvers)
|
||||
self.win.solver_LIST.itemSelectionChanged.connect(self._solver_selection_changed)
|
||||
self.win.solver_LIST.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.win.solver_LIST.customContextMenuRequested.connect(self._solver_context_menu_requested)
|
||||
|
||||
# Driver Connections
|
||||
self.win.add_driver_BTN.pressed.connect(self._add_drivers)
|
||||
self.win.remove_driver_BTN.pressed.connect(self._remove_drivers)
|
||||
self.win.import_drivers_ACT.triggered.connect(self._import_drivers)
|
||||
self.win.export_drivers_ACT.triggered.connect(partial(self._export_drivers, True))
|
||||
self.win.export_selected_drivers_ACT.triggered.connect(self._export_drivers)
|
||||
self.win.driver_transforms_LIST.itemDoubleClicked.connect(
|
||||
partial(
|
||||
self._select_in_scene,
|
||||
self.win.driver_transforms_LIST
|
||||
)
|
||||
)
|
||||
self.win.driven_transforms_LIST.itemDoubleClicked.connect(
|
||||
partial(
|
||||
self._select_in_scene,
|
||||
self.win.driven_transforms_LIST
|
||||
)
|
||||
)
|
||||
|
||||
# Driven Connections
|
||||
self.win.add_driven_BTN.pressed.connect(self._add_driven)
|
||||
self.win.remove_driven_BTN.pressed.connect(self._remove_driven)
|
||||
|
||||
# Pose Connections
|
||||
self.win.add_pose_BTN.pressed.connect(self._create_pose)
|
||||
self.win.delete_pose_BTN.pressed.connect(self._delete_pose)
|
||||
self.win.edit_pose_BTN.pressed.connect(self._update_pose)
|
||||
self.win.mirror_pose_BTN.pressed.connect(self._mirror_pose)
|
||||
self.win.rename_pose_BTN.pressed.connect(self._rename_pose)
|
||||
self.win.mute_pose_BTN.pressed.connect(self._mute_pose)
|
||||
self.win.pose_LIST.itemSelectionChanged.connect(self._pose_selection_changed)
|
||||
|
||||
# Blendshape Connections
|
||||
self.win.create_blendshape_BTN.pressed.connect(self._create_blendshape)
|
||||
self.win.add_existing_blendshape_BTN.pressed.connect(self._add_blendshape)
|
||||
self.win.edit_blendshape_BTN.pressed.connect(self._edit_blendshape)
|
||||
|
||||
# Utility Connections
|
||||
self.win.mirror_mapping_BTN.clicked.connect(self.set_mirror_file)
|
||||
self.win.use_maya_style_ACT.triggered.connect(self._toggle_stylesheet)
|
||||
self.win.documentation_ACT.triggered.connect(self._open_documentation)
|
||||
|
||||
self.win.use_maya_style_ACT.setChecked(bool(int(settings.SettingsManager.get_setting("UseMayaStyle") or 0)))
|
||||
|
||||
# Create a log dock widget
|
||||
self._log_widget = log_widget.LogWidget()
|
||||
# Add the log widget as a handler for the current log to display log messages in the UI
|
||||
LOG.addHandler(self._log_widget)
|
||||
# Add the dock to the window
|
||||
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self._log_widget.log_dock)
|
||||
|
||||
# Set the stylesheet
|
||||
self._set_stylesheet()
|
||||
# Empty vars to store the currently edited solver and blendshape, None if not editing either
|
||||
self._editing_solver = None
|
||||
self._editing_blendshape = None
|
||||
self._valid_actions = []
|
||||
|
||||
# Generate lists of UI elements for different UI states.
|
||||
# Solver context enables/disables the specified widgets when a solver is in edit mode or not
|
||||
self._solver_context_ui_elements = [self.win.add_pose_BTN, self.win.delete_pose_BTN,
|
||||
self.win.edit_pose_BTN, self.win.mirror_pose_BTN, self.win.mute_pose_BTN,
|
||||
self.win.add_driven_BTN, self.win.remove_driven_BTN,
|
||||
self.win.create_blendshape_BTN,
|
||||
self.win.add_existing_blendshape_BTN, self.win.edit_blendshape_BTN,
|
||||
self.win.add_driver_BTN, self.win.remove_driver_BTN,
|
||||
self.win.rename_pose_BTN]
|
||||
# Blendshape editing context enables/disables the specified widgets when a blendshape is being edited or not
|
||||
self._blendshape_editing_context_ui_elements = [self.win.add_pose_BTN, self.win.delete_pose_BTN,
|
||||
self.win.edit_pose_BTN, self.win.mirror_pose_BTN,
|
||||
self.win.mute_pose_BTN,
|
||||
self.win.add_driven_BTN, self.win.remove_driven_BTN,
|
||||
self.win.create_blendshape_BTN,
|
||||
self.win.add_existing_blendshape_BTN,
|
||||
self.win.add_driver_BTN, self.win.remove_driver_BTN,
|
||||
self.win.create_solver_BTN, self.win.delete_solver_BTN,
|
||||
self.win.toggle_edit_BTN, self.win.rename_pose_BTN]
|
||||
|
||||
# Disable all the solver elements by default
|
||||
for element in self._solver_context_ui_elements:
|
||||
element.setEnabled(False)
|
||||
|
||||
self.show(dockable=True)
|
||||
|
||||
@property
|
||||
def valid_actions(self):
|
||||
return self._valid_actions
|
||||
|
||||
def add_rbf_solver(self, solver, edit=False):
|
||||
"""
|
||||
Add a solver to the view
|
||||
:param solver :type api.RBFNode: solver reference
|
||||
:param edit :type bool: is this solver in edit mode?
|
||||
"""
|
||||
# Create a new widget item with the solver name
|
||||
item = QtWidgets.QListWidgetItem(str(solver))
|
||||
# Add the solver and the edit status as custom data on the item
|
||||
item.setData(QtCore.Qt.UserRole, {"solver": solver, "edit": edit})
|
||||
# If we are editing, keep track of it
|
||||
if edit:
|
||||
self._editing_solver = solver
|
||||
# Add the item to the solver list
|
||||
self.win.solver_LIST.addItem(item)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear all the lists in the UI
|
||||
"""
|
||||
self.win.solver_LIST.clear()
|
||||
self.win.driver_transforms_LIST.clear()
|
||||
self.win.driven_transforms_LIST.clear()
|
||||
self.win.pose_LIST.clear()
|
||||
|
||||
def delete_solver(self, solver):
|
||||
"""
|
||||
Delete the specified solver from the view
|
||||
:param solver :type api.RBFNode: solver ref
|
||||
"""
|
||||
# Iterate through the solvers
|
||||
for i in range(self.win.solver_LIST.count()):
|
||||
# Get the item
|
||||
item = self.win.solver_LIST.item(i)
|
||||
# If the solver matches the solver from the item data
|
||||
if item.data(QtCore.Qt.UserRole)['solver'] == solver:
|
||||
# Remove the item from the list and break early
|
||||
self.win.solver_LIST.takeItem(self.win.solver_LIST.row(item))
|
||||
break
|
||||
|
||||
def display_extensions(self, extensions):
|
||||
"""
|
||||
Adds any found extensions to the utilities dock
|
||||
:param extensions :type list: list of PoseWranglerExtension instances
|
||||
"""
|
||||
# Clear the existing extension layout
|
||||
for i in reversed(range(0, self.win.extension_layout.count())):
|
||||
item = self.win.extension_layout.itemAt(i)
|
||||
if isinstance(item, QtWidgets.QWidgetItem):
|
||||
widget = item.widget()
|
||||
widget.deleteLater()
|
||||
del widget
|
||||
self.win.extension_layout.removeItem(item)
|
||||
|
||||
categories = {}
|
||||
for extension in extensions:
|
||||
if extension.view:
|
||||
category_name = extension.__category__ or "Default"
|
||||
if category_name in categories:
|
||||
category_widget = categories[category_name]
|
||||
else:
|
||||
category_widget = category.CategoryWidget(category_name)
|
||||
categories[category_name] = category_widget
|
||||
self.win.extension_layout.addWidget(category_widget)
|
||||
category_widget.add_extension(extension.view)
|
||||
|
||||
self.win.utilities_DOCK.setMinimumWidth(
|
||||
self.win.extension_layout.sizeHint().width() +
|
||||
self.win.action_scroll_area.verticalScrollBar().sizeHint().width()
|
||||
)
|
||||
|
||||
def edit_blendshape(self, pose_name, edit=False):
|
||||
"""
|
||||
Set the UI in blendshape edit mode or finish editing.
|
||||
:param pose_name :type str: name of the pose to edit or finish editing
|
||||
:param edit :type bool: True = edit mode, False = finish editing
|
||||
"""
|
||||
# Iterate through all of the driven transforms in the list
|
||||
for i in range(self.win.driven_transforms_LIST.count()):
|
||||
# Get the item
|
||||
item = self.win.driven_transforms_LIST.item(i)
|
||||
# Find the custom data stored on the item
|
||||
item_data = item.data(QtCore.Qt.UserRole)
|
||||
# If the item type is a blendshape and the pose matches the current pose, break
|
||||
if item_data['type'] == 'blendshape' and item_data['pose_name'] == pose_name:
|
||||
break
|
||||
# If no break was hit, exit early because no match was found
|
||||
else:
|
||||
return
|
||||
# If we are editing
|
||||
if edit:
|
||||
# If we are already editing a blendshape, finish editing the previous one first
|
||||
if self._editing_blendshape is not None:
|
||||
# Get the existing item data from the currently edited blendshape
|
||||
existing_item_data = self._editing_blendshape.data(QtCore.Qt.UserRole)
|
||||
# Finish editing
|
||||
self.event_edit_blendshape.emit(
|
||||
existing_item_data['pose_name'], False,
|
||||
existing_item_data['solver']
|
||||
)
|
||||
# Update the icon
|
||||
self._editing_blendshape.setIcon(QtGui.QIcon(":/blendShape.png"))
|
||||
# Store a ref to the new blendshape being edited
|
||||
self._editing_blendshape = item
|
||||
# Update the button text
|
||||
self.win.edit_blendshape_BTN.setText("Finish Editing Blendshape")
|
||||
# Update the icon to show edit mode
|
||||
item.setIcon(QtGui.QIcon(":/fileTextureEdit.png"))
|
||||
# Disable all the non blendshape related ui elements
|
||||
for element in self._blendshape_editing_context_ui_elements:
|
||||
element.setEnabled(False)
|
||||
# If we are not editing
|
||||
else:
|
||||
# Clear the ref to the edited blendshape
|
||||
self._editing_blendshape = None
|
||||
# Revert the button text
|
||||
self.win.edit_blendshape_BTN.setText("Edit Blendshape")
|
||||
# Revert the icon
|
||||
item.setIcon(QtGui.QIcon(":/blendShape.png"))
|
||||
# Re-enable all the ui elements
|
||||
for element in self._blendshape_editing_context_ui_elements:
|
||||
element.setEnabled(True)
|
||||
# Store the edit status in the item data
|
||||
item_data['edit'] = edit
|
||||
# Update the item data on the widget
|
||||
item.setData(QtCore.Qt.UserRole, item_data)
|
||||
|
||||
def edit_solver(self, solver, edit=False):
|
||||
"""
|
||||
Set the UI in solver edit mode or finish editing.
|
||||
:param solver :type str: name of the solver to edit or finish editing
|
||||
:param edit :type bool: True = edit mode, False = finish editing
|
||||
"""
|
||||
# Iterate through the solvers
|
||||
for i in range(self.win.solver_LIST.count()):
|
||||
# Get the widget item
|
||||
item = self.win.solver_LIST.item(i)
|
||||
# If the solver matches the item data, break
|
||||
if item.data(QtCore.Qt.UserRole)['solver'] == solver:
|
||||
break
|
||||
# No match found, return early
|
||||
else:
|
||||
return
|
||||
# Grab the item data for the item
|
||||
item_data = item.data(QtCore.Qt.UserRole)
|
||||
|
||||
# If edit mode
|
||||
if edit:
|
||||
# If we are already editing a solver, finish editing that one first
|
||||
if self._editing_solver is not None:
|
||||
# Update the icon
|
||||
self._editing_solver.setIcon(QtGui.QIcon())
|
||||
# Finish editing
|
||||
self.event_edit_solver.emit(False, self._editing_solver)
|
||||
# Store the new item as the current edited solver
|
||||
self._editing_solver = item
|
||||
# Update the button text
|
||||
self.win.toggle_edit_BTN.setText("Finish Editing '{solver}'".format(solver=solver))
|
||||
# Set the edit icon
|
||||
item.setIcon(QtGui.QIcon(":/fileTextureEdit.png"))
|
||||
else:
|
||||
# Clear the current solver
|
||||
self._editing_solver = None
|
||||
# Revert the button text
|
||||
self.win.toggle_edit_BTN.setText("Edit Selected Driver")
|
||||
# Clear the edit icon
|
||||
item.setIcon(QtGui.QIcon())
|
||||
|
||||
# Update the item data with the new edit status
|
||||
item_data['edit'] = edit
|
||||
# Set the item data
|
||||
item.setData(QtCore.Qt.UserRole, item_data)
|
||||
|
||||
# Update the solver related ui elements with the current edit status
|
||||
for element in self._solver_context_ui_elements:
|
||||
element.setEnabled(edit)
|
||||
|
||||
self.win.delete_solver_BTN.setEnabled(not edit)
|
||||
self.win.create_solver_BTN.setEnabled(not edit)
|
||||
self.win.refresh_BTN.setEnabled(not edit)
|
||||
self.win.mirror_solver_BTN.setEnabled(not edit)
|
||||
# If a pose is selected, go to it. This will cause transforms to pop if edits weren't saved
|
||||
self._pose_selection_changed()
|
||||
|
||||
def get_context(self):
|
||||
"""
|
||||
Gets the current state of the ui
|
||||
:return: PoseWranglerUIContext
|
||||
"""
|
||||
return ui_context.PoseWranglerUIContext(
|
||||
current_solvers=[i.text() for i in self.win.solver_LIST.selectedItems()],
|
||||
current_poses=[i.text() for i in self.win.pose_LIST.selectedItems()],
|
||||
current_drivers=[i.text() for i in self.win.driver_transforms_LIST.selectedItems()],
|
||||
current_driven=[i.text() for i in self.win.driven_transforms_LIST.selectedItems()],
|
||||
solvers=[self.win.solver_LIST.item(i).text() for i in range(0, self.win.solver_LIST.count())],
|
||||
poses=[self.win.pose_LIST.item(i).text() for i in range(0, self.win.pose_LIST.count())],
|
||||
drivers=[self.win.driver_transforms_LIST.item(i).text() for i in
|
||||
range(0, self.win.driver_transforms_LIST.count())],
|
||||
driven=[self.win.driven_transforms_LIST.item(i).text() for i in
|
||||
range(0, self.win.driven_transforms_LIST.count())],
|
||||
)
|
||||
|
||||
def load_solver_settings(self, solver, drivers, driven_transforms, poses):
|
||||
"""
|
||||
Display the settings for the specified solver
|
||||
:param solver :type RBFNode: solver reference
|
||||
:param drivers :type dict: dictionary of driver name: driver data
|
||||
:param driven_transforms :type dict: dictionary of driven transform type: driven transforms
|
||||
:param poses :type dict: dictionary of pose data
|
||||
"""
|
||||
# Grab the current selection from the driver, driven and pose list
|
||||
current_drivers = [i.text() for i in self.win.driver_transforms_LIST.selectedItems()]
|
||||
current_driven = [i.text() for i in self.win.driven_transforms_LIST.selectedItems()]
|
||||
current_poses = [i.text() for i in self.win.pose_LIST.selectedItems()]
|
||||
|
||||
# Clear the driver, driven and pose lists
|
||||
self.win.driver_transforms_LIST.clear()
|
||||
self.win.driven_transforms_LIST.clear()
|
||||
self.win.pose_LIST.clear()
|
||||
|
||||
# Iterate through each driver
|
||||
for driver_name, driver_data in drivers.items():
|
||||
# Create new item widget
|
||||
item = QtWidgets.QListWidgetItem(driver_name)
|
||||
# Set the icon to a joint
|
||||
item.setIcon(QtGui.QIcon(":/kinJoint.png"))
|
||||
# Add item to the list
|
||||
self.win.driver_transforms_LIST.addItem(item)
|
||||
# Set the item data so it can be referenced later
|
||||
item.setData(QtCore.Qt.UserRole, driver_data)
|
||||
|
||||
# Iterate through the driven transforms transform nodes
|
||||
for driven_transform in driven_transforms['transform']:
|
||||
# Create new item widget
|
||||
item = QtWidgets.QListWidgetItem(driven_transform)
|
||||
# Store the solver and item type
|
||||
item.setData(QtCore.Qt.UserRole, {'type': 'transform', 'solver': solver})
|
||||
# Set the icon to a joint
|
||||
item.setIcon(QtGui.QIcon(":/kinJoint.png"))
|
||||
# Add item to the list
|
||||
self.win.driven_transforms_LIST.addItem(item)
|
||||
|
||||
# Iterate through all the driven transforms blendshapes
|
||||
for blendshape_mesh, pose_name in driven_transforms['blendshape'].items():
|
||||
# Create new item widget
|
||||
item = QtWidgets.QListWidgetItem(blendshape_mesh)
|
||||
# Store the solver, pose associated with the blendshape and the item type
|
||||
item.setData(QtCore.Qt.UserRole, {'type': 'blendshape', 'pose_name': pose_name, 'solver': solver})
|
||||
# Set the icon to blendshape
|
||||
item.setIcon(QtGui.QIcon(":/blendShape.png"))
|
||||
# Add item to the list
|
||||
self.win.driven_transforms_LIST.addItem(item)
|
||||
|
||||
# Iterate through the poses
|
||||
for pose, pose_data in poses.items():
|
||||
# Create item and add it to the list
|
||||
item = QtWidgets.QListWidgetItem(pose)
|
||||
self.win.pose_LIST.addItem(item)
|
||||
muted = not pose_data.get('target_enable', True)
|
||||
if muted:
|
||||
font = item.font()
|
||||
font.setStrikeOut(True)
|
||||
item.setFont(font)
|
||||
# Set icon to a pose
|
||||
item.setIcon(QtGui.QIcon(":/p-head.png"))
|
||||
|
||||
# Iterate through the solvers
|
||||
for row in range(self.win.solver_LIST.count()):
|
||||
# Find the item
|
||||
item = self.win.solver_LIST.item(row)
|
||||
# If the current solver matches this item, select it
|
||||
if solver == item.data(QtCore.Qt.UserRole)['solver']:
|
||||
item.setSelected(True)
|
||||
break
|
||||
|
||||
# Create a map between widgets: names of previously selected items
|
||||
existing_selection_to_list_map = {
|
||||
self.win.driver_transforms_LIST: current_drivers,
|
||||
self.win.driven_transforms_LIST: current_driven,
|
||||
self.win.pose_LIST: current_poses
|
||||
}
|
||||
# Iterate through the widgets: target selections
|
||||
for list_ref, current in existing_selection_to_list_map.items():
|
||||
# Block signals so we don't fire any selection changed events
|
||||
list_ref.blockSignals(True)
|
||||
# Iterate through the list widget
|
||||
for i in range(list_ref.count()):
|
||||
# Get the item
|
||||
item = list_ref.item(i)
|
||||
# Check if the text is in the list of currently selected items (case sensitive) and select/deselect
|
||||
# as appropriate
|
||||
if item.text() in current:
|
||||
item.setSelected(True)
|
||||
else:
|
||||
item.setSelected(False)
|
||||
# Unblock the signals
|
||||
list_ref.blockSignals(False)
|
||||
|
||||
def set_mirror_file(self):
|
||||
"""
|
||||
Open up a dialog to get a new mirror mapping file
|
||||
"""
|
||||
# Open dialog
|
||||
result = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self, "Mirror Mapping File",
|
||||
os.path.join(os.path.dirname(__file__), 'mirror_mappings'),
|
||||
"Mirror Mapping File (*.json)"
|
||||
)
|
||||
# If no file specified, exit early
|
||||
if not result[0]:
|
||||
return
|
||||
# Emit the event
|
||||
self.event_set_mirror_mapping.emit(result[0])
|
||||
|
||||
def update_mirror_mapping_file(self, path):
|
||||
"""
|
||||
Update the mirror mapping widget with the specified path
|
||||
:param path :type str: file path to the mirror mapping file
|
||||
"""
|
||||
# Set the text
|
||||
self.win.mirror_mapping_LINE.setText(path)
|
||||
|
||||
# ================================================ Solvers ======================================================= #
|
||||
|
||||
def _create_solver(self):
|
||||
"""
|
||||
Create a new solver with the given name
|
||||
"""
|
||||
# Popup input widget to get the solver name
|
||||
interp_name, ok = QtWidgets.QInputDialog.getText(self, 'Create Solver', 'Solver Name:')
|
||||
# Hack to fix broken styling caused by using a QInputDialog
|
||||
self.win.create_solver_BTN.setEnabled(False)
|
||||
self.win.create_solver_BTN.setEnabled(True)
|
||||
# If no name, exit
|
||||
if not interp_name:
|
||||
return
|
||||
|
||||
if not interp_name.lower().endswith('_uerbfsolver'):
|
||||
interp_name += "_UERBFSolver"
|
||||
# Trigger solver creation
|
||||
self.event_create_solver.emit(interp_name)
|
||||
|
||||
def _delete_solver(self):
|
||||
"""
|
||||
Delete selected solvers
|
||||
"""
|
||||
# Get the current solver selection
|
||||
selected_items = self.win.solver_LIST.selectedItems()
|
||||
|
||||
selected_solvers = []
|
||||
# Iterate through the selection in reverse
|
||||
for item in reversed(selected_items):
|
||||
# Grab the solver from the item data
|
||||
selected_solvers.append(item.data(QtCore.Qt.UserRole)['solver'])
|
||||
|
||||
# Delete the solvers from the backend first
|
||||
for solver in selected_solvers:
|
||||
self.event_delete_solver.emit(solver)
|
||||
|
||||
def _edit_solver_toggle(self):
|
||||
"""
|
||||
Toggle edit mode for the currently selected driver
|
||||
"""
|
||||
# Grab the current selection
|
||||
selection = self.win.solver_LIST.selectedItems()
|
||||
if selection:
|
||||
# Grab the last item
|
||||
item = selection[-1]
|
||||
# Get the item data
|
||||
item_data = item.data(QtCore.Qt.UserRole)
|
||||
# Get the solver
|
||||
solver = item_data['solver']
|
||||
# Trigger edit mode enabled/disabled depending on the solvers current edit state
|
||||
self.event_edit_solver.emit(not item_data.get('edit', False), solver)
|
||||
|
||||
def _get_item_from_solver(self, solver):
|
||||
"""
|
||||
Get the widget associated with this solver
|
||||
:param solver :type api.RBFNode: solver ref
|
||||
:return :type QtWidgets.QListWidgetItem or None: widget
|
||||
"""
|
||||
# Iterate through the solver list
|
||||
for i in range(self.win.solver_LIST.count()):
|
||||
# Get the item
|
||||
item = self.win.solver_LIST.item(i)
|
||||
# If the solver stored in the item data matches, return it
|
||||
if item.data(QtCore.Qt.UserRole)['solver'] == solver:
|
||||
return item
|
||||
# No match found, returning None
|
||||
|
||||
def _get_selected_solvers(self):
|
||||
"""
|
||||
:return: List of current RBFNodes
|
||||
"""
|
||||
# Get the solver from the item data for each item in the current selection
|
||||
return [item.data(QtCore.Qt.UserRole)['solver'] for item in self.win.solver_LIST.selectedItems()]
|
||||
|
||||
def _mirror_solver(self):
|
||||
"""
|
||||
Mirror the selected solvers
|
||||
"""
|
||||
# For each solver, trigger mirroring
|
||||
for solver in self._get_selected_solvers():
|
||||
self.event_mirror_solver.emit(solver)
|
||||
|
||||
def _refresh_solvers(self):
|
||||
"""
|
||||
Refresh the solver list
|
||||
"""
|
||||
self.event_refresh_solvers.emit()
|
||||
|
||||
def _solver_selection_changed(self):
|
||||
"""
|
||||
When the solver selection changes, update the driver/driven and poses list accordingly
|
||||
"""
|
||||
# Get the current selection
|
||||
items = self.win.solver_LIST.selectedItems()
|
||||
# If we have a selection
|
||||
if items:
|
||||
# Grab the last selected item
|
||||
item = items[-1]
|
||||
# Grab the item data
|
||||
item_data = item.data(QtCore.Qt.UserRole)
|
||||
# Set the current solver to the solver associated with this widget
|
||||
self.event_set_current_solver.emit(item_data['solver'])
|
||||
# Get the solvers edit status
|
||||
edit = item_data.get('edit', False)
|
||||
# Enable/disable the ui elements accordingly
|
||||
for element in self._solver_context_ui_elements:
|
||||
element.setEnabled(edit)
|
||||
# If we are in edit mode
|
||||
if edit:
|
||||
# Update the button
|
||||
self.win.toggle_edit_BTN.setText("Finish Editing '{solver}'".format(solver=item_data['solver']))
|
||||
# Set the icon
|
||||
item.setIcon(QtGui.QIcon(":/fileTextureEdit.png"))
|
||||
# No selection, clear all the lists
|
||||
else:
|
||||
self.win.driver_transforms_LIST.clear()
|
||||
self.win.driven_transforms_LIST.clear()
|
||||
self.win.pose_LIST.clear()
|
||||
|
||||
# ================================================ Drivers ======================================================= #
|
||||
|
||||
def _add_drivers(self):
|
||||
"""
|
||||
Add drivers.
|
||||
"""
|
||||
# Emit nothing so that the current scene selection will be used
|
||||
self.event_add_drivers.emit()
|
||||
|
||||
def _remove_drivers(self):
|
||||
"""
|
||||
Remove the specified drivers
|
||||
"""
|
||||
# Emit the selected drivers to be deleted
|
||||
self.event_remove_drivers.emit(
|
||||
[i.data(QtCore.Qt.UserRole)
|
||||
for i in self.win.driver_transforms_LIST.selectedItems()]
|
||||
)
|
||||
|
||||
# ================================================ Driven ======================================================== #
|
||||
|
||||
def _add_driven(self):
|
||||
"""
|
||||
Add driven nodes to the solver
|
||||
"""
|
||||
# Emit no driven nodes so that it uses scene selection, no solver so that it uses the current solver and set
|
||||
# edit mode to true
|
||||
self.event_add_driven.emit(None, None, True)
|
||||
|
||||
def _remove_driven(self):
|
||||
"""
|
||||
Remove driven transforms from the specified solver
|
||||
"""
|
||||
# Get currently selected driven items
|
||||
items = self.win.driven_transforms_LIST.selectedItems()
|
||||
# Exit early if nothing is selected
|
||||
if not items:
|
||||
return
|
||||
# Get the solver for the last item (they should all have the same solver)
|
||||
solver = items[-1].data(QtCore.Qt.UserRole)['solver']
|
||||
# Trigger event to remove specified nodes for the solver
|
||||
self.event_remove_driven.emit([i.text() for i in items], solver)
|
||||
|
||||
# ============================================== Blendshapes ===================================================== #
|
||||
|
||||
def _add_blendshape(self):
|
||||
"""
|
||||
Add an existing blendshape to the solver for the current pose
|
||||
"""
|
||||
# Get current solver
|
||||
solver = self._get_selected_solvers()
|
||||
# Get current pose
|
||||
poses = self._get_selected_poses()
|
||||
if not solver or not poses:
|
||||
LOG.warning("Unable to add blendshape, please select a solver and a pose and try again")
|
||||
return
|
||||
self.event_add_blendshape.emit(poses[-1], "", "", solver[-1])
|
||||
|
||||
def _create_blendshape(self):
|
||||
"""
|
||||
Create a blendshape for the current solver at the current pose
|
||||
"""
|
||||
# Get current solver
|
||||
solver = self._get_selected_solvers()
|
||||
# Get current pose
|
||||
poses = self._get_selected_poses()
|
||||
if not solver or not poses:
|
||||
LOG.warning("Unable to create blendshape, please select a solver and a pose and try again")
|
||||
return
|
||||
# Create a blendshape for the last pose selected, with the current mesh selection, in edit mode enabled and
|
||||
# for the last solver selected
|
||||
self.event_create_blendshape.emit(poses[-1], None, True, solver[-1])
|
||||
|
||||
def _edit_blendshape(self):
|
||||
"""
|
||||
Edit or finish editing the selected blendshape
|
||||
"""
|
||||
# Get the current blendshape being edited
|
||||
blendshape = self._editing_blendshape
|
||||
# If none is being edited, see if we have a blendshape selected
|
||||
if blendshape is None:
|
||||
# Get all the blendshapes selected
|
||||
selection = [sel for sel in self.win.driven_transforms_LIST.selectedItems() if
|
||||
sel.data(QtCore.Qt.UserRole)['type'] == 'blendshape']
|
||||
# Update the blendshape var
|
||||
if selection:
|
||||
blendshape = selection[-1]
|
||||
# If we have a blendshape
|
||||
if blendshape:
|
||||
# Get the item data
|
||||
item_data = blendshape.data(QtCore.Qt.UserRole)
|
||||
# Get the associated pose name and solver
|
||||
pose_name = item_data['pose_name']
|
||||
solver = item_data['solver']
|
||||
# Trigger editing the blendshape for the pose name, using the opposite to the current edit status and for
|
||||
# the associated solver
|
||||
self.event_edit_blendshape.emit(pose_name, not item_data.get('edit', False), solver)
|
||||
|
||||
# ================================================= Poses ======================================================== #
|
||||
|
||||
def _create_pose(self):
|
||||
"""
|
||||
Create a new pose
|
||||
"""
|
||||
# Get the current solver
|
||||
selected_solvers = self._get_selected_solvers()
|
||||
|
||||
# Get the last selection
|
||||
solver = selected_solvers[-1]
|
||||
# Display popup to get pose name
|
||||
pose_name, ok = QtWidgets.QInputDialog.getText(self, 'Create Pose', 'Pose Name:')
|
||||
# Hack to fix broken styling caused by using a QInputDialog
|
||||
self.win.add_pose_BTN.setEnabled(False)
|
||||
self.win.add_pose_BTN.setEnabled(True)
|
||||
|
||||
if pose_name:
|
||||
# If a pose name is specified, create pose
|
||||
self.event_add_pose.emit(pose_name, solver)
|
||||
|
||||
def _delete_pose(self):
|
||||
"""
|
||||
Delete the selected poses
|
||||
"""
|
||||
# Get the currently selected poses
|
||||
poses = self._get_selected_poses()
|
||||
# Exit early if nothing is selected
|
||||
if not poses:
|
||||
return
|
||||
# Get the selected solver
|
||||
solver = self._get_selected_solvers()[-1]
|
||||
# Block the signals to stop UI event updates
|
||||
self.win.pose_LIST.blockSignals(True)
|
||||
# For each selected pose, delete it
|
||||
for pose_name in poses:
|
||||
self.event_delete_pose.emit(pose_name, solver)
|
||||
self.win.pose_LIST.blockSignals(False)
|
||||
|
||||
def _get_selected_poses(self):
|
||||
"""
|
||||
Get the selected poses
|
||||
:return :type list: list of selected pose names
|
||||
"""
|
||||
return [item.text() for item in self.win.pose_LIST.selectedItems()]
|
||||
|
||||
def _mirror_pose(self):
|
||||
"""
|
||||
Mirror the selected poses
|
||||
"""
|
||||
# Get the pose selection
|
||||
poses = self._get_selected_poses()
|
||||
# Exit early if nothing is selected
|
||||
if not poses:
|
||||
return
|
||||
# Get the selected solver
|
||||
solver = self._get_selected_solvers()[-1]
|
||||
# For each pose, mirror it. This will create a mirrored solver if it doesn't already exist
|
||||
for pose_name in poses:
|
||||
self.event_mirror_pose.emit(pose_name, solver)
|
||||
|
||||
def _mute_pose(self):
|
||||
"""
|
||||
Toggles the muted status for the poses the selected poses
|
||||
"""
|
||||
# Get the pose selection
|
||||
poses = self._get_selected_poses()
|
||||
# Exit early if nothing is selected
|
||||
if not poses:
|
||||
return
|
||||
|
||||
solver = self._get_selected_solvers()[-1]
|
||||
# For each pose, mirror it. This will create a mirrored solver if it doesn't already exist
|
||||
for pose_name in poses:
|
||||
self.event_mute_pose.emit(pose_name, None, solver)
|
||||
|
||||
def _pose_selection_changed(self):
|
||||
"""
|
||||
On pose selection changed, go to the new pose
|
||||
"""
|
||||
# Get the current pose selection
|
||||
selected_poses = self._get_selected_poses()
|
||||
if selected_poses:
|
||||
# Get the last pose
|
||||
selected_pose = selected_poses[-1]
|
||||
# Get the selected solver
|
||||
solver = self._get_selected_solvers()[-1]
|
||||
# Go to pose
|
||||
self.event_go_to_pose.emit(selected_pose, solver)
|
||||
|
||||
def _rename_pose(self):
|
||||
"""
|
||||
Rename the currently selected pose
|
||||
"""
|
||||
# Get the current pose selection
|
||||
poses = self._get_selected_poses()
|
||||
# Exit early if nothing is selected
|
||||
if not poses:
|
||||
return
|
||||
# Get the last selected pose name
|
||||
pose_name = poses[-1]
|
||||
# Get the current solver
|
||||
solver = self._get_selected_solvers()[-1]
|
||||
# Create a popup to get the new pose name
|
||||
new_name, ok = QtWidgets.QInputDialog.getText(self, 'Rename Pose', 'New Pose Name:')
|
||||
# Hack to fix broken styling caused by using a QInputDialog
|
||||
self.win.rename_pose_BTN.setEnabled(False)
|
||||
self.win.rename_pose_BTN.setEnabled(True)
|
||||
# Exit if no new name is specified
|
||||
if not new_name:
|
||||
return
|
||||
# Get the existing pose names
|
||||
existing_names = [self.win.pose_LIST.item(i).text().lower() for i in range(self.win.pose_LIST.count())]
|
||||
# If the pose name already exists, log it and exit
|
||||
if new_name.lower() in existing_names:
|
||||
LOG.error("Pose '{pose_name}' already exists".format(pose_name=new_name))
|
||||
return
|
||||
# Pose doesn't already exist, rename it
|
||||
self.event_rename_pose.emit(pose_name, new_name, solver)
|
||||
|
||||
def _update_pose(self):
|
||||
"""
|
||||
Update the pose for the given solver
|
||||
"""
|
||||
|
||||
# Get the current pose selection
|
||||
poses = self._get_selected_poses()
|
||||
# Exit early if nothing is selected
|
||||
if not poses:
|
||||
return
|
||||
# Get the last selected pose name
|
||||
pose_name = poses[-1]
|
||||
# Get the current solver
|
||||
solver = self._get_selected_solvers()[-1]
|
||||
# Update the pose
|
||||
self.event_update_pose.emit(pose_name, solver)
|
||||
|
||||
# ================================================== IO ========================================================== #
|
||||
|
||||
def _import_drivers(self):
|
||||
"""
|
||||
Generate a popup to find a json file to import serialized solver data from
|
||||
"""
|
||||
# Get a json file path
|
||||
file_path = QtWidgets.QFileDialog.getOpenFileName(None, "Pose Wrangler Format", "", "*.json")[0]
|
||||
# If no path is specified, exit early
|
||||
if file_path == "":
|
||||
return
|
||||
# Import the drivers
|
||||
self.event_import_drivers.emit(file_path)
|
||||
|
||||
def _export_drivers(self, all_drivers=False):
|
||||
"""
|
||||
Export drivers to a file
|
||||
:param all_drivers :type bool: should all drivers be exported or just the current selection?
|
||||
"""
|
||||
# Get the export file path
|
||||
file_path = QtWidgets.QFileDialog.getSaveFileName(None, "Pose Wrangler Format", "", "*.json")[0]
|
||||
# If no path is specified, exit early
|
||||
if file_path == "":
|
||||
return
|
||||
|
||||
# If all drivers is specified, get all solvers
|
||||
if all_drivers:
|
||||
target_solvers = []
|
||||
else:
|
||||
# Get the currently selected solvers
|
||||
target_solvers = self._get_selected_solvers()
|
||||
# If no solvers found, skip export
|
||||
if not target_solvers:
|
||||
LOG.warning("Unable to export. No solvers selected")
|
||||
return
|
||||
|
||||
# Export drivers
|
||||
self.event_export_drivers.emit(file_path, target_solvers)
|
||||
|
||||
# =============================================== Utilities ====================================================== #
|
||||
def _open_documentation(self):
|
||||
"""
|
||||
Open the documentation
|
||||
"""
|
||||
index_html = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '../..', 'docs', 'site', 'html', 'index.html')
|
||||
)
|
||||
webbrowser.open(index_html)
|
||||
|
||||
def _set_stylesheet(self):
|
||||
"""
|
||||
Load the style.qss file and set the stylesheet
|
||||
:return:
|
||||
"""
|
||||
if bool(int(settings.SettingsManager.get_setting("UseMayaStyle") or 0)):
|
||||
styles_path = os.path.join(os.path.dirname(__file__), 'maya_style.qss')
|
||||
else:
|
||||
styles_path = os.path.join(os.path.dirname(__file__), 'style.qss')
|
||||
|
||||
with open(styles_path) as style:
|
||||
self.setStyleSheet(style.read())
|
||||
|
||||
def _toggle_stylesheet(self, use_maya_style=False):
|
||||
"""
|
||||
Toggles between the maya and UE stylesheet
|
||||
:param use_maya_style :type bool: on or off
|
||||
"""
|
||||
settings.SettingsManager.set_setting("UseMayaStyle", int(use_maya_style))
|
||||
self._set_stylesheet()
|
||||
|
||||
def _select_in_scene(self, list_widget=None, item=None):
|
||||
"""
|
||||
Select the specified items in the scene
|
||||
:param list_widget :type QtWidgets.QListWidget or None: optional list widget to select from
|
||||
:param item :type QtWidgets.QListWidgetItem or None: optional item to select
|
||||
"""
|
||||
# If no item is provided, select all the items from the list widget specified
|
||||
if item is None:
|
||||
items = [list_widget.item(i).text() for i in range(list_widget.count())]
|
||||
# Otherwise select the current item
|
||||
else:
|
||||
items = [item.text()]
|
||||
# If we have items, select them
|
||||
if items:
|
||||
self.event_select.emit(items)
|
||||
|
||||
def _solver_context_menu_requested(self, point):
|
||||
"""
|
||||
Create a context menu for the solver list widget
|
||||
:param point :type QtCore.QPoint: screen position of the request
|
||||
"""
|
||||
# Create a new menu
|
||||
menu = QtWidgets.QMenu(parent=self)
|
||||
self.event_get_valid_actions.emit(self.get_context())
|
||||
|
||||
# Dict to store sub menu name: sub menu
|
||||
sub_menus = {}
|
||||
# Iterate through the valid actions
|
||||
for action in self._valid_actions:
|
||||
# Set the current menu to the base menu
|
||||
current_menu = menu
|
||||
# If the action has a category
|
||||
if action.__category__:
|
||||
# If the category doesn't already exist, create it
|
||||
if action.__category__ not in sub_menus:
|
||||
sub_menus[action.__category__] = menu.addMenu(action.__category__)
|
||||
# Set the current menu to the category menu
|
||||
current_menu = sub_menus[action.__category__]
|
||||
# Create a new action on the current menu
|
||||
action_widget = current_menu.addAction(action.__display_name__)
|
||||
action_widget.setToolTip(action.__tooltip__)
|
||||
# Connect the action trigger to execute the action, passing through the action and data to execute
|
||||
action_widget.triggered.connect(partial(self._trigger_context_menu_action, action))
|
||||
# Draw the menu at the current cursor pos
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def _trigger_context_menu_action(self, action):
|
||||
"""
|
||||
Execute the specified action with the given data
|
||||
:param action :type base_action.BaseAction: action class
|
||||
"""
|
||||
# Execute the action with the data
|
||||
action.execute(ui_context=self.get_context())
|
204
Scripts/Animation/epic_pose_wrangler/v2/view/style.qss
Normal file
204
Scripts/Animation/epic_pose_wrangler/v2/view/style.qss
Normal file
@ -0,0 +1,204 @@
|
||||
QWidget {
|
||||
background-color: rgb(27, 28, 30);
|
||||
color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
|
||||
QMenu {
|
||||
background-color: rgb(4, 4, 4);
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: rgb(50, 50, 50);
|
||||
}
|
||||
|
||||
QWidget:disabled {
|
||||
color: rgb(112, 117, 120);
|
||||
}
|
||||
|
||||
QLineEdit {
|
||||
border-radius: 2px;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
selection-background-color: rgb(5, 127, 255);
|
||||
background: rgb(4, 4, 4);
|
||||
|
||||
}
|
||||
|
||||
QDoubleSpinBox {
|
||||
border-radius: 2px;
|
||||
selection-background-color: rgb(5, 127, 255);
|
||||
background: rgb(4, 4, 4);
|
||||
|
||||
}
|
||||
|
||||
QSpinBox {
|
||||
border-radius: 2px;
|
||||
selection-background-color: rgb(5, 127, 255);
|
||||
background: rgb(4, 4, 4);
|
||||
|
||||
}
|
||||
|
||||
QComboBox {
|
||||
background-color: #2c2d2f;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollArea {
|
||||
border: 2px solid rgb(9, 10, 12);
|
||||
}
|
||||
|
||||
QFrame#background {
|
||||
border: 2px solid rgb(9, 10, 12);
|
||||
}
|
||||
|
||||
QListWidget {
|
||||
background-color: rgb(15, 15, 15);
|
||||
}
|
||||
|
||||
QListWidget[Log=true] {
|
||||
background-color: rgb(15, 15, 15);
|
||||
background-image: url("PoseWrangler:epic.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom right;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
QFrame {
|
||||
border-radius: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
QTreeWidget QHeaderView:section {
|
||||
border-style: none;
|
||||
padding: 8px;
|
||||
color: rgb(0, 255, 0);
|
||||
background-color: #1e2126;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
border-radius: 2px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(40, 40, 40);
|
||||
}
|
||||
|
||||
|
||||
QPushButton:hover {
|
||||
background: rgb(100, 100, 100);
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background: rgb(41, 164, 255);
|
||||
}
|
||||
|
||||
|
||||
QProgressBar {
|
||||
background: rgb(50, 50, 50);
|
||||
color: rgb(10, 10, 10);
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgb(100, 100, 100);
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
border-radius: 5px;
|
||||
background: QLinearGradient(
|
||||
x1:1,
|
||||
y1: 0,
|
||||
x2: 0,
|
||||
y2: 0,
|
||||
stop: 1 rgb(41, 164, 255),
|
||||
stop: 0.4 rgb(5, 127, 255));
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background: rgb(100, 100, 100);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::sub-page:vertical {
|
||||
border-radius: -5px;
|
||||
background: rgb(15, 15, 15);
|
||||
}
|
||||
|
||||
QScrollBar::add-page:vertical {
|
||||
border-radius: -5px;
|
||||
background: rgb(15, 15, 15);
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background: rgb(100, 100, 100);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::sub-page:horizontal {
|
||||
border-radius: -5px;
|
||||
background: rgb(15, 15, 15);
|
||||
}
|
||||
|
||||
QScrollBar::add-page:horizontal {
|
||||
border-radius: -5px;
|
||||
background: rgb(15, 15, 15);
|
||||
}
|
||||
|
||||
QToolTip {
|
||||
border-radius: 10px;
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(10, 10, 10);
|
||||
padding: 5px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
QCheckBox {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
QCheckBox::indicator {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
QGroupBox {
|
||||
border: 0px solid rgb(100, 100, 100);
|
||||
border-radius: 0px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
QLabel[heading=true] {
|
||||
background-color: rgb(50, 50, 50);
|
||||
border-radius: 0px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
QPushButton[Category=true] {
|
||||
text-align: left;
|
||||
background-color: #323232;
|
||||
}
|
||||
|
||||
QPushButton::hover[Category=true] {
|
||||
text-align: left;
|
||||
background-color: #323232;
|
||||
}
|
||||
|
||||
QPushButton[LogButton=true] {
|
||||
background-color: #323232;
|
||||
}
|
||||
|
||||
QPushButton::hover[LogButton=true] {
|
||||
background-color: #474646;
|
||||
}
|
||||
|
||||
QPushButton::checked[LogButton=true] {
|
||||
background-color: #202328;
|
||||
}
|
45
Scripts/Animation/epic_pose_wrangler/v2/view/ui_context.py
Normal file
45
Scripts/Animation/epic_pose_wrangler/v2/view/ui_context.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
class PoseWranglerUIContext(object):
|
||||
def __init__(
|
||||
self, current_solvers, current_poses, current_drivers, current_driven, solvers, poses, drivers, driven
|
||||
):
|
||||
self._current_solvers = current_solvers
|
||||
self._current_poses = current_poses
|
||||
self._current_drivers = current_drivers
|
||||
self._current_driven = current_driven
|
||||
self._solvers = solvers
|
||||
self._poses = poses
|
||||
self._drivers = drivers
|
||||
self._driven = driven
|
||||
|
||||
@property
|
||||
def current_solvers(self):
|
||||
return self._current_solvers
|
||||
|
||||
@property
|
||||
def current_poses(self):
|
||||
return self._current_poses
|
||||
|
||||
@property
|
||||
def current_drivers(self):
|
||||
return self._current_drivers
|
||||
|
||||
@property
|
||||
def current_driven(self):
|
||||
return self._current_driven
|
||||
|
||||
@property
|
||||
def solvers(self):
|
||||
return self._solvers
|
||||
|
||||
@property
|
||||
def poses(self):
|
||||
return self._poses
|
||||
|
||||
@property
|
||||
def drivers(self):
|
||||
return self._drivers
|
||||
|
||||
@property
|
||||
def driven(self):
|
||||
return self._driven
|
@ -0,0 +1,45 @@
|
||||
# Copyright Epic Games, Inc. All Rights Reserved.
|
||||
from PySide2 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class CategoryWidget(QtWidgets.QWidget):
|
||||
def __init__(self, name):
|
||||
super(CategoryWidget, self).__init__()
|
||||
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout = QtWidgets.QVBoxLayout()
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
self._category_button = QtWidgets.QPushButton(name)
|
||||
font = self._category_button.font()
|
||||
font.setBold(True)
|
||||
font.setPointSize(10)
|
||||
self._category_button.setFont(font)
|
||||
self._category_button.setIcon(QtGui.QIcon(QtGui.QPixmap("PoseWrangler:frame_open.png")))
|
||||
self._category_button.setIconSize(QtCore.QSize(16, 16))
|
||||
self._category_button.clicked.connect(self._toggle_category_visibility)
|
||||
self._category_button.setCheckable(True)
|
||||
self._category_button.setChecked(True)
|
||||
self._category_button.setProperty("Category", True)
|
||||
main_layout.addWidget(self._category_button)
|
||||
|
||||
self._category_container = QtWidgets.QWidget()
|
||||
self._category_container.setContentsMargins(0, 0, 0, 0)
|
||||
self._category_layout = QtWidgets.QVBoxLayout()
|
||||
self._category_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._category_layout.setSpacing(0)
|
||||
self._category_container.setLayout(self._category_layout)
|
||||
main_layout.addWidget(self._category_container)
|
||||
|
||||
def _toggle_category_visibility(self):
|
||||
self._category_container.setVisible(self._category_button.isChecked())
|
||||
self._category_button.setIcon(
|
||||
QtGui.QIcon(QtGui.QPixmap("PoseWrangler:frame_open.png"))
|
||||
if self._category_button.isChecked() else QtGui.QIcon(
|
||||
QtGui.QPixmap("PoseWrangler:frame_closed.png")
|
||||
)
|
||||
)
|
||||
|
||||
def add_extension(self, widget):
|
||||
self._category_layout.addWidget(widget)
|
Loading…
Reference in New Issue
Block a user