#!/usr/bin/env python # -*- coding: utf-8 -*- """ Tool - BlendShape UI Provides the UI for editing and managing BlendShapes """ import os import sys import maya.cmds as cmds try: from PySide2 import QtCore, QtGui, QtWidgets except ImportError: try: from PySide6 import QtCore, QtGui, QtWidgets except ImportError: from PySide import QtCore, QtGui, QtWidgets # Import configuration import config class BlendShapeWidget(QtWidgets.QWidget): """Widget for editing and managing BlendShapes""" def __init__(self, parent=None): """Initialize the BlendShape widget""" super(BlendShapeWidget, self).__init__(parent) # Initialize UI self._create_widgets() self._create_layouts() self._create_connections() def _create_widgets(self): """Create widgets for the BlendShape interface""" # BlendShape Selection section self.blendshape_group = QtWidgets.QGroupBox("BlendShape Selection") self.blendshape_label = QtWidgets.QLabel("BlendShape Node:") self.blendshape_combo = QtWidgets.QComboBox() self.blendshape_combo.setEditable(True) self.blendshape_combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.refresh_blendshape_button = QtWidgets.QPushButton() self.refresh_blendshape_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "refresh.png"))) self.refresh_blendshape_button.setToolTip("Refresh BlendShape nodes in the scene") self.refresh_blendshape_button.setFixedSize(24, 24) self.create_blendshape_button = QtWidgets.QPushButton("Create BlendShape") self.create_blendshape_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "create.png"))) # Target Mesh section self.target_group = QtWidgets.QGroupBox("Target Mesh") self.target_label = QtWidgets.QLabel("Target Mesh:") self.target_combo = QtWidgets.QComboBox() self.target_combo.setEditable(True) self.target_combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.refresh_target_button = QtWidgets.QPushButton() self.refresh_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "refresh.png"))) self.refresh_target_button.setToolTip("Refresh meshes in the scene") self.refresh_target_button.setFixedSize(24, 24) self.select_target_button = QtWidgets.QPushButton("Select Target") self.select_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "select.png"))) # BlendShape Targets section self.targets_group = QtWidgets.QGroupBox("BlendShape Targets") self.targets_tree = QtWidgets.QTreeWidget() self.targets_tree.setHeaderLabels(["Target Name", "Weight", "Status"]) self.targets_tree.setColumnWidth(0, 200) self.targets_tree.setColumnWidth(1, 100) self.targets_tree.setAlternatingRowColors(True) # Target Controls section self.controls_group = QtWidgets.QGroupBox("Target Controls") self.add_target_button = QtWidgets.QPushButton("Add Target") self.add_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "add.png"))) self.remove_target_button = QtWidgets.QPushButton("Remove Target") self.remove_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "remove.png"))) self.edit_target_button = QtWidgets.QPushButton("Edit Target") self.edit_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "edit.png"))) self.duplicate_target_button = QtWidgets.QPushButton("Duplicate Target") self.duplicate_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "duplicate.png"))) # Batch Operations section self.batch_group = QtWidgets.QGroupBox("Batch Operations") self.export_all_button = QtWidgets.QPushButton("Export All Targets") self.export_all_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "export.png"))) self.import_targets_button = QtWidgets.QPushButton("导入目标") self.import_targets_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "import.png"))) self.connect_to_rig_button = QtWidgets.QPushButton("连接到控制器") self.connect_to_rig_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "connect.png"))) def _create_layouts(self): """Create layouts for the BlendShape interface""" # BlendShape Selection layout blendshape_layout = QtWidgets.QGridLayout() blendshape_layout.addWidget(self.blendshape_label, 0, 0) blendshape_layout.addWidget(self.blendshape_combo, 0, 1) blendshape_layout.addWidget(self.refresh_blendshape_button, 0, 2) blendshape_layout.addWidget(self.create_blendshape_button, 1, 1) self.blendshape_group.setLayout(blendshape_layout) # Target Mesh layout target_layout = QtWidgets.QGridLayout() target_layout.addWidget(self.target_label, 0, 0) target_layout.addWidget(self.target_combo, 0, 1) target_layout.addWidget(self.refresh_target_button, 0, 2) target_layout.addWidget(self.select_target_button, 1, 1) self.target_group.setLayout(target_layout) # BlendShape Targets layout targets_layout = QtWidgets.QVBoxLayout() targets_layout.addWidget(self.targets_tree) self.targets_group.setLayout(targets_layout) # Target Controls layout controls_layout = QtWidgets.QGridLayout() controls_layout.addWidget(self.add_target_button, 0, 0) controls_layout.addWidget(self.remove_target_button, 0, 1) controls_layout.addWidget(self.edit_target_button, 1, 0) controls_layout.addWidget(self.duplicate_target_button, 1, 1) self.controls_group.setLayout(controls_layout) # Batch Operations layout batch_layout = QtWidgets.QHBoxLayout() batch_layout.addWidget(self.export_all_button) batch_layout.addWidget(self.import_targets_button) batch_layout.addWidget(self.connect_to_rig_button) self.batch_group.setLayout(batch_layout) # Main layout main_layout = QtWidgets.QVBoxLayout() main_layout.addWidget(self.blendshape_group) main_layout.addWidget(self.target_group) main_layout.addWidget(self.targets_group) main_layout.addWidget(self.controls_group) main_layout.addWidget(self.batch_group) self.setLayout(main_layout) def _create_connections(self): """Create signal/slot connections for the BlendShape interface""" # Connect buttons self.refresh_blendshape_button.clicked.connect(self._refresh_blendshapes) self.create_blendshape_button.clicked.connect(self._create_blendshape) self.refresh_target_button.clicked.connect(self._refresh_targets) self.select_target_button.clicked.connect(self._select_target) self.add_target_button.clicked.connect(self._add_target) self.remove_target_button.clicked.connect(self._remove_target) self.edit_target_button.clicked.connect(self._edit_target) self.duplicate_target_button.clicked.connect(self._duplicate_target) self.export_all_button.clicked.connect(self._export_all_targets) self.import_targets_button.clicked.connect(self._import_targets) self.connect_to_rig_button.clicked.connect(self._connect_to_rig) # Connect combo boxes self.blendshape_combo.currentIndexChanged.connect(self._on_blendshape_changed) self.target_combo.currentIndexChanged.connect(self._on_target_changed) # Connect tree widget self.targets_tree.itemClicked.connect(self._on_target_selected) self.targets_tree.itemDoubleClicked.connect(self._on_target_double_clicked) # Initial refresh self._refresh_blendshapes() self._refresh_targets() def _refresh_blendshapes(self): """Refresh the list of BlendShape nodes in the scene""" current_text = self.blendshape_combo.currentText() self.blendshape_combo.clear() # Get all BlendShape nodes in the scene blendshapes = cmds.ls(type="blendShape") blendshapes.sort() self.blendshape_combo.addItems(blendshapes) # Restore previous selection if possible index = self.blendshape_combo.findText(current_text) if index >= 0: self.blendshape_combo.setCurrentIndex(index) # Update targets tree self._update_targets_tree() def _create_blendshape(self): """Create a new BlendShape node""" target = self.target_combo.currentText() if not target: QtWidgets.QMessageBox.warning( self, "Warning", "请先选择一个目标网格" ) return # Get a name for the new BlendShape node name, ok = QtWidgets.QInputDialog.getText( self, "创建 BlendShape", "BlendShape 名称:", QtWidgets.QLineEdit.Normal, f"{target}_blendShape" ) if ok and name: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.create_blendshape(target, name) if result: # Refresh BlendShape list self._refresh_blendshapes() # Select the new BlendShape index = self.blendshape_combo.findText(name) if index >= 0: self.blendshape_combo.setCurrentIndex(index) QtWidgets.QMessageBox.information( self, "Success", f"成功创建 BlendShape: {name}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"创建 BlendShape 失败: {name}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"创建 BlendShape 出错: {str(e)}" ) def _refresh_targets(self): """Refresh the list of target meshes in the scene""" current_text = self.target_combo.currentText() self.target_combo.clear() # Get all mesh shapes in the scene meshes = cmds.ls(type="mesh") mesh_transforms = [] for mesh in meshes: # Get the transform node for the mesh transform = cmds.listRelatives(mesh, parent=True) if transform: mesh_transforms.append(transform[0]) # Add unique mesh transforms to the combo box unique_transforms = list(set(mesh_transforms)) unique_transforms.sort() self.target_combo.addItems(unique_transforms) # Restore previous selection if possible index = self.target_combo.findText(current_text) if index >= 0: self.target_combo.setCurrentIndex(index) def _select_target(self): """Select the current target mesh in the scene""" target = self.target_combo.currentText() if target: cmds.select(target) def _on_blendshape_changed(self, index): """Handle BlendShape combo box change""" if index >= 0: # Update targets tree self._update_targets_tree() def _on_target_changed(self, index): """Handle target combo box change""" if index >= 0: target = self.target_combo.itemText(index) # Select the target in the scene cmds.select(target) def _update_targets_tree(self): """Update the targets tree with current BlendShape information""" self.targets_tree.clear() blendshape = self.blendshape_combo.currentText() if not blendshape: return # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: # Get targets information targets_info = blendshape_utils.get_targets_info(blendshape) if targets_info: # Add targets to tree for target_name, target_info in targets_info.items(): weight = target_info.get("weight", 0.0) status = target_info.get("status", "正常") item = QtWidgets.QTreeWidgetItem(self.targets_tree, [ target_name, str(weight), status ]) # Create weight slider slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) slider.setRange(0, 100) slider.setValue(int(weight * 100)) slider.setProperty("target_name", target_name) slider.valueChanged.connect(self._on_weight_changed) self.targets_tree.setItemWidget(item, 1, slider) except Exception as e: print(f"更新目标树出错: {str(e)}") def _on_target_selected(self, item, column): """Handle target tree item selection""" target_name = item.text(0) blendshape = self.blendshape_combo.currentText() if blendshape and target_name: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: # Select the target in the scene if it exists as a separate mesh target_mesh = blendshape_utils.get_target_mesh(blendshape, target_name) if target_mesh: cmds.select(target_mesh) except Exception: pass def _on_target_double_clicked(self, item, column): """Handle target tree item double click""" if column == 0: # Target name column self._edit_target() def _on_weight_changed(self, value): """Handle weight slider change""" sender = self.sender() if not sender: return target_name = sender.property("target_name") blendshape = self.blendshape_combo.currentText() if blendshape and target_name: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: weight = value / 100.0 blendshape_utils.set_target_weight(blendshape, target_name, weight) except Exception as e: print(f"设置目标权重出错: {str(e)}") def _add_target(self): """Add a new target to the BlendShape""" blendshape = self.blendshape_combo.currentText() if not blendshape: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a BlendShape node first" ) return # Get selected mesh selection = cmds.ls(selection=True) if not selection: QtWidgets.QMessageBox.warning( self, "Warning", "请先选择一个网格模型作为目标" ) return target_mesh = selection[0] # Get a name for the new target name, ok = QtWidgets.QInputDialog.getText( self, "添加目标", "目标名称:", QtWidgets.QLineEdit.Normal, f"Target_{len(cmds.blendShape(blendshape, query=True, target=True) or [])}" ) if ok and name: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.add_target(blendshape, target_mesh, name) if result: # Update targets tree self._update_targets_tree() QtWidgets.QMessageBox.information( self, "Success", f"成功添加目标: {name}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"添加目标失败: {name}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"添加目标出错: {str(e)}" ) def _remove_target(self): """Remove a target from the BlendShape""" blendshape = self.blendshape_combo.currentText() if not blendshape: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a BlendShape node first" ) return # Get selected target selected_items = self.targets_tree.selectedItems() if not selected_items: QtWidgets.QMessageBox.warning( self, "Warning", "请先在目标列表中选择一个目标" ) return target_name = selected_items[0].text(0) # Confirm removal result = QtWidgets.QMessageBox.question( self, "确认", f"确定要移除目标 {target_name} 吗?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No ) if result == QtWidgets.QMessageBox.Yes: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.remove_target(blendshape, target_name) if result: # Update targets tree self._update_targets_tree() QtWidgets.QMessageBox.information( self, "Success", f"成功移除目标: {target_name}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"移除目标失败: {target_name}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"移除目标出错: {str(e)}" ) def _edit_target(self): """Edit a target in the BlendShape""" blendshape = self.blendshape_combo.currentText() if not blendshape: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a BlendShape node first" ) return # Get selected target selected_items = self.targets_tree.selectedItems() if not selected_items: QtWidgets.QMessageBox.warning( self, "Warning", "请先在目标列表中选择一个目标" ) return target_name = selected_items[0].text(0) # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.edit_target(blendshape, target_name) if result: QtWidgets.QMessageBox.information( self, "Success", f"成功编辑目标: {target_name}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"编辑目标失败: {target_name}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"编辑目标出错: {str(e)}" ) def _duplicate_target(self): """Duplicate a target in the BlendShape""" blendshape = self.blendshape_combo.currentText() if not blendshape: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a BlendShape node first" ) return # Get selected target selected_items = self.targets_tree.selectedItems() if not selected_items: QtWidgets.QMessageBox.warning( self, "Warning", "请先在目标列表中选择一个目标" ) return target_name = selected_items[0].text(0) # Get a name for the new target new_name, ok = QtWidgets.QInputDialog.getText( self, "复制目标", "新目标名称:", QtWidgets.QLineEdit.Normal, f"{target_name}_copy" ) if ok and new_name: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.duplicate_target(blendshape, target_name, new_name) if result: # Update targets tree self._update_targets_tree() QtWidgets.QMessageBox.information( self, "Success", f"成功复制目标 {target_name} 为 {new_name}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"复制目标失败: {target_name}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"复制目标出错: {str(e)}" ) def _export_all_targets(self): """Export all targets to separate files""" blendshape = self.blendshape_combo.currentText() if not blendshape: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a BlendShape node first" ) return dir_path = QtWidgets.QFileDialog.getExistingDirectory( self, "选择导出目录", "" ) if dir_path: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.export_all_targets(blendshape, dir_path) if result: QtWidgets.QMessageBox.information( self, "Success", f"Successfully exported all targets to: {dir_path}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"Failed to export targets: {dir_path}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error exporting targets: {str(e)}" ) def _import_targets(self): """Import targets from files""" blendshape = self.blendshape_combo.currentText() if not blendshape: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a BlendShape node first" ) return file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames( self, "Select Target Files", "", "Maya Files (*.ma *.mb);;OBJ Files (*.obj);;All Files (*.*)" ) if file_paths: # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.import_targets(blendshape, file_paths) if result: # Update targets tree self._update_targets_tree() QtWidgets.QMessageBox.information( self, "Success", f"Successfully imported {len(file_paths)} targets" ) else: QtWidgets.QMessageBox.warning( self, "Warning", "Failed to import targets" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error importing targets: {str(e)}" ) def _connect_to_rig(self): """Connect BlendShape targets to rig controls""" blendshape = self.blendshape_combo.currentText() if not blendshape: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a BlendShape node first" ) return # Get the rig control selection = cmds.ls(selection=True) if not selection: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a controller first" ) return control = selection[0] # Import the blendshape utils here to avoid circular imports from utils.blendshape_utils import BlendShapeUtils blendshape_utils = BlendShapeUtils() try: result = blendshape_utils.connect_to_rig(blendshape, control) if result: QtWidgets.QMessageBox.information( self, "Success", f"Successfully connected {blendshape} to {control}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"Failed to connect to controller: {control}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error connecting to controller: {str(e)}" )