2339 lines
104 KiB
Python
2339 lines
104 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
import json
|
||
import os
|
||
from direct.showbase.DirectObject import DirectObject
|
||
from direct.task import Task
|
||
from panda3d.core import CollisionTraverser, CollisionNode, CollisionHandlerQueue, CollisionRay
|
||
from panda3d.core import BitMask32, Vec3, Point3, Plane
|
||
from PyQt5.QtWidgets import QMessageBox, QFileDialog, QDialog, QVBoxLayout, QHBoxLayout
|
||
from PyQt5.QtWidgets import QLabel, QPushButton, QTextEdit, QComboBox, QGroupBox
|
||
from PyQt5.QtCore import Qt
|
||
|
||
# 导入维修系统GUI
|
||
try:
|
||
from core.maintenance_gui import MaintenanceGUI
|
||
except ImportError as e:
|
||
print(f"⚠️ 导入维修GUI失败: {e}")
|
||
MaintenanceGUI = None
|
||
|
||
|
||
class AssemblyInteractionManager(DirectObject):
|
||
"""拆装交互管理器(优化版)"""
|
||
|
||
def __init__(self, world):
|
||
DirectObject.__init__(self)
|
||
self.world = world
|
||
self.config_data = None
|
||
self.current_step = 0
|
||
self.total_steps = 0
|
||
self.is_active = False
|
||
|
||
# --- 模式控制 ---
|
||
self.mode = "training" # 默认训练模式,可选 "training" 或 "exam"
|
||
|
||
# --- 拖拽相关(优化) ---
|
||
self.dragging_model = None
|
||
self.drag_offset = Vec3(0, 0, 0)
|
||
self.drag_plane = Plane(Vec3(0, 1, 0), Point3(0, 0, 0)) # 初始化一个平面
|
||
|
||
# --- 安装/拆卸状态管理 ---
|
||
self.model_original_positions = {} # 记录模型的原始位置
|
||
self.model_current_states = {} # 记录模型当前状态 ('installed' 或 'removed')
|
||
|
||
# --- UI组件 ---
|
||
self.step_dialog = None
|
||
|
||
# --- 维修系统GUI ---
|
||
self.maintenance_gui = None
|
||
if MaintenanceGUI:
|
||
self.maintenance_gui = MaintenanceGUI(world)
|
||
|
||
# --- 操作权限控制 ---
|
||
self.operation_enabled = True # 是否允许进行操作
|
||
|
||
# --- 考核相关 ---
|
||
self.exam_score = 0 # 考核总分
|
||
self.exam_max_score = 0 # 考核满分
|
||
self.step_scores = {} # 每步得分记录
|
||
|
||
# --- 碰撞检测(优化) ---
|
||
self.picker_traverser = CollisionTraverser('picker_traverser')
|
||
self.collision_handler = CollisionHandlerQueue()
|
||
self.picker_ray_node = None
|
||
|
||
print("拆装交互管理器初始化完成")
|
||
|
||
def init_exam_mode(self):
|
||
"""初始化考核模式"""
|
||
print("📝 初始化考核模式...")
|
||
|
||
# 重置考核数据
|
||
self.exam_score = 0
|
||
self.exam_max_score = 0
|
||
self.step_scores = {}
|
||
|
||
# 计算总分
|
||
steps = self.config_data.get('steps', [])
|
||
for i, step_data in enumerate(steps):
|
||
step_score = step_data.get('score', 10) # 默认每步10分
|
||
self.exam_max_score += step_score
|
||
self.step_scores[i] = {
|
||
'max_score': step_score,
|
||
'current_score': step_score, # 初始满分,操作错误时扣分
|
||
'tool_error': False, # 是否有工具错误
|
||
'operation_attempts': 0 # 操作尝试次数
|
||
}
|
||
|
||
print(f"📝 考核模式初始化完成,总分: {self.exam_max_score} 分")
|
||
|
||
def play_step_audio(self, step_data):
|
||
"""播放步骤音频"""
|
||
try:
|
||
# 考核模式下不播放音频
|
||
if self.mode == "exam":
|
||
print("🔇 考核模式,音频播放已禁用")
|
||
return
|
||
|
||
# 检查是否启用了自动播放音频
|
||
settings = self.config_data.get('settings', {})
|
||
auto_play_audio = settings.get('auto_play_audio', True)
|
||
|
||
if not auto_play_audio:
|
||
print("🔇 音频播放已禁用")
|
||
return
|
||
|
||
# 获取音频文件路径
|
||
audio_file = step_data.get('audio_file', '')
|
||
|
||
if not audio_file or audio_file.strip() == '':
|
||
print("🔇 当前步骤没有配置音频文件")
|
||
return
|
||
|
||
print(f"🔊 准备播放步骤音频: {audio_file}")
|
||
|
||
import os
|
||
# 检查音频文件格式
|
||
file_ext = os.path.splitext(audio_file)[1].lower()
|
||
print(f"🔊 音频文件格式: {file_ext}")
|
||
|
||
# 对于MP3文件,优先使用pygame播放
|
||
if file_ext in ['.mp3', '.m4a', '.aac']:
|
||
print("🔊 检测到MP3/M4A/AAC格式,优先尝试pygame播放")
|
||
if self.play_audio_with_pygame(audio_file):
|
||
return
|
||
print("🔊 pygame不可用,尝试备用播放方法")
|
||
self.play_audio_fallback(audio_file)
|
||
return
|
||
|
||
# 检查音频文件是否存在
|
||
import os
|
||
if not os.path.isfile(audio_file):
|
||
# 如果不是绝对路径,尝试在项目目录下查找
|
||
if not os.path.isabs(audio_file):
|
||
# 尝试在当前目录、Resources目录等位置查找
|
||
possible_paths = [
|
||
audio_file,
|
||
os.path.join("Resources", audio_file),
|
||
os.path.join("Resources", "audio", audio_file),
|
||
os.path.join("audio", audio_file),
|
||
os.path.join("sounds", audio_file)
|
||
]
|
||
|
||
audio_file_found = None
|
||
for path in possible_paths:
|
||
if os.path.isfile(path):
|
||
audio_file_found = path
|
||
break
|
||
|
||
if audio_file_found:
|
||
audio_file = audio_file_found
|
||
print(f"🔊 找到音频文件: {audio_file}")
|
||
else:
|
||
print(f"❌ 找不到音频文件: {audio_file}")
|
||
print(f" 尝试的路径: {possible_paths}")
|
||
return
|
||
else:
|
||
print(f"❌ 音频文件不存在: {audio_file}")
|
||
return
|
||
|
||
# 使用Panda3D的音频系统播放音频
|
||
if hasattr(self.world, 'loader') and self.world.loader:
|
||
try:
|
||
# 检查音频系统是否启用
|
||
from panda3d.core import AudioManager
|
||
print(f"🔊 音频管理器状态: {AudioManager.createAudioManager()}")
|
||
|
||
# 加载音频文件
|
||
print(f"🔊 尝试加载音频文件: {audio_file}")
|
||
audio_sound = self.world.loader.loadSfx(audio_file)
|
||
|
||
if audio_sound:
|
||
print(f"✅ 音频文件加载成功: {os.path.basename(audio_file)}")
|
||
print(f"🔊 音频状态: {audio_sound.status()}")
|
||
audio_length = audio_sound.length()
|
||
print(f"🔊 音频长度: {audio_length:.2f}秒")
|
||
|
||
# 检查音频文件是否有效
|
||
if audio_length <= 0:
|
||
print("⚠️ 音频长度为0,可能是格式不支持或文件损坏")
|
||
print("🔊 尝试使用pygame播放")
|
||
if not self.play_audio_with_pygame(audio_file):
|
||
self.play_audio_fallback(audio_file)
|
||
return
|
||
|
||
# 设置音量(在播放前设置)
|
||
audio_sound.setVolume(0.8)
|
||
print(f"🔊 设置音量: 80%")
|
||
|
||
# 播放音频
|
||
audio_sound.play()
|
||
print(f"✅ 开始播放音频: {os.path.basename(audio_file)}")
|
||
|
||
# 等待一小段时间让播放状态更新
|
||
import time
|
||
time.sleep(0.1)
|
||
|
||
final_status = audio_sound.status()
|
||
print(f"🔊 播放后状态: {final_status}")
|
||
|
||
# 检查是否真的在播放 (状态码2表示PLAYING)
|
||
if final_status == 2: # PLAYING
|
||
print("✅ 音频正在播放中")
|
||
elif final_status == 1: # READY
|
||
print("⚠️ 音频处于就绪状态但未播放,尝试pygame")
|
||
if not self.play_audio_with_pygame(audio_file):
|
||
self.play_audio_fallback(audio_file)
|
||
else:
|
||
print(f"⚠️ 音频状态异常: {final_status}")
|
||
if not self.play_audio_with_pygame(audio_file):
|
||
self.play_audio_fallback(audio_file)
|
||
else:
|
||
print(f"❌ 无法加载音频文件: {audio_file}")
|
||
print("🔊 Panda3D加载失败,尝试pygame")
|
||
if not self.play_audio_with_pygame(audio_file):
|
||
self.play_audio_fallback(audio_file)
|
||
|
||
except Exception as e:
|
||
print(f"❌ Panda3D音频播放失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
# 尝试使用pygame播放(备用方案)
|
||
print("🔊 Panda3D播放失败,尝试pygame")
|
||
if not self.play_audio_with_pygame(audio_file):
|
||
self.play_audio_fallback(audio_file)
|
||
else:
|
||
print("⚠️ 找不到Panda3D音频加载器,尝试pygame")
|
||
if not self.play_audio_with_pygame(audio_file):
|
||
self.play_audio_fallback(audio_file)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 音频播放失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def play_audio_with_pygame(self, audio_file):
|
||
"""使用pygame播放音频(优先方法)"""
|
||
try:
|
||
import pygame
|
||
print("🔊 使用pygame播放音频")
|
||
|
||
# 初始化pygame mixer
|
||
if not pygame.mixer.get_init():
|
||
pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024)
|
||
pygame.mixer.init()
|
||
print("🔊 pygame mixer初始化完成")
|
||
|
||
# 停止当前播放的音频(如果有)
|
||
if pygame.mixer.music.get_busy():
|
||
pygame.mixer.music.stop()
|
||
print("🔊 停止之前的音频")
|
||
|
||
# 加载并播放音频文件
|
||
pygame.mixer.music.load(audio_file)
|
||
pygame.mixer.music.set_volume(0.8)
|
||
pygame.mixer.music.play()
|
||
|
||
print(f"✅ pygame开始播放音频: {os.path.basename(audio_file)}")
|
||
|
||
# 在后台线程中监控播放完成
|
||
def wait_for_pygame_completion():
|
||
import time
|
||
while pygame.mixer.music.get_busy():
|
||
time.sleep(0.1)
|
||
print(f"🎵 pygame音频播放完成: {os.path.basename(audio_file)}")
|
||
|
||
import threading
|
||
threading.Thread(target=wait_for_pygame_completion, daemon=True).start()
|
||
|
||
print("🎵 音频将在后台完整播放")
|
||
return True
|
||
|
||
except ImportError:
|
||
print("⚠️ pygame未安装")
|
||
return False
|
||
except Exception as e:
|
||
print(f"⚠️ pygame播放失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def play_audio_fallback(self, audio_file):
|
||
"""备用音频播放方法"""
|
||
import os
|
||
print(f"🔊 尝试备用音频播放方法...")
|
||
|
||
# 方法1: 使用系统默认播放器(最简单)
|
||
try:
|
||
print("🔊 尝试使用系统默认播放器")
|
||
if os.name == 'nt': # Windows
|
||
os.startfile(audio_file)
|
||
print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}")
|
||
return
|
||
elif os.name == 'posix': # Linux/Mac
|
||
os.system(f'xdg-open "{audio_file}"')
|
||
print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}")
|
||
return
|
||
except Exception as e:
|
||
print(f"⚠️ 系统默认播放器失败: {e}")
|
||
|
||
# 方法2: 尝试使用Windows Media Player
|
||
try:
|
||
if os.name == 'nt': # Windows
|
||
print("🔊 尝试使用Windows Media Player")
|
||
cmd = f'start wmplayer "{audio_file}"'
|
||
os.system(cmd)
|
||
print(f"✅ 使用Windows Media Player播放音频: {os.path.basename(audio_file)}")
|
||
return
|
||
except Exception as e:
|
||
print(f"⚠️ Windows Media Player播放失败: {e}")
|
||
|
||
# 方法3: 使用PowerShell播放WAV文件
|
||
try:
|
||
if os.name == 'nt':
|
||
file_ext = os.path.splitext(audio_file)[1].lower()
|
||
if file_ext in ['.wav']:
|
||
print("🔊 尝试使用PowerShell播放WAV文件")
|
||
import subprocess
|
||
cmd = f'powershell -c "(New-Object Media.SoundPlayer \\"{audio_file}\\").PlaySync()"'
|
||
subprocess.run(cmd, shell=True)
|
||
print(f"✅ 使用PowerShell播放音频: {os.path.basename(audio_file)}")
|
||
return
|
||
except Exception as e:
|
||
print(f"⚠️ PowerShell播放失败: {e}")
|
||
|
||
# 方法4: 简单的系统调用
|
||
try:
|
||
print("🔊 尝试使用系统默认播放器")
|
||
if os.name == 'nt': # Windows
|
||
os.startfile(audio_file)
|
||
elif os.name == 'posix': # Linux/Mac
|
||
os.system(f'xdg-open "{audio_file}"')
|
||
print(f"✅ 使用系统默认播放器播放音频: {os.path.basename(audio_file)}")
|
||
except Exception as e:
|
||
print(f"❌ 所有备用音频播放方法都失败了: {e}")
|
||
print("💡 建议检查:")
|
||
print(" 1. 音频文件是否存在且格式正确")
|
||
print(" 2. 系统音频驱动是否正常")
|
||
print(" 3. 音频文件是否损坏")
|
||
|
||
def set_operation_enabled(self, enabled):
|
||
"""设置操作是否启用"""
|
||
self.operation_enabled = enabled
|
||
print(f"🔧 操作权限: {'启用' if enabled else '禁用'}")
|
||
|
||
def check_operation_permission(self):
|
||
"""检查操作权限"""
|
||
if not self.is_active:
|
||
print("⚠️ 交互模式未激活")
|
||
return False
|
||
|
||
# 获取当前步骤数据
|
||
if self.current_step >= self.total_steps:
|
||
print("⚠️ 所有步骤已完成")
|
||
return False
|
||
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
required_tool = step_data.get('required_tool', '无')
|
||
|
||
# 获取当前选择的工具(优先从维修GUI获取)
|
||
current_tool = "无"
|
||
if self.maintenance_gui:
|
||
current_tool = self.maintenance_gui.get_current_tool()
|
||
elif hasattr(self, 'step_dialog') and self.step_dialog and hasattr(self.step_dialog, 'tool_combo'):
|
||
current_tool = self.step_dialog.tool_combo.currentText()
|
||
|
||
# 使用 getChinese 处理工具名称
|
||
if hasattr(self.world, 'getChinese'):
|
||
required_tool = self.world.getChinese(required_tool)
|
||
current_tool = self.world.getChinese(current_tool)
|
||
|
||
print(f"🔧 工具检查: 需要'{required_tool}',当前'{current_tool}'")
|
||
|
||
# 检查工具是否匹配
|
||
tool_matches = self.check_tool_match(current_tool, required_tool)
|
||
|
||
if self.mode == "training":
|
||
# 训练模式:显示GUI警告并阻止错误操作
|
||
if not tool_matches:
|
||
print("⚠️ 训练模式 - 工具不匹配,无法进行操作")
|
||
|
||
# 在维修GUI中显示警告
|
||
if self.maintenance_gui:
|
||
warning_msg = f"请先选择 '{required_tool}' 工具!"
|
||
self.maintenance_gui.show_warning(warning_msg, 2.0)
|
||
# 也在Qt对话框中显示警告(仅在非维修系统模式下)
|
||
elif hasattr(self, 'step_dialog') and self.step_dialog:
|
||
QMessageBox.warning(self.step_dialog, "工具不匹配",
|
||
f"当前步骤需要使用 '{required_tool}' 工具,请先选择正确的工具!")
|
||
return False
|
||
return True
|
||
else:
|
||
# 考核模式:不显示提示,但记录工具错误并阻止操作
|
||
if not tool_matches:
|
||
print(f"❌ 考核模式 - 工具错误:需要'{required_tool}',实际选择'{current_tool}',操作被阻止")
|
||
|
||
# 记录工具错误(扣分)
|
||
self.record_tool_error()
|
||
return False
|
||
return True
|
||
|
||
def check_tool_match(self, current_tool, required_tool):
|
||
"""检查工具是否匹配"""
|
||
# 如果步骤不要求特定工具,任何工具都可以
|
||
if required_tool == "无" or required_tool == "" or not required_tool:
|
||
return True
|
||
|
||
# 精确匹配
|
||
if current_tool == required_tool:
|
||
return True
|
||
|
||
return False
|
||
|
||
def record_tool_error(self):
|
||
"""记录工具错误(考核模式下扣分)"""
|
||
if self.mode != "exam":
|
||
return
|
||
|
||
# 获取当前步骤的考核记录
|
||
if self.current_step in self.step_scores:
|
||
step_record = self.step_scores[self.current_step]
|
||
|
||
# 如果是第一次工具错误,扣除一定分数
|
||
if not step_record['tool_error']:
|
||
step_record['tool_error'] = True
|
||
# 扣除该步骤50%的分数作为工具错误惩罚
|
||
# penalty = step_record['max_score'] * 0.5
|
||
penalty = step_record['max_score']
|
||
step_record['current_score'] = max(0, step_record['current_score'] - penalty)
|
||
|
||
print(f"📝 考核记录:工具错误,扣除{penalty:.0f}分,当前步骤剩余{step_record['current_score']:.0f}分")
|
||
|
||
# 增加操作尝试次数
|
||
step_record['operation_attempts'] += 1
|
||
|
||
def start_interaction_mode(self, mode="training"):
|
||
"""启动交互模式"""
|
||
try:
|
||
# 设置模式
|
||
self.mode = mode
|
||
print(f"🎯 启动拆装交互模式: {self.mode}")
|
||
|
||
# 如果没有配置数据,尝试加载
|
||
if not self.config_data:
|
||
if not self.load_configuration():
|
||
return False
|
||
|
||
if not self.config_data or not self.config_data.get('steps'):
|
||
QMessageBox.warning(None, "警告", "没有找到有效的配置步骤!\n请先在'拆装配置'中设置步骤。")
|
||
return False
|
||
|
||
self.total_steps = len(self.config_data['steps'])
|
||
self.current_step = 0
|
||
self.is_active = True
|
||
|
||
# 设置维修系统GUI
|
||
if self.maintenance_gui:
|
||
tools_list = self.config_data.get('tools', ['手'])
|
||
self.maintenance_gui.setup_gui(tools_list, mode)
|
||
self.maintenance_gui.show_gui()
|
||
print(f"✅ 维修系统GUI已启动,模式: {mode}")
|
||
|
||
# 如果是考核模式,初始化考核相关数据
|
||
|
||
if self.mode == "exam":
|
||
self.init_exam_mode()
|
||
# 显示考核开始提示
|
||
if self.maintenance_gui:
|
||
exam_start_msg = f"📝 考核模式已启动\n\n总分: {self.exam_max_score} 分\n步骤数: {self.total_steps}\n\n✅ 可以选择和切换工具\n❌ 不会显示步骤描述和语音提示\n❌ 使用错误工具会扣分"
|
||
self.maintenance_gui.update_step_info(exam_start_msg)
|
||
print("📝 考核开始提示已在GUI中显示")
|
||
|
||
# 设置碰撞检测
|
||
self.setup_picking()
|
||
|
||
# 拦截Qt层面的鼠标事件处理
|
||
print("🔄 拦截Qt层面的鼠标事件处理")
|
||
print(f"🔍 检查world对象: {type(self.world).__name__}")
|
||
print(f"🔍 是否有qtWidget属性: {hasattr(self.world, 'qtWidget')}")
|
||
if hasattr(self.world, 'qtWidget'):
|
||
print(f"🔍 qtWidget值: {self.world.qtWidget}")
|
||
print(f"🔍 qtWidget类型: {type(self.world.qtWidget)}")
|
||
|
||
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||
# 备份原有的鼠标事件处理方法
|
||
self.original_mouse_press_event = self.world.qtWidget.mousePressEvent
|
||
self.original_mouse_release_event = self.world.qtWidget.mouseReleaseEvent
|
||
|
||
print(f"🔍 原有鼠标事件方法: {self.original_mouse_press_event}")
|
||
|
||
# 替换为我们的处理方法
|
||
self.world.qtWidget.mousePressEvent = self.qt_mouse_press_event
|
||
self.world.qtWidget.mouseReleaseEvent = self.qt_mouse_release_event
|
||
print("✅ Qt鼠标事件已被拦截")
|
||
else:
|
||
print("⚠️ 找不到Qt部件,使用Panda3D事件系统")
|
||
# 作为备用方案,仍然绑定Panda3D事件
|
||
self.accept("mouse1", self.on_mouse_down)
|
||
self.accept("mouse1-up", self.on_mouse_up)
|
||
|
||
# 绑定键盘备用触发
|
||
self.accept("space", self.trigger_drag_by_keyboard)
|
||
self.accept("s", self.show_models_status) # S键显示模型状态
|
||
self.accept("r", self.record_current_positions) # R键记录当前位置为原始位置
|
||
|
||
print("✅ 拆装交互事件绑定完成")
|
||
print("💡 提示: 按S键可查看所有模型状态,按R键重新记录原始位置")
|
||
|
||
print("拆装交互模式已启动,核心事件已绑定。")
|
||
print("请点击目标模型进行操作。")
|
||
|
||
# 调试:列出场景中的所有模型
|
||
self.list_scene_models()
|
||
|
||
# 显示步骤指引界面(维修系统使用GUI界面,跳过Qt对话框)
|
||
if not self.maintenance_gui:
|
||
self.show_step_dialog()
|
||
|
||
# 开始第一步
|
||
self.start_current_step()
|
||
|
||
print(f"共 {self.total_steps} 步")
|
||
return True
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(None, "错误", f"启动交互模式失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def load_configuration(self):
|
||
"""加载拆装配置"""
|
||
try:
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
None, "选择拆装配置文件", "",
|
||
"JSON文件 (*.json);;所有文件 (*)"
|
||
)
|
||
if not file_path:
|
||
return False
|
||
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
self.config_data = json.load(f)
|
||
|
||
self.total_steps = len(self.config_data.get('steps', []))
|
||
self.current_step = 0
|
||
|
||
# 从配置文件中恢复原始位置信息
|
||
self.load_original_positions_from_config()
|
||
|
||
print(f"成功加载配置: {file_path}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(None, "错误", f"加载配置失败: {str(e)}")
|
||
return False
|
||
|
||
def load_original_positions_from_config(self):
|
||
"""从配置文件中加载模型的原始位置信息"""
|
||
print("📍 从配置文件恢复模型原始位置...")
|
||
|
||
# 清空现有的位置记录
|
||
self.model_original_positions.clear()
|
||
self.model_current_states.clear()
|
||
|
||
# 从配置文件的models部分读取原始位置(models是数组格式)
|
||
models_config = self.config_data.get('models', [])
|
||
|
||
for model_data in models_config:
|
||
model_name = model_data.get('name')
|
||
if not model_name:
|
||
continue
|
||
|
||
# 尝试从配置文件中获取原始位置
|
||
original_pos_data = model_data.get('original_pos')
|
||
|
||
if original_pos_data and len(original_pos_data) >= 3:
|
||
# 如果配置文件中有原始位置,使用配置文件中的位置
|
||
original_pos = Vec3(original_pos_data[0], original_pos_data[1], original_pos_data[2])
|
||
self.model_original_positions[model_name] = original_pos
|
||
self.model_current_states[model_name] = 'installed'
|
||
print(f" 📍 {model_name}: 从配置文件恢复原始位置 {original_pos}")
|
||
else:
|
||
# 如果配置文件中没有原始位置,使用当前场景中的位置作为原始位置
|
||
target_node = self.find_model_node(model_name)
|
||
if target_node and not target_node.isEmpty():
|
||
current_pos = target_node.getPos()
|
||
self.model_original_positions[model_name] = current_pos
|
||
self.model_current_states[model_name] = 'installed'
|
||
print(f" 📍 {model_name}: 使用当前场景位置作为原始位置 {current_pos}")
|
||
else:
|
||
print(f" ❌ {model_name}: 在场景中找不到对应模型")
|
||
|
||
# 如果配置文件中没有models部分,从步骤中提取模型信息
|
||
if not models_config:
|
||
print(" 配置文件中没有models部分,从步骤中提取模型信息...")
|
||
for step_data in self.config_data.get('steps', []):
|
||
target_model_name = step_data.get('target_model')
|
||
if target_model_name and target_model_name not in self.model_original_positions:
|
||
target_node = self.find_model_node(target_model_name)
|
||
if target_node and not target_node.isEmpty():
|
||
current_pos = target_node.getPos()
|
||
self.model_original_positions[target_model_name] = current_pos
|
||
self.model_current_states[target_model_name] = 'installed'
|
||
print(f" 📍 {target_model_name}: 从场景记录原始位置 {current_pos}")
|
||
|
||
print(f"📍 原始位置恢复完成,共记录 {len(self.model_original_positions)} 个模型")
|
||
|
||
# 检查配置文件的步骤完整性
|
||
self.check_step_completeness()
|
||
|
||
def check_step_completeness(self):
|
||
"""检查配置文件的步骤完整性"""
|
||
print("\n🔍 检查步骤配置完整性...")
|
||
|
||
steps = self.config_data.get('steps', [])
|
||
if not steps:
|
||
print("⚠️ 配置文件中没有步骤")
|
||
return
|
||
|
||
# 统计每个模型的拆卸和安装步骤
|
||
model_operations = {}
|
||
|
||
for i, step in enumerate(steps):
|
||
target_model = step.get('target_model')
|
||
step_type = self.normalize_step_type(step)
|
||
|
||
if target_model:
|
||
if target_model not in model_operations:
|
||
model_operations[target_model] = {'disassemble': [], 'assemble': []}
|
||
|
||
model_operations[target_model][step_type].append(i + 1)
|
||
|
||
# 检查每个模型是否有配对的拆卸和安装步骤
|
||
incomplete_models = []
|
||
for model_name, operations in model_operations.items():
|
||
disassemble_count = len(operations['disassemble'])
|
||
assemble_count = len(operations['assemble'])
|
||
|
||
print(f" 📋 {model_name}: 拆卸步骤 {disassemble_count} 个, 安装步骤 {assemble_count} 个")
|
||
|
||
if disassemble_count > 0 and assemble_count == 0:
|
||
incomplete_models.append(model_name)
|
||
print(f" ⚠️ 只有拆卸步骤,缺少安装步骤")
|
||
elif disassemble_count != assemble_count:
|
||
print(f" ⚠️ 拆卸和安装步骤数量不匹配")
|
||
|
||
if incomplete_models:
|
||
print(f"\n⚠️ 发现 {len(incomplete_models)} 个模型缺少安装步骤:")
|
||
for model in incomplete_models:
|
||
print(f" - {model}")
|
||
print("💡 建议为每个拆卸的模型添加对应的安装步骤")
|
||
else:
|
||
print("\n✅ 所有模型都有完整的拆卸和安装步骤配置")
|
||
|
||
def setup_picking(self):
|
||
"""设置用于鼠标拾取的碰撞检测系统"""
|
||
try:
|
||
# 创建一个碰撞射线
|
||
picker_ray = CollisionRay()
|
||
|
||
# 创建一个用于放置射线的碰撞节点
|
||
picker_ray_node = CollisionNode('mouse_ray')
|
||
picker_ray_node.addSolid(picker_ray)
|
||
|
||
# 设置碰撞掩码,确保它只与可拾取的物体碰撞
|
||
# 这里我们假设可拾取的物体有第1位的碰撞比特
|
||
picker_ray_node.setFromCollideMask(BitMask32.bit(1))
|
||
picker_ray_node.setIntoCollideMask(BitMask32.allOff())
|
||
|
||
# 将碰撞节点附加到相机上,这样射线就会跟随相机移动
|
||
self.picker_ray_node = self.world.camera.attachNewNode(picker_ray_node)
|
||
|
||
# 将碰撞节点添加到遍历器中,并指定一个处理器来存储碰撞结果
|
||
self.picker_traverser.addCollider(self.picker_ray_node, self.collision_handler)
|
||
|
||
print("射线拾取系统设置完成")
|
||
|
||
except Exception as e:
|
||
print(f"设置碰撞检测失败: {e}")
|
||
|
||
def show_step_dialog(self):
|
||
"""显示步骤指引对话框"""
|
||
if self.step_dialog:
|
||
self.step_dialog.close()
|
||
|
||
# 获取主窗口作为父窗口
|
||
parent_window = None
|
||
if hasattr(self.world, 'main_window') and self.world.main_window:
|
||
parent_window = self.world.main_window
|
||
|
||
self.step_dialog = StepGuideDialog(self, parent_window)
|
||
self.step_dialog.show()
|
||
|
||
def start_current_step(self):
|
||
"""开始当前步骤"""
|
||
if self.current_step >= self.total_steps:
|
||
self.finish_interaction()
|
||
return
|
||
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
|
||
# 兼容中文和英文配置格式
|
||
operation_type = self.normalize_operation_type(step_data)
|
||
step_type = self.normalize_step_type(step_data)
|
||
target_model_name = step_data.get('target_model', '未设置')
|
||
|
||
# 调试信息:显示字段映射结果
|
||
print(f"🔄 字段映射: '{step_data.get('type', 'N/A')}' -> '{step_type}'")
|
||
print(f"🔄 操作映射: '{step_data.get('interaction_type', 'N/A')}' -> '{operation_type}'")
|
||
|
||
# 使用 getChinese 处理步骤名称
|
||
step_name = step_data.get('name', '未命名')
|
||
if hasattr(self.world, 'getChinese'):
|
||
step_name = self.world.getChinese(step_name)
|
||
target_model_name = self.world.getChinese(target_model_name)
|
||
|
||
print(f"\n=== 开始第 {self.current_step + 1} 步: {step_name} ===")
|
||
print(f"目标模型: {target_model_name}")
|
||
print(f"操作类型: {operation_type}")
|
||
print(f"步骤类型: {step_type}")
|
||
|
||
# 显示容差信息
|
||
if step_type == 'assemble':
|
||
snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0)
|
||
print(f"吸附容差: {snap_tolerance:.1f} 单位")
|
||
else:
|
||
disassemble_threshold = step_data.get('disassemble_threshold', 5.0)
|
||
print(f"拆卸阈值: {disassemble_threshold:.1f} 单位(距离原位)")
|
||
|
||
# 检查模型的原始位置记录
|
||
if target_model_name:
|
||
if target_model_name in self.model_original_positions:
|
||
original_pos = self.model_original_positions[target_model_name]
|
||
print(f"📍 模型 '{target_model_name}' 的原始位置: {original_pos}")
|
||
else:
|
||
# 如果没有记录,使用当前位置作为原始位置(但会给出警告)
|
||
target_node = self.find_model_node(target_model_name)
|
||
if target_node and not target_node.isEmpty():
|
||
current_pos = target_node.getPos()
|
||
self.model_original_positions[target_model_name] = current_pos
|
||
self.model_current_states[target_model_name] = 'installed'
|
||
print(f"⚠️ 模型 '{target_model_name}' 没有预设原始位置,使用当前位置: {current_pos}")
|
||
print(" 建议在配置文件中预设模型的original_position以确保一致性")
|
||
|
||
# 如果是安装步骤,确保模型显示
|
||
if step_type == 'assemble' and target_model_name:
|
||
target_node = self.find_model_node(target_model_name)
|
||
if target_node and not target_node.isEmpty():
|
||
if target_node.isHidden():
|
||
print(f"👁️ 安装步骤开始 - 显示隐藏的模型: {target_model_name}")
|
||
target_node.show()
|
||
else:
|
||
print(f"✅ 模型 '{target_model_name}' 已经可见")
|
||
|
||
# 播放步骤音频(如果有配置)
|
||
self.play_step_audio(step_data)
|
||
|
||
# 更新维修系统GUI的步骤信息
|
||
if self.maintenance_gui:
|
||
step_name = step_data.get('name', f'步骤 {self.current_step + 1}')
|
||
step_description = step_data.get('description', '')
|
||
|
||
# 使用 getChinese 处理文本
|
||
if hasattr(self.world, 'getChineseFont'):
|
||
step_name = self.world.getChineseFont()
|
||
step_description = self.world.getChineseFont()
|
||
|
||
if self.mode == "exam":
|
||
# 考核模式:只显示步骤编号和名称,不显示描述
|
||
step_info = f"Step {self.current_step + 1}/{self.total_steps}: {step_name}"
|
||
print(f"📝 考核模式步骤信息: {step_info}")
|
||
else:
|
||
# 训练模式:显示完整信息
|
||
step_info = f"Step: {self.current_step + 1}/{self.total_steps}: {step_name}"
|
||
if step_description:
|
||
step_info += f"\n{step_description}"
|
||
print(f"📚 训练模式步骤信息: {step_info}")
|
||
|
||
self.maintenance_gui.update_step_info(step_info)
|
||
|
||
# 更新Qt步骤对话框(仅在非维修系统模式下)
|
||
if self.step_dialog and not self.maintenance_gui:
|
||
self.step_dialog.update_step_info(step_data, self.current_step + 1, self.total_steps)
|
||
|
||
# 准备交互(主要是确保目标模型可以被拾取)
|
||
self.prepare_interaction(step_data)
|
||
|
||
def prepare_interaction(self, step_data):
|
||
"""准备交互,主要是设置目标模型的碰撞掩码"""
|
||
target_model_name = step_data.get('target_model')
|
||
if not target_model_name:
|
||
print("警告: 当前步骤没有指定目标模型")
|
||
return
|
||
|
||
target_node = self.find_model_node(target_model_name)
|
||
if not target_node:
|
||
print(f"警告: 找不到目标模型 '{target_model_name}'")
|
||
return
|
||
|
||
# 给目标模型设置碰撞掩码,使其可以被射线拾取
|
||
# BitMask32.bit(1) 对应 setup_picking 中设置的 from_collide_mask
|
||
target_node.setCollideMask(BitMask32.bit(1))
|
||
print(f"已为模型 '{target_model_name}' 设置碰撞掩码,等待用户点击。")
|
||
|
||
def find_model_node(self, model_name):
|
||
"""在场景中查找指定名称的模型节点"""
|
||
# find() 是Panda3D提供的更高效的节点查找方法
|
||
return self.world.render.find(f"**/{model_name}")
|
||
|
||
def list_scene_models(self):
|
||
"""列出场景中的所有模型(调试用)"""
|
||
print("\n📋 场景中的模型列表:")
|
||
all_nodes = self.world.render.findAllMatches("**")
|
||
model_count = 0
|
||
for node_path in all_nodes:
|
||
node_name = node_path.getName()
|
||
# 过滤掉系统节点和碰撞节点
|
||
if (node_name and
|
||
node_name not in ['render', 'camera', 'aspect2d', 'pixel2d', 'cam'] and
|
||
not node_name.startswith("modelCollision_") and
|
||
not node_name.startswith("collision") and
|
||
not node_name.startswith("picker")):
|
||
print(f" - {node_name}")
|
||
model_count += 1
|
||
print(f"总共找到 {model_count} 个模型节点\n")
|
||
|
||
def show_models_status(self):
|
||
"""显示所有模型的当前状态"""
|
||
if not self.model_original_positions:
|
||
print("📊 没有记录任何模型状态")
|
||
return
|
||
|
||
print("\n📊 模型状态信息:")
|
||
for model_name, original_pos in self.model_original_positions.items():
|
||
current_state = self.model_current_states.get(model_name, 'unknown')
|
||
model_node = self.find_model_node(model_name)
|
||
|
||
if model_node and not model_node.isEmpty():
|
||
current_pos = model_node.getPos()
|
||
distance_from_original = (current_pos - original_pos).length()
|
||
|
||
status_icon = "🔧" if current_state == 'installed' else "📦" if current_state == 'removed' else "❓"
|
||
print(f" {status_icon} {model_name}:")
|
||
print(f" 状态: {current_state}")
|
||
print(f" 原始位置: {original_pos}")
|
||
print(f" 当前位置: {current_pos}")
|
||
print(f" 距离原位: {distance_from_original:.2f}")
|
||
else:
|
||
print(f" ❌ {model_name}: 找不到模型节点")
|
||
print()
|
||
|
||
def record_current_positions(self):
|
||
"""将当前所有模型的位置记录为原始位置"""
|
||
print("\n📍 重新记录所有模型的原始位置...")
|
||
|
||
# 从步骤中提取所有模型
|
||
all_models = set()
|
||
for step_data in self.config_data.get('steps', []):
|
||
target_model = step_data.get('target_model')
|
||
if target_model:
|
||
all_models.add(target_model)
|
||
|
||
# 记录每个模型的当前位置为原始位置
|
||
for model_name in all_models:
|
||
target_node = self.find_model_node(model_name)
|
||
if target_node and not target_node.isEmpty():
|
||
current_pos = target_node.getPos()
|
||
self.model_original_positions[model_name] = current_pos
|
||
self.model_current_states[model_name] = 'installed'
|
||
print(f" 📍 {model_name}: 记录新的原始位置 {current_pos}")
|
||
else:
|
||
print(f" ❌ {model_name}: 在场景中找不到对应模型")
|
||
|
||
print(f"📍 重新记录完成,共记录 {len(self.model_original_positions)} 个模型")
|
||
print("💡 提示: 如需永久保存这些位置,请在配置界面中重新保存配置文件")
|
||
|
||
def normalize_step_type(self, step_data):
|
||
"""标准化步骤类型,兼容中文和英文配置"""
|
||
# 优先使用英文字段
|
||
step_type = step_data.get('step_type')
|
||
if step_type:
|
||
return step_type
|
||
|
||
# 兼容中文字段
|
||
type_field = step_data.get('type', '')
|
||
if type_field == '拆卸':
|
||
return 'disassemble'
|
||
elif type_field == '安装':
|
||
return 'assemble'
|
||
else:
|
||
return 'disassemble' # 默认值
|
||
|
||
def normalize_operation_type(self, step_data):
|
||
"""标准化操作类型,兼容中文和英文配置"""
|
||
# 优先使用英文字段
|
||
operation_type = step_data.get('operation_type')
|
||
if operation_type:
|
||
return operation_type
|
||
|
||
# 兼容中文字段
|
||
interaction_type = step_data.get('interaction_type', '')
|
||
if interaction_type == '鼠标拖拽':
|
||
return 'drag'
|
||
elif interaction_type == '点击触发':
|
||
return 'click'
|
||
else:
|
||
return 'drag' # 默认值
|
||
|
||
def analyze_final_state(self):
|
||
"""分析最终状态,检查哪些模型没有正确回到原位"""
|
||
print("\n📊 最终状态分析:")
|
||
|
||
if not self.model_original_positions:
|
||
print("没有模型状态记录")
|
||
return
|
||
|
||
incorrect_models = []
|
||
for model_name, original_pos in self.model_original_positions.items():
|
||
model_node = self.find_model_node(model_name)
|
||
if model_node and not model_node.isEmpty():
|
||
current_pos = model_node.getPos()
|
||
distance = (current_pos - original_pos).length()
|
||
current_state = self.model_current_states.get(model_name, 'unknown')
|
||
|
||
# 检查是否应该在原位但实际不在原位的模型
|
||
if current_state == 'installed' and distance > 1.0: # 容差1.0单位
|
||
incorrect_models.append({
|
||
'name': model_name,
|
||
'expected_pos': original_pos,
|
||
'actual_pos': current_pos,
|
||
'distance': distance
|
||
})
|
||
print(f"❌ {model_name}: 应该在原位但偏离了 {distance:.2f} 单位")
|
||
print(f" 期望位置: {original_pos}")
|
||
print(f" 实际位置: {current_pos}")
|
||
elif current_state == 'installed' and distance <= 1.0:
|
||
print(f"✅ {model_name}: 正确安装在原位 (偏差 {distance:.2f})")
|
||
elif current_state == 'removed':
|
||
print(f"📦 {model_name}: 已拆卸 (距离原位 {distance:.2f})")
|
||
|
||
if incorrect_models:
|
||
print(f"\n⚠️ 发现 {len(incorrect_models)} 个模型没有正确回到原位")
|
||
print("💡 建议检查安装步骤的配置和吸附容差设置")
|
||
else:
|
||
print("\n✅ 所有模型都处于正确状态")
|
||
|
||
def qt_mouse_press_event(self, event):
|
||
"""Qt鼠标按下事件处理(选择性拦截版)"""
|
||
from PyQt5.QtCore import Qt
|
||
|
||
# 只拦截左键,其他按键使用原有处理方式
|
||
if event.button() == Qt.LeftButton:
|
||
print('🖱️ 左键被拦截处理')
|
||
print(f'🖱️ 左键点击位置: ({event.x()}, {event.y()})')
|
||
|
||
# 检查是否点击在GUI区域
|
||
if self.maintenance_gui and self.is_click_in_gui_area(event.x(), event.y()):
|
||
print('🎯 点击在GUI区域,直接处理工具按钮点击')
|
||
# 直接处理工具按钮点击,不依赖DirectGUI事件
|
||
button_clicked = self.handle_gui_button_click(event.x(), event.y())
|
||
if button_clicked:
|
||
print(f'🎯 工具按钮点击已处理: {button_clicked}')
|
||
event.accept()
|
||
return
|
||
|
||
# 如果不是按钮点击,让原有事件处理器处理
|
||
if hasattr(self, 'original_mouse_press_event'):
|
||
self.original_mouse_press_event(event)
|
||
else:
|
||
event.ignore()
|
||
return
|
||
|
||
# 点击在3D区域,进行拦截处理
|
||
print('🎯 点击在3D区域,进行拦截处理')
|
||
self.handle_qt_mouse_click(event.x(), event.y())
|
||
event.accept()
|
||
else:
|
||
# 其他按键使用原有处理方式
|
||
if hasattr(self, 'original_mouse_press_event'):
|
||
self.original_mouse_press_event(event)
|
||
else:
|
||
event.ignore()
|
||
|
||
def is_click_in_gui_area(self, click_x, click_y):
|
||
"""检查鼠标点击是否在GUI区域(高精度版本)"""
|
||
try:
|
||
if not self.maintenance_gui:
|
||
return False
|
||
|
||
# 获取窗口尺寸用于坐标转换
|
||
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||
widget_width = self.world.qtWidget.width()
|
||
widget_height = self.world.qtWidget.height()
|
||
else:
|
||
# 使用默认窗口尺寸
|
||
widget_width = 1380
|
||
widget_height = 750
|
||
|
||
# 使用与maintenance_gui完全相同的坐标转换算法
|
||
aspect_x = (click_x / widget_width) * 2.67 - 1.33
|
||
aspect_y = 1.0 - (click_y / widget_height) * 2.0
|
||
|
||
# 额外的坐标验证(与Panda3D标准坐标系对比)
|
||
# Panda3D aspect2d坐标系: x范围[-1.33, 1.33], y范围[-1.0, 1.0]
|
||
normalized_x = (click_x / widget_width) * 2.0 - 1.0 # 标准化到[-1, 1]
|
||
normalized_y = 1.0 - (click_y / widget_height) * 2.0 # 标准化到[-1, 1],Y轴翻转
|
||
aspect_ratio = widget_width / widget_height
|
||
panda_x = normalized_x * aspect_ratio # 应用宽高比
|
||
panda_y = normalized_y
|
||
|
||
print(f"🔍 精确GUI区域检查: Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})")
|
||
print(f"🔍 验证坐标转换: 标准化({normalized_x:.3f}, {normalized_y:.3f}) -> Panda3D({panda_x:.3f}, {panda_y:.3f})")
|
||
print(f"📏 窗口信息: 尺寸({widget_width}x{widget_height}), 宽高比({aspect_ratio:.3f})")
|
||
|
||
# 快速检查工具按钮区域
|
||
if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools:
|
||
# 工具按钮区域(底部条带)
|
||
tools_area_left = -1.1
|
||
tools_area_right = 0.5
|
||
tools_area_bottom = -1.0
|
||
tools_area_top = -0.5
|
||
|
||
if (tools_area_left <= aspect_x <= tools_area_right and
|
||
tools_area_bottom <= aspect_y <= tools_area_top):
|
||
print(f"✅ 点击在工具按钮区域内")
|
||
return True
|
||
|
||
# 检查其他GUI区域(步骤显示、当前工具、警告等)
|
||
other_gui_areas = []
|
||
|
||
# 步骤显示区域(顶部)
|
||
if hasattr(self.maintenance_gui, 'step_text') and self.maintenance_gui.step_text:
|
||
other_gui_areas.append({
|
||
'name': '步骤显示区域',
|
||
'left': -1.8, 'right': 1.8,
|
||
'bottom': 0.5, 'top': 1.1
|
||
})
|
||
|
||
# 当前工具显示区域(右侧)
|
||
if hasattr(self.maintenance_gui, 'current_tool_text') and self.maintenance_gui.current_tool_text:
|
||
other_gui_areas.append({
|
||
'name': '当前工具显示区域',
|
||
'left': 0.2, 'right': 1.8,
|
||
'bottom': 0.6, 'top': 1.0
|
||
})
|
||
|
||
# 警告显示区域(中央)
|
||
if hasattr(self.maintenance_gui, 'warning_text') and self.maintenance_gui.warning_text:
|
||
other_gui_areas.append({
|
||
'name': '警告显示区域',
|
||
'left': -1.5, 'right': 1.5,
|
||
'bottom': 0.0, 'top': 0.6
|
||
})
|
||
|
||
# 检查其他GUI区域
|
||
for area in other_gui_areas:
|
||
if (area['left'] <= aspect_x <= area['right'] and
|
||
area['bottom'] <= aspect_y <= area['top']):
|
||
print(f"✅ 点击在GUI区域内: {area['name']}")
|
||
return True
|
||
|
||
print("❌ 点击不在任何GUI区域内")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"❌ 检查GUI区域失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def handle_gui_button_click(self, click_x, click_y):
|
||
"""直接处理GUI按钮点击"""
|
||
try:
|
||
if not self.maintenance_gui:
|
||
return None
|
||
|
||
# 获取窗口尺寸用于坐标转换
|
||
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||
widget_width = self.world.qtWidget.width()
|
||
widget_height = self.world.qtWidget.height()
|
||
else:
|
||
widget_width = 1380
|
||
widget_height = 750
|
||
|
||
# 使用与maintenance_gui完全相同的坐标转换算法
|
||
aspect_x = (click_x / widget_width) * 2.67 - 1.33
|
||
aspect_y = 1.0 - (click_y / widget_height) * 2.0
|
||
|
||
print(f"🎯 直接处理按钮点击:Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})")
|
||
|
||
# 检查工具按钮点击
|
||
if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools:
|
||
button_width = 0.4
|
||
button_height = 0.25
|
||
button_spacing = 0.45
|
||
start_x = -0.8
|
||
start_y = -0.75
|
||
click_padding = 0.05 # 点击容差
|
||
|
||
for i, tool_data in enumerate(self.maintenance_gui.available_tools):
|
||
# 获取工具名称
|
||
if isinstance(tool_data, dict):
|
||
tool_name = tool_data.get('name', str(tool_data))
|
||
else:
|
||
tool_name = str(tool_data)
|
||
|
||
# 计算按钮位置
|
||
button_x = start_x + i * button_spacing
|
||
button_y = start_y
|
||
|
||
# 计算按钮边界(包含容差)
|
||
left = button_x - (button_width/2 + click_padding)
|
||
right = button_x + (button_width/2 + click_padding)
|
||
bottom = button_y - (button_height/2 + click_padding)
|
||
top = button_y + (button_height/2 + click_padding)
|
||
|
||
print(f"🔍 检查按钮{i} '{tool_name}': 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]")
|
||
|
||
# 检查点击是否在按钮范围内
|
||
if (left <= aspect_x <= right and bottom <= aspect_y <= top):
|
||
print(f"✅ 直接匹配按钮{i} '{tool_name}'!调用工具选择")
|
||
# 直接调用维修GUI的工具选择方法
|
||
self.maintenance_gui.on_tool_selected(tool_name)
|
||
return tool_name
|
||
|
||
print("❌ 点击不在任何按钮范围内")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"❌ 直接处理按钮点击失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def qt_mouse_release_event(self, event):
|
||
"""Qt鼠标释放事件处理(维修系统优化版)"""
|
||
from PyQt5.QtCore import Qt
|
||
|
||
# 只拦截左键释放,其他按键使用原有处理方式
|
||
if event.button() == Qt.LeftButton:
|
||
print('🖱️ 左键释放被拦截处理')
|
||
|
||
# 维修系统模式:先让原有处理器处理
|
||
if self.maintenance_gui:
|
||
if hasattr(self, 'original_mouse_release_event'):
|
||
self.original_mouse_release_event(event)
|
||
|
||
# 处理3D交互的鼠标释放
|
||
self.on_mouse_up()
|
||
event.accept()
|
||
else:
|
||
# 其他按键释放使用原有处理方式
|
||
if hasattr(self, 'original_mouse_release_event'):
|
||
self.original_mouse_release_event(event)
|
||
else:
|
||
event.ignore()
|
||
|
||
def handle_qt_mouse_click(self, x, y):
|
||
"""处理Qt鼠标点击(转换为3D世界坐标)"""
|
||
try:
|
||
if not self.is_active:
|
||
print("⚠️ 交互模式未激活")
|
||
return
|
||
|
||
# 检查操作权限
|
||
if not self.check_operation_permission():
|
||
return
|
||
|
||
# 获取当前步骤的目标模型
|
||
if self.current_step >= self.total_steps:
|
||
print("⚠️ 所有步骤已完成")
|
||
return
|
||
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
target_model_name = step_data.get('target_model')
|
||
|
||
if not target_model_name:
|
||
print("⚠️ 当前步骤没有指定目标模型")
|
||
return
|
||
|
||
print(f'🎯 检测点击目标: {target_model_name}')
|
||
|
||
# 获取窗口尺寸进行坐标转换
|
||
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||
widget = self.world.qtWidget
|
||
win_width = widget.width()
|
||
win_height = widget.height()
|
||
|
||
# 转换为Panda3D的标准化坐标
|
||
mx = 2.0 * x / float(win_width) - 1.0
|
||
my = 1.0 - 2.0 * y / float(win_height)
|
||
|
||
# 执行射线检测
|
||
self.perform_ray_cast(mx, my, target_model_name)
|
||
else:
|
||
print("❌ 找不到Qt部件,无法进行坐标转换")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Qt鼠标点击处理失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def perform_ray_cast(self, mx, my, target_model_name):
|
||
"""执行射线检测"""
|
||
try:
|
||
# 获取鼠标射线
|
||
from panda3d.core import Point3, Point2, CollisionTraverser, CollisionHandlerQueue
|
||
from panda3d.core import CollisionNode, CollisionRay, BitMask32
|
||
|
||
near_point = Point3()
|
||
far_point = Point3()
|
||
self.world.cam.node().getLens().extrude(Point2(mx, my), near_point, far_point)
|
||
|
||
# 转换到世界坐标系
|
||
world_near = self.world.render.getRelativePoint(self.world.camera, near_point)
|
||
world_far = self.world.render.getRelativePoint(self.world.camera, far_point)
|
||
|
||
# 进行真正的射线检测
|
||
picker = CollisionTraverser()
|
||
queue = CollisionHandlerQueue()
|
||
|
||
# 创建射线碰撞检测
|
||
pickerNode = CollisionNode('mouseRay')
|
||
pickerNP = self.world.camera.attachNewNode(pickerNode)
|
||
|
||
# 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位)
|
||
pickerNode.setFromCollideMask(BitMask32.bit(1))
|
||
|
||
# 使用相机坐标系的点创建射线
|
||
direction = far_point - near_point
|
||
direction.normalize()
|
||
pickerNode.addSolid(CollisionRay(near_point, direction))
|
||
|
||
picker.addCollider(pickerNP, queue)
|
||
picker.traverse(self.world.render)
|
||
|
||
# 检查是否点击到了目标模型
|
||
hit_target_model = False
|
||
hit_point = None
|
||
|
||
if queue.getNumEntries() > 0:
|
||
# 遍历所有碰撞结果
|
||
for i in range(queue.getNumEntries()):
|
||
entry = queue.getEntry(i)
|
||
hit_node_path = entry.getIntoNodePath()
|
||
hit_pos = entry.getSurfacePoint(self.world.render)
|
||
|
||
# 检查是否击中了目标模型或其相关的碰撞体
|
||
if self.is_target_model_hit(hit_node_path, target_model_name):
|
||
print(f"✅ 击中目标模型: {hit_node_path.getName()}")
|
||
hit_target_model = True
|
||
hit_point = hit_pos
|
||
break
|
||
|
||
# 清理碰撞检测节点
|
||
pickerNP.removeNode()
|
||
|
||
if hit_target_model:
|
||
# 查找目标模型节点
|
||
target_node = self.find_model_node(target_model_name)
|
||
if target_node and not target_node.isEmpty():
|
||
# 根据操作类型决定行为
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
operation_type = self.normalize_operation_type(step_data)
|
||
|
||
if operation_type == 'drag':
|
||
print(f'✅ 开始拖拽目标模型: {target_node.getName()}')
|
||
self.start_dragging(target_node, hit_point)
|
||
elif operation_type == 'click':
|
||
print(f'✅ 点击触发位移模型: {target_node.getName()}')
|
||
self.trigger_click_movement(target_node, step_data)
|
||
else:
|
||
print(f"❌ 未知的操作类型: {operation_type}")
|
||
else:
|
||
print(f"❌ 找不到目标模型节点: {target_model_name}")
|
||
else:
|
||
print(f"❌ 没有点击到目标模型 '{target_model_name}',请点击正确的模型")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 射线检测失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def is_target_model_hit(self, hit_node_path, target_model_name):
|
||
"""检查击中的节点是否是目标模型"""
|
||
hit_name = hit_node_path.getName()
|
||
|
||
# 直接匹配模型名称
|
||
if hit_name == target_model_name:
|
||
return True
|
||
|
||
# 检查是否是模型的碰撞体(通常以modelCollision_开头)
|
||
if hit_name.startswith("modelCollision_"):
|
||
collision_model_name = hit_name.replace("modelCollision_", "", 1)
|
||
if collision_model_name == target_model_name:
|
||
return True
|
||
|
||
# 检查父节点是否是目标模型
|
||
parent = hit_node_path.getParent()
|
||
while parent and not parent.isEmpty() and parent != self.world.render:
|
||
parent_name = parent.getName()
|
||
if parent_name == target_model_name:
|
||
return True
|
||
parent = parent.getParent()
|
||
|
||
# 检查子节点路径
|
||
current = hit_node_path
|
||
while current and not current.isEmpty():
|
||
if current.getName() == target_model_name:
|
||
return True
|
||
current = current.getParent()
|
||
|
||
return False
|
||
|
||
def on_mouse_down(self):
|
||
"""鼠标按下事件(优化版 v2:处理兄弟节点结构的碰撞体)"""
|
||
if not self.is_active or not self.world.mouseWatcherNode.hasMouse():
|
||
return
|
||
# 获取鼠标位置并发射射线
|
||
mouse_pos = self.world.mouseWatcherNode.getMouse()
|
||
picker_ray = self.picker_ray_node.node().getSolid(0)
|
||
picker_ray.setFromLens(self.world.camNode, mouse_pos.getX(), mouse_pos.getY())
|
||
|
||
# 执行遍历
|
||
self.picker_traverser.traverse(self.world.render)
|
||
|
||
if self.collision_handler.getNumEntries() > 0:
|
||
# 获取最近的碰撞点
|
||
self.collision_handler.sortEntries()
|
||
entry = self.collision_handler.getEntry(0)
|
||
|
||
hit_node_path = entry.getIntoNodePath()
|
||
hit_point = entry.getSurfacePoint(self.world.render)
|
||
|
||
# --- 这是新的逻辑 ---
|
||
# 打印被击中的碰撞体名字,用于调试
|
||
print(f"射线击中了碰撞节点: {hit_node_path.getName()}")
|
||
|
||
# 根据命名约定推导模型名称
|
||
hit_name = hit_node_path.getName()
|
||
derived_model_name = ""
|
||
|
||
# 假设碰撞节点的命名规则是 "前缀 + 模型名"
|
||
if hit_name.startswith("modelCollision_"):
|
||
derived_model_name = hit_name.replace("modelCollision_", "", 1)
|
||
else:
|
||
derived_model_name = hit_name
|
||
|
||
print(f"根据命名约定,推导出的模型名称为: {derived_model_name}")
|
||
|
||
# 获取当前步骤的目标模型名称
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
target_model_name = step_data.get('target_model')
|
||
|
||
# 检查推导出的名称是否是当前步骤需要操作的目标
|
||
if derived_model_name == target_model_name:
|
||
print(f"✅ 击中正确!目标是 '{target_model_name}'")
|
||
|
||
# 根据推导出的正确名称,去场景中查找可以被拖动的那个节点
|
||
model_to_drag = self.find_model_node(derived_model_name)
|
||
|
||
if model_to_drag and not model_to_drag.isEmpty():
|
||
self.start_dragging(model_to_drag, hit_point)
|
||
else:
|
||
print(f"❌ 错误:虽然名称匹配,但在场景中找不到名为 '{derived_model_name}' 的可拖动节点!")
|
||
else:
|
||
print(f"❌ 击中的不是当前目标模型 (需要 '{target_model_name}', 击中了 '{derived_model_name}')")
|
||
def start_dragging(self, model_node, hit_point):
|
||
"""开始拖拽模型(优化版:计算偏移量)"""
|
||
# 再次检查操作权限(双重保险)
|
||
if not self.check_operation_permission():
|
||
print("⚠️ 权限检查失败,无法开始拖拽")
|
||
return
|
||
|
||
self.dragging_model = model_node
|
||
|
||
# 1. 定义一个拖拽平面。这里我们使用一个平行于XZ的平面(法线为Y轴),
|
||
# 并且该平面穿过模型的初始中心点。
|
||
# 这意味着模型将在其初始高度上水平移动。
|
||
# 您可以根据需要更改法线,例如 Vec3(0, 0, 1) 表示在XY平面上拖拽。
|
||
plane_normal = Vec3(0, 1, 0)
|
||
self.drag_plane = Plane(plane_normal, model_node.getPos())
|
||
|
||
# 2. 计算鼠标射线与拖拽平面的交点。
|
||
# 这个交点是鼠标在“拖拽世界”中的3D位置。
|
||
plane_intersection_point = Point3()
|
||
if self.drag_plane.intersectsLine(plane_intersection_point,
|
||
self.world.camera.getPos(self.world.render),
|
||
hit_point):
|
||
# 3. 计算模型中心点和这个交点之间的偏移量。
|
||
# 在整个拖拽过程中,我们将维持这个偏移量,以避免模型跳跃。
|
||
self.drag_offset = model_node.getPos() - plane_intersection_point
|
||
print(f"开始拖拽模型: {model_node.getName()}")
|
||
print(f"初始位置: {model_node.getPos()}")
|
||
|
||
# 启动拖拽任务
|
||
self.world.taskMgr.add(self.drag_task, "drag_model_task")
|
||
else:
|
||
print("警告:无法计算拖拽平面的交点,拖拽取消。")
|
||
self.dragging_model = None
|
||
|
||
def drag_task(self, task):
|
||
"""拖拽任务(优化版:应用偏移量)"""
|
||
if not self.dragging_model or not self.world.mouseWatcherNode.hasMouse():
|
||
# 如果模型被置空或鼠标不在窗口内,则停止任务
|
||
return task.done
|
||
|
||
# 获取最新的鼠标位置
|
||
mouse_pos = self.world.mouseWatcherNode.getMouse()
|
||
|
||
# 再次从相机发射射线以获取鼠标在3D空间中的指向
|
||
near_point = Point3()
|
||
far_point = Point3()
|
||
self.world.cam.node().getLens().extrude(mouse_pos, near_point, far_point)
|
||
|
||
# 将近点和远点转换到世界坐标系
|
||
origin_point = self.world.render.getRelativePoint(self.world.camera, near_point)
|
||
direction_point = self.world.render.getRelativePoint(self.world.camera, far_point)
|
||
|
||
# 计算射线与我们之前定义的拖拽平面的交点
|
||
intersection_point = Point3()
|
||
if self.drag_plane.intersectsLine(intersection_point, origin_point, direction_point):
|
||
# 新的位置 = 当前鼠标在拖拽平面上的3D位置 + 初始偏移量
|
||
new_pos = intersection_point + self.drag_offset
|
||
self.dragging_model.setPos(new_pos)
|
||
|
||
# 初始化距离打印时间(如果还没有)
|
||
if not hasattr(self, '_last_distance_print_time'):
|
||
self._last_distance_print_time = 0
|
||
|
||
# 在拖拽过程中,实时显示距离原始位置的距离
|
||
if hasattr(self, 'config_data') and self.current_step < len(self.config_data.get('steps', [])):
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
step_type = self.normalize_step_type(step_data)
|
||
target_model_name = step_data.get('target_model', '')
|
||
|
||
if (target_model_name in self.model_original_positions and
|
||
hasattr(self, '_last_distance_print_time')):
|
||
|
||
# 每0.5秒显示一次距离信息
|
||
current_time = task.time
|
||
if current_time - getattr(self, '_last_distance_print_time', 0) > 0.5:
|
||
original_pos = self.model_original_positions[target_model_name]
|
||
distance = (new_pos - original_pos).length()
|
||
|
||
if step_type == 'assemble':
|
||
snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0)
|
||
if distance <= snap_tolerance * 1.2: # 只在接近时显示
|
||
print(f"📏 安装拖拽中 - 距离原位: {distance:.2f} (需要 <= {snap_tolerance:.2f})")
|
||
else: # 拆卸模式
|
||
disassemble_threshold = step_data.get('disassemble_threshold', 5.0)
|
||
if distance >= disassemble_threshold * 0.8: # 接近阈值时显示
|
||
print(f"📏 拆卸拖拽中 - 距离原位: {distance:.2f} (需要 >= {disassemble_threshold:.2f})")
|
||
|
||
self._last_distance_print_time = current_time
|
||
|
||
return task.cont
|
||
|
||
def stop_dragging(self):
|
||
"""停止拖拽"""
|
||
if not self.dragging_model:
|
||
return
|
||
|
||
print(f"停止拖拽模型: {self.dragging_model.getName()}")
|
||
print(f"最终位置: {self.dragging_model.getPos()}")
|
||
|
||
# 停止拖拽任务
|
||
self.world.taskMgr.remove("drag_model_task")
|
||
|
||
# 检查是否完成了当前步骤
|
||
self.check_step_completion()
|
||
|
||
# 清理拖拽状态
|
||
self.dragging_model = None
|
||
|
||
def on_mouse_up(self):
|
||
"""鼠标抬起事件"""
|
||
self.stop_dragging()
|
||
|
||
def check_step_completion(self):
|
||
"""检查步骤是否完成"""
|
||
if not self.dragging_model:
|
||
return
|
||
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
step_type = self.normalize_step_type(step_data)
|
||
print(f"检查步骤完成: {step_type} ----------------------------------------")
|
||
target_model_name = step_data.get('target_model', '')
|
||
current_pos = self.dragging_model.getPos()
|
||
|
||
if step_type == 'assemble':
|
||
# 安装模式:检查是否接近原始位置
|
||
if target_model_name in self.model_original_positions:
|
||
original_pos = self.model_original_positions[target_model_name]
|
||
distance_to_original = (original_pos - current_pos).length()
|
||
# 吸附容差:优先使用配置的snap_tolerance,否则使用拆卸时的tolerance,最后默认5.0
|
||
snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0)
|
||
|
||
print(f"🔧 安装模式 - 距离原始位置: {distance_to_original:.2f}")
|
||
print(f"🎯 吸附容差: {snap_tolerance:.2f}")
|
||
print(f"🔍 吸附条件: {distance_to_original:.2f} <= {snap_tolerance:.2f} = {distance_to_original <= snap_tolerance}")
|
||
|
||
if distance_to_original <= snap_tolerance:
|
||
# 自动吸附到原始位置
|
||
print(f"✨ 自动吸附到原始位置!")
|
||
print(f"📍 精确位置: {original_pos}")
|
||
self.dragging_model.setPos(original_pos)
|
||
self.model_current_states[target_model_name] = 'installed'
|
||
print(f"步骤 {self.current_step + 1} 完成!模型已安装到原始位置")
|
||
self.complete_current_step()
|
||
return
|
||
else:
|
||
print(f"距离原始位置还有 {distance_to_original:.2f} 单位,需要更接近才能自动吸附")
|
||
else:
|
||
print("⚠️ 没有找到模型的原始位置记录")
|
||
|
||
else:
|
||
# 拆卸模式:检查是否远离原始位置
|
||
if target_model_name in self.model_original_positions:
|
||
original_pos = self.model_original_positions[target_model_name]
|
||
distance_from_original = (current_pos - original_pos).length()
|
||
disassemble_threshold = step_data.get('disassemble_threshold', 5.0) # 拆卸距离阈值
|
||
|
||
print(f"🔧 拆卸模式 - 距离原始位置: {distance_from_original:.2f}")
|
||
print(f"🎯 拆卸阈值: {disassemble_threshold:.2f}")
|
||
print(f"🔍 拆卸条件: {distance_from_original:.2f} >= {disassemble_threshold:.2f} = {distance_from_original >= disassemble_threshold}")
|
||
|
||
if distance_from_original >= disassemble_threshold:
|
||
print(f"步骤 {self.current_step + 1} 完成!模型已拆卸,距离原位 {distance_from_original:.2f} >= {disassemble_threshold:.2f}")
|
||
if target_model_name:
|
||
self.model_current_states[target_model_name] = 'removed'
|
||
self.complete_current_step()
|
||
return
|
||
else:
|
||
print(f"需要将模型拖拽到距离原位至少 {disassemble_threshold:.2f} 单位(当前 {distance_from_original:.2f})")
|
||
else:
|
||
# 如果没有原始位置记录,回退到原来的目标位置模式
|
||
print("⚠️ 没有找到模型的原始位置记录,使用目标位置模式")
|
||
target_pos_data = step_data.get('target_position')
|
||
|
||
if target_pos_data is None:
|
||
# 如果没有定义目标位置,拆卸后即完成
|
||
print("拆卸模式 - 没有定义目标位置,视为完成。")
|
||
if target_model_name:
|
||
self.model_current_states[target_model_name] = 'removed'
|
||
self.complete_current_step()
|
||
return
|
||
|
||
target_pos = Vec3(*target_pos_data)
|
||
distance = (target_pos - current_pos).length()
|
||
tolerance = step_data.get('tolerance', 5)
|
||
|
||
if distance <= tolerance:
|
||
print(f"步骤 {self.current_step + 1} 完成!拆卸到目标位置 距离 {distance:.2f} < 容差 {tolerance}")
|
||
if target_model_name:
|
||
self.model_current_states[target_model_name] = 'removed'
|
||
self.complete_current_step()
|
||
else:
|
||
print(f"距离目标位置还有 {distance:.2f} 单位,未达到容差 {tolerance}")
|
||
|
||
def complete_current_step(self):
|
||
"""完成当前步骤"""
|
||
# 记录操作完成(考核模式下)
|
||
if self.mode == "exam" and self.current_step in self.step_scores:
|
||
self.step_scores[self.current_step]['operation_attempts'] += 1
|
||
print(f"📝 考核记录:步骤 {self.current_step + 1} 完成,操作次数: {self.step_scores[self.current_step]['operation_attempts']}")
|
||
|
||
# 在进入下一步之前,取消当前模型的碰撞,避免干扰
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
target_model_name = step_data.get('target_model')
|
||
if target_model_name:
|
||
node = self.find_model_node(target_model_name)
|
||
if node:
|
||
node.setCollideMask(BitMask32.allOff())
|
||
|
||
# 显示当前所有模型的状态
|
||
self.show_models_status()
|
||
|
||
self.current_step += 1
|
||
|
||
if self.current_step >= self.total_steps:
|
||
self.finish_interaction()
|
||
else:
|
||
self.start_current_step()
|
||
|
||
def finish_interaction(self):
|
||
"""完成所有交互"""
|
||
print("\n🎉 所有拆装步骤已完成!")
|
||
self.is_active = False
|
||
self.ignoreAll()
|
||
|
||
# 清理维修系统GUI
|
||
if self.maintenance_gui:
|
||
self.maintenance_gui.cleanup_gui()
|
||
print("✅ 维修系统GUI已清理")
|
||
|
||
# 恢复原有的Qt鼠标事件处理
|
||
print("🔄 恢复原有的Qt鼠标事件处理")
|
||
if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||
self.world.qtWidget.mousePressEvent = self.original_mouse_press_event
|
||
self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event
|
||
print("✅ 原有Qt鼠标事件处理已恢复")
|
||
else:
|
||
print("⚠️ 没有找到备份的Qt事件处理方法")
|
||
|
||
if self.step_dialog:
|
||
self.step_dialog.close()
|
||
self.step_dialog = None
|
||
|
||
# 根据模式显示不同的完成结果
|
||
if self.mode == "exam":
|
||
# 考核模式:显示详细的考核结果
|
||
self.show_exam_results()
|
||
else:
|
||
# 训练模式:显示训练完成提示
|
||
print("\n🎉 训练模式完成!")
|
||
completion_msg = "🎉 训练完成!\n\n所有维修步骤已完成!\n现在可以尝试考核模式检验学习成果。"
|
||
|
||
if self.maintenance_gui:
|
||
self.maintenance_gui.update_step_info(completion_msg)
|
||
print("📋 训练完成信息已在GUI中显示")
|
||
else:
|
||
QMessageBox.information(None, "训练完成", completion_msg)
|
||
|
||
def stop_interaction(self):
|
||
"""用户主动停止交互"""
|
||
print("\n🛑 用户主动停止拆装交互")
|
||
|
||
try:
|
||
# 设置为非活动状态
|
||
self.is_active = False
|
||
self.ignoreAll()
|
||
|
||
# 清理维修系统GUI
|
||
if self.maintenance_gui:
|
||
self.maintenance_gui.cleanup_gui()
|
||
print("✅ 维修系统GUI已清理")
|
||
|
||
# 恢复原有的Qt鼠标事件处理
|
||
print("🔄 恢复原有的Qt鼠标事件处理")
|
||
if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||
self.world.qtWidget.mousePressEvent = self.original_mouse_press_event
|
||
self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event
|
||
print("✅ 原有Qt鼠标事件处理已恢复")
|
||
else:
|
||
print("⚠️ 没有找到备份的Qt事件处理方法")
|
||
|
||
# 关闭步骤对话框
|
||
if self.step_dialog:
|
||
self.step_dialog.close()
|
||
self.step_dialog = None
|
||
|
||
# 显示停止确认信息
|
||
if self.mode == "exam":
|
||
stop_msg = "🛑 考核已停止\n\n您的考核进度没有被保存。"
|
||
else:
|
||
stop_msg = "🛑 训练已停止\n\n您可以随时重新开始训练。"
|
||
|
||
if self.maintenance_gui:
|
||
# 如果GUI还在,显示停止信息
|
||
self.maintenance_gui.update_step_info(stop_msg)
|
||
else:
|
||
QMessageBox.information(None, "操作停止", stop_msg)
|
||
|
||
print("✅ 拆装交互已停止")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 停止拆装交互失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def show_exam_results(self):
|
||
"""显示考核结果"""
|
||
try:
|
||
# 计算最终得分
|
||
final_score = 0
|
||
total_operations = 0
|
||
total_errors = 0
|
||
|
||
for step_record in self.step_scores.values():
|
||
final_score += step_record['current_score']
|
||
total_operations += step_record['operation_attempts']
|
||
if step_record['tool_error']:
|
||
total_errors += 1
|
||
|
||
# 计算得分率和准确率
|
||
score_rate = (final_score / self.exam_max_score * 100) if self.exam_max_score > 0 else 0
|
||
accuracy_rate = ((self.total_steps - total_errors) / self.total_steps * 100) if self.total_steps > 0 else 100
|
||
|
||
# 生成考核报告
|
||
print(f"\n{'='*50}")
|
||
print(f"🎓 维修技能考核结果报告")
|
||
print(f"{'='*50}")
|
||
print(f"📊 总体成绩:")
|
||
print(f" 总得分: {final_score:.0f} / {self.exam_max_score:.0f} 分")
|
||
print(f" 得分率: {score_rate:.1f}%")
|
||
print(f" 操作准确率: {accuracy_rate:.1f}%")
|
||
print(f" 总操作次数: {total_operations}")
|
||
print(f" 错误次数: {total_errors}")
|
||
|
||
# 评级系统
|
||
if score_rate >= 90:
|
||
grade = "优秀"
|
||
grade_icon = "🏆"
|
||
grade_desc = "表现卓越,技能熟练"
|
||
elif score_rate >= 80:
|
||
grade = "良好"
|
||
grade_icon = "🥈"
|
||
grade_desc = "表现良好,基本掌握"
|
||
elif score_rate >= 60:
|
||
grade = "及格"
|
||
grade_icon = "✅"
|
||
grade_desc = "达到基本要求"
|
||
else:
|
||
grade = "不及格"
|
||
grade_icon = "❌"
|
||
grade_desc = "需要加强练习"
|
||
|
||
print(f"\n🏅 评级: {grade_icon} {grade}")
|
||
print(f" 评语: {grade_desc}")
|
||
|
||
# 详细步骤分析
|
||
print(f"\n📋 详细步骤分析:")
|
||
print(f"{'-'*40}")
|
||
|
||
perfect_steps = 0
|
||
for step_idx, step_record in self.step_scores.items():
|
||
step_data = self.config_data['steps'][step_idx]
|
||
step_name = step_data.get('name', f'步骤{step_idx+1}')
|
||
max_score = step_record['max_score']
|
||
current_score = step_record['current_score']
|
||
tool_error = step_record['tool_error']
|
||
attempts = step_record['operation_attempts']
|
||
|
||
# 判断步骤完成质量
|
||
if current_score >= max_score:
|
||
step_status = "🟢 完美"
|
||
perfect_steps += 1
|
||
elif current_score >= max_score * 0.8:
|
||
step_status = "🟡 良好"
|
||
elif current_score > 0:
|
||
step_status = "🟠 一般"
|
||
else:
|
||
step_status = "🔴 失败"
|
||
|
||
print(f"{step_name}:")
|
||
print(f" 状态: {step_status}")
|
||
print(f" 得分: {current_score:.0f}/{max_score:.0f} 分")
|
||
print(f" 工具使用: {'❌ 错误' if tool_error else '✅ 正确'}")
|
||
print(f" 操作次数: {attempts}")
|
||
print()
|
||
|
||
# 生成改进建议
|
||
suggestions = []
|
||
if total_errors > 0:
|
||
suggestions.append("• 注意选择正确的工具进行操作")
|
||
if total_operations > self.total_steps * 1.5:
|
||
suggestions.append("• 减少不必要的操作,提高操作效率")
|
||
if perfect_steps < self.total_steps * 0.5:
|
||
suggestions.append("• 多练习以提高操作的精确度")
|
||
if score_rate < 80:
|
||
suggestions.append("• 建议重新学习相关理论知识")
|
||
|
||
if suggestions:
|
||
print("💡 改进建议:")
|
||
for suggestion in suggestions:
|
||
print(f" {suggestion}")
|
||
print()
|
||
|
||
print(f"{'='*50}")
|
||
|
||
# 为GUI显示准备简化版本的报告
|
||
gui_report = f"🎓 考核完成!\n\n"
|
||
gui_report += f"📊 成绩总览:\n"
|
||
gui_report += f"总得分: {final_score:.0f}/{self.exam_max_score:.0f} 分 ({score_rate:.1f}%)\n"
|
||
gui_report += f"评级: {grade_icon} {grade}\n"
|
||
gui_report += f"操作准确率: {accuracy_rate:.1f}%\n\n"
|
||
gui_report += f"完美完成步骤: {perfect_steps}/{self.total_steps}\n"
|
||
gui_report += f"总操作次数: {total_operations}\n"
|
||
gui_report += f"错误次数: {total_errors}"
|
||
|
||
# 在维修GUI中显示详细考核结果
|
||
if self.maintenance_gui:
|
||
# 创建详细的考核结果数据
|
||
exam_result_data = {
|
||
'final_score': final_score,
|
||
'max_score': self.exam_max_score,
|
||
'score_rate': score_rate,
|
||
'accuracy_rate': accuracy_rate,
|
||
'grade': grade,
|
||
'grade_icon': grade_icon,
|
||
'grade_desc': grade_desc,
|
||
'total_operations': total_operations,
|
||
'total_errors': total_errors,
|
||
'perfect_steps': perfect_steps,
|
||
'total_steps': self.total_steps,
|
||
'step_details': [],
|
||
'suggestions': suggestions
|
||
}
|
||
|
||
# 添加每步详情
|
||
for step_idx, step_record in self.step_scores.items():
|
||
step_data = self.config_data['steps'][step_idx]
|
||
step_name = step_data.get('name', f'步骤{step_idx+1}')
|
||
max_score = step_record['max_score']
|
||
current_score = step_record['current_score']
|
||
tool_error = step_record['tool_error']
|
||
attempts = step_record['operation_attempts']
|
||
|
||
# 判断步骤完成质量
|
||
if current_score >= max_score:
|
||
step_status = "🟢 完美"
|
||
elif current_score >= max_score * 0.8:
|
||
step_status = "🟡 良好"
|
||
elif current_score > 0:
|
||
step_status = "🟠 一般"
|
||
else:
|
||
step_status = "🔴 失败"
|
||
|
||
exam_result_data['step_details'].append({
|
||
'name': step_name,
|
||
'status': step_status,
|
||
'current_score': current_score,
|
||
'max_score': max_score,
|
||
'tool_error': tool_error,
|
||
'attempts': attempts
|
||
})
|
||
|
||
# 使用GUI显示考核结果
|
||
self.maintenance_gui.show_exam_results(exam_result_data)
|
||
print("📋 考核结果已在GUI中显示")
|
||
else:
|
||
# 如果没有GUI,使用简单的控制台输出
|
||
print("⚠️ 维修GUI不可用,考核结果仅在控制台显示")
|
||
|
||
# 保存考核结果到文件(可选)
|
||
self.save_exam_results(final_score, score_rate, grade)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 显示考核结果失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
QMessageBox.critical(None, "错误", f"显示考核结果失败: {str(e)}")
|
||
|
||
def save_exam_results(self, final_score, score_rate, grade):
|
||
"""保存考核结果到文件"""
|
||
try:
|
||
import datetime
|
||
import json
|
||
|
||
# 生成考核记录
|
||
exam_record = {
|
||
"timestamp": datetime.datetime.now().isoformat(),
|
||
"mode": "exam",
|
||
"final_score": final_score,
|
||
"max_score": self.exam_max_score,
|
||
"score_rate": score_rate,
|
||
"grade": grade,
|
||
"total_steps": self.total_steps,
|
||
"step_details": {}
|
||
}
|
||
|
||
# 添加每步详情
|
||
for step_idx, step_record in self.step_scores.items():
|
||
step_name = self.config_data['steps'][step_idx].get('name', f'步骤{step_idx+1}')
|
||
exam_record["step_details"][step_name] = {
|
||
"max_score": step_record['max_score'],
|
||
"current_score": step_record['current_score'],
|
||
"tool_error": step_record['tool_error'],
|
||
"operation_attempts": step_record['operation_attempts']
|
||
}
|
||
|
||
# 保存到文件
|
||
filename = f"exam_result_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||
filepath = os.path.join(".", filename)
|
||
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
json.dump(exam_record, f, ensure_ascii=False, indent=2)
|
||
|
||
print(f"📄 考核结果已保存到: {filepath}")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 保存考核结果失败: {e}")
|
||
# 不影响主流程,只是记录警告
|
||
|
||
def stop_interaction_mode(self):
|
||
"""停止交互模式"""
|
||
self.is_active = False
|
||
self.ignoreAll()
|
||
|
||
# 恢复原有的Qt鼠标事件处理
|
||
print("🔄 恢复原有的Qt鼠标事件处理")
|
||
if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||
self.world.qtWidget.mousePressEvent = self.original_mouse_press_event
|
||
self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event
|
||
print("✅ 原有Qt鼠标事件处理已恢复")
|
||
else:
|
||
print("⚠️ 没有找到备份的Qt事件处理方法")
|
||
|
||
if self.step_dialog:
|
||
self.step_dialog.close()
|
||
self.step_dialog = None
|
||
|
||
self.world.taskMgr.remove("drag_model_task")
|
||
print("拆装交互模式已停止")
|
||
|
||
def trigger_drag_by_keyboard(self):
|
||
"""通过键盘触发拖拽(备用方案)"""
|
||
if not self.is_active or self.dragging_model:
|
||
return
|
||
|
||
print("⌨️ 使用键盘备用方案触发拖拽")
|
||
step_data = self.config_data['steps'][self.current_step]
|
||
target_model_name = step_data.get('target_model')
|
||
|
||
if not target_model_name:
|
||
print("当前步骤没有指定目标模型")
|
||
return
|
||
|
||
target_node = self.find_model_node(target_model_name)
|
||
if not target_node:
|
||
print(f"找不到目标模型: {target_model_name}")
|
||
return
|
||
|
||
# 对于键盘触发,我们无法获取精确的hit_point,
|
||
# 所以直接使用模型的中心点作为模拟的点击点。
|
||
self.start_dragging(target_node, target_node.getPos())
|
||
|
||
def trigger_click_movement(self, model_node, step_data):
|
||
"""触发点击位移动画"""
|
||
try:
|
||
# 检查操作权限(双重保险)
|
||
if not self.check_operation_permission():
|
||
print("⚠️ 权限检查失败,无法进行点击触发位移")
|
||
return
|
||
|
||
step_type = self.normalize_step_type(step_data)
|
||
target_model_name = step_data.get('target_model', '')
|
||
|
||
print(f"🎬 开始点击触发位移: {model_node.getName()}")
|
||
print(f"📋 步骤类型: {step_type}")
|
||
|
||
if step_type == 'disassemble':
|
||
# 拆卸模式:移动到目标位置然后隐藏
|
||
self.perform_disassemble_click_movement(model_node, step_data)
|
||
elif step_type == 'assemble':
|
||
# 安装模式:显示模型然后移动到原始位置
|
||
self.perform_assemble_click_movement(model_node, step_data)
|
||
else:
|
||
print(f"❌ 未知的步骤类型: {step_type}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 点击触发位移失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def perform_disassemble_click_movement(self, model_node, step_data):
|
||
"""执行拆卸点击位移"""
|
||
target_model_name = step_data.get('target_model', '')
|
||
|
||
# 记录当前位置为原始位置(如果还没有记录)
|
||
if target_model_name not in self.model_original_positions:
|
||
current_pos = model_node.getPos()
|
||
self.model_original_positions[target_model_name] = current_pos
|
||
print(f"📍 记录模型 '{target_model_name}' 的原始位置: {current_pos}")
|
||
|
||
# 获取目标位移位置
|
||
target_pos_data = step_data.get('target_position')
|
||
if not target_pos_data:
|
||
print("❌ 拆卸步骤缺少target_position配置")
|
||
return
|
||
|
||
target_pos = Vec3(*target_pos_data)
|
||
print(f"🎯 目标位置: {target_pos}")
|
||
|
||
# 获取动画时长
|
||
animation_duration = step_data.get('animation_duration', 2.0)
|
||
|
||
# 创建位移动画序列
|
||
from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func
|
||
|
||
move_interval = LerpPosInterval(
|
||
model_node,
|
||
animation_duration,
|
||
target_pos,
|
||
name=f"disassemble_{target_model_name}"
|
||
)
|
||
|
||
# 动画完成后的回调
|
||
def on_disassemble_complete():
|
||
print(f"✅ 拆卸动画完成,隐藏模型: {target_model_name}")
|
||
model_node.hide() # 隐藏模型
|
||
self.model_current_states[target_model_name] = 'removed'
|
||
self.complete_current_step() # 完成当前步骤
|
||
|
||
# 创建动画序列
|
||
disassemble_sequence = Sequence(
|
||
move_interval,
|
||
Func(on_disassemble_complete),
|
||
name=f"disassemble_sequence_{target_model_name}"
|
||
)
|
||
|
||
print(f"🎬 开始拆卸动画,持续时间: {animation_duration}秒")
|
||
disassemble_sequence.start()
|
||
|
||
def perform_assemble_click_movement(self, model_node, step_data):
|
||
"""执行安装点击位移"""
|
||
target_model_name = step_data.get('target_model', '')
|
||
|
||
# 确保模型可见(通常在步骤开始时已经显示,这里是双重保险)
|
||
if model_node.isHidden():
|
||
print(f"👁️ 点击触发时显示模型: {target_model_name}")
|
||
model_node.show()
|
||
else:
|
||
print(f"✅ 模型 '{target_model_name}' 已经可见,开始安装动画")
|
||
|
||
# 获取原始位置
|
||
if target_model_name not in self.model_original_positions:
|
||
print(f"❌ 找不到模型 '{target_model_name}' 的原始位置")
|
||
return
|
||
|
||
original_pos = self.model_original_positions[target_model_name]
|
||
print(f"🏠 原始位置: {original_pos}")
|
||
|
||
# 获取动画时长
|
||
animation_duration = step_data.get('animation_duration', 2.0)
|
||
|
||
# 创建位移动画序列
|
||
from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func
|
||
|
||
move_interval = LerpPosInterval(
|
||
model_node,
|
||
animation_duration,
|
||
original_pos,
|
||
name=f"assemble_{target_model_name}"
|
||
)
|
||
|
||
# 动画完成后的回调
|
||
def on_assemble_complete():
|
||
print(f"✅ 安装动画完成,模型回到原位: {target_model_name}")
|
||
model_node.setPos(original_pos) # 确保精确位置
|
||
self.model_current_states[target_model_name] = 'installed'
|
||
self.complete_current_step() # 完成当前步骤
|
||
|
||
# 创建动画序列
|
||
assemble_sequence = Sequence(
|
||
move_interval,
|
||
Func(on_assemble_complete),
|
||
name=f"assemble_sequence_{target_model_name}"
|
||
)
|
||
|
||
print(f"🎬 开始安装动画,持续时间: {animation_duration}秒")
|
||
assemble_sequence.start()
|
||
|
||
|
||
class StepGuideDialog(QDialog):
|
||
"""步骤指引对话框(UI代码保持不变)"""
|
||
|
||
def __init__(self, interaction_manager, parent=None):
|
||
super().__init__(parent)
|
||
self.interaction_manager = interaction_manager
|
||
self.current_required_tool = "无" # 当前步骤要求的工具
|
||
self.mode = interaction_manager.mode # 获取模式
|
||
|
||
self.chinese_font = None
|
||
if hasattr(interaction_manager.world,'getChineseFont'):
|
||
self.chinese_font = interaction_manager.world.getChineseFont()
|
||
|
||
self.setupUI()
|
||
|
||
def setupUI(self):
|
||
if self.mode == "exam":
|
||
self.setWindowTitle("拆装考核")
|
||
else:
|
||
self.setWindowTitle("拆装步骤指引")
|
||
|
||
self.setFixedSize(450, 400)
|
||
self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
|
||
layout = QVBoxLayout(self)
|
||
|
||
# 步骤信息
|
||
self.step_info_label = QLabel("准备开始...")
|
||
self.step_info_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2E86C1;")
|
||
if self.chinese_font:
|
||
self.step_info_label.setFont(self.chinese_font)
|
||
layout.addWidget(self.step_info_label)
|
||
|
||
# 步骤描述(考核模式下隐藏)
|
||
if self.mode != "exam":
|
||
self.step_desc_text = QTextEdit()
|
||
self.step_desc_text.setMaximumHeight(100)
|
||
self.step_desc_text.setReadOnly(True)
|
||
if self.chinese_font:
|
||
self.step_desc_text.setFont(self.chinese_font)
|
||
layout.addWidget(self.step_desc_text)
|
||
else:
|
||
self.step_desc_text = None
|
||
|
||
# 工具选择组
|
||
tool_group = QGroupBox("工具选择")
|
||
if self.chinese_font:
|
||
tool_group.setFont(self.chinese_font)
|
||
tool_layout = QVBoxLayout(tool_group)
|
||
|
||
# 在训练模式下显示要求的工具,考核模式下不显示
|
||
if self.mode != "exam":
|
||
self.required_tool_label = QLabel("当前步骤要求工具: 无")
|
||
self.required_tool_label.setStyleSheet("font-weight: bold; color: #E74C3C;")
|
||
if self.chinese_font:
|
||
self.required_tool_label.setFont(self.chinese_font)
|
||
tool_layout.addWidget(self.required_tool_label)
|
||
else:
|
||
self.required_tool_label = None
|
||
|
||
current_tool_layout = QHBoxLayout()
|
||
current_tool_label = QLabel("当前选择工具:")
|
||
if self.chinese_font:
|
||
current_tool_label.setFont(self.chinese_font)
|
||
current_tool_layout.addWidget(current_tool_label)
|
||
self.current_tool_combo = QComboBox()
|
||
self.current_tool_combo.addItem("无") # 默认只有"无"选项
|
||
self.current_tool_combo.currentTextChanged.connect(self.on_tool_changed)
|
||
if self.chinese_font:
|
||
self.current_tool_combo.setFont(self.chinese_font)
|
||
current_tool_layout.addWidget(self.current_tool_combo)
|
||
tool_layout.addLayout(current_tool_layout)
|
||
|
||
# 在训练模式下显示工具状态,考核模式下不显示
|
||
if self.mode != "exam":
|
||
self.tool_status_label = QLabel("✅ 工具匹配,可以进行操作")
|
||
self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;")
|
||
if self.chinese_font:
|
||
self.tool_status_label.setFont(self.chinese_font)
|
||
tool_layout.addWidget(self.tool_status_label)
|
||
else:
|
||
self.tool_status_label = None
|
||
|
||
layout.addWidget(tool_group)
|
||
|
||
# 操作提示(考核模式下隐藏)
|
||
if self.mode != "exam":
|
||
self.operation_label = QLabel("操作提示:")
|
||
self.operation_label.setStyleSheet("font-weight: bold;")
|
||
if self.chinese_font:
|
||
self.operation_label.setFont(self.chinese_font)
|
||
layout.addWidget(self.operation_label)
|
||
self.operation_text = QTextEdit()
|
||
self.operation_text.setMaximumHeight(80)
|
||
self.operation_text.setReadOnly(True)
|
||
if self.chinese_font:
|
||
self.operation_text.setFont(self.chinese_font)
|
||
layout.addWidget(self.operation_text)
|
||
else:
|
||
self.operation_label = None
|
||
self.operation_text = None
|
||
|
||
# 按钮
|
||
button_layout = QHBoxLayout()
|
||
self.skip_button = QPushButton("跳过当前步骤")
|
||
if self.chinese_font:
|
||
self.skip_button.setFont(self.chinese_font)
|
||
self.skip_button.clicked.connect(self.skip_current_step)
|
||
button_layout.addWidget(self.skip_button)
|
||
self.stop_button = QPushButton("停止交互")
|
||
if self.chinese_font:
|
||
self.stop_button.setFont(self.chinese_font)
|
||
self.stop_button.clicked.connect(self.stop_interaction)
|
||
button_layout.addWidget(self.stop_button)
|
||
layout.addLayout(button_layout)
|
||
|
||
# 初始化工具状态
|
||
self.sync_current_tool()
|
||
self.update_tool_status()
|
||
|
||
def sync_current_tool(self):
|
||
"""初始化拆装工具状态"""
|
||
# 从配置文件中加载工具列表
|
||
self.load_tools_from_config()
|
||
# 默认选择"无"
|
||
self.current_tool_combo.setCurrentText("无")
|
||
print(f"🔧 初始化拆装工具: 无")
|
||
|
||
def load_tools_from_config(self):
|
||
"""从配置文件中加载工具列表"""
|
||
# 清空现有工具选项(保留"无")
|
||
self.current_tool_combo.clear()
|
||
self.current_tool_combo.addItem("无")
|
||
|
||
# 从配置文件中加载工具
|
||
if hasattr(self.interaction_manager, 'config_data') and self.interaction_manager.config_data:
|
||
tools = self.interaction_manager.config_data.get('tools', [])
|
||
for tool in tools:
|
||
tool_name = tool.get('name', '')
|
||
if tool_name and tool_name != "无":
|
||
self.current_tool_combo.addItem(tool_name)
|
||
print(f"🔧 加载工具: {tool_name}")
|
||
|
||
if len(tools) > 0:
|
||
print(f"🔧 从配置文件加载了 {len(tools)} 个工具")
|
||
else:
|
||
print("🔧 配置文件中没有定义工具")
|
||
else:
|
||
print("⚠️ 无法访问配置数据")
|
||
|
||
def on_tool_changed(self, tool_name):
|
||
"""工具改变时的处理"""
|
||
print(f"🔧 用户选择拆装工具: {tool_name}")
|
||
|
||
# 拆装工具不需要同步到编辑器的工具管理器
|
||
# 这里是独立的拆装工具选择
|
||
|
||
# 更新工具状态显示
|
||
self.update_tool_status()
|
||
|
||
def update_tool_status(self):
|
||
"""更新工具状态显示"""
|
||
current_tool = self.current_tool_combo.currentText()
|
||
required_tool = self.current_required_tool
|
||
|
||
# 检查工具是否匹配
|
||
tool_matches = self.check_tool_permission(current_tool, required_tool)
|
||
|
||
if self.mode == "training":
|
||
# 训练模式:显示工具匹配状态
|
||
if tool_matches:
|
||
self.tool_status_label.setText("✅ 工具匹配,可以进行操作")
|
||
self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;")
|
||
# 启用交互操作
|
||
self.interaction_manager.set_operation_enabled(True)
|
||
else:
|
||
self.tool_status_label.setText(f"❌ 工具不匹配,需要选择 '{required_tool}' 工具")
|
||
self.tool_status_label.setStyleSheet("color: #E74C3C; font-weight: bold;")
|
||
# 禁用交互操作
|
||
self.interaction_manager.set_operation_enabled(False)
|
||
else:
|
||
# 考核模式:不显示工具状态,但仍然需要正确的工具才能操作
|
||
# 这里不再总是启用操作,而是让权限检查在实际操作时进行
|
||
self.interaction_manager.set_operation_enabled(True)
|
||
|
||
def check_tool_permission(self, current_tool, required_tool):
|
||
"""检查工具权限"""
|
||
# 如果步骤不要求特定工具,任何工具都可以
|
||
if required_tool == "无" or required_tool == "" or not required_tool:
|
||
return True
|
||
|
||
# 精确匹配
|
||
if current_tool == required_tool:
|
||
return True
|
||
|
||
return False
|
||
|
||
def update_step_info(self, step_data, current_step, total_steps):
|
||
step_name = step_data.get('name', f'步骤 {current_step}')
|
||
|
||
if self.mode == "exam":
|
||
# 考核模式:显示考核信息
|
||
step_score = step_data.get('score', 10)
|
||
self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name} (分值: {step_score})")
|
||
else:
|
||
# 训练模式:正常显示
|
||
self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name}")
|
||
|
||
# 为动态文本设置中文字体
|
||
if self.chinese_font:
|
||
self.step_info_label.setFont(self.chinese_font)
|
||
|
||
# 步骤描述(考核模式下不显示)
|
||
if self.step_desc_text is not None:
|
||
step_desc = step_data.get('description', '无描述')
|
||
self.step_desc_text.setPlainText(step_desc)
|
||
if self.chinese_font:
|
||
self.step_desc_text.setFont(self.chinese_font)
|
||
|
||
# 更新工具要求
|
||
self.current_required_tool = step_data.get('required_tool', '无')
|
||
if self.required_tool_label is not None:
|
||
self.required_tool_label.setText(f"当前步骤要求工具: {self.current_required_tool}")
|
||
if self.chinese_font:
|
||
self.required_tool_label.setFont(self.chinese_font)
|
||
|
||
# 更新工具状态
|
||
self.update_tool_status()
|
||
|
||
operation_type = self.interaction_manager.normalize_operation_type(step_data)
|
||
step_type = self.interaction_manager.normalize_step_type(step_data)
|
||
target_model = step_data.get('target_model', '未指定')
|
||
|
||
if operation_type == 'drag':
|
||
if step_type == 'assemble':
|
||
snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0)
|
||
operation_hint = f"🔧 安装操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 到原始位置附近。\n当距离原始位置 {snap_tolerance:.1f} 单位内时,模型会自动吸附到正确位置。"
|
||
else:
|
||
disassemble_threshold = step_data.get('disassemble_threshold', 5.0)
|
||
operation_hint = f"🔧 拆卸操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 脱离原位。\n拖拽到距离原始位置 {disassemble_threshold:.1f} 单位外即可完成。"
|
||
elif operation_type == 'click':
|
||
if step_type == 'assemble':
|
||
animation_duration = step_data.get('animation_duration', 2.0)
|
||
operation_hint = f"🔧 安装操作(点击触发)\n请点击模型 '{target_model}' 触发自动安装。\n模型将自动移动到原始位置(动画时长 {animation_duration:.1f}秒)。"
|
||
else:
|
||
target_pos_data = step_data.get('target_position')
|
||
animation_duration = step_data.get('animation_duration', 2.0)
|
||
if target_pos_data:
|
||
from panda3d.core import Vec3
|
||
target_pos = Vec3(*target_pos_data)
|
||
operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n模型将自动移动到位置 {target_pos}(动画时长 {animation_duration:.1f}秒),然后隐藏。"
|
||
else:
|
||
operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n⚠️ 警告:缺少target_position配置"
|
||
else:
|
||
# 未知操作类型的回退提示
|
||
if step_type == 'assemble':
|
||
operation_hint = f"🔧 安装操作\n请操作模型 '{target_model}' 将其安装到原位。"
|
||
else:
|
||
operation_hint = f"🔧 拆卸操作\n请操作模型 '{target_model}' 将其拆卸。"
|
||
|
||
# 操作提示(考核模式下不显示)
|
||
if self.operation_text is not None:
|
||
self.operation_text.setPlainText(operation_hint)
|
||
if self.chinese_font:
|
||
self.operation_text.setFont(self.chinese_font)
|
||
|
||
def skip_current_step(self):
|
||
if self.interaction_manager.is_active:
|
||
# 停止可能正在进行的拖拽
|
||
if self.interaction_manager.dragging_model:
|
||
self.interaction_manager.stop_dragging()
|
||
self.interaction_manager.complete_current_step()
|
||
|
||
def stop_interaction(self):
|
||
self.interaction_manager.stop_interaction_mode()
|
||
self.close()
|
||
|
||
def closeEvent(self, event):
|
||
reply = QMessageBox.question(self, "确认", "是否要停止拆装交互?",
|
||
QMessageBox.Yes | QMessageBox.No)
|
||
if reply == QMessageBox.Yes:
|
||
self.stop_interaction()
|
||
event.accept()
|
||
else:
|
||
event.ignore() |