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
# # 模型节点也可以作为父节点