From e9039ab832edc09561727e6b545f5d07e7ed1116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Mon, 22 Sep 2025 10:04:20 +0800 Subject: [PATCH 1/5] =?UTF-8?q?1.=E8=99=9A=E6=8B=9F=E7=BB=B4=E4=BF=AE?= =?UTF-8?q?=E7=AE=80=E5=8D=95=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RenderPipelineFile/config/daytime.yaml | 2 +- core/assembly_interaction.py | 1613 ++++++++++++++++++++++ core/collision_manager.py | 8 +- ui/assembly_disassembly_config.py | 944 +++++++++++++ ui/assembly_disassembly_config_simple.py | 1036 ++++++++++++++ ui/main_window.py | 40 +- ui/property_panel.py | 18 +- 7 files changed, 3645 insertions(+), 16 deletions(-) create mode 100644 core/assembly_interaction.py create mode 100644 ui/assembly_disassembly_config.py create mode 100644 ui/assembly_disassembly_config_simple.py diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index a1862356..6bb60b02 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,7 +17,7 @@ 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.5000000000]]] + sun_azimuth: [[[0.5000000000,0.4944444444]]] sun_altitude: [[[0.5000000000,0.9666666667]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py new file mode 100644 index 00000000..7ff48dc7 --- /dev/null +++ b/core/assembly_interaction.py @@ -0,0 +1,1613 @@ +#!/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 + + +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.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 + + # --- 操作权限控制 --- + self.operation_enabled = True # 是否允许进行操作 + + # --- 碰撞检测(优化) --- + self.picker_traverser = CollisionTraverser('picker_traverser') + self.collision_handler = CollisionHandlerQueue() + self.picker_ray_node = None + + print("拆装交互管理器初始化完成") + + def play_step_audio(self, step_data): + """播放步骤音频""" + try: + # 检查是否启用了自动播放音频 + 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 start_interaction_mode(self): + """启动交互模式""" + try: + 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 + + # 设置碰撞检测 + 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() + + # 显示步骤指引界面 + 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() + + self.step_dialog = StepGuideDialog(self) + 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) + + if self.step_dialog: + 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()})') + # 调用我们的处理逻辑 + self.handle_qt_mouse_click(event.x(), event.y()) + # 接受左键事件,阻止进一步传播 + event.accept() + else: + # 其他按键(右键、中键等)使用原有处理方式 + # print(f'🖱️ 其他按键({event.button()})使用原有处理方式') # 静默处理 + if hasattr(self, 'original_mouse_press_event'): + self.original_mouse_press_event(event) + else: + event.ignore() + + def qt_mouse_release_event(self, event): + """Qt鼠标释放事件处理(只拦截左键)""" + from PyQt5.QtCore import Qt + + # 只拦截左键释放,其他按键使用原有处理方式 + if event.button() == Qt.LeftButton: + print('🖱️ 左键释放被拦截处理') + self.on_mouse_up() + # 接受左键释放事件 + event.accept() + else: + # 其他按键释放使用原有处理方式 + # print(f'🖱️ 其他按键释放({event.button()})使用原有处理方式') # 静默处理 + 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.operation_enabled: + print("⚠️ 工具不匹配,无法进行操作") + if hasattr(self, 'step_dialog') and self.step_dialog: + QMessageBox.warning(self.step_dialog, "工具不匹配", + f"当前步骤需要使用 '{self.step_dialog.current_required_tool}' 工具,请先选择正确的工具!") + 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.operation_enabled: + 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): + """完成当前步骤""" + # 在进入下一步之前,取消当前模型的碰撞,避免干扰 + 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() + + # 恢复原有的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 + + QMessageBox.information(None, "完成", "所有拆装步骤已完成!") + + 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.operation_enabled: + 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): + super().__init__() + self.interaction_manager = interaction_manager + self.current_required_tool = "无" # 当前步骤要求的工具 + self.setupUI() + + def setupUI(self): + 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) + + # 步骤描述 + self.step_desc_text = QTextEdit() + self.step_desc_text.setMaximumHeight(100) + self.step_desc_text.setReadOnly(True) + layout.addWidget(self.step_desc_text) + + # 工具选择组 + tool_group = QGroupBox("工具选择") + tool_layout = QVBoxLayout(tool_group) + + self.required_tool_label = QLabel("当前步骤要求工具: 无") + self.required_tool_label.setStyleSheet("font-weight: bold; color: #E74C3C;") + tool_layout.addWidget(self.required_tool_label) + + 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) + + self.tool_status_label = QLabel("✅ 工具匹配,可以进行操作") + self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") + tool_layout.addWidget(self.tool_status_label) + + layout.addWidget(tool_group) + + # 操作提示 + 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) + + # 按钮 + 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 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) + + 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}') + self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name}") + + step_desc = step_data.get('description', '无描述') + self.step_desc_text.setPlainText(step_desc) + + # 更新工具要求 + self.current_required_tool = step_data.get('required_tool', '无') + 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}' 将其拆卸。" + + 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/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..cea49925 --- /dev/null +++ b/ui/assembly_disassembly_config_simple.py @@ -0,0 +1,1036 @@ +# -*- 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) + + 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': '无' + } + + 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._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 + item.setText(new_name) + + 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) + + 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) + 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) + + 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() + + # 确保数据更新到list_item的UserRole中 + self._current_step_item.setData(Qt.UserRole, step_data) + + # 更新列表项的显示文本 + self._current_step_item.setText(step_data['name']) + + # 同时更新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']: + 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_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 4e9139b4..f07f6569 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -10,7 +10,7 @@ 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, @@ -709,6 +709,14 @@ 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.cesiumMenu = menubar.addMenu('Cesium') self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset) @@ -1851,7 +1859,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 @@ -2864,6 +2872,34 @@ 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: + 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() + + except Exception as e: + QMessageBox.critical(self, "错误", f"启动拆装交互失败: {str(e)}") + import traceback + traceback.print_exc() + def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() diff --git a/ui/property_panel.py b/ui/property_panel.py index d37a91b2..d73175b1 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -4756,12 +4756,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: @@ -4800,7 +4800,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 @@ -5186,7 +5186,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方法 @@ -5204,7 +5204,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) @@ -6812,7 +6812,7 @@ class PropertyPanelManager: # print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np - #print("未找到匹配的几何节点") + print("未找到匹配的几何节点") return None def _findSpecificGeomNodeForMaterial(self, target_material): @@ -8097,7 +8097,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 @@ -8131,7 +8131,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 @@ -8165,7 +8165,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: From a76277b2fcfb6ffd3fc71fb1a47046ea3840e21b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Thu, 25 Sep 2025 14:22:34 +0800 Subject: [PATCH 2/5] =?UTF-8?q?1.=E8=99=9A=E6=8B=9F=E7=BB=B4=E4=BF=AE?= =?UTF-8?q?=E8=80=83=E6=A0=B8=E7=AE=80=E5=8D=95=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/assembly_interaction.py | 295 +++++++++++++++++++---- ui/assembly_disassembly_config_simple.py | 45 +++- ui/main_window.py | 125 +++++++++- 3 files changed, 411 insertions(+), 54 deletions(-) diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py index 7ff48dc7..c58caafb 100644 --- a/core/assembly_interaction.py +++ b/core/assembly_interaction.py @@ -22,6 +22,9 @@ class AssemblyInteractionManager(DirectObject): self.current_step = 0 self.total_steps = 0 self.is_active = False + + # --- 模式控制 --- + self.mode = "training" # 默认训练模式,可选 "training" 或 "exam" # --- 拖拽相关(优化) --- self.dragging_model = None @@ -37,6 +40,11 @@ class AssemblyInteractionManager(DirectObject): # --- 操作权限控制 --- self.operation_enabled = True # 是否允许进行操作 + + # --- 考核相关 --- + self.exam_score = 0 # 考核总分 + self.exam_max_score = 0 # 考核满分 + self.step_scores = {} # 每步得分记录 # --- 碰撞检测(优化) --- self.picker_traverser = CollisionTraverser('picker_traverser') @@ -44,10 +52,38 @@ class AssemblyInteractionManager(DirectObject): 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) @@ -294,10 +330,89 @@ class AssemblyInteractionManager(DirectObject): """设置操作是否启用""" 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', '无') + + # 获取当前选择的工具 + current_tool = "无" + if hasattr(self, 'step_dialog') and self.step_dialog and self.step_dialog.current_tool_combo: + current_tool = self.step_dialog.current_tool_combo.currentText() + + # 检查工具是否匹配 + tool_matches = self.check_tool_match(current_tool, required_tool) + + if self.mode == "training": + # 训练模式:显示提示并阻止错误操作 + if not tool_matches: + print("⚠️ 训练模式 - 工具不匹配,无法进行操作") + if 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): + def start_interaction_mode(self, mode="training"): """启动交互模式""" try: + # 设置模式 + self.mode = mode + print(f"🎯 启动拆装交互模式: {self.mode}") + if not self.load_configuration(): return False @@ -308,6 +423,10 @@ class AssemblyInteractionManager(DirectObject): self.total_steps = len(self.config_data['steps']) self.current_step = 0 self.is_active = True + + # 如果是考核模式,初始化考核相关数据 + if self.mode == "exam": + self.init_exam_mode() # 设置碰撞检测 self.setup_picking() @@ -793,11 +912,7 @@ class AssemblyInteractionManager(DirectObject): return # 检查操作权限 - if not self.operation_enabled: - print("⚠️ 工具不匹配,无法进行操作") - if hasattr(self, 'step_dialog') and self.step_dialog: - QMessageBox.warning(self.step_dialog, "工具不匹配", - f"当前步骤需要使用 '{self.step_dialog.current_required_tool}' 工具,请先选择正确的工具!") + if not self.check_operation_permission(): return # 获取当前步骤的目标模型 @@ -1002,8 +1117,8 @@ class AssemblyInteractionManager(DirectObject): def start_dragging(self, model_node, hit_point): """开始拖拽模型(优化版:计算偏移量)""" # 再次检查操作权限(双重保险) - if not self.operation_enabled: - print("⚠️ 工具不匹配,无法开始拖拽") + if not self.check_operation_permission(): + print("⚠️ 权限检查失败,无法开始拖拽") return self.dragging_model = model_node @@ -1231,7 +1346,59 @@ class AssemblyInteractionManager(DirectObject): self.step_dialog.close() self.step_dialog = None - QMessageBox.information(None, "完成", "所有拆装步骤已完成!") + # 如果是考核模式,显示考核结果 + if self.mode == "exam": + self.show_exam_results() + else: + QMessageBox.information(None, "完成", "所有拆装步骤已完成!") + + def show_exam_results(self): + """显示考核结果""" + # 计算最终得分 + final_score = 0 + for step_record in self.step_scores.values(): + final_score += step_record['current_score'] + + # 计算得分率 + score_rate = (final_score / self.exam_max_score * 100) if self.exam_max_score > 0 else 0 + + # 生成详细报告 + report = f"📝 拆装考核结果报告\n" + report += f"{'='*40}\n\n" + report += f"总得分: {final_score:.0f} / {self.exam_max_score:.0f} 分\n" + report += f"得分率: {score_rate:.1f}%\n\n" + + # 评级 + if score_rate >= 90: + grade = "优秀 ⭐⭐⭐" + elif score_rate >= 80: + grade = "良好 ⭐⭐" + elif score_rate >= 60: + grade = "及格 ⭐" + else: + grade = "不及格 ❌" + + report += f"评级: {grade}\n\n" + report += f"各步骤详情:\n" + report += f"{'-'*30}\n" + + # 显示每步详情 + for step_idx, step_record in self.step_scores.items(): + step_name = self.config_data['steps'][step_idx]['name'] + max_score = step_record['max_score'] + current_score = step_record['current_score'] + tool_error = "❌" if step_record['tool_error'] else "✅" + attempts = step_record['operation_attempts'] + + report += f"{step_name}:\n" + report += f" 得分: {current_score:.0f}/{max_score:.0f} 分\n" + report += f" 工具使用: {tool_error}\n" + report += f" 操作次数: {attempts}\n\n" + + print(report) + + # 显示结果对话框 + QMessageBox.information(None, "考核完成", report) def stop_interaction_mode(self): """停止交互模式""" @@ -1280,8 +1447,8 @@ class AssemblyInteractionManager(DirectObject): """触发点击位移动画""" try: # 检查操作权限(双重保险) - if not self.operation_enabled: - print("⚠️ 工具不匹配,无法进行点击触发位移") + if not self.check_operation_permission(): + print("⚠️ 权限检查失败,无法进行点击触发位移") return step_type = self.normalize_step_type(step_data) @@ -1410,10 +1577,15 @@ class StepGuideDialog(QDialog): super().__init__() self.interaction_manager = interaction_manager self.current_required_tool = "无" # 当前步骤要求的工具 + self.mode = interaction_manager.mode # 获取模式 self.setupUI() def setupUI(self): - self.setWindowTitle("拆装步骤指引") + if self.mode == "exam": + self.setWindowTitle("拆装考核") + else: + self.setWindowTitle("拆装步骤指引") + self.setFixedSize(450, 400) self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) layout = QVBoxLayout(self) @@ -1423,19 +1595,26 @@ class StepGuideDialog(QDialog): self.step_info_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2E86C1;") layout.addWidget(self.step_info_label) - # 步骤描述 - self.step_desc_text = QTextEdit() - self.step_desc_text.setMaximumHeight(100) - self.step_desc_text.setReadOnly(True) - layout.addWidget(self.step_desc_text) + # 步骤描述(考核模式下隐藏) + 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) - self.required_tool_label = QLabel("当前步骤要求工具: 无") - self.required_tool_label.setStyleSheet("font-weight: bold; color: #E74C3C;") - tool_layout.addWidget(self.required_tool_label) + # 在训练模式下显示要求的工具,考核模式下不显示 + 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("当前选择工具:")) @@ -1445,20 +1624,28 @@ class StepGuideDialog(QDialog): current_tool_layout.addWidget(self.current_tool_combo) tool_layout.addLayout(current_tool_layout) - self.tool_status_label = QLabel("✅ 工具匹配,可以进行操作") - self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") - tool_layout.addWidget(self.tool_status_label) + # 在训练模式下显示工具状态,考核模式下不显示 + 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) - # 操作提示 - 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) + # 操作提示(考核模式下隐藏) + 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() @@ -1522,16 +1709,22 @@ class StepGuideDialog(QDialog): # 检查工具是否匹配 tool_matches = self.check_tool_permission(current_tool, required_tool) - 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) + 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.tool_status_label.setText(f"❌ 工具不匹配,需要选择 '{required_tool}' 工具") - self.tool_status_label.setStyleSheet("color: #E74C3C; font-weight: bold;") - # 禁用交互操作 - self.interaction_manager.set_operation_enabled(False) + # 考核模式:不显示工具状态,但仍然需要正确的工具才能操作 + # 这里不再总是启用操作,而是让权限检查在实际操作时进行 + self.interaction_manager.set_operation_enabled(True) def check_tool_permission(self, current_tool, required_tool): """检查工具权限""" @@ -1547,14 +1740,24 @@ class StepGuideDialog(QDialog): def update_step_info(self, step_data, current_step, total_steps): step_name = step_data.get('name', f'步骤 {current_step}') - self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name}") + + 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}") - step_desc = step_data.get('description', '无描述') - self.step_desc_text.setPlainText(step_desc) + # 步骤描述(考核模式下不显示) + 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', '无') - self.required_tool_label.setText(f"当前步骤要求工具: {self.current_required_tool}") + if self.required_tool_label is not None: + self.required_tool_label.setText(f"当前步骤要求工具: {self.current_required_tool}") # 更新工具状态 self.update_tool_status() @@ -1590,7 +1793,9 @@ class StepGuideDialog(QDialog): else: operation_hint = f"🔧 拆卸操作\n请操作模型 '{target_model}' 将其拆卸。" - self.operation_text.setPlainText(operation_hint) + # 操作提示(考核模式下不显示) + if self.operation_text is not None: + self.operation_text.setPlainText(operation_hint) def skip_current_step(self): if self.interaction_manager.is_active: diff --git a/ui/assembly_disassembly_config_simple.py b/ui/assembly_disassembly_config_simple.py index cea49925..9397f650 100644 --- a/ui/assembly_disassembly_config_simple.py +++ b/ui/assembly_disassembly_config_simple.py @@ -192,6 +192,22 @@ class AssemblyDisassemblyConfigDialog(QDialog): 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) # 目标模型 @@ -519,12 +535,15 @@ class AssemblyDisassemblyConfigDialog(QDialog): 'target_position': [0, 0, 0], 'audio_file': '', 'hint_text': '', - 'required_tool': '无' + 'required_tool': '无', + 'score': 10 # 默认分值 } self.config_data['steps'].append(step_data) - list_item = QListWidgetItem(step_name) + # 显示步骤名称和分值 + 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) @@ -587,7 +606,9 @@ class AssemblyDisassemblyConfigDialog(QDialog): step_data = item.data(Qt.UserRole) new_name = f"步骤 {i + 1}" step_data['name'] = new_name - item.setText(new_name) + step_score = step_data.get('score', 10) + display_text = f"{new_name} ({step_score}分)" + item.setText(display_text) def onStepSelectionChanged(self): """步骤选择改变""" @@ -657,6 +678,9 @@ class AssemblyDisassemblyConfigDialog(QDialog): self.required_tool_combo.setCurrentIndex(index) else: self.required_tool_combo.setCurrentIndex(0) + + # 加载分值配置 + self.step_score_spin.setValue(step_data.get('score', 10)) finally: # 确保信号重新连接 @@ -678,6 +702,7 @@ class AssemblyDisassemblyConfigDialog(QDialog): 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 @@ -697,6 +722,7 @@ class AssemblyDisassemblyConfigDialog(QDialog): 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数据中""" @@ -724,12 +750,16 @@ class AssemblyDisassemblyConfigDialog(QDialog): 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) - # 更新列表项的显示文本 - self._current_step_item.setText(step_data['name']) + # 更新列表项的显示文本,包含分值信息 + 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) @@ -869,7 +899,10 @@ class AssemblyDisassemblyConfigDialog(QDialog): # 加载步骤配置 self.config_data['steps'] = data.get('steps', []) for step_data in self.config_data['steps']: - list_item = QListWidgetItem(step_data['name']) + 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) diff --git a/ui/main_window.py b/ui/main_window.py index f07f6569..8d7443c3 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -16,7 +16,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction 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 @@ -2886,20 +2886,139 @@ class MainWindow(QMainWindow): 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() + # 启动交互模式,传递模式参数 + 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() +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 + + def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() From d32993b903c9114a24e28419ff4582f08f17c8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Thu, 25 Sep 2025 17:51:46 +0800 Subject: [PATCH 3/5] =?UTF-8?q?1.=E5=90=88=E5=B9=B6=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/assembly_interaction.py | 11 +++-- scene/scene_manager.py | 2 +- ui/main_window.py | 80 +++++++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py index c58caafb..3d1c9417 100644 --- a/core/assembly_interaction.py +++ b/core/assembly_interaction.py @@ -640,7 +640,12 @@ class AssemblyInteractionManager(DirectObject): if self.step_dialog: self.step_dialog.close() - self.step_dialog = StepGuideDialog(self) + # 获取主窗口作为父窗口 + 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): @@ -1573,8 +1578,8 @@ class AssemblyInteractionManager(DirectObject): class StepGuideDialog(QDialog): """步骤指引对话框(UI代码保持不变)""" - def __init__(self, interaction_manager): - super().__init__() + def __init__(self, interaction_manager, parent=None): + super().__init__(parent) self.interaction_manager = interaction_manager self.current_required_tool = "无" # 当前步骤要求的工具 self.mode = interaction_manager.mode # 获取模式 diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 37ee239a..f064e7cf 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -1995,7 +1995,7 @@ class SceneManager: new_element = gui_manager.createGUI2DImage( pos=tuple(absolute_position), image_path=image_path, - size=scale_value * 0.2 + size=scale_value * 0.1 ) elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'): size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 diff --git a/ui/main_window.py b/ui/main_window.py index 7e5f5739..addf2aa3 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -28,7 +28,7 @@ try: WEB_ENGINE_AVAILABLE = True except ImportError: QWebEngineView = None - WEB_ENGINE_AVAILABLE = False + WEB_ENGINE_AVAILABLE = False class MainWindow(QMainWindow): """主窗口类""" @@ -720,6 +720,9 @@ class MainWindow(QMainWindow): 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') @@ -3236,6 +3239,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("🧹 清理工具管理器进程...") @@ -3469,6 +3482,71 @@ class MainWindow(QMainWindow): 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) + + # 启动拆装交互系统 + 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("❌ 错误:拆装交互系统未初始化") + 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): """拆装模式选择对话框""" From 64df3107fe6ccfc1d2e275cf460a0b142d43c9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Fri, 26 Sep 2025 09:24:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?1.=E8=99=9A=E6=8B=9F=E7=BB=B4=E4=BF=AE?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/assembly_interaction.py | 542 +++++++++++++++-- core/maintenance_gui.py | 1023 ++++++++++++++++++++++++++++++++ ui/main_window.py | 26 +- ui/maintenance_system.py | 658 ++++++++++++++++++++ ui/simple_maintenance_login.py | 220 +++++++ 5 files changed, 2395 insertions(+), 74 deletions(-) create mode 100644 core/maintenance_gui.py create mode 100644 ui/maintenance_system.py create mode 100644 ui/simple_maintenance_login.py diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py index 3d1c9417..4e6bae63 100644 --- a/core/assembly_interaction.py +++ b/core/assembly_interaction.py @@ -11,6 +11,13 @@ from PyQt5.QtWidgets import QMessageBox, QFileDialog, QDialog, QVBoxLayout, QHBo 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): """拆装交互管理器(优化版)""" @@ -38,6 +45,11 @@ class AssemblyInteractionManager(DirectObject): # --- UI组件 --- self.step_dialog = None + # --- 维修系统GUI --- + self.maintenance_gui = None + if MaintenanceGUI: + self.maintenance_gui = MaintenanceGUI(world) + # --- 操作权限控制 --- self.operation_enabled = True # 是否允许进行操作 @@ -345,19 +357,29 @@ class AssemblyInteractionManager(DirectObject): step_data = self.config_data['steps'][self.current_step] required_tool = step_data.get('required_tool', '无') - # 获取当前选择的工具 + # 获取当前选择的工具(优先从维修GUI获取) current_tool = "无" - if hasattr(self, 'step_dialog') and self.step_dialog and self.step_dialog.current_tool_combo: - current_tool = self.step_dialog.current_tool_combo.currentText() + 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("⚠️ 训练模式 - 工具不匹配,无法进行操作") - if hasattr(self, 'step_dialog') and self.step_dialog: + + # 在维修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 @@ -413,8 +435,10 @@ class AssemblyInteractionManager(DirectObject): self.mode = mode print(f"🎯 启动拆装交互模式: {self.mode}") - if not self.load_configuration(): - return False + # 如果没有配置数据,尝试加载 + 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请先在'拆装配置'中设置步骤。") @@ -424,9 +448,21 @@ class AssemblyInteractionManager(DirectObject): 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() @@ -470,8 +506,9 @@ class AssemblyInteractionManager(DirectObject): # 调试:列出场景中的所有模型 self.list_scene_models() - # 显示步骤指引界面 - self.show_step_dialog() + # 显示步骤指引界面(维修系统使用GUI界面,跳过Qt对话框) + if not self.maintenance_gui: + self.show_step_dialog() # 开始第一步 self.start_current_step() @@ -706,7 +743,26 @@ class AssemblyInteractionManager(DirectObject): # 播放步骤音频(如果有配置) self.play_step_audio(step_data) - if self.step_dialog: + # 更新维修系统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) # 准备交互(主要是确保目标模型可以被拾取) @@ -872,38 +928,211 @@ class AssemblyInteractionManager(DirectObject): print("\n✅ 所有模型都处于正确状态") def qt_mouse_press_event(self, event): - """Qt鼠标按下事件处理(只拦截左键)""" + """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: - # 其他按键(右键、中键等)使用原有处理方式 - # print(f'🖱️ 其他按键({event.button()})使用原有处理方式') # 静默处理 + # 其他按键使用原有处理方式 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鼠标释放事件处理(只拦截左键)""" + """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: # 其他按键释放使用原有处理方式 - # print(f'🖱️ 其他按键释放({event.button()})使用原有处理方式') # 静默处理 if hasattr(self, 'original_mouse_release_event'): self.original_mouse_release_event(event) else: @@ -1314,6 +1543,11 @@ class AssemblyInteractionManager(DirectObject): 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') @@ -1338,6 +1572,11 @@ class AssemblyInteractionManager(DirectObject): 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: @@ -1351,59 +1590,234 @@ class AssemblyInteractionManager(DirectObject): self.step_dialog.close() self.step_dialog = None - # 如果是考核模式,显示考核结果 + # 根据模式显示不同的完成结果 if self.mode == "exam": + # 考核模式:显示详细的考核结果 self.show_exam_results() else: - QMessageBox.information(None, "完成", "所有拆装步骤已完成!") + # 训练模式:显示训练完成提示 + 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 show_exam_results(self): """显示考核结果""" - # 计算最终得分 - final_score = 0 - for step_record in self.step_scores.values(): - final_score += step_record['current_score'] - - # 计算得分率 - score_rate = (final_score / self.exam_max_score * 100) if self.exam_max_score > 0 else 0 - - # 生成详细报告 - report = f"📝 拆装考核结果报告\n" - report += f"{'='*40}\n\n" - report += f"总得分: {final_score:.0f} / {self.exam_max_score:.0f} 分\n" - report += f"得分率: {score_rate:.1f}%\n\n" - - # 评级 - if score_rate >= 90: - grade = "优秀 ⭐⭐⭐" - elif score_rate >= 80: - grade = "良好 ⭐⭐" - elif score_rate >= 60: - grade = "及格 ⭐" - else: - grade = "不及格 ❌" - - report += f"评级: {grade}\n\n" - report += f"各步骤详情:\n" - report += f"{'-'*30}\n" - - # 显示每步详情 - for step_idx, step_record in self.step_scores.items(): - step_name = self.config_data['steps'][step_idx]['name'] - max_score = step_record['max_score'] - current_score = step_record['current_score'] - tool_error = "❌" if step_record['tool_error'] else "✅" - attempts = step_record['operation_attempts'] + try: + # 计算最终得分 + final_score = 0 + total_operations = 0 + total_errors = 0 - report += f"{step_name}:\n" - report += f" 得分: {current_score:.0f}/{max_score:.0f} 分\n" - report += f" 工具使用: {tool_error}\n" - report += f" 操作次数: {attempts}\n\n" - - print(report) - - # 显示结果对话框 - QMessageBox.information(None, "考核完成", report) + 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): """停止交互模式""" diff --git a/core/maintenance_gui.py b/core/maintenance_gui.py new file mode 100644 index 00000000..a5cadb99 --- /dev/null +++ b/core/maintenance_gui.py @@ -0,0 +1,1023 @@ +#!/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 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.current_tool = "手" # 当前选择的工具 + 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() + + # 创建警告文本(仅训练模式需要) + 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_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 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 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.mode == "training": + 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} 已显示") + + 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() + + print("✅ 维修系统GUI隐藏") + + except Exception as e: + print(f"❌ 隐藏维修系统GUI失败: {e}") \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index addf2aa3..01dc5b6c 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -3516,18 +3516,24 @@ class MainWindow(QMainWindow): 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) - + # 初始化拆装交互系统(如果还没有) + 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, "错误", "拆装交互系统未初始化") + print("❌ 维修科目启动失败") + QMessageBox.warning(self, "错误", "维修科目启动失败") except Exception as e: print(f"❌ 启动维修科目失败: {e}") 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/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 From d69cff3466f7df4da80400f3344ac055a8cad582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Sun, 28 Sep 2025 18:14:54 +0800 Subject: [PATCH 5/5] =?UTF-8?q?1.=E8=99=9A=E6=8B=9F=E7=BB=B4=E4=BF=AE?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=80=E5=87=BABt=EF=BC=88=E4=BD=86?= =?UTF-8?q?=E6=98=AF=E6=9C=89Bug=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/assembly_interaction.py | 48 ++++++++++++++++++++ core/maintenance_gui.py | 85 ++++++++++++++++++++++++++++++++---- 2 files changed, 124 insertions(+), 9 deletions(-) diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py index 4e6bae63..56285bcc 100644 --- a/core/assembly_interaction.py +++ b/core/assembly_interaction.py @@ -456,6 +456,7 @@ class AssemblyInteractionManager(DirectObject): print(f"✅ 维修系统GUI已启动,模式: {mode}") # 如果是考核模式,初始化考核相关数据 + if self.mode == "exam": self.init_exam_mode() # 显示考核开始提示 @@ -1605,6 +1606,53 @@ class AssemblyInteractionManager(DirectObject): 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: diff --git a/core/maintenance_gui.py b/core/maintenance_gui.py index a5cadb99..cbc66836 100644 --- a/core/maintenance_gui.py +++ b/core/maintenance_gui.py @@ -10,6 +10,7 @@ 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 # 引入全局变量 @@ -39,9 +40,10 @@ class MaintenanceGUI(DirectObject): self.current_tool_text = None # 当前工具显示文本 self.warning_text = None # 警告文本 self.tool_buttons = [] # 工具按钮列表 + self.stop_button = None # 停止按钮 # 状态 - self.current_tool = "手" # 当前选择的工具 + self.current_tool = None # 当前选择的工具,初始为无 self.available_tools = [] # 可用工具列表 self.current_step_info = "" # 当前步骤信息 self.mode = "training" # 模式:training 或 exam @@ -82,6 +84,9 @@ class MaintenanceGUI(DirectObject): self.create_tool_buttons() self.create_current_tool_text() + # 创建停止按钮(训练和考核模式都需要) + # self.create_stop_button() + # 创建警告文本(仅训练模式需要) if mode == "training": self.create_warning_text() @@ -439,6 +444,39 @@ class MaintenanceGUI(DirectObject): 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: @@ -522,6 +560,22 @@ class MaintenanceGUI(DirectObject): 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: @@ -955,6 +1009,11 @@ class MaintenanceGUI(DirectObject): 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) @@ -986,14 +1045,19 @@ class MaintenanceGUI(DirectObject): self.step_text.show() print(" ✅ 步骤文本已显示") - if self.mode == "training": - 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.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显示完成") @@ -1017,6 +1081,9 @@ class MaintenanceGUI(DirectObject): for button in self.tool_buttons: button.hide() + if self.stop_button: + self.stop_button.hide() + print("✅ 维修系统GUI隐藏") except Exception as e: