#!/usr/bin/env python # -*- coding: utf-8 -*- #===================================== 1. Module Imports ===================================== import maya.cmds as cmds import maya.mel as mel import sys import os from scripts.config import data try: from PySide2 import QtCore, QtGui, QtWidgets from shiboken2 import wrapInstance print("从PySide2加载Qt和shiboken2") except ImportError: try: from PySide6 import QtCore, QtGui, QtWidgets from shiboken6 import wrapInstance print("从PySide6加载Qt和shiboken6") except ImportError: try: from PySide import QtCore, QtGui, QtWidgets from shiboken import wrapInstance print("从PySide加载Qt和shiboken") except ImportError as e: print(f"Qt加载失败: {str(e)}") QtCore = QtGui = QtWidgets = None wrapInstance = None #===================================== 2. Base Widget Class ===================================== class BaseWidget(QtWidgets.QWidget): """基础控件类""" def __init__(self, parent=None): super(BaseWidget, self).__init__(parent) self.setup_ui() def setup_ui(self): """设置UI""" pass class SearchLineEdit(QtWidgets.QLineEdit): """搜索输入框""" def __init__(self, parent=None): super(SearchLineEdit, self).__init__(parent) self.setup_ui() def setup_ui(self): self.setPlaceholderText("搜索...") self.setClearButtonEnabled(True) self.setMinimumWidth(200) class LoadButton(QtWidgets.QPushButton): """加载按钮""" def __init__(self, text="", icon_name="target.png", parent=None): super(LoadButton, self).__init__(parent) self.setup_ui(text, icon_name) def setup_ui(self, text, icon_name): if text: self.setText(text) icon_path = os.path.join(data.ICONS_PATH, icon_name) if os.path.exists(icon_path): self.setIcon(QtGui.QIcon(icon_path)) self.setFixedSize(25, 25) class ModelInput(QtWidgets.QWidget): """模型输入组件""" def __init__(self, label="", parent=None): super(ModelInput, self).__init__(parent) self.label = label self.setup_ui() def setup_ui(self): layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) # 标签 if self.label: label = QtWidgets.QLabel(self.label) layout.addWidget(label) # 输入框 self.line_edit = QtWidgets.QLineEdit() layout.addWidget(self.line_edit) # 加载按钮 self.load_btn = LoadButton() layout.addWidget(self.load_btn) class LODGroup(QtWidgets.QGroupBox): """LOD分组""" def __init__(self, lod_index, parent=None): super(LODGroup, self).__init__(parent) self.lod_index = lod_index self.setup_ui() def setup_ui(self): self.setTitle(f"LOD{self.lod_index}") # 主布局 main_layout = QtWidgets.QVBoxLayout(self) # 顶部工具栏 toolbar = QtWidgets.QHBoxLayout() toolbar.addStretch() # 删除按钮 delete_btn = LoadButton(icon_name="delete.png") delete_btn.clicked.connect(lambda: self.delete_lod()) toolbar.addWidget(delete_btn) main_layout.addLayout(toolbar) # 模型输入列表 self.model_list = QtWidgets.QVBoxLayout() self.add_model_inputs() main_layout.addLayout(self.model_list) def add_model_inputs(self): """添加模型输入组件""" # 根据LOD级别添加不同的输入组件 model_types = self.get_model_types() for model_type, label in model_types: input_widget = ModelInput(label) input_widget.load_btn.clicked.connect( lambda checked, t=model_type: self.load_model(t)) self.model_list.addWidget(input_widget) def get_model_types(self): """获取当前LOD级别需要的模型类型""" # 基础类型 types = [ ("head", "头部"), ("teeth", "牙齿") ] # 根据LOD级别添加额外类型 if self.lod_index <= 3: types.extend([ ("eye_l", "左眼"), ("eye_r", "右眼"), ("iris", "虹膜"), ("eyelash", "睫毛"), ("eyelid", "眼睑") ]) if self.lod_index <= 2: types.append(("gums", "牙龈")) if self.lod_index <= 1: types.append(("cartilage", "软骨")) if self.lod_index <= 2: types.append(("body", "身体")) return types def load_model(self, model_type): """加载模型""" from scripts.utils import model_utils model_utils.load_model(self.lod_index, model_type) def delete_lod(self): """删除当前LOD""" from scripts.utils import model_utils model_utils.delete_lod(self.lod_index) class IconButton(QtWidgets.QPushButton): """图标按钮""" def __init__(self, icon_name, text=""): super(IconButton, self).__init__() self.setText(text) if icon_name: icon_path = os.path.join(data.ICONS_PATH, icon_name) if os.path.exists(icon_path): self.setIcon(QtGui.QIcon(icon_path)) self.setIconSize(QtCore.QSize(24, 24)) # 设置图标大小 self.setMinimumHeight(32) # 设置最小高度 # 设置文字在图标右侧 self.setStyleSheet(""" QPushButton { text-align: left; padding-left: 5px; } """) class SliderWithValue(QtWidgets.QWidget): """带数值显示的滑块""" valueChanged = QtCore.Signal(float) def __init__(self, min_val=0.0, max_val=1.0, default=0.0, parent=None): super(SliderWithValue, self).__init__(parent) self.min_val = min_val self.max_val = max_val self.default = default self.setup_ui() def setup_ui(self): layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # 滑块 self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.slider.setRange(0, 100) self.slider.setValue(self.to_slider_value(self.default)) self.slider.valueChanged.connect(self.on_slider_changed) layout.addWidget(self.slider) # 数值显示 self.value_spin = QtWidgets.QDoubleSpinBox() self.value_spin.setRange(self.min_val, self.max_val) self.value_spin.setValue(self.default) self.value_spin.setSingleStep(0.01) self.value_spin.valueChanged.connect(self.on_spin_changed) layout.addWidget(self.value_spin) def to_slider_value(self, value): """转换为滑块值""" return int((value - self.min_val) * 100 / (self.max_val - self.min_val)) def to_real_value(self, slider_value): """转换为实际值""" return self.min_val + slider_value * (self.max_val - self.min_val) / 100 def on_slider_changed(self, value): real_value = self.to_real_value(value) self.value_spin.setValue(real_value) self.valueChanged.emit(real_value) def on_spin_changed(self, value): self.slider.setValue(self.to_slider_value(value)) self.valueChanged.emit(value) class DNABrowser(QtWidgets.QWidget): """DNA浏览器""" dnaSelected = QtCore.Signal(str) # 发送选中的DNA文件路径 def __init__(self, parent=None): super(DNABrowser, self).__init__(parent) self.setup_ui() self.load_dna_files() def setup_ui(self): layout = QtWidgets.QVBoxLayout(self) # 工具栏 toolbar = QtWidgets.QHBoxLayout() # 缩放滑块 self.zoom_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.zoom_slider.setRange(50, 200) self.zoom_slider.setValue(100) self.zoom_slider.valueChanged.connect(self.on_zoom_changed) toolbar.addWidget(QtWidgets.QLabel("缩放:")) toolbar.addWidget(self.zoom_slider) layout.addLayout(toolbar) # DNA网格视图 self.grid_view = QtWidgets.QListView() self.grid_view.setViewMode(QtWidgets.QListView.IconMode) self.grid_view.setIconSize(QtCore.QSize(100, 100)) self.grid_view.setSpacing(10) self.grid_view.setResizeMode(QtWidgets.QListView.Adjust) self.grid_view.clicked.connect(self.on_dna_selected) self.model = QtGui.QStandardItemModel() self.grid_view.setModel(self.model) layout.addWidget(self.grid_view) def load_dna_files(self): """加载DNA文件""" self.model.clear() # 从DNA目录加载文件 dna_path = data.DNA_FILE_PATH img_path = data.DNA_IMG_PATH if os.path.exists(dna_path): for file in os.listdir(dna_path): if file.endswith(".dna"): # 创建项 item = QtGui.QStandardItem() item.setText(os.path.splitext(file)[0]) item.setData(os.path.join(dna_path, file), QtCore.Qt.UserRole) # 设置图标 img_file = os.path.join(img_path, f"{os.path.splitext(file)[0]}.png") if os.path.exists(img_file): item.setIcon(QtGui.QIcon(img_file)) self.model.appendRow(item) def on_zoom_changed(self, value): """缩放改变""" size = value self.grid_view.setIconSize(QtCore.QSize(size, size)) def on_dna_selected(self, index): """DNA选中""" item = self.model.itemFromIndex(index) dna_path = item.data(QtCore.Qt.UserRole) self.dnaSelected.emit(dna_path) class DescriptionWidget(QtWidgets.QGroupBox): """描述信息组件""" def __init__(self, parent=None): super(DescriptionWidget, self).__init__("描述", parent) self.setup_ui() def setup_ui(self): layout = QtWidgets.QFormLayout(self) # 名称 self.name_edit = QtWidgets.QLineEdit() layout.addRow("名称:", self.name_edit) # 原型 self.prototype_combo = QtWidgets.QComboBox() self.prototype_combo.addItems(["亚洲人", "黑人", "高加素人", "拉美裔", "外星人", "其他"]) layout.addRow("原型:", self.prototype_combo) # 性别 self.gender_combo = QtWidgets.QComboBox() self.gender_combo.addItems(["男性", "女性", "其他"]) layout.addRow("性别:", self.gender_combo) # 年龄 self.age_spin = QtWidgets.QSpinBox() self.age_spin.setRange(0, 100) self.age_spin.setValue(24) layout.addRow("年龄:", self.age_spin) # 变换单位 self.unit_combo = QtWidgets.QComboBox() self.unit_combo.addItems(["厘米", "米"]) layout.addRow("变换单位:", self.unit_combo) # 旋转单位 self.rotation_combo = QtWidgets.QComboBox() self.rotation_combo.addItems(["角度", "弧度"]) layout.addRow("旋转单位:", self.rotation_combo) # 坐标向上 self.up_combo = QtWidgets.QComboBox() self.up_combo.addItems(["Y轴向上", "Z轴向上"]) layout.addRow("坐标向上:", self.up_combo) # LOD计数 self.lod_spin = QtWidgets.QSpinBox() self.lod_spin.setRange(1, 8) self.lod_spin.setValue(8) layout.addRow("LOD计数:", self.lod_spin) class BlendShapeList(QtWidgets.QWidget): """BlendShape列表组件""" def __init__(self, title="BlendShapes", parent=None): super(BlendShapeList, self).__init__(parent) self.title = title self.setup_ui() def setup_ui(self): layout = QtWidgets.QVBoxLayout(self) # 标题栏 title_layout = QtWidgets.QHBoxLayout() title_label = QtWidgets.QLabel(self.title) self.count_label = QtWidgets.QLabel("[0/0]") title_layout.addWidget(title_label) title_layout.addWidget(self.count_label) title_layout.addStretch() layout.addLayout(title_layout) # 搜索栏 self.search_edit = SearchLineEdit() self.search_edit.textChanged.connect(self.filter_items) layout.addWidget(self.search_edit) # BS列表 self.list_widget = QtWidgets.QListWidget() self.list_widget.setSelectionMode(QtWidgets.QListWidget.ExtendedSelection) layout.addWidget(self.list_widget) def filter_items(self, text): """过滤列表项""" for i in range(self.list_widget.count()): item = self.list_widget.item(i) item.setHidden(text.lower() not in item.text().lower()) def update_count(self): """更新计数""" visible = sum(1 for i in range(self.list_widget.count()) if not self.list_widget.item(i).isHidden()) total = self.list_widget.count() self.count_label.setText(f"[{visible}/{total}]") class BlendShapeControls(QtWidgets.QWidget): """BlendShape控制组件""" def __init__(self, parent=None): super(BlendShapeControls, self).__init__(parent) self.setup_ui() def setup_ui(self): layout = QtWidgets.QVBoxLayout(self) # 工具栏 toolbar = QtWidgets.QHBoxLayout() # 恢复表情按钮 reset_btn = IconButton("reset.png", "恢复表情") reset_btn.clicked.connect(self.reset_blendshapes) toolbar.addWidget(reset_btn) # 混合筛选按钮 filter_btn = IconButton("blendRaw.png", "混合筛选") filter_btn.clicked.connect(self.filter_blendshapes) toolbar.addWidget(filter_btn) # 分类按钮组 for i in range(1, 7): btn = QtWidgets.QPushButton(str(i) if i > 1 else "全部") btn.setCheckable(True) btn.setFixedWidth(40) toolbar.addWidget(btn) layout.addLayout(toolbar) # 数值控制 value_layout = QtWidgets.QHBoxLayout() self.value_slider = SliderWithValue(min_val=0.0, max_val=1.0, default=0.01) value_layout.addWidget(self.value_slider) self.all_check = QtWidgets.QCheckBox("全部") value_layout.addWidget(self.all_check) layout.addLayout(value_layout) # 范围控制 range_layout = QtWidgets.QHBoxLayout() range_plus = QtWidgets.QPushButton("范围 +") range_minus = QtWidgets.QPushButton("范围 -") range_layout.addWidget(range_plus) range_layout.addWidget(range_minus) layout.addLayout(range_layout) def reset_blendshapes(self): """重置所有BlendShape""" from scripts.utils import adjust_utils adjust_utils.reset_blendshapes() def filter_blendshapes(self): """过滤BlendShape""" from scripts.utils import adjust_utils adjust_utils.filter_blendshapes() class BlendShapeTools(QtWidgets.QWidget): """BlendShape工具组件""" def __init__(self, parent=None): super(BlendShapeTools, self).__init__(parent) self.setup_ui() def setup_ui(self): layout = QtWidgets.QVBoxLayout(self) # 工具按钮 tools_layout = QtWidgets.QHBoxLayout() # 翻转 flip_btn = IconButton("mirrorL.png", "翻转") tools_layout.addWidget(flip_btn) # 镜像目标 mirror_btn = IconButton("symmetry.png", "镜像目标") tools_layout.addWidget(mirror_btn) # 查找翻转目标 find_flip_btn = IconButton("mirrorR.png", "查找翻转目标") tools_layout.addWidget(find_flip_btn) # 添加混合目标 add_bs_btn = IconButton("blendShape.png", "添加混合目标") tools_layout.addWidget(add_bs_btn) # 删除混合目标 del_bs_btn = IconButton("blendShape.png", "删除混合目标") tools_layout.addWidget(del_bs_btn) # 批量混合目标 batch_bs_btn = IconButton("blendShape.png", "批量混合目标") tools_layout.addWidget(batch_bs_btn) layout.addLayout(tools_layout) # 功能开关 toggle_layout = QtWidgets.QHBoxLayout() toggles = [ ("PSD", "psd.png"), ("BSE", "blendShape.png"), ("KEY", "centerCurrentTime.png"), ("MIR", "mirrorR.png"), ("ARK", "ARKit52.png"), ("CTR", "ctrl_hide.png") ] for text, icon in toggles: btn = IconButton(icon, text) btn.setCheckable(True) toggle_layout.addWidget(btn) layout.addLayout(toggle_layout) # 表情控制 expr_layout = QtWidgets.QHBoxLayout() expr_btns = [ ("还原默认表情", "reset.png"), ("选择选择表情", "expressions_current.png"), ("写入当前表情", "expression.png"), ("控制面板查找", "controller.png"), ("选择关联关节", "kinJoint.png"), ("写入镜像表情", "ctrl_hide.png") ] for text, icon in expr_btns: btn = IconButton(icon, text) expr_layout.addWidget(btn) layout.addLayout(expr_layout) class WeightEditorWidget(QtWidgets.QWidget): """权重编辑器界面""" weightChanged = QtCore.Signal(str, float) # 权重变化信号(目标名称, 权重值) def __init__(self, parent=None): super(WeightEditorWidget, self).__init__(parent) self.bs_node = None self.setup_ui() def setup_ui(self): """创建界面""" layout = QtWidgets.QVBoxLayout(self) # 顶部工具栏 toolbar = QtWidgets.QHBoxLayout() # BlendShape节点选择 self.bs_combo = QtWidgets.QComboBox() self.bs_combo.currentTextChanged.connect(self.on_bs_changed) toolbar.addWidget(self.bs_combo) # 刷新按钮 refresh_btn = IconButton("refresh.png", "刷新") refresh_btn.clicked.connect(self.refresh_weights) toolbar.addWidget(refresh_btn) # 优化按钮 optimize_btn = IconButton("optimize.png", "优化权重") optimize_btn.clicked.connect(self.optimize_weights) toolbar.addWidget(optimize_btn) # 分析按钮 analyze_btn = IconButton("analyze.png", "分析权重") analyze_btn.clicked.connect(self.analyze_weights) toolbar.addWidget(analyze_btn) layout.addLayout(toolbar) # 搜索栏 self.search_edit = SearchLineEdit() self.search_edit.textChanged.connect(self.filter_targets) layout.addWidget(self.search_edit) # 权重列表 self.weight_list = QtWidgets.QTreeWidget() self.weight_list.setHeaderLabels(["目标", "权重"]) self.weight_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.weight_list.itemChanged.connect(self.on_item_changed) layout.addWidget(self.weight_list) # 底部工具栏 bottom_toolbar = QtWidgets.QHBoxLayout() # 批量设置 bottom_toolbar.addWidget(QtWidgets.QLabel("批量设置:")) self.batch_spin = QtWidgets.QDoubleSpinBox() self.batch_spin.setRange(0, 1) self.batch_spin.setSingleStep(0.1) bottom_toolbar.addWidget(self.batch_spin) apply_btn = IconButton("apply.png", "应用") apply_btn.clicked.connect(self.apply_batch_weight) bottom_toolbar.addWidget(apply_btn) bottom_toolbar.addStretch() # 快捷按钮 for value in [0, 0.5, 1.0]: btn = QtWidgets.QPushButton(str(value)) btn.clicked.connect(lambda x, v=value: self.quick_set_weight(v)) bottom_toolbar.addWidget(btn) layout.addLayout(bottom_toolbar) def set_blendshape(self, bs_node): """设置BlendShape节点""" self.bs_node = bs_node self.refresh_weights() def refresh_weights(self): """刷新权重列表""" from scripts.utils import adjust_utils self.weight_list.clear() if not self.bs_node: return # 获取所有目标 targets = adjust_utils.bs_manager.get_all_targets(self.bs_node) # 添加目标项 for target in targets: weight = adjust_utils.bs_manager.get_weight(self.bs_node, target) item = QtWidgets.QTreeWidgetItem([target, str(weight)]) item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) self.weight_list.addTopLevelItem(item) def filter_targets(self, text): """过滤目标""" for i in range(self.weight_list.topLevelItemCount()): item = self.weight_list.topLevelItem(i) item.setHidden(text.lower() not in item.text(0).lower()) def on_item_changed(self, item, column): """项目变化""" if column == 1: # 权重列 try: target = item.text(0) weight = float(item.text(1)) self.weightChanged.emit(target, weight) except ValueError: pass def on_bs_changed(self, bs_node): """BlendShape节点变化""" self.set_blendshape(bs_node) def optimize_weights(self): """优化权重""" from scripts.utils import adjust_utils if self.bs_node: adjust_utils.optimize_blendshape_weights(self.bs_node) self.refresh_weights() def analyze_weights(self): """分析权重""" from scripts.utils import adjust_utils if self.bs_node: results = adjust_utils.analyze_blendshape_weights(self.bs_node) if results: # 显示分析结果 msg = ( f"总目标数: {results['total_targets']}\n" f"活动目标: {results['active_targets']}\n" f"未使用目标: {results['unused_targets']}\n" f"全权重目标: {results['full_weight_targets']}\n" f"部分权重目标: {results['partial_weight_targets']}\n\n" "权重分布:\n" ) for weight, count in sorted(results["weight_distribution"].items()): msg += f" {weight:.1f}: {count}\n" QtWidgets.QMessageBox.information(self, "权重分析", msg) def apply_batch_weight(self): """应用批量权重""" from scripts.utils import adjust_utils weight = self.batch_spin.value() # 获取选中项 items = self.weight_list.selectedItems() if not items: return # 设置权重 for item in items: target = item.text(0) adjust_utils.bs_manager.set_weight(self.bs_node, target, weight) item.setText(1, str(weight)) def quick_set_weight(self, weight): """快速设置权重""" self.batch_spin.setValue(weight) self.apply_batch_weight() class ExpressionPreviewWidget(QtWidgets.QWidget): """表情预览组件""" def __init__(self, parent=None): super(ExpressionPreviewWidget, self).__init__(parent) self.expr_set = None self.setup_ui() def setup_ui(self): """创建界面""" layout = QtWidgets.QVBoxLayout(self) # 顶部工具栏 toolbar = QtWidgets.QHBoxLayout() # 表情集选择 self.expr_combo = QtWidgets.QComboBox() self.expr_combo.currentTextChanged.connect(self.on_expr_changed) toolbar.addWidget(self.expr_combo) # 刷新按钮 refresh_btn = IconButton("refresh.png", "刷新") refresh_btn.clicked.connect(self.refresh_expressions) toolbar.addWidget(refresh_btn) # 播放控制 self.play_btn = IconButton("play.png", "播放") self.play_btn.setCheckable(True) self.play_btn.clicked.connect(self.toggle_play) toolbar.addWidget(self.play_btn) # 循环播放 self.loop_btn = IconButton("loop.png", "循环") self.loop_btn.setCheckable(True) toolbar.addWidget(self.loop_btn) layout.addLayout(toolbar) # 预览区域 preview_group = QtWidgets.QGroupBox("预览") preview_layout = QtWidgets.QVBoxLayout(preview_group) # 时间控制 time_layout = QtWidgets.QHBoxLayout() self.time_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.time_slider.setRange(0, 100) self.time_slider.valueChanged.connect(self.on_time_changed) time_layout.addWidget(self.time_slider) self.time_spin = QtWidgets.QSpinBox() self.time_spin.setRange(0, 100) self.time_spin.valueChanged.connect(self.time_slider.setValue) time_layout.addWidget(self.time_spin) preview_layout.addLayout(time_layout) # 权重控制 weight_layout = QtWidgets.QHBoxLayout() self.weight_slider = SliderWithValue(min_val=0.0, max_val=1.0, default=1.0) self.weight_slider.valueChanged.connect(self.on_weight_changed) weight_layout.addWidget(self.weight_slider) preview_layout.addLayout(weight_layout) layout.addWidget(preview_group) # 表情列表 list_group = QtWidgets.QGroupBox("表情列表") list_layout = QtWidgets.QVBoxLayout(list_group) # 搜索栏 self.search_edit = SearchLineEdit() self.search_edit.textChanged.connect(self.filter_expressions) list_layout.addWidget(self.search_edit) # 表情树 self.expr_tree = QtWidgets.QTreeWidget() self.expr_tree.setHeaderLabels(["表情", "权重"]) self.expr_tree.itemChanged.connect(self.on_item_changed) list_layout.addWidget(self.expr_tree) layout.addWidget(list_group) # 创建定时器 self.play_timer = QtCore.QTimer() self.play_timer.timeout.connect(self.update_time) def set_expression(self, expr_set): """设置表情集""" self.expr_set = expr_set self.refresh_expressions() def refresh_expressions(self): """刷新表情列表""" from scripts.utils import adjust_utils self.expr_tree.clear() if not self.expr_set: return # 获取表情数据 target_data = cmds.getAttr(f"{self.expr_set}.targets") for item in target_split(";"): name, value = item.split(":") tree_item = QtWidgets.QTreeWidgetItem([name, value]) tree_item.setFlags(tree_item.flags() | QtCore.Qt.ItemIsEditable) self.expr_tree.addTopLevelItem(tree_item) def filter_expressions(self, text): """过滤表情""" for i in range(self.expr_tree.topLevelItemCount()): item = self.expr_tree.topLevelItem(i) item.setHidden(text.lower() not in item.text(0).lower()) def on_expr_changed(self, expr_set): """表情集变化""" self.set_expression(expr_set) def on_item_changed(self, item, column): """项目变化""" if column == 1: # 权重列 try: target = item.text(0) weight = float(item.text(1)) self.apply_expression_weight(target, weight) except ValueError: pass def on_time_changed(self, time): """时间变化""" self.time_spin.setValue(time) self.update_preview() def on_weight_changed(self, weight): """权重变化""" self.update_preview() def toggle_play(self, checked): """切换播放状态""" if checked: self.play_timer.start(33) # ~30fps self.play_btn.setIcon(QtGui.QIcon(os.path.join(data.ICONS_PATH, "pause.png"))) else: self.play_timer.stop() self.play_btn.setIcon(QtGui.QIcon(os.path.join(data.ICONS_PATH, "play.png"))) def update_time(self): """更新时间""" current = self.time_slider.value() if current >= self.time_slider.maximum(): if self.loop_btn.isChecked(): self.time_slider.setValue(0) else: self.play_btn.setChecked(False) else: self.time_slider.setValue(current + 1) def update_preview(self): """更新预览""" from scripts.utils import adjust_utils if not self.expr_set: return # 获取当前权重 weight = self.weight_slider.value_spin.value() # 应用表情 adjust_utils.apply_expression_set(self.expr_set, weight) def apply_expression_weight(self, target, weight): """应用表情权重""" from scripts.utils import adjust_utils if not self.expr_set: return # 更新表情集数据 target_data = cmds.getAttr(f"{self.expr_set}.targets") targets = [] for item in target_split(";"): name, value = item.split(":") if name == target: targets.append(f"{name}:{weight}") else: targets.append(item) cmds.setAttr(f"{self.expr_set}.targets", ";".join(targets), type="string") # 更新预览 self.update_preview() class ExpressionCombinationPreview(QtWidgets.QWidget): """表情组合预览组件""" def __init__(self, parent=None): super(ExpressionCombinationPreview, self).__init__(parent) self.combo = None self.setup_ui() def setup_ui(self): """创建界面""" layout = QtWidgets.QVBoxLayout(self) # 顶部工具栏 toolbar = QtWidgets.QHBoxLayout() # 组合选择 self.combo_combo = QtWidgets.QComboBox() self.combo_combo.currentTextChanged.connect(self.on_combo_changed) toolbar.addWidget(self.combo_combo) # 刷新按钮 refresh_btn = IconButton("refresh.png", "刷新") refresh_btn.clicked.connect(self.refresh_combinations) toolbar.addWidget(refresh_btn) # 创建组合 create_btn = IconButton("create.png", "创建组合") create_btn.clicked.connect(self.create_combination) toolbar.addWidget(create_btn) # 删除组合 delete_btn = IconButton("delete.png", "删除组合") delete_btn.clicked.connect(self.delete_combination) toolbar.addWidget(delete_btn) layout.addLayout(toolbar) # 预览区域 preview_group = QtWidgets.QGroupBox("预览") preview_layout = QtWidgets.QVBoxLayout(preview_group) # 总权重控制 weight_layout = QtWidgets.QHBoxLayout() weight_layout.addWidget(QtWidgets.QLabel("总权重:")) self.weight_slider = SliderWithValue(min_val=0.0, max_val=1.0, default=1.0) self.weight_slider.valueChanged.connect(self.on_weight_changed) weight_layout.addWidget(self.weight_slider) preview_layout.addLayout(weight_layout) layout.addWidget(preview_group) # 表情列表 list_group = QtWidgets.QGroupBox("组合表情") list_layout = QtWidgets.QVBoxLayout(list_group) # 搜索栏 self.search_edit = SearchLineEdit() self.search_edit.textChanged.connect(self.filter_expressions) list_layout.addWidget(self.search_edit) # 表情树 self.expr_tree = QtWidgets.QTreeWidget() self.expr_tree.setHeaderLabels(["表情", "权重"]) self.expr_tree.itemChanged.connect(self.on_item_changed) list_layout.addWidget(self.expr_tree) # 表情工具栏 expr_toolbar = QtWidgets.QHBoxLayout() # 添加表情 add_btn = IconButton("add.png", "添加表情") add_btn.clicked.connect(self.add_expression) expr_toolbar.addWidget(add_btn) # 移除表情 remove_btn = IconButton("remove.png", "移除表情") remove_btn.clicked.connect(self.remove_expression) expr_toolbar.addWidget(remove_btn) # 上移 up_btn = IconButton("up.png", "上移") up_btn.clicked.connect(self.move_expression_up) expr_toolbar.addWidget(up_btn) # 下移 down_btn = IconButton("down.png", "下移") down_btn.clicked.connect(self.move_expression_down) expr_toolbar.addWidget(down_btn) list_layout.addLayout(expr_toolbar) layout.addWidget(list_group) def set_combination(self, combo): """设置表情组合""" self.combo = combo self.refresh_combinations() def refresh_combinations(self): """刷新组合列表""" from scripts.utils import adjust_utils self.expr_tree.clear() if not self.combo: return # 获取组合数据 expr_data = cmds.getAttr(f"{self.combo}.expressions") for item in expr_split(";"): expr_set, weight = item.split(":") tree_item = QtWidgets.QTreeWidgetItem([expr_set, weight]) tree_item.setFlags(tree_item.flags() | QtCore.Qt.ItemIsEditable) self.expr_tree.addTopLevelItem(tree_item) def filter_expressions(self, text): """过滤表情""" for i in range(self.expr_tree.topLevelItemCount()): item = self.expr_tree.topLevelItem(i) item.setHidden(text.lower() not in item.text(0).lower()) def on_combo_changed(self, combo): """组合变化""" self.set_combination(combo) def on_item_changed(self, item, column): """项目变化""" if column == 1: # 权重列 try: expr_set = item.text(0) weight = float(item.text(1)) self.update_expression_weight(expr_set, weight) except ValueError: pass def on_weight_changed(self, weight): """总权重变化""" from scripts.utils import adjust_utils if self.combo: adjust_utils.apply_expression_combination(self.combo, weight) def create_combination(self): """创建组合""" name, ok = QtWidgets.QInputDialog.getText( self, "创建组合", "请输入组合名称:") if ok and name: from scripts.utils import adjust_utils combo = adjust_utils.create_expression_combination(name, []) if combo: self.combo_combo.addItem(combo) self.combo_combo.setCurrentText(combo) def delete_combination(self): """删除组合""" if self.combo: reply = QtWidgets.QMessageBox.question( self, "删除组合", f"确定要删除组合 {self.combo} 吗?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No ) if reply == QtWidgets.QMessageBox.Yes: cmds.delete(self.combo) self.combo_combo.removeItem( self.combo_combo.findText(self.combo)) self.combo = None self.refresh_combinations() def add_expression(self): """添加表情""" from scripts.utils import adjust_utils if not self.combo: return # 获取场景中的表情集 expr_sets = cmds.ls(type="objectSet", long=True) expr_sets = [s for s in expr_sets if "_exprSet" in s] if not expr_sets: QtWidgets.QMessageBox.warning( self, "添加表情", "场景中没有可用的表情集") return # 选择表情集 expr_set, ok = QtWidgets.QInputDialog.getItem( self, "添加表情", "选择表情集:", expr_sets, 0, False) if ok and expr_set: # 添加到组合 expr_data = cmds.getAttr(f"{self.combo}.expressions") if expr_data: expr_data += f";{expr_set}:1.0" else: expr_data = f"{expr_set}:1.0" cmds.setAttr(f"{self.combo}.expressions", expr_data, type="string") # 刷新列表 self.refresh_combinations() def remove_expression(self): """移除表情""" items = self.expr_tree.selectedItems() if not items: return # 更新组合数据 expr_data = cmds.getAttr(f"{self.combo}.expressions") exprs = [] for item in expr_split(";"): expr_set, weight = item.split(":") if expr_set not in [i.text(0) for i in items]: exprs.append(item) cmds.setAttr(f"{self.combo}.expressions", ";".join(exprs), type="string") # 刷新列表 self.refresh_combinations() def move_expression_up(self): """上移表情""" item = self.expr_tree.currentItem() if not item: return index = self.expr_tree.indexOfTopLevelItem(item) if index <= 0: return # 移动项目 self.expr_tree.takeTopLevelItem(index) self.expr_tree.insertTopLevelItem(index - 1, item) self.expr_tree.setCurrentItem(item) # 更新组合数据 self.update_expression_order() def move_expression_down(self): """下移表情""" item = self.expr_tree.currentItem() if not item: return index = self.expr_tree.indexOfTopLevelItem(item) if index >= self.expr_tree.topLevelItemCount() - 1: return # 移动项目 self.expr_tree.takeTopLevelItem(index) self.expr_tree.insertTopLevelItem(index + 1, item) self.expr_tree.setCurrentItem(item) # 更新组合数据 self.update_expression_order() def update_expression_order(self): """更新表情顺序""" exprs = [] for i in range(self.expr_tree.topLevelItemCount()): item = self.expr_tree.topLevelItem(i) exprs.append(f"{item.text(0)}:{item.text(1)}") cmds.setAttr(f"{self.combo}.expressions", ";".join(exprs), type="string") def update_expression_weight(self, expr_set, weight): """更新表情权重""" # 更新组合数据 expr_data = cmds.getAttr(f"{self.combo}.expressions") exprs = [] for item in expr_split(";"): name, value = item.split(":") if name == expr_set: exprs.append(f"{name}:{weight}") else: exprs.append(item) cmds.setAttr(f"{self.combo}.expressions", ";".join(exprs), type="string") # 更新预览 self.on_weight_changed(self.weight_slider.value_spin.value()) class WeightCurveEditor(QtWidgets.QWidget): """权重曲线编辑器""" def __init__(self, parent=None): super(WeightCurveEditor, self).__init__(parent) self.curve = None self.setup_ui() def setup_ui(self): """创建界面""" layout = QtWidgets.QVBoxLayout(self) # 顶部工具栏 toolbar = QtWidgets.QHBoxLayout() # 曲线选择 self.curve_combo = QtWidgets.QComboBox() self.curve_combo.currentTextChanged.connect(self.on_curve_changed) toolbar.addWidget(self.curve_combo) # 创建曲线 create_btn = IconButton("create.png", "创建曲线") create_btn.clicked.connect(self.create_curve) toolbar.addWidget(create_btn) # 删除曲线 delete_btn = IconButton("delete.png", "删除曲线") delete_btn.clicked.connect(self.delete_curve) toolbar.addWidget(delete_btn) # 重置曲线 reset_btn = IconButton("reset.png", "重置曲线") reset_btn.clicked.connect(self.reset_curve) toolbar.addWidget(reset_btn) layout.addLayout(toolbar) # 曲线编辑区域 curve_group = QtWidgets.QGroupBox("曲线编辑") curve_layout = QtWidgets.QVBoxLayout(curve_group) # 添加曲线视图 curve_view = self.setup_curve_view() curve_layout.addWidget(curve_view) # 添加关键帧控制 key_controls = self.setup_key_controls() curve_layout.addWidget(key_controls) layout.addWidget(curve_group) def set_curve(self, curve): """设置曲线""" self.curve = curve self.refresh_view() def refresh_view(self): """刷新视图""" self.curve_scene.clear() if not self.curve: return # 绘制网格 self.draw_grid() # 绘制曲线 self.draw_curve() # 绘制关键帧 self.draw_keyframes() def draw_grid(self): """绘制网格""" # 获取视图大小 width = self.curve_view.width() height = self.curve_view.height() # 绘制水平线 for i in range(11): y = height * i / 10 self.curve_scene.addLine(0, y, width, y, QtGui.QPen(QtGui.QColor(100, 100, 100))) # 绘制垂直线 for i in range(11): x = width * i / 10 self.curve_scene.addLine(x, 0, x, height, QtGui.QPen(QtGui.QColor(100, 100, 100))) def draw_curve(self): """绘制曲线""" if not self.curve: return # 获取曲线数据 times = cmds.keyframe(self.curve, q=True, timeChange=True) values = cmds.keyframe(self.curve, q=True, valueChange=True) if not times or not values: return # 创建路径 path = QtGui.QPainterPath() path.moveTo(times[0], values[0]) for i in range(1, len(times)): path.lineTo(times[i], values[i]) # 添加到场景 self.curve_scene.addPath(path, QtGui.QPen(QtGui.QColor(0, 255, 0), 2)) def draw_keyframes(self): """绘制关键帧""" if not self.curve: return # 获取关键帧 times = cmds.keyframe(self.curve, q=True, timeChange=True) values = cmds.keyframe(self.curve, q=True, valueChange=True) if not times or not values: return # 绘制关键帧点 for t, v in zip(times, values): self.curve_scene.addEllipse(t-3, v-3, 6, 6, QtGui.QPen(QtGui.QColor(255, 255, 0)), QtGui.QBrush(QtGui.QColor(255, 255, 0))) def create_curve(self): """创建曲线""" from scripts.utils import adjust_utils name, ok = QtWidgets.QInputDialog.getText( self, "创建曲线", "请输入曲线名称:") if ok and name: curve = adjust_utils.create_weight_curve(name) if curve: self.curve_combo.addItem(curve) self.curve_combo.setCurrentText(curve) def delete_curve(self): """删除曲线""" if self.curve: reply = QtWidgets.QMessageBox.question( self, "删除曲线", f"确定要删除曲线 {self.curve} 吗?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No ) if reply == QtWidgets.QMessageBox.Yes: cmds.delete(self.curve) self.curve_combo.removeItem( self.curve_combo.findText(self.curve)) self.curve = None self.refresh_view() def reset_curve(self): """重置曲线""" if not self.curve: return # 删除所有关键帧 cmds.cutKey(self.curve) # 添加默认关键帧 start = self.start_spin.value() end = self.end_spin.value() cmds.setKeyframe(self.curve, time=start, value=0) cmds.setKeyframe(self.curve, time=end, value=0) self.refresh_view() def on_curve_changed(self, curve): """曲线变化""" self.set_curve(curve) def setup_curve_view(self): """设置曲线视图""" # 创建视图容器 view_container = QtWidgets.QWidget() view_layout = QtWidgets.QVBoxLayout(view_container) view_layout.setContentsMargins(0, 0, 0, 0) # 添加标尺 self.h_ruler = TimeRuler(QtCore.Qt.Horizontal) self.v_ruler = ValueRuler(QtCore.Qt.Vertical) # 创建视图区域 view_area = QtWidgets.QWidget() grid_layout = QtWidgets.QGridLayout(view_area) grid_layout.setSpacing(0) grid_layout.setContentsMargins(0, 0, 0, 0) # 添加标尺和视图 grid_layout.addWidget(self.v_ruler, 1, 0) grid_layout.addWidget(self.curve_view, 1, 1) grid_layout.addWidget(self.h_ruler, 0, 1) view_layout.addWidget(view_area) # 添加缩放控制 zoom_layout = QtWidgets.QHBoxLayout() # 水平缩放 zoom_layout.addWidget(QtWidgets.QLabel("水平缩放:")) self.h_zoom = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.h_zoom.setRange(10, 200) self.h_zoom.setValue(100) self.h_zoom.valueChanged.connect(self.update_view_scale) zoom_layout.addWidget(self.h_zoom) # 垂直缩放 zoom_layout.addWidget(QtWidgets.QLabel("垂直缩放:")) self.v_zoom = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.v_zoom.setRange(10, 200) self.v_zoom.setValue(100) self.v_zoom.valueChanged.connect(self.update_view_scale) zoom_layout.addWidget(self.v_zoom) view_layout.addLayout(zoom_layout) return view_container def update_view_scale(self): """更新视图缩放""" # 获取缩放值 h_scale = self.h_zoom.value() / 100.0 v_scale = self.v_zoom.value() / 100.0 # 更新变换 self.curve_view.setTransform( QtGui.QTransform().scale(h_scale, v_scale)) # 更新标尺 self.h_ruler.setScale(h_scale) self.v_ruler.setScale(v_scale) # 刷新视图 self.refresh_view() def setup_key_controls(self): """设置关键帧控制""" controls = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(controls) layout.setContentsMargins(0, 0, 0, 0) # 关键帧导航 nav_group = QtWidgets.QGroupBox("关键帧导航") nav_layout = QtWidgets.QHBoxLayout(nav_group) prev_key = IconButton("prevKey.png", "上一帧") prev_key.clicked.connect(self.goto_prev_key) nav_layout.addWidget(prev_key) next_key = IconButton("nextKey.png", "下一帧") next_key.clicked.connect(self.goto_next_key) nav_layout.addWidget(next_key) layout.addWidget(nav_group) # 关键帧编辑 edit_group = QtWidgets.QGroupBox("关键帧编辑") edit_layout = QtWidgets.QHBoxLayout(edit_group) # 时间输入 edit_layout.addWidget(QtWidgets.QLabel("时间:")) self.time_edit = QtWidgets.QSpinBox() self.time_edit.setRange(-9999, 9999) self.time_edit.valueChanged.connect(self.on_time_changed) edit_layout.addWidget(self.time_edit) # 值输入 edit_layout.addWidget(QtWidgets.QLabel("值:")) self.value_edit = QtWidgets.QDoubleSpinBox() self.value_edit.setRange(-9999.0, 9999.0) self.value_edit.setDecimals(3) self.value_edit.valueChanged.connect(self.on_value_changed) edit_layout.addWidget(self.value_edit) layout.addWidget(edit_group) return controls def goto_prev_key(self): """跳转到上一个关键帧""" if not self.curve: return current = cmds.currentTime(q=True) times = cmds.keyframe(self.curve, q=True, timeChange=True) if not times: return # 查找上一帧 prev_time = max([t for t in times if t < current], default=None) if prev_time is not None: cmds.currentTime(prev_time) self.time_edit.setValue(prev_time) def goto_next_key(self): """跳转到下一个关键帧""" if not self.curve: return current = cmds.currentTime(q=True) times = cmds.keyframe(self.curve, q=True, timeChange=True) if not times: return # 查找下一帧 next_time = min([t for t in times if t > current], default=None) if next_time is not None: cmds.currentTime(next_time) self.time_edit.setValue(next_time) def on_time_changed(self, time): """时间变化""" if not self.curve: return # 更新当前时间 cmds.currentTime(time) # 更新值显示 value = cmds.getValue(self.curve, time=time) self.value_edit.setValue(value) # 刷新视图 self.refresh_view() def on_value_changed(self, value): """值变化""" if not self.curve: return # 获取当前时间 time = self.time_edit.value() # 设置关键帧 cmds.setKeyframe(self.curve, time=time, value=value) # 刷新视图 self.refresh_view() def apply_curve_to_target(self): """应用曲线到目标""" if not self.curve: return # 选择目标 sel = cmds.ls(sl=True) if not sel: QtWidgets.QMessageBox.warning( self, "应用曲线", "请先选择目标对象") return # 获取目标属性 attrs = [] for obj in sel: # 获取可设置关键帧的属性 keyable_attrs = cmds.listAttr(obj, keyable=True) or [] for attr in keyable_attrs: attrs.append(f"{obj}.{attr}") if not attrs: QtWidgets.QMessageBox.warning( self, "应用曲线", "选中的对象没有可设置关键帧的属性") return # 选择属性 attr, ok = QtWidgets.QInputDialog.getItem( self, "应用曲线", "选择目标属性:", attrs, 0, False) if ok and attr: # 连接曲线到属性 try: cmds.connectAttr(f"{self.curve}.output", attr) QtWidgets.QMessageBox.information( self, "应用曲线", f"已将曲线应用到 {attr}") except Exception as e: QtWidgets.QMessageBox.warning( self, "应用曲线", f"应用曲线失败: {str(e)}")