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 911ee147..eb4f5810 100644 --- a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py +++ b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py @@ -116,7 +116,25 @@ class InternalLightManager(object): source.set_slot(slot) def remove_light(self, light): - print("111111111111111111111111111111111111111111111111") + 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!") @@ -133,7 +151,10 @@ 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) cmd_remove.push_int(first_source_slot) # 使用保存的slot值 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 cfd85590..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,7 +478,6 @@ class TerrainManager: print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}") print(f"✓ 重新设置了地形碰撞体") - return modified except Exception as e: @@ -405,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')}") @@ -461,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 babc94c2..bf122ef1 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -509,7 +509,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) @@ -779,7 +783,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图像创建独立的材质 @@ -2070,7 +2075,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 @@ -2105,19 +2112,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 @@ -2910,7 +2920,7 @@ class GUIManager: 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) @@ -2990,11 +3000,11 @@ class GUIManager: 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, 100) scaleSpinBox.setSingleStep(0.1) diff --git a/main.py b/main.py index 137b8566..edf872b9 100644 --- a/main.py +++ b/main.py @@ -105,7 +105,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 69ee1d49..4f5544dd 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -970,9 +970,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: @@ -981,14 +986,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): """获取模型列表""" @@ -1770,43 +1776,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 3e802bfb..75f4324a 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -49,177 +49,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: - #灯光节点添加特殊处理 - if self.isLightNode(nodePath): - deleteAction = menu.addAction("删除灯光") - deleteAction.triggered.connect(lambda:self.deleteLightNode(nodePath,item)) - else: - deleteAction = menu.addAction("删除") - deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) - - # 显示菜单 - menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) - - def isLightNode(self, nodePath): - try: - if not nodePath or nodePath.isEmpty(): - return False - - # 修复:统一使用 rp_light_object - if hasattr(nodePath, 'getPythonTag'): - light_object = nodePath.getPythonTag('rp_light_object') - if light_object is not None: - return True - - if hasattr(nodePath, 'getTag'): - light_type = nodePath.getTag('light_type') - if light_type in ["spot_light", "point_light"]: - return True - - if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight: - return True - if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight: - return True - - return False - except Exception as e: - print(f"判断节点是否是灯光节点失败: {str(e)}") - return False - - def deleteLightNode(self, nodePath, item): - """专门处理灯光节点的删除""" - try: - print(f"开始删除灯光节点: {nodePath.getName()}") - - # 从RenderPipeline中移除灯光(如果存在) - if hasattr(nodePath, 'getPythonTag'): - light_object = nodePath.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - print("从RenderPipeline移除灯光") - self.world.render_pipeline.remove_light(light_object) - nodePath.clearPythonTag('rp_light_object') - - if hasattr(self.world,'Spotlight') and nodePath in self.world.Spotlight: - self.world.Spotlight.remove(nodePath) - print("从Spotlight列表中删除") - if hasattr(self.world,'Pointlight') and nodePath in self.world.Pointlight: - self.world.Pointlight.remove(nodePath) - print("从Pointlight列表中移除") - - if hasattr(self.world,'selection'): - if self.world.selection.selectedNode == nodePath: - self.world.selection.clearSelectionBox() - self.world.selection.clearGizmo() - self.world.selection.selectedNode = None - self.world.selection.selectedObject = None - - print(f"移除节点{nodePath.getName()}") - nodePath.removeNode() - - parentItem = item.parent() - if parentItem: - parentItem.removeChild(item) - - print(f"成功删除灯光节点{nodePath.getName()}") - - if hasattr(self.world,'property_panel'): - self.world.property_panel.clearPropertyPanel() - if hasattr(self.world,'selection'): - self.world.selection.updateSelection(None) - - except Exception as e: - print(f"删除灯光节点失败: {str(e)}") - - def _recursiveRemoveLights(self, nodePath): - """递归删除节点及其子节点中的所有灯光""" - if nodePath.isEmpty(): - return - - # 先递归处理所有子节点 - for child in nodePath.getChildren(): - self._recursiveRemoveLights(child) - - # 然后处理当前节点 - if self.isLightNode(nodePath): - print(f"删除子灯光节点: {nodePath.getName()}") - - # 从RenderPipeline中移除灯光 - if hasattr(nodePath, 'getPythonTag'): - light_object = nodePath.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) - nodePath.clearPythonTag('rp_light_object') - - # 从灯光列表中移除 - if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight: - self.world.Spotlight.remove(nodePath) - if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight: - self.world.Pointlight.remove(nodePath) - - 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): """检查是否是模型节点或其子节点""" @@ -248,9 +154,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 bce4d014..3149265d 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -858,11 +858,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 fe7c776b..37d21acf 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -138,6 +138,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) @@ -520,12 +525,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: @@ -1018,27 +1023,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 c84deb42..f794ee07 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 @@ -1002,7 +1003,6 @@ class CustomAssetsTreeWidget(QTreeWidget): internal_paths.append(filepath) # 检查是否是模型文件(用于向外拖拽) if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - print(f"模型路ing!!!!!!!!!!!!!!!!!{QUrl.fromLocalFile(filepath)}") urls.append(QUrl.fromLocalFile(filepath)) # 设置内部拖拽数据 @@ -1369,6 +1369,7 @@ class CustomTreeWidget(QTreeWidget): parent = wrapinstance(0, QWidget) super().__init__(parent) self.world = world + # self.selectedItems = None self.initData() self.setupUI() # 初始化界面 self.setupContextMenu() # 初始化右键菜单 @@ -1402,9 +1403,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) @@ -1682,7 +1688,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: @@ -1773,6 +1781,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 @@ -1823,7 +1845,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) @@ -1839,7 +1861,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)}\" 吗?" @@ -1854,11 +1876,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 @@ -1874,38 +1904,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'): - try: - self.world.render_pipeline.remove_light(light_object) - print(f"移除灯光{panda_node.getName()}") - except Exception as e: - print(f"移除灯光失败: {str(e)}") - panda_node.clearPythonTag('rp_light_object') - #self.world.render_pipeline.remove_light(light_object) - - if hasattr(self.world,'gui_manager') and hasattr(self.world.gui_manager,'gui_elements'): - if panda_node in self.world.gui_manager.gui_elements: - self.world.gui_manager.gui_elements.remove(panda_node) - print(f"从gui_elements列表中移除{panda_node.getName()}") + 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) - # if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: - # self.world.Spotlight.remove(panda_node) - - if hasattr(self.world,'Spotlight'): - self.world.Spotlight = [light for light in self.world.Spotlight if light != panda_node] - if panda_node in self.world.Spotlight: - print(f"从Spotlight列表中移除{panda_node.getName()}") - - # if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: - # self.world.Pointlight.remove(panda_node) - - if hasattr(self.world,'Pointlight'): - self.world.Pointlight = [light for light in self.world.Pointlight if light != panda_node] - if panda_node in self.world.Pointlight: - print(f"从Pointlight列表中移除{panda_node.getName()}") + # 从后往前删除,避免索引问题 + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] # 从Panda3D场景中移除 try: @@ -1935,7 +1959,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): @@ -1967,51 +2128,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 + # 1. 修改内部函数,让它返回创建的节点 + def addNodeToTree(node, parentItem, force=False): + """内部递归函数,现在会返回创建的顶级节点项""" + if not force and should_skip(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(): + # 递归调用,但我们只关心顶级的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 == "" - def addNodeToTree(node, parentItem, force=False): - if not force and should_skip(node): - return 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) - return nodeItem + # 使用一个变量来确保无论哪个分支都有返回值 + new_qt_item = None + node_name = "" try: - from PyQt5.QtWidgets import QTreeWidgetItem - from PyQt5.QtCore import Qt - - # 初始化new_qt_item变量 - new_qt_item = None - if node_type == "IMPORTED_MODEL_NODE": - node_name = node.getTag("file") if hasattr(node, 'getTag') else "model" + # 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): @@ -2085,7 +2263,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 # # 模型节点也可以作为父节点