1
0
forked from Rowland/EG
EG/ui/assembly_disassembly_config_simple.py

1069 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
拆装交互配置界面 - 简化版本
"""
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget,
QWidget, QLabel, QListWidget, QListWidgetItem,
QPushButton, QComboBox, QLineEdit, QTextEdit,
QGroupBox, QGridLayout, QCheckBox, QSplitter,
QTreeWidget, QTreeWidgetItem, QMessageBox,
QFileDialog, QScrollArea, QDoubleSpinBox)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
import json
class AssemblyDisassemblyConfigDialog(QDialog):
"""拆装配置主对话框 - 简化版本"""
def __init__(self, parent=None, world=None):
super().__init__(parent)
self.world = world
self.config_data = {
'models': [], # 参与拆装的模型列表
'steps': [], # 拆装步骤配置
'tools': [], # 使用的工具列表
'settings': {} # 全局设置
}
self.setupUI()
self.loadSceneModels()
# 初始化当前编辑的步骤项
self._current_step_item = None
def setupUI(self):
"""设置界面"""
self.setWindowTitle("拆装交互配置")
self.setModal(False)
self.resize(1000, 700)
# 主布局
main_layout = QVBoxLayout(self)
# 标题
title_label = QLabel("模型拆装交互配置")
title_label.setFont(QFont("", 16, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title_label)
# 创建标签页
self.tab_widget = QTabWidget()
main_layout.addWidget(self.tab_widget)
# 模型选择标签页
self.models_tab = self.createModelsTab()
self.tab_widget.addTab(self.models_tab, "模型选择")
# 步骤配置标签页
self.steps_tab = self.createStepsTab()
self.tab_widget.addTab(self.steps_tab, "步骤配置")
# 工具配置标签页
self.tools_tab = self.createToolsTab()
self.tab_widget.addTab(self.tools_tab, "工具配置")
# 全局设置标签页
self.settings_tab = self.createSettingsTab()
self.tab_widget.addTab(self.settings_tab, "全局设置")
# 底部按钮
button_layout = QHBoxLayout()
self.save_button = QPushButton("保存配置")
self.load_button = QPushButton("加载配置")
self.preview_button = QPushButton("预览效果")
self.apply_button = QPushButton("应用配置")
button_layout.addWidget(self.save_button)
button_layout.addWidget(self.load_button)
button_layout.addStretch()
button_layout.addWidget(self.preview_button)
button_layout.addWidget(self.apply_button)
main_layout.addLayout(button_layout)
# 连接信号
self.connectSignals()
def createModelsTab(self):
"""创建模型选择标签页"""
widget = QWidget()
layout = QVBoxLayout(widget)
# 分割器
splitter = QSplitter(Qt.Horizontal)
layout.addWidget(splitter)
# 左侧 - 场景模型
left_group = QGroupBox("场景模型")
left_layout = QVBoxLayout(left_group)
# 添加搜索框
search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("搜索模型:"))
self.model_search_edit = QLineEdit()
self.model_search_edit.setPlaceholderText("输入模型名称进行搜索...")
search_layout.addWidget(self.model_search_edit)
left_layout.addLayout(search_layout)
self.scene_models_tree = QTreeWidget()
self.scene_models_tree.setHeaderLabel("场景中的模型")
left_layout.addWidget(self.scene_models_tree)
# 操作按钮
button_layout = QHBoxLayout()
self.add_model_button = QPushButton("添加到配置 →")
self.remove_model_button = QPushButton("← 从配置移除")
button_layout.addWidget(self.add_model_button)
button_layout.addWidget(self.remove_model_button)
left_layout.addLayout(button_layout)
splitter.addWidget(left_group)
# 右侧 - 已配置模型
right_group = QGroupBox("已配置模型")
right_layout = QVBoxLayout(right_group)
self.config_models_list = QListWidget()
right_layout.addWidget(self.config_models_list)
splitter.addWidget(right_group)
return widget
def createStepsTab(self):
"""创建步骤配置标签页"""
widget = QWidget()
layout = QVBoxLayout(widget)
# 分割器
splitter = QSplitter(Qt.Horizontal)
layout.addWidget(splitter)
# 左侧 - 步骤列表
left_group = QGroupBox("拆装步骤")
left_layout = QVBoxLayout(left_group)
# 步骤操作按钮
step_buttons = QHBoxLayout()
self.add_step_button = QPushButton("添加步骤")
self.remove_step_button = QPushButton("删除步骤")
self.move_up_button = QPushButton("上移")
self.move_down_button = QPushButton("下移")
step_buttons.addWidget(self.add_step_button)
step_buttons.addWidget(self.remove_step_button)
step_buttons.addWidget(self.move_up_button)
step_buttons.addWidget(self.move_down_button)
left_layout.addLayout(step_buttons)
self.steps_list = QListWidget()
left_layout.addWidget(self.steps_list)
splitter.addWidget(left_group)
# 右侧 - 步骤详细配置
right_group = QGroupBox("步骤详细配置")
right_layout = QVBoxLayout(right_group)
# 滚动区域
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_content = QWidget()
scroll_layout = QVBoxLayout(scroll_content)
# 基本信息
basic_group = QGroupBox("基本信息")
basic_layout = QGridLayout(basic_group)
basic_layout.addWidget(QLabel("步骤名称:"), 0, 0)
self.step_name_edit = QLineEdit()
basic_layout.addWidget(self.step_name_edit, 0, 1)
basic_layout.addWidget(QLabel("步骤描述:"), 1, 0)
self.step_desc_edit = QTextEdit()
self.step_desc_edit.setMaximumHeight(80)
basic_layout.addWidget(self.step_desc_edit, 1, 1)
basic_layout.addWidget(QLabel("步骤类型:"), 2, 0)
self.step_type_combo = QComboBox()
self.step_type_combo.addItems(["拆卸", "安装"])
basic_layout.addWidget(self.step_type_combo, 2, 1)
# 步骤分值(考核模式用)
basic_layout.addWidget(QLabel("步骤分值:"), 3, 0)
self.step_score_spin = QDoubleSpinBox()
self.step_score_spin.setRange(0, 100)
self.step_score_spin.setValue(10) # 默认10分
self.step_score_spin.setSingleStep(1)
self.step_score_spin.setDecimals(0)
self.step_score_spin.setSuffix("")
basic_layout.addWidget(self.step_score_spin, 3, 1)
# 添加分值说明
score_note = QLabel("注:在考核模式下,此分值将用于计算该步骤的得分。")
score_note.setWordWrap(True)
score_note.setStyleSheet("color: #666; font-style: italic; font-size: 10px;")
basic_layout.addWidget(score_note, 4, 0, 1, 2)
scroll_layout.addWidget(basic_group)
# 目标模型
target_group = QGroupBox("目标模型")
target_layout = QGridLayout(target_group)
target_layout.addWidget(QLabel("目标模型:"), 0, 0)
self.target_model_combo = QComboBox()
target_layout.addWidget(self.target_model_combo, 0, 1)
target_layout.addWidget(QLabel("交互方式:"), 1, 0)
self.interaction_combo = QComboBox()
self.interaction_combo.addItems(["鼠标拖拽", "点击触发"])
target_layout.addWidget(self.interaction_combo, 1, 1)
scroll_layout.addWidget(target_group)
# 位置配置
pos_group = QGroupBox("位置配置")
pos_layout = QGridLayout(pos_group)
# 添加说明文字
pos_note = QLabel("注:拖拽交互支持自由拖拽,无轴向限制。以下为参考目标位置:")
pos_note.setWordWrap(True)
pos_note.setStyleSheet("color: #666; font-style: italic;")
pos_layout.addWidget(pos_note, 0, 0, 1, 2)
pos_layout.addWidget(QLabel("参考位置 X:"), 1, 0)
self.pos_x_spin = QDoubleSpinBox()
self.pos_x_spin.setRange(-1000, 1000)
pos_layout.addWidget(self.pos_x_spin, 1, 1)
pos_layout.addWidget(QLabel("参考位置 Y:"), 2, 0)
self.pos_y_spin = QDoubleSpinBox()
self.pos_y_spin.setRange(-1000, 1000)
pos_layout.addWidget(self.pos_y_spin, 2, 1)
pos_layout.addWidget(QLabel("参考位置 Z:"), 3, 0)
self.pos_z_spin = QDoubleSpinBox()
self.pos_z_spin.setRange(-1000, 1000)
pos_layout.addWidget(self.pos_z_spin, 3, 1)
scroll_layout.addWidget(pos_group)
# 动画配置(仅点击触发模式)
animation_group = QGroupBox("动画配置")
animation_layout = QGridLayout(animation_group)
# 动画时长
animation_layout.addWidget(QLabel("动画时长(秒):"), 0, 0)
self.animation_duration_spin = QDoubleSpinBox()
self.animation_duration_spin.setRange(0.1, 10.0)
self.animation_duration_spin.setValue(2.0)
self.animation_duration_spin.setSingleStep(0.1)
self.animation_duration_spin.setDecimals(1)
animation_layout.addWidget(self.animation_duration_spin, 0, 1)
# 说明文字
animation_note = QLabel("注:仅在'点击触发'交互方式下有效,用于控制模型自动移动的速度。")
animation_note.setWordWrap(True)
animation_note.setStyleSheet("color: #666; font-style: italic;")
animation_layout.addWidget(animation_note, 1, 0, 1, 2)
scroll_layout.addWidget(animation_group)
# 提示配置
hint_group = QGroupBox("提示配置")
hint_layout = QGridLayout(hint_group)
hint_layout.addWidget(QLabel("提示音频:"), 0, 0)
audio_layout = QHBoxLayout()
self.audio_edit = QLineEdit()
self.browse_audio_button = QPushButton("浏览...")
audio_layout.addWidget(self.audio_edit)
audio_layout.addWidget(self.browse_audio_button)
hint_layout.addLayout(audio_layout, 0, 1)
hint_layout.addWidget(QLabel("提示文字:"), 1, 0)
self.hint_text_edit = QTextEdit()
self.hint_text_edit.setMaximumHeight(60)
hint_layout.addWidget(self.hint_text_edit, 1, 1)
scroll_layout.addWidget(hint_group)
# 工具配置
tool_group = QGroupBox("使用工具")
tool_layout = QGridLayout(tool_group)
tool_layout.addWidget(QLabel("所需工具:"), 0, 0)
self.required_tool_combo = QComboBox()
self.required_tool_combo.addItem("")
tool_layout.addWidget(self.required_tool_combo, 0, 1)
scroll_layout.addWidget(tool_group)
scroll_area.setWidget(scroll_content)
right_layout.addWidget(scroll_area)
splitter.addWidget(right_group)
return widget
def createToolsTab(self):
"""创建工具配置标签页"""
widget = QWidget()
layout = QVBoxLayout(widget)
# 说明文字
info_label = QLabel("配置拆装过程中需要使用的工具:")
layout.addWidget(info_label)
# 工具添加
add_layout = QHBoxLayout()
add_layout.addWidget(QLabel("工具名称:"))
self.tool_name_edit = QLineEdit()
self.add_tool_button = QPushButton("添加工具")
add_layout.addWidget(self.tool_name_edit)
add_layout.addWidget(self.add_tool_button)
add_layout.addStretch()
layout.addLayout(add_layout)
# 工具列表
self.tools_list = QListWidget()
layout.addWidget(self.tools_list)
# 工具操作按钮
tool_buttons = QHBoxLayout()
self.edit_tool_button = QPushButton("编辑工具")
self.delete_tool_button = QPushButton("删除工具")
tool_buttons.addWidget(self.edit_tool_button)
tool_buttons.addWidget(self.delete_tool_button)
tool_buttons.addStretch()
layout.addLayout(tool_buttons)
return widget
def createSettingsTab(self):
"""创建全局设置标签页"""
widget = QWidget()
layout = QVBoxLayout(widget)
# 说明文字
info_label = QLabel("配置拆装交互的全局设置:")
layout.addWidget(info_label)
# 设置组
settings_group = QGroupBox("全局设置")
settings_layout = QGridLayout(settings_group)
# 自动播放提示音
self.auto_audio_check = QCheckBox("自动播放步骤提示音")
self.auto_audio_check.setChecked(True)
settings_layout.addWidget(self.auto_audio_check, 0, 0, 1, 2)
# 显示提示文字
self.show_hint_check = QCheckBox("显示步骤提示文字")
self.show_hint_check.setChecked(True)
settings_layout.addWidget(self.show_hint_check, 1, 0, 1, 2)
# 步骤完成确认
self.confirm_check = QCheckBox("步骤完成需要确认")
settings_layout.addWidget(self.confirm_check, 2, 0, 1, 2)
layout.addWidget(settings_group)
# 预设配置
presets_group = QGroupBox("预设配置")
presets_layout = QVBoxLayout(presets_group)
self.presets_list = QListWidget()
presets_layout.addWidget(self.presets_list)
preset_buttons = QHBoxLayout()
self.save_preset_button = QPushButton("保存为预设")
self.load_preset_button = QPushButton("加载预设")
self.delete_preset_button = QPushButton("删除预设")
preset_buttons.addWidget(self.save_preset_button)
preset_buttons.addWidget(self.load_preset_button)
preset_buttons.addWidget(self.delete_preset_button)
presets_layout.addLayout(preset_buttons)
layout.addWidget(presets_group)
layout.addStretch()
return widget
def connectSignals(self):
"""连接信号槽"""
# 模型搜索
self.model_search_edit.textChanged.connect(self.filterSceneModels)
# 模型操作
self.add_model_button.clicked.connect(self.addModelsToConfig)
self.remove_model_button.clicked.connect(self.removeModelsFromConfig)
# 步骤操作
self.add_step_button.clicked.connect(self.addStep)
self.remove_step_button.clicked.connect(self.removeStep)
self.move_up_button.clicked.connect(self.moveStepUp)
self.move_down_button.clicked.connect(self.moveStepDown)
self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged)
# 步骤配置实时保存 - 使用专门的连接方法
self.connectStepConfigSignals()
# 工具操作
self.add_tool_button.clicked.connect(self.addTool)
self.edit_tool_button.clicked.connect(self.editTool)
self.delete_tool_button.clicked.connect(self.deleteTool)
# 文件操作
self.browse_audio_button.clicked.connect(self.browseAudioFile)
# 按钮操作
self.save_button.clicked.connect(self.saveConfiguration)
self.load_button.clicked.connect(self.loadConfiguration)
self.preview_button.clicked.connect(self.previewConfiguration)
self.apply_button.clicked.connect(self.applyConfiguration)
def loadSceneModels(self):
"""加载场景中的模型"""
if not self.world:
return
self.scene_models_tree.clear()
self._addNodeToTree(self.world.render, self.scene_models_tree)
self.scene_models_tree.expandAll()
def _addNodeToTree(self, node, parent_item):
"""递归添加节点到树形控件"""
if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']:
for child in node.getChildren():
self._addNodeToTree(child, parent_item)
return
if isinstance(parent_item, QTreeWidget):
item = QTreeWidgetItem(parent_item)
else:
item = QTreeWidgetItem(parent_item)
item.setText(0, node.getName())
item.setData(0, Qt.UserRole, node)
for child in node.getChildren():
self._addNodeToTree(child, item)
def filterSceneModels(self, search_text):
"""根据搜索文本过滤场景模型"""
def filterItems(item):
item_text = item.text(0).lower()
search_lower = search_text.lower()
# 检查当前项是否匹配
matches = search_lower in item_text if search_text else True
# 检查子项
child_matches = False
for i in range(item.childCount()):
child_item = item.child(i)
if filterItems(child_item):
child_matches = True
# 显示/隐藏项
item.setHidden(not (matches or child_matches))
return matches or child_matches
# 从根项开始过滤
for i in range(self.scene_models_tree.topLevelItemCount()):
filterItems(self.scene_models_tree.topLevelItem(i))
def addModelsToConfig(self):
"""添加选中的模型到配置"""
selected_items = self.scene_models_tree.selectedItems()
for item in selected_items:
node = item.data(0, Qt.UserRole)
if node:
model_name = node.getName()
existing_items = self.config_models_list.findItems(model_name, Qt.MatchExactly)
if not existing_items:
list_item = QListWidgetItem(model_name)
list_item.setData(Qt.UserRole, node)
self.config_models_list.addItem(list_item)
self.config_data['models'].append({
'name': model_name,
'node': node,
'original_pos': node.getPos(),
'original_hpr': node.getHpr()
})
self.target_model_combo.addItem(model_name)
def removeModelsFromConfig(self):
"""从配置中移除选中的模型"""
selected_items = self.config_models_list.selectedItems()
for item in selected_items:
model_name = item.text()
row = self.config_models_list.row(item)
self.config_models_list.takeItem(row)
self.config_data['models'] = [
model for model in self.config_data['models']
if model['name'] != model_name
]
index = self.target_model_combo.findText(model_name)
if index >= 0:
self.target_model_combo.removeItem(index)
def addStep(self):
"""添加步骤"""
step_count = len(self.config_data['steps']) + 1
step_name = f"步骤 {step_count}"
step_data = {
'name': step_name,
'description': '',
'type': '拆卸',
'target_model': '',
'interaction_type': '鼠标拖拽',
'target_position': [0, 0, 0],
'audio_file': '',
'hint_text': '',
'required_tool': '',
'score': 10 # 默认分值
}
self.config_data['steps'].append(step_data)
# 显示步骤名称和分值
display_text = f"{step_name} ({step_data['score']}分)"
list_item = QListWidgetItem(display_text)
list_item.setData(Qt.UserRole, step_data)
self.steps_list.addItem(list_item)
# 设置当前编辑的步骤项并选中
self._current_step_item = list_item
self.steps_list.setCurrentItem(list_item)
# 清空界面并显示新步骤的默认配置
self.loadStepConfiguration(step_data)
def removeStep(self):
"""删除步骤"""
current_item = self.steps_list.currentItem()
if not current_item:
QMessageBox.warning(self, "警告", "请选择要删除的步骤")
return
row = self.steps_list.row(current_item)
self.steps_list.takeItem(row)
self.config_data['steps'].pop(row)
# 如果删除的是当前编辑的步骤,清空当前步骤项
if self._current_step_item == current_item:
self._current_step_item = None
self.renumberSteps()
def moveStepUp(self):
"""上移步骤"""
current_row = self.steps_list.currentRow()
if current_row <= 0:
return
self.config_data['steps'][current_row], self.config_data['steps'][current_row - 1] = \
self.config_data['steps'][current_row - 1], self.config_data['steps'][current_row]
current_item = self.steps_list.takeItem(current_row)
self.steps_list.insertItem(current_row - 1, current_item)
self.steps_list.setCurrentRow(current_row - 1)
self.renumberSteps()
def moveStepDown(self):
"""下移步骤"""
current_row = self.steps_list.currentRow()
if current_row >= self.steps_list.count() - 1:
return
self.config_data['steps'][current_row], self.config_data['steps'][current_row + 1] = \
self.config_data['steps'][current_row + 1], self.config_data['steps'][current_row]
current_item = self.steps_list.takeItem(current_row)
self.steps_list.insertItem(current_row + 1, current_item)
self.steps_list.setCurrentRow(current_row + 1)
self.renumberSteps()
def renumberSteps(self):
"""重新编号步骤"""
for i in range(self.steps_list.count()):
item = self.steps_list.item(i)
step_data = item.data(Qt.UserRole)
new_name = f"步骤 {i + 1}"
step_data['name'] = new_name
step_score = step_data.get('score', 10)
display_text = f"{new_name} ({step_score}分)"
item.setText(display_text)
def onStepSelectionChanged(self):
"""步骤选择改变"""
# 简单直接的切换逻辑
current_item = self.steps_list.currentItem()
if not current_item:
self._current_step_item = None
self._clearStepConfigurationUI()
return
# 记录当前步骤项并加载配置
self._current_step_item = current_item
self.loadStepConfiguration(current_item.data(Qt.UserRole))
def _clearStepConfigurationUI(self):
"""清空步骤配置界面"""
self.disconnectStepConfigSignals()
try:
self.step_name_edit.setText("")
self.step_desc_edit.setPlainText("")
self.step_type_combo.setCurrentIndex(0)
self.target_model_combo.setCurrentIndex(0)
self.interaction_combo.setCurrentIndex(0)
self.pos_x_spin.setValue(0)
self.pos_y_spin.setValue(0)
self.pos_z_spin.setValue(0)
self.audio_edit.setText("")
self.hint_text_edit.setPlainText("")
self.required_tool_combo.setCurrentIndex(0)
finally:
self.connectStepConfigSignals()
def loadStepConfiguration(self, step_data):
"""加载步骤配置到界面"""
# 临时断开信号,避免加载时触发保存
self.disconnectStepConfigSignals()
try:
# 加载所有配置项到界面
self.step_name_edit.setText(step_data.get('name', ''))
self.step_desc_edit.setPlainText(step_data.get('description', ''))
self.step_type_combo.setCurrentText(step_data.get('type', '拆卸'))
target_model = step_data.get('target_model', '')
index = self.target_model_combo.findText(target_model)
if index >= 0:
self.target_model_combo.setCurrentIndex(index)
else:
self.target_model_combo.setCurrentIndex(0)
self.interaction_combo.setCurrentText(step_data.get('interaction_type', '鼠标拖拽'))
target_pos = step_data.get('target_position', [0, 0, 0])
self.pos_x_spin.setValue(target_pos[0])
self.pos_y_spin.setValue(target_pos[1])
self.pos_z_spin.setValue(target_pos[2])
# 加载动画时长
self.animation_duration_spin.setValue(step_data.get('animation_duration', 2.0))
self.audio_edit.setText(step_data.get('audio_file', ''))
self.hint_text_edit.setPlainText(step_data.get('hint_text', ''))
# 加载工具配置
required_tool = step_data.get('required_tool', '')
index = self.required_tool_combo.findText(required_tool)
if index >= 0:
self.required_tool_combo.setCurrentIndex(index)
else:
self.required_tool_combo.setCurrentIndex(0)
# 加载分值配置
self.step_score_spin.setValue(step_data.get('score', 10))
finally:
# 确保信号重新连接
self.connectStepConfigSignals()
def disconnectStepConfigSignals(self):
"""断开步骤配置的信号连接"""
try:
# 精确断开特定的信号连接
self.step_name_edit.textChanged.disconnect(self.saveCurrentStepConfig)
self.step_desc_edit.textChanged.disconnect(self.saveCurrentStepConfig)
self.step_type_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig)
self.target_model_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig)
self.interaction_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig)
self.pos_x_spin.valueChanged.disconnect(self.saveCurrentStepConfig)
self.pos_y_spin.valueChanged.disconnect(self.saveCurrentStepConfig)
self.pos_z_spin.valueChanged.disconnect(self.saveCurrentStepConfig)
self.animation_duration_spin.valueChanged.disconnect(self.saveCurrentStepConfig)
self.audio_edit.textChanged.disconnect(self.saveCurrentStepConfig)
self.hint_text_edit.textChanged.disconnect(self.saveCurrentStepConfig)
self.required_tool_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig)
self.step_score_spin.valueChanged.disconnect(self.saveCurrentStepConfig)
except:
# 忽略断开连接时的错误
pass
def connectStepConfigSignals(self):
"""连接步骤配置的信号"""
# 重新连接信号到保存方法
self.step_name_edit.textChanged.connect(self.saveCurrentStepConfig)
self.step_desc_edit.textChanged.connect(self.saveCurrentStepConfig)
self.step_type_combo.currentTextChanged.connect(self.saveCurrentStepConfig)
self.target_model_combo.currentTextChanged.connect(self.saveCurrentStepConfig)
self.interaction_combo.currentTextChanged.connect(self.saveCurrentStepConfig)
self.pos_x_spin.valueChanged.connect(self.saveCurrentStepConfig)
self.pos_y_spin.valueChanged.connect(self.saveCurrentStepConfig)
self.pos_z_spin.valueChanged.connect(self.saveCurrentStepConfig)
self.animation_duration_spin.valueChanged.connect(self.saveCurrentStepConfig)
self.audio_edit.textChanged.connect(self.saveCurrentStepConfig)
self.hint_text_edit.textChanged.connect(self.saveCurrentStepConfig)
self.required_tool_combo.currentTextChanged.connect(self.saveCurrentStepConfig)
self.step_score_spin.valueChanged.connect(self.saveCurrentStepConfig)
def saveCurrentStepConfig(self):
"""保存当前步骤的配置到list_item的UserRole数据中"""
# 检查当前步骤项是否有效
if not hasattr(self, '_current_step_item') or self._current_step_item is None:
return
# 获取当前步骤的数据
step_data = self._current_step_item.data(Qt.UserRole)
if not step_data:
return
# 从界面控件收集所有配置数据
step_data['name'] = self.step_name_edit.text()
step_data['description'] = self.step_desc_edit.toPlainText()
step_data['type'] = self.step_type_combo.currentText()
step_data['target_model'] = self.target_model_combo.currentText()
step_data['interaction_type'] = self.interaction_combo.currentText()
step_data['target_position'] = [
self.pos_x_spin.value(),
self.pos_y_spin.value(),
self.pos_z_spin.value()
]
step_data['animation_duration'] = self.animation_duration_spin.value()
step_data['audio_file'] = self.audio_edit.text()
step_data['hint_text'] = self.hint_text_edit.toPlainText()
step_data['required_tool'] = self.required_tool_combo.currentText()
step_data['score'] = int(self.step_score_spin.value()) # 保存分值
# 确保数据更新到list_item的UserRole中
self._current_step_item.setData(Qt.UserRole, step_data)
# 更新列表项的显示文本,包含分值信息
step_name = step_data['name']
step_score = step_data.get('score', 10)
display_text = f"{step_name} ({step_score}分)"
self._current_step_item.setText(display_text)
# 同时更新config_data中的对应数据保持数据一致性
current_row = self.steps_list.row(self._current_step_item)
if 0 <= current_row < len(self.config_data['steps']):
self.config_data['steps'][current_row] = step_data
def addTool(self):
"""添加工具"""
tool_name = self.tool_name_edit.text().strip()
if not tool_name:
QMessageBox.warning(self, "警告", "请输入工具名称")
return
existing_items = self.tools_list.findItems(tool_name, Qt.MatchExactly)
if existing_items:
QMessageBox.warning(self, "警告", "工具已存在")
return
self.tools_list.addItem(tool_name)
self.config_data['tools'].append({'name': tool_name, 'description': ''})
# 同时添加到步骤配置的工具下拉框中
self.required_tool_combo.addItem(tool_name)
self.tool_name_edit.clear()
def editTool(self):
"""编辑工具"""
current_item = self.tools_list.currentItem()
if not current_item:
QMessageBox.warning(self, "警告", "请选择要编辑的工具")
return
QMessageBox.information(self, "提示", "工具编辑功能待实现")
def deleteTool(self):
"""删除工具"""
current_item = self.tools_list.currentItem()
if not current_item:
QMessageBox.warning(self, "警告", "请选择要删除的工具")
return
tool_name = current_item.text()
row = self.tools_list.row(current_item)
self.tools_list.takeItem(row)
self.config_data['tools'] = [
tool for tool in self.config_data['tools']
if tool['name'] != tool_name
]
# 同时从步骤配置的工具下拉框中移除
index = self.required_tool_combo.findText(tool_name)
if index >= 0:
self.required_tool_combo.removeItem(index)
def browseAudioFile(self):
"""浏览音频文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择音频文件", "",
"音频文件 (*.wav *.mp3 *.ogg);;所有文件 (*)"
)
if file_path:
self.audio_edit.setText(file_path)
def saveConfiguration(self):
"""保存配置"""
file_path, _ = QFileDialog.getSaveFileName(
self, "保存拆装配置", "",
"JSON文件 (*.json);;所有文件 (*)"
)
if file_path:
try:
save_data = {
'models': [
{
'name': model['name'],
'original_pos': list(model['original_pos']),
'original_hpr': list(model['original_hpr']),
'node_path': self._getNodePath(model['node']),
'parent_name': model['node'].getParent().getName() if model['node'].getParent() else None,
'node_type': type(model['node']).__name__
} for model in self.config_data['models']
],
'steps': self.config_data['steps'],
'tools': self.config_data['tools'],
'settings': {
'auto_play_audio': self.auto_audio_check.isChecked(),
'show_hint_text': self.show_hint_check.isChecked(),
'require_confirmation': self.confirm_check.isChecked()
}
}
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(save_data, f, ensure_ascii=False, indent=2)
QMessageBox.information(self, "成功", "配置保存成功")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}")
def loadConfiguration(self):
"""加载配置"""
file_path, _ = QFileDialog.getOpenFileName(
self, "加载拆装配置", "",
"JSON文件 (*.json);;所有文件 (*)"
)
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
self.loadConfigurationData(data)
QMessageBox.information(self, "成功", "配置加载成功")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载配置失败: {str(e)}")
def loadConfigurationData(self, data):
"""加载配置数据到界面"""
# 清空现有配置
self.config_models_list.clear()
self.target_model_combo.clear()
self.steps_list.clear()
# 清空配置数据中的模型信息
self.config_data['models'] = []
# 重置当前编辑的步骤项
self._current_step_item = None
# 加载模型配置
self._loadModelConfiguration(data.get('models', []))
# 加载步骤配置
self.config_data['steps'] = data.get('steps', [])
for step_data in self.config_data['steps']:
step_name = step_data['name']
step_score = step_data.get('score', 10)
display_text = f"{step_name} ({step_score}分)"
list_item = QListWidgetItem(display_text)
list_item.setData(Qt.UserRole, step_data)
self.steps_list.addItem(list_item)
# 加载工具配置
self.tools_list.clear()
self.required_tool_combo.clear()
self.required_tool_combo.addItem("")
self.config_data['tools'] = data.get('tools', [])
for tool in self.config_data['tools']:
self.tools_list.addItem(tool['name'])
self.required_tool_combo.addItem(tool['name'])
# 加载全局设置
settings = data.get('settings', {})
self.auto_audio_check.setChecked(settings.get('auto_play_audio', True))
self.show_hint_check.setChecked(settings.get('show_hint_text', True))
self.confirm_check.setChecked(settings.get('require_confirmation', False))
def _loadModelConfiguration(self, saved_models):
"""加载模型配置,通过路径精确匹配场景中的模型节点"""
if not saved_models or not self.world:
return
# 遍历保存的模型配置
for saved_model in saved_models:
model_name = saved_model.get('name', '')
node_path = saved_model.get('node_path', '')
parent_name = saved_model.get('parent_name', '')
scene_node = None
# 方法1: 优先使用路径匹配(最精确)
if node_path:
scene_node = self._findNodeByPath(self.world.render, node_path)
if scene_node:
print(f"通过路径找到模型: {model_name} -> {node_path}")
# 方法2: 如果路径匹配失败,尝试名称+父节点匹配
if not scene_node and parent_name:
scene_node = self._findNodeByNameAndParent(self.world.render, model_name, parent_name)
if scene_node:
print(f"通过名称+父节点找到模型: {model_name} (父节点: {parent_name})")
# 方法3: 最后尝试简单的名称匹配
if not scene_node:
scene_model_map = {}
self._buildSceneModelMap(self.world.render, scene_model_map)
if model_name in scene_model_map:
scene_node = scene_model_map[model_name]
print(f"通过名称找到模型: {model_name}")
# 如果找到了对应的节点,添加到配置中
if scene_node:
# 添加到已配置模型列表
list_item = QListWidgetItem(model_name)
list_item.setData(Qt.UserRole, scene_node)
self.config_models_list.addItem(list_item)
# 添加到目标模型下拉框
self.target_model_combo.addItem(model_name)
# 更新config_data中的模型信息保留原始位置信息
original_pos = saved_model.get('original_pos', [0, 0, 0])
original_hpr = saved_model.get('original_hpr', [0, 0, 0])
self.config_data['models'].append({
'name': model_name,
'node': scene_node,
'original_pos': tuple(original_pos),
'original_hpr': tuple(original_hpr)
})
else:
# 如果场景中找不到对应的模型,给出详细提示
print(f"警告: 无法在场景中找到模型 '{model_name}'")
if node_path:
print(f" - 尝试的路径: {node_path}")
if parent_name:
print(f" - 预期父节点: {parent_name}")
print(f" - 请检查场景结构是否发生变化")
def _buildSceneModelMap(self, node, model_map):
"""递归构建场景模型名称到节点的映射"""
if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']:
# 跳过系统节点,但遍历其子节点
for child in node.getChildren():
self._buildSceneModelMap(child, model_map)
return
# 将当前节点添加到映射中
model_name = node.getName()
model_map[model_name] = node
# 递归处理子节点
for child in node.getChildren():
self._buildSceneModelMap(child, model_map)
def _getNodePath(self, node):
"""获取节点的完整路径,用于精确定位"""
if not node:
return ""
path_parts = []
current_node = node
# 从当前节点向上遍历到根节点,构建路径
while current_node and current_node.getName() != 'render':
path_parts.append(current_node.getName())
current_node = current_node.getParent()
# 反转路径,从根到叶
path_parts.reverse()
return '/'.join(path_parts) if path_parts else node.getName()
def _findNodeByPath(self, root_node, node_path):
"""根据路径在场景中查找节点"""
if not node_path:
return None
# 分割路径
path_parts = node_path.split('/')
current_node = root_node
# 沿着路径查找
for part in path_parts:
found = False
for child in current_node.getChildren():
if child.getName() == part:
current_node = child
found = True
break
if not found:
return None
return current_node
def _findNodeByNameAndParent(self, root_node, node_name, parent_name):
"""根据节点名称和父节点名称查找节点"""
def search_recursive(node):
# 检查当前节点的所有子节点
for child in node.getChildren():
# 如果找到匹配的节点名称和父节点名称
if (child.getName() == node_name and
child.getParent() and
child.getParent().getName() == parent_name):
return child
# 递归搜索子节点
result = search_recursive(child)
if result:
return result
return None
return search_recursive(root_node)
def previewConfiguration(self):
"""预览配置效果"""
QMessageBox.information(self, "提示", "预览功能待实现")
def applyConfiguration(self):
"""应用配置"""
QMessageBox.information(self, "提示", "应用配置功能待实现")