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: