diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 9c9622cd..6bb60b02 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -5,7 +5,7 @@ control_points: clouds: - cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000],[0.4932562620,0.9526462396]]] + cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000]]] color_correction: camera_iso: [[[0.4708024067,0.2757660168]]] camera_shutter: [[[0.5134061147,0.0552217053]]] @@ -17,8 +17,8 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.4833333333]]] - sun_altitude: [[[0.5000000000,0.9777777778]]] + sun_azimuth: [[[0.5000000000,0.4944444444]]] + sun_altitude: [[[0.5000000000,0.9666666667]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: fog_ramp_size: [[[0.5510597303,0.7409470752]]] diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py new file mode 100644 index 00000000..56285bcc --- /dev/null +++ b/core/assembly_interaction.py @@ -0,0 +1,2285 @@ +#!/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() + + 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}'") + + print(f"\n=== 开始第 {self.current_step + 1} 步: {step_data.get('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', '') + + if self.mode == "exam": + # 考核模式:只显示步骤编号和名称,不显示描述 + step_info = f"考核步骤 {self.current_step + 1}/{self.total_steps}: {step_name}" + print(f"📝 考核模式步骤信息: {step_info}") + else: + # 训练模式:显示完整信息 + step_info = f"步骤 {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.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;") + 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) + layout.addWidget(self.step_desc_text) + else: + self.step_desc_text = None + + # 工具选择组 + tool_group = QGroupBox("工具选择") + tool_layout = QVBoxLayout(tool_group) + + # 在训练模式下显示要求的工具,考核模式下不显示 + if self.mode != "exam": + self.required_tool_label = QLabel("当前步骤要求工具: 无") + self.required_tool_label.setStyleSheet("font-weight: bold; color: #E74C3C;") + tool_layout.addWidget(self.required_tool_label) + else: + self.required_tool_label = None + + current_tool_layout = QHBoxLayout() + current_tool_layout.addWidget(QLabel("当前选择工具:")) + self.current_tool_combo = QComboBox() + self.current_tool_combo.addItem("无") # 默认只有"无"选项 + self.current_tool_combo.currentTextChanged.connect(self.on_tool_changed) + 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;") + 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;") + layout.addWidget(self.operation_label) + self.operation_text = QTextEdit() + self.operation_text.setMaximumHeight(80) + self.operation_text.setReadOnly(True) + layout.addWidget(self.operation_text) + else: + self.operation_label = None + self.operation_text = None + + # 按钮 + button_layout = QHBoxLayout() + self.skip_button = QPushButton("跳过当前步骤") + self.skip_button.clicked.connect(self.skip_current_step) + button_layout.addWidget(self.skip_button) + self.stop_button = QPushButton("停止交互") + 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.step_desc_text is not None: + step_desc = step_data.get('description', '无描述') + self.step_desc_text.setPlainText(step_desc) + + # 更新工具要求 + 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}") + + # 更新工具状态 + 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) + + 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() \ No newline at end of file diff --git a/core/collision_manager.py b/core/collision_manager.py index 6c4b3e73..6117ea30 100644 --- a/core/collision_manager.py +++ b/core/collision_manager.py @@ -19,10 +19,10 @@ class CollisionManager: # === 基础碰撞掩码定义 === # 每个掩码使用不同的位(bit)来标识,可以进行位运算组合 - 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测 - 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测 - 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测 - 'MODEL_COLLISION': BitMask32.bit(3), # 模型碰撞 - 通用模型间碰撞检测 + # 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测 + # 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测 + # 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测 + 'MODEL_COLLISION': BitMask32.bit(6), # 模型碰撞 - 通用模型间碰撞检测 } # 碰撞体形状类型 diff --git a/core/maintenance_gui.py b/core/maintenance_gui.py new file mode 100644 index 00000000..cbc66836 --- /dev/null +++ b/core/maintenance_gui.py @@ -0,0 +1,1090 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +维修系统场景GUI界面 +包括步骤显示、工具选择按钮和警告提示 +""" + +from direct.gui.DirectGui import * +from direct.gui import DirectGuiGlobals as DGG +from direct.showbase.DirectObject import DirectObject +from direct.task import Task +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import TextNode, Vec3, Vec4 + +# 引入全局变量 +import builtins + + +class MaintenanceGUI(DirectObject): + """维修系统GUI管理器""" + + def __init__(self, world): + DirectObject.__init__(self) + self.world = world + + # 获取GUI父节点引用 + self.aspect2d = None + print(f"🔍 初始化维修GUI,world类型: {type(world).__name__}") + print(f"🔍 world是否有aspect2d: {hasattr(world, 'aspect2d')}") + + if hasattr(world, 'aspect2d'): + self.aspect2d = world.aspect2d + print("✅ 从world获取aspect2d成功") + else: + print("❌ world没有aspect2d属性") + + # GUI元素 + self.step_text = None # 步骤显示文本 + self.current_tool_text = None # 当前工具显示文本 + self.warning_text = None # 警告文本 + self.tool_buttons = [] # 工具按钮列表 + self.stop_button = None # 停止按钮 + + # 状态 + self.current_tool = None # 当前选择的工具,初始为无 + self.available_tools = [] # 可用工具列表 + self.current_step_info = "" # 当前步骤信息 + self.mode = "training" # 模式:training 或 exam + + # 警告显示任务 + self.warning_task = None + + print("✅ 维修系统GUI初始化完成") + + # 添加测试快捷键 + self.accept("t", self.test_tool_selection) # 按T键测试工具选择 + self.accept("1", lambda: self.test_specific_button(0)) # 按1键测试第一个按钮 + self.accept("2", lambda: self.test_specific_button(1)) # 按2键测试第二个按钮 + self.accept("3", lambda: self.test_specific_button(2)) # 按3键测试第三个按钮 + self.accept("p", self.print_button_positions) # 按P键打印按钮位置 + + # 添加鼠标监控任务 + self.mouse_monitor_task = None + + def setup_gui(self, tools_list, mode="training"): + """设置GUI界面""" + try: + print(f"🎨 设置维修系统GUI,工具列表: {tools_list}, 模式: {mode}") + + self.available_tools = tools_list if tools_list else ["手"] + self.mode = mode + + # 重新尝试获取aspect2d引用 + self.get_aspect2d_reference() + + # 清理现有GUI + self.cleanup_gui() + + # 创建步骤显示文本 + self.create_step_text() + + # 创建工具选择按钮(训练和考核模式都需要) + self.create_tool_buttons() + self.create_current_tool_text() + + # 创建停止按钮(训练和考核模式都需要) + # self.create_stop_button() + + # 创建警告文本(仅训练模式需要) + if mode == "training": + self.create_warning_text() + + print("✅ 维修系统GUI设置完成") + + # 启动鼠标监控任务(训练和考核模式都需要) + self.start_mouse_monitor() + + except Exception as e: + print(f"❌ 设置维修系统GUI失败: {e}") + import traceback + traceback.print_exc() + + def start_mouse_monitor(self): + """启动鼠标监控任务""" + try: + # 停止可能存在的旧任务 + if self.mouse_monitor_task: + taskMgr.remove(self.mouse_monitor_task) + + # 启动新的监控任务 + self.mouse_monitor_task = taskMgr.add(self.monitor_mouse_clicks, "mouse_monitor") + print("✅ 鼠标监控任务已启动") + print(f"✅ 任务管理器中的任务: {[task.getName() for task in taskMgr.getAllTasks()]}") + + # 测试一下任务是否能正常运行 + self.test_mouse_monitor() + + except Exception as e: + print(f"❌ 启动鼠标监控失败: {e}") + import traceback + traceback.print_exc() + + def test_mouse_monitor(self): + """测试鼠标监控功能""" + try: + print("🧪 测试鼠标监控功能...") + + # 尝试直接调用监控函数 + result = self.monitor_mouse_clicks(None) + print(f"🧪 监控函数调用结果: {result}") + + except Exception as e: + print(f"❌ 测试鼠标监控失败: {e}") + import traceback + traceback.print_exc() + + def monitor_mouse_clicks(self, task): + """监控鼠标点击并手动检测按钮""" + try: + # 添加计数器来减少日志输出 + if not hasattr(self, '_monitor_counter'): + self._monitor_counter = 0 + self._monitor_counter += 1 + + # 每100帧输出一次状态 + if self._monitor_counter % 100 == 1: + print(f"🔄 鼠标监控运行中... (帧 {self._monitor_counter})") + + # 检查鼠标状态 + mouse_watcher = None + if hasattr(self.world, 'mouseWatcherNode'): + mouse_watcher = self.world.mouseWatcherNode + if self._monitor_counter % 100 == 1: + print(f"✅ 从world获取mouseWatcherNode: {mouse_watcher}") + else: + # 使用全局base对象 + try: + import builtins + if hasattr(builtins, 'base') and hasattr(builtins.base, 'mouseWatcherNode'): + mouse_watcher = builtins.base.mouseWatcherNode + if self._monitor_counter % 100 == 1: + print(f"✅ 从base获取mouseWatcherNode: {mouse_watcher}") + except: + if self._monitor_counter % 100 == 1: + print("❌ 无法获取mouseWatcherNode") + + if mouse_watcher: + has_mouse = mouse_watcher.hasMouse() + if has_mouse: + mouse_pos = mouse_watcher.getMouse() + + # 检查左键状态 + mouse_x = mouse_pos.getX() + mouse_y = mouse_pos.getY() + + # 检查左键是否被按下 + is_button_down = mouse_watcher.isButtonDown('mouse1') + + # 如果没有记录当前状态,初始化 + if not hasattr(self, '_last_button_state'): + self._last_button_state = False + + # 检测按钮从未按下到按下的状态变化(按下瞬间) + if is_button_down and not self._last_button_state: + print(f"🖱️ 鼠标监控:检测到按钮按下,位置({mouse_x:.3f}, {mouse_y:.3f})") + + # 检查这次点击是否在按钮区域 + clicked_button = self.check_button_click(mouse_x, mouse_y) + if clicked_button is not None: + print(f"🎯 鼠标监控检测到按钮点击: {clicked_button}") + self.on_tool_selected(clicked_button) + + # 更新按钮状态 + self._last_button_state = is_button_down + else: + if self._monitor_counter % 100 == 1: + print("❌ mouseWatcher为空") + + return task.cont if task else None + + except Exception as e: + print(f"❌ 鼠标监控异常: {e}") + import traceback + traceback.print_exc() + return task.cont if task else None + + def check_button_click(self, mouse_x, mouse_y): + """检查鼠标点击是否在按钮区域""" + try: + button_width = 0.4 # 与create_tool_buttons保持一致 + button_height = 0.25 # 与create_tool_buttons保持一致 + button_spacing = 0.45 # 与create_tool_buttons保持一致 + start_x = -0.8 # 与create_tool_buttons保持一致 + start_y = -0.75 # 与create_tool_buttons保持一致 + + print(f"🔍 检查按钮点击:鼠标({mouse_x:.3f}, {mouse_y:.3f})") + + for i, tool_data in enumerate(self.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 + right = button_x + button_width/2 + bottom = button_y - button_height/2 + top = button_y + button_height/2 + + print(f" 按钮{i} '{tool_name}': 中心({button_x:.3f}, {button_y:.3f}), 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + + # 检查鼠标是否在按钮范围内 + if (left <= mouse_x <= right and bottom <= mouse_y <= top): + print(f"✅ 点击在按钮{i} '{tool_name}' 范围内") + return tool_name + + print("❌ 点击不在任何按钮范围内") + return None + + except Exception as e: + print(f"❌ 检查按钮点击异常: {e}") + return None + + def handle_mouse_click(self, qt_x, qt_y): + """直接处理鼠标点击(从拆装交互系统调用)""" + try: + print(f"🖱️ 直接处理鼠标点击:Qt坐标({qt_x}, {qt_y})") + + # 将Qt坐标转换为aspect2d坐标 + # 获取窗口尺寸 + 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 + + # 坐标转换(与之前拆装交互系统中的算法一致) + aspect_x = (qt_x / widget_width) * 2.67 - 1.33 + aspect_y = 1.0 - (qt_y / widget_height) * 2.0 + + print(f"🔄 坐标转换:Qt({qt_x}, {qt_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") + + # 检查是否点击在按钮区域 + clicked_button = self.check_button_click(aspect_x, aspect_y) + if clicked_button: + print(f"🎯 直接处理:检测到按钮点击 '{clicked_button}'") + self.on_tool_selected(clicked_button) + return True + + return False + + except Exception as e: + print(f"❌ 直接处理鼠标点击失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_aspect2d_reference(self): + """获取aspect2d引用""" + if self.aspect2d: + return # 已经有引用了 + + try: + print("🔍 重新尝试获取aspect2d引用...") + + # 方法1: 直接使用world对象(推荐) + if hasattr(self.world, 'aspect2d'): + self.aspect2d = self.world.aspect2d + print("✅ 从world获取aspect2d成功") + return + + # 方法2: 使用world对象作为父节点 + # 在main.py的Panda3DWorld中,world对象本身就是ShowBase的实例 + self.aspect2d = self.world.aspect2d if hasattr(self.world, 'aspect2d') else None + + if self.aspect2d: + print("✅ 使用world.aspect2d成功") + else: + print("❌ 无法获取aspect2d引用") + + except Exception as e: + print(f"❌ 获取aspect2d引用失败: {e}") + import traceback + traceback.print_exc() + + def create_step_text(self): + """创建步骤显示文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建步骤文本") + return + + self.step_text = DirectLabel( + text="准备开始维修训练...", + text_align=TextNode.ALeft, + text_scale=0.08, + text_fg=(1, 1, 1, 1), + text_bg=(0, 0, 0, 0.7), + frameColor=(0, 0, 0, 0.5), + frameSize=(-1.8, 1.8, -0.3, 0.3), + pos=(-1.7, 0, 0.8), + parent=self.aspect2d + ) + print("✅ 步骤显示文本创建成功") + + except Exception as e: + print(f"❌ 创建步骤文本失败: {e}") + import traceback + traceback.print_exc() + + def create_tool_buttons(self): + """创建工具选择按钮""" + try: + print(f"🔧 开始创建工具按钮...") + print(f" aspect2d引用: {self.aspect2d}") + print(f" 可用工具: {self.available_tools}") + print(f" 模式: {self.mode}") + + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建工具按钮") + print(" 尝试重新获取aspect2d引用...") + self.get_aspect2d_reference() + if not self.aspect2d: + print("❌ 重新获取aspect2d失败") + return + + print(f"🔧 使用aspect2d创建工具按钮: {self.available_tools}") + + # 按钮布局参数(优化为更容易点击) + button_width = 0.4 # 增加宽度 + button_height = 0.25 # 增加高度,让按钮更容易点击 + button_spacing = 0.45 # 增加间距 + start_x = -0.8 # 进一步向右移动 + start_y = -0.75 # 向上移动一点 + + for i, tool in enumerate(self.available_tools): + # 处理工具数据格式 + if isinstance(tool, dict): + tool_name = tool.get('name', str(tool)) + tool_description = tool.get('description', '') + else: + tool_name = str(tool) + tool_description = '' + + print(f" 正在创建按钮 {i+1}/{len(self.available_tools)}: {tool_name}") + + # 计算按钮位置(横向排列) + pos_x = start_x + i * button_spacing + pos_y = start_y + + # 创建按钮 + button = DirectButton( + text=tool_name, + text_scale=0.06, + text_fg=(1, 1, 1, 1), + frameColor=(0.3, 0.3, 0.8, 0.8), + frameSize=(-button_width/2, button_width/2, -button_height/2, button_height/2), + pos=(pos_x, 0, pos_y), + command=self.on_tool_selected, + extraArgs=[tool_name], + parent=self.aspect2d, + # 按钮交互设置 + relief=DGG.RAISED, + borderWidth=(0.01, 0.01), + text_pos=(0, -0.02), + # 强制启用鼠标交互 + enableEdit=1, + # 音效设置 + clickSound=None, + rolloverSound=None, + # 确保按钮优先级和可见性 + sortOrder=1000, # 更高的排序值 + state=DGG.NORMAL, # 确保按钮状态正常 + # 鼠标事件设置 + suppressMouse=0 + ) + + # 手动绑定额外的事件监听 + button.bind(DGG.B1PRESS, self.button_press_handler, extraArgs=[tool_name]) + button.bind(DGG.B1RELEASE, self.button_release_handler, extraArgs=[tool_name]) + + self.tool_buttons.append(button) + print(f" ✅ 成功创建工具按钮: {tool_name} at ({pos_x}, {pos_y})") + + print(f"🎯 总共创建了 {len(self.tool_buttons)} 个工具按钮") + + # 设置默认选择的工具样式 + self.update_tool_button_styles() + + except Exception as e: + print(f"❌ 创建工具按钮失败: {e}") + import traceback + traceback.print_exc() + + def create_current_tool_text(self): + """创建当前工具显示文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建当前工具文本") + return + + self.current_tool_text = DirectLabel( + text=f"当前工具: {self.current_tool}", + text_align=TextNode.ALeft, + text_scale=0.07, + text_fg=(1, 1, 0, 1), + text_bg=(0, 0, 0, 0.7), + frameColor=(0, 0, 0, 0.5), + frameSize=(-0.8, 0.8, -0.2, 0.2), + pos=(1.0, 0, 0.8), + parent=self.aspect2d + ) + print("✅ 当前工具文本创建成功") + + except Exception as e: + print(f"❌ 创建当前工具文本失败: {e}") + import traceback + traceback.print_exc() + + def create_stop_button(self): + """创建停止按钮""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建停止按钮") + return + + # 停止按钮位置:右下角 + self.stop_button = DirectButton( + text="停止", + text_align=TextNode.ACenter, + text_scale=0.06, + text_fg=(1, 1, 1, 1), + text_bg=(0, 0, 0, 0), + frameColor=(0.8, 0.2, 0.2, 0.9), # 红色背景 + frameSize=(-0.15, 0.15, -0.08, 0.08), + pos=(1.15, 0, -0.85), # 右下角位置 + parent=self.aspect2d, + command=self.on_stop_clicked, + relief=DGG.RAISED, + borderWidth=(0.01, 0.01), + rolloverSound=None, + clickSound=None, + pressEffect=1 + ) + + print("✅ 停止按钮创建成功") + + except Exception as e: + print(f"❌ 创建停止按钮失败: {e}") + import traceback + traceback.print_exc() + + def create_warning_text(self): + """创建警告文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建警告文本") + return + + self.warning_text = DirectLabel( + text="", + text_align=TextNode.ACenter, + text_scale=0.09, + text_fg=(1, 0, 0, 1), + text_bg=(1, 1, 0, 0.8), + frameColor=(1, 0, 0, 0.8), + frameSize=(-1.5, 1.5, -0.3, 0.3), + pos=(0, 0, 0.3), + parent=self.aspect2d + ) + # 默认隐藏警告文本 + self.warning_text.hide() + print("✅ 警告文本创建成功") + + except Exception as e: + print(f"❌ 创建警告文本失败: {e}") + import traceback + traceback.print_exc() + + def button_press_handler(self, tool_name, event=None): + """按钮按下事件处理""" + print(f"🖱️ 按钮按下事件:{tool_name}") + + def button_release_handler(self, tool_name, event=None): + """按钮释放事件处理""" + print(f"🖱️ 按钮释放事件:{tool_name}") + print(f"🔄 直接调用工具选择:{tool_name}") + self.on_tool_selected(tool_name) + + def on_tool_selected(self, tool): + """工具选择回调""" + try: + print(f"🎯 按钮点击事件触发!") + print(f"🔧 选择工具: {tool}") + + # 考核模式下额外提示 + if hasattr(self, 'mode') and self.mode == "exam": + print(f"📝 考核模式工具选择: {tool}") + + print(f"📍 当前工具变更: {self.current_tool} -> {tool}") + + self.current_tool = tool + + # 更新当前工具显示 + if self.current_tool_text: + self.current_tool_text['text'] = f"当前工具: {tool}" + print(f"✅ 当前工具显示已更新") + + # 更新按钮样式 + self.update_tool_button_styles() + print(f"✅ 按钮样式已更新") + + # 通知拆装交互系统工具变更 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog: + # 更新步骤对话框中的工具选择 + step_dialog = self.world.assembly_interaction.step_dialog + if hasattr(step_dialog, 'tool_combo'): + # 同步工具选择 + tool_index = -1 + for i in range(step_dialog.tool_combo.count()): + if step_dialog.tool_combo.itemText(i) == tool: + tool_index = i + break + if tool_index >= 0: + step_dialog.tool_combo.setCurrentIndex(tool_index) + print(f"✅ Qt对话框工具选择已同步") + + print(f"✅ 工具切换完成: {tool}") + + except Exception as e: + print(f"❌ 工具选择失败: {e}") + import traceback + traceback.print_exc() + + def on_stop_clicked(self): + """停止按钮点击回调""" + try: + print("🛑 用户点击停止按钮") + + # 通知拆装交互系统停止 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + self.world.assembly_interaction.stop_interaction() + else: + print("⚠️ 拆装交互系统不存在,无法停止") + + except Exception as e: + print(f"❌ 停止操作失败: {e}") + import traceback + traceback.print_exc() + + def update_tool_button_styles(self): + """更新工具按钮样式""" + try: + for i, button in enumerate(self.tool_buttons): + # 处理工具数据格式 + tool_data = self.available_tools[i] + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + if tool_name == self.current_tool: + # 选中状态:亮蓝色 + button['frameColor'] = (0.2, 0.8, 1.0, 0.9) + button['text_fg'] = (0, 0, 0, 1) + else: + # 未选中状态:深蓝色 + button['frameColor'] = (0.3, 0.3, 0.8, 0.8) + button['text_fg'] = (1, 1, 1, 1) + + except Exception as e: + print(f"❌ 更新按钮样式失败: {e}") + import traceback + traceback.print_exc() + + def update_step_info(self, step_text): + """更新步骤信息""" + try: + print(f"📋 更新步骤信息: {step_text}") + + self.current_step_info = step_text + + if self.step_text: + self.step_text['text'] = step_text + + except Exception as e: + print(f"❌ 更新步骤信息失败: {e}") + + def show_warning(self, message, duration=2.0): + """显示警告信息(仅训练模式)""" + try: + # 考核模式下不显示警告 + if hasattr(self, 'mode') and self.mode == "exam": + print(f"📝 考核模式:隐藏警告 - {message}") + return + + print(f"⚠️ 显示警告: {message}") + + if not self.warning_text: + return + + # 取消之前的警告任务 + if self.warning_task: + taskMgr.remove(self.warning_task) + self.warning_task = None + + # 显示警告文本 + self.warning_text['text'] = message + self.warning_text.show() + + # 设置自动隐藏任务 + self.warning_task = taskMgr.doMethodLater( + duration, + self.hide_warning, + 'hide_warning_task' + ) + + except Exception as e: + print(f"❌ 显示警告失败: {e}") + + def hide_warning(self, task=None): + """隐藏警告信息""" + try: + if self.warning_text: + self.warning_text.hide() + + if self.warning_task: + self.warning_task = None + + return Task.done + + except Exception as e: + print(f"❌ 隐藏警告失败: {e}") + return Task.done + + def test_tool_selection(self): + """测试工具选择功能(按T键触发)""" + if self.available_tools: + # 循环选择工具进行测试 + current_index = 0 + for i, tool_data in enumerate(self.available_tools): + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + if tool_name == self.current_tool: + current_index = i + break + + # 选择下一个工具 + next_index = (current_index + 1) % len(self.available_tools) + next_tool_data = self.available_tools[next_index] + + if isinstance(next_tool_data, dict): + next_tool_name = next_tool_data.get('name', str(next_tool_data)) + else: + next_tool_name = str(next_tool_data) + + print(f"⌨️ 键盘测试:切换到工具 '{next_tool_name}'") + self.on_tool_selected(next_tool_name) + + def test_specific_button(self, button_index): + """测试特定按钮(通过索引)""" + if not hasattr(self, 'available_tools') or not self.available_tools: + print(f"⌨️ 按钮{button_index}: 没有可用工具") + return + + if button_index >= len(self.available_tools): + print(f"⌨️ 按钮{button_index}: 索引超出范围(最大{len(self.available_tools)-1})") + return + + # 获取工具名称 + tool_data = self.available_tools[button_index] + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + print(f"⌨️ 按钮{button_index}测试:切换到工具 '{tool_name}'") + self.on_tool_selected(tool_name) + + def print_button_positions(self): + """打印所有按钮的位置信息""" + try: + if not hasattr(self, 'available_tools') or not self.available_tools: + print("📍 没有可用工具按钮") + return + + print("📍 ===== 按钮位置信息 =====") + + button_width = 0.4 + button_height = 0.25 + button_spacing = 0.45 + start_x = -0.8 + start_y = -0.75 + + for i, tool_data in enumerate(self.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 + right = button_x + button_width/2 + bottom = button_y - button_height/2 + top = button_y + button_height/2 + + print(f"📍 按钮{i} '{tool_name}':") + print(f" 中心: ({button_x:.3f}, {button_y:.3f})") + print(f" 范围: x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + print(f" 尺寸: {button_width} x {button_height}") + + print("📍 ========================") + + except Exception as e: + print(f"❌ 打印按钮位置失败: {e}") + import traceback + traceback.print_exc() + + def show_exam_results(self, exam_data): + """显示考核结果(GUI界面)""" + try: + print("🎓 开始显示考核结果GUI...") + + # 清理现有GUI元素 + self.cleanup_gui() + + # 创建考核结果界面 + self.create_exam_result_gui(exam_data) + + print("✅ 考核结果GUI显示完成") + + except Exception as e: + print(f"❌ 显示考核结果GUI失败: {e}") + import traceback + traceback.print_exc() + + def create_exam_result_gui(self, exam_data): + """创建考核结果GUI界面""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建考核结果GUI") + return + + # 主背景 - 黑色背景 + from direct.gui.DirectGui import DirectFrame + self.exam_bg = DirectFrame( + frameColor=(0, 0, 0, 0.95), # 更深的黑色背景 + frameSize=(-2.0, 2.0, -1.0, 1.0), + pos=(0, 0, 0), + parent=self.aspect2d + ) + + # 标题 + self.exam_title = DirectLabel( + text="🎓 考核结果报告", + text_align=TextNode.ACenter, + text_scale=0.12, + text_fg=(1, 1, 0, 1), # 黄色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.5, 1.5, -0.15, 0.15), + pos=(0, 0, 0.8), + parent=self.aspect2d + ) + + # 总体成绩区域 + score_text = f"📊 总得分: {exam_data['final_score']:.0f}/{exam_data['max_score']:.0f} 分 ({exam_data['score_rate']:.1f}%)\n" + score_text += f"🏅 评级: {exam_data['grade_icon']} {exam_data['grade']} - {exam_data['grade_desc']}\n" + score_text += f"🎯 操作准确率: {exam_data['accuracy_rate']:.1f}% | 完美步骤: {exam_data['perfect_steps']}/{exam_data['total_steps']}" + + # 根据成绩选择文字颜色(在黑色背景下更突出) + if exam_data['score_rate'] >= 90: + score_fg_color = (0.2, 1.0, 0.2, 1) # 亮绿色 + elif exam_data['score_rate'] >= 80: + score_fg_color = (0.8, 1.0, 0.2, 1) # 亮黄绿色 + elif exam_data['score_rate'] >= 60: + score_fg_color = (1.0, 0.8, 0.2, 1) # 亮橙色 + else: + score_fg_color = (1.0, 0.3, 0.3, 1) # 亮红色 + + self.exam_score = DirectLabel( + text=score_text, + text_align=TextNode.ACenter, + text_scale=0.07, + text_fg=score_fg_color, # 根据成绩动态变色 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.9, 1.9, -0.3, 0.3), + pos=(0, 0, 0.35), + parent=self.aspect2d + ) + + # 步骤详情区域 + steps_text = "📋 步骤详情:\n" + for step in exam_data['step_details']: + steps_text += f"{step['status']} {step['name']}: {step['current_score']:.0f}/{step['max_score']:.0f}分" + if step['tool_error']: + steps_text += " ❌工具错误" + steps_text += f" ({step['attempts']}次操作)\n" + + self.exam_steps = DirectLabel( + text=steps_text, + text_align=TextNode.ALeft, + text_scale=0.06, + text_fg=(0.9, 0.9, 0.9, 1), # 浅灰色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.8, 1.8, -0.4, 0.4), + pos=(0, 0, -0.1), + parent=self.aspect2d + ) + + # 改进建议区域(如果有建议) + if exam_data['suggestions']: + suggestions_text = "💡 改进建议:\n" + for suggestion in exam_data['suggestions']: + suggestions_text += f"{suggestion}\n" + + self.exam_suggestions = DirectLabel( + text=suggestions_text, + text_align=TextNode.ALeft, + text_scale=0.05, + text_fg=(1, 0.8, 0.2, 1), # 橙黄色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.8, 1.8, -0.25, 0.25), + pos=(0, 0, -0.7), + parent=self.aspect2d + ) + + # 倒计时显示 + self.exam_countdown_text = DirectLabel( + text="10秒后自动关闭", + text_align=TextNode.ACenter, + text_scale=0.06, + text_fg=(0.8, 0.8, 0.8, 1), # 灰色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-0.8, 0.8, -0.1, 0.1), + pos=(0, 0, -0.8), + parent=self.aspect2d + ) + + # 启动10秒倒计时任务 + self.start_countdown_timer() + + print("✅ 考核结果GUI元素创建完成") + + except Exception as e: + print(f"❌ 创建考核结果GUI失败: {e}") + import traceback + traceback.print_exc() + + def close_exam_results(self): + """关闭考核结果界面""" + try: + print("🔄 关闭考核结果界面...") + + # 清理考核结果GUI元素 + if hasattr(self, 'exam_bg') and self.exam_bg: + self.exam_bg.removeNode() + self.exam_bg = None + + if hasattr(self, 'exam_title') and self.exam_title: + self.exam_title.removeNode() + self.exam_title = None + + if hasattr(self, 'exam_score') and self.exam_score: + self.exam_score.removeNode() + self.exam_score = None + + if hasattr(self, 'exam_steps') and self.exam_steps: + self.exam_steps.removeNode() + self.exam_steps = None + + if hasattr(self, 'exam_suggestions') and self.exam_suggestions: + self.exam_suggestions.removeNode() + self.exam_suggestions = None + + if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: + self.exam_countdown_text.removeNode() + self.exam_countdown_text = None + + # 停止倒计时任务 + if hasattr(self, 'exam_countdown_task') and self.exam_countdown_task: + taskMgr.remove(self.exam_countdown_task) + self.exam_countdown_task = None + + print("✅ 考核结果界面已关闭") + + except Exception as e: + print(f"❌ 关闭考核结果界面失败: {e}") + import traceback + traceback.print_exc() + + def start_countdown_timer(self): + """启动10秒倒计时""" + try: + self.countdown_seconds = 10 + print(f"⏰ 启动考核结果倒计时:{self.countdown_seconds}秒") + + # 启动倒计时任务 + self.exam_countdown_task = taskMgr.doMethodLater( + 1.0, # 每秒执行一次 + self.update_countdown, + 'exam_countdown_task' + ) + + except Exception as e: + print(f"❌ 启动倒计时失败: {e}") + import traceback + traceback.print_exc() + + def update_countdown(self, task): + """更新倒计时显示""" + try: + self.countdown_seconds -= 1 + + if self.countdown_seconds > 0: + # 更新倒计时显示 + if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: + self.exam_countdown_text['text'] = f"{self.countdown_seconds}秒后自动关闭" + + print(f"⏰ 倒计时:{self.countdown_seconds}秒") + + # 继续倒计时 + return task.again + else: + # 倒计时结束,自动关闭 + print("⏰ 倒计时结束,自动关闭考核结果界面") + self.close_exam_results() + return task.done + + except Exception as e: + print(f"❌ 更新倒计时失败: {e}") + import traceback + traceback.print_exc() + return task.done + + + def get_current_tool(self): + """获取当前选择的工具""" + return self.current_tool + + def set_mode(self, mode): + """设置模式""" + self.mode = mode + print(f"🎯 维修GUI模式设置为: {mode}") + + def cleanup_gui(self): + """清理GUI元素""" + try: + print("🧹 清理维修系统GUI") + + # 清理步骤文本 + if self.step_text: + self.step_text.destroy() + self.step_text = None + + # 清理当前工具文本 + if self.current_tool_text: + self.current_tool_text.destroy() + self.current_tool_text = None + + # 清理警告文本 + if self.warning_text: + self.warning_text.destroy() + self.warning_text = None + + # 清理工具按钮 + for button in self.tool_buttons: + if button: + button.destroy() + self.tool_buttons.clear() + + # 清理停止按钮 + if hasattr(self, 'stop_button') and self.stop_button: + self.stop_button.destroy() + self.stop_button = None + + # 取消警告任务 + if self.warning_task: + taskMgr.remove(self.warning_task) + self.warning_task = None + + # 取消鼠标监控任务 + if self.mouse_monitor_task: + taskMgr.remove(self.mouse_monitor_task) + self.mouse_monitor_task = None + + # 清理考核结果界面 + self.close_exam_results() + + print("✅ 维修系统GUI清理完成") + + except Exception as e: + print(f"❌ 清理维修系统GUI失败: {e}") + + def show_gui(self): + """显示GUI""" + try: + print(f"🎨 显示维修系统GUI...") + print(f" 模式: {self.mode}") + print(f" 步骤文本: {self.step_text is not None}") + print(f" 当前工具文本: {self.current_tool_text is not None}") + print(f" 工具按钮数量: {len(self.tool_buttons)}") + + if self.step_text: + self.step_text.show() + print(" ✅ 步骤文本已显示") + + # 当前工具文本和工具按钮在训练和考核模式都需要显示 + if self.current_tool_text: + self.current_tool_text.show() + print(" ✅ 当前工具文本已显示") + + for i, button in enumerate(self.tool_buttons): + button.show() + print(f" ✅ 工具按钮 {i+1} 已显示") + + # 停止按钮在训练和考核模式都需要显示 + if self.stop_button: + self.stop_button.show() + print(" ✅ 停止按钮已显示") + + print("✅ 维修系统GUI显示完成") + + except Exception as e: + print(f"❌ 显示维修系统GUI失败: {e}") + import traceback + traceback.print_exc() + + def hide_gui(self): + """隐藏GUI""" + try: + if self.step_text: + self.step_text.hide() + + if self.current_tool_text: + self.current_tool_text.hide() + + if self.warning_text: + self.warning_text.hide() + + for button in self.tool_buttons: + button.hide() + + if self.stop_button: + self.stop_button.hide() + + print("✅ 维修系统GUI隐藏") + + except Exception as e: + print(f"❌ 隐藏维修系统GUI失败: {e}") \ No newline at end of file diff --git a/ui/assembly_disassembly_config.py b/ui/assembly_disassembly_config.py new file mode 100644 index 00000000..e663decc --- /dev/null +++ b/ui/assembly_disassembly_config.py @@ -0,0 +1,944 @@ +# -*- coding: utf-8 -*- +""" +拆装交互配置界面 +实现模型的分步拆装交互功能配置 +""" + +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, + QWidget, QLabel, QListWidget, QListWidgetItem, + QPushButton, QComboBox, QSpinBox, QLineEdit, + QTextEdit, QGroupBox, QGridLayout, QCheckBox, + QSplitter, QTreeWidget, QTreeWidgetItem, + QMessageBox, QFileDialog, QScrollArea, + QFrame, QDoubleSpinBox, QSlider, QProgressBar) +from PyQt5.QtCore import Qt, pyqtSignal, QTimer +from PyQt5.QtGui import QIcon, QPixmap, QFont +import os +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() + + def setupUI(self): + """设置界面""" + self.setWindowTitle("拆装交互配置") + self.setModal(False) # 设置为非模态对话框 + self.resize(1200, 800) + + # 主布局 + main_layout = QVBoxLayout(self) + + # 创建分割器 + splitter = QSplitter(Qt.Horizontal) + main_layout.addWidget(splitter) + + # 左侧面板 - 模型和工具选择 + left_panel = self.createLeftPanel() + splitter.addWidget(left_panel) + + # 右侧面板 - 步骤配置 + right_panel = self.createRightPanel() + splitter.addWidget(right_panel) + + # 设置分割器比例 + splitter.setSizes([400, 800]) + + # 底部按钮 + 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 createLeftPanel(self): + """创建左侧面板""" + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + # 场景模型选择区域 + models_group = QGroupBox("场景模型") + models_layout = QVBoxLayout(models_group) + + # 模型搜索框 + search_layout = QHBoxLayout() + search_label = QLabel("搜索:") + self.model_search_edit = QLineEdit() + self.model_search_edit.setPlaceholderText("输入模型名称搜索...") + search_layout.addWidget(search_label) + search_layout.addWidget(self.model_search_edit) + models_layout.addLayout(search_layout) + + # 场景模型列表 + self.scene_models_list = QTreeWidget() + self.scene_models_list.setHeaderLabel("场景中的模型") + self.scene_models_list.setSelectionMode(QTreeWidget.ExtendedSelection) + models_layout.addWidget(self.scene_models_list) + + # 选中模型操作按钮 + model_buttons_layout = QHBoxLayout() + self.add_to_config_button = QPushButton("添加到配置") + self.remove_from_config_button = QPushButton("从配置移除") + model_buttons_layout.addWidget(self.add_to_config_button) + model_buttons_layout.addWidget(self.remove_from_config_button) + models_layout.addLayout(model_buttons_layout) + + left_layout.addWidget(models_group) + + # 已配置模型列表 + config_models_group = QGroupBox("已配置模型") + config_models_layout = QVBoxLayout(config_models_group) + + self.config_models_list = QListWidget() + config_models_layout.addWidget(self.config_models_list) + + left_layout.addWidget(config_models_group) + + # 工具配置区域 + tools_group = QGroupBox("工具配置") + tools_layout = QVBoxLayout(tools_group) + + # 工具添加 + tool_add_layout = QHBoxLayout() + self.tool_name_edit = QLineEdit() + self.tool_name_edit.setPlaceholderText("工具名称") + self.add_tool_button = QPushButton("添加工具") + tool_add_layout.addWidget(self.tool_name_edit) + tool_add_layout.addWidget(self.add_tool_button) + tools_layout.addLayout(tool_add_layout) + + # 工具列表 + self.tools_list = QListWidget() + tools_layout.addWidget(self.tools_list) + + # 工具操作按钮 + tool_buttons_layout = QHBoxLayout() + self.edit_tool_button = QPushButton("编辑") + self.remove_tool_button = QPushButton("删除") + tool_buttons_layout.addWidget(self.edit_tool_button) + tool_buttons_layout.addWidget(self.remove_tool_button) + tools_layout.addLayout(tool_buttons_layout) + + left_layout.addWidget(tools_group) + + return left_widget + + def createRightPanel(self): + """创建右侧面板""" + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + # 步骤配置标签页 + self.tab_widget = QTabWidget() + + # 步骤列表标签页 + self.steps_tab = self.createStepsTab() + self.tab_widget.addTab(self.steps_tab, "步骤配置") + + # 全局设置标签页 + self.settings_tab = self.createSettingsTab() + self.tab_widget.addTab(self.settings_tab, "全局设置") + + right_layout.addWidget(self.tab_widget) + + return right_widget + + def createStepsTab(self): + """创建步骤配置标签页""" + steps_widget = QWidget() + steps_layout = QHBoxLayout(steps_widget) + + # 左侧步骤列表 + steps_list_layout = QVBoxLayout() + + # 步骤列表标题和操作按钮 + steps_header_layout = QHBoxLayout() + steps_label = QLabel("拆装步骤") + steps_label.setFont(QFont("", 12, QFont.Bold)) + self.add_step_button = QPushButton("添加步骤") + self.remove_step_button = QPushButton("删除步骤") + steps_header_layout.addWidget(steps_label) + steps_header_layout.addStretch() + steps_header_layout.addWidget(self.add_step_button) + steps_header_layout.addWidget(self.remove_step_button) + steps_list_layout.addLayout(steps_header_layout) + + # 步骤列表 + self.steps_list = QListWidget() + steps_list_layout.addWidget(self.steps_list) + + # 步骤顺序调整按钮 + step_order_layout = QHBoxLayout() + self.move_step_up_button = QPushButton("上移") + self.move_step_down_button = QPushButton("下移") + step_order_layout.addWidget(self.move_step_up_button) + step_order_layout.addWidget(self.move_step_down_button) + steps_list_layout.addLayout(step_order_layout) + + # 右侧步骤详细配置 + self.step_config_widget = self.createStepConfigWidget() + + # 添加到布局 + steps_layout.addLayout(steps_list_layout, 1) + steps_layout.addWidget(self.step_config_widget, 2) + + return steps_widget + + def createStepConfigWidget(self): + """创建步骤详细配置部件""" + config_widget = QWidget() + config_layout = QVBoxLayout(config_widget) + + # 滚动区域 + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + # 步骤基本信息 + basic_info_group = QGroupBox("基本信息") + basic_info_layout = QGridLayout(basic_info_group) + + # 步骤名称 + basic_info_layout.addWidget(QLabel("步骤名称:"), 0, 0) + self.step_name_edit = QLineEdit() + basic_info_layout.addWidget(self.step_name_edit, 0, 1) + + # 步骤描述 + basic_info_layout.addWidget(QLabel("步骤描述:"), 1, 0) + self.step_description_edit = QTextEdit() + self.step_description_edit.setMaximumHeight(80) + basic_info_layout.addWidget(self.step_description_edit, 1, 1) + + # 步骤类型 + basic_info_layout.addWidget(QLabel("步骤类型:"), 2, 0) + self.step_type_combo = QComboBox() + self.step_type_combo.addItems(["拆卸", "安装"]) + basic_info_layout.addWidget(self.step_type_combo, 2, 1) + + scroll_layout.addWidget(basic_info_group) + + # 目标模型配置 + target_model_group = QGroupBox("目标模型") + target_model_layout = QGridLayout(target_model_group) + + # 目标模型选择 + target_model_layout.addWidget(QLabel("目标模型:"), 0, 0) + self.target_model_combo = QComboBox() + target_model_layout.addWidget(self.target_model_combo, 0, 1) + + # 交互方式 + target_model_layout.addWidget(QLabel("交互方式:"), 1, 0) + self.interaction_type_combo = QComboBox() + self.interaction_type_combo.addItems(["鼠标拖拽", "点击触发"]) + target_model_layout.addWidget(self.interaction_type_combo, 1, 1) + + scroll_layout.addWidget(target_model_group) + + # 拖拽交互配置 + self.drag_config_group = QGroupBox("拖拽交互配置") + drag_config_layout = QGridLayout(self.drag_config_group) + + # 目标位置 + drag_config_layout.addWidget(QLabel("目标位置 X:"), 0, 0) + self.target_pos_x_spin = QDoubleSpinBox() + self.target_pos_x_spin.setRange(-1000, 1000) + self.target_pos_x_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_x_spin, 0, 1) + + drag_config_layout.addWidget(QLabel("目标位置 Y:"), 1, 0) + self.target_pos_y_spin = QDoubleSpinBox() + self.target_pos_y_spin.setRange(-1000, 1000) + self.target_pos_y_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_y_spin, 1, 1) + + drag_config_layout.addWidget(QLabel("目标位置 Z:"), 2, 0) + self.target_pos_z_spin = QDoubleSpinBox() + self.target_pos_z_spin.setRange(-1000, 1000) + self.target_pos_z_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_z_spin, 2, 1) + + # 拖拽约束 + self.constrain_x_check = QCheckBox("约束X轴") + self.constrain_y_check = QCheckBox("约束Y轴") + self.constrain_z_check = QCheckBox("约束Z轴") + + constraint_layout = QHBoxLayout() + constraint_layout.addWidget(self.constrain_x_check) + constraint_layout.addWidget(self.constrain_y_check) + constraint_layout.addWidget(self.constrain_z_check) + + drag_config_layout.addWidget(QLabel("拖拽约束:"), 3, 0) + drag_config_layout.addLayout(constraint_layout, 3, 1) + + scroll_layout.addWidget(self.drag_config_group) + + # 点击触发配置 + self.click_config_group = QGroupBox("点击触发配置") + click_config_layout = QGridLayout(self.click_config_group) + + # 位移距离 + click_config_layout.addWidget(QLabel("位移距离 X:"), 0, 0) + self.move_distance_x_spin = QDoubleSpinBox() + self.move_distance_x_spin.setRange(-100, 100) + self.move_distance_x_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_x_spin, 0, 1) + + click_config_layout.addWidget(QLabel("位移距离 Y:"), 1, 0) + self.move_distance_y_spin = QDoubleSpinBox() + self.move_distance_y_spin.setRange(-100, 100) + self.move_distance_y_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_y_spin, 1, 1) + + click_config_layout.addWidget(QLabel("位移距离 Z:"), 2, 0) + self.move_distance_z_spin = QDoubleSpinBox() + self.move_distance_z_spin.setRange(-100, 100) + self.move_distance_z_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_z_spin, 2, 1) + + # 位移时间 + click_config_layout.addWidget(QLabel("位移时间(秒):"), 3, 0) + self.move_duration_spin = QDoubleSpinBox() + self.move_duration_spin.setRange(0.1, 10.0) + self.move_duration_spin.setSingleStep(0.1) + self.move_duration_spin.setValue(1.0) + click_config_layout.addWidget(self.move_duration_spin, 3, 1) + + # 完成后隐藏 + self.hide_after_move_check = QCheckBox("位移完成后隐藏模型") + click_config_layout.addWidget(self.hide_after_move_check, 4, 0, 1, 2) + + scroll_layout.addWidget(self.click_config_group) + + # 提示音频配置 + audio_config_group = QGroupBox("步骤提示") + audio_config_layout = QGridLayout(audio_config_group) + + # 提示音频文件 + audio_config_layout.addWidget(QLabel("提示音频:"), 0, 0) + audio_file_layout = QHBoxLayout() + self.audio_file_edit = QLineEdit() + self.browse_audio_button = QPushButton("浏览...") + audio_file_layout.addWidget(self.audio_file_edit) + audio_file_layout.addWidget(self.browse_audio_button) + audio_config_layout.addLayout(audio_file_layout, 0, 1) + + # 提示文字 + audio_config_layout.addWidget(QLabel("提示文字:"), 1, 0) + self.hint_text_edit = QTextEdit() + self.hint_text_edit.setMaximumHeight(60) + audio_config_layout.addWidget(self.hint_text_edit, 1, 1) + + scroll_layout.addWidget(audio_config_group) + + # 使用工具配置 + tool_config_group = QGroupBox("使用工具") + tool_config_layout = QGridLayout(tool_config_group) + + tool_config_layout.addWidget(QLabel("所需工具:"), 0, 0) + self.required_tool_combo = QComboBox() + self.required_tool_combo.addItem("无") + tool_config_layout.addWidget(self.required_tool_combo, 0, 1) + + scroll_layout.addWidget(tool_config_group) + + # 设置滚动区域 + scroll_area.setWidget(scroll_content) + config_layout.addWidget(scroll_area) + + # 默认隐藏点击配置 + self.click_config_group.setVisible(False) + + return config_widget + + def createSettingsTab(self): + """创建全局设置标签页""" + settings_widget = QWidget() + settings_layout = QVBoxLayout(settings_widget) + + # 全局设置组 + global_settings_group = QGroupBox("全局设置") + global_settings_layout = QGridLayout(global_settings_group) + + # 自动播放提示音 + self.auto_play_audio_check = QCheckBox("自动播放步骤提示音") + self.auto_play_audio_check.setChecked(True) + global_settings_layout.addWidget(self.auto_play_audio_check, 0, 0, 1, 2) + + # 显示步骤提示文字 + self.show_hint_text_check = QCheckBox("显示步骤提示文字") + self.show_hint_text_check.setChecked(True) + global_settings_layout.addWidget(self.show_hint_text_check, 1, 0, 1, 2) + + # 步骤完成确认 + self.require_confirmation_check = QCheckBox("步骤完成需要确认") + global_settings_layout.addWidget(self.require_confirmation_check, 2, 0, 1, 2) + + # 拖拽灵敏度 + global_settings_layout.addWidget(QLabel("拖拽灵敏度:"), 3, 0) + self.drag_sensitivity_slider = QSlider(Qt.Horizontal) + self.drag_sensitivity_slider.setRange(1, 10) + self.drag_sensitivity_slider.setValue(5) + global_settings_layout.addWidget(self.drag_sensitivity_slider, 3, 1) + + # 动画速度 + global_settings_layout.addWidget(QLabel("动画速度:"), 4, 0) + self.animation_speed_slider = QSlider(Qt.Horizontal) + self.animation_speed_slider.setRange(1, 10) + self.animation_speed_slider.setValue(5) + global_settings_layout.addWidget(self.animation_speed_slider, 4, 1) + + settings_layout.addWidget(global_settings_group) + + # 预设配置组 + presets_group = QGroupBox("预设配置") + presets_layout = QVBoxLayout(presets_group) + + # 预设列表 + self.presets_list = QListWidget() + presets_layout.addWidget(self.presets_list) + + # 预设操作按钮 + preset_buttons_layout = QHBoxLayout() + self.save_preset_button = QPushButton("保存为预设") + self.load_preset_button = QPushButton("加载预设") + self.delete_preset_button = QPushButton("删除预设") + preset_buttons_layout.addWidget(self.save_preset_button) + preset_buttons_layout.addWidget(self.load_preset_button) + preset_buttons_layout.addWidget(self.delete_preset_button) + presets_layout.addLayout(preset_buttons_layout) + + settings_layout.addWidget(presets_group) + + # 添加弹性空间 + settings_layout.addStretch() + + return settings_widget + + def connectSignals(self): + """连接信号槽""" + # 模型搜索 + self.model_search_edit.textChanged.connect(self.filterSceneModels) + + # 模型操作 + self.add_to_config_button.clicked.connect(self.addModelsToConfig) + self.remove_from_config_button.clicked.connect(self.removeModelsFromConfig) + + # 工具操作 + self.add_tool_button.clicked.connect(self.addTool) + self.edit_tool_button.clicked.connect(self.editTool) + self.remove_tool_button.clicked.connect(self.removeTool) + + # 步骤操作 + self.add_step_button.clicked.connect(self.addStep) + self.remove_step_button.clicked.connect(self.removeStep) + self.move_step_up_button.clicked.connect(self.moveStepUp) + self.move_step_down_button.clicked.connect(self.moveStepDown) + self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged) + + # 步骤配置变化 + self.interaction_type_combo.currentTextChanged.connect(self.onInteractionTypeChanged) + self.browse_audio_button.clicked.connect(self.browseAudioFile) + + # 步骤配置实时更新 + self.step_name_edit.textChanged.connect(self.updateCurrentStep) + self.step_description_edit.textChanged.connect(self.updateCurrentStep) + self.step_type_combo.currentTextChanged.connect(self.updateCurrentStep) + self.target_model_combo.currentTextChanged.connect(self.updateCurrentStep) + + # 按钮操作 + 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_list.clear() + + # 遍历场景中的所有节点 + self._addNodeToTree(self.world.render, self.scene_models_list) + + # 展开根节点 + self.scene_models_list.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, text): + """过滤场景模型列表""" + # 简单的文本过滤实现 + def filterItems(item): + item_text = item.text(0).lower() + text_lower = text.lower() + + # 检查当前项是否匹配 + matches = text_lower in item_text + + # 检查子项 + 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_list.topLevelItemCount()): + filterItems(self.scene_models_list.topLevelItem(i)) + + def addModelsToConfig(self): + """添加选中的模型到配置""" + selected_items = self.scene_models_list.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 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.required_tool_combo.addItem(tool_name) + + # 添加到配置数据 + self.config_data['tools'].append({ + 'name': tool_name, + 'description': '', + 'icon': '' + }) + + # 清空输入框 + 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 removeTool(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) + + # 从下拉框中移除 + index = self.required_tool_combo.findText(tool_name) + if index >= 0: + self.required_tool_combo.removeItem(index) + + # 从配置数据中移除 + self.config_data['tools'] = [ + tool for tool in self.config_data['tools'] + if tool['name'] != tool_name + ] + + 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], + 'constraints': {'x': False, 'y': False, 'z': False}, + 'move_distance': [0, 0, 0], + 'move_duration': 1.0, + 'hide_after_move': False, + 'audio_file': '', + 'hint_text': '', + 'required_tool': '无' + } + + # 添加到配置数据 + self.config_data['steps'].append(step_data) + + # 添加到步骤列表 + list_item = QListWidgetItem(step_name) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 选中新添加的步骤 + self.steps_list.setCurrentItem(list_item) + + 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) + + # 重新编号步骤 + 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 + item.setText(new_name) + + def onStepSelectionChanged(self): + """步骤选择改变""" + current_item = self.steps_list.currentItem() + if not current_item: + return + + step_data = current_item.data(Qt.UserRole) + if not step_data: + return + + # 更新步骤配置界面 + self.loadStepConfiguration(step_data) + + def loadStepConfiguration(self, step_data): + """加载步骤配置到界面""" + # 基本信息 + self.step_name_edit.setText(step_data.get('name', '')) + self.step_description_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) + + # 交互方式 + interaction_type = step_data.get('interaction_type', '鼠标拖拽') + self.interaction_type_combo.setCurrentText(interaction_type) + self.onInteractionTypeChanged(interaction_type) + + # 拖拽配置 + target_pos = step_data.get('target_position', [0, 0, 0]) + self.target_pos_x_spin.setValue(target_pos[0]) + self.target_pos_y_spin.setValue(target_pos[1]) + self.target_pos_z_spin.setValue(target_pos[2]) + + constraints = step_data.get('constraints', {'x': False, 'y': False, 'z': False}) + self.constrain_x_check.setChecked(constraints.get('x', False)) + self.constrain_y_check.setChecked(constraints.get('y', False)) + self.constrain_z_check.setChecked(constraints.get('z', False)) + + # 点击触发配置 + move_distance = step_data.get('move_distance', [0, 0, 0]) + self.move_distance_x_spin.setValue(move_distance[0]) + self.move_distance_y_spin.setValue(move_distance[1]) + self.move_distance_z_spin.setValue(move_distance[2]) + + self.move_duration_spin.setValue(step_data.get('move_duration', 1.0)) + self.hide_after_move_check.setChecked(step_data.get('hide_after_move', False)) + + # 提示配置 + self.audio_file_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) + + def onInteractionTypeChanged(self, interaction_type): + """交互方式改变""" + if interaction_type == "鼠标拖拽": + self.drag_config_group.setVisible(True) + self.click_config_group.setVisible(False) + else: # 点击触发 + self.drag_config_group.setVisible(False) + self.click_config_group.setVisible(True) + + def updateCurrentStep(self): + """更新当前步骤数据""" + current_item = self.steps_list.currentItem() + if not current_item: + return + + step_data = current_item.data(Qt.UserRole) + if not step_data: + return + + # 更新步骤数据 + step_data['name'] = self.step_name_edit.text() + step_data['description'] = self.step_description_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_type_combo.currentText() + + # 更新列表项显示 + current_item.setText(step_data['name']) + + def browseAudioFile(self): + """浏览音频文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, "选择音频文件", "", + "音频文件 (*.wav *.mp3 *.ogg *.flac);;所有文件 (*)" + ) + + if file_path: + self.audio_file_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']) + } for model in self.config_data['models'] + ], + 'steps': self.config_data['steps'], + 'tools': self.config_data['tools'], + 'settings': { + 'auto_play_audio': self.auto_play_audio_check.isChecked(), + 'show_hint_text': self.show_hint_text_check.isChecked(), + 'require_confirmation': self.require_confirmation_check.isChecked(), + 'drag_sensitivity': self.drag_sensitivity_slider.value(), + 'animation_speed': self.animation_speed_slider.value() + } + } + + 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['steps'] = data.get('steps', []) + for step_data in self.config_data['steps']: + list_item = QListWidgetItem(step_data['name']) + 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_play_audio_check.setChecked(settings.get('auto_play_audio', True)) + self.show_hint_text_check.setChecked(settings.get('show_hint_text', True)) + self.require_confirmation_check.setChecked(settings.get('require_confirmation', False)) + self.drag_sensitivity_slider.setValue(settings.get('drag_sensitivity', 5)) + self.animation_speed_slider.setValue(settings.get('animation_speed', 5)) + + def previewConfiguration(self): + """预览配置效果""" + QMessageBox.information(self, "提示", "预览功能待实现") + + def applyConfiguration(self): + """应用配置""" + QMessageBox.information(self, "提示", "应用配置功能待实现") \ No newline at end of file diff --git a/ui/assembly_disassembly_config_simple.py b/ui/assembly_disassembly_config_simple.py new file mode 100644 index 00000000..9397f650 --- /dev/null +++ b/ui/assembly_disassembly_config_simple.py @@ -0,0 +1,1069 @@ +# -*- 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, "提示", "应用配置功能待实现") \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index a13fb789..6d61980a 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -10,13 +10,13 @@ import os import sys from PyQt5.QtGui import QKeySequence, QIcon, QPalette, QColor -from PyQt5.QtWebEngineWidgets import QWebEngineView +# from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout, QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog, - QSpinBox, QFrame) + QSpinBox, QFrame, QRadioButton, QTextEdit) from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import OrthographicLens @@ -713,6 +713,17 @@ class MainWindow(QMainWindow): self.scriptMenu.addSeparator() self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器') + # 交互菜单 + self.interactionMenu = menubar.addMenu('交互') + self.assemblyDisassemblyConfigAction = self.interactionMenu.addAction('拆装配置') + self.assemblyDisassemblyConfigAction.triggered.connect(self.onOpenAssemblyDisassemblyConfig) + self.interactionMenu.addSeparator() + self.startAssemblyInteractionAction = self.interactionMenu.addAction('开始拆装交互') + self.startAssemblyInteractionAction.triggered.connect(self.onStartAssemblyInteraction) + self.interactionMenu.addSeparator() + self.maintenanceSystemAction = self.interactionMenu.addAction('🔧 维修系统') + self.maintenanceSystemAction.triggered.connect(self.onOpenMaintenanceSystem) + self.cesiumMenu = menubar.addMenu('Cesium') self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset) @@ -2540,7 +2551,7 @@ class MainWindow(QMainWindow): if not WEB_ENGINE_AVAILABLE: return None try: - from PyQt5.QtWebEngineWidgets import QWebEngineView + # from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWidgets import QDockWidget from PyQt5.QtCore import QUrl import os @@ -3239,6 +3250,16 @@ class MainWindow(QMainWindow): try: print("🔄 正在关闭应用程序...") + # 关闭拆装交互相关的弹窗 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + print("🧹 关闭拆装交互弹窗...") + if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog: + self.world.assembly_interaction.step_dialog.close() + self.world.assembly_interaction.step_dialog = None + # 停止交互模式 + if self.world.assembly_interaction.is_active: + self.world.assembly_interaction.stop_interaction_mode() + # 清理工具管理器中的进程 if hasattr(self.world, 'tool_manager') and self.world.tool_manager: print("🧹 清理工具管理器进程...") @@ -3444,6 +3465,224 @@ class MainWindow(QMainWindow): else: QMessageBox.warning(self, "错误", "高度图地形创建失败!") + def onOpenAssemblyDisassemblyConfig(self): + """打开拆装配置界面""" + try: + from ui.assembly_disassembly_config_simple import AssemblyDisassemblyConfigDialog + config_dialog = AssemblyDisassemblyConfigDialog(self, self.world) + config_dialog.show() + except Exception as e: + QMessageBox.critical(self, "错误", f"打开拆装配置界面失败: {str(e)}") + import traceback + traceback.print_exc() + + def onStartAssemblyInteraction(self): + """开始拆装交互""" + try: + # 显示模式选择对话框 + mode_dialog = AssemblyModeSelectionDialog(self) + if mode_dialog.exec_() != QDialog.Accepted: + return + + selected_mode = mode_dialog.get_selected_mode() + print(f"🎯 用户选择的拆装模式: {selected_mode}") + + from core.assembly_interaction import AssemblyInteractionManager + + # 检查是否已有交互管理器实例 + if not hasattr(self.world, 'assembly_interaction'): + self.world.assembly_interaction = AssemblyInteractionManager(self.world) + + # 启动交互模式,传递模式参数 + self.world.assembly_interaction.start_interaction_mode(mode=selected_mode) + + except Exception as e: + QMessageBox.critical(self, "错误", f"启动拆装交互失败: {str(e)}") + import traceback + traceback.print_exc() + + def onOpenMaintenanceSystem(self): + """打开维修系统""" + try: + # 导入简化的登录界面 + from ui.simple_maintenance_login import SimpleMaintenanceLoginDialog + from ui.maintenance_system import MaintenanceSubjectDialog, MaintenanceSystemManager + + print("🔧 启动维修系统...") + + # 显示登录界面 + login_dialog = SimpleMaintenanceLoginDialog(self) + + if login_dialog.exec_() == QDialog.Accepted: + print("✅ 登录成功,显示科目选择界面") + + # 获取当前项目路径 + project_path = None + if hasattr(self.world, 'project_manager') and self.world.project_manager: + project_path = self.world.project_manager.getCurrentProjectPath() + + # 显示科目选择界面 + subject_dialog = MaintenanceSubjectDialog(project_path, self) + + def on_subject_selected(subject_path, mode): + """处理科目选择""" + try: + print(f"🎯 启动维修科目: {subject_path}") + print(f"📝 模式: {mode}") + + # 加载科目配置 + import json + with open(subject_path, 'r', encoding='utf-8') as f: + subject_config = json.load(f) + + # 初始化拆装交互系统(如果还没有) + from core.assembly_interaction import AssemblyInteractionManager + + if not hasattr(self.world, 'assembly_interaction') or not self.world.assembly_interaction: + print("🔧 初始化拆装交互系统...") + self.world.assembly_interaction = AssemblyInteractionManager(self.world) + + # 设置配置并启动交互模式 + self.world.assembly_interaction.config_data = subject_config + + # 启动交互模式 + success = self.world.assembly_interaction.start_interaction_mode(mode=mode) + + if success: + print(f"✅ 维修科目启动成功") + else: + print("❌ 维修科目启动失败") + QMessageBox.warning(self, "错误", "维修科目启动失败") + + except Exception as e: + print(f"❌ 启动维修科目失败: {e}") + QMessageBox.critical(self, "错误", f"启动维修科目失败:\n{str(e)}") + import traceback + traceback.print_exc() + + subject_dialog.subject_selected.connect(on_subject_selected) + subject_dialog.exec_() + + else: + print("ℹ️ 用户取消了登录") + + except Exception as e: + print(f"❌ 打开维修系统失败: {e}") + QMessageBox.critical(self, "错误", f"打开维修系统失败:\n{str(e)}") + import traceback + traceback.print_exc() + +class AssemblyModeSelectionDialog(QDialog): + """拆装模式选择对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.selected_mode = "training" # 默认选择训练模式 + self.setupUI() + + def setupUI(self): + self.setWindowTitle("选择拆装模式") + self.setFixedSize(400, 300) + self.setModal(True) + + layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel("请选择拆装交互模式") + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #2E86C1; margin: 10px;") + title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(title_label) + + # 模式选择组 + mode_group = QGroupBox("模式选择") + mode_layout = QVBoxLayout(mode_group) + + # 训练模式 + self.training_radio = QRadioButton("训练模式") + self.training_radio.setChecked(True) # 默认选中 + self.training_radio.setStyleSheet("font-size: 14px; margin: 5px;") + mode_layout.addWidget(self.training_radio) + + training_desc = QTextEdit() + training_desc.setMaximumHeight(60) + training_desc.setReadOnly(True) + training_desc.setPlainText("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导") + training_desc.setStyleSheet("background-color: #f0f8ff; border: 1px solid #ccc; margin-left: 20px;") + mode_layout.addWidget(training_desc) + + # 考核模式 + self.exam_radio = QRadioButton("考核模式") + self.exam_radio.setStyleSheet("font-size: 14px; margin: 5px;") + mode_layout.addWidget(self.exam_radio) + + exam_desc = QTextEdit() + exam_desc.setMaximumHeight(60) + exam_desc.setReadOnly(True) + exam_desc.setPlainText("• 不显示步骤描述\n• 工具选择错误时不提示,直接扣分\n• 不播放语音指导") + exam_desc.setStyleSheet("background-color: #fff5f5; border: 1px solid #ccc; margin-left: 20px;") + mode_layout.addWidget(exam_desc) + + layout.addWidget(mode_group) + + # 按钮 + button_layout = QHBoxLayout() + + self.ok_button = QPushButton("开始") + self.ok_button.setStyleSheet(""" + QPushButton { + background-color: #27AE60; + color: white; + font-size: 14px; + font-weight: bold; + padding: 8px 20px; + border: none; + border-radius: 4px; + } + QPushButton:hover { + background-color: #2ECC71; + } + """) + self.ok_button.clicked.connect(self.accept) + + self.cancel_button = QPushButton("取消") + self.cancel_button.setStyleSheet(""" + QPushButton { + background-color: #95A5A6; + color: white; + font-size: 14px; + padding: 8px 20px; + border: none; + border-radius: 4px; + } + QPushButton:hover { + background-color: #BDC3C7; + } + """) + self.cancel_button.clicked.connect(self.reject) + + button_layout.addStretch() + button_layout.addWidget(self.ok_button) + button_layout.addWidget(self.cancel_button) + + layout.addLayout(button_layout) + + # 连接单选按钮信号 + self.training_radio.toggled.connect(self.on_mode_changed) + self.exam_radio.toggled.connect(self.on_mode_changed) + + def on_mode_changed(self): + """模式改变时的处理""" + if self.training_radio.isChecked(): + self.selected_mode = "training" + elif self.exam_radio.isChecked(): + self.selected_mode = "exam" + print(f"🔄 模式选择改变: {self.selected_mode}") + + def get_selected_mode(self): + """获取选中的模式""" + return self.selected_mode + + # ==================== VR事件处理 ==================== def onEnterVR(self): diff --git a/ui/maintenance_system.py b/ui/maintenance_system.py new file mode 100644 index 00000000..77d609ea --- /dev/null +++ b/ui/maintenance_system.py @@ -0,0 +1,658 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +维修系统界面模块 +包含登录界面、科目列表、模式选择等功能 +""" + +import os +import json +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, + QLineEdit, QPushButton, QLabel, QListWidget, QListWidgetItem, + QMessageBox, QRadioButton, QTextEdit, QSplitter +) +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QFont, QPixmap + + +class MaintenanceLoginDialog(QDialog): + """维修系统登录对话框""" + + login_success = pyqtSignal() # 登录成功信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUI() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 登录") + self.setFixedSize(400, 300) + self.setModal(True) + + # 应用简化样式 - 确保可见性和可用性 + self.setStyleSheet(""" + QDialog { + background-color: #f5f5f5; + color: #333333; + } + QLabel { + color: #333333; + font-size: 14px; + } + QLineEdit { + background-color: white; + color: #333333; + border: 2px solid #cccccc; + border-radius: 4px; + padding: 8px; + font-size: 14px; + min-height: 20px; + } + QLineEdit:focus { + border-color: #0078d4; + } + QPushButton { + background-color: #0078d4; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + min-height: 30px; + } + QPushButton:hover { + background-color: #106ebe; + } + QPushButton:pressed { + background-color: #005a9e; + } + QGroupBox { + font-size: 16px; + font-weight: bold; + color: #0078d4; + border: 2px solid #cccccc; + border-radius: 4px; + margin-top: 10px; + padding-top: 10px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + """) + + layout = QVBoxLayout(self) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + # 标题 + title_label = QLabel("🔧 维修系统") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 20px; + font-weight: bold; + color: #0078d4; + margin: 10px 0px; + """) + layout.addWidget(title_label) + + # 登录表单 + login_group = QGroupBox("身份验证") + login_layout = QFormLayout(login_group) + login_layout.setSpacing(10) + login_layout.setContentsMargins(15, 15, 15, 15) + + # 用户名 + self.username_edit = QLineEdit() + self.username_edit.setPlaceholderText("请输入用户名") + self.username_edit.setText("") # 确保清空 + login_layout.addRow("用户名:", self.username_edit) + + # 密码 + self.password_edit = QLineEdit() + self.password_edit.setEchoMode(QLineEdit.Password) + self.password_edit.setPlaceholderText("请输入密码") + self.password_edit.setText("") # 确保清空 + login_layout.addRow("密码:", self.password_edit) + + layout.addWidget(login_group) + + # 提示信息 + hint_label = QLabel("💡 默认账号密码均为: admin") + hint_label.setAlignment(Qt.AlignCenter) + hint_label.setStyleSheet(""" + color: #666666; + font-size: 12px; + margin: 5px 0px; + """) + layout.addWidget(hint_label) + + # 按钮 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + self.cancel_btn = QPushButton("取消") + self.login_btn = QPushButton("登录") + + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.login_btn) + + layout.addLayout(button_layout) + + # 连接信号 + self.login_btn.clicked.connect(self.on_login) + self.cancel_btn.clicked.connect(self.reject) + self.password_edit.returnPressed.connect(self.on_login) + + # 设置默认焦点和tab顺序 + self.username_edit.setFocus() + self.setTabOrder(self.username_edit, self.password_edit) + self.setTabOrder(self.password_edit, self.login_btn) + self.setTabOrder(self.login_btn, self.cancel_btn) + + print("🔧 登录界面创建完成") + print(f"📝 用户名输入框: {self.username_edit}") + print(f"📝 密码输入框: {self.password_edit}") + print(f"📝 登录按钮: {self.login_btn}") + print(f"📝 取消按钮: {self.cancel_btn}") + + def on_login(self): + """处理登录""" + username = self.username_edit.text().strip() + password = self.password_edit.text().strip() + + # 验证账号密码 + if username == "admin" and password == "admin": + print("✅ 维修系统登录成功") + self.login_success.emit() + self.accept() + else: + QMessageBox.warning(self, "登录失败", + "用户名或密码错误!\n请使用: admin / admin") + self.password_edit.clear() + self.password_edit.setFocus() + + +class MaintenanceSubjectDialog(QDialog): + """维修系统科目选择对话框""" + + subject_selected = pyqtSignal(str, str) # 科目路径, 模式 + + def __init__(self, project_path, parent=None): + super().__init__(parent) + self.project_path = project_path + self.subjects_path = os.path.join(project_path, "Subjects") if project_path else None + self.current_subject_path = None + self.setupUI() + self.load_subjects() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 科目选择") + self.setFixedSize(800, 600) + self.setModal(True) + + # 应用样式 + self.setStyleSheet(""" + QDialog { + background-color: #1e1e2e; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + } + QListWidget { + background-color: #2d2d44; + color: #e0e0ff; + border: 2px solid #3a3a4a; + border-radius: 8px; + padding: 5px; + font-size: 14px; + } + QListWidget::item { + padding: 8px; + border-radius: 4px; + margin: 2px; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: #8b5cf6; + color: white; + } + QTextEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 2px solid #3a3a4a; + border-radius: 8px; + padding: 8px; + font-size: 12px; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QGroupBox { + font-size: 14px; + font-weight: bold; + color: #8b5cf6; + border: 2px solid #3a3a4a; + border-radius: 8px; + margin: 10px 0px; + padding-top: 15px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + QRadioButton { + color: #e0e0ff; + font-size: 14px; + spacing: 8px; + } + QRadioButton::indicator { + width: 16px; + height: 16px; + } + QRadioButton::indicator:unchecked { + border: 2px solid #3a3a4a; + border-radius: 8px; + background-color: #2d2d44; + } + QRadioButton::indicator:checked { + border: 2px solid #8b5cf6; + border-radius: 8px; + background-color: #8b5cf6; + } + """) + + layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel("📚 选择维修科目") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 20px; + font-weight: bold; + color: #8b5cf6; + margin: 15px 0px; + """) + layout.addWidget(title_label) + + # 主要内容区域 + main_splitter = QSplitter(Qt.Horizontal) + + # 左侧:科目列表 + left_widget = QGroupBox("科目列表") + left_layout = QVBoxLayout(left_widget) + + self.subject_list = QListWidget() + self.subject_list.itemClicked.connect(self.on_subject_selected) + left_layout.addWidget(self.subject_list) + + main_splitter.addWidget(left_widget) + + # 右侧:科目详情和模式选择 + right_widget = QGroupBox("科目详情") + right_layout = QVBoxLayout(right_widget) + + # 科目描述 + self.subject_description = QTextEdit() + self.subject_description.setReadOnly(True) + self.subject_description.setMaximumHeight(150) + self.subject_description.setPlainText("请选择一个科目查看详情...") + right_layout.addWidget(self.subject_description) + + # 模式选择 + mode_group = QGroupBox("选择模式") + mode_layout = QVBoxLayout(mode_group) + + self.training_radio = QRadioButton("🎓 训练模式") + self.training_radio.setChecked(True) + mode_layout.addWidget(self.training_radio) + + training_desc = QLabel("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导") + training_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") + mode_layout.addWidget(training_desc) + + self.exam_radio = QRadioButton("📋 考核模式") + mode_layout.addWidget(self.exam_radio) + + exam_desc = QLabel("• 不显示步骤描述\n• 不提供工具选择提示\n• 无语音指导,独立完成操作") + exam_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") + mode_layout.addWidget(exam_desc) + + right_layout.addWidget(mode_group) + + main_splitter.addWidget(right_widget) + + # 设置分割器比例 + main_splitter.setSizes([300, 500]) + layout.addWidget(main_splitter) + + # 底部按钮 + button_layout = QHBoxLayout() + + self.refresh_btn = QPushButton("🔄 刷新") + self.cancel_btn = QPushButton("取消") + self.start_btn = QPushButton("开始") + self.start_btn.setEnabled(False) + + button_layout.addWidget(self.refresh_btn) + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.start_btn) + + layout.addLayout(button_layout) + + # 连接信号 + self.refresh_btn.clicked.connect(self.load_subjects) + self.cancel_btn.clicked.connect(self.reject) + self.start_btn.clicked.connect(self.on_start) + + def load_subjects(self): + """加载科目列表""" + self.subject_list.clear() + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("请选择一个科目查看详情...") + + if not self.subjects_path or not os.path.exists(self.subjects_path): + # 添加提示项 + item = QListWidgetItem("📁 未找到Subjects目录") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + + if not self.project_path: + self.subject_description.setPlainText( + "❌ 错误:没有打开的项目\n\n" + "请先通过 文件 -> 打开 菜单打开一个项目,\n" + "然后再使用维修系统功能。" + ) + else: + self.subject_description.setPlainText( + f"❌ 错误:找不到Subjects目录\n\n" + f"项目路径: {self.project_path}\n" + f"期望目录: {self.subjects_path}\n\n" + "请在项目根目录下创建Subjects文件夹,\n" + "并在其中放置科目的JSON配置文件。" + ) + return + + # 扫描JSON文件 + json_files = [] + try: + for file in os.listdir(self.subjects_path): + if file.endswith('.json'): + json_path = os.path.join(self.subjects_path, file) + json_files.append((file, json_path)) + except Exception as e: + print(f"❌ 扫描Subjects目录失败: {e}") + item = QListWidgetItem("❌ 读取目录失败") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + return + + if not json_files: + item = QListWidgetItem("📄 目录中没有JSON文件") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + self.subject_description.setPlainText( + f"ℹ️ 提示:Subjects目录为空\n\n" + f"目录路径: {self.subjects_path}\n\n" + "请在此目录中放置维修科目的JSON配置文件。" + ) + return + + # 加载科目 + loaded_count = 0 + for filename, filepath in json_files: + try: + with open(filepath, 'r', encoding='utf-8') as f: + subject_data = json.load(f) + + # 获取科目名称 + subject_name = subject_data.get('name', filename.replace('.json', '')) + + # 创建列表项 + item = QListWidgetItem(f"📋 {subject_name}") + item.setData(Qt.UserRole, { + 'path': filepath, + 'data': subject_data, + 'filename': filename + }) + self.subject_list.addItem(item) + loaded_count += 1 + + except Exception as e: + print(f"❌ 加载科目文件失败 {filename}: {e}") + item = QListWidgetItem(f"❌ {filename} (加载失败)") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + + print(f"✅ 成功加载 {loaded_count} 个维修科目") + + def on_subject_selected(self, item): + """处理科目选择""" + try: + print(f"📋 科目选择事件触发,item: {item}") + + if not item: + print("❌ item为空") + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("请选择一个有效的科目。") + return + + subject_info = item.data(Qt.UserRole) + print(f"📋 科目信息: {subject_info}") + + if not subject_info: + print("❌ 科目信息为空") + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("此科目无法加载,请检查文件格式。") + return + + self.current_subject_path = subject_info['path'] + self.start_btn.setEnabled(True) + + print(f"✅ 选择科目: {subject_info.get('filename', 'unknown')}") + + # 显示科目详情 + subject_data = subject_info['data'] + description = self.format_subject_description(subject_data, subject_info['filename']) + self.subject_description.setPlainText(description) + + print("✅ 科目详情显示完成") + + except Exception as e: + print(f"❌ 科目选择处理失败: {e}") + import traceback + traceback.print_exc() + + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText(f"处理科目信息时出错:\n{str(e)}") + + # 显示错误对话框 + QMessageBox.critical(self, "错误", f"处理科目选择时出错:\n{str(e)}") + + def format_subject_description(self, subject_data, filename): + """格式化科目描述""" + try: + print(f"🔧 格式化科目描述: {filename}") + + if not subject_data: + return "❌ 科目数据为空" + + lines = [] + + # 基本信息 + name = subject_data.get('name', '未知科目') + lines.append(f"📋 科目: {name}") + lines.append(f"📁 文件: {filename}") + lines.append("") + + # 描述信息 + if 'description' in subject_data and subject_data['description']: + lines.append(f"📝 描述: {subject_data['description']}") + lines.append("") + + # 步骤信息 + steps = subject_data.get('steps', []) + if steps and isinstance(steps, list): + lines.append(f"📊 步骤数量: {len(steps)}") + + # 计算总分 + try: + total_score = 0 + for step in steps: + if isinstance(step, dict): + score = step.get('score', 0) + if isinstance(score, (int, float)): + total_score += score + + if total_score > 0: + lines.append(f"💯 总分: {total_score} 分") + except Exception as e: + print(f"⚠️ 计算总分时出错: {e}") + + lines.append("") + lines.append("📋 步骤列表:") + + for i, step in enumerate(steps, 1): + try: + if isinstance(step, dict): + step_name = step.get('name', f'步骤{i}') + step_score = step.get('score', 0) + + if isinstance(step_score, (int, float)) and step_score > 0: + lines.append(f" {i}. {step_name} ({step_score}分)") + else: + lines.append(f" {i}. {step_name}") + else: + lines.append(f" {i}. 步骤{i} (格式错误)") + except Exception as e: + print(f"⚠️ 处理步骤{i}时出错: {e}") + lines.append(f" {i}. 步骤{i} (处理错误)") + else: + lines.append("⚠️ 此科目没有配置步骤") + + # 工具信息 + tools = subject_data.get('tools', []) + if tools and isinstance(tools, list): + try: + valid_tools = [str(tool) for tool in tools if tool] + if valid_tools: + lines.append("") + lines.append(f"🔧 可用工具: {', '.join(valid_tools)}") + except Exception as e: + print(f"⚠️ 处理工具列表时出错: {e}") + lines.append("") + lines.append("🔧 可用工具: (处理错误)") + + result = '\n'.join(lines) + print(f"✅ 科目描述格式化完成,长度: {len(result)}") + return result + + except Exception as e: + print(f"❌ 格式化科目描述失败: {e}") + import traceback + traceback.print_exc() + + return f"❌ 格式化科目描述时出错:\n{str(e)}\n\n文件: {filename}" + + def on_start(self): + """开始科目""" + if not self.current_subject_path: + QMessageBox.warning(self, "错误", "请先选择一个科目!") + return + + # 获取选择的模式 + mode = "training" if self.training_radio.isChecked() else "exam" + + print(f"🚀 开始维修科目: {self.current_subject_path}, 模式: {mode}") + + # 发射信号 + self.subject_selected.emit(self.current_subject_path, mode) + self.accept() + + +class MaintenanceSystemManager: + """维修系统管理器""" + + def __init__(self, world): + self.world = world + self.login_dialog = None + self.subject_dialog = None + + def show_maintenance_system(self, parent_window): + """显示维修系统""" + # 创建登录对话框 + self.login_dialog = MaintenanceLoginDialog(parent_window) + self.login_dialog.login_success.connect( + lambda: self.show_subject_selection(parent_window) + ) + self.login_dialog.exec_() + + def show_subject_selection(self, parent_window): + """显示科目选择界面""" + # 获取当前项目路径 + project_path = None + if hasattr(self.world, 'project_manager') and self.world.project_manager: + project_path = self.world.project_manager.getCurrentProjectPath() + + # 创建科目选择对话框 + self.subject_dialog = MaintenanceSubjectDialog(project_path, parent_window) + self.subject_dialog.subject_selected.connect(self.start_maintenance_subject) + self.subject_dialog.exec_() + + def start_maintenance_subject(self, subject_path, mode): + """开始维修科目""" + try: + print(f"🎯 启动维修科目: {subject_path}") + print(f"📝 模式: {mode}") + + # 加载科目配置 + with open(subject_path, 'r', encoding='utf-8') as f: + subject_config = json.load(f) + + # 启动拆装交互系统 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + # 设置配置 + self.world.assembly_interaction.config_data = subject_config + + # 启动交互模式 + self.world.assembly_interaction.start_interaction_mode(mode=mode) + + print(f"✅ 维修科目启动成功") + else: + print("❌ 错误:拆装交互系统未初始化") + + except Exception as e: + print(f"❌ 启动维修科目失败: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/ui/property_panel.py b/ui/property_panel.py index 1be6a178..39c81d3b 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -4806,12 +4806,12 @@ class PropertyPanelManager: if geom_node: geom_node_name = geom_node.getName() unique_name = f"{geom_node_name}({model_name})" - #print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") + print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") else: material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" unique_name = f"{material_name}({model_name})" - #print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") + print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") # 处理重复名称 if unique_name in name_counter: @@ -4850,7 +4850,7 @@ class PropertyPanelManager: # 基础颜色编辑 base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: - #print(f"材质基础颜色: {base_color}") + print(f"材质基础颜色: {base_color}") # 基础颜色标题 color_row = 2 if material_status != "标准PBR材质" else 1 @@ -5236,7 +5236,7 @@ class PropertyPanelManager: try: # 方法1: 尝试获取base_color属性 if hasattr(material, 'base_color') and material.base_color is not None: - #print(f"✓ 找到base_color属性: {material.base_color}") + print(f"✓ 找到base_color属性: {material.base_color}") return material.base_color # 方法2: 尝试调用get_base_color方法 @@ -5254,7 +5254,7 @@ class PropertyPanelManager: try: diffuse_color = material.getDiffuse() if diffuse_color is not None: - #print(f"✓ 从diffuse颜色获取: {diffuse_color}") + print(f"✓ 从diffuse颜色获取: {diffuse_color}") # 同时设置为base_color if hasattr(material, 'set_base_color'): material.set_base_color(diffuse_color) @@ -6862,7 +6862,7 @@ class PropertyPanelManager: # print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np - #print("未找到匹配的几何节点") + print("未找到匹配的几何节点") return None def _findSpecificGeomNodeForMaterial(self, target_material): @@ -8147,7 +8147,7 @@ class PropertyPanelManager: format_info = self._getModelFormat(origin_model) processed = [] - #print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") for name in anim_names: display_name = name @@ -8181,7 +8181,7 @@ class PropertyPanelManager: display_name = name processed.append((display_name, original_name)) - #print(f"[动画分析] {original_name} → {display_name}") + print(f"[动画分析] {original_name} → {display_name}") return processed @@ -8215,7 +8215,7 @@ class PropertyPanelManager: if frames > 1: valid_anims += 1 total_frames += frames - #print(f"[动画分析] '{anim_name}': {frames} 帧") + print(f"[动画分析] '{anim_name}': {frames} 帧") else: print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") except Exception as e: diff --git a/ui/simple_maintenance_login.py b/ui/simple_maintenance_login.py new file mode 100644 index 00000000..2cf5ba45 --- /dev/null +++ b/ui/simple_maintenance_login.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +简化的维修系统登录界面 +解决显示和输入问题 +""" + +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, + QLineEdit, QPushButton, QLabel, QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal + + +class SimpleMaintenanceLoginDialog(QDialog): + """简化的维修系统登录对话框""" + + login_success = pyqtSignal() # 登录成功信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUI() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 登录") + self.resize(400, 250) + self.setModal(True) + + # 设置简洁的样式 + self.setStyleSheet(""" + QDialog { + background-color: #ffffff; + font-family: "Microsoft YaHei", Arial, sans-serif; + } + QGroupBox { + font-size: 14px; + font-weight: bold; + color: #2c3e50; + border: 2px solid #bdc3c7; + border-radius: 5px; + margin-top: 10px; + padding-top: 10px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + } + QLabel { + color: #2c3e50; + font-size: 14px; + } + QLineEdit { + background-color: #ffffff; + border: 2px solid #bdc3c7; + border-radius: 3px; + padding: 6px 8px; + font-size: 14px; + color: #2c3e50; + min-height: 16px; + } + QLineEdit:focus { + border-color: #3498db; + background-color: #ffffff; + } + QPushButton { + background-color: #3498db; + color: white; + border: none; + padding: 8px 20px; + border-radius: 3px; + font-size: 14px; + font-weight: bold; + min-height: 16px; + } + QPushButton:hover { + background-color: #2980b9; + } + QPushButton:pressed { + background-color: #21618c; + } + QPushButton#cancel_btn { + background-color: #95a5a6; + } + QPushButton#cancel_btn:hover { + background-color: #7f8c8d; + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setSpacing(20) + main_layout.setContentsMargins(30, 30, 30, 30) + + # 标题 + title_label = QLabel("🔧 维修系统") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 18px; + font-weight: bold; + color: #2c3e50; + margin: 10px 0px; + """) + main_layout.addWidget(title_label) + + # 登录表单组 + form_group = QGroupBox("身份验证") + form_layout = QFormLayout(form_group) + form_layout.setSpacing(15) + form_layout.setContentsMargins(20, 20, 20, 20) + + # 用户名输入框 + self.username_edit = QLineEdit() + self.username_edit.setPlaceholderText("请输入用户名") + form_layout.addRow("用户名:", self.username_edit) + + # 密码输入框 + self.password_edit = QLineEdit() + self.password_edit.setEchoMode(QLineEdit.Password) + self.password_edit.setPlaceholderText("请输入密码") + form_layout.addRow("密码:", self.password_edit) + + main_layout.addWidget(form_group) + + # 提示信息 + hint_label = QLabel("💡 默认账号密码均为: admin") + hint_label.setAlignment(Qt.AlignCenter) + hint_label.setStyleSheet(""" + color: #7f8c8d; + font-size: 12px; + margin: 5px 0px; + """) + main_layout.addWidget(hint_label) + + # 按钮布局 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + self.cancel_btn = QPushButton("取消") + self.cancel_btn.setObjectName("cancel_btn") + + self.login_btn = QPushButton("登录") + + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.login_btn) + + main_layout.addLayout(button_layout) + + # 连接信号 + self.login_btn.clicked.connect(self.handle_login) + self.cancel_btn.clicked.connect(self.reject) + self.password_edit.returnPressed.connect(self.handle_login) + + # 设置焦点和Tab顺序 + self.username_edit.setFocus() + self.setTabOrder(self.username_edit, self.password_edit) + self.setTabOrder(self.password_edit, self.login_btn) + self.setTabOrder(self.login_btn, self.cancel_btn) + + print("✅ 简化登录界面创建完成") + print(f"📝 窗口大小: {self.size().width()}x{self.size().height()}") + + def handle_login(self): + """处理登录""" + username = self.username_edit.text().strip() + password = self.password_edit.text().strip() + + print(f"🔍 登录尝试: 用户名='{username}', 密码='{password}'") + + # 验证账号密码 + if username == "admin" and password == "admin": + print("✅ 登录验证成功") + self.login_success.emit() + self.accept() + else: + print("❌ 登录验证失败") + QMessageBox.warning( + self, + "登录失败", + "用户名或密码错误!\n\n请使用:\n用户名: admin\n密码: admin" + ) + self.password_edit.clear() + self.password_edit.setFocus() + + +def test_simple_login(): + """测试简化登录界面""" + import sys + from PyQt5.QtWidgets import QApplication + + print("🧪 测试简化登录界面") + print("=" * 30) + + app = QApplication(sys.argv) + + dialog = SimpleMaintenanceLoginDialog() + + print("💡 请测试以下功能:") + print("1. 检查是否显示完整的登录界面") + print("2. 在用户名框输入 'admin'") + print("3. 在密码框输入 'admin'") + print("4. 点击登录按钮") + + def on_success(): + print("🎉 登录成功!") + QMessageBox.information(None, "成功", "登录测试成功!") + + dialog.login_success.connect(on_success) + + result = dialog.exec_() + print(f"📊 测试结果: {'成功' if result == QDialog.Accepted else '取消'}") + + return result == QDialog.Accepted + + +if __name__ == "__main__": + test_simple_login() \ No newline at end of file