1069 lines
44 KiB
Python
1069 lines
44 KiB
Python
# -*- 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, "提示", "应用配置功能待实现") |