diff --git a/QPanda3D/QPanda3DWidget.py b/QPanda3D/QPanda3DWidget.py index 249fc355..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 @@ -32,9 +33,17 @@ class QPanda3DSynchronizer(QTimer): self.setInterval(int(dt)) self.timeout.connect(self.tick) + # def tick(self): + # taskMgr.step() + # self.qPanda3DWidget.update() + def tick(self): - taskMgr.step() - self.qPanda3DWidget.update() + try: + taskMgr.step() + self.qPanda3DWidget.update() + except: + # 静默处理所有异常,包括 has_mat() 断言错误 + pass def get_panda_key_modifiers(evt): diff --git a/RenderPipelineFile/rpcore/native/__init__.py b/RenderPipelineFile/rpcore/native/__init__.py index 16fa5411..a2f219a3 100644 --- a/RenderPipelineFile/rpcore/native/__init__.py +++ b/RenderPipelineFile/rpcore/native/__init__.py @@ -73,7 +73,6 @@ native_module = None # If the module was built, use it, otherwise use the python wrappers if NATIVE_CXX_LOADED: - print(f'12121212121212121212121212') try: from panda3d import _rplight as _native_module # pylint: disable=wrong-import-position RPObject.global_debug("CORE", "Using panda3d-supplied core module") 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/Resources/models/DancingTwerk.glb b/Resources/models/DancingTwerk.glb new file mode 100644 index 00000000..1567bf93 Binary files /dev/null and b/Resources/models/DancingTwerk.glb differ diff --git a/Resources/models/Haqijingzhu.glb b/Resources/models/Haqijingzhu.glb new file mode 100644 index 00000000..0b5e1df0 Binary files /dev/null and b/Resources/models/Haqijingzhu.glb differ diff --git a/Resources/models/JQB_auto_converted.glb b/Resources/models/JQB_auto_converted.glb new file mode 100644 index 00000000..89df9e5b Binary files /dev/null and b/Resources/models/JQB_auto_converted.glb differ diff --git a/core/Command_System.py b/core/Command_System.py new file mode 100644 index 00000000..fd8d245b --- /dev/null +++ b/core/Command_System.py @@ -0,0 +1,648 @@ +from abc import ABC, abstractmethod +from collections import deque +from typing import List +from panda3d.core import NodePath, Point3 + + +class Command(ABC): + """ + 抽象命令类,所有具体命令都需要继承此类 + """ + + @abstractmethod + def execute(self): + """ + 执行命令 + """ + pass + + @abstractmethod + def undo(self): + """ + 撤销命令 + """ + pass + + @abstractmethod + def redo(self): + """ + 重做命令 + """ + pass + + +class CommandManager: + """ + 命令管理器,负责管理命令的执行、撤销和重做 + """ + + def __init__(self, max_history: int = 100): + # 用于存储已执行的命令的历史记录 + self._undo_stack: deque = deque(maxlen=max_history) + # 用于存储已撤销的命令,支持重做 + self._redo_stack: deque = deque(maxlen=max_history) + # 最大历史记录数 + self._max_history = max_history + + def execute_command(self, command: Command): + """ + 执行命令,并将其添加到撤销栈中 + """ + try: + command.execute() + self._undo_stack.append(command) + # 清空重做栈,因为执行新命令后就无法重做之前的命令了 + self._redo_stack.clear() + except Exception as e: + print(f"执行命令时出错: {e}") + raise + + def undo(self) -> bool: + """ + 撤销上一个命令 + 返回是否成功撤销 + """ + if not self._undo_stack: + return False + + try: + command = self._undo_stack.pop() + command.undo() + self._redo_stack.append(command) + return True + except Exception as e: + print(f"撤销命令时出错: {e}") + # 如果撤销失败,将命令放回撤销栈 + self._undo_stack.append(command) + return False + + def redo(self) -> bool: + """ + 重做上一个被撤销的命令 + 返回是否成功重做 + """ + if not self._redo_stack: + return False + + try: + command = self._redo_stack.pop() + command.redo() + self._undo_stack.append(command) + return True + except Exception as e: + print(f"重做命令时出错: {e}") + # 如果重做失败,将命令放回重做栈 + self._redo_stack.append(command) + return False + + def can_undo(self) -> bool: + """ + 检查是否可以撤销 + """ + return len(self._undo_stack) > 0 + + def can_redo(self) -> bool: + """ + 检查是否可以重做 + """ + return len(self._redo_stack) > 0 + + def clear_history(self): + """ + 清空所有历史记录 + """ + self._undo_stack.clear() + self._redo_stack.clear() + + def get_undo_count(self) -> int: + """ + 获取可撤销的命令数量 + """ + return len(self._undo_stack) + + def get_redo_count(self) -> int: + """ + 获取可重做的命令数量 + """ + return len(self._redo_stack) + + +# 示例命令实现 +class MoveNodeCommand(Command): + """ + 移动节点命令示例 + """ + + def __init__(self, node: NodePath, old_pos, new_pos): + self.node = node + self.old_pos = old_pos + self.new_pos = new_pos + + def execute(self): + """ + 执行移动操作 + """ + self.node.setPos(self.new_pos) + + def undo(self): + """ + 撤销移动操作 + """ + self.node.setPos(self.old_pos) + + def redo(self): + """ + 重做移动操作 + """ + self.node.setPos(self.new_pos) + + +class DeleteNodeCommand(Command): + """ + 删除节点命令示例 + """ + + def __init__(self, node: NodePath, parent_node: NodePath,world=None): + self.node = node + self.parent_node = parent_node + self.world = world + + self.node_name = node.getName() + self.node_pos = node.getPos() + self.node_hpr = node.getHpr() + self.node_scale = node.getScale() + + # 保存节点类型信息 + self.node_type = "NODE" + if node.hasTag("tree_item_type"): + self.node_type = node.getTag("tree_item_type") + elif node.hasTag("gui_type"): + gui_type = node.getTag("gui_type") + if gui_type == "button": + self.node_type = "GUI_BUTTON" + elif gui_type == "label": + self.node_type = "GUI_LABEL" + elif gui_type == "entry": + self.node_type = "GUI_ENTRY" + elif gui_type == "2d_image": + self.node_type = "GUI_IMAGE" + elif gui_type == "3d_text": + self.node_type = "GUI_3DTEXT" + elif gui_type == "3d_image": + self.node_type = "GUI_3DIMAGE" + elif gui_type == "video_screen": + self.node_type = "GUI_VIDEO_SCREEN" + elif gui_type == "2d_video_screen": + self.node_type = "GUI_2D_VIDEO_SCREEN" + elif node.hasTag("light_type"): + self.node_type = "LIGHT_NODE" + elif node.hasTag("element_type") and node.getTag("element_type") == "cesium_tileset": + self.node_type = "CESIUM_TILESET_NODE" + elif node.hasTag("is_scene_element"): + self.node_type = "SCENE_NODE" + + self.node_tags = {} + if hasattr(node,'hasTag'): + for tag_key in node.getNetTag('tags').split(',') if node.hasTag('tags') else []: + if node.hasTag(tag_key): + self.node_tags[tag_key] = node.getTag(tag_key) + else: + try: + if hasattr(node,'getTag'): + common_tags = ['is_scene_element','tree_item_type','gui_type','light_type', + 'element_type','file','model_path','video_path','image_path', + 'gui_text','name','created_by_user'] + for tag in common_tags: + if node.hasTag(tag): + self.node_tags[tag] = node.getTag(tag) + except: + pass + + self.node_python_tags = {} + if hasattr(node,'getPythonTagKeys'): + try: + for tag_key in node.getPythonTagKeys(): + self.node_python_tags[tag_key] = node.getPythonTag(tag_key) + except Exception as e: + pass + + # 对于特定类型的节点,保存额外的数据 + self.extra_data = {} + if self.node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN", "GUI_2D_VIDEO_SCREEN"]: + if node.hasTag("gui_text"): + self.extra_data["gui_text"] = node.getTag("gui_text") + if node.hasTag("video_path"): + self.extra_data["video_path"] = node.getTag("video_path") + if node.hasTag("image_path"): + self.extra_data["image_path"] = node.getTag("image_path") + elif self.node_type == "LIGHT_NODE": + if node.hasTag("light_type"): + self.extra_data["light_type"] = node.getTag("light_type") + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + self.extra_data["light_data"] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else 70, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else 0.4, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True), + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) + } + elif self.node_type == "CESIUM_TILESET_NODE": + if node.hasTag("tileset_url"): + self.extra_data["tileset_url"] = node.getTag("tileset_url") + + def execute(self): + """ + 执行删除操作 + """ + # 从world的相应列表中移除节点引用 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + if self.node_type == "LIGHT_NODE": + if self.node.hasTag("light_type"): + light_type = self.node.getTag("light_type") + if light_type == "spot_light" and hasattr(scene_manager, + 'Spotlight') and self.node in scene_manager.Spotlight: + scene_manager.Spotlight.remove(self.node) + elif light_type == "point_light" and hasattr(scene_manager, + 'Pointlight') and self.node in scene_manager.Pointlight: + scene_manager.Pointlight.remove(self.node) + elif self.node_type == "IMPORTED_MODEL_NODE" and hasattr(scene_manager, + 'models') and self.node in scene_manager.models: + scene_manager.models.remove(self.node) + elif self.node_type.startswith("GUI_") and hasattr(self.world, + 'gui_elements') and self.node in self.world.gui_elements: + self.world.gui_elements.remove(self.node) + elif self.node_type == "CESIUM_TILESET_NODE": + # 从tilesets列表中移除 + if hasattr(scene_manager, 'tilesets'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(scene_manager.tilesets): + if tileset_info.get('node') == self.node: + tilesets_to_remove.append(i) + for i in reversed(tilesets_to_remove): + del scene_manager.tilesets[i] + + # 从场景图中移除节点 + if self.node and not self.node.isEmpty(): + self.node.removeNode() + + def undo(self): + """ + 撤销删除操作(重新创建节点) + """ + try: + # 使用场景管理器重建节点 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + + # 创建节点数据字典 + node_data = { + 'name': self.node_name, + 'node_type': self.node_type, + 'pos': (self.node_pos.x, self.node_pos.y, self.node_pos.z), + 'hpr': (self.node_hpr.x, self.node_hpr.y, self.node_hpr.z), + 'scale': (self.node_scale.x, self.node_scale.y, self.node_scale.z), + 'tags': self.node_tags + } + + # 添加额外数据 + if self.extra_data: + if self.node_type.startswith("GUI_"): + node_data['gui_data'] = self.extra_data + elif self.node_type == "LIGHT_NODE": + node_data['light_data'] = self.extra_data.get('light_data', {}) + elif self.node_type == "CESIUM_TILESET_NODE": + node_data['tileset_url'] = self.extra_data.get('tileset_url', '') + + # 重建节点 + new_node = scene_manager.recreateNodeFromData(node_data, self.parent_node) + + if new_node: + print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") + # 更新节点引用 + self.node = new_node + else: + print(f"❌ 撤销删除操作失败,无法重建节点 {self.node_name}") + else: + print("❌ 无法撤销删除操作,缺少场景管理器引用") + + except Exception as e: + print(f"❌ 撤销删除操作时出错: {e}") + import traceback + traceback.print_exc() + + def redo(self): + """ + 重做删除操作 + """ + self.execute() + + +class RotateNodeCommand(Command): + """ + 旋转节点命令 + """ + + def __init__(self, node: NodePath, old_hpr, new_hpr): + self.node = node + self.old_hpr = old_hpr + self.new_hpr = new_hpr + + def execute(self): + """ + 执行旋转操作 + """ + self.node.setHpr(self.new_hpr) + + def undo(self): + """ + 撤销旋转操作 + """ + self.node.setHpr(self.old_hpr) + + def redo(self): + """ + 重做旋转操作 + """ + self.node.setHpr(self.new_hpr) + + +class ScaleNodeCommand(Command): + """ + 缩放节点命令 + """ + + def __init__(self, node: NodePath, old_scale, new_scale): + self.node = node + self.old_scale = old_scale + self.new_scale = new_scale + + def execute(self): + """ + 执行缩放操作 + """ + self.node.setScale(self.new_scale) + + def undo(self): + """ + 撤销缩放操作 + """ + self.node.setScale(self.old_scale) + + def redo(self): + """ + 重做缩放操作 + """ + self.node.setScale(self.new_scale) + + +class CreateNodeCommand(Command): + """ + 创建节点命令 + """ + + def __init__(self, node_creator_func,parent_node, *args, **kwargs): + self.node_creator_func = node_creator_func + self.parent_node = parent_node + self.args = args + self.kwargs = kwargs + self.created_node = None + + def execute(self): + """ + 执行创建节点操作 + """ + self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs) + return self.created_node + + def undo(self): + """ + 撤销创建节点操作 + """ + if self.created_node: + self.created_node.removeNode() + + def redo(self): + """ + 重做创建节点操作 + """ + self.execute() + + +class ReparentNodeCommand(Command): + """ + 重新设置节点父子关系命令 - 增强版(同时处理Panda3D和Qt树) + """ + + 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()) + self.world_scale = node.getScale(self.world.render if self.world else node.getParent()) + # 同时保存局部坐标,因为在父节点改变后可能需要恢复 + self.local_pos = node.getPos() + 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(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + def undo(self): + """ + 撤销重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + 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(): + if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1": + # 原父节点是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.old_parent) + else: + # 原父节点是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d下") + else: + # 如果原父节点为空,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d") + else: + # 普通3D节点的处理 + if self.old_parent and not self.old_parent.isEmpty(): + self.node.wrtReparentTo(self.old_parent) + else: + # 如果原父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + def redo(self): + """ + 重做重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + 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(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + 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/InfoPanelManager.py b/core/InfoPanelManager.py index 232e2139..35000ffe 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -145,7 +145,7 @@ class InfoPanelManager(DirectObject): text_scale=0.045, text_fg=content_color, text_align=TextNode.ALeft, - text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行 + text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行 pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05), parent=panel_node, relief=None, @@ -529,9 +529,7 @@ class InfoPanelManager(DirectObject): -size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05 ) - # 设置一个非常大的换行值,几乎不自动换行 - panel_data['content_label']['text_wordwrap'] = 500 - print(f"更新面板换行: 设置为500(几乎不换行)") + panel_data['content_label']['text_wordwrap'] = 0 # 如果有背景图片,也需要更新其大小 if 'bg_image' in panel_data and panel_data['bg_image']: @@ -575,8 +573,9 @@ class InfoPanelManager(DirectObject): if 'content_size' in properties: panel_data['content_label']['text_scale'] = properties['content_size'] props['content_size'] = properties['content_size'] + current_size = props.get('size',(1.0,0.6)) # 当字体大小改变时,仍然保持较大的换行值 - panel_data['content_label']['text_wordwrap'] = 500 + panel_data['content_label']['text_wordwrap'] = 0 # 更新背景图片 if 'bg_image' in properties: 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/script_system.py b/core/script_system.py index 2191077c..d9aa465c 100644 --- a/core/script_system.py +++ b/core/script_system.py @@ -253,7 +253,7 @@ class ScriptLoader: for component in components_to_remove: self.script_manager.remove_script_from_object(component.game_object, script_name) - + # 从sys.modules中移除 module = self.loaded_modules[script_name] if module.__name__ in sys.modules: @@ -696,18 +696,62 @@ class {class_name}(ScriptBase): return False script_components = self.object_scripts[game_object] + removed = False + for component in script_components[:]: # 复制列表以避免修改时出错 if component.script_instance.__class__.__name__ == script_name: # 从引擎移除 self.engine.remove_script_component(component) # 从对象脚本列表移除 script_components.remove(component) + removed = True print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}") - return True + + + if not script_components: + del self.object_scripts[game_object] + + # 更新节点上保存的脚本信息标签 + if removed: + self._update_node_script_tags_after_removal(game_object, script_name) - return False - + return removed + + def _update_node_script_tags_after_removal(self, game_object, removed_script_name): + """在移除脚本后更新节点标签""" + try: + # 获取对象上剩余的脚本 + remaining_scripts = self.get_scripts_on_object(game_object) + + if not remaining_scripts: + # 如果没有其他脚本,清除所有脚本标签 + if game_object.hasTag("has_scripts"): + game_object.clearTag("has_scripts") + if game_object.hasTag("scripts_info"): + game_object.clearTag("scripts_info") + print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签") + else: + # 如果还有其他脚本,更新脚本信息标签 + script_info_list = [] + for script_component in remaining_scripts: + script_name = script_component.script_name + script_class = script_component.script_instance.__class__ + script_file = self.loader.find_script_file(script_name) or "" + + script_info_list.append({ + "name": script_name, + "file": script_file + }) + + import json + game_object.setTag("has_scripts", "true") + game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) + print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本") + + except Exception as e: + print(f"更新节点标签失败: {e}") + def get_scripts_on_object(self, game_object) -> List[ScriptComponent]: """获取对象上的所有脚本""" return self.object_scripts.get(game_object, []) diff --git a/core/selection.py b/core/selection.py index c84b8eee..281a36aa 100644 --- a/core/selection.py +++ b/core/selection.py @@ -16,7 +16,6 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta from direct.task.TaskManagerGlobal import taskMgr import math - class SelectionSystem: """选择和变换系统类""" @@ -64,6 +63,13 @@ class SelectionSystem: "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 } + #性能优化相关 + self._optimized_node = False + self._last_update_time = 0 + self._cached_bounds = {} + self._gizmo_update_interval = 0.1 + self._selection_box_update_interval = 0.2 + self._current_cursor = None self._default_cursor = None @@ -118,11 +124,8 @@ class SelectionSystem: def createSelectionBox(self, nodePath): """为选中的节点创建选择框""" try: - #print(f" 开始创建选择框,目标节点: {nodePath.getName()}") - - # 如果已有选择框,先移除 if self.selectionBox: - print(" 移除现有选择框") + #print(" 移除现有选择框") self.selectionBox.removeNode() self.selectionBox = None @@ -130,21 +133,12 @@ class SelectionSystem: print(" 目标节点为空,取消创建") return - # 创建选择框作为render的子节点,但会实时跟踪目标节点 self.selectionBox = self.world.render.attachNewNode("selectionBox") - self.selectionBoxTarget = nodePath # 保存目标节点引用 - #print(f" 选择框节点创建完成: {self.selectionBox}") + self.selectionBoxTarget = nodePath - # 启动选择框更新任务 taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - #print(" 选择框更新任务已启动") - - # 初始更新选择框 - #print(" 开始初始化选择框几何体...") self.updateSelectionBoxGeometry() - #print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") - except Exception as e: print(f" ✗ 创建选择框失败: {str(e)}") import traceback @@ -277,51 +271,58 @@ class SelectionSystem: traceback.print_exc() def updateSelectionBoxTask(self, task): - """选择框更新任务""" + """选择框更新任务 - 平衡性能和实时性""" try: - if not hasattr(self,'_last_selection_box_update'): + update_interval = 0.1 + + if not hasattr(self, '_last_selection_box_update'): self._last_selection_box_update = 0 import time current_time = time.time() - if current_time - self._last_selection_box_update < 0.1: + if current_time - self._last_selection_box_update < update_interval: return task.cont self._last_selection_box_update = current_time - #检查目标节点是否已被删除 + # 检查目标节点是否已被删除 self.checkAndClearIfTargetDeleted() if not self.selectionBox or not self.selectionBoxTarget: - return task.done # 结束任务 + return task.done # 检查目标节点是否还存在 if self.selectionBoxTarget.isEmpty(): self.clearSelectionBox() return task.done - # 获取目标节点在世界坐标系中的当前边界框(使用正确的API) - currentMinPoint = Point3() - currentMaxPoint = Point3() - if not self.selectionBoxTarget.calcTightBounds(currentMinPoint, currentMaxPoint, self.world.render): - return task.cont + # 检查目标节点是否发生了变化(位置、旋转、缩放) + current_transform = self._getNodeTransformKey(self.selectionBoxTarget) - # 检查边界框是否发生变化(位置或大小) - if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or - self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint): - - # 更新选择框几何体 + if (not hasattr(self, '_last_transform_key') or + self._last_transform_key != current_transform): + # 节点发生了变化,更新选择框 self.updateSelectionBoxGeometry() + self._last_transform_key = current_transform - # 保存当前边界框信息 - self._lastMinPoint = currentMinPoint - self._lastMaxPoint = currentMaxPoint - - return task.cont # 继续任务 + return task.cont except Exception as e: print(f"选择框更新任务出错: {str(e)}") return task.done + def _getNodeTransformKey(self, node): + """获取节点变换的关键信息,用于快速比较""" + try: + # 获取节点的关键变换信息 + pos = node.getPos(self.world.render) + hpr = node.getHpr(self.world.render) + scale = node.getScale(self.world.render) + + # 返回一个可以比较的元组 + return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z) + except: + return None + def clearSelectionBox(self): """清除选择框""" if self.selectionBox: @@ -905,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), @@ -921,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) @@ -1514,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) # 坐标轴的世界位置 # 添加对缩放的支持:保存初始缩放值 @@ -1886,6 +1882,43 @@ class SelectionSystem: """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") + if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: + current_pos = self.gizmoTarget.getPos() + + if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and + (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or + 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 + 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 + self.gizmoTargetStartScale): + current_scale = self.gizmoTarget.getScale() + if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or + abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or + abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): + from core.Command_System import ScaleNodeCommand + command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) + self.world.command_manager.execute_command(command) + # 如果是旋转操作且旋转发生了变化,则创建旋转命令 + elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and + self.gizmoTargetStartHpr): + current_hpr = self.gizmoTarget.getHpr() + if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or + abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or + abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): + from core.Command_System import RotateNodeCommand + command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) + self.world.command_manager.execute_command(command) + # 恢复所有轴的颜色 for axis_name in ["x", "y", "z"]: self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) @@ -1909,31 +1942,20 @@ class SelectionSystem: def updateSelection(self, nodePath): try: - # 检查是否要选择的对象已经是当前选中的对象 if self.selectedNode == nodePath: - #print("要选择的对象已经是当前选中的对象,跳过重复更新") 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}") - - # 检查是否为双击 - is_double_click = self.checkDoubleClick(nodePath) - if is_double_click: - print(f"检测到双击 {node_name},执行聚焦") - # 双击时直接执行聚焦,不执行选择逻辑 - self.focusCameraOnSelectedNodeAdvanced() - print("=== 选择状态更新完成 ===\n") - return # 直接返回,不执行下面的选择逻辑 + #print(f"新选择的节点: {node_name}") self.selectedNode = nodePath # 添加兼容性属性 @@ -1965,7 +1987,6 @@ class SelectionSystem: else: print("× 坐标轴创建失败") - print(f"✓ 选中了节点: {node_name}") else: print("清除选择...") self.clearSelectionBox() @@ -1979,12 +2000,69 @@ 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: + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + + # 创建选择框 + print("创建选择框...") + self.createSelectionBox(nodePath) + if self.selectionBox: + box_name = "Unknown" + if self.selectionBox and not self.selectionBox.isEmpty(): + box_name = self.selectionBox.getName() + print(f"✓ 选择框创建成功: {box_name}") + else: + print("× 选择框创建失败") + + # 创建坐标轴 + print("创建坐标轴...") + self.createGizmo(nodePath) + if self.gizmo: + gizmo_name = "Unknown" + if self.gizmo and not self.gizmo.isEmpty(): + gizmo_name = self.gizmo.getName() + print(f"✓ 坐标轴创建成功: {gizmo_name}") + else: + print("× 坐标轴创建失败") + + print(f"✓ 选中了节点: {node_name}") + else: + print("清除选择...") + self.clearSelectionBox() + self.clearGizmo() + print("✓ 取消选择") + + except Exception as e: + print(f"更新选择视觉效果失败: {e}") + def getSelectedNode(self): """获取当前选中的节点""" return self.selectedNode @@ -2173,8 +2251,31 @@ class SelectionSystem: maxPoint = Point3() if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): - print("无法计算选中节点的边界框") - return False + print("无法计算选中节点的边界框,使用节点为位置作为替代方案") + node_pos = self.selectedNode.getPos(self.world.render) + optimal_distance = 10.0 + current_cam_pos = self.world.cam.getPos() + view_direction = node_pos - current_cam_pos + if view_direction.length()<0.001: + view_direction = Vec3(5,-5,2) + view_direction.normalize() + target_cam_pos = node_pos - (view_direction * optimal_distance) + + temp_node =self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(node_pos) + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + currrent_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) + print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}") + return True # 计算节点中心点和大小 center = Point3( @@ -2202,7 +2303,7 @@ class SelectionSystem: view_direction.normalize() # 计算合适的观察距离 - optimal_distance = max(size * 2.0, 5.0) + optimal_distance = max(size * 1, 1.0) # 计算目标摄像机位置 target_cam_pos = center + (view_direction * optimal_distance) @@ -2341,100 +2442,6 @@ class SelectionSystem: traceback.print_exc() return task.done - def focusCameraOnSelectedNode(self): - """将摄像机聚焦到选中的节点(无动画版本,但仍保持平滑转向)""" - try: - if not self.selectedNode or self.selectedNode.isEmpty(): - print("没有选中的节点,无法聚焦") - return False - - # 获取选中节点的边界框 - minPoint = Point3() - maxPoint = Point3() - - if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): - print("无法计算选中节点的边界框") - return False - - # 计算节点中心点 - center = Point3( - (minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5 - ) - - # 计算节点的大小(直径) - size = (maxPoint - minPoint).length() - - # 如果节点太小,使用默认大小 - if size < 0.1: - size = 5.0 - - # 获取当前摄像机位置 - current_cam_pos = Point3(self.world.cam.getPos()) - - # 计算观察方向 - view_direction = current_cam_pos - center - if view_direction.length() < 0.001: - # 如果摄像机正好在中心点,使用默认方向 - view_direction = Vec3(5, -5, 2) # 默认观察方向 - - # 标准化方向向量 - view_direction.normalize() - - # 计算合适的距离(基于节点大小) - optimal_distance = max(size * 3.0, 10.0) # 距离节点的距离是节点大小的3倍或至少10个单位 - - # 计算新的摄像机位置 - new_cam_pos = center + (view_direction * optimal_distance) - - # 平滑地设置摄像机位置和朝向 - # 创建临时节点用于计算目标朝向 - temp_lookat = self.world.render.attachNewNode("temp_lookat") - temp_lookat.setPos(center) - - # 获取当前朝向和目标朝向 - current_hpr = Vec3(self.world.cam.getHpr()) - - # 设置摄像机到目标位置 - self.world.cam.setPos(new_cam_pos) - self.world.cam.lookAt(temp_lookat) - target_hpr = Vec3(self.world.cam.getHpr()) - - # 恢复当前位置 - self.world.cam.setPos(current_cam_pos) - self.world.cam.setHpr(current_hpr) - - # 清理临时节点 - temp_lookat.removeNode() - - # 使用一个简单的任务来平滑过渡 - class SmoothCameraMoveData: - def __init__(self, start_pos, end_pos, start_hpr, end_hpr): - self.start_pos = Point3(start_pos) - self.end_pos = Point3(end_pos) - self.start_hpr = Vec3(start_hpr) - self.end_hpr = Vec3(end_hpr) - self.elapsed_time = 0.0 - self.duration = 0.5 # 0.5秒的平滑过渡 - - self._smooth_camera_move_data = SmoothCameraMoveData( - current_cam_pos, new_cam_pos, current_hpr, target_hpr - ) - - taskMgr.remove("smoothCameraMoveTask") - taskMgr.add(self._smoothCameraMoveTask, "smoothCameraMoveTask") - - print(f"摄像机开始聚焦到节点: {self.selectedNode.getName()}") - print(f"节点中心: {center}, 大小: {size:.2f}") - return True - - except Exception as e: - print(f"聚焦摄像机到选中节点失败: {str(e)}") - import traceback - traceback.print_exc() - return False - def _smoothCameraMoveTask(self, task): """平滑摄像机移动任务""" try: @@ -2529,6 +2536,7 @@ class SelectionSystem: # 检查是否为双击(同一节点且在时间阈值内) is_double_click = (self._last_clicked_node == target_node and + target_node is not None and current_time - self._last_click_time < self._double_click_threshold) if is_double_click: @@ -2539,8 +2547,12 @@ class SelectionSystem: # 无论是点击模型还是坐标轴,都执行聚焦 if target_node and not target_node.isEmpty(): print(f"双击聚焦到节点: {target_node.getName()}") - # 执行聚焦 - self.focusCameraOnSelectedNodeAdvanced() + if self.selectedNode != target_node: + self.updateSelection(target_node) + else: + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") # 重置状态以避免三击等误触发 self._last_click_time = 0 @@ -2598,21 +2610,20 @@ class SelectionSystem: import time current_time = time.time() - # 检查节点和时间 - time_diff = current_time - self._last_click_time - is_same_node = (self._last_clicked_node == nodePath) + # 必须是同一节点且在时间阈值内 + is_double_click = (self._last_clicked_node == nodePath and + nodePath is not None and + current_time - self._last_click_time < self._double_click_threshold) - # 如果是同一节点且在时间阈值内,认为是双击 - if is_same_node and time_diff < self._double_click_threshold: - # 只有在双击时才重置状态 + if is_double_click: + # 重置状态 self._last_click_time = 0 self._last_clicked_node = None return True else: - # 只有在非双击情况下才更新状态 - if not is_same_node: - self._last_click_time = current_time - self._last_clicked_node = nodePath + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath return False except Exception as e: @@ -2786,4 +2797,23 @@ class SelectionSystem: # 清理其他资源 self.clearSelectionBox() - self.clearGizmo() \ No newline at end of file + self.clearGizmo() + def clearSelection(self): + """清除当前选择""" + try: + self.selectedNode = None + self.selectedObject = None + self.clearSelectionBox() + self.clearGizmo() + + # 清除树形控件中的选择 + if (hasattr(self.world, 'interface_manager') and + self.world.interface_manager and + hasattr(self.world.interface_manager, 'treeWidget') and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + + print("已清除选择") + except Exception as e: + print(f"清除选择失败: {e}") + diff --git a/core/world.py b/core/world.py index 0f4ec28a..4f0ef3a3 100644 --- a/core/world.py +++ b/core/world.py @@ -323,8 +323,8 @@ class CoreWorld(Panda3DWorld): self.ground.setP(-90) self.ground.setZ(-1.0) self.ground.setColor(0.8, 0.8, 0.8, 1) - # self.ground.setTag("is_scene_element", "1") - # self.ground.setTag("tree_item_type", "SCENE_NODE") + self.ground.setTag("is_scene_element", "1") + self.ground.setTag("tree_item_type", "SCENE_NODE") # 创建支持贴图的材质 mat = Material() @@ -343,6 +343,8 @@ class CoreWorld(Panda3DWorld): self.ground2.setZ(49) # 在X轴方向偏移 self.ground2.setColor(0.8, 0.8, 0.8, 1) self.ground2.set_material(mat) + self.ground2.setTag("is_scene_element", "1") + self.ground2.setTag("tree_item_type", "SCENE_NODE") # 创建第三个相同的地面,位置在另一个方向 self.ground3 = self.render.attachNewNode(cm.generate()) @@ -352,6 +354,8 @@ class CoreWorld(Panda3DWorld): self.ground3.setZ(49) # 在X轴负方向偏移 self.ground3.setColor(0.8, 0.8, 0.8, 1) self.ground3.set_material(mat) + self.ground3.setTag("is_scene_element", "1") + self.ground3.setTag("tree_item_type", "SCENE_NODE") self.ground4 = self.render.attachNewNode(cm.generate()) # self.ground3.setR(90) @@ -360,6 +364,8 @@ class CoreWorld(Panda3DWorld): self.ground4.setZ(49) # 在X轴负方向偏移 self.ground4.setColor(0.8, 0.8, 0.8, 1) self.ground4.set_material(mat) + self.ground4.setTag("is_scene_element", "1") + self.ground4.setTag("tree_item_type", "SCENE_NODE") self.ground5 = self.render.attachNewNode(cm.generate()) self.ground5.setP(180) @@ -368,6 +374,8 @@ class CoreWorld(Panda3DWorld): self.ground5.setZ(49) # 在X轴负方向偏移 self.ground5.setColor(0.8, 0.8, 0.8, 1) self.ground5.set_material(mat) + self.ground5.setTag("is_scene_element", "1") + self.ground5.setTag("tree_item_type", "SCENE_NODE") self.ground6 = self.render.attachNewNode(cm.generate()) self.ground6.setP(90) @@ -375,6 +383,8 @@ class CoreWorld(Panda3DWorld): self.ground6.setZ(99) # 在X轴负方向偏移 self.ground6.setColor(0.8, 0.8, 0.8, 1) self.ground6.set_material(mat) + self.ground6.setTag("is_scene_element", "1") + self.ground6.setTag("tree_item_type", "SCENE_NODE") # 应用默认PBR效果,确保支持贴图 try: @@ -547,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/gui/gui_manager.py b/gui/gui_manager.py index bbd0beff..22bb47ef 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -24,80 +24,80 @@ except ImportError: WEB_ENGINE_AVAILABLE = False print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") - def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): - from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib - - # 参数类型检查和转换 - if isinstance(size, (list, tuple)): - if len(size) >= 2: - x_size, y_size = float(size[0]), float(size[1]) - else: - x_size = y_size = float(size[0]) if size else 1.0 - else: - x_size = y_size = float(size) - - # 创建卡片 - cm = CardMaker('gui_3d_image') - cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) - - # 创建3D图像节点 - image_node = self.world.render.attachNewNode(cm.generate()) - image_node.setPos(*pos) - - # 为3D图像创建独立的材质 - material = Material(f"image-material-{len(self.gui_elements)}") - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setDiffuse(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) - material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 - image_node.setMaterial(material, 1) - - image_node.setTransparency(TransparencyAttrib.MAlpha) - - # 如果提供了图像路径,则加载纹理 - if image_path: - self.update3DImageTexture(image_node, image_path) - - # 应用PBR效果(如果可用) - try: - if hasattr(self, 'render_pipeline') and self.render_pipeline: - self.render_pipeline.set_effect( - image_node, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": False, - "render_envmap": True, - "disable_children_effects": True - }, - 50 - ) - print("✓ GUI 3D图像PBR效果已应用") - except Exception as e: - print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") - - # 为GUI元素添加标识(效仿3D文本方法) - image_node.setTag("gui_type", "3d_image") - image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") - image_node.setTag("is_scene_element", "1") - image_node.setTag("tree_item_type", "GUI_3DIMAGE") - if image_path: - image_node.setTag("gui_image_path", image_path) - image_node.setTag("is_gui_element", "1") - - self.gui_elements.append(image_node) - - # 更新场景树 - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() - - print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") - return image_node + # def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + # from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib + # + # # 参数类型检查和转换 + # if isinstance(size, (list, tuple)): + # if len(size) >= 2: + # x_size, y_size = float(size[0]), float(size[1]) + # else: + # x_size = y_size = float(size[0]) if size else 1.0 + # else: + # x_size = y_size = float(size) + # + # # 创建卡片 + # cm = CardMaker('gui_3d_image') + # cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) + # + # # 创建3D图像节点 + # image_node = self.world.render.attachNewNode(cm.generate()) + # image_node.setPos(*pos) + # + # # 为3D图像创建独立的材质 + # material = Material(f"image-material-{len(self.gui_elements)}") + # material.setBaseColor(LColor(1, 1, 1, 1)) + # material.setDiffuse(LColor(1, 1, 1, 1)) + # material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + # material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + # material.setShininess(10.0) + # material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + # image_node.setMaterial(material, 1) + # + # image_node.setTransparency(TransparencyAttrib.MAlpha) + # + # # 如果提供了图像路径,则加载纹理 + # if image_path: + # self.update3DImageTexture(image_node, image_path) + # + # # 应用PBR效果(如果可用) + # try: + # if hasattr(self, 'render_pipeline') and self.render_pipeline: + # self.render_pipeline.set_effect( + # image_node, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": False, + # "render_envmap": True, + # "disable_children_effects": True + # }, + # 50 + # ) + # print("✓ GUI 3D图像PBR效果已应用") + # except Exception as e: + # print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + # + # # 为GUI元素添加标识(效仿3D文本方法) + # image_node.setTag("gui_type", "3d_image") + # image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + # image_node.setTag("is_scene_element", "1") + # image_node.setTag("tree_item_type", "GUI_3DIMAGE") + # if image_path: + # image_node.setTag("gui_image_path", image_path) + # image_node.setTag("is_gui_element", "1") + # + # self.gui_elements.append(image_node) + # + # # 更新场景树 + # if hasattr(self.world, 'updateSceneTree'): + # self.world.updateSceneTree() + # + # print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") + # return image_node class GUIManager: """GUI元素管理系统类""" @@ -326,6 +326,7 @@ class GUIManager: label.setTag("is_scene_element", "1") label.setTag("created_by_user", "1") label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + label.setTag("name",label_name) label.setName(label_name) # 如果有GUI父节点,建立引用关系 @@ -416,6 +417,10 @@ class GUIManager: parent_gui_node = None print(f"📎 挂载到3D父节点: {parent_item.text(0)}") + font = None + if hasattr(self.world,'getChineseFont'): + font = self.world.getChineseFont() + entry = DirectEntry( text="", pos=gui_pos, @@ -426,7 +431,9 @@ class GUIManager: numLines=1, width=12, focus=0, - parent=parent_gui_node # 设置GUI父节点 + parent=parent_gui_node, # 设置GUI父节点 + text_font = font, + frameSize=(-0.1,0.1,-0.05,0.05) ) # 设置节点标签 @@ -438,6 +445,7 @@ class GUIManager: entry.setTag("is_scene_element", "1") entry.setTag("created_by_user", "1") entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + entry.setTag("name",entry_name) entry.setName(entry_name) # 如果有GUI父节点,建立引用关系 @@ -572,6 +580,7 @@ class GUIManager: image_node.setTag("tree_item_type", "GUI_IMAGE") image_node.setTag("created_by_user", "1") image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + image_node.setTag("name",image_name) image_node.setName(image_name) # 如果有GUI父节点,建立引用关系 @@ -771,6 +780,7 @@ class GUIManager: def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): """创建3D空间图片""" try: + from panda3d.core import TextNode from PyQt5.QtCore import Qt @@ -861,6 +871,7 @@ class GUIManager: image_node.setTag("is_scene_element", "1") image_node.setTag("tree_item_type", "GUI_3DIMAGE") image_node.setTag("created_by_user", "1") + image_node.setTag("name",image_name) # 添加到GUI元素列表 self.gui_elements.append(image_node) @@ -979,6 +990,7 @@ class GUIManager: video_screen.setTag("is_scene_element", "1") video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") + video_screen.setTag("name",screen_name) # 设置视频路径标签 if video_path and os.path.exists(video_path): @@ -1496,9 +1508,12 @@ class GUIManager: video_screen.setTag("is_scene_element", "1") video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") - - video_screen.setTag("video_path", video_path if video_path else "") - print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}") + video_screen.setTag("name",screen_name) + # 修复后 + if video_path is not None: + video_screen.setTag("video_path", video_path) + else: + video_screen.setTag("video_path", "") # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") @@ -2173,9 +2188,17 @@ class GUIManager: print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") if property_name == "text": + original_frame_size = None + if hasattr(gui_element,'getFrameSize'): + try: + original_frame_size = gui_element.getFrameSize() + except: + pass if gui_type in ["button", "label"]: gui_element['text'] = value print(f"成功更新2D GUI文本: {value}") + # if gui_type == "button": + # self._resizeButtonToText(gui_element,value,original_frame_size) elif gui_type == "entry": gui_element.set(value) print(f"成功更新输入框文本: {value}") @@ -2254,6 +2277,64 @@ class GUIManager: traceback.print_exc() return False + def _resizeButtonToText(self, button, text, original_frame_size=None): + """ + 根据文本内容调整按钮大小 + + Args: + button: DirectButton 对象 + text: 新的文本内容 + original_frame_size: 原始frameSize,用于计算合适的padding + """ + try: + # 获取按钮当前的文本缩放 + text_scale = 0.03 # 默认文本缩放 + padding = 0.2 # 默认边距 + + # 尝试从按钮获取实际的文本缩放值 + if hasattr(button, 'getTextScale'): + text_scale = button.getTextScale() + elif hasattr(button, 'textScale'): + text_scale = button.textScale + + # 根据原始frameSize计算合适的padding + if original_frame_size and len(original_frame_size) >= 4: + # 基于原始按钮宽度计算合适的padding + padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1) + + # 更精确的文本尺寸估算 + # 考虑中文字符和英文字符的不同宽度 + chinese_chars = len([c for c in text if ord(c) > 127]) + english_chars = len(text) - chinese_chars + + # 中文字符通常比英文字符宽 + char_width = text_scale * 0.6 + text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width) + text_height = text_scale * 1.2 # 文本高度 + + # 计算新的frameSize,确保有足够的边距 + half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3 + half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15 + + # 正确设置frameSize - 使用字典方式设置 + new_frame = (-half_width, half_width, -half_height, half_height) + button['frameSize'] = new_frame # 使用字典方式设置 + + print(f"按钮大小已调整: {new_frame} (文本: {text})") + + except Exception as e: + print(f"调整按钮大小失败: {e}") + # 如果自动调整失败,保持原有大小或设置一个合理的默认大小 + try: + if original_frame_size: + # 保持原有大小 + button['frameSize'] = original_frame_size + else: + # 设置一个合理的默认大小 + button['frameSize'] = (-0.5, 0.5, -0.15, 0.15) + except: + pass + def duplicateGUIElement(self, gui_element): """复制GUI元素""" try: diff --git a/main.py b/main.py index c52d28c4..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 @@ -23,6 +25,7 @@ from core.vr_manager import VRManager from core.vr_input_handler import VRInputHandler from core.alvr_streamer import ALVRStreamer from core.patrol_system import PatrolSystem +from core.Command_System import CommandManager from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager @@ -110,6 +113,8 @@ class MyWorld(CoreWorld): self.info_panel_manager = InfoPanelManager(self) + self.command_manager = CommandManager() + # 初始化碰撞管理器 from core.collision_manager import CollisionManager self.collision_manager = CollisionManager(self) @@ -235,11 +240,11 @@ class MyWorld(CoreWorld): """创建2D GUI文本输入框""" return self.gui_manager.createGUIEntry(pos, placeholder, size) - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): """创建3D空间文本""" return self.gui_manager.createGUI3DText(pos, text, size) - def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)): + def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1)): """创建3D图片""" return self.gui_manager.createGUI3DImage(pos,text,size) @@ -895,6 +900,22 @@ class MyWorld(CoreWorld): except Exception as e: print(f"创建默认自动朝向巡检路线失败: {e}") + def _serializeNode(self, node): + """序列化节点数据""" + try: + return self.world.scene_manager.serializeNode(node) + except Exception as e: + print(f"序列化节点失败: {e}") + return None + + def _deserializeNode(self, node_data, parent_node): + """反序列化节点数据""" + try: + return self.world.scene_manager.deserializeNode(node_data, parent_node) + except Exception as e: + print(f"反序列化节点失败: {e}") + return None + # ==================== 项目管理功能代理 ==================== # 以下函数代理到project_manager模块的对应功能 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 cd031d65..37ee239a 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -7,8 +7,11 @@ """ import os +import shutil +import time from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeWidgetItem from panda3d.core import ( ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, @@ -22,6 +25,7 @@ from pathlib import Path from panda3d.egg import EggData, EggVertexPool from direct.actor.Actor import Actor from QPanda3D.Panda3DWorld import get_render_pipeline +from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq from scene import util class CesiumIntegration: @@ -141,24 +145,20 @@ class SceneManager: print("加载模型失败") return None - - # 设置模型名称 model_name = os.path.basename(filepath) # 确保名称有效 if not model_name: model_name = "imported_model" model.setName(model_name) - - # 使用安全方法将模型添加到场景 - #self._safeReparentTo(model, self.world.render) + # 将模型添加到场景 + model.reparentTo(self.world.render) # 设置模型名称 model_name = os.path.basename(filepath) model.setName(model_name) - # 将模型添加到场景 - model.reparentTo(self.world.render) + # 保存原始路径和转换后的路径 model.setTag("model_path", filepath) model.setTag("original_path", original_filepath) @@ -166,18 +166,31 @@ class SceneManager: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") - # 可选的单位转换(主要针对FBX) - if apply_unit_conversion and filepath.lower().endswith('.fbx'): - #print("应用FBX单位转换(厘米到米)...") - self._applyUnitConversion(model, 0.01) + #特殊处理FBX模型 + if filepath.lower().endswith('.fbx'): + print("检测到FBX模型,应用特殊处理...") - # 智能缩放标准化(处理FBX子节点的大缩放值) - if normalize_scales and filepath.lower().endswith('.fbx'): - #print("标准化FBX模型缩放层级...") - self._normalizeModelScales(model) + # 将模型缩放设置为原来的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'): + # #print("应用FBX单位转换(厘米到米)...") + # self._applyUnitConversion(model, 0.01) + # + # # 智能缩放标准化(处理FBX子节点的大缩放值) + # if normalize_scales and filepath.lower().endswith('.fbx'): + # #print("标准化FBX模型缩放层级...") + # self._normalizeModelScales(model) # 调整模型位置到地面 - self._adjustModelToGround(model) + model.setPos(0,0,0) + #self._adjustModelToGround(model) # 创建并设置基础材质 print("\n=== 开始设置材质 ===") @@ -775,7 +788,6 @@ class SceneManager: """为模型设置碰撞检测(增强版本)""" try: - # 创建碰撞节点 cNode = CollisionNode(f'modelCollision_{model.getName()}') @@ -887,9 +899,16 @@ class SceneManager: "position": list(gui_node.getPos()), "rotation": list(gui_node.getHpr()), "scale": list(gui_node.getScale()), - "tags": {} + "tags": {}, + "parent_name":None } + parent = gui_node.getParent() + if parent and not parent.isEmpty(): + parent_name = parent.getName() + if parent_name not in ["render","aspect2d","render2d"]: + gui_info["parent_name"] = parent_name + # 收集所有标签(仅对NodePath类型的对象) if hasattr(gui_node, 'getTagNames'): for tag in gui_node.getTagNames(): @@ -951,24 +970,31 @@ class SceneManager: gui_info["panel_data"] = gui_node.getTag("info_panel_data") if hasattr(self.world, 'script_manager') and self.world.script_manager: - script_manager = self.world.script_manager - # 获取挂载在此节点上的所有脚本 - scripts = script_manager.get_scripts_on_object(gui_node) - if scripts: - gui_info["scripts"] = [] - for script_component in scripts: - script_name = script_component.script_name # 使用脚本组件的名称 - print(f"收集脚本信息: {script_name}") - - # 获取脚本类的文件路径 - script_class = script_component.script_instance.__class__ - script_file = self._get_script_file_path(script_class, script_name) - - gui_info["scripts"].append({ - "name": script_name, - "file": script_file - }) - print(f"脚本 {script_name} 来自文件: {script_file}") + try: + script_manager = self.world.script_manager + scripts = script_manager.get_scripts_on_object(gui_node) + if scripts: + gui_info["scripts"] = [] + for script_component in scripts: + try: + script_name = script_manager.script_name + #获取脚本路径 + script_class = script_component.script_instance.__class__ + script_file = self._get_script_file_path(script_class,script_name) + #只有当脚本文件存在时才保存 + if script_file and os.path.exists(script_file): + gui_info["scirpts"].append({ + "name":script_name, + "file":script_file + }) + print(f"收集脚本信息{script_name}from {script_file}") + else: + print(f"警告: 脚本文件不存在: {script_file}") + except Exception as e: + print(f"收集单个脚本信息失败{script_name},错误{e}") + continue + except Exception as e: + print(f"收集脚本信息失败{e}") print(f"成功收集GUI元素信息: {gui_info}") return gui_info @@ -1059,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 = [] @@ -1109,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: @@ -1126,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(): @@ -1147,6 +1232,22 @@ class SceneManager: node.setTag("transform_scale", str(node.getScale())) print(f"保存节点 {node.getName()} 的变换信息") + # 保存父子关系信息 - 关键修改 + parent = node.getParent() + if parent and not parent.isEmpty() and parent != self.world.render: + # 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系 + if parent.getName() not in ["render", "aspect2d", "render2d"]: + # 检查父节点是否也是场景中的模型 + is_parent_model = False + for model in self.models: + if model == parent: + is_parent_model = True + break + + if is_parent_model: + node.setTag("parent_name", parent.getName()) + print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}") + # 获取当前状态 state = node.getState() @@ -1185,7 +1286,6 @@ class SceneManager: node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown" node.setTag("saved_gui_type", gui_type) - # 保存GUI元素的通用属性 if hasattr(node, 'getPythonTag'): # 保存任何Python标签数据 @@ -1205,6 +1305,30 @@ class SceneManager: import json node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False)) + if hasattr(self.world,'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + scripts = script_manager.get_scripts_on_object(node) + if scripts: + node.setTag("has_scripts", "true") + script_info_list = [] + for script_component in scripts: + script_name = script_component.script_name + print(f"保存脚本信息: {script_name}") + + # 获取脚本类的文件路径 + script_class = script_component.script_instance.__class__ + script_file = self._get_script_file_path(script_class, script_name) + + script_info_list.append({ + "name": script_name, + "file": script_file + }) + + # 将脚本信息保存为JSON字符串 + import json + node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) + print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本") + try: print("--- 打印当前场景图 (render) ---") self.world.render.ls() @@ -1361,11 +1485,21 @@ class SceneManager: processed_lights = [] # 用于存储处理后的GUI元素,避免重复处理 + #存储所有加载的节点,用于后续处理父子关系 + loaded_nodes = {} #name->nodePath映射 + # 遍历场景中的所有节点 def processNode(nodePath, depth=0): indent = " " * depth print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") + #存储节点以便后续处理父子关系 + 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节点") @@ -1459,6 +1593,51 @@ class SceneManager: nodePath.setScale(scale) print(f"{indent}恢复缩放: {scale}") + if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true": + if hasattr(self.world,'script_manager') and self.world.script_manager: + try: + import json + scripts_info = json.loads(nodePath.getTag("scripts_info")) + print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本") + + script_manager = self.world.script_manager + for script_info in scripts_info: + script_name = script_info["name"] + script_file = script_info.get("file","") + + print(f"尝试重新挂载脚本{script_name}from {script_file}") + + if script_name not in script_manager.loader.script_classes: + if script_file and os.path.exists(script_file): + print(f"从文件加载脚本:{script_file}") + loaded_class = script_manager.load_script_from_file(script_file) + if loaded_class is None: + print(f"从文件加载脚本失败{script_file}") + script_path = self._find_scrip_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载{script_path}") + script_manager.load_script_from_file(script_path) + else: + script_path = self._find_script_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载: {script_path}") + script_manager.load_script_from_file(script_path) + else: + print(f"找不到脚本文件: {script_name}") + if script_name in script_manager.loader.script_classes: + script_component = script_manager.add_script_to_object(nodePath,script_name) + if script_component: + print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}") + else: + print(f"为 {nodePath.getName()} 添加脚本失败: {script_name}") + else: + print(f"脚本 {script_name} 不可用,跳过挂载") + except Exception as e: + print(f"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() + + # 恢复材质属性 def parseColor(color_str): """解析颜色字符串为Vec4""" @@ -1554,11 +1733,11 @@ class SceneManager: self._fixModelStructure(nodePath) - if self.world.property_panel._hasCollision(nodePath): - print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") - else: - print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") - self.setupCollision(nodePath) + # if self.world.property_panel._hasCollision(nodePath): + # print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") + # else: + # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") + # self.setupCollision(nodePath) self.models.append(nodePath) # 递归处理子节点 @@ -1568,6 +1747,10 @@ class SceneManager: print("\n开始处理场景节点...") processNode(scene) + #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 + print("\n开始重建父子关系...") + self._rebuildParentChildRelationships(loaded_nodes) + # 加载GUI信息并重新创建非3D的GUI元素 gui_info_file = filename.replace('.bam', '_gui.json') if os.path.exists(gui_info_file): @@ -1599,7 +1782,7 @@ class SceneManager: # 更新场景树 #self.updateSceneTree() - # self._get_tree_widget().create_model_items(scene) + #self._get_tree_widget().create_model_items(scene) print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}") if len(self.world.gui_elements) > 0: @@ -1617,8 +1800,56 @@ class SceneManager: traceback.print_exc() return False + def _rebuildParentChildRelationships(self, loaded_nodes): + try: + parent_child_relations = [] + for node_name, node in loaded_nodes.items(): + if node.hasTag("parent_name"): + parent_name = node.getTag("parent_name") + if parent_name in loaded_nodes: + parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组 + print(f"发现父子关系:{parent_name}->{node_name}") + else: + print(f"警告:节点{node_name}的父节点{parent_name}不存在") + for child_node, parent_node in parent_child_relations: + try: + child_node.wrtReparentTo(parent_node) + print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}") + except Exception as e: + print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}") + + if not parent_child_relations: + print("尝试从场景结构推断父子关系") + self._inferParentChildRelationships(loaded_nodes) + + print("父子关系重建完成") + except Exception as e: + print(f"重建父子关系时出错: {e}") + import traceback + traceback.print_exc() + + + except Exception as e: + print(f"重建父子关系时出错: {e}") + import traceback + traceback.print_exc() + + def _inferParentChildRelationships(self, loaded_nodes): + """从场景结构推断父子关系""" + try: + # 这里可以添加更复杂的父子关系推断逻辑 + # 例如,根据节点名称、位置关系等进行推断 + # 目前保持简单,后续可以扩展 + print("父子关系推断完成(当前为空实现)") + except Exception as e: + print(f"推断父子关系时出错: {e}") + def _shouldSkipNodeInTree(self, nodePath): """判断节点是否应该在场景树中跳过显示""" + + if nodePath.getName().startswith('ground'): + return True + # 跳过render节点的递归 if nodePath.getName() == "render": return True @@ -1642,17 +1873,45 @@ class SceneManager: return True return False + def _recreateGUIElementsFromData(self, gui_data): """根据保存的GUI数据重新创建GUI元素""" try: gui_manager = getattr(self.world, 'gui_manager', None) - info_panel_manager = getattr(self.world,'info_panel_manager',None) + property_manager = getattr(self.world, 'property_panel', None) + info_panel_manager = getattr(self.world, 'info_panel_manager', None) if not gui_manager: print("GUI管理器未找到,无法重建GUI元素") return print(f"开始重建 {len(gui_data)} 个GUI元素...") processed_names = set() + created_elements = {} + # 存储原始的缩放和位置信息,用于后续计算 + element_original_data = {} + + # 第一遍:收集所有元素信息 + for i, gui_info in enumerate(gui_data): + name = gui_info.get("name", f"gui_element_{i}") + element_original_data[name] = { + "scale": gui_info.get("scale", [1, 1, 1]), + "position": gui_info.get("position", [0, 0, 0]), + "parent_name": gui_info.get("parent_name") + } + + valid_parents = set() + for gui_info in gui_data: + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") + valid_parents.add(name) + + if hasattr(self.world, 'gui_elements'): + for elem in self.world.gui_elements: + if elem and not elem.isEmpty(): + valid_parents.add(elem.getName()) + + valid_parents.add("render") + valid_parents.add("aspect2d") + valid_parents.add("render2d") pos = (0, 0, 0) for i, gui_info in enumerate(gui_data): @@ -1668,12 +1927,17 @@ class SceneManager: bg_image_path = gui_info.get("bg_image_path", "") # 背景图片路径 panel_id = gui_info.get("panel_id", name) # 信息面板ID panel_data = gui_info.get("panel_data", None) # 面板数据 + parent_name = gui_info.get("parent_name") # 检查是否已经处理过同名元素 if name in processed_names: print(f"跳过重复元素: {name}") continue + if parent_name and parent_name not in valid_parents: + print(f"⚠️ 跳过元素 {name},因为其父级 {parent_name} 不存在") + continue + processed_names.add(name) print(f"重建GUI元素: {name} (类型: {gui_type})") @@ -1682,131 +1946,131 @@ 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) + + if parent_name and parent_name in element_original_data: + parent_data = element_original_data[parent_name] + parent_scale = parent_data["scale"] + + if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", + "2d_video_screen"]: + # 位置需要乘以父级缩放来得到绝对位置 + for j in range(min(len(absolute_position), len(parent_scale))): + absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + # 缩放需要乘以父级缩放来得到绝对缩放 + for j in range(min(len(absolute_scale), len(parent_scale))): + absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + print(f" 绝对位置: {absolute_position}") + print(f" 绝对缩放: {absolute_scale}") # 根据类型创建相应的GUI元素 new_element = None if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'): new_element = gui_manager.createGUIButton( - pos=tuple(position), + pos=tuple(absolute_position), text=text, - size=scale[0] if scale and len(scale) > 0 else 1.0 + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 ) elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'): - scale_value = scale[0] if scale and len(scale) > 0 else 1.0 + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 new_element = gui_manager.createGUILabel( - pos=tuple(position), + pos=tuple(absolute_position), text=text, size=scale_value ) elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'): new_element = gui_manager.createGUIEntry( - pos=tuple(position), + pos=tuple(absolute_position), placeholder=text, - size=scale[0] if scale and len(scale) > 0 else 1.0 + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 ) elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): - scale_value = scale[0] if scale and len(scale) > 0 else 0.2 + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.2 new_element = gui_manager.createGUI2DImage( - pos=tuple(position), + 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'): - size = scale[0] if scale and len(scale) > 0 else 0.5 + 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(position), + pos=tuple(absolute_position), text=text, size=size ) elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): # 处理3D图像 # 根据缩放值的数量处理尺寸 - if len(scale) >= 3: - size = (scale[0] * 2, scale[1] * 2) - elif len(scale) >= 2: - size = (scale[0] * 2, scale[1] * 2) + if len(absolute_scale) >= 3: + size = (absolute_scale[0] * 2, absolute_scale[1] * 2) + elif len(absolute_scale) >= 2: + size = (absolute_scale[0] * 2, absolute_scale[1] * 2) elif len(scale) >= 1: - size = (scale[0] * 2, scale[0] * 2) + size = (absolute_scale[0] * 2, absolute_scale[0] * 2) else: size = (1.0, 1.0) new_element = gui_manager.createGUI3DImage( - pos=tuple(position), + pos=tuple(absolute_position), 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(position), - size=scale, + pos=tuple(absolute_position), + size=absolute_scale, video_path=video_path ) - 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 + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + from direct.task.TaskManagerGlobal import taskMgr - taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask') + def load_video_stream_task(task): + if hasattr(property_manager, '_loadVideoFromURLWithOpenCV_3D'): + property_manager._loadVideoFromURLWithOpenCV_3D(new_element, video_path) + return task.done - elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'): + taskMgr.doMethodLater(0.5, load_video_stream_task, 'loadVideoStreamTask') + else: + if hasattr(gui_manager, 'loadVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr + + def load_video_file_task(task): + gui_manager.loadVideoFile(new_element, video_path) + return task.done + + taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask') + + elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): new_element = gui_manager.createGUI2DVideoScreen( - pos=tuple(position), - size=scale, + pos=tuple(absolute_position), + size=absolute_scale, video_path=video_path ) - elif gui_type in ["info_panel", "info_panel_3d"]: - # 重建信息面板 - if info_panel_manager: - try: - if panel_data: - # 从序列化数据重建面板 - import json - panel_data_obj = json.loads(panel_data) - new_element = info_panel_manager.recreatePanelFromData(panel_data_obj) - else: - # 创建新的面板 - if gui_type == "info_panel_3d": - # 3D信息面板 - new_element = info_panel_manager.create3DInfoPanel( - panel_id=panel_id, - position=tuple(position) if len(position) >= 3 else (0, 0, 0), - size=tuple(scale) if len(scale) >= 2 else (1.0, 0.6), - visible=not tags.get("hidden", False) - ) - else: - # 2D信息面板 - pos_2d = (position[0], position[2]) if len(position) >= 3 else (0, 0) - new_element = info_panel_manager.createInfoPanel( - panel_id=panel_id, - position=pos_2d, - size=tuple(scale) if len(scale) >= 2 else (1.0, 0.6), - visible=not tags.get("hidden", False) - ) + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + pass + # from direct.task.TaskManagerGlobal import taskMgr + # def load_2d_video_stream_task(task): + # if hasattr(property_manager,'_loadVideoFromURLWithOpenCV'): + # property_manager._loadVideoFromURLWithOpenCV(new_element,video_path) + # return task.done + # taskMgr.doMethodLater(0.1,load_2d_video_stream_task,'load2DVideoStreamTask') + else: + if hasattr(property_manager, 'load2DVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr - # 设置背景图片 - if bg_image_path and hasattr(info_panel_manager, 'setPanelBackgroundImage'): - info_panel_manager.setPanelBackgroundImage(panel_id, bg_image_path) - - # 更新面板内容 - if text: - title, content = text.split('\n', 1) if '\n' in text else ("信息面板", text) - if gui_type == "info_panel_3d": - info_panel_manager.update3DPanelContent(panel_id, title=title, - content=content) - else: - info_panel_manager.updatePanelContent(panel_id, title=title, - content=content) - except Exception as e: - print(f"重建信息面板失败: {e}") - import traceback - traceback.print_exc() - else: - print("信息面板管理器未找到,无法重建信息面板") + 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: @@ -1836,8 +2100,103 @@ class SceneManager: elif hasattr(new_element, '_tags'): new_element._tags.update(tags) - # 重新挂载脚本(如果有的话) - # 在 _recreateGUIElementsFromData 方法中找到重新挂载脚本的部分,替换为以下代码: + created_elements[name] = new_element + + print(f"GUI元素重建成功: {name}") + else: + print(f"无法重建GUI元素: {name} (类型: {gui_type})") + + except Exception as e: + print(f"重建GUI元素失败 {name}: {e}") + import traceback + 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") + + 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 + + # 然后设置父子级关系 + 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}") + + # 更新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) + + # 将子项添加到新的父项下 + parent_item.addChild(child_item) + print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") + else: + 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 + + except Exception as e: + print(f"设置父子级关系时出错: {e}") + # 第三遍:重新挂载脚本 + print("开始重新挂载脚本...") + for gui_info in gui_data: + try: + name = gui_info.get("name") + if name in created_elements and "scripts" in gui_info: + new_element = created_elements[name] # 重新挂载脚本(如果有的话) if "scripts" in gui_info and hasattr(self.world, @@ -1877,24 +2236,82 @@ class SceneManager: print(f"成功为 {name} 添加脚本: {script_name}") else: print(f"为 {name} 添加脚本失败: {script_name}") - - print(f"GUI元素重建成功: {name}") - else: - print(f"无法重建GUI元素: {name} (类型: {gui_type})") - except Exception as e: - print(f"重建GUI元素失败 {name}: {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() + def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name): + """在Qt树中查找或创建指定元素对应的项""" + try: + # 首先尝试查找现有的项 + existing_item = self._findQtTreeItem(tree_widget, target_element) + if existing_item: + return existing_item + + # 如果找不到,创建新的项 + # 找到场景根节点 + scene_root = None + for i in range(tree_widget.topLevelItemCount()): + top_item = tree_widget.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + scene_root = top_item + break + + if not scene_root: + print("无法找到场景根节点") + return None + + # 创建新的Qt树项 + new_item = QTreeWidgetItem(scene_root, [element_name]) + new_item.setData(0, Qt.UserRole, target_element) + new_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") # 或根据元素类型设置适当的类型 + + print(f"为元素 {element_name} 创建了新的Qt树项") + return new_item + + except Exception as e: + print(f"查找或创建Qt树项失败: {e}") + return None + + def _findQtTreeItem(self, tree_widget, target_element): + """在Qt树中查找指定元素对应的项""" + try: + def search_recursive(parent_item): + # 检查当前项 + if parent_item: + item_element = parent_item.data(0, Qt.UserRole) + if item_element == target_element: + return parent_item + + # 递归检查子项 + for i in range(parent_item.childCount()): + child_item = parent_item.child(i) + result = search_recursive(child_item) + if result: + return result + return None + + # 从根节点开始搜索 + root = tree_widget.invisibleRootItem() + for i in range(root.childCount()): + top_item = root.child(i) + result = search_recursive(top_item) + if result: + return result + + return None + except Exception as e: + print(f"查找Qt树项失败: {e}") + return None def _find_script_in_directory(self, script_name): """在脚本目录中查找脚本文件""" try: @@ -1926,6 +2343,7 @@ class SceneManager: try: from RenderPipelineFile.rpcore import SpotLight from QPanda3D.Panda3DWorld import get_render_pipeline + from panda3d.core import Vec3 # 创建聚光灯对象 light = SpotLight() @@ -1943,8 +2361,8 @@ class SceneManager: light.casts_shadows = True light.shadow_map_resolution = 256 - # 设置位置 - light.setPos(light_node.getPos()) + light_pos = light_node.getPos() + light.setPos(light_pos) # 添加到渲染管线 render_pipeline = get_render_pipeline() @@ -1963,6 +2381,8 @@ class SceneManager: print(f"重新创建聚光灯: {light_node.getName()}") except Exception as e: print(f"重新创建聚光灯失败: {str(e)}") + import traceback + traceback.print_exc() def _recreatePointLight(self, light_node): """重新创建点光源""" @@ -2005,6 +2425,8 @@ class SceneManager: print(f"重新创建点光源: {light_node.getName()}") except Exception as e: print(f"重新创建点光源失败: {str(e)}") + import traceback + traceback.print_exc() def _cleanupAuxiliaryNodes(self): """清理场景中可能存在的辅助节点""" @@ -2139,6 +2561,8 @@ class SceneManager: light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.setPos(*pos) + light_np.setTransform(TransformState.makeIdentity()) + # 创建聚光灯对象 light = SpotLight() light.direction = Vec3(0, 0, -1) @@ -2148,7 +2572,7 @@ class SceneManager: light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 - light.setPos(*pos) + light.setPos(pos) # 添加到渲染管线 render_pipeline.add_light(light) @@ -2245,11 +2669,12 @@ class SceneManager: light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.setPos(*pos) + # 确保变换矩阵有效 + light_np.setTransform(TransformState.makeIdentity()) + # 创建点光源对象 light = PointLight() - light.setPos(*pos) - light.energy = 5000 light.radius = 1000 light.inner_radius = 0.4 @@ -2324,177 +2749,6 @@ class SceneManager: pass return None - # def createSpotLight(self, pos=(0, 0, 0)): - # """创建聚光灯 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建聚光灯节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建聚光灯节点 - # created_nodes = tree_widget.create_item("spot_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建聚光灯失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建聚光灯时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createSpotLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import SpotLight, RenderPipeline - # from panda3d.core import Vec3,NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("SpotlightAttachNode") - # light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # self.half_energy = 5000 - # self.lamp_fov = 70 - # self.lamp_radius = 1000 - # - # light = SpotLight() - # light.direction = Vec3(0, 0, -1) # 光照方向 - # light.fov = self.lamp_fov # 光源角度(类似手电筒) - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.energy = self.half_energy # 光照强度 - # light.radius = self.lamp_radius # 影响范围 - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # light.setPos(*pos) - # #light_np.setPos(*pos) - # - # #light_np = render_pipeline.add_light(light, parent=self.world.render) - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Spotlight_{len(self.Spotlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # #light_np.reparentTo(self.world.render) # 挂载到场景根节点 - # - # light_np.setTag("light_type", "spot_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # light_np.setPythonTag("rp_light_object", light) - # - # self.Spotlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # #print("nikan"+light_np.getHpr()) - - # def createPointLight(self, pos=(0, 0, 0)): - # """创建点光源 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建点光源节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建点光源节点 - # created_nodes = tree_widget.create_item("point_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建点光源失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建点光源时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createPointLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import PointLight, RenderPipeline - # from panda3d.core import Vec3, NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("PointlightAttachNode") - # light_np.reparentTo(self.world.render) - # - # - # light = PointLight() - # light.setPos(*pos) - # light_np.setPos(*pos) - # light.energy = 5000 - # light.radius = 1000 - # light.inner_radius = 0.4 - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Pointlight{len(self.Pointlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # - # #light_np = NodePath(f"PointLight_{len(self.Pointlight)}") - # #light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # light_np.setTag("light_type", "point_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # # 保存光源对象引用(重要!用于属性面板) - # light_np.setPythonTag("rp_light_object", light) - # - # self.Pointlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # return light,light_np # ==================== GLB 转换方法 ==================== @@ -3102,4 +3356,739 @@ except Exception as e: return None + def serializeNode(self, node): + """序列化节点为字典数据""" + try: + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + + # 特殊处理不同类型的节点 + if hasattr(node.node(), 'getClassType'): + node_class = node.node().getClassType().getName() + node_data['node_class'] = node_class + + # 递归序列化子节点 + for child in node.getChildren(): + # 跳过辅助节点 + if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')): + child_data = self.serializeNode(child) + if child_data: + node_data['children'].append(child_data) + + return node_data + + except Exception as e: + print(f"序列化节点 {node.getName()} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def deserializeNode(self, node_data, parent_node): + """从字典数据反序列化节点""" + try: + # 创建新节点 + node_name = node_data.get('name', 'node') + new_node = parent_node.attachNewNode(node_name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + new_node.setTag(tag_key, tag_value) + + # 根据节点类型进行特殊处理 + node_type = node_data.get('type', '') + node_class = node_data.get('node_class', '') + + # 特殊处理光源节点 + if 'light_type' in node_data.get('tags', {}): + light_type = node_data['tags']['light_type'] + if light_type == 'spot_light': + self._recreateSpotLight(new_node) + elif light_type == 'point_light': + self._recreatePointLight(new_node) + + # 递归创建子节点 + for child_data in node_data.get('children', []): + self.deserializeNode(child_data, new_node) + + return new_node + + except Exception as e: + print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def serializeNodeForCopy(self, node): + """序列化节点用于复制操作,完整保存视觉属性""" + try: + if not node or node.isEmpty(): + return None + + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + try: + if hasattr(node, 'getTagKeys'): + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + except Exception as e: + print(f"获取标签时出错: {e}") + + # 保存视觉属性 + try: + # 保存颜色属性 + if hasattr(node, 'getColor'): + color = node.getColor() + node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW()) + + # 保存材质属性 + if hasattr(node, 'getMaterial'): + material = node.getMaterial() + if material: + material_data = {} + material_data['base_color'] = ( + material.getBaseColor().getX(), + material.getBaseColor().getY(), + material.getBaseColor().getZ(), + material.getBaseColor().getW() + ) + material_data['ambient'] = ( + material.getAmbient().getX(), + material.getAmbient().getY(), + material.getAmbient().getZ(), + material.getAmbient().getW() + ) + material_data['diffuse'] = ( + material.getDiffuse().getX(), + material.getDiffuse().getY(), + material.getDiffuse().getZ(), + material.getDiffuse().getW() + ) + material_data['specular'] = ( + material.getSpecular().getX(), + material.getSpecular().getY(), + material.getSpecular().getZ(), + material.getSpecular().getW() + ) + material_data['shininess'] = material.getShininess() + node_data['material'] = material_data + + except Exception as e: + print(f"保存视觉属性时出错: {e}") + + # 根据节点类型保存特定信息 + if node.hasTag("tree_item_type"): + node_type = node.getTag("tree_item_type") + node_data['node_type'] = node_type + + # 保存特定类型节点的额外信息 + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + # 保存光源特定信息 + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + node_data['light_data'] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else None, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light, + 'casts_shadows') else True, + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr( + rp_light, 'shadow_map_resolution') else 256 + } + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]: + # 保存GUI元素特定信息 + node_data['gui_data'] = self._serializeGUIData(node) + elif node_type == "IMPORTED_MODEL_NODE": + # 保存模型特定信息 + node_data['model_data'] = self._serializeModelData(node) + + return node_data + + except Exception as e: + print(f"序列化节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _serializeGUIData(self, node): + """序列化GUI元素数据""" + try: + gui_data = {} + + # 保存GUI相关的通用属性 + if node.hasTag("gui_type"): + gui_data['gui_type'] = node.getTag("gui_type") + + # 保存文本内容(如果有的话) + if node.hasTag("text"): + gui_data['text'] = node.getTag("text") + + # 保存其他GUI相关标签 + gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size'] + for tag in gui_tags: + if node.hasTag(tag): + gui_data[tag] = node.getTag(tag) + + return gui_data + except Exception as e: + print(f"序列化GUI数据失败: {e}") + return {} + + def _serializeModelData(self, node): + """序列化模型数据,包括材质信息""" + try: + model_data = {} + + # 保存模型相关的标签 + model_tags = ['model_path', 'file', 'element_type'] + for tag in model_tags: + if node.hasTag(tag): + model_data[tag] = node.getTag(tag) + + # 保存材质信息 + try: + # 获取模型的材质信息 + if hasattr(node, 'getState'): + state = node.getState() + if state: + # 保存基础颜色信息 + from panda3d.core import ColorAttrib + color_attrib = state.getColor() + if color_attrib: + model_data['base_color'] = ( + color_attrib.getColor().getX(), + color_attrib.getColor().getY(), + color_attrib.getColor().getZ(), + color_attrib.getColor().getW() + ) + + # 保存其他材质属性 + from panda3d.core import MaterialAttrib + material_attrib = state.getAttrib(MaterialAttrib.getClassType()) + if material_attrib: + material = material_attrib.getMaterial() + if material: + # 保存基础颜色 + base_color = material.getBaseColor() + model_data['material_base_color'] = ( + base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW() + ) + + # 保存环境光颜色 + ambient_color = material.getAmbient() + model_data['material_ambient_color'] = ( + ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(), + ambient_color.getW() + ) + + # 保存漫反射颜色 + diffuse_color = material.getDiffuse() + model_data['material_diffuse_color'] = ( + diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(), + diffuse_color.getW() + ) + + # 保存高光颜色 + specular_color = material.getSpecular() + model_data['material_specular_color'] = ( + specular_color.getX(), specular_color.getY(), specular_color.getZ(), + specular_color.getW() + ) + + # 保存粗糙度和金属度等参数 + model_data['material_roughness'] = material.getRoughness() + model_data['material metallic'] = material.getMetallic() + + except Exception as e: + print(f"保存材质信息时出错: {e}") + + return model_data + except Exception as e: + print(f"序列化模型数据失败: {e}") + return {} + + def recreateNodeFromData(self, node_data, parent_node): + """根据数据重建节点,并确保在场景树中显示""" + try: + if not node_data or not parent_node or parent_node.isEmpty(): + return None + + print(f"正在重建节点 {node_data}") + node_type = node_data.get('node_type', '') + original_name = node_data.get('name', 'node') + + # 生成唯一名称 + unique_name = self._generateUniqueName(original_name, parent_node) + + # 根据节点类型调用相应的重建方法 + new_node = None + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + new_node = self._recreateLightFromData(node_data, parent_node, unique_name) + elif node_type == "CESIUM_TILESET_NODE": + new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]: + new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) + elif node_type == "IMPORTED_MODEL_NODE": + new_node = self._recreateModelFromData(node_data, parent_node, unique_name) + else: + # 创建普通节点 + new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name) + + # 如果成功创建节点,确保它在场景树中显示 + if new_node: + # 尝试更新场景树以显示新节点 + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + # 查找父节点在场景树中的对应项 + parent_item = self._findTreeItemForNode(parent_node) + if parent_item: + # 添加新节点到场景树 + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE") + except Exception as e: + print(f"添加节点到场景树时出错: {e}") + + return new_node + + except Exception as e: + print(f"重建节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _findTreeItemForNode(self, node): + """根据节点查找对应的场景树项""" + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + # 遍历场景树查找匹配的节点项 + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + result = self._findTreeItemForNodeRecursive(item, node) + if result: + return result + return None + except Exception as e: + print(f"查找场景树项时出错: {e}") + return None + + def _findTreeItemForNodeRecursive(self, item, target_node): + """递归查找场景树项""" + try: + # 检查当前项是否匹配 + item_node = getattr(item, 'node_path', None) + if not item_node: + item_node = getattr(item, 'node', None) + + if item_node and item_node == target_node: + return item + + # 递归检查子项 + for i in range(item.childCount()): + child_item = item.child(i) + result = self._findTreeItemForNodeRecursive(child_item, target_node) + if result: + return result + + return None + except Exception as e: + print(f"递归查找场景树项时出错: {e}") + return None + + def _recreateLightFromData(self, node_data, parent_node, name): + """根据数据重建光源""" + try: + light_type = node_data.get('tags', {}).get('light_type', 'spot_light') + + # 创建光源 + if light_type == 'spot_light': + light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0))) + else: # point_light + light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0))) + + if light_node: + # 设置名称 + light_node.setName(name) + + # 恢复其他属性 + light_data = node_data.get('light_data', {}) + rp_light = light_node.getPythonTag("rp_light_object") + if rp_light and light_data: + if 'energy' in light_data: + rp_light.energy = light_data['energy'] + if 'radius' in light_data: + rp_light.radius = light_data['radius'] + if 'fov' in light_data and hasattr(rp_light, 'fov'): + rp_light.fov = light_data['fov'] + if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'): + rp_light.inner_radius = light_data['inner_radius'] + if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'): + rp_light.casts_shadows = light_data['casts_shadows'] + if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'): + rp_light.shadow_map_resolution = light_data['shadow_map_resolution'] + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name', 'light_type']: + light_node.setTag(tag_key, str(tag_value)) + + return light_node + + except Exception as e: + print(f"重建光源失败: {e}") + return None + + def _recreateTilesetFromData(self, node_data, parent_node, name): + """根据数据重建Tileset""" + try: + tileset_url = node_data.get('tileset_url', '') + if not tileset_url: + return None + + # 使用现有方法加载tileset + position = node_data.get('pos', (0, 0, 0)) + tileset_node = self.load_cesium_tileset(tileset_url, position) + + if tileset_node: + # 设置名称 + tileset_node.setName(name) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + tileset_node.setTag(tag_key, str(tag_value)) + + return tileset_node + + except Exception as e: + print(f"重建Tileset失败: {e}") + return None + + def _recreateGUIFromData(self, node_data, parent_node, name): + """根据数据重建GUI元素""" + try: + gui_data = node_data.get('gui_data', {}) + #gui_type = gui_data.get('gui_type', '') + gui_type = node_data.get("tags").get("gui_type", "") + + print(f"正在重建GUI元素: {gui_type}") + print(f"正在重建GUI元素: {node_data}") + + # 根据GUI类型调用相应的创建方法 + new_gui_element = None + + if gui_type == "button" and hasattr(self.world, 'createGUIButton'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + print(pos,text,size) + new_gui_element = self.world.createGUIButton(pos,text,size) + elif gui_type == "label" and hasattr(self.world, 'createGUILabel'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUILabel(pos,text,size) + elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUIEntry(pos,text,size) + elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('image_path', '') + size = node_data.get('size', 1) + new_gui_element = self.world.createGUI2DImage(pos, image_path, size) + elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'): + print("正在创建3D文本!!!") + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags', {}).get('gui_text', '') + scale = node_data.get('scale', 1) + if isinstance(scale, (list, tuple)): + scale = scale[0] if len(scale) > 0 else 1 + print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}") + new_gui_element = self.world.createGUI3DText(pos, text, scale) + elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('gui_image_path', '') + scale = node_data.get('scale', (1, 1)) + if isinstance(scale, (int, float)): + scale = (scale, scale) + elif isinstance(scale, (list, tuple)) and len(scale) >= 2: + scale = (scale[0], scale[1]) + else: + scale = (1, 1) + print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") + new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale) + elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1,1)) + new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) + elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1, 1)) + new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) + + if new_gui_element: + # 设置名称和变换 + if hasattr(new_gui_element, 'setName'): + new_gui_element.setName(name) + + # 设置位置、旋转、缩放 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + if hasattr(new_gui_element, 'setPos'): + new_gui_element.setPos(*pos) + if hasattr(new_gui_element, 'setHpr'): + new_gui_element.setHpr(*hpr) + if hasattr(new_gui_element, 'setScale'): + new_gui_element.setScale(*scale) + + # 恢复文本内容 + if 'text' in gui_data and hasattr(new_gui_element, 'setText'): + new_gui_element.setText(gui_data['text']) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']: + new_gui_element.setTag(tag_key, str(tag_value)) + + print(f"GUI元素重建成功: {name}") + + return new_gui_element + + except Exception as e: + print(f"重建GUI元素失败: {e}") + import traceback + traceback.print_exc() + return None + + def _recreateModelFromData(self, node_data, parent_node, name): + """根据数据重建模型,保持材质""" + try: + model_data = node_data.get('model_data', {}) + model_path = model_data.get('model_path', model_data.get('file', '')) + + if not model_path or not os.path.exists(model_path): + # 如果原始模型文件不存在,创建一个基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + # 导入模型,保持原有参数 + model = self.importModel( + model_path, + apply_unit_conversion=False, # 已经处理过的模型不需要再次转换 + normalize_scales=False, # 保持原有缩放 + auto_convert_to_glb=False # 已经处理过的模型不需要再次转换 + ) + + if model: + # 设置名称 + model.setName(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + model.setPos(*pos) + model.setHpr(*hpr) + model.setScale(*scale) + + # 恢复材质信息 + try: + self._restoreModelMaterial(model, model_data) + except Exception as e: + print(f"恢复模型材质时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + model.setTag(tag_key, str(tag_value)) + + # 添加到模型列表 + if model not in self.models: + self.models.append(model) + + return model + + except Exception as e: + print(f"重建模型失败: {e}") + # 出错时创建基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + def _restoreModelMaterial(self, model, model_data): + """恢复模型材质""" + try: + # 恢复基础颜色 + if 'base_color' in model_data: + from panda3d.core import ColorAttrib + base_color = model_data['base_color'] + color = (base_color[0], base_color[1], base_color[2], base_color[3]) + model.setColor(color) + + # 恢复复杂材质属性 + if any(key.startswith('material_') for key in model_data.keys()): + from panda3d.core import Material + + # 创建新材质或获取现有材质 + material = Material() + + # 恢复基础颜色 + if 'material_base_color' in model_data: + base_color = model_data['material_base_color'] + material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3])) + + # 恢复环境光颜色 + if 'material_ambient_color' in model_data: + ambient_color = model_data['material_ambient_color'] + material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3])) + + # 恢复漫反射颜色 + if 'material_diffuse_color' in model_data: + diffuse_color = model_data['material_diffuse_color'] + material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])) + + # 恢复高光颜色 + if 'material_specular_color' in model_data: + specular_color = model_data['material_specular_color'] + material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3])) + + # 恢复粗糙度和金属度 + if 'material_roughness' in model_data: + material.setRoughness(model_data['material_roughness']) + + if 'material_metallic' in model_data: + material.setMetallic(model_data['material_metallic']) + + # 应用材质到模型 + model.setMaterial(material) + + except Exception as e: + print(f"恢复材质失败: {e}") + + def _createBasicNodeFromData(self, node_data, parent_node, name): + """创建基本节点,保持视觉属性""" + try: + new_node = parent_node.attachNewNode(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复视觉属性 + try: + # 恢复颜色 + if 'color' in node_data: + color_data = node_data['color'] + new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3]) + + # 恢复材质 + if 'material' in node_data: + from panda3d.core import Material + material_data = node_data['material'] + material = Material() + + if 'base_color' in material_data: + bc = material_data['base_color'] + material.setBaseColor((bc[0], bc[1], bc[2], bc[3])) + + if 'ambient' in material_data: + ac = material_data['ambient'] + material.setAmbient((ac[0], ac[1], ac[2], ac[3])) + + if 'diffuse' in material_data: + dc = material_data['diffuse'] + material.setDiffuse((dc[0], dc[1], dc[2], dc[3])) + + if 'specular' in material_data: + sc = material_data['specular'] + material.setSpecular((sc[0], sc[1], sc[2], sc[3])) + + if 'shininess' in material_data: + material.setShininess(material_data['shininess']) + + new_node.setMaterial(material) + + except Exception as e: + print(f"恢复视觉属性时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + new_node.setTag(tag_key, str(tag_value)) + + return new_node + + except Exception as e: + print(f"创建基本节点失败: {e}") + return None + + def _generateUniqueName(self, base_name, parent_node): + """生成唯一节点名称""" + try: + # 移除可能的数字后缀 + import re + import time + name_base = re.sub(r'_\d+$', '', base_name) + + # 查找现有同名节点 + counter = 1 + unique_name = base_name + while True: + # 检查父节点下是否已存在同名子节点 + existing_node = parent_node.find(unique_name) + if existing_node.isEmpty(): + break + unique_name = f"{name_base}_{counter}" + counter += 1 + if counter > 1000: # 防止无限循环 + unique_name = f"{name_base}_{int(time.time())}" + break + + return unique_name + except: + return f"{base_name}_{int(time.time())}" + diff --git a/scripts/RotatorScript.py b/scripts/RotatorScript.py index cea56986..bdc49495 100644 --- a/scripts/RotatorScript.py +++ b/scripts/RotatorScript.py @@ -21,6 +21,10 @@ class RotatorScript(ScriptBase): self.log(f"旋转速度: {self.rotation_speed_y}度/秒") def update(self, dt): + # 检查 gameObject 是否存在且不为空 + if not self.gameObject or self.gameObject.isEmpty(): + print("RotatorScript: gameObject is empty or None, skipping update") + return """每帧更新""" if not self.is_rotating: return diff --git a/ui/interface_manager.py b/ui/interface_manager.py index e5e0c626..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对象 @@ -339,6 +339,23 @@ class InterfaceManager: groundItem.setData(0, Qt.UserRole, self.world.ground) groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + ground_nodes = [ + ('ground2','地板2'), + ('ground3','地板3'), + ('ground4','地板4'), + ('ground5','地板5'), + ('ground6','地板6') + ] + + for attr_name,display_name in ground_nodes: + if hasattr(self.world,attr_name): + ground_node = getattr(self.world,attr_name) + if ground_node: + extraGroundItem = QTreeWidgetItem(sceneRoot,[display_name]) + extraGroundItem.setData(0,Qt.UserRole,ground_node) + extraGroundItem.setData(0,Qt.UserRole+1,"SCENE_NODE") + + #添加灯光节点 for light in self.world.Spotlight: if light: diff --git a/ui/main_window.py b/ui/main_window.py index 8d7443c3..7e5f5739 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -19,6 +19,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction QSpinBox, QFrame, QRadioButton, QTextEdit) from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import OrthographicLens from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget from ui.icon_manager import get_icon_manager, get_icon @@ -37,6 +38,10 @@ class MainWindow(QMainWindow): self.world = world self.world.main_window = self # 关键:让world对象能访问主窗口 + #剪切板相关属性 + self.clipboard = [] + self.clipboard_mode = None + # 初始化图标管理器并打印调试信息 self.icon_manager = get_icon_manager() print("🔧 图标管理器初始化完成") @@ -447,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)") @@ -816,6 +820,238 @@ class MainWindow(QMainWindow): # 统一连接信号到处理方法 self.connectCreateMenuActions() + def setupViewMenuActions(self): + """设置视图菜单动作""" + # 连接视图菜单事件 + self.viewPerspectiveAction.triggered.connect(self.onViewPerspective) + #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._original_camera_fov = 80 + self._original_camera_pos = (0, -50, 20) + self._original_camera_lookat = (0, 0, 0) + + self._grid_visible = False + def onViewPerspective(self): + """切换到透视视图""" + try: + lens = self.world.cam.node().getLens() + lens.setFov(self._original_camera_fov) + except Exception as e: + print(f"切换到透视视图失败{e}") + + def onViewOrthographic(self): + """切换到正交视图""" + try: + # 保存当前相机设置(如果是透视模式) + lens = self.world.cam.node().getLens() + if not hasattr(self, '_saved_perspective_settings'): + self._saved_perspective_settings = { + 'fov': lens.getFov()[0], + 'pos': self.world.cam.getPos(), + 'hpr': self.world.cam.getHpr() + } + + # 获取窗口尺寸 + win_width, win_height = self.world.getWindowSize() + aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 + + # 修改现有镜头为正交投影 + if not isinstance(lens, OrthographicLens): + # 保存当前镜头类型 + self._original_lens = lens + + # 创建正交镜头并替换现有镜头 + ortho_lens = OrthographicLens() + ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 + ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 + + # 应用正交镜头 + self.world.cam.node().setLens(ortho_lens) + else: + # 如果已经是正交镜头,则调整其参数 + film_height = 20 + film_width = film_height * aspect_ratio + lens.setFilmSize(film_width, film_height) + + print("切换到正交视图") + except Exception as e: + print(f"切换正交视图失败: {e}") + + def onViewTop(self): + """切换到俯视图(正交)""" + try: + # 保存当前设置 + self._saveCurrentCameraSettings() + + # 设置正交投影 + self._setupOrthographicLens() + + # 设置摄像机位置(从上方俯视) + self.world.cam.setPos(0, 0, 30) + self.world.cam.lookAt(0, 0, 0) + self.world.cam.setHpr(0, -90, 0) # 朝下看 + + # 更新菜单项文本 + self._updateViewMenuText() + + print("切换到俯视图") + except Exception as e: + print(f"切换俯视图失败: {e}") + + def onViewFront(self): + """切换到前视图(正交)""" + try: + # 保存当前设置 + self._saveCurrentCameraSettings() + + # 设置正交投影 + self._setupOrthographicLens() + + # 设置摄像机位置(从前方向看) + self.world.cam.setPos(0, -30, 0) + self.world.cam.lookAt(0, 0, 0) + self.world.cam.setHpr(0, 0, 0) # 正面朝向 + + # 更新菜单项文本 + self._updateViewMenuText() + + print("切换到前视图") + except Exception as e: + print(f"切换前视图失败: {e}") + + def onViewGrid(self): + """切换网格显示/隐藏""" + try: + # 切换网格显示状态 + self._grid_visible = not self._grid_visible + + # 查找网格节点 + grid_node = self.world.render.find("**/grid") + if grid_node.isEmpty(): + # 如果网格不存在则创建 + self._createGridView() + grid_node = self.world.render.find("**/grid") + + # 设置网格可见性 + if not grid_node.isEmpty(): + if self._grid_visible: + grid_node.show() + self.viewGridAction.setText("隐藏网格") + print("网格已显示") + else: + grid_node.hide() + self.viewGridAction.setText("显示网格") + print("网格已隐藏") + else: + print("网格节点未找到") + + except Exception as e: + print(f"切换网格显示失败: {e}") + def _createGridView(self): + """创建网格视图""" + try: + from panda3d.core import LineSegs,Vec3 + grid_node = self.world.render.attachNewNode("grid") + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(0.3,0.3,0.3,1.0) + grid_size = 20 + grid_step = 1 + + for i in range(-grid_size,grid_size+1,grid_step): + lines.moveTo(Vec3(-grid_size,i,0)) + lines.drawTo(Vec3(grid_size,i,0)) + + grid_node.attachNewNode(lines.create()) + # 添加中心轴线(红色X轴,绿色Y轴) + axis_lines = LineSegs() + axis_lines.setThickness(2.0) + # X轴(红色) + axis_lines.setColor(1.0,0.0,0.0,1.0) + axis_lines.moveTo(Vec3(0,0,0)) + axis_lines.drawTo(Vec3(grid_size,0,0)) + # Y轴(绿色) + axis_lines.setColor(0.0, 1.0, 0.0, 1.0) + axis_lines.moveTo(Vec3(0, 0, 0)) + axis_lines.drawTo(Vec3(0, grid_size, 0)) + + grid_node.attachNewNode(axis_lines.create()) + + print("网格已创建") + except Exception as e: + print(f"创建网格失败{e}") + + def _saveCurrentCameraSettings(self): + """保存当前相机设置""" + try: + lens = self.world.cam.node().getLens() + self._saved_camera_settings = { + 'lens_type': 'perspective' if not isinstance(lens, OrthographicLens) else 'orthographic', + 'fov': lens.getFov()[0] if hasattr(lens, 'getFov') else None, + 'film_size': (lens.getFilmSize()[0], lens.getFilmSize()[1]) if hasattr(lens, 'getFilmSize') else None, + 'pos': self.world.cam.getPos(), + 'hpr': self.world.cam.getHpr() + } + except Exception as e: + print(f"保存相机设置失败: {e}") + + def _setupOrthographicLens(self): + """设置正交镜头""" + try: + win_width, win_height = self.world.getWindowSize() + aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 + + from panda3d.core import OrthographicLens + ortho_lens = OrthographicLens() + ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 + ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 + + self.world.cam.node().setLens(ortho_lens) + except Exception as e: + print(f"设置正交镜头失败: {e}") + + def _updateViewMenuText(self): + """更新视图菜单文本""" + try: + lens = self.world.cam.node().getLens() + from panda3d.core import OrthographicLens + + # 更新正交/透视视图动作文本 + if isinstance(lens, OrthographicLens): + self.viewOrthographicAction.setText("切换到透视视图") + self.viewOrthographicAction.triggered.disconnect() + self.viewOrthographicAction.triggered.connect(self.onViewPerspective) + else: + self.viewOrthographicAction.setText("切换到正交视图") + self.viewOrthographicAction.triggered.disconnect() + self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) + except Exception as e: + print(f"更新视图菜单文本失败: {e}") + + # 如果需要在窗口大小改变时调整正交镜头,可以添加以下方法 + def _onWindowResized(self): + """窗口大小改变时的处理""" + try: + lens = self.world.cam.node().getLens() + from panda3d.core import OrthographicLens + + # 如果当前是正交镜头,需要根据新窗口大小调整 + if isinstance(lens, OrthographicLens): + win_width, win_height = self.world.getWindowSize() + if win_height != 0: + aspect_ratio = win_width / win_height + film_height = 20 + film_width = film_height * aspect_ratio + lens.setFilmSize(film_width, film_height) + except Exception as e: + print(f"窗口大小调整失败: {e}") + + def connectCreateMenuActions(self): """统一连接创建菜单的信号到处理方法""" # 连接到world对象的创建方法 @@ -1572,6 +1808,9 @@ class MainWindow(QMainWindow): self.buildAction.triggered.connect(lambda: buildPackage(self)) self.exitAction.triggered.connect(QApplication.instance().quit) + #添加保存项目快捷键盘 + self.saveAction.setShortcut(QKeySequence.Save) + # 连接工具事件 self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑")) self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑")) @@ -1601,6 +1840,21 @@ class MainWindow(QMainWindow): lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0)) print("已连接点击信号") + self.undoAction.triggered.connect(self.onUndo) + self.redoAction.triggered.connect(self.onRedo) + self.cutAction.triggered.connect(self.onCut) + self.copyAction.triggered.connect(self.onCopy) + self.pasteAction.triggered.connect(self.onPaste) + + self.undoAction.setShortcut(QKeySequence.Undo) + self.redoAction.setShortcut(QKeySequence.Redo) + self.cutAction.setShortcut(QKeySequence.Cut) + self.copyAction.setShortcut(QKeySequence.Copy) + self.pasteAction.setShortcut(QKeySequence.Paste) + + #连接视图菜单事件 + self.setupViewMenuActions() + # 连接工具切换信号 #self.toolGroup.buttonClicked.connect(self.onToolChanged) @@ -1611,6 +1865,313 @@ class MainWindow(QMainWindow): # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + def onCopy(self): + """复制操作""" + try: + selected_item = self.treeWidget.currentItem() + if not selected_item: + QMessageBox.information(self, "提示", "请先选择要复制的节点") + return + + # 获取选中的节点 + selected_node = getattr(selected_item, 'node_path', None) + if not selected_node: + selected_node = getattr(selected_item, 'node', None) + + if not selected_node and hasattr(self.world, 'selection'): + selected_node = getattr(self.world.selection, 'selectedNode', None) + + if not selected_node or selected_node.isEmpty(): + QMessageBox.warning(self, "错误", "无法获取选中节点") + return + + # 检查是否是根节点 + if selected_node.getName() == "render": + QMessageBox.warning(self, "错误", "不能复制根节点") + return + + # 序列化节点数据 + node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) + if not node_data: + QMessageBox.warning(self, "错误", "无法序列化选中节点") + return + + # 存储到剪切板 + self.clipboard = [node_data] + self.clipboard_mode = "copy" + + QMessageBox.information(self, "成功", "节点已复制到剪切板") + + except Exception as e: + QMessageBox.critical(self, "错误", f"复制操作失败: {str(e)}") + + def onCut(self): + """剪切操作""" + try: + selected_item = self.treeWidget.currentItem() + if not selected_item: + QMessageBox.information(self, "提示", "请先选择要剪切的节点") + return + + # 获取选中的节点 + selected_node = getattr(selected_item, 'node_path', None) + if not selected_node: + selected_node = getattr(selected_item, 'node', None) + + if not selected_node and hasattr(self.world, 'selection'): + selected_node = getattr(self.world.selection, 'selectedNode', None) + + if not selected_node or selected_node.isEmpty(): + QMessageBox.warning(self, "错误", "无法获取选中节点") + return + + # 检查是否是根节点或特殊节点 + if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]: + QMessageBox.warning(self, "错误", "不能剪切根节点或系统节点") + return + + # 序列化节点数据 + node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) + if not node_data: + QMessageBox.warning(self, "错误", "无法序列化选中节点") + return + + # 存储到剪切板 + self.clipboard = [node_data] + self.clipboard_mode = "cut" + + # 删除原节点 + self.treeWidget.delete_items([selected_item]) + + QMessageBox.information(self, "成功", "节点已剪切到剪切板") + + except Exception as e: + QMessageBox.critical(self, "错误", f"剪切操作失败: {str(e)}") + + def onPaste(self): + """粘贴操作""" + try: + if not self.clipboard: + QMessageBox.information(self, "提示", "剪切板为空") + return + + # 获取粘贴目标节点 + parent_item = self.treeWidget.currentItem() + parent_node = None + + # 如果选中了节点,将其作为父节点 + if parent_item: + parent_node = getattr(parent_item, 'node_path', None) + if not parent_node: + parent_node = getattr(parent_item, 'node', None) + + # 确保获取到有效的父节点 + if parent_node and not parent_node.isEmpty(): + print(f"将粘贴到选中的节点: {parent_node.getName()}") + else: + parent_node = None + + # 如果没有选中有效节点,默认粘贴到render节点下 + if not parent_node: + print("未选中有效节点,将粘贴到根节点下") + # 查找render节点 + for i in range(self.treeWidget.topLevelItemCount()): + item = self.treeWidget.topLevelItem(i) + if item.text(0) == "render": + parent_item = item + break + + # 如果找到了render节点项,获取对应的节点 + if parent_item: + parent_node = getattr(parent_item, 'node_path', None) + if not parent_node: + parent_node = getattr(parent_item, 'node', None) + + # 如果仍然没有找到父节点项,直接使用world.render + if not parent_node: + parent_node = self.world.render + + # 检查父节点有效性 + if not parent_node or parent_node.isEmpty(): + QMessageBox.warning(self, "错误", "无法获取有效的父节点") + return + + # 检查目标节点是否为允许的父节点类型 + parent_name = parent_node.getName() + if parent_name in ["camera", "ambientLight", "directionalLight"]: + QMessageBox.warning(self, "错误", "不能粘贴到该类型节点下") + return + + # 粘贴节点 + pasted_nodes = [] + for node_data in self.clipboard: + print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}") + new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node) + if new_node: + pasted_nodes.append(new_node) + print(f"成功粘贴节点: {new_node.getName()}") + else: + print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}") + + # 如果是剪切操作,清空剪切板 + if self.clipboard_mode == "cut": + self.clipboard.clear() + self.clipboard_mode = None + + QMessageBox.information(self, "成功", f"已粘贴 {len(pasted_nodes)} 个节点") + + except Exception as e: + QMessageBox.critical(self, "错误", f"粘贴操作失败: {str(e)}") + + def _serializeNode(self, node): + """序列化节点数据""" + try: + if not node or node.isEmpty(): + return None + + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + try: + # 使用更安全的方式获取标签 + if hasattr(node, 'getTagKeys'): + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + except Exception as e: + print(f"获取标签时出错: {e}") + + # 递归序列化子节点(跳过辅助节点) + try: + if hasattr(node, 'getChildren'): + for child in node.getChildren(): + # 跳过辅助节点 + child_name = child.getName() if hasattr(child, 'getName') else "" + if not child_name.startswith(('gizmo', 'selectionBox', 'grid')): + child_data = self._serializeNode(child) + if child_data: + node_data['children'].append(child_data) + except Exception as e: + print(f"序列化子节点时出错: {e}") + + return node_data + + except Exception as e: + print(f"序列化节点失败: {e}") + return None + + def _deserializeNode(self, node_data, parent_node): + """反序列化节点数据""" + try: + if not node_data or not parent_node or parent_node.isEmpty(): + return None + + # 创建新节点 + node_name = node_data.get('name', 'node') + new_node = parent_node.attachNewNode(node_name) + + # 设置变换 + try: + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + except Exception as e: + print(f"设置变换时出错: {e}") + + # 恢复标签 + try: + for tag_key, tag_value in node_data.get('tags', {}).items(): + new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串 + except Exception as e: + print(f"恢复标签时出错: {e}") + + # 递归创建子节点 + try: + for child_data in node_data.get('children', []): + self._deserializeNode(child_data, new_node) + except Exception as e: + print(f"创建子节点时出错: {e}") + + return new_node + + except Exception as e: + print(f"反序列化节点失败: {e}") + return None + + def _deleteNode(self, node, tree_item): + """删除节点""" + try: + if not node or node.isEmpty(): + return + + # 特殊处理选中节点 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node: + self.world.selection.clearSelection() + + # 从场景中删除节点 + node.removeNode() + + # 从树形控件中删除项目 + if tree_item: + try: + parent = tree_item.parent() + if parent: + parent.removeChild(tree_item) + else: + index = self.treeWidget.indexOfTopLevelItem(tree_item) + if index >= 0: + self.treeWidget.takeTopLevelItem(index) + except Exception as e: + print(f"从树形控件删除项目时出错: {e}") + + except Exception as e: + print(f"删除节点失败: {e}") + + # 添加撤销/重做功能的基础实现 + def onUndo(self): + """撤销操作""" + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_undo(): + success = self.world.command_manager.undo() + if success: + print("成功操作") + else: + print("撤销失败") + QMessageBox.information(self,"提示","撤销操作失败") + else: + print("没有可撤销的操作") + QMessageBox.information(self,"提示","没有可撤销的操作") + else: + print("命令管理器未初始化") + QMessageBox.information(self,"提示","命令系统未初始化") + + def onRedo(self): + """重做操作""" + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_redo(): + success = self.world.command_manager.redo() + if success: + print("成功重做") + else: + print("重做失败") + QMessageBox.information(self,"提示","重做操作失败") + else: + print("没有可重做的操作") + QMessageBox.information(self,"提示","没有可重做的操作") + else: + print("命令管理器未初始化") + QMessageBox.information(self,"提示","命令系统未初始化") def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: diff --git a/ui/property_panel.py b/ui/property_panel.py index d73175b1..eba770b4 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 @@ -105,9 +106,10 @@ class PropertyPanelManager: def updatePropertyPanel(self, item): """更新属性面板显示""" - if not self._propertyLayout or not self._propertyLayout.parent(): - print("属性布局未设置或没有父部件!") - return + # if not self._propertyLayout or not self._propertyLayout.parent(): + # print("属性布局未设置或没有父部件!") + # return + #更健壮的有效性检查 self._cleanupAllReferences() self.clearPropertyPanel() @@ -768,6 +770,7 @@ class PropertyPanelManager: if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见 # 检查对象是否仍然有效 spinbox.blockSignals(True) + spinbox.setKeyboardTracking(False) # 确保禁用键盘跟踪 spinbox.setValue(value) spinbox.blockSignals(False) except RuntimeError as e: @@ -790,42 +793,79 @@ class PropertyPanelManager: # 位置控件 transform_layout.addWidget(QLabel("相对位置"), 0, 0) - # 创建并设置 X, Y, Z 标签居中 - x_label = QLabel("X") - y_label = QLabel("Y") - z_label = QLabel("Z") - x_label.setAlignment(Qt.AlignCenter) - y_label.setAlignment(Qt.AlignCenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(y_label, 0, 2) - transform_layout.addWidget(z_label, 0, 3) - - # 位置数值输入框 - self.pos_x = self._createSafeSpinBox(-1000, 1000) - self.pos_y = self._createSafeSpinBox(-1000, 1000) - self.pos_z = self._createSafeSpinBox(-1000, 1000) - + # X坐标 + transform_layout.addWidget(QLabel("X:"), 1, 0) + self.pos_x = QLineEdit() + self.pos_x.setText(str(round(nodePath.getX(), 6))) + self.pos_x.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'x')) transform_layout.addWidget(self.pos_x, 1, 1) - transform_layout.addWidget(self.pos_y, 1, 2) - transform_layout.addWidget(self.pos_z, 1, 3) - # 世界位置 (只读) - transform_layout.addWidget(QLabel("世界位置"), 2, 0) - self.world_pos_x = self._createSafeSpinBox(-10000, 10000, True) # 只读 - self.world_pos_y = self._createSafeSpinBox(-10000, 10000, True) - self.world_pos_z = self._createSafeSpinBox(-10000, 10000, True) + # Y坐标 + transform_layout.addWidget(QLabel("Y:"), 1, 2) + self.pos_y = QLineEdit() + self.pos_y.setText(str(round(nodePath.getY(), 6))) + self.pos_y.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'y')) + transform_layout.addWidget(self.pos_y, 1, 3) - transform_layout.addWidget(self.world_pos_x, 2, 1) - transform_layout.addWidget(self.world_pos_y, 2, 2) - transform_layout.addWidget(self.world_pos_z, 2, 3) + # Z坐标 + transform_layout.addWidget(QLabel("Z:"), 1, 4) + self.pos_z = QLineEdit() + self.pos_z.setText(str(round(nodePath.getZ(), 6))) + self.pos_z.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'z')) + transform_layout.addWidget(self.pos_z, 1, 5) return transform_layout + except Exception as e: - print(f"创建变换控件失败: {e}") + print(f"创建变换控件时出错: {e}") return None + def _onPositionEditFinished(self, nodePath, axis): + """位置编辑完成时的处理""" + try: + # 检查控件是否仍然有效 + if not hasattr(self, f'pos_{axis}') or getattr(self, f'pos_{axis}') is None: + return + + line_edit = getattr(self, f'pos_{axis}') + if line_edit is None or line_edit.isHidden(): + return + + # 获取文本并转换为数值 + text = line_edit.text() + try: + new_value = float(text) + except ValueError: + print(f"无效的数值输入: {text}") + # 恢复原来的值 + if axis == 'x': + line_edit.setText(str(round(nodePath.getX(), 6))) + elif axis == 'y': + line_edit.setText(str(round(nodePath.getY(), 6))) + elif axis == 'z': + line_edit.setText(str(round(nodePath.getZ(), 6))) + return + + # 根据轴设置位置 + if axis == 'x': + nodePath.setX(new_value) + elif axis == 'y': + nodePath.setY(new_value) + elif axis == 'z': + nodePath.setZ(new_value) + + print(f"位置已更新: {nodePath.getName()} {axis.upper()} = {new_value}") + + # 如果是坐标轴节点,需要更新坐标轴位置 + if hasattr(self.world, 'selection_manager'): + selection_manager = self.world.selection_manager + if (hasattr(selection_manager, 'gizmoTarget') and + selection_manager.gizmoTarget == nodePath): + # 更新坐标轴位置 + selection_manager._updateGizmoPositionAndOrientation() + + except Exception as e: + print(f"更新位置时出错: {e}") def _createSafeSpinBox(self, min_val, max_val, read_only=False): """创建安全的数值框""" try: @@ -3346,6 +3386,12 @@ class PropertyPanelManager: video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) path_label = QLabel(video_path) path_label.setWordWrap(True) + if len(video_path)>30: + display_path = video_path[:27]+"..." + path_label.setToolTip(video_path) + else: + display_path = video_path + path_label.setText(display_path) path_label.setStyleSheet("color: #00AAFF;") video_info_layout.addWidget(path_label, 0, 1) elif os.path.exists(video_path): @@ -8570,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() @@ -8805,9 +8874,9 @@ except Exception as e: current_row += 1 # X, Y, Z 位置调整 - self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) - self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) - self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): @@ -8857,7 +8926,7 @@ except Exception as e: spinbox = QDoubleSpinBox() spinbox.setRange(min_val, max_val) spinbox.setDecimals(decimals) - spinbox.setSingleStep(0.1) + spinbox.setSingleStep(0.01) return spinbox def _addSphereParameters(self, model, layout, start_row): @@ -8868,7 +8937,7 @@ except Exception as e: radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) - self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): @@ -8900,9 +8969,9 @@ except Exception as e: current_row += 1 # 宽度、长度、高度 - self.collision_width = self._createCollisionSpinBox(0.1, 100, 2) - self.collision_length = self._createCollisionSpinBox(0.1, 100, 2) - self.collision_height = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2) + self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2) + self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): @@ -8950,7 +9019,7 @@ except Exception as e: radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) - self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): @@ -8978,7 +9047,7 @@ except Exception as e: height_label = QLabel("高度:") layout.addWidget(height_label, current_row, 0) - self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 if not self._hasCollision(model): @@ -9847,7 +9916,7 @@ except Exception as e: radius_label.setVisible(True) layout.addWidget(radius_label, current_row, 0) - self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_radius.setVisible(True) self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) layout.addWidget(self.collision_radius, current_row, 1) @@ -9864,9 +9933,9 @@ except Exception as e: layout.addWidget(size_label, current_row, 0) current_row += 1 - self.collision_width = self._createCollisionSpinBox(0.1, 100, 2) - self.collision_length = self._createCollisionSpinBox(0.1, 100, 2) - self.collision_height = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2) width_label = QLabel("宽度:") width_label.setVisible(True) @@ -9903,7 +9972,7 @@ except Exception as e: radius_label = QLabel("半径:") layout.addWidget(radius_label, current_row, 0) - self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) layout.addWidget(self.collision_capsule_radius, current_row, 1) current_row += 1 @@ -9911,7 +9980,7 @@ except Exception as e: height_label = QLabel("高度:") layout.addWidget(height_label, current_row, 0) - self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) layout.addWidget(self.collision_capsule_height, current_row, 1) current_row += 1 diff --git a/ui/widgets.py b/ui/widgets.py index e6ef58f6..d5978703 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -1367,6 +1367,27 @@ class CustomConsoleDockWidget(QWidget): """) toolbar.addWidget(self.timestampBtn) + self.fpsLabel = QLabel("FPS:0.0") + self.fpsLabel.setStyleSheet(""" + QLabel { + background-color: #2d2d44; + color: #80ff80; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + font-family: 'Consolas', 'Monaco', monospace; + } + """) + self.fpsLabel.setMinimumWidth(100) + self.fpsLabel.setAlignment(Qt.AlignCenter) + toolbar.addWidget(self.fpsLabel) + + # 帧率更新定时器 + self.fpsTimer = QTimer() + self.fpsTimer.timeout.connect(self.updateFPS) + self.fpsTimer.start(1000) # 每秒更新一次 + toolbar.addStretch() layout.addLayout(toolbar) @@ -1408,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget): # 添加欢迎信息 self.addMessage("🎮 编辑器控制台已启动", "INFO") + def updateFPS(self): + try: + if hasattr(self.world,'clock'): + fps = self.world.clock.getAverageFrameRate() + self.fpsLabel.setText(f"FPS:{fps:.1f}") + + # 根据帧率设置颜色 + if fps >= 50: + color = "#80ff80" # 绿色 - 优秀 + elif fps >= 30: + color = "#ffff80" # 黄色 - 一般 + else: + color = "#ff8080" # 红色 - 较差 + + self.fpsLabel.setStyleSheet(f""" + QLabel {{ + background-color: #2d2d44; + color: {color}; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + font-family: 'Consolas', 'Monaco', monospace; + }} + """) + except Exception as e: + pass # 静默处理错误,避免影响控制台功能 + def setupConsoleRedirect(self): """设置控制台重定向""" import sys @@ -1873,7 +1922,7 @@ class CustomTreeWidget(QTreeWidget): elif is_dragged_3d_gui: if is_target_3d_scene: print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return True + return False elif is_target_2d_gui: print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") @@ -1899,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget): elif is_target_3d_gui: print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下") print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") - return True + return False else: print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") return False @@ -2089,7 +2138,7 @@ class CustomTreeWidget(QTreeWidget): return # 默认选中场景根节点,通常是第一个顶级节点 - next_item_to_select = self.topLevelItem(0) + #next_item_to_select = self.topLevelItem(0) # 3. 执行删除循环 deleted_count = 0 @@ -2116,7 +2165,6 @@ class CustomTreeWidget(QTreeWidget): if hasattr(panda_node, 'getPythonTag'): light_object = panda_node.getPythonTag('rp_light_object') if light_object and hasattr(self.world, 'render_pipeline'): - print(f'11111111111111111111111111,{light_object.casts_shadows}') self.world.render_pipeline.remove_light(light_object) # 从world列表中移除 @@ -2174,25 +2222,7 @@ class CustomTreeWidget(QTreeWidget): # 4. 删除操作完成后,更新UI --- if deleted_count > 0: print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") - - # 检查预备选择的节点是否还有效 (例如,父节点可能也一同被删了) - # 如果next_item_to_select在树中找不到了,就退回到选择根节点 - if next_item_to_select and self.indexFromItem(next_item_to_select).isValid(): - new_selection_item = next_item_to_select - else: - # 如果之前的父节点也一并被删除了,就默认选择场景根节点 - new_selection_item = self.topLevelItem(0) - - if new_selection_item: - # 设置UI树的选择 - self.setCurrentItem(new_selection_item) - # 获取对应的Panda3D节点 - new_panda_node = new_selection_item.data(0, Qt.UserRole) - # 调用您提供的函数来更新选择状态和属性面板 - #self.update_selection_and_properties(new_panda_node, new_selection_item) - else: - # 如果连根节点都没有了(例如清空场景),则清空选择 - self.update_selection_and_properties(None, None) + self.update_selection_and_properties(None, None) def delete_item(self, panda_node): """删除指定节点 panda3D(node)- 优化和修复版本""" @@ -2200,6 +2230,14 @@ class CustomTreeWidget(QTreeWidget): print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") return + # #如果有命令管理系统,则使用命令系统 + # if hasattr(self.world,'command_manager') and self.world.command_manager: + # from core.Command_System import DeleteNodeCommand + # parent_node = panda_node.getParent() + # command = DeleteNodeCommand(panda_node,parent_node) + # self.world.command_manager.execute_command(command) + # return + # --- 关键修复:在操作前,安全地获取节点名字 --- node_name_for_logging = panda_node.getName()