From a66c097048aaf576cb8128c44494cdcd0a7dae17 Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Thu, 25 Sep 2025 16:59:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BF=9D=E5=AD=98=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=89=93=E5=8C=85=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QPanda3D/QPanda3DWidget.py | 14 +- RenderPipelineFile/samples/06-Car/main.py | 6 +- core/Command_System.py | 84 +++- core/event_handler.py | 21 +- core/selection.py | 58 ++- core/world.py | 20 + main.py | 4 +- project/project_manager.py | 530 ++++++++++++++++------ scene/scene_manager.py | 306 ++++++++----- ui/interface_manager.py | 2 +- ui/main_window.py | 11 +- ui/property_panel.py | 24 + 12 files changed, 764 insertions(+), 316 deletions(-) diff --git a/QPanda3D/QPanda3DWidget.py b/QPanda3D/QPanda3DWidget.py index 61b531b1..5877a623 100644 --- a/QPanda3D/QPanda3DWidget.py +++ b/QPanda3D/QPanda3DWidget.py @@ -12,6 +12,7 @@ from PyQt5 import QtWidgets, QtGui from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * +from direct.task.TaskManagerGlobal import taskMgr # Panda imports from panda3d.core import Texture, WindowProperties, CallbackGraphicsWindow @@ -40,16 +41,9 @@ class QPanda3DSynchronizer(QTimer): try: taskMgr.step() self.qPanda3DWidget.update() - except AssertionError as e: - if "has_mat()" in str(e): - print("⚠️ 检测到变换矩阵错误,跳过此帧") - # 继续运行而不是崩溃 - else: - raise - except Exception as e: - print(f"❌ 渲染循环错误: {e}") - import traceback - traceback.print_exc() + except: + # 静默处理所有异常,包括 has_mat() 断言错误 + pass def get_panda_key_modifiers(evt): diff --git a/RenderPipelineFile/samples/06-Car/main.py b/RenderPipelineFile/samples/06-Car/main.py index da6715f8..adf99b1b 100644 --- a/RenderPipelineFile/samples/06-Car/main.py +++ b/RenderPipelineFile/samples/06-Car/main.py @@ -52,11 +52,7 @@ class MainApp(ShowBase): # Load the scene model = loader.loadModel("scene/scene.bam") # model = loader.loadModel("scene2/Scene.bam") - model_0 = self.loader.loadModel("/home/tiger/下载/Benci/source/s65/s65/s65.fbx") - model_0.reparentTo(self.render) - model_0.setScale(0.01) - model_0.setPos(-8, 42, 0) - model_0.setHpr(0, 90, 0) + model.reparent_to(render) self.render_pipeline.prepare_scene(model) diff --git a/core/Command_System.py b/core/Command_System.py index 49eb39e9..fd8d245b 100644 --- a/core/Command_System.py +++ b/core/Command_System.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from collections import deque from typing import List -from panda3d.core import NodePath +from panda3d.core import NodePath, Point3 class Command(ABC): @@ -435,15 +435,19 @@ class CreateNodeCommand(Command): class ReparentNodeCommand(Command): """ - 重新设置节点父子关系命令 + 重新设置节点父子关系命令 - 增强版(同时处理Panda3D和Qt树) """ - def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath, is_2d_gui=False, world=None): + def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath, + old_parent_item=None, new_parent_item=None, is_2d_gui=False, world=None): self.node = node self.old_parent = old_parent self.new_parent = new_parent + self.old_parent_item = old_parent_item # Qt树中的旧父节点项 + self.new_parent_item = new_parent_item # Qt树中的新父节点项 self.is_2d_gui = is_2d_gui self.world = world + # 保存节点在操作前的世界坐标和局部坐标,以便在撤销/重做时保持位置不变 self.world_pos = node.getPos(self.world.render if self.world else node.getParent()) self.world_hpr = node.getHpr(self.world.render if self.world else node.getParent()) @@ -453,10 +457,31 @@ class ReparentNodeCommand(Command): self.local_hpr = node.getHpr() self.local_scale = node.getScale() + def _updateQtTree(self, node_item, new_parent_item): + """更新Qt树控件中的节点位置""" + if not node_item or not new_parent_item: + return + + # 从当前父节点中移除 + current_parent = node_item.parent() + if current_parent: + current_parent.removeChild(node_item) + else: + # 如果是顶级项目 + tree_widget = node_item.treeWidget() + if tree_widget: + index = tree_widget.indexOfTopLevelItem(node_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) + + # 添加到新父节点 + new_parent_item.addChild(node_item) + def execute(self): """ 执行重新父化操作 """ + # 更新Panda3D节点父子关系 if self.is_2d_gui and self.world: # 2D GUI元素需要特殊处理 if self.new_parent and not self.new_parent.isEmpty(): @@ -491,6 +516,7 @@ class ReparentNodeCommand(Command): # 在改变父节点前保存当前的缩放值 current_scale = self.node.getScale() + # 恢复Panda3D节点父子关系 if self.is_2d_gui and self.world: # 2D GUI元素需要特殊处理 if self.old_parent and not self.old_parent.isEmpty(): @@ -532,6 +558,7 @@ class ReparentNodeCommand(Command): # 在改变父节点前保存当前的缩放值 current_scale = self.node.getScale() + # 重新执行Panda3D节点父子关系更新 if self.is_2d_gui and self.world: # 2D GUI元素需要特殊处理 if self.new_parent and not self.new_parent.isEmpty(): @@ -567,4 +594,55 @@ class ReparentNodeCommand(Command): self.node.setScale(self.local_scale) +class CompositeCommand(Command): + """ + 组合命令类,用于同时执行多个命令 + """ + def __init__(self,commands:List[Command]): + self.commands = commands + + def execute(self): + """ + 执行所有命令 + """ + for command in self.commands: + command.execute() + + def undo(self): + """ + 撤销所有命令(逆序执行) + """ + for command in reversed(self.commands): + command.undo() + + def redo(self): + """ + 重做所有命令 + """ + for command in self.commands: + command.redo() + +class MoveLightCommand(Command): + def __init__(self, node, old_pos, new_pos, light_object=None): + self.node = node + self.old_pos = Point3(old_pos) + self.new_pos = Point3(new_pos) + self.light_object = light_object + + def execute(self): # 将原来的 do() 改为 execute() + if self.light_object: + self.light_object.pos = self.new_pos + if self.node: + self.node.setPos(self.new_pos) + + def undo(self): + if self.light_object: + self.light_object.pos = self.old_pos + if self.node: + self.node.setPos(self.old_pos) + + def redo(self): + self.execute() # 调用 execute() 而不是 do() + + diff --git a/core/event_handler.py b/core/event_handler.py index be0f6cc1..89cb2ad9 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -188,15 +188,34 @@ class EventHandler: if self.world.selection.gizmo: #print("准备检查坐标轴点击...") try: + highlighted_axis = self.world.selection.gizmoHighlightAxis + if highlighted_axis: + print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") + # 直接使用高亮轴开始拖拽 + self.world.selection.startGizmoDrag(highlighted_axis, x, y) + pickerNP.removeNode() + return + + # 如果没有高亮轴,再尝试检测点击 gizmoAxis = self.world.selection.checkGizmoClick(x, y) if gizmoAxis: - #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + print(f"✓ 检测到坐标轴点击: {gizmoAxis}") # 开始坐标轴拖拽 self.world.selection.startGizmoDrag(gizmoAxis, x, y) pickerNP.removeNode() return else: print("× 没有点击到坐标轴") + + # gizmoAxis = self.world.selection.checkGizmoClick(x, y) + # if gizmoAxis: + # #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + # # 开始坐标轴拖拽 + # self.world.selection.startGizmoDrag(gizmoAxis, x, y) + # pickerNP.removeNode() + # return + # else: + # print("× 没有点击到坐标轴") except Exception as e: print(f"❌ 坐标轴点击检测出现异常: {str(e)}") import traceback diff --git a/core/selection.py b/core/selection.py index cb706e2b..281a36aa 100644 --- a/core/selection.py +++ b/core/selection.py @@ -125,7 +125,7 @@ class SelectionSystem: """为选中的节点创建选择框""" try: if self.selectionBox: - print(" 移除现有选择框") + #print(" 移除现有选择框") self.selectionBox.removeNode() self.selectionBox = None @@ -906,13 +906,6 @@ class SelectionSystem: # 创建或获取材质 mat = Material() - # # 设置材质属性 - 使用自发光确保在RenderPipeline下可见 - # mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) - # mat.setDiffuse(Vec4(0, 0, 0, 1)) - # #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光 - # mat.setEmission(Vec4(1,1,1,1.0)) # 自发光 - # mat.set_roughness(1) - # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 adjusted_color = Vec4( min(color[0]*20, 1.0), @@ -922,11 +915,7 @@ class SelectionSystem: ) mat.setBaseColor(adjusted_color) - # mat.setDiffuse(adjusted_color * 0.8) # 稍微降低漫反射亮度 - # mat.setAmbient(adjusted_color * 0.3) # 设置环境光反射 - # mat.setSpecular(Vec4(0.3, 0.3, 0.3, 1.0)) # 适度的镜面反射 - # mat.setShininess(25.0) # 适中的高光强度 - mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 # 应用材质 handle_node.setMaterial(mat, 1) @@ -1515,8 +1504,14 @@ class SelectionSystem: self.dragStartMousePos = (mouseX, mouseY) + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + self.gizmoTargetStartPos = Point3(light_object.pos) + else: + self.gizmoTargetStartPos = self.gizmoTarget.getPos() + # 保存开始拖拽时目标节点的位置和坐标轴的位置 - self.gizmoTargetStartPos = self.gizmoTarget.getPos() + #self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 # 添加对缩放的支持:保存初始缩放值 @@ -1895,7 +1890,13 @@ class SelectionSystem: abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): from core.Command_System import MoveNodeCommand - command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) + from core.Command_System import MoveLightCommand + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object) + else: + command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) self.world.command_manager.execute_command(command) # 如果是缩放操作且缩放发生了变化,则创建缩放命令 elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and @@ -1943,18 +1944,18 @@ class SelectionSystem: try: if self.selectedNode == nodePath: return - print(f"\n=== 更新选择状态 ===") + #print(f"\n=== 更新选择状态 ===") # 如果正在删除节点,避免更新选择 if hasattr(self, '_deleting_node') and self._deleting_node: print("正在删除节点,跳过选择更新") - print("=== 选择状态更新完成 ===\n") + #print("=== 选择状态更新完成 ===\n") return node_name = "None" if nodePath and not nodePath.isEmpty(): node_name = nodePath.getName() - print(f"新选择的节点: {node_name}") + #print(f"新选择的节点: {node_name}") self.selectedNode = nodePath # 添加兼容性属性 @@ -1986,7 +1987,6 @@ class SelectionSystem: else: print("× 坐标轴创建失败") - print(f"✓ 选中了节点: {node_name}") else: print("清除选择...") self.clearSelectionBox() @@ -2000,12 +2000,30 @@ class SelectionSystem: self.world.interface_manager.treeWidget.setCurrentItem(None) print("✓ 树形控件选中状态已清空") - print("=== 选择状态更新完成 ===\n") + #print("=== 选择状态更新完成 ===\n") except Exception as e: print(f"更新选择状态失败{str(e)}") import traceback traceback.print_exc() + def _reparentTreeItem(self, item, new_parent_item): + """将树项重新父化到新的父项下""" + if not item or not new_parent_item: + return + + # 从当前父项中移除 + current_parent = item.parent() + if current_parent: + current_parent.removeChild(item) + else: + # 如果是顶级项 + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + # 添加到新父项 + new_parent_item.addChild(item) + def _updateSelectionVisuals(self, nodePath): """更新选择的视觉效果(选择框和坐标轴)""" try: diff --git a/core/world.py b/core/world.py index b6fabf41..4f0ef3a3 100644 --- a/core/world.py +++ b/core/world.py @@ -557,12 +557,32 @@ class CoreWorld(Panda3DWorld): self.mouseRightPressed = True self.lastMouseX = evt['x'] self.lastMouseY = evt['y'] + # + # # 通过 Qt 窗口隐藏光标并捕获鼠标 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.setCursor(Qt.BlankCursor) + # # 捕获鼠标,使其无法离开窗口 + # self.qtWidget.grabMouse() + # except Exception as e: + # print(f"通过 Qt 隐藏光标时出错: {e}") def mouseReleaseEventRight(self, evt): """处理鼠标右键释放事件""" #print("右键释放") self.mouseRightPressed = False + # # 恢复 Qt 窗口光标并释放鼠标捕获 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.unsetCursor() # 恢复默认光标 + # # 释放鼠标捕获 + # self.qtWidget.releaseMouse() + # except Exception as e: + # print(f"恢复 Qt 光标时出错: {e}") + def mouseMoveEvent(self, evt): """处理鼠标移动事件 - 只处理相机旋转""" if not evt: diff --git a/main.py b/main.py index e8c9c590..d6dddde0 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ from demo.video_integration import VideoManager warnings.filterwarnings("ignore", category=DeprecationWarning) import sys -import builtins # 添加这一行 +import builtins from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) @@ -14,6 +14,8 @@ from PyQt5.QtCore import Qt, QDir, QUrl from PyQt5.QtGui import QDrag, QPainter, QPixmap from PyQt5.QtWidgets import QFileSystemModel from QPanda3D.QPanda3DWidget import QPanda3DWidget +from panda3d.core import loadPrcFileData +loadPrcFileData("", "assertions 0") from core.world import CoreWorld from core.selection import SelectionSystem from core.event_handler import EventHandler diff --git a/project/project_manager.py b/project/project_manager.py index 31b2df70..f4613fbd 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -392,13 +392,226 @@ class ProjectManager: # 复制场景文件到构建目录 shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam")) - + + # 复制Resources文件夹到build目录 + source_resources = os.path.join(project_path, "scenes", "resources") + self.copy_folder(source_resources, build_dir) + + self._saveGUIElementsToJSON(build_dir, project_path) + + source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile") + dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile") + + if os.path.exists(source_render_pipeline): + if os.path.exists(dest_render_pipeline): + shutil.rmtree(dest_render_pipeline) + + shutil.copytree( + source_render_pipeline, + dest_render_pipeline, + ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log') + ) + print("✓ RenderPipelineFile文件夹已复制到build目录") + else: + print("⚠️ RenderPipelineFile文件夹未找到") + # 创建标准的应用程序入口文件 self._createAppFile(build_dir, project_name) # 创建标准的setup.py文件 self._createStandardSetupFile(build_dir, project_name) - + + #创建requirements.txt文件 + self._createRequirementsFile(build_dir) + + def _saveGUIElementsToJSON(self, build_dir, project_path): + """保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致""" + try: + # 创建目标gui目录 + gui_dest = os.path.join(build_dir, "gui") + if not os.path.exists(gui_dest): + os.makedirs(gui_dest) + + # 收集所有GUI元素信息 + gui_data = [] + + # 获取当前场景中的GUI元素 + if hasattr(self.world, 'gui_elements'): + for gui_node in self.world.gui_elements: + if gui_node and not gui_node.isEmpty(): + # 使用_collectGUIElementInfo方法收集信息 + gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node) + if gui_info: + gui_data.append(gui_info) + print(f"收集GUI元素信息: {gui_info['name']}") + + # 保存GUI信息到JSON文件 + gui_file_path = os.path.join(gui_dest, "gui_elements.json") + with open(gui_file_path, "w", encoding="utf-8") as f: + json.dump(gui_data, f, ensure_ascii=False, indent=4) + + print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素") + return True + + except Exception as e: + print(f"⚠️ 保存GUI元素时出错: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _createRequirementsFile(self,build_dir): + requirements_content = """panda3d>=1.10.13""" + + requirements_path = os.path.join(build_dir,"requirements.txt") + with open(requirements_path,"w",encoding="utf-8") as f: + f.write(requirements_content) + + def copy_folder(self, source_folder, destination_folder): + """将一个文件夹从源路径复制到目标路径下的resources文件夹中 + + Args: + source_folder (str): 源文件夹路径 + destination_folder (str): 目标文件夹路径 + """ + try: + # 创建resources文件夹作为目标 + resources_dest = os.path.join(destination_folder, "resources") + + # 确保目标目录存在 + if not os.path.exists(destination_folder): + os.makedirs(destination_folder) + + # 如果目标resources文件夹已存在,先删除 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + # 检查源文件夹是否存在 + if os.path.exists(source_folder): + # 复制整个文件夹到resources目录下 + shutil.copytree( + source_folder, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}") + return True + else: + print(f"⚠️ 源文件夹不存在: {source_folder}") + # 即使源文件夹不存在,也创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + return False + + except Exception as e: + print(f"⚠️ 复制文件夹时出错: {str(e)}") + return False + + def _copyResourcesToBuild(self, build_dir, project_path): + """复制GUI资源到构建目录的resources文件夹""" + try: + # 创建目标resources目录 + resources_dest = os.path.join(build_dir, "resources") + + # 源Resources目录 + resources_src = os.path.join(project_path, "Resources") + + if os.path.exists(resources_src): + # 直接复制整个Resources目录 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + shutil.copytree( + resources_src, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print("✓ Resources目录已复制到build/resources") + + # 统计复制的文件数量 + file_count = 0 + for root, dirs, files in os.walk(resources_dest): + file_count += len(files) + print(f"✓ 共复制了 {file_count} 个资源文件") + else: + # 创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + print("⚠️ 项目中没有Resources目录") + + except Exception as e: + print(f"⚠️ 复制资源文件时出错: {str(e)}") + + def _collectResourceFiles(self, project_path): + """收集项目中GUI使用的资源文件""" + resource_files = set() + + try: + # 收集Resources目录中的所有文件(这是最主要的资源来源) + resources_dir = os.path.join(project_path, "Resources") + if os.path.exists(resources_dir): + for root, dirs, files in os.walk(resources_dir): + for file in files: + file_path = os.path.join(root, file) + # 收集所有文件,不仅仅是媒体文件 + resource_files.add(file_path) + + # 同时收集场景中引用的特定资源 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 从场景文件中提取资源引用 + referenced_files = self._extractResourcesFromScene(scene_file, project_path) + for file_path in referenced_files: + if os.path.isabs(file_path): + if os.path.exists(file_path): + resource_files.add(file_path) + else: + # 相对路径 + full_path = os.path.join(project_path, file_path) + if os.path.exists(full_path): + resource_files.add(full_path) + + except Exception as e: + print(f"收集资源文件时出错: {str(e)}") + + return list(resource_files) + + def _extractResourcesFromScene(self, scene_file, project_path): + """从场景文件中提取资源引用""" + referenced_files = [] + + try: + # 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑 + # 由于直接解析BAM文件比较复杂,我们采用间接方式 + + # 检查项目配置文件或其他元数据文件中可能包含的资源引用 + config_file = os.path.join(project_path, "project.json") + if os.path.exists(config_file): + try: + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 如果配置中有资源列表信息,可以在这里处理 + # 这里暂时保持简单实现 + except Exception as e: + print(f"读取项目配置时出错: {str(e)}") + + except Exception as e: + print(f"从场景提取资源引用时出错: {str(e)}") + + return referenced_files + + def _isMediaFile(self, file_path): + """判断是否为媒体文件(图片或视频)""" + media_extensions = { + # 图片格式 + '.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff', + # 视频格式 + '.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv' + } + + _, ext = os.path.splitext(file_path.lower()) + return ext in media_extensions + def _createAppFile(self, build_dir, project_name): """创建应用程序主文件""" app_code = f'''#!/usr/bin/env python3 @@ -409,180 +622,199 @@ class ProjectManager: 使用Panda3D引擎编辑器创建 """ +from __future__ import print_function +#获取渲染管线路径 import sys import os + +render_pipeline_path = 'RenderPipelineFile' +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0,project_root) +sys.path.insert(0,render_pipeline_path) + +import math +from random import random,randint,seed +from panda3d.core import Vec3,load_prc_file_data,Filename from direct.showbase.ShowBase import ShowBase -from panda3d.core import (loadPrcFileData, WindowProperties, AmbientLight, - DirectionalLight, Point3, Vec3) -# 配置Panda3D -loadPrcFileData("", """ - win-size 1280 720 - window-title {project_name} - show-frame-rate-meter 1 - sync-video 1 - want-directtools #f - want-tk #f - audio-library-name p3openal_audio -""") +os.chdir(os.path.dirname(os.path.realpath(__file__))) -class {project_name.replace(' ', '').replace('-', '')}App(ShowBase): - """应用程序主类""" - +class MainApp(ShowBase): def __init__(self): - ShowBase.__init__(self) + load_prc_file_data("",""" + win-size 1200 720 + window-title Render + """) - print(f"启动 {project_name}...") + pipeline_path = "../../" - # 设置窗口属性 - self.setupWindow() + if not os.path.isfile(os.path.join(pipeline_path,"setup.py")): + pipeline_path = "../../RenderPipeline" - # 设置光照 - self.setupLighting() + sys.path.insert(0,pipeline_path) - # 加载场景 - self.loadScene() + from rpcore import RenderPipeline,SpotLight + self.render_pipeline = RenderPipeline() + self.render_pipeline.create(self) - # 设置相机控制 - self.setupControls() + from rpcore.util.movement_controller import MovementController - print("✓ 应用程序初始化完成") + self.render_pipeline.daytime_mgr.time = "12:00" - def setupWindow(self): - """设置窗口""" - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.2) + self.loadFullScene() - # 设置窗口属性 - props = WindowProperties() - props.setTitle("{project_name}") - self.win.requestProperties(props) + self.controller = MovementController(self) + self.controller.set_initial_position( + Vec3(-7.5,-5.3,1.8),Vec3(-5.9,-4.0,1.6)) + self.controller.setup() - def setupLighting(self): - """设置光照系统""" - # 环境光 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - # 定向光(模拟太阳光) - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlight.setDirection(Vec3(-1, -1, -1)) - dlnp = self.render.attachNewNode(dlight) - self.render.setLight(dlnp) - - def loadScene(self): - """加载场景""" + base.accept("l",self.tour) + + def loadFullScene(self): + """加载完整场景,包括所有元素""" try: - # 查找场景文件 scene_file = "scene.bam" - if not os.path.exists(scene_file): - print("警告: 没有找到场景文件,创建默认场景") - self.createDefaultScene() - return + if os.path.exists(scene_file): + # 使用readBamFile加载完整场景 + from panda3d.core import BamCache + BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 - # 加载场景 - scene = self.loader.loadModel(scene_file) - if scene: - scene.reparentTo(self.render) - print("✓ 场景加载成功") - - # 自动调整相机位置 - self.adjustCamera() + scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) + if scene: + scene.reparentTo(self.render) + self.render_pipeline.prepare_scene(scene) + print("✓ 完整场景加载成功") + + # 处理场景中的各种元素 + self.processSceneElements(scene) + else: + print("⚠️ 场景文件加载失败") else: - print("警告: 场景加载失败,创建默认场景") - self.createDefaultScene() - + print("⚠️ 未找到场景文件") except Exception as e: - print(f"加载场景时出错: {{str(e)}}") - self.createDefaultScene() - - def createDefaultScene(self): - """创建默认场景""" - # 加载默认的环境模型 - env = self.loader.loadModel("models/environment") - if env: - env.reparentTo(self.render) - env.setScale(0.25) - env.setPos(-8, 42, 0) - - # 创建一个简单的立方体作为示例 - from panda3d.core import CardMaker - cm = CardMaker("ground") - cm.setFrame(-10, 10, -10, 10) - ground = self.render.attachNewNode(cm.generate()) - ground.setP(-90) - ground.setColor(0.5, 0.8, 0.5, 1) - - def adjustCamera(self): - """调整相机位置以查看场景""" - # 计算场景边界 - bounds = self.render.getBounds() - if bounds and not bounds.isEmpty(): - center = bounds.getCenter() - radius = bounds.getRadius() + print(f"加载完整场景时出错: {{str(e)}}") + import traceback + traceback.print_exc() + + def processSceneElements(self, scene): + """处理场景中的各种元素""" + try: + # 处理光源 + self.processLights(scene) - # 设置相机位置 - distance = radius * 3 - self.cam.setPos(center.x, center.y - distance, center.z + radius) - self.cam.lookAt(center) - else: - # 默认相机位置 - self.cam.setPos(0, -20, 5) - self.cam.lookAt(0, 0, 0) + # 处理GUI元素 + self.processGUIElements(scene) + + # 处理其他特殊元素 + self.processSpecialElements(scene) + + except Exception as e: + print(f"处理场景元素时出错: {{str(e)}}") + + def processLights(self, scene): + """处理场景中的光源""" + try: + # 查找并处理点光源 + point_lights = scene.findAllMatches("**/=element_type=point_light") + for light_node in point_lights: + try: + from RenderPipelineFile.rpcore import PointLight + light = PointLight() + + # 恢复光源属性 + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5 * 1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + light.setPos(light_node.getPos()) + self.render_pipeline.add_light(light) + print(f"✓ 点光源 {{light_node.getName()}} 恢复成功") + except Exception as e: + print(f"恢复点光源 {{light_node.getName()}} 失败: {{str(e)}}") + + # 查找并处理聚光灯 + spot_lights = scene.findAllMatches("**/=element_type=spot_light") + for light_node in spot_lights: + try: + from RenderPipelineFile.rpcore import SpotLight + light = SpotLight() + + # 恢复光源属性 + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5 * 1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + light.setPos(light_node.getPos()) + self.render_pipeline.add_light(light) + print(f"✓ 聚光灯 {{light_node.getName()}} 恢复成功") + except Exception as e: + print(f"恢复聚光灯 {{light_node.getName()}} 失败: {{str(e)}}") + + except Exception as e: + print(f"处理光源时出错: {{str(e)}}") + + def processGUIElements(self, scene): + """处理场景中的GUI元素""" + try: + # 查找并处理2D图像 + images_2d = scene.findAllMatches("**/=gui_type=image_2d") + for img_node in images_2d: + try: + # GUI元素通常在场景加载时自动处理 + print(f"✓ 2D图像 {{img_node.getName()}} 已加载") + except Exception as e: + print(f"处理2D图像 {{img_node.getName()}} 失败: {{str(e)}}") + + except Exception as e: + print(f"处理GUI元素时出错: {{str(e)}}") + + def processSpecialElements(self, scene): + """处理特殊元素""" + try: + # 处理Cesium Tilesets + tilesets = scene.findAllMatches("**/=element_type=cesium_tileset") + for tileset_node in tilesets: + try: + # Tilesets需要特殊处理,这里只是标记 + print(f"✓ Cesium Tileset {{tileset_node.getName()}} 已识别") + except Exception as e: + print(f"处理Cesium Tileset {{tileset_node.getName()}} 失败: {{str(e)}}") + + except Exception as e: + print(f"处理特殊元素时出错: {{str(e)}}") - def setupControls(self): - """设置相机控制""" - # 启用鼠标控制 - self.accept("wheel_up", self.zoomIn) - self.accept("wheel_down", self.zoomOut) - - # 键盘控制说明 - print("\\n=== 控制说明 ===") - print("鼠标滚轮: 缩放") - print("ESC: 退出") - print("================\\n") - - # ESC键退出 - self.accept("escape", sys.exit) - - def zoomIn(self): - """放大""" - pos = self.cam.getPos() - lookAt = Point3(0, 0, 0) # 假设看向原点 - direction = (lookAt - pos).normalized() - newPos = pos + direction * 2 - self.cam.setPos(newPos) - - def zoomOut(self): - """缩小""" - pos = self.cam.getPos() - lookAt = Point3(0, 0, 0) # 假设看向原点 - direction = (lookAt - pos).normalized() - newPos = pos - direction * 2 - self.cam.setPos(newPos) + def tour(self): + mopath = ( + (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), + (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), + (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), + (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), + (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), + (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), + ) + self.controller.play_motion_path(mopath,3.0) -def main(): - """主函数""" - try: - app = {project_name.replace(' ', '').replace('-', '')}App() - app.run() - except Exception as e: - print(f"应用程序启动失败: {{str(e)}}") - import traceback - traceback.print_exc() - input("按Enter键退出...") - -if __name__ == "__main__": - main() +MainApp().run() ''' - + app_path = os.path.join(build_dir, "main.py") with open(app_path, "w", encoding="utf-8") as f: f.write(app_code) - + def _createStandardSetupFile(self, build_dir, project_name): """创建标准的setup.py文件 - 按照Panda3D官方文档""" setup_code = f'''#!/usr/bin/env python3 diff --git a/scene/scene_manager.py b/scene/scene_manager.py index f11c0832..37ee239a 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -7,6 +7,7 @@ """ import os +import shutil import time from PyQt5.QtCore import Qt @@ -165,17 +166,17 @@ class SceneManager: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") - # 特殊处理FBX模型 - # if filepath.lower().endswith('.fbx'): - # print("检测到FBX模型,应用特殊处理...") - # - # # 将模型缩放设置为原来的1/100 - # model.setScale(0.01) - # print("设置模型缩放为 0.01 (原始大小的1/100)") - # - # # 设置模型旋转为 (0, 90, 0) - # model.setHpr(0, 90, 0) - # print("设置模型旋转为 (0, 90, 0)") + #特殊处理FBX模型 + if filepath.lower().endswith('.fbx'): + print("检测到FBX模型,应用特殊处理...") + + # 将模型缩放设置为原来的1/100 + model.setScale(0.01) + print("设置模型缩放为 0.01 (原始大小的1/100)") + + # 设置模型旋转为 (0, 90, 0) + model.setHpr(0, 90, 0) + print("设置模型旋转为 (0, 90, 0)") # # 可选的单位转换(主要针对FBX # if apply_unit_conversion and filepath.lower().endswith('.fbx'): @@ -1084,6 +1085,10 @@ class SceneManager: if directory and not os.path.exists(directory): os.makedirs(directory) + resources_dir = os.path.join(directory,"resources") + if not os.path.exists(resources_dir): + os.makedirs(resources_dir) + # 存储需要临时隐藏的节点,以便保存后恢复 nodes_to_restore = [] @@ -1134,11 +1139,76 @@ class SceneManager: print(self.world.gui_elements) # 收集GUI元素信息(排除3D文本和3D图像) gui_data = [] + copied_resources = {} for gui_node in gui_elements: gui_info = self._collectGUIElementInfo(gui_node) if gui_info: + gui_type = gui_info.get("type","") + #处理2d图片 + if gui_type =="2d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + if original_path and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir,resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path,new_path) + copied_resources[original_path] = new_path + print(f"复制图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理3D图片 + elif gui_type == "3d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制3D图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制3D图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理背景图片 + if "bg_image_path" in gui_info and gui_info["bg_image_path"]: + original_path = gui_info["bg_image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制背景图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制背景图片资源失败: {original_path}, 错误: {e}") + gui_info["bg_image_path"] = new_path + + # 处理视频资源 + if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info: + original_path = gui_info["video_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制视频资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制视频资源失败: {original_path}, 错误: {e}") + gui_info["video_path"] = new_path + gui_data.append(gui_info) - print(f"添加GUI信息{gui_info['name']}") + print(f"添加GUI信息: {gui_info['name']}") # 保存GUI信息到JSON文件(确保即使没有GUI元素也创建有效的空JSON数组) try: @@ -1151,16 +1221,6 @@ class SceneManager: import traceback traceback.print_exc() - # 添加tilesets节点 - for tileset_info in self.tilesets: - if tileset_info.get('node') and not tileset_info['node'].isEmpty(): - all_nodes.append(tileset_info['node']) - - # 添加Cesium tilesets节点 - for tileset_name, tileset_info in self.cesium_integration.tilesets.items(): - if tileset_info.get('node') and not tileset_info['node'].isEmpty(): - all_nodes.append(tileset_info['node']) - # 保存所有节点的信息 for node in all_nodes: if node.isEmpty(): @@ -1436,6 +1496,10 @@ class SceneManager: #存储节点以便后续处理父子关系 loaded_nodes[nodePath.getName()] = nodePath + if nodePath.getName().startswith('ground'): + print(f"{indent}跳过ground节点: {nodePath.getName()}") + return + # 跳过render节点的递归 if nodePath.getName() == "render" and depth > 0: print(f"{indent}跳过重复的render节点") @@ -1782,6 +1846,10 @@ class SceneManager: def _shouldSkipNodeInTree(self, nodePath): """判断节点是否应该在场景树中跳过显示""" + + if nodePath.getName().startswith('ground'): + return True + # 跳过render节点的递归 if nodePath.getName() == "render": return True @@ -1805,12 +1873,13 @@ class SceneManager: return True return False + def _recreateGUIElementsFromData(self, gui_data): """根据保存的GUI数据重新创建GUI元素""" try: gui_manager = getattr(self.world, 'gui_manager', None) property_manager = getattr(self.world, 'property_panel', None) - info_panel_manager = getattr(self.world,'info_panel_manager',None) + info_panel_manager = getattr(self.world, 'info_panel_manager', None) if not gui_manager: print("GUI管理器未找到,无法重建GUI元素") return @@ -1818,7 +1887,7 @@ class SceneManager: processed_names = set() created_elements = {} - #存储原始的缩放和位置信息,用于后续计算 + # 存储原始的缩放和位置信息,用于后续计算 element_original_data = {} # 第一遍:收集所有元素信息 @@ -1832,10 +1901,10 @@ class SceneManager: valid_parents = set() for gui_info in gui_data: - name = gui_info.get("name",f"gui_element_{gui_info.get('index',0)}") + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") valid_parents.add(name) - if hasattr(self.world,'gui_elements'): + if hasattr(self.world, 'gui_elements'): for elem in self.world.gui_elements: if elem and not elem.isEmpty(): valid_parents.add(elem.getName()) @@ -1877,7 +1946,7 @@ class SceneManager: print(f" 文本: {text}") print(f" 图像路径: {image_path}") print(f" 背景图片路径: {bg_image_path}") - print(f"视频路径:{video_path}") + print(f" 视频路径: {video_path}") absolute_position = list(position) absolute_scale = list(scale) @@ -1926,9 +1995,9 @@ class SceneManager: new_element = gui_manager.createGUI2DImage( pos=tuple(absolute_position), image_path=image_path, - size=scale_value*0.2 + size=scale_value * 0.2 ) - elif gui_type == "3d_text" and hasattr(gui_manager,'createGUI3DText'): + elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'): size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 new_element = gui_manager.createGUI3DText( pos=tuple(absolute_position), @@ -1952,7 +2021,7 @@ class SceneManager: image_path=image_path, size=size ) - elif gui_type == "video_screen" and hasattr(gui_manager,'createVideoScreen'): + elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'): new_element = gui_manager.createVideoScreen( pos=tuple(absolute_position), size=absolute_scale, @@ -1961,30 +2030,24 @@ class SceneManager: if video_path and new_element: if video_path.startswith("http://") or video_path.startswith("https://"): from direct.task.TaskManagerGlobal import taskMgr + def load_video_stream_task(task): - if hasattr(property_manager,'_loadVideoFromURLWithOpenCV_3D'): - property_manager._loadVideoFromURLWithOpenCV_3D(new_element,video_path) + if hasattr(property_manager, '_loadVideoFromURLWithOpenCV_3D'): + property_manager._loadVideoFromURLWithOpenCV_3D(new_element, video_path) return task.done taskMgr.doMethodLater(0.5, load_video_stream_task, 'loadVideoStreamTask') else: - if hasattr(gui_manager,'loadVideoFile'): + if hasattr(gui_manager, 'loadVideoFile'): from direct.task.TaskManagerGlobal import taskMgr + def load_video_file_task(task): - gui_manager.loadVideoFile(new_element,video_path) + gui_manager.loadVideoFile(new_element, video_path) return task.done - taskMgr.doMethodLater(0.1,load_video_file_task,'loadVideoFileTask') - # if video_path and new_element and hasattr(gui_manager, 'loadVideoFile'): - # # 延迟一帧执行,确保节点完全初始化 - # from direct.task.TaskManagerGlobal import taskMgr - # def load_video_task(task): - # gui_manager.loadVideoFile(new_element, video_path) - # return task.done - # - # taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask') + taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask') - elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'): + elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): new_element = gui_manager.createGUI2DVideoScreen( pos=tuple(absolute_position), size=absolute_scale, @@ -2000,13 +2063,14 @@ class SceneManager: # return task.done # taskMgr.doMethodLater(0.1,load_2d_video_stream_task,'load2DVideoStreamTask') else: - if hasattr(property_manager,'load2DVideoFile'): + if hasattr(property_manager, 'load2DVideoFile'): from direct.task.TaskManagerGlobal import taskMgr - def load_2d_video_file_task(task): - property_manager.load2DVideoFile(new_element,video_path) - return task.done - taskMgr.doMethodLater(0.1,load_2d_video_file_task,'load2DVideoFileTask') + def load_2d_video_file_task(task): + property_manager.load2DVideoFile(new_element, video_path) + return task.done + + taskMgr.doMethodLater(0.1, load_2d_video_file_task, 'load2DVideoFileTask') # 如果创建成功,设置属性 if new_element: @@ -2048,84 +2112,84 @@ class SceneManager: traceback.print_exc() continue - # 第二遍:设置父子级关系并更新Qt树 - print("开始设置父子级关系...") - try: - # 创建父子级关系映射 - parent_child_map = {} - for gui_info in gui_data: - name = gui_info.get("name") - parent_name = gui_info.get("parent_name") + # 第二遍:设置父子级关系并更新Qt树 + print("开始设置父子级关系...") + try: + # 创建父子级关系映射 + parent_child_map = {} + for gui_info in gui_data: + name = gui_info.get("name") + parent_name = gui_info.get("parent_name") - if name and parent_name and parent_name in created_elements: - parent_child_map[name] = parent_name - print(f"父子级关系映射: {parent_name} -> {name}") + if name and parent_name and parent_name in created_elements: + parent_child_map[name] = parent_name + print(f"父子级关系映射: {parent_name} -> {name}") - # 按正确的顺序设置父子级关系并更新Qt树 - tree_widget = self._get_tree_widget() - if tree_widget: - # 先将所有元素添加到Qt树中 - qt_tree_items = {} - for name, element in created_elements.items(): - # 尝试在Qt树中找到对应的项,如果找不到则创建 - qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) - if qt_item: - qt_tree_items[name] = qt_item + # 按正确的顺序设置父子级关系并更新Qt树 + tree_widget = self._get_tree_widget() + if tree_widget: + # 先将所有元素添加到Qt树中 + qt_tree_items = {} + for name, element in created_elements.items(): + # 尝试在Qt树中找到对应的项,如果找不到则创建 + qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) + if qt_item: + qt_tree_items[name] = qt_item - # 然后设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] + # 然后设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") - # 更新Qt树显示 - if child_name in qt_tree_items and parent_name in qt_tree_items: - child_item = qt_tree_items[child_name] - parent_item = qt_tree_items[parent_name] + # 更新Qt树显示 + if child_name in qt_tree_items and parent_name in qt_tree_items: + child_item = qt_tree_items[child_name] + parent_item = qt_tree_items[parent_name] - # 从当前位置移除子项 - if child_item.parent(): - child_item.parent().removeChild(child_item) - else: - # 如果是顶级项,从树中移除 - index = tree_widget.indexOfTopLevelItem(child_item) - if index >= 0: - tree_widget.takeTopLevelItem(index) + # 从当前位置移除子项 + if child_item.parent(): + child_item.parent().removeChild(child_item) + else: + # 如果是顶级项,从树中移除 + index = tree_widget.indexOfTopLevelItem(child_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) - # 将子项添加到新的父项下 - parent_item.addChild(child_item) - print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") - else: - print(f"元素 {child_name} 不支持 reparentTo 操作") + # 将子项添加到新的父项下 + parent_item.addChild(child_item) + print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") else: - print(f"元素未找到: 父级={parent_name}, 子级={child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue - else: - # 如果没有tree_widget,只设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] + print(f"元素 {child_name} 不支持 reparentTo 操作") + else: + print(f"元素未找到: 父级={parent_name}, 子级={child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + else: + # 如果没有tree_widget,只设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue - except Exception as e: - print(f"设置父子级关系时出错: {e}") + except Exception as e: + print(f"设置父子级关系时出错: {e}") # 第三遍:重新挂载脚本 print("开始重新挂载脚本...") for gui_info in gui_data: @@ -2173,13 +2237,15 @@ class SceneManager: else: print(f"为 {name} 添加脚本失败: {script_name}") except Exception as e: - print(f"重新挂载脚本失败 {gui_info.get('name', 'unknown')}: {e}") + print(f"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() continue - print("GUI元素重建完成") + print(f"GUI元素重建完成,共创建 {len(created_elements)} 个元素") except Exception as e: - print(f"重建GUI元素时发生错误: {e}") + print(f"重建GUI元素时出错: {e}") import traceback traceback.print_exc() diff --git a/ui/interface_manager.py b/ui/interface_manager.py index 08a8284a..959e3201 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -51,7 +51,7 @@ class InterfaceManager: def onTreeItemClicked(self, item, column): """处理树形控件项目点击事件""" - print(f"树形控件点击事件触发,item: {item}, column: {column}") + #print(f"树形控件点击事件触发,item: {item}, column: {column}") # 检查是否点击了空白区域 # 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象 diff --git a/ui/main_window.py b/ui/main_window.py index ecb46dfd..daf4096e 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -452,8 +452,7 @@ class MainWindow(QMainWindow): select_icon = get_icon('select_tool', QSize(16, 16)) if not select_icon.isNull(): self.selectTool.setIcon(select_icon) - else: - self.selectTool.setText('选择') # 如果没有图标则显示文字 + self.selectTool.setText('选择') # 如果没有图标则显示文字 self.selectTool.setIconSize(QSize(16, 16)) self.selectTool.setCheckable(True) self.selectTool.setToolTip("选择工具 (Q)") @@ -817,11 +816,11 @@ class MainWindow(QMainWindow): """设置视图菜单动作""" # 连接视图菜单事件 self.viewPerspectiveAction.triggered.connect(self.onViewPerspective) - self.viewTopAction.triggered.connect(self.onViewTop) - self.viewFrontAction.triggered.connect(self.onViewFront) + #self.viewTopAction.triggered.connect(self.onViewTop) + #self.viewFrontAction.triggered.connect(self.onViewFront) self.viewOrthographicAction = self.viewMenu.addAction('正交视图') # 添加正交视图动作 - self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) - self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接 + #self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) + #self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接 # 保存原始相机设置 self._original_camera_fov = 80 diff --git a/ui/property_panel.py b/ui/property_panel.py index cf0942f6..2ee965f4 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -12,6 +12,7 @@ from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor from direct.gui import DirectGui +from direct.task.TaskManagerGlobal import taskMgr from idna import check_label from jinja2.compiler import has_safe_repr from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3 @@ -8615,12 +8616,35 @@ except Exception as e: actor = self._getActor(origin_model) if not actor: return + + original_world_pos = origin_model.getPos(self.world.render) + original_world_hpr = origin_model.getHpr(self.world.render) + original_world_scale = origin_model.getScale(self.world.render) + actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) + origin_model.hide() actor.show() + #创建人物来维持世界坐标不变 + def maintainWorldPosition(task): + try: + if not actor.isEmpty(): + actor.setPos(self.world.render,original_world_pos) + actor.setHpr(self.world.render,original_world_hpr) + actor.setScale(self.world.render,original_world_scale) + return task.cont + else: + return task.done + except: + return task.done + + taskMgr.add(maintainWorldPosition,f"maintain_anim_pos_{id(actor)}") + + + if hasattr(self, 'animation_combo'): # 获取原始动画名称(存储在 userData 中) current_index = self.animation_combo.currentIndex()