Merge remote-tracking branch 'origin/main_ch_eg' into addRender

# Conflicts:
#	RenderPipelineFile/rpcore/pynative/internal_light_manager.py
#	core/terrain_manager.py
#	gui/gui_manager.py
#	scene/scene_manager.py
#	ui/interface_manager.py
#	ui/widgets.py
This commit is contained in:
Hector 2025-09-10 09:32:54 +08:00
commit 9fc8f2f5ca
15 changed files with 767 additions and 483 deletions

2
.idea/misc.xml generated
View File

@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.12 (PythonProject)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (EG)" project-jdk-type="Python SDK" />
</project>

View File

@ -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

View File

@ -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")

View File

@ -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--;
// }
}
}
}

View File

@ -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值

View File

@ -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):

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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
# 先递归删除所有子节点中的灯光

View File

@ -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):

View File

@ -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)}")

View File

@ -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):
"""删除指定节点 panda3Dnode- 优化和修复版本"""
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
# # 模型节点也可以作为父节点