908 lines
31 KiB
Python
908 lines
31 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Rigging function module
|
||
绑定系统功能模块 - 提供绑定系统相关的功能函数
|
||
"""
|
||
|
||
#===================================== IMPORT MODULES =====================================
|
||
import maya.cmds as cmds
|
||
import pymel.core as pm
|
||
import maya.mel as mel
|
||
from maya import OpenMayaUI as omui
|
||
from scripts.ui.Qt import QtWidgets, QtCore, QtGui
|
||
from scripts.ui.Qt.QtCompat import wrapInstance
|
||
import webbrowser
|
||
import subprocess
|
||
import importlib
|
||
import traceback
|
||
import sys
|
||
import os
|
||
# 导入配置
|
||
import config
|
||
TOOL_NAME = config.TOOL_NAME
|
||
TOOL_VERSION = config.TOOL_VERSION
|
||
TOOL_AUTHOR = config.TOOL_AUTHOR
|
||
TOOL_YEAR = config.TOOL_YEAR
|
||
TOOL_MOD_FILENAME = config.TOOL_MOD_FILENAME
|
||
TOOL_LANG = config.TOOL_LANG
|
||
TOOL_WSCL_NAME = config.TOOL_WSCL_NAME
|
||
TOOL_HELP_URL = config.TOOL_HELP_URL
|
||
TOOL_PATH = config.TOOL_PATH
|
||
SCRIPTS_PATH = config.SCRIPTS_PATH
|
||
TOOL_MAIN_SCRIPT = config.TOOL_MAIN_SCRIPT
|
||
UI_PATH = config.UI_PATH
|
||
STYLE_FILE = config.STYLE_FILE
|
||
ICONS_PATH = config.ICONS_PATH
|
||
TOOL_ICON = config.TOOL_ICON
|
||
ASSETS_PATH = config.ASSETS_PATH
|
||
DNA_FILE_PATH = config.DNA_FILE_PATH
|
||
DNA_IMG_PATH = config.DNA_IMG_PATH
|
||
TOOL_COMMAND_ICON = config.TOOL_COMMAND_ICON
|
||
TOOL_WIDTH = config.TOOL_WIDTH
|
||
TOOL_HEIGHT = config.TOOL_HEIGHT
|
||
# Localization
|
||
from scripts.ui import localization
|
||
TEXT = localization.TEXT
|
||
|
||
#========================================== GLOBALS ========================================
|
||
# 存储当前选中的关节和控制器信息
|
||
selected_joint = None
|
||
selected_controller = None
|
||
|
||
# 存储关节和控制器的原始属性,用于撤销操作
|
||
original_joint_properties = {}
|
||
original_controller_properties = {}
|
||
|
||
# DNA预览按钮默认尺寸
|
||
DEFAULT_DNA_BUTTON_SIZE = (90, 120) # 保持3:4的宽高比例
|
||
|
||
#========================================== FUNCTIONS ========================================
|
||
|
||
#------------------------------------ DNA PREVIEW ------------------------------------
|
||
def get_dna_files():
|
||
"""
|
||
获取所有DNA文件及其对应的预览图
|
||
|
||
Returns:
|
||
list: 包含DNA文件信息的字典列表,每个字典包含:
|
||
- name: DNA文件名(不含扩展名)
|
||
- dna_path: DNA文件的完整路径
|
||
- image_path: 预览图的完整路径
|
||
"""
|
||
dna_files = []
|
||
default_preview = os.path.join(DNA_IMG_PATH, "Preview.png")
|
||
|
||
# 调试信息
|
||
print(f"加载DNA文件目录: {DNA_FILE_PATH}")
|
||
print(f"加载DNA图片目录: {DNA_IMG_PATH}")
|
||
|
||
# 确保目录存在
|
||
if not os.path.exists(DNA_FILE_PATH):
|
||
print(f"错误: DNA文件目录不存在: {DNA_FILE_PATH}")
|
||
return dna_files
|
||
|
||
if not os.path.exists(DNA_IMG_PATH):
|
||
print(f"错误: DNA图片目录不存在: {DNA_IMG_PATH}")
|
||
|
||
if not os.path.exists(default_preview):
|
||
print(f"错误: 默认预览图不存在: {default_preview}")
|
||
|
||
# 列出所有DNA文件
|
||
try:
|
||
dna_file_list = os.listdir(DNA_FILE_PATH)
|
||
print(f"找到 {len(dna_file_list)} 个文件在 DNA目录")
|
||
except Exception as e:
|
||
print(f"列出DNA文件失败: {str(e)}")
|
||
return dna_files
|
||
|
||
# 获取所有可用的预览图文件名(不带扩展名),只查询一次目录提高性能
|
||
available_images = set()
|
||
try:
|
||
img_files = os.listdir(DNA_IMG_PATH)
|
||
for img_file in img_files:
|
||
if img_file.lower().endswith('.png'):
|
||
base_name = os.path.splitext(img_file)[0]
|
||
available_images.add(base_name)
|
||
except Exception as e:
|
||
print(f"列出图片文件失败: {str(e)}")
|
||
|
||
# 处理每个DNA文件
|
||
for file_name in dna_file_list:
|
||
if file_name.lower().endswith('.dna'):
|
||
# 获取不含扩展名的文件名
|
||
name = os.path.splitext(file_name)[0]
|
||
dna_path = os.path.join(DNA_FILE_PATH, file_name).replace("\\", "/")
|
||
|
||
# 检查预览图是否存在,不重复检查文件系统提高性能
|
||
if name in available_images:
|
||
image_path = os.path.join(DNA_IMG_PATH, f"{name}.png").replace("\\", "/")
|
||
else:
|
||
image_path = default_preview
|
||
print(f"未找到预览图: {name}.png, 使用默认预览图")
|
||
|
||
dna_files.append({
|
||
"name": name,
|
||
"dna_path": dna_path,
|
||
"image_path": image_path
|
||
})
|
||
|
||
print(f"成功加载 {len(dna_files)} 个DNA文件")
|
||
return dna_files
|
||
|
||
def load_selected_dna(dna_path, ui_instance=None):
|
||
"""
|
||
加载用户选择的DNA文件
|
||
|
||
Args:
|
||
dna_path (str): DNA文件路径
|
||
ui_instance: UI实例,用于更新UI状态
|
||
|
||
Returns:
|
||
bool: 是否成功加载
|
||
"""
|
||
try:
|
||
if not os.path.exists(dna_path):
|
||
print(f"DNA文件不存在: {dna_path}")
|
||
return False
|
||
|
||
print(f"加载DNA文件: {dna_path}")
|
||
|
||
# 更新UI中的DNA路径输入框(如果有UI实例)
|
||
if ui_instance and hasattr(ui_instance, 'controls') and 'presets_dna_input' in ui_instance.controls:
|
||
ui_instance.controls['presets_dna_input'].setText(dna_path)
|
||
|
||
# 这里可以添加实际的DNA文件加载逻辑
|
||
# 例如通过MetaHuman DNA Calibration库读取DNA文件
|
||
|
||
return True
|
||
except Exception as e:
|
||
print(f"加载DNA文件失败: {str(e)}")
|
||
return False
|
||
|
||
def calculate_grid_layout(parent_width, item_width, spacing):
|
||
"""
|
||
根据父容器宽度计算每行可以容纳的项目数量
|
||
|
||
Args:
|
||
parent_width (int): 父容器宽度
|
||
item_width (int): 每个项目的宽度
|
||
spacing (int): 项目间的间距
|
||
|
||
Returns:
|
||
int: 每行可容纳的项目数量
|
||
"""
|
||
if parent_width <= 0 or item_width <= 0:
|
||
return 1
|
||
|
||
# 计算每行可容纳的项目数量
|
||
available_width = parent_width - spacing # 减去左边距
|
||
items_per_row = max(1, int(available_width / (item_width + spacing)))
|
||
|
||
return items_per_row
|
||
|
||
# 预缓存的图片缓存,减少重复加载
|
||
_pixmap_cache = {}
|
||
_pixmap_cache_max_size = 50 # 最多缓存50张图片
|
||
_pixmap_cache_lru = [] # 最近最少使用列表,用于清理缓存
|
||
|
||
def get_pixmap(image_path, width, height):
|
||
"""
|
||
从缓存获取QPixmap或创建新的
|
||
|
||
Args:
|
||
image_path: 图片路径
|
||
width: 所需宽度
|
||
height: 所需高度
|
||
|
||
Returns:
|
||
QPixmap: 图片对象
|
||
"""
|
||
cache_key = f"{image_path}_{width}_{height}"
|
||
|
||
# 检查缓存
|
||
if cache_key in _pixmap_cache:
|
||
# 更新LRU列表
|
||
global _pixmap_cache_lru
|
||
if cache_key in _pixmap_cache_lru:
|
||
_pixmap_cache_lru.remove(cache_key)
|
||
_pixmap_cache_lru.append(cache_key)
|
||
return _pixmap_cache[cache_key]
|
||
|
||
try:
|
||
# 加载原始图片
|
||
pixmap = QtGui.QPixmap(image_path)
|
||
|
||
if not pixmap.isNull():
|
||
# 缩放图片
|
||
if width > 0 and height > 0:
|
||
pixmap = pixmap.scaled(width, height, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||
|
||
# 缓存清理 - 如果超出最大缓存大小,删除最不常用的项目
|
||
global _pixmap_cache_max_size
|
||
if len(_pixmap_cache) >= _pixmap_cache_max_size and _pixmap_cache_lru:
|
||
oldest_key = _pixmap_cache_lru.pop(0)
|
||
if oldest_key in _pixmap_cache:
|
||
del _pixmap_cache[oldest_key]
|
||
|
||
# 存入缓存
|
||
_pixmap_cache[cache_key] = pixmap
|
||
_pixmap_cache_lru.append(cache_key)
|
||
return pixmap
|
||
else:
|
||
# 创建一个纯色背景作为替代
|
||
fallback_pixmap = QtGui.QPixmap(width, height)
|
||
fallback_pixmap.fill(QtGui.QColor(60, 60, 60))
|
||
return fallback_pixmap
|
||
except Exception as e:
|
||
print(f"加载图像失败 ({image_path}): {str(e)}")
|
||
fallback_pixmap = QtGui.QPixmap(width, height)
|
||
fallback_pixmap.fill(QtGui.QColor(60, 60, 60))
|
||
return fallback_pixmap
|
||
|
||
def create_dna_preview_button(dna_info, item_size, on_click_callback):
|
||
"""
|
||
创建DNA预览按钮
|
||
|
||
Args:
|
||
dna_info (dict): DNA文件信息
|
||
item_size (tuple): 按钮尺寸 (宽, 高)
|
||
on_click_callback (function): 点击回调函数
|
||
|
||
Returns:
|
||
QPushButton: 创建的DNA预览按钮
|
||
"""
|
||
try:
|
||
# 调试信息
|
||
print(f"创建DNA按钮: {dna_info['name']}, 路径: {dna_info['image_path']}, 尺寸: {item_size}")
|
||
|
||
# 创建带有图标和文字的垂直布局按钮
|
||
dna_button = QtWidgets.QWidget()
|
||
dna_button.setFixedSize(item_size[0], item_size[1])
|
||
dna_button.setObjectName(f"dna_button_{dna_info['name']}")
|
||
dna_button.setCursor(QtCore.Qt.PointingHandCursor)
|
||
|
||
# 创建垂直布局
|
||
layout = QtWidgets.QVBoxLayout(dna_button)
|
||
layout.setContentsMargins(4, 4, 4, 4) # 设置小的内边距,让按钮内容不会顶到边缘
|
||
layout.setSpacing(2) # 图像和文字之间的小间距
|
||
|
||
# 创建图像标签
|
||
img_label = QtWidgets.QLabel()
|
||
img_label.setAlignment(QtCore.Qt.AlignCenter)
|
||
|
||
# 计算图像区域高度 - 预留足够空间给文字
|
||
text_height = 20 # 文字区域高度
|
||
img_height = item_size[1] - text_height - layout.contentsMargins().top() - layout.contentsMargins().bottom() - layout.spacing()
|
||
img_width = item_size[0] - layout.contentsMargins().left() - layout.contentsMargins().right()
|
||
|
||
# 加载并缩放图像
|
||
pixmap = QtGui.QPixmap(dna_info['image_path'])
|
||
if pixmap.isNull():
|
||
print(f"错误: 无法加载图像 {dna_info['image_path']}")
|
||
# 创建一个默认的纯色图片
|
||
pixmap = QtGui.QPixmap(img_width, img_height)
|
||
pixmap.fill(QtGui.QColor(60, 60, 60))
|
||
else:
|
||
# 缩放图像以适应,但保持纵横比
|
||
pixmap = pixmap.scaled(
|
||
img_width,
|
||
img_height,
|
||
QtCore.Qt.KeepAspectRatio,
|
||
QtCore.Qt.SmoothTransformation
|
||
)
|
||
|
||
# 设置图像标签的图像
|
||
img_label.setPixmap(pixmap)
|
||
img_label.setMinimumHeight(img_height)
|
||
img_label.setMaximumHeight(img_height)
|
||
|
||
# 创建文本标签
|
||
text_label = QtWidgets.QLabel(dna_info['name'])
|
||
text_label.setAlignment(QtCore.Qt.AlignCenter)
|
||
text_label.setStyleSheet("""
|
||
color: white;
|
||
font-size: 10px;
|
||
font-weight: bold;
|
||
padding: 2px;
|
||
""")
|
||
text_label.setWordWrap(True)
|
||
text_label.setFixedHeight(text_height)
|
||
|
||
# 添加控件到布局
|
||
layout.addWidget(img_label, 1) # 图像区域占据更多空间
|
||
layout.addWidget(text_label, 0) # 文字区域固定高度
|
||
|
||
# 设置整体样式 - 包括边框和圆角
|
||
dna_button.setStyleSheet(f"""
|
||
QWidget#{dna_button.objectName()} {{
|
||
background-color: #333333;
|
||
border: 1px solid #444444;
|
||
border-radius: 8px;
|
||
}}
|
||
""")
|
||
|
||
# 创建透明按钮覆盖整个区域以处理点击
|
||
overlay_button = QtWidgets.QPushButton(dna_button)
|
||
overlay_button.setObjectName("overlayButton")
|
||
overlay_button.setStyleSheet("""
|
||
#overlayButton {
|
||
background-color: transparent;
|
||
border: none;
|
||
border-radius: 8px;
|
||
}
|
||
#overlayButton:hover {
|
||
background-color: rgba(0, 122, 204, 0.2);
|
||
border: 1px solid #007ACC;
|
||
border-radius: 8px;
|
||
}
|
||
#overlayButton:pressed {
|
||
background-color: rgba(0, 122, 204, 0.3);
|
||
border: 1px solid #007ACC;
|
||
border-radius: 8px;
|
||
}
|
||
""")
|
||
overlay_button.resize(dna_button.size())
|
||
overlay_button.clicked.connect(lambda: on_click_callback(dna_info['dna_path']))
|
||
|
||
# 存储DNA路径属性
|
||
dna_button.setProperty("dna_path", dna_info['dna_path'])
|
||
|
||
return dna_button
|
||
except Exception as e:
|
||
print(f"创建DNA预览按钮失败 ({dna_info.get('name', 'unknown')}): {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# 返回一个简单的替代按钮
|
||
fallback_widget = QtWidgets.QWidget()
|
||
fallback_widget.setFixedSize(item_size[0], item_size[1])
|
||
fallback_widget.setStyleSheet("""
|
||
background-color: #333333;
|
||
border: 1px solid #444444;
|
||
border-radius: 8px;
|
||
""")
|
||
|
||
fallback_layout = QtWidgets.QVBoxLayout(fallback_widget)
|
||
fallback_layout.setContentsMargins(4, 4, 4, 4)
|
||
|
||
fallback_label = QtWidgets.QLabel(dna_info.get('name', 'DNA'))
|
||
fallback_label.setAlignment(QtCore.Qt.AlignCenter)
|
||
fallback_label.setStyleSheet("color: white; font-weight: bold;")
|
||
|
||
fallback_layout.addWidget(fallback_label)
|
||
|
||
# 添加点击功能
|
||
if on_click_callback:
|
||
fallback_button = QtWidgets.QPushButton(fallback_widget)
|
||
fallback_button.setStyleSheet("""
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 8px;
|
||
""")
|
||
fallback_button.resize(fallback_widget.size())
|
||
fallback_button.clicked.connect(lambda: on_click_callback(dna_info.get('dna_path', '')))
|
||
|
||
return fallback_widget
|
||
|
||
# 缓存上一次的布局信息,避免不必要的重新计算
|
||
_last_layout_info = {
|
||
"parent_width": 0,
|
||
"item_size": (0, 0),
|
||
"spacing": 0,
|
||
"items_per_row": 0
|
||
}
|
||
|
||
def load_dna_preview_buttons(ui_instance):
|
||
"""
|
||
为UI加载DNA预览按钮
|
||
|
||
Args:
|
||
ui_instance: UI实例
|
||
"""
|
||
try:
|
||
# 检查UI实例是否有效
|
||
if not ui_instance or not hasattr(ui_instance, 'controls'):
|
||
print("无效的UI实例,无法加载DNA预览按钮")
|
||
return False
|
||
|
||
# 检查是否有dna_button_size属性,没有则初始化
|
||
if not hasattr(ui_instance, 'dna_button_size'):
|
||
ui_instance.dna_button_size = DEFAULT_DNA_BUTTON_SIZE
|
||
print(f"警告: UI实例没有dna_button_size属性,使用默认值{DEFAULT_DNA_BUTTON_SIZE}")
|
||
|
||
# 获取DNA文件列表
|
||
dna_files = ui_instance.dna_files
|
||
|
||
# 检查是否有DNA文件
|
||
if not dna_files:
|
||
# 如果还没有加载DNA文件,则先加载
|
||
ui_instance.dna_files = get_dna_files()
|
||
dna_files = ui_instance.dna_files
|
||
if not dna_files:
|
||
print("没有找到DNA文件,无法加载预览按钮")
|
||
if 'presets_count_label' in ui_instance.controls:
|
||
ui_instance.controls['presets_count_label'].setText("0")
|
||
return False
|
||
|
||
# 更新预设数量标签
|
||
if 'presets_count_label' in ui_instance.controls:
|
||
ui_instance.controls['presets_count_label'].setText(str(len(dna_files)))
|
||
|
||
# 获取滚动区域宽度
|
||
if 'presets_content' in ui_instance.controls:
|
||
scroll_width = ui_instance.controls['presets_content'].width()
|
||
else:
|
||
scroll_width = 400 # 默认宽度
|
||
|
||
# 更新DNA网格布局
|
||
update_dna_grid_layout(
|
||
ui_instance.controls["presets_flow_layout"],
|
||
scroll_width,
|
||
ui_instance.dna_button_size,
|
||
10, # 间距
|
||
dna_files,
|
||
lambda dna_path: on_dna_button_clicked(dna_path, ui_instance)
|
||
)
|
||
|
||
return True
|
||
except Exception as e:
|
||
import traceback
|
||
print(f"加载DNA预览按钮失败: {str(e)}")
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def update_dna_grid_layout(flow_layout, parent_width, item_size, spacing, dna_files, on_dna_clicked):
|
||
"""
|
||
更新DNA布局,根据父容器宽度动态调整DNA按钮布局
|
||
|
||
Args:
|
||
flow_layout: 流式布局
|
||
parent_width (int): 父容器宽度
|
||
item_size (tuple): 项目尺寸 (宽, 高)
|
||
spacing (int): 项目间的间距
|
||
dna_files (list): DNA文件信息列表
|
||
on_dna_clicked (function): DNA按钮点击回调函数
|
||
"""
|
||
try:
|
||
# 检查参数有效性
|
||
if flow_layout is None:
|
||
print("错误: flow_layout 为空")
|
||
return
|
||
|
||
if not dna_files:
|
||
print("警告: 没有DNA文件可以显示")
|
||
return
|
||
|
||
# 先清空现有控件
|
||
while flow_layout.count() > 0:
|
||
item = flow_layout.takeAt(0)
|
||
if item and item.widget():
|
||
widget = item.widget()
|
||
widget.setParent(None)
|
||
widget.deleteLater()
|
||
|
||
# 创建并添加DNA预览按钮
|
||
for dna_info in dna_files:
|
||
try:
|
||
# 创建DNA预览按钮
|
||
dna_button = create_dna_preview_button(dna_info, item_size, on_dna_clicked)
|
||
|
||
# 添加到流式布局
|
||
flow_layout.addWidget(dna_button)
|
||
except Exception as e:
|
||
print(f"添加DNA按钮失败 ({dna_info['name']}): {str(e)}")
|
||
|
||
except Exception as e:
|
||
print(f"更新DNA布局失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# 保存上次调整大小的时间,用于限制调整频率
|
||
_last_resize_time = 0
|
||
_resize_throttle = 200 # 毫秒
|
||
|
||
def on_presets_content_resize(event, ui_instance):
|
||
"""
|
||
预设内容区域大小变化事件,限制更新频率以提高性能
|
||
流式布局会自动处理大部分布局工作,这里只需更新缩放比例
|
||
|
||
Args:
|
||
event: 调整大小事件
|
||
ui_instance: UI实例
|
||
"""
|
||
try:
|
||
# 检查UI实例是否有效
|
||
if not ui_instance or not hasattr(ui_instance, 'controls') or "presets_content" not in ui_instance.controls:
|
||
# 如果UI实例无效,只调用原始的resizeEvent
|
||
QtWidgets.QWidget.resizeEvent(event.widget(), event)
|
||
return
|
||
|
||
# 声明全局变量
|
||
global _last_resize_time
|
||
|
||
# 调用父类的resizeEvent
|
||
QtWidgets.QWidget.resizeEvent(ui_instance.controls["presets_content"], event)
|
||
|
||
# 获取当前时间
|
||
current_time = QtCore.QTime.currentTime().msecsSinceStartOfDay()
|
||
|
||
# 如果距离上次更新不足阈值时间,则跳过更新
|
||
if current_time - _last_resize_time < _resize_throttle:
|
||
return
|
||
|
||
# 更新上次调整时间
|
||
_last_resize_time = current_time
|
||
|
||
# 对于流式布局,大多数情况下不需要手动刷新
|
||
# 但在某些情况下可能需要手动触发布局更新
|
||
if hasattr(ui_instance.controls["presets_flow_layout"], "update"):
|
||
ui_instance.controls["presets_flow_layout"].update()
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
print(f"调整DNA预览布局失败: {str(e)}")
|
||
traceback.print_exc()
|
||
|
||
def on_dna_button_clicked(dna_path, ui_instance):
|
||
"""
|
||
DNA按钮点击事件
|
||
|
||
Args:
|
||
dna_path: DNA文件路径
|
||
ui_instance: UI实例
|
||
"""
|
||
try:
|
||
# 检查UI实例是否有效
|
||
if not ui_instance or not hasattr(ui_instance, 'controls'):
|
||
print("警告: UI实例无效,无法更新DNA路径")
|
||
# 仍然尝试加载DNA文件,即使无法更新UI
|
||
load_selected_dna(dna_path)
|
||
return
|
||
|
||
# 加载选中的DNA文件
|
||
load_selected_dna(dna_path, ui_instance)
|
||
|
||
# 更新UI中的DNA路径输入框
|
||
if "presets_dna_input" in ui_instance.controls:
|
||
ui_instance.controls["presets_dna_input"].setText(dna_path)
|
||
else:
|
||
print("警告: 'presets_dna_input' 未创建")
|
||
except Exception as e:
|
||
import traceback
|
||
print(f"点击DNA按钮处理失败: {str(e)}")
|
||
traceback.print_exc()
|
||
|
||
#------------------------------------ UI UTILITIES ------------------------------------
|
||
def browse_file(parent_widget, title, line_edit, file_type=None):
|
||
"""
|
||
浏览文件对话框
|
||
|
||
Args:
|
||
parent_widget: 父窗口
|
||
title: 对话框标题
|
||
line_edit: 要更新的文本框
|
||
file_type: 文件类型筛选
|
||
"""
|
||
try:
|
||
# 设置文件过滤器
|
||
if file_type == "dna":
|
||
file_filter = "DNA文件 (*.dna);;所有文件 (*.*)"
|
||
elif file_type == "fbx":
|
||
file_filter = "FBX文件 (*.fbx);;所有文件 (*.*)"
|
||
else:
|
||
file_filter = "所有文件 (*.*)"
|
||
|
||
# 打开文件选择对话框
|
||
file_path = cmds.fileDialog2(
|
||
fileFilter=file_filter,
|
||
dialogStyle=2,
|
||
caption=TEXT(title, title)
|
||
)
|
||
|
||
if file_path:
|
||
# 更新文本框
|
||
line_edit.setText(file_path[0])
|
||
return file_path[0]
|
||
return None
|
||
except Exception as e:
|
||
print(f"浏览文件失败: {str(e)}")
|
||
return None
|
||
|
||
#------------------------------------ DNA OPERATIONS ------------------------------------
|
||
def import_dna():
|
||
"""
|
||
导入DNA文件
|
||
|
||
从文件中导入DNA数据
|
||
"""
|
||
try:
|
||
# 打开文件选择对话框
|
||
file_path = cmds.fileDialog2(
|
||
fileFilter="DNA文件 (*.dna);;所有文件 (*.*)",
|
||
dialogStyle=2,
|
||
caption="选择DNA文件"
|
||
)
|
||
|
||
if not file_path:
|
||
print("未选择文件")
|
||
return False
|
||
|
||
print(f"导入DNA文件: {file_path[0]}")
|
||
# 实现DNA导入逻辑
|
||
return True
|
||
except Exception as e:
|
||
print(f"导入DNA文件失败: {str(e)}")
|
||
return False
|
||
|
||
def export_dna():
|
||
"""
|
||
导出DNA文件
|
||
|
||
将当前绑定数据导出为DNA文件
|
||
"""
|
||
try:
|
||
# 打开文件保存对话框
|
||
file_path = cmds.fileDialog2(
|
||
fileFilter="DNA文件 (*.dna);;所有文件 (*.*)",
|
||
dialogStyle=2,
|
||
caption="保存DNA文件",
|
||
fileMode=0
|
||
)
|
||
|
||
if not file_path:
|
||
print("未选择保存位置")
|
||
return False
|
||
|
||
# 确保文件扩展名为.dna
|
||
if not file_path[0].lower().endswith('.dna'):
|
||
file_path[0] += '.dna'
|
||
|
||
print(f"导出DNA文件: {file_path[0]}")
|
||
# 实现DNA导出逻辑
|
||
return True
|
||
except Exception as e:
|
||
print(f"导出DNA文件失败: {str(e)}")
|
||
return False
|
||
|
||
#------------------------------------ Rigging ------------------------------------
|
||
def remove_all(*args):
|
||
"""移除所有绑定组件
|
||
|
||
从场景中删除所有绑定相关的组件,包括关节、控制器等
|
||
"""
|
||
try:
|
||
# 获取所有关节
|
||
all_joints = cmds.ls(type="joint")
|
||
|
||
# 获取所有可能的控制器(这里简化为所有曲线)
|
||
all_controllers = cmds.ls(type="nurbsCurve")
|
||
all_controller_transforms = []
|
||
|
||
# 获取控制器的变换节点
|
||
for ctrl in all_controllers:
|
||
parent = cmds.listRelatives(ctrl, parent=True)
|
||
if parent:
|
||
all_controller_transforms.extend(parent)
|
||
|
||
# 确认删除
|
||
result = cmds.confirmDialog(
|
||
title=TEXT("confirm_delete", "确认删除"),
|
||
message=TEXT("delete_all_confirm", "确定要删除所有绑定组件吗?"),
|
||
button=[TEXT("yes", "是"), TEXT("no", "否")],
|
||
defaultButton=TEXT("no", "否"),
|
||
cancelButton=TEXT("no", "否"),
|
||
dismissString=TEXT("no", "否")
|
||
)
|
||
|
||
if result == TEXT("yes", "是"):
|
||
# 删除所有控制器
|
||
if all_controller_transforms:
|
||
cmds.delete(all_controller_transforms)
|
||
|
||
# 删除所有关节
|
||
if all_joints:
|
||
cmds.delete(all_joints)
|
||
|
||
print("成功删除所有绑定组件")
|
||
return True
|
||
else:
|
||
print("取消删除操作")
|
||
return False
|
||
except Exception as e:
|
||
print(f"删除绑定组件失败: {str(e)}")
|
||
return False
|
||
|
||
def import_skeleton(*args):
|
||
"""导入骨骼
|
||
|
||
从文件中导入骨骼结构
|
||
"""
|
||
try:
|
||
# 打开文件选择对话框
|
||
file_path = cmds.fileDialog2(
|
||
fileFilter="Maya文件 (*.ma *.mb);;FBX文件 (*.fbx);;所有文件 (*.*)",
|
||
dialogStyle=2,
|
||
caption="选择骨骼文件"
|
||
)
|
||
|
||
if not file_path:
|
||
print("未选择文件")
|
||
return False
|
||
|
||
file_path = file_path[0] # fileDialog2返回的是列表
|
||
|
||
# 根据文件类型选择导入方法
|
||
if file_path.lower().endswith(('.ma', '.mb')):
|
||
# 导入Maya文件
|
||
cmds.file(file_path, i=True, type="mayaAscii" if file_path.lower().endswith('.ma') else "mayaBinary",
|
||
ignoreVersion=True, mergeNamespacesOnClash=False, namespace="skeleton")
|
||
elif file_path.lower().endswith('.fbx'):
|
||
# 导入FBX文件
|
||
cmds.file(file_path, i=True, type="FBX", ignoreVersion=True)
|
||
else:
|
||
print(f"不支持的文件类型: {file_path}")
|
||
return False
|
||
|
||
print(f"成功导入骨骼: {file_path}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"导入骨骼失败: {str(e)}")
|
||
return False
|
||
|
||
def build_rigging(*args):
|
||
"""构建绑定系统
|
||
|
||
基于当前场景中的骨骼构建完整的绑定系统
|
||
"""
|
||
try:
|
||
# 获取场景中的所有关节
|
||
all_joints = cmds.ls(type="joint")
|
||
|
||
if not all_joints:
|
||
print("场景中没有骨骼,无法构建绑定")
|
||
return False
|
||
|
||
# 为每个关节创建控制器
|
||
for joint in all_joints:
|
||
# 获取关节位置
|
||
pos = cmds.xform(joint, query=True, translation=True, worldSpace=True)
|
||
|
||
# 创建控制器(这里简化为创建一个NURBS圆环)
|
||
ctrl = cmds.circle(name=f"{joint}_ctrl", normal=[1, 0, 0], radius=1)[0]
|
||
|
||
# 移动控制器到关节位置
|
||
cmds.move(pos[0], pos[1], pos[2], ctrl)
|
||
|
||
# 创建约束
|
||
cmds.parentConstraint(ctrl, joint, maintainOffset=True)
|
||
|
||
print("成功构建绑定系统")
|
||
return True
|
||
except Exception as e:
|
||
print(f"构建绑定系统失败: {str(e)}")
|
||
return False
|
||
|
||
def clear_pixmap_cache():
|
||
"""
|
||
清除图片缓存
|
||
在不再需要缓存时调用,例如关闭窗口时
|
||
"""
|
||
global _pixmap_cache, _pixmap_cache_lru
|
||
_pixmap_cache.clear()
|
||
_pixmap_cache_lru.clear()
|
||
print("图片缓存已清除")
|
||
|
||
#------------------------------------ UI 预览调整函数 ------------------------------------
|
||
def on_presets_slider_changed(ui_instance, value):
|
||
"""
|
||
预设滑块值变化处理 - 调整预览按钮大小
|
||
|
||
Args:
|
||
ui_instance: UI实例
|
||
value: 滑块当前值
|
||
"""
|
||
try:
|
||
# 检查UI实例是否有效
|
||
if not ui_instance or not hasattr(ui_instance, 'controls'):
|
||
print("警告: UI实例无效,无法调整预览按钮大小")
|
||
return
|
||
|
||
# 检查是否有dna_button_size属性,没有则初始化
|
||
if not hasattr(ui_instance, 'dna_button_size'):
|
||
ui_instance.dna_button_size = DEFAULT_DNA_BUTTON_SIZE
|
||
print(f"警告: UI实例没有dna_button_size属性,使用默认值{DEFAULT_DNA_BUTTON_SIZE}")
|
||
|
||
# 调试尺寸信息
|
||
print(f"当前按钮基础尺寸: {DEFAULT_DNA_BUTTON_SIZE[0]}x{DEFAULT_DNA_BUTTON_SIZE[1]}")
|
||
|
||
# 获取滑块范围
|
||
min_val = ui_instance.controls["presets_slider"].minimum()
|
||
max_val = ui_instance.controls["presets_slider"].maximum()
|
||
mid_val = (min_val + max_val) / 2
|
||
|
||
# 确保滑块值在范围内
|
||
value = max(min_val, min(max_val, value))
|
||
|
||
# 计算缩放比例 - 更平滑的缩放效果
|
||
# 以100为中心点,低于100缩小,高于100放大
|
||
if value <= mid_val:
|
||
# 从0.8到1.0
|
||
scale_factor = 0.8 + 0.2 * ((value - min_val) / (mid_val - min_val))
|
||
else:
|
||
# 从1.0到1.2
|
||
scale_factor = 1.0 + 0.2 * ((value - mid_val) / (max_val - mid_val))
|
||
|
||
# 确保在合理范围内
|
||
scale_factor = max(0.7, min(1.3, scale_factor))
|
||
|
||
# 从基础尺寸计算缩放后的尺寸
|
||
new_width = int(DEFAULT_DNA_BUTTON_SIZE[0] * scale_factor)
|
||
new_height = int(DEFAULT_DNA_BUTTON_SIZE[1] * scale_factor)
|
||
|
||
# 确保尺寸不会太小或太大
|
||
new_width = max(60, min(130, new_width))
|
||
new_height = max(80, min(170, new_height))
|
||
|
||
# 确保宽高比例正确 - 3:4比例
|
||
aspect_ratio = 0.75 # 宽度/高度 = 3/4
|
||
|
||
# 优先以高度为准,调整宽度
|
||
new_width = int(new_height * aspect_ratio)
|
||
|
||
# 确保宽高为偶数
|
||
if new_width % 2 != 0:
|
||
new_width += 1
|
||
if new_height % 2 != 0:
|
||
new_height += 1
|
||
|
||
# 输出调试信息
|
||
print(f"调整DNA按钮大小: {new_width}x{new_height}, 缩放因子: {scale_factor:.2f}")
|
||
|
||
# 更新按钮尺寸
|
||
ui_instance.dna_button_size = (new_width, new_height)
|
||
|
||
# 强制刷新布局 - 先清空布局,然后重新加载
|
||
flow_layout = ui_instance.controls["presets_flow_layout"]
|
||
|
||
# 重新加载预览按钮
|
||
load_dna_preview_buttons(ui_instance)
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
print(f"调整DNA预览按钮大小失败: {str(e)}")
|
||
traceback.print_exc()
|
||
|
||
def load_dna_files(ui_instance):
|
||
"""
|
||
加载DNA文件和预览
|
||
在所有UI控件创建完成后执行
|
||
|
||
Args:
|
||
ui_instance: UI实例
|
||
"""
|
||
try:
|
||
# 检查UI实例是否有效
|
||
if not ui_instance or not hasattr(ui_instance, 'controls'):
|
||
print("警告: UI实例无效,无法加载DNA文件")
|
||
return
|
||
|
||
# 加载DNA文件和预览图
|
||
ui_instance.dna_files = get_dna_files()
|
||
|
||
# 更新预设数量标签
|
||
if 'presets_count_label' in ui_instance.controls:
|
||
ui_instance.controls['presets_count_label'].setText(str(len(ui_instance.dna_files)))
|
||
|
||
# 调用加载预览按钮函数
|
||
load_dna_preview_buttons(ui_instance)
|
||
|
||
# 自动选中第一个DNA文件 (如果存在)
|
||
if ui_instance.dna_files and 'presets_dna_input' in ui_instance.controls:
|
||
ui_instance.controls['presets_dna_input'].setText(ui_instance.dna_files[0]['dna_path'])
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
print(f"加载DNA文件失败: {str(e)}")
|
||
traceback.print_exc() |