From 26d10f41172d919d9a6a42eee20adfeff839ecbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Wed, 10 Sep 2025 09:12:38 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9Bug=EF=BC=8820250910?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 2 +- RenderPipelineFile/rpcore/light_manager.py | 4 + RenderPipelineFile/rpcore/native/__init__.py | 2 + .../native/source/pointer_slot_storage.h | 7 +- .../rpcore/pynative/internal_light_manager.py | 22 + RenderPipelineFile/rpcore/render_pipeline.py | 1 + core/terrain_manager.py | 410 ++++++++----- gui/gui_manager.py | 560 +++++++++--------- main.py | 2 +- project/project_manager.py | 9 +- scene/scene_manager.py | 135 +++-- ui/interface_manager.py | 160 ++--- ui/main_window.py | 10 +- ui/property_panel.py | 59 +- ui/widgets.py | 416 +++++++++---- 15 files changed, 1096 insertions(+), 703 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index a0ea9286..51b9fc17 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/RenderPipelineFile/rpcore/light_manager.py b/RenderPipelineFile/rpcore/light_manager.py index b24ec006..fe9e4ceb 100644 --- a/RenderPipelineFile/rpcore/light_manager.py +++ b/RenderPipelineFile/rpcore/light_manager.py @@ -90,6 +90,10 @@ class LightManager(RPObject): def remove_light(self, light): """ Removes a light """ + print(f'333333333333333333333333333333,{light.casts_shadows}') + # from RenderPipelineFile.rpcore.pynative.internal_light_manager import InternalLightManager + # inter = InternalLightManager() + # inter.remove_light(light) self.internal_mgr.remove_light(light) self.pta_max_light_index[0] = self.internal_mgr.max_light_index diff --git a/RenderPipelineFile/rpcore/native/__init__.py b/RenderPipelineFile/rpcore/native/__init__.py index 3ba9d575..16fa5411 100644 --- a/RenderPipelineFile/rpcore/native/__init__.py +++ b/RenderPipelineFile/rpcore/native/__init__.py @@ -73,6 +73,7 @@ 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") @@ -80,6 +81,7 @@ if NATIVE_CXX_LOADED: RPObject.global_debug("CORE", "Using native core module") from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position else: + print(f'343434343434343434343434343') from rpcore import pynative as _native_module # pylint: disable=wrong-import-position RPObject.global_debug("CORE", "Using simulated python-wrapper module") diff --git a/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h b/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h index 979732ec..4c3e6964 100644 --- a/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h +++ b/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h @@ -161,7 +161,12 @@ public: // Update maximum index if (slot == _max_index) { - while (_max_index >= 0 && !_data[_max_index--]); + while (_max_index >= 0 && !_data[_max_index--]); + // 正确的修复代码 +// while (_max_index >= 0 && _data[_max_index] == NULL) { +// _max_index--; +// } + } } } diff --git a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py index 1d8c480d..69b84381 100644 --- a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py +++ b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py @@ -116,6 +116,25 @@ class InternalLightManager(object): source.set_slot(slot) def remove_light(self, light): + print(f'44444444444444444444444444') + print("\n" + "=" * 50) + print(f"DEBUG: Entering remove_light for light object: {light.casts_shadows}") + if light: + print(f" - Light's Slot: {light.get_slot() if light.has_slot() else 'No Slot'}") + print(f" - Does it cast shadows? light.get_casts_shadows() -> {light.get_casts_shadows()}") + else: + print(" - Light object is None!") + + print("\n --- State of Light System BEFORE removal ---") + if hasattr(self, '_lights') and hasattr(self._lights, '_data'): + # 打印出当前所有灯光对象,看看有没有异常 + print(f" - Light Data Array (_data):") + for i, l_obj in enumerate(self._lights._data): + print(f" [{i}]: {l_obj}") + + print(f"\n - Max Index (_max_index): {self._lights._max_index}") + print(f" - Num Entries (_num_entries): {self._lights._num_entries}") + print("=" * 50 + "\n") assert light is not None if not light.has_slot(): print("ERROR: Could not detach light, light was not attached!") @@ -132,6 +151,9 @@ class InternalLightManager(object): # 关键修复:先保存第一个source的slot,再清理 first_source = light.get_shadow_source(0) first_source_slot = first_source.get_slot() # 保存slot值 + + # --- 在这里加上打印语句 --- + print(f"DEBUG: Removing shadow sources. Start Slot: {first_source_slot}, Count: {num_sources}") # 先发送GPU移除命令(在清理之前) cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources) diff --git a/RenderPipelineFile/rpcore/render_pipeline.py b/RenderPipelineFile/rpcore/render_pipeline.py index 0424783f..a7ac29df 100644 --- a/RenderPipelineFile/rpcore/render_pipeline.py +++ b/RenderPipelineFile/rpcore/render_pipeline.py @@ -208,6 +208,7 @@ class RenderPipeline(RPObject): def remove_light(self, light): """ Removes a previously attached light, check out the LightManager remove_light documentation for further information. """ + print(f'222222222222222222222222222,{light.casts_shadows}') self.light_mgr.remove_light(light) def load_ies_profile(self, filename): diff --git a/core/terrain_manager.py b/core/terrain_manager.py index 07ae3d7f..bb7e0e47 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -1,9 +1,13 @@ # core/terrain_manager.py import time +import urllib + from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight import os +from scene import util + class TerrainManager: """地形管理类""" @@ -31,106 +35,148 @@ class TerrainManager: return None try: - # 创建GeoMipTerrain对象 - terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain = GeoMipTerrain(terrain_name) + print(f"🔆 开始创建高度图地形") - # 加载高度图 - height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) - - # 检查并调整图像尺寸为2的幂次方加1 - width, height = height_image.getXSize(), height_image.getYSize() - print(f"原始图像尺寸: {width}x{height}") - - # 找到最接近的有效尺寸 - valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] - target_size = 129 # 默认尺寸 - - # 选择最接近的尺寸 - max_dim = max(width, height) - for size in valid_sizes: - if size >= max_dim: - target_size = size - break - else: - target_size = valid_sizes[-1] # 使用最大尺寸 - - # 如果需要,调整图像尺寸 - if width != target_size or height != target_size: - print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") - # 使用正确的图像缩放方法 - resized_image = PNMImage(target_size, target_size) - resized_image.quickFilterFrom(height_image) - height_image = resized_image - - # 使用正确的方法设置高度图 - terrain.setHeightfield(height_image) - - # 设置地形参数 - terrain.setBruteforce(True) # 使用LOD - - terrain.setBlockSize(32) - # terrain.setNearFarThreshold(50.0,200.0) - - # 生成地形 - terrain.generate() - - # 获取地形节点 - terrain_node = terrain.getRoot() - - if terrain_node.isEmpty(): - print("错误:无法生成有效的地形节点") + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") return None - node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" - terrain_node.setName(node_name) + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None - center_offset = (target_size - 1) / 2 - terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) + created_terrains = [] + try: + parent_item, parent_node = target_parents[0] - # 设置缩放 - terrain_node.setScale(scale[0], scale[1], scale[2]) + # 创建GeoMipTerrain对象 + terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) - # 将地形添加到场景中 - terrain_node.reparentTo(self.world.render) + # 加载高度图 + height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) - from panda3d.core import BitMask32 - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 - # 为地形的所有子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) + # 检查并调整图像尺寸为2的幂次方加1 + width, height = height_image.getXSize(), height_image.getYSize() + print(f"原始图像尺寸: {width}x{height}") - # 添加材质 - self._applyTerrainMaterial(terrain_node) + # 找到最接近的有效尺寸 + valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] + target_size = 129 # 默认尺寸 - terrain_node.setPythonTag("selectable", True) + # 选择最接近的尺寸 + max_dim = max(width, height) + for size in valid_sizes: + if size >= max_dim: + target_size = size + break + else: + target_size = valid_sizes[-1] # 使用最大尺寸 - # 保存地形信息(包括高度图的副本) - terrain_info = { - 'terrain': terrain, - 'node': terrain_node, - 'heightmap': heightmap_path, - 'heightfield': height_image, # 保存高度图副本 - 'scale': scale, - 'name': node_name - } + # 如果需要,调整图像尺寸 + if width != target_size or height != target_size: + print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") + # 使用正确的图像缩放方法 + resized_image = PNMImage(target_size, target_size) + resized_image.quickFilterFrom(height_image) + height_image = resized_image - self.terrains.append(terrain_info) + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) - # 更新场景树(再次检查节点是否有效) - if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, - 'updateSceneTree'): - try: - self.world.scene_manager.updateSceneTree() - except Exception as e: - print(f"警告: 更新场景树时出错: {e}") + # 设置地形参数 + terrain.setBruteforce(True) # 使用LOD - print(f"✓ 成功从 {heightmap_path} 创建地形") - return terrain_info + terrain.setBlockSize(32) + # terrain.setNearFarThreshold(50.0,200.0) + + # 生成地形 + terrain.generate() + + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的地形节点") + return None + + node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" + terrain_node.setName(node_name) + + center_offset = (target_size - 1) / 2 + terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) + + # 设置缩放 + terrain_node.setScale(scale[0], scale[1], scale[2]) + + # 将地形添加到场景中 + terrain_node.reparentTo(parent_node) + + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + terrain_node.setPythonTag("selectable", True) + + # 保存地形信息(包括高度图的副本) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': heightmap_path, + 'heightfield': height_image, # 保存高度图副本 + 'scale': scale, + 'name': node_name + } + + self.terrains.append(terrain_info) + + print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: + created_terrains.append((terrain_node, qt_item)) + else: + created_terrains.append((terrain_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}") + return None + + # 处理创建结果 + if not created_terrains: + print("❌ 没有成功创建任何高度图地形") + return None + + # 选中最后创建的光源 + if created_terrains: + last_light_np, last_qt_item = created_terrains[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形") + + # 返回值处理 + if len(created_terrains) == 1: + return created_terrains[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 except Exception as e: - print(f"创建地形时出错: {e}") + print(f"❌ 创建高度图地形过程失败: {str(e)}") import traceback traceback.print_exc() return None @@ -138,84 +184,126 @@ class TerrainManager: def createFlatTerrain(self, size=(0.3, 0.3), resolution=129): """创建平面地形""" try: - # 确保分辨率是2的幂次方加1 (如129, 257, 513等) - valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] - if resolution not in valid_resolutions: - # 找到最接近的有效分辨率 - closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) - print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") - resolution = closest_res + print(f"🔆 开始创建平面地形") - # 创建GeoMipTerrain对象 - terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain = GeoMipTerrain(terrain_name) - - # 创建一个平面高度图,尺寸必须是2^n+1 - height_image = PNMImage(resolution, resolution) - height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) - - # 使用正确的方法设置高度图 - terrain.setHeightfield(height_image) - terrain.setBruteforce(True) # 设置LOD - - # 设置地形参数 - terrain.setBlockSize(32) # 设置块大小 - - # 生成地形 - terrain.generate() - - # 获取地形节点 - terrain_node = terrain.getRoot() - - if terrain_node.isEmpty(): - print("错误:无法生成有效的平面地形节点") + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") return None - node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain_node.setName(node_name) + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None - center_offset = (resolution - 1) / 2 - terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) + created_terrains = [] - # 将地形添加到场景中 - terrain_node.reparentTo(self.world.render) + try: + parent_item, parent_node = target_parents[0] + # 确保分辨率是2的幂次方加1 (如129, 257, 513等) + valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] + if resolution not in valid_resolutions: + # 找到最接近的有效分辨率 + closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) + print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") + resolution = closest_res - # 为地形添加碰撞体 - from panda3d.core import BitMask32 - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 - # 为地形的所有子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) + # 创建GeoMipTerrain对象 + terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) - # 添加材质 - self._applyTerrainMaterial(terrain_node) + # 创建一个平面高度图,尺寸必须是2^n+1 + height_image = PNMImage(resolution, resolution) + height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) - # 保存地形信息(包括高度图) - terrain_info = { - 'terrain': terrain, - 'node': terrain_node, - 'heightmap': None, - 'heightfield': height_image, # 保存高度图 - 'scale': (size[0], size[1], 50), - 'name': node_name - } + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) + terrain.setBruteforce(True) # 设置LOD - self.terrains.append(terrain_info) + # 设置地形参数 + terrain.setBlockSize(32) # 设置块大小 - # 更新场景树(再次检查节点是否有效) - if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, - 'updateSceneTree'): - try: - self.world.scene_manager.updateSceneTree() - except Exception as e: - print(f"警告: 更新场景树时出错: {e}") + # 生成地形 + terrain.generate() - print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}") - return terrain_info + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的平面地形节点") + return None + + node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain_node.setName(node_name) + + center_offset = (resolution - 1) / 2 + terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) + + # 将地形添加到场景中 + terrain_node.reparentTo(parent_node) + + # 为地形添加碰撞体 + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + # 保存地形信息(包括高度图) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': None, + 'heightfield': height_image, # 保存高度图 + 'scale': (size[0], size[1], 50), + 'name': node_name + } + + self.terrains.append(terrain_info) + + print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: + created_terrains.append((terrain_node, qt_item)) + else: + created_terrains.append((terrain_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}") + return None + + # 处理创建结果 + if not created_terrains: + print("❌ 没有成功创建任何平面地形") + return None + + # 选中最后创建的光源 + if created_terrains: + last_light_np, last_qt_item = created_terrains[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形") + + # 返回值处理 + if len(created_terrains) == 1: + return created_terrains[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 except Exception as e: - print(f"创建平面地形时出错: {e}") + print(f"❌ 创建平面地形过程失败: {str(e)}") import traceback traceback.print_exc() return None @@ -390,10 +478,6 @@ class TerrainManager: print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}") print(f"✓ 重新设置了地形碰撞体") - # 更新场景树 - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - self.world.scene_manager.updateSceneTree() - return modified except Exception as e: @@ -408,14 +492,17 @@ class TerrainManager: # 从场景中移除地形节点 terrain_node = terrain_info['node'] if terrain_node and not terrain_node.isEmpty(): - terrain_node.removeNode() + tree_widget = self._get_tree_widget() + if tree_widget: + tree_widget.delete_items(tree_widget.selectedItems()) + # terrain_node.removeNode() + # + # # 从列表中移除 + # self.terrains.remove(terrain_info) - # 从列表中移除 - self.terrains.remove(terrain_info) - - # 更新场景树 - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - self.world.scene_manager.updateSceneTree() + # # 更新场景树 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}") @@ -464,6 +551,7 @@ class TerrainManager: if terrain_info in self.terrains: terrain_node = terrain_info['node'] if terrain_node and os.path.exists(texture_path): + texture_path = util.normalize_model_path(texture_path) # 加载纹理 texture = self.world.loader.loadTexture(texture_path) if texture: diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 95aaa827..c8e262e1 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -426,7 +426,11 @@ class GUIManager: cm = CardMaker('gui-2d-image') cm.setFrame(-size, size, -size, size) - image_node = self.world.aspect2d.attachNewNode(cm.generate()) + # image_node = self.world.aspect2d.attachNewNode(cm.generate()) + if parent_gui_node: + image_node = parent_gui_node.attachNewNode(cm.generate()) + else: + image_node = self.world.aspect2d.attachNewNode(cm.generate()) image_node.setPos(gui_pos) image_node.setBin('fixed', 0) image_node.setDepthWrite(False) @@ -696,7 +700,8 @@ class GUIManager: 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 = self.world.render.attachNewNode(cm.generate()) + image_node = parent_node.attachNewNode(cm.generate()) image_node.setPos(*pos) # 为3D图像创建独立的材质 @@ -919,7 +924,9 @@ class GUIManager: pass return None + # 暂无滑块功能 def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3): + pass """创建2D GUI滑块""" from direct.gui.DirectGui import DirectSlider @@ -954,19 +961,22 @@ class GUIManager: """删除GUI元素""" try: if gui_element in self.gui_elements: - # 移除GUI元素 - if hasattr(gui_element, 'removeNode'): - gui_element.removeNode() - elif hasattr(gui_element, 'destroy'): - gui_element.destroy() - - # 从列表中移除 - self.gui_elements.remove(gui_element) + # # 移除GUI元素 + # if hasattr(gui_element, 'removeNode'): + # gui_element.removeNode() + # elif hasattr(gui_element, 'destroy'): + # gui_element.destroy() + # + # # 从列表中移除 + # self.gui_elements.remove(gui_element) # 更新场景树 # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() + tree_widget = self._get_tree_widget() + if tree_widget: + tree_widget.delete_items(tree_widget.selectedItems()) + # if hasattr(self.world, 'updateSceneTree'): + # self.world.updateSceneTree() print(f"删除GUI元素: {gui_element}") return True @@ -1617,269 +1627,269 @@ class GUIManager: # ==================== GUI属性面板方法 ==================== - def updateGUIPropertyPanel(self, gui_element, layout): - """更新GUI元素属性面板""" - gui_type = gui_element.getTag("gui_type") - gui_text = gui_element.getTag("gui_text") - - # GUI类型显示 - typeLabel = QLabel("GUI类型:") - typeValue = QLabel(gui_type) - typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") - layout.addRow(typeLabel, typeValue) - - # 文本属性(如果适用) - if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: - textLabel = QLabel("文本:") - textEdit = QLineEdit(gui_text or "") - - # 创建一个更新函数来处理文本变化 - def updateText(text): - success = self.editGUIElement(gui_element, "text", text) - if success: - # 更新场景树显示的名称 - # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() - - textEdit.textChanged.connect(updateText) - layout.addRow(textLabel, textEdit) - - # 位置属性 - if hasattr(gui_element, 'getPos'): - # 根据GUI类型设置组名 - if gui_type in ["button", "label", "entry", "2d_image"]: - transform_group = QGroupBox("变换 Rect Transform") - else: - transform_group = QGroupBox("变换 Transform") - - transform_layout = QGridLayout() - - pos = gui_element.getPos() - - # 根据GUI类型决定位置编辑方式 - if gui_type in ["button", "label", "entry","2d_image"]: - # 2D GUI组件使用屏幕坐标 - logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 - logical_z = pos.getZ() / 0.1 - - transform_layout.addWidget(QLabel("屏幕位置"), 0, 0) - - x_label = QLabel("X") - z_label = QLabel("z") - x_label.setAlignment(Qt.Aligncenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(z_label, 0, 2) - - xPos = QDoubleSpinBox() - xPos.setRange(-50, 50) - xPos.setValue(logical_x) - xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) - transform_layout.addWidget(xPos, 1, 1) - - zPos = QDoubleSpinBox() - zPos.setRange(-50, 50) - zPos.setValue(logical_z) - zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) - transform_layout.addWidget(zPos, 1, 2) - - # 显示实际屏幕坐标(只读) - transform_layout.addWidget(QLabel("实际坐标"), 2, 0) - - actualXLabel = QLabel(f"{pos.getX():.3f}") - actualXLabel.setStyleSheet("color: gray; font-size: 10px;") - actualZLabel = QLabel(f"{pos.getZ():.3f}") - actualZLabel.setStyleSheet("color: gray; font-size: 10px;") - - transform_layout.addWidget(actualXLabel, 3, 1) - transform_layout.addWidget(actualZLabel, 3, 2) - # 添加宽度和高度控件(对于2D图像) - if gui_type == "2d_image": - - # 添加排序控制组 - sort_group = QGroupBox("渲染顺序") - sort_layout = QGridLayout() - - # 获取当前的sort值,如果没有设置则默认为0 - current_sort = int(gui_element.getTag("sort") or "0") - - sort_layout.addWidget(QLabel("层级:"), 0, 0) - - sort_spin = QSpinBox() - sort_spin.setRange(-1000, 1000) # 设置合理的范围 - sort_spin.setValue(current_sort) - - # 创建更新排序的函数 - def updateSort(value): - try: - # 设置标签 - gui_element.setTag("sort", str(value)) - # 应用sort到节点 - 使用fixed bin和指定的值 - gui_element.setBin("fixed", value) - print(f"✓ 更新2D图像渲染顺序: {value}") - except Exception as e: - print(f"✗ 更新2D图像渲染顺序失败: {e}") - - sort_spin.valueChanged.connect(updateSort) - sort_layout.addWidget(sort_spin, 0, 1) - - # 添加说明标签 - sort_help = QLabel("(数值越大越靠前)") - sort_help.setStyleSheet("font-size: 10px; color: gray;") - sort_layout.addWidget(sort_help, 1, 0, 1, 2) - - sort_group.setLayout(sort_layout) - layout.addWidget(sort_group) - - scale = gui_element.getScale() - width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, - (tuple, list)) else scale - height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, - (tuple, list)) and len( - scale) > 1 else scale - - # 宽度控件 - transform_layout.addWidget(QLabel("宽度"), 4, 0) - widthSpinBox = QDoubleSpinBox() - widthSpinBox.setRange(0.1, 10) - widthSpinBox.setSingleStep(0.1) - widthSpinBox.setValue(width) - widthSpinBox.valueChanged.connect( - lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) - transform_layout.addWidget(widthSpinBox, 4, 1) - - # 高度控件 - transform_layout.addWidget(QLabel("高度"), 4, 2) - heightSpinBox = QDoubleSpinBox() - heightSpinBox.setRange(0.1, 10) - heightSpinBox.setSingleStep(0.1) - heightSpinBox.setValue(height) - heightSpinBox.valueChanged.connect( - lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) - transform_layout.addWidget(heightSpinBox, 4, 3) - - else: - # 3D GUI组件使用世界坐标 - 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) - - # 位置数值输入框 - xPos = QDoubleSpinBox() - xPos.setRange(-100, 100) - xPos.setValue(pos.getX()) - xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) - transform_layout.addWidget(xPos, 1, 1) - - yPos = QDoubleSpinBox() - yPos.setRange(-100, 100) - yPos.setValue(pos.getY()) - yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) - transform_layout.addWidget(yPos, 1, 2) - - zPos = QDoubleSpinBox() - zPos.setRange(-100, 100) - zPos.setValue(pos.getZ()) - zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) - transform_layout.addWidget(zPos, 1, 3) - - # 缩放属性 - scale = gui_element.getScale() - transform_layout.addWidget(QLabel("缩放"), 2, 0) - - # X, Y, Z 缩放标签居中 - sx_label = QLabel("X") - sy_label = QLabel("Y") - sz_label = QLabel("Z") - sx_label.setAlignment(Qt.AlignCenter) - sy_label.setAlignment(Qt.AlignCenter) - sz_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(sx_label, 2, 1) - transform_layout.addWidget(sy_label, 2, 2) - transform_layout.addWidget(sz_label, 2, 3) - - # 缩放数值输入框 - scale_x = QDoubleSpinBox() - scale_x.setRange(0.01, 10) - scale_x.setSingleStep(0.1) - scale_x.setValue( - scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) - scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) - transform_layout.addWidget(scale_x, 3, 1) - - scale_y = QDoubleSpinBox() - scale_y.setRange(0.01, 10) - scale_y.setSingleStep(0.1) - scale_y.setValue( - scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( - scale) > 1 else scale) - scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) - transform_layout.addWidget(scale_y, 3, 2) - - scale_z = QDoubleSpinBox() - scale_z.setRange(0.01, 10) - scale_z.setSingleStep(0.1) - scale_z.setValue( - scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( - scale) > 2 else scale) - scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) - transform_layout.addWidget(scale_z, 3, 3) - transform_group.setLayout(transform_layout) - self._propertyLayout.addWidget(transform_group) - - # 缩放属性 - if hasattr(gui_element, 'getScale'): - scale = gui_element.getScale() - - scaleSpinBox = QDoubleSpinBox() - scaleSpinBox.setRange(0.01, 10) - scaleSpinBox.setSingleStep(0.1) - scaleSpinBox.setValue(scale.getX()) - scaleSpinBox.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "scale", v)) - layout.addRow("缩放:", scaleSpinBox) - - # 颜色属性(针对2D GUI) - if gui_type in ["button", "label"]: - colorButton = QPushButton("选择颜色") - colorButton.clicked.connect(lambda: self.selectGUIColor(gui_element)) - layout.addRow("背景颜色:", colorButton) - - # 3D特有属性 - if gui_type in ["3d_text", "virtual_screen"]: - # 旋转属性 - if hasattr(gui_element, 'getHpr'): - hpr = gui_element.getHpr() - - hRot = QDoubleSpinBox() - hRot.setRange(-180, 180) - hRot.setValue(hpr.getX()) - hRot.valueChanged.connect(lambda v: gui_element.setH(v)) - layout.addRow("旋转 H:", hRot) - - pRot = QDoubleSpinBox() - pRot.setRange(-180, 180) - pRot.setValue(hpr.getY()) - pRot.valueChanged.connect(lambda v: gui_element.setP(v)) - layout.addRow("旋转 P:", pRot) - - rRot = QDoubleSpinBox() - rRot.setRange(-180, 180) - rRot.setValue(hpr.getZ()) - rRot.valueChanged.connect(lambda v: gui_element.setR(v)) - layout.addRow("旋转 R:", rRot) + # def updateGUIPropertyPanel(self, gui_element, layout): + # """更新GUI元素属性面板""" + # gui_type = gui_element.getTag("gui_type") + # gui_text = gui_element.getTag("gui_text") + # + # # GUI类型显示 + # typeLabel = QLabel("GUI类型:") + # typeValue = QLabel(gui_type) + # typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") + # layout.addRow(typeLabel, typeValue) + # + # # 文本属性(如果适用) + # if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: + # textLabel = QLabel("文本:") + # textEdit = QLineEdit(gui_text or "") + # + # # 创建一个更新函数来处理文本变化 + # def updateText(text): + # success = self.editGUIElement(gui_element, "text", text) + # if success: + # # 更新场景树显示的名称 + # # 安全地调用updateSceneTree + # if hasattr(self.world, 'updateSceneTree'): + # self.world.updateSceneTree() + # + # textEdit.textChanged.connect(updateText) + # layout.addRow(textLabel, textEdit) + # + # # 位置属性 + # if hasattr(gui_element, 'getPos'): + # # 根据GUI类型设置组名 + # if gui_type in ["button", "label", "entry", "2d_image"]: + # transform_group = QGroupBox("变换 Rect Transform") + # else: + # transform_group = QGroupBox("变换 Transform") + # + # transform_layout = QGridLayout() + # + # pos = gui_element.getPos() + # + # # 根据GUI类型决定位置编辑方式 + # if gui_type in ["button", "label", "entry","2d_image"]: + # # 2D GUI组件使用屏幕坐标 + # logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 + # logical_z = pos.getZ() / 0.1 + # + # transform_layout.addWidget(QLabel("屏幕位置"), 0, 0) + # + # x_label = QLabel("X") + # z_label = QLabel("z") + # x_label.setAlignment(Qt.Aligncenter) + # z_label.setAlignment(Qt.AlignCenter) + # + # transform_layout.addWidget(x_label, 0, 1) + # transform_layout.addWidget(z_label, 0, 2) + # + # xPos = QDoubleSpinBox() + # xPos.setRange(-50, 50) + # xPos.setValue(logical_x) + # xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) + # transform_layout.addWidget(xPos, 1, 1) + # + # zPos = QDoubleSpinBox() + # zPos.setRange(-50, 50) + # zPos.setValue(logical_z) + # zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) + # transform_layout.addWidget(zPos, 1, 2) + # + # # 显示实际屏幕坐标(只读) + # transform_layout.addWidget(QLabel("实际坐标"), 2, 0) + # + # actualXLabel = QLabel(f"{pos.getX():.3f}") + # actualXLabel.setStyleSheet("color: gray; font-size: 10px;") + # actualZLabel = QLabel(f"{pos.getZ():.3f}") + # actualZLabel.setStyleSheet("color: gray; font-size: 10px;") + # + # transform_layout.addWidget(actualXLabel, 3, 1) + # transform_layout.addWidget(actualZLabel, 3, 2) + # # 添加宽度和高度控件(对于2D图像) + # if gui_type == "2d_image": + # + # # 添加排序控制组 + # sort_group = QGroupBox("渲染顺序") + # sort_layout = QGridLayout() + # + # # 获取当前的sort值,如果没有设置则默认为0 + # current_sort = int(gui_element.getTag("sort") or "0") + # + # sort_layout.addWidget(QLabel("层级:"), 0, 0) + # + # sort_spin = QSpinBox() + # sort_spin.setRange(-1000, 1000) # 设置合理的范围 + # sort_spin.setValue(current_sort) + # + # # 创建更新排序的函数 + # def updateSort(value): + # try: + # # 设置标签 + # gui_element.setTag("sort", str(value)) + # # 应用sort到节点 - 使用fixed bin和指定的值 + # gui_element.setBin("fixed", value) + # print(f"✓ 更新2D图像渲染顺序: {value}") + # except Exception as e: + # print(f"✗ 更新2D图像渲染顺序失败: {e}") + # + # sort_spin.valueChanged.connect(updateSort) + # sort_layout.addWidget(sort_spin, 0, 1) + # + # # 添加说明标签 + # sort_help = QLabel("(数值越大越靠前)") + # sort_help.setStyleSheet("font-size: 10px; color: gray;") + # sort_layout.addWidget(sort_help, 1, 0, 1, 2) + # + # sort_group.setLayout(sort_layout) + # layout.addWidget(sort_group) + # + # scale = gui_element.getScale() + # width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, + # (tuple, list)) else scale + # height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, + # (tuple, list)) and len( + # scale) > 1 else scale + # + # # 宽度控件 + # transform_layout.addWidget(QLabel("宽度"), 4, 0) + # widthSpinBox = QDoubleSpinBox() + # widthSpinBox.setRange(0.1, 10) + # widthSpinBox.setSingleStep(0.1) + # widthSpinBox.setValue(width) + # widthSpinBox.valueChanged.connect( + # lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + # transform_layout.addWidget(widthSpinBox, 4, 1) + # + # # 高度控件 + # transform_layout.addWidget(QLabel("高度"), 4, 2) + # heightSpinBox = QDoubleSpinBox() + # heightSpinBox.setRange(0.1, 10) + # heightSpinBox.setSingleStep(0.1) + # heightSpinBox.setValue(height) + # heightSpinBox.valueChanged.connect( + # lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + # transform_layout.addWidget(heightSpinBox, 4, 3) + # + # else: + # # 3D GUI组件使用世界坐标 + # 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) + # + # # 位置数值输入框 + # xPos = QDoubleSpinBox() + # xPos.setRange(-100, 100) + # xPos.setValue(pos.getX()) + # xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) + # transform_layout.addWidget(xPos, 1, 1) + # + # yPos = QDoubleSpinBox() + # yPos.setRange(-100, 100) + # yPos.setValue(pos.getY()) + # yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) + # transform_layout.addWidget(yPos, 1, 2) + # + # zPos = QDoubleSpinBox() + # zPos.setRange(-100, 100) + # zPos.setValue(pos.getZ()) + # zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) + # transform_layout.addWidget(zPos, 1, 3) + # + # # 缩放属性 + # scale = gui_element.getScale() + # transform_layout.addWidget(QLabel("缩放"), 2, 0) + # + # # X, Y, Z 缩放标签居中 + # sx_label = QLabel("X") + # sy_label = QLabel("Y") + # sz_label = QLabel("Z") + # sx_label.setAlignment(Qt.AlignCenter) + # sy_label.setAlignment(Qt.AlignCenter) + # sz_label.setAlignment(Qt.AlignCenter) + # + # transform_layout.addWidget(sx_label, 2, 1) + # transform_layout.addWidget(sy_label, 2, 2) + # transform_layout.addWidget(sz_label, 2, 3) + # + # # 缩放数值输入框 + # scale_x = QDoubleSpinBox() + # scale_x.setRange(0.01, 10) + # scale_x.setSingleStep(0.1) + # scale_x.setValue( + # scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) + # scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + # transform_layout.addWidget(scale_x, 3, 1) + # + # scale_y = QDoubleSpinBox() + # scale_y.setRange(0.01, 10) + # scale_y.setSingleStep(0.1) + # scale_y.setValue( + # scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( + # scale) > 1 else scale) + # scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) + # transform_layout.addWidget(scale_y, 3, 2) + # + # scale_z = QDoubleSpinBox() + # scale_z.setRange(0.01, 10) + # scale_z.setSingleStep(0.1) + # scale_z.setValue( + # scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( + # scale) > 2 else scale) + # scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + # transform_layout.addWidget(scale_z, 3, 3) + # transform_group.setLayout(transform_layout) + # self._propertyLayout.addWidget(transform_group) + # + # # 缩放属性 + # if hasattr(gui_element, 'getScale'): + # scale = gui_element.getScale() + # + # scaleSpinBox = QDoubleSpinBox() + # scaleSpinBox.setRange(0.01, 10) + # scaleSpinBox.setSingleStep(0.1) + # scaleSpinBox.setValue(scale.getX()) + # scaleSpinBox.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "scale", v)) + # layout.addRow("缩放:", scaleSpinBox) + # + # # 颜色属性(针对2D GUI) + # if gui_type in ["button", "label"]: + # colorButton = QPushButton("选择颜色") + # colorButton.clicked.connect(lambda: self.selectGUIColor(gui_element)) + # layout.addRow("背景颜色:", colorButton) + # + # # 3D特有属性 + # if gui_type in ["3d_text", "virtual_screen"]: + # # 旋转属性 + # if hasattr(gui_element, 'getHpr'): + # hpr = gui_element.getHpr() + # + # hRot = QDoubleSpinBox() + # hRot.setRange(-180, 180) + # hRot.setValue(hpr.getX()) + # hRot.valueChanged.connect(lambda v: gui_element.setH(v)) + # layout.addRow("旋转 H:", hRot) + # + # pRot = QDoubleSpinBox() + # pRot.setRange(-180, 180) + # pRot.setValue(hpr.getY()) + # pRot.valueChanged.connect(lambda v: gui_element.setP(v)) + # layout.addRow("旋转 P:", pRot) + # + # rRot = QDoubleSpinBox() + # rRot.setRange(-180, 180) + # rRot.setValue(hpr.getZ()) + # rRot.valueChanged.connect(lambda v: gui_element.setR(v)) + # layout.addRow("旋转 R:", rRot) def selectGUIColor(self, gui_element): """选择GUI元素颜色""" diff --git a/main.py b/main.py index ed81d628..f77656da 100644 --- a/main.py +++ b/main.py @@ -102,7 +102,7 @@ class MyWorld(CoreWorld): self.collision_manager = CollisionManager(self) # 调试选项 - self.debug_collision = True # 是否显示碰撞体 + self.debug_collision = False # 是否显示碰撞体 # 默认启用模型间碰撞检测(可选) self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) diff --git a/project/project_manager.py b/project/project_manager.py index e54a9995..cb55a175 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -9,7 +9,6 @@ import os import sys import json -import re import datetime import subprocess import shutil @@ -47,7 +46,9 @@ class ProjectManager: project_path = dialog.projectPath project_name = dialog.projectName - full_project_path = os.path.join(project_path, project_name) + # full_project_path = os.path.join(project_path, project_name) + full_project_path = os.path.normpath(os.path.join(project_path, project_name)) + print(f"full_project_path: {full_project_path}") try: # 创建项目文件夹结构 @@ -72,10 +73,10 @@ class ProjectManager: json.dump(project_config, f, ensure_ascii=False, indent=4) print(f"项目配置文件已创建: {config_file}") - + # 清空当前场景 self._clearCurrentScene() - + # 自动保存初始场景 scene_file = os.path.join(scenes_path, "scene.bam") if self.world.scene_manager.saveScene(scene_file): diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 472b2f16..5383e9a5 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -804,6 +804,7 @@ class SceneManager: # 保存场景 success = self.world.render.writeBamFile(filename) + print(f"success: {success}") return success except Exception as e: @@ -948,9 +949,14 @@ class SceneManager: """删除模型""" try: if model in self.models: - model.removeNode() - self.models.remove(model) - self.updateSceneTree() + tree_widget = self._get_tree_widget() + if not tree_widget: + return False + + tree_widget.delete_items(tree_widget.selectedItems()) + # model.removeNode() + # self.models.remove(model) + # self.updateSceneTree() print(f"删除模型: {model.getName()}") return True except Exception as e: @@ -959,14 +965,15 @@ class SceneManager: def clearAllModels(self): """清除所有模型""" - try: - for model in self.models: - model.removeNode() - self.models.clear() - self.updateSceneTree() - print("清除所有模型完成") - except Exception as e: - print(f"清除所有模型失败: {str(e)}") + pass + # try: + # for model in self.models: + # model.removeNode() + # self.models.clear() + # self.updateSceneTree() + # print("清除所有模型完成") + # except Exception as e: + # print(f"清除所有模型失败: {str(e)}") def getModels(self): """获取模型列表""" @@ -1751,43 +1758,95 @@ except Exception as e: return False def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)): + """ + 加载 Cesium 3D Tileset - 采用新的创建逻辑,支持多选和更完善的UI交互。 + """ try: - print(f"加载 Cesium 3D Tiles: {tileset_url}") + from panda3d.core import NodePath + print(f"🗺️ 开始加载 Cesium 3D Tiles: {tileset_url}") - # 创建一个容器节点来管理tileset - node_name = f"cesium_tileset_{len(self.tilesets)}" - tileset_node = self.world.render.attachNewNode(node_name) - tileset_node.setPos(*position) + # 1. 获取UI控件和目标父节点 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None - #添加标签以便场景树识别 - tileset_node.setTag("is_scene_element","1") - tileset_node.setTag("element_type","cesium_tileset") - tileset_node.setTag("tileset_url",tileset_url) - tileset_node.setTag("file",f"tileset_{len(self.tilesets)}") + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点来附加Tileset") + return None - # 存储tileset信息 - tileset_info = { - 'url': tileset_url, - 'node': tileset_node, - 'position': position, - 'tiles': {} - } + created_tilesets = [] - self.tilesets.append(tileset_info) + # 2. 遍历所有选中的父节点,并为其创建Tileset + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + node_name = f"cesium_tileset_{len(self.tilesets)}" - # 创建一个临时的可视化占位符,让用户能看到节点已添加 - self._create_placeholder_geometry(tileset_node) + # 创建一个容器节点来管理tileset,并挂载到父节点 + tileset_node = parent_node.attachNewNode(node_name) + tileset_node.setPos(*position) - # 异步加载tileset数据 - self._load_tileset_async(tileset_url, tileset_info) + # 添加标签以便场景识别和保存 + tileset_node.setTag("is_scene_element", "1") + tileset_node.setTag("element_type", "cesium_tileset") + tileset_node.setTag("tileset_url", tileset_url) + # 使用唯一名称作为文件标识,代替索引 + tileset_node.setTag("file", node_name) - # 更新场景树 - self.updateSceneTree() - print(f"✓ Cesium 3D Tiles 加载请求已发送") - return tileset_node + # 存储tileset核心信息 + tileset_info = { + 'url': tileset_url, + 'node': tileset_node, + 'position': position, + 'tiles': {} # 用于后续管理瓦片 + } + self.tilesets.append(tileset_info) + + # 创建一个临时的可视化占位符,让用户能看到节点已添加 + self._create_placeholder_geometry(tileset_node) + + # 异步加载tileset的实际数据 + self._load_tileset_async(tileset_url, tileset_info) + + print(f"✅ 为 {parent_item.text(0)} 加载 Tileset 成功: {node_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(tileset_node, parent_item, "CESIUM_TILESET_NODE") + if qt_item: + created_tilesets.append((tileset_node, qt_item)) + else: + created_tilesets.append((tileset_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 加载 Tileset 失败: {str(e)}") + continue # 继续尝试为下一个父节点创建 + + # 3. 处理创建结果 + if not created_tilesets: + print("❌ 没有成功加载任何 Tileset") + return None + + # 选中最后创建的Tileset并更新UI + if created_tilesets: + last_tileset_node, last_qt_item = created_tilesets[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择状态和属性面板 + tree_widget.update_selection_and_properties(last_tileset_node, last_qt_item) + + print(f"🎉 总共加载了 {len(created_tilesets)} 个 Cesium Tileset 实例") + + # 4. 返回值处理 + if len(created_tilesets) == 1: + return created_tilesets[0][0] # 单个实例返回NodePath + else: + return [node for node, _ in created_tilesets] # 多个实例返回NodePath列表 except Exception as e: - print(f"❌ 加载 Cesium 3D Tiles 失败: {e}") + print(f"❌ 加载 Cesium 3D Tiles 过程失败: {str(e)}") import traceback traceback.print_exc() return None diff --git a/ui/interface_manager.py b/ui/interface_manager.py index d2e8e4ac..f4b00061 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -48,83 +48,83 @@ class InterfaceManager: self.world.selection.updateSelection(None) #self.world.property_panel.clearPropertyPanel() - def showTreeContextMenu(self, position): - """显示树形控件的右键菜单""" - item = self.treeWidget.itemAt(position) - if not item: - return - - # 获取节点对象 - nodePath = item.data(0, Qt.UserRole) - if not nodePath: - return - - # 创建菜单 - menu = QMenu() - - # 检查是否是GUI元素 - if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"): - # GUI元素菜单 - editAction = menu.addAction("编辑") - editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath)) - - deleteAction = menu.addAction("删除GUI元素") - deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath)) - - duplicateAction = menu.addAction("复制") - duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath)) - - elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset": - deleteAction = menu.addAction("删除 Cesium Tileset") - deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) - - else: - # 为模型节点或其子节点添加删除选项 - parentItem = item.parent() - if parentItem: - if self.isModelOrChild(item): - deleteAction = menu.addAction("删除") - deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) - else: - deleteAction = menu.addAction("删除") - deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) - - # 显示菜单 - menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) - - def deleteCesiumTileset(self, nodePath, item): - """删除 Cesium tileset""" - try: - # 从场景中移除 - nodePath.removeNode() - - # 从 tilesets 列表中移除 - if hasattr(self.world, 'scene_manager'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - if tileset_info['node'] == nodePath: - tilesets_to_remove.append(i) - - # 从后往前删除,避免索引问题 - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - - # 从树形控件中移除 - parentItem = item.parent() - if parentItem: - parentItem.removeChild(item) - - print(f"成功删除 Cesium tileset: {nodePath.getName()}") - - # 清空属性面板和选择框 - self.world.property_panel.clearPropertyPanel() - self.world.selection.updateSelection(None) - - # 更新场景树 - self.updateSceneTree() - - except Exception as e: - print(f"删除 Cesium tileset 失败: {str(e)}") + # def showTreeContextMenu(self, position): + # """显示树形控件的右键菜单""" + # item = self.treeWidget.itemAt(position) + # if not item: + # return + # + # # 获取节点对象 + # nodePath = item.data(0, Qt.UserRole) + # if not nodePath: + # return + # + # # 创建菜单 + # menu = QMenu() + # + # # 检查是否是GUI元素 + # if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"): + # # GUI元素菜单 + # editAction = menu.addAction("编辑") + # editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath)) + # + # deleteAction = menu.addAction("删除GUI元素") + # deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath)) + # + # duplicateAction = menu.addAction("复制") + # duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath)) + # + # elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset": + # deleteAction = menu.addAction("删除 Cesium Tileset") + # deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) + # + # else: + # # 为模型节点或其子节点添加删除选项 + # parentItem = item.parent() + # if parentItem: + # if self.isModelOrChild(item): + # deleteAction = menu.addAction("删除") + # deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) + # else: + # deleteAction = menu.addAction("删除") + # deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) + # + # # 显示菜单 + # menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) + # + # def deleteCesiumTileset(self, nodePath, item): + # """删除 Cesium tileset""" + # try: + # # 从场景中移除 + # nodePath.removeNode() + # + # # 从 tilesets 列表中移除 + # if hasattr(self.world, 'scene_manager'): + # tilesets_to_remove = [] + # for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + # if tileset_info['node'] == nodePath: + # tilesets_to_remove.append(i) + # + # # 从后往前删除,避免索引问题 + # for i in reversed(tilesets_to_remove): + # del self.world.scene_manager.tilesets[i] + # + # # 从树形控件中移除 + # parentItem = item.parent() + # if parentItem: + # parentItem.removeChild(item) + # + # print(f"成功删除 Cesium tileset: {nodePath.getName()}") + # + # # 清空属性面板和选择框 + # self.world.property_panel.clearPropertyPanel() + # self.world.selection.updateSelection(None) + # + # # 更新场景树 + # self.updateSceneTree() + # + # except Exception as e: + # print(f"删除 Cesium tileset 失败: {str(e)}") def isModelOrChild(self, item): """检查是否是模型节点或其子节点""" @@ -148,9 +148,9 @@ class InterfaceManager: if terrain_to_remove: self.world.terrain_manager.deleteTerrain(terrain_to_remove) print(f"成功删除地形节点:{nodePath.getName()}") - self.updateSceneTree() - self.world.property_panel.clearPropertyPanel() - self.world.selection.updateSelection(None) + # self.updateSceneTree() + # self.world.property_panel.clearPropertyPanel() + # self.world.selection.updateSelection(None) return # 从场景中移除 diff --git a/ui/main_window.py b/ui/main_window.py index 27e974e1..8cb52c8a 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -813,11 +813,11 @@ class MainWindow(QMainWindow): self.toolGroup.buttonClicked.connect(self.onToolChanged) # 连接脚本菜单事件 - self.createScriptAction.triggered.connect(self.onCreateScriptDialog) - self.loadScriptAction.triggered.connect(self.onLoadScriptFile) - self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) - self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) - self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + # self.createScriptAction.triggered.connect(self.onCreateScriptDialog) + # self.loadScriptAction.triggered.connect(self.onLoadScriptFile) + # self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) + # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) + # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) def onCreateCesiumView(self): diff --git a/ui/property_panel.py b/ui/property_panel.py index 8edc81b2..3b002b60 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -133,6 +133,11 @@ class PropertyPanelManager: # 根据模型的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) self.name_input = QLineEdit(itemText) + + self.name_input.returnPressed.connect( + lambda: self.world.treeWidget.update_item_name(self.name_input.text(), item) + ) + name_layout.addWidget(self.active_check) name_layout.addWidget(self.name_input) self.name_group.setLayout(name_layout) @@ -515,12 +520,12 @@ class PropertyPanelManager: success = self.world.terrain_manager.deleteTerrain(terrain_info) if success: # 更新场景树 - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, - 'updateSceneTree'): - self.world.scene_manager.updateSceneTree() - - # 清空属性面板 - self.clearPropertyPanel() + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, + # 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + # + # # 清空属性面板 + # self.clearPropertyPanel() print(f"✓ 地形已删除: {terrain_info.get('name', '未知')}") else: @@ -994,27 +999,29 @@ class PropertyPanelManager: ) if reply == QMessageBox.Yes: - # 从场景中移除 - nodePath.removeNode() + tree_widget = self.world.treeWidget + tree_widget.delete_item(nodePath) + # # 从场景中移除 + # nodePath.removeNode() + # + # # 从 tilesets 列表中移除 + # if hasattr(self.world, 'scene_manager'): + # tilesets_to_remove = [] + # for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + # if tileset_info['node'] == nodePath: + # tilesets_to_remove.append(i) + # + # # 从后往前删除,避免索引问题 + # for i in reversed(tilesets_to_remove): + # del self.world.scene_manager.tilesets[i] + # + # # 更新场景树 + # self.world.scene_manager.updateSceneTree() + # + # # 清空属性面板 + # self.clearPropertyPanel() - # 从 tilesets 列表中移除 - if hasattr(self.world, 'scene_manager'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - if tileset_info['node'] == nodePath: - tilesets_to_remove.append(i) - - # 从后往前删除,避免索引问题 - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - - # 更新场景树 - self.world.scene_manager.updateSceneTree() - - # 清空属性面板 - self.clearPropertyPanel() - - print(f"成功删除 Cesium tileset: {nodePath.getName()}") + # print(f"成功删除 Cesium tileset: {nodePath.getName()}") except Exception as e: print(f"删除 Cesium tileset 失败: {str(e)}") diff --git a/ui/widgets.py b/ui/widgets.py index f7364990..bd955394 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -17,6 +17,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout, from PyQt5.QtCore import Qt, QUrl, QMimeData from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush from PyQt5.sip import wrapinstance +from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import ModelRoot from QPanda3D.QPanda3DWidget import QPanda3DWidget @@ -1302,6 +1303,7 @@ class CustomTreeWidget(QTreeWidget): parent = wrapinstance(0, QWidget) super().__init__(parent) self.world = world + # self.selectedItems = None self.initData() self.setupUI() # 初始化界面 self.setupContextMenu() # 初始化右键菜单 @@ -1333,9 +1335,14 @@ class CustomTreeWidget(QTreeWidget): "LIGHT_NODE", "CAMERA_NODE", "IMPORTED_MODEL_NODE", - "MODEL_NODE" + "MODEL_NODE", + "TERRAIN_NODE", + "CESIUM_TILESET_NODE" } + # 这是一个最佳实践,它让代码的意图变得非常清晰。 + self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types) + def setupUI(self): """初始化UI设置""" self.setHeaderHidden(True) @@ -1427,105 +1434,91 @@ class CustomTreeWidget(QTreeWidget): print("用户取消了菜单选择") def dropEvent(self, event): - dragged_item = self.currentItem() - target_item = self.itemAt(event.pos()) - if not dragged_item or not target_item: + # 1. 获取所有被拖拽的项 + dragged_items = self.selectedItems() + if not dragged_items: event.ignore() return - if not self.isValidParentChild(dragged_item, target_item): - event.ignore() - return + # 2. 在执行Qt的默认拖拽前,记录所有拖拽项的原始状态 + drag_info = [] + for item in dragged_items: + panda_node = item.data(0, Qt.UserRole) + if not panda_node or panda_node.is_empty(): + continue # 跳过无效节点 - dragged_node = dragged_item.data(0, Qt.UserRole) - target_node = target_item.data(0, Qt.UserRole) + drag_info.append({ + "item": item, + "node": panda_node, + "old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None + }) - if not dragged_node or not target_node: - event.ignore() - return - - # # 检查是否是有效的父子关系 - # if self.isValidParentChild(dragged_item, target_item): - # # 保存当前的世界坐标 - # world_pos = dragged_node.getPos(self.world.render) - # - # # 更新场景图中的父子关系 - # dragged_node.wrtReparentTo(target_node) - # - # # 接受拖放事件,更新树形控件 - # super().dropEvent(event) - # - # #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item) - # # 更新属性面板 - # self.world.updatePropertyPanel(dragged_item) - # self.world.property_panel._syncEffectiveVisibility(dragged_node) - - print(f"dragged_node: {dragged_node}, target_node: {target_node}") - - # 记录拖拽前的父节点 - old_parent_item = dragged_item.parent() - old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None - - # 执行Qt默认拖拽 + # 3. 执行Qt的默认拖拽,让UI树先行更新 + # 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点 super().dropEvent(event) - # 检查拖拽后的父节点 - new_parent_item = dragged_item.parent() - new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None - - # 同步Panda3D场景图的父子关系 + # 4. 遍历记录下的信息,同步每一个Panda3D节点的状态 try: - # 检查是否是跨层级拖拽(父节点发生变化) - if old_parent_node != new_parent_node: - print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") + for info in drag_info: + dragged_item = info["item"] + dragged_node = info["node"] + old_parent_node = info["old_parent_node"] - # 保存世界坐标位置 - world_pos = dragged_node.getPos(self.world.render) - world_hpr = dragged_node.getHpr(self.world.render) - world_scale = dragged_node.getScale(self.world.render) + # 获取拖拽后的新父节点 + new_parent_item = dragged_item.parent() + new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None - # 检查是否是2D GUI元素 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - is_2d_gui = dragged_type in self.gui_2d_types + # 仅当父节点实际发生变化时才执行重新父化 + if old_parent_node != new_parent_node: + print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") - # 重新父化到新的父节点 - if new_parent_node: - if is_2d_gui: - # 2D GUI元素需要特殊处理 - if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1": - # 目标是GUI元素,直接重新父化 - dragged_node.reparentTo(new_parent_node) + # # 保存世界坐标位置 + # world_pos = dragged_node.getPos(self.world.render) + # world_hpr = dragged_node.getHpr(self.world.render) + # world_scale = dragged_node.getScale(self.world.render) + + # 检查是否是2D GUI元素 + dragged_type = dragged_item.data(0, Qt.UserRole + 1) + is_2d_gui = dragged_type in self.gui_2d_types + + # 重新父化到新的父节点 + if new_parent_node: + if is_2d_gui: + # 2D GUI元素需要特殊处理 + if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + dragged_node.wrtReparentTo(new_parent_node) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + # from direct.showbase.ShowBase import aspect2d + dragged_node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下") else: - # 目标是3D节点,保持GUI特性,重新父化到aspect2d - from direct.showbase.ShowBase import aspect2d - dragged_node.reparentTo(aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下") + # 非GUI元素正常重新父化 + dragged_node.wrtReparentTo(new_parent_node) else: - # 非GUI元素正常重新父化 - dragged_node.reparentTo(new_parent_node) - else: - # 如果新父节点为None,根据元素类型决定父节点 - if is_2d_gui: - from direct.showbase.ShowBase import aspect2d - dragged_node.reparentTo(aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d") - else: - dragged_node.reparentTo(self.world.render) + # 如果新父节点为None,根据元素类型决定父节点 + if is_2d_gui: + # from direct.showbase.ShowBase import aspect2d + dragged_node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d") + else: + dragged_node.wrtReparentTo(self.world.render) - # 恢复世界坐标位置(对于2D GUI可能需要调整) - if is_2d_gui: - # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 - dragged_node.setPos(world_pos) - dragged_node.setHpr(world_hpr) - dragged_node.setScale(world_scale) - else: - dragged_node.setPos(self.world.render, world_pos) - dragged_node.setHpr(self.world.render, world_hpr) - dragged_node.setScale(self.world.render, world_scale) + # # 恢复世界坐标位置(对于2D GUI可能需要调整) + # if is_2d_gui: + # # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 + # dragged_node.setPos(world_pos) + # dragged_node.setHpr(world_hpr) + # dragged_node.setScale(world_scale) + # else: + # dragged_node.setPos(self.world.render, world_pos) + # dragged_node.setHpr(self.world.render, world_hpr) + # dragged_node.setScale(self.world.render, world_scale) - print(f"✅ Panda3D父子关系已更新") - else: - print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") + print(f"✅ Panda3D父子关系已更新") + else: + print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") except Exception as e: print(f"⚠️ 同步Panda3D场景图失败: {e}") @@ -1648,7 +1641,9 @@ class CustomTreeWidget(QTreeWidget): # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下 if is_dragged_2d_gui: - if is_target_2d_gui: + if target_type == "SCENE_ROOT": + return True + elif is_target_2d_gui: print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}") return True elif is_target_3d_gui: @@ -1739,6 +1734,20 @@ class CustomTreeWidget(QTreeWidget): def dragMoveEvent(self, event): """处理拖动事件""" + indicator_pos = self.dropIndicatorPosition() + indicator_str = "Unknown" + + if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem: + indicator_str = "OnItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem: + indicator_str = "AboveItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem: + indicator_str = "BelowItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: + indicator_str = "OnViewport" + + print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') + if event.source() != self: event.ignore() return @@ -1789,7 +1798,7 @@ class CustomTreeWidget(QTreeWidget): if not selected_items: return - # 过滤掉不能删除的节点 + # 1. 过滤掉不能删除的节点 deletable_items = [] for item in selected_items: node_type = item.data(0, Qt.UserRole + 1) @@ -1805,7 +1814,7 @@ class CustomTreeWidget(QTreeWidget): QMessageBox.information(self, "提示", "没有可删除的节点") return - # 确认删除 + # 2. 确认删除 item_count = len(deletable_items) if item_count == 1: message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?" @@ -1820,11 +1829,19 @@ class CustomTreeWidget(QTreeWidget): if reply != QMessageBox.Yes: return - # 执行删除 + # 默认选中场景根节点,通常是第一个顶级节点 + next_item_to_select = self.topLevelItem(0) + + # 3. 执行删除循环 deleted_count = 0 for item in deletable_items: try: + # 在删除前,记录其父节点,作为删除后的新选择 + # 选择最后一个被删除项的父节点作为新的焦点 + if item.parent(): + next_item_to_select = item.parent() panda_node = item.data(0, Qt.UserRole) + if panda_node: # 清理选择状态 if (hasattr(self.world, 'selection') and @@ -1840,15 +1857,32 @@ 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列表中移除 + if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements: + self.world.gui_elements.remove(panda_node) if hasattr(self.world, 'models') and panda_node in self.world.models: self.world.models.remove(panda_node) if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: self.world.Spotlight.remove(panda_node) if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: self.world.Pointlight.remove(panda_node) + if hasattr(self.world, 'terrains') and panda_node in self.world.terrains: + self.world.terrains.remove(panda_node) + if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets: + # self.world.tilesets.remove(panda_node) + # 从 tilesets 列表中移除 + if hasattr(self.world, 'scene_manager'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + if tileset_info['node'] == panda_node: + tilesets_to_remove.append(i) + + # 从后往前删除,避免索引问题 + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] # 从Panda3D场景中移除 panda_node.removeNode() @@ -1872,7 +1906,144 @@ class CustomTreeWidget(QTreeWidget): # if hasattr(self.world, 'property_panel'): # self.world.property_panel.clearPropertyPanel() - print(f"✅ 已删除 {deleted_count} 个节点") + # 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) + + def delete_item(self, panda_node): + """删除指定节点 panda3D(node)- 优化和修复版本""" + if not panda_node or panda_node.is_empty(): + print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") + return + + # --- 关键修复:在操作前,安全地获取节点名字 --- + node_name_for_logging = panda_node.getName() + + # 1. 寻找对应的Qt Item + item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot()) + + # 场景清理(无论是否找到item,都应该执行) + self._cleanup_panda_node_resources(panda_node) + panda_node.removeNode() + + # 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出 + if not item: + print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") + return + + try: + # 2. 过滤受保护节点 + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中 + print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。") + return + + # 3. 从UI树中移除 + parent_for_next_selection = item.parent() + if item.parent(): + item.parent().removeChild(item) + else: + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + print(f"✅ 成功删除节点: {node_name_for_logging}") + + # 4. 更新UI + print(f"🔄 正在更新UI...") + if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid(): + new_selection_item = parent_for_next_selection + else: + new_selection_item = self.topLevelItem(0) + + if new_selection_item: + self.setCurrentItem(new_selection_item) + new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole) + self.update_selection_and_properties(new_panda_node_to_select, new_selection_item) + else: + self.update_selection_and_properties(None, None) + + except Exception as e: + print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}") + import traceback + traceback.print_exc() + + def _cleanup_panda_node_resources(self, panda_node): + """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" + if not panda_node or panda_node.is_empty(): + return + try: + # 清理选择状态 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node: + self.world.selection.updateSelection(None) + # 清理属性面板 + if hasattr(self.world, 'property_panel'): + self.world.property_panel.removeActorForModel(panda_node) + # 清理灯光 + if hasattr(panda_node, 'getPythonTag'): + light_object = panda_node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + # 从各种world管理列表中移除 + lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains'] + for list_name in lists_to_check: + if hasattr(self.world, list_name): + world_list = getattr(self.world, list_name) + if panda_node in world_list: + world_list.remove(panda_node) + # 特殊处理tilesets + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'): + tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if + info.get('node') == panda_node] + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。") + except Exception as e: + # 即便这里出错,也要打印信息,但不要让整个删除流程中断 + print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}") + + # def mousePressEvent(self, event): + # """鼠标按下事件""" + # if event.button() == Qt.LeftButton: + # if self.currentItem(): + # print(f"self.currentItem() = {self.currentItem()}") + # else: + # print(f"self.currentItem() = None") + # + # # 调用父类处理其他事件 + # super().mousePressEvent(event) + + def update_item_name(self, text, item): + """ 树节点名字 """ + if not item: + return + try: + # 正确的代码 + node = item.data(0, Qt.UserRole) + + item.setText(0, text) + node.setName(text) + except Exception as e: + print(e) # ==================== 辅助方法 ==================== def _findSceneRoot(self): @@ -1904,45 +2075,68 @@ class CustomTreeWidget(QTreeWidget): def add_node_to_tree_widget(self, node, parent_item, node_type): """将node元素添加到树形控件""" + # BLACK_LIST 和依赖项导入保持不变 BLACK_LIST = {'', '**', 'temp', 'collision'} + from panda3d.core import CollisionNode, ModelRoot + from PyQt5.QtWidgets import QTreeWidgetItem + from PyQt5.QtCore import Qt - from panda3d.core import CollisionNode - - def should_skip(node): - name = node.getName() - return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name=="" - - def addNodeToTree(node,parentItem,force = False): + # 1. 修改内部函数,让它返回创建的节点 + def addNodeToTree(node, parentItem, force=False): + """内部递归函数,现在会返回创建的顶级节点项""" if not force and should_skip(node): - return - nodeItem = QTreeWidgetItem(parentItem,[node.getName()]) - nodeItem.setData(0,Qt.UserRole, node) + return None # 如果跳过,返回None + + nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) + nodeItem.setData(0, Qt.UserRole, node) nodeItem.setData(0, Qt.UserRole + 1, node_type) for child in node.getChildren(): - addNodeToTree(child,nodeItem,force = False) + # 递归调用,但我们只关心顶级的nodeItem + addNodeToTree(child, nodeItem, force=False) + + return nodeItem # <-- 新增:返回创建的QTreeWidgetItem + + def should_skip(node): + name = node.getName() + return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance( + node.node(), ModelRoot) or name == "" + + # 使用一个变量来确保无论哪个分支都有返回值 + new_qt_item = None + node_name = "" try: - from PyQt5.QtWidgets import QTreeWidgetItem - from PyQt5.QtCore import Qt if node_type == "IMPORTED_MODEL_NODE": - node_name = node.getTag("file") if hasattr(node, 'getTag') else "model" - addNodeToTree(node, parent_item, force=True) + # getTag('file') 可能是你自己设置的tag,这里假设它存在 + node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() + + # 2. 接收 addNodeToTree 的返回值 + new_qt_item = addNodeToTree(node, parent_item, force=True) + else: node_name = node.getName() if hasattr(node, 'getName') else "node" new_qt_item = QTreeWidgetItem(parent_item, [node_name]) new_qt_item.setData(0, Qt.UserRole, node) new_qt_item.setData(0, Qt.UserRole + 1, node_type) - # 展开父节点 - if hasattr(parent_item, 'setExpanded'): - parent_item.setExpanded(True) + # 确保 new_qt_item 成功创建后再继续操作 + if new_qt_item: + # 展开父节点 + if hasattr(parent_item, 'setExpanded'): + parent_item.setExpanded(True) - print(f"✅ Qt树节点添加成功: {node_name}") - return new_qt_item + print(f"✅ Qt树节点添加成功: {node_name}") + return new_qt_item + else: + # 如果 addNodeToTree 因为 should_skip 返回了 None + print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。") + return None except Exception as e: + import traceback print(f"❌ 添加node到树形控件失败: {str(e)}") + traceback.print_exc() # 打印更详细的错误堆栈,方便调试 return None def update_selection_and_properties(self, node, qt_item): @@ -2016,7 +2210,7 @@ class CustomTreeWidget(QTreeWidget): node_type = item.data(0, Qt.UserRole + 1) # 场景根节点和普通场景节点可以作为父节点 - if node_type in self.gui_3d_types and self.scene_3d_types: + if node_type in self.valid_3d_parent_types: return True # # 模型节点也可以作为父节点