forked from Rowland/EG
聚焦功能完善
This commit is contained in:
parent
fc2550ce03
commit
b5545b4b60
@ -32,9 +32,24 @@ class QPanda3DSynchronizer(QTimer):
|
||||
self.setInterval(int(dt))
|
||||
self.timeout.connect(self.tick)
|
||||
|
||||
# def tick(self):
|
||||
# taskMgr.step()
|
||||
# self.qPanda3DWidget.update()
|
||||
|
||||
def tick(self):
|
||||
taskMgr.step()
|
||||
self.qPanda3DWidget.update()
|
||||
try:
|
||||
taskMgr.step()
|
||||
self.qPanda3DWidget.update()
|
||||
except AssertionError as e:
|
||||
if "has_mat()" in str(e):
|
||||
print("⚠️ 检测到变换矩阵错误,跳过此帧")
|
||||
# 继续运行而不是崩溃
|
||||
else:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"❌ 渲染循环错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def get_panda_key_modifiers(evt):
|
||||
|
||||
@ -64,6 +64,13 @@ class SelectionSystem:
|
||||
"z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮
|
||||
}
|
||||
|
||||
#性能优化相关
|
||||
self._optimized_node = False
|
||||
self._last_update_time = 0
|
||||
self._cached_bounds = {}
|
||||
self._gizmo_update_interval = 0.1
|
||||
self._selection_box_update_interval = 0.2
|
||||
|
||||
self._current_cursor = None
|
||||
self._default_cursor = None
|
||||
|
||||
@ -141,7 +148,7 @@ class SelectionSystem:
|
||||
|
||||
# 初始更新选择框
|
||||
#print(" 开始初始化选择框几何体...")
|
||||
self.updateSelectionBoxGeometry()
|
||||
#self.updateSelectionBoxGeometry()
|
||||
|
||||
#print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框")
|
||||
|
||||
@ -277,51 +284,58 @@ class SelectionSystem:
|
||||
traceback.print_exc()
|
||||
|
||||
def updateSelectionBoxTask(self, task):
|
||||
"""选择框更新任务"""
|
||||
"""选择框更新任务 - 平衡性能和实时性"""
|
||||
try:
|
||||
if not hasattr(self,'_last_selection_box_update'):
|
||||
update_interval = 0.1
|
||||
|
||||
if not hasattr(self, '_last_selection_box_update'):
|
||||
self._last_selection_box_update = 0
|
||||
|
||||
import time
|
||||
current_time = time.time()
|
||||
if current_time - self._last_selection_box_update < 0.1:
|
||||
if current_time - self._last_selection_box_update < update_interval:
|
||||
return task.cont
|
||||
self._last_selection_box_update = current_time
|
||||
|
||||
#检查目标节点是否已被删除
|
||||
# 检查目标节点是否已被删除
|
||||
self.checkAndClearIfTargetDeleted()
|
||||
|
||||
if not self.selectionBox or not self.selectionBoxTarget:
|
||||
return task.done # 结束任务
|
||||
return task.done
|
||||
|
||||
# 检查目标节点是否还存在
|
||||
if self.selectionBoxTarget.isEmpty():
|
||||
self.clearSelectionBox()
|
||||
return task.done
|
||||
|
||||
# 获取目标节点在世界坐标系中的当前边界框(使用正确的API)
|
||||
currentMinPoint = Point3()
|
||||
currentMaxPoint = Point3()
|
||||
if not self.selectionBoxTarget.calcTightBounds(currentMinPoint, currentMaxPoint, self.world.render):
|
||||
return task.cont
|
||||
# 检查目标节点是否发生了变化(位置、旋转、缩放)
|
||||
current_transform = self._getNodeTransformKey(self.selectionBoxTarget)
|
||||
|
||||
# 检查边界框是否发生变化(位置或大小)
|
||||
if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or
|
||||
self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint):
|
||||
|
||||
# 更新选择框几何体
|
||||
if (not hasattr(self, '_last_transform_key') or
|
||||
self._last_transform_key != current_transform):
|
||||
# 节点发生了变化,更新选择框
|
||||
self.updateSelectionBoxGeometry()
|
||||
self._last_transform_key = current_transform
|
||||
|
||||
# 保存当前边界框信息
|
||||
self._lastMinPoint = currentMinPoint
|
||||
self._lastMaxPoint = currentMaxPoint
|
||||
|
||||
return task.cont # 继续任务
|
||||
return task.cont
|
||||
|
||||
except Exception as e:
|
||||
print(f"选择框更新任务出错: {str(e)}")
|
||||
return task.done
|
||||
|
||||
def _getNodeTransformKey(self, node):
|
||||
"""获取节点变换的关键信息,用于快速比较"""
|
||||
try:
|
||||
# 获取节点的关键变换信息
|
||||
pos = node.getPos(self.world.render)
|
||||
hpr = node.getHpr(self.world.render)
|
||||
scale = node.getScale(self.world.render)
|
||||
|
||||
# 返回一个可以比较的元组
|
||||
return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z)
|
||||
except:
|
||||
return None
|
||||
|
||||
def clearSelectionBox(self):
|
||||
"""清除选择框"""
|
||||
if self.selectionBox:
|
||||
@ -1909,9 +1923,7 @@ class SelectionSystem:
|
||||
|
||||
def updateSelection(self, nodePath):
|
||||
try:
|
||||
# 检查是否要选择的对象已经是当前选中的对象
|
||||
if self.selectedNode == nodePath:
|
||||
#print("要选择的对象已经是当前选中的对象,跳过重复更新")
|
||||
return
|
||||
print(f"\n=== 更新选择状态 ===")
|
||||
|
||||
@ -2203,8 +2215,31 @@ class SelectionSystem:
|
||||
maxPoint = Point3()
|
||||
|
||||
if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
print("无法计算选中节点的边界框")
|
||||
return False
|
||||
print("无法计算选中节点的边界框,使用节点为位置作为替代方案")
|
||||
node_pos = self.selectedNode.getPos(self.world.render)
|
||||
optimal_distance = 10.0
|
||||
current_cam_pos = self.world.cam.getPos()
|
||||
view_direction = node_pos - current_cam_pos
|
||||
if view_direction.length()<0.001:
|
||||
view_direction = Vec3(5,-5,2)
|
||||
view_direction.normalize()
|
||||
target_cam_pos = node_pos - (view_direction * optimal_distance)
|
||||
|
||||
temp_node =self.world.render.attachNewNode("temp_lookat_target")
|
||||
temp_node.setPos(node_pos)
|
||||
dummy_cam = self.world.render.attachNewNode("dummy_camera")
|
||||
dummy_cam.setPos(target_cam_pos)
|
||||
dummy_cam.lookAt(temp_node)
|
||||
target_cam_hpr = Vec3(dummy_cam.getHpr())
|
||||
|
||||
temp_node.removeNode()
|
||||
dummy_cam.removeNode()
|
||||
|
||||
currrent_cam_pos = Point3(self.world.cam.getPos())
|
||||
current_cam_hpr = Vec3(self.world.cam.getHpr())
|
||||
self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr)
|
||||
print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}")
|
||||
return True
|
||||
|
||||
# 计算节点中心点和大小
|
||||
center = Point3(
|
||||
@ -2371,100 +2406,6 @@ class SelectionSystem:
|
||||
traceback.print_exc()
|
||||
return task.done
|
||||
|
||||
def focusCameraOnSelectedNode(self):
|
||||
"""将摄像机聚焦到选中的节点(无动画版本,但仍保持平滑转向)"""
|
||||
try:
|
||||
if not self.selectedNode or self.selectedNode.isEmpty():
|
||||
print("没有选中的节点,无法聚焦")
|
||||
return False
|
||||
|
||||
# 获取选中节点的边界框
|
||||
minPoint = Point3()
|
||||
maxPoint = Point3()
|
||||
|
||||
if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
print("无法计算选中节点的边界框")
|
||||
return False
|
||||
|
||||
# 计算节点中心点
|
||||
center = Point3(
|
||||
(minPoint.x + maxPoint.x) * 0.5,
|
||||
(minPoint.y + maxPoint.y) * 0.5,
|
||||
(minPoint.z + maxPoint.z) * 0.5
|
||||
)
|
||||
|
||||
# 计算节点的大小(直径)
|
||||
size = (maxPoint - minPoint).length()
|
||||
|
||||
# 如果节点太小,使用默认大小
|
||||
if size < 0.1:
|
||||
size = 5.0
|
||||
|
||||
# 获取当前摄像机位置
|
||||
current_cam_pos = Point3(self.world.cam.getPos())
|
||||
|
||||
# 计算观察方向
|
||||
view_direction = current_cam_pos - center
|
||||
if view_direction.length() < 0.001:
|
||||
# 如果摄像机正好在中心点,使用默认方向
|
||||
view_direction = Vec3(5, -5, 2) # 默认观察方向
|
||||
|
||||
# 标准化方向向量
|
||||
view_direction.normalize()
|
||||
|
||||
# 计算合适的距离(基于节点大小)
|
||||
optimal_distance = max(size * 3.0, 10.0) # 距离节点的距离是节点大小的3倍或至少10个单位
|
||||
|
||||
# 计算新的摄像机位置
|
||||
new_cam_pos = center + (view_direction * optimal_distance)
|
||||
|
||||
# 平滑地设置摄像机位置和朝向
|
||||
# 创建临时节点用于计算目标朝向
|
||||
temp_lookat = self.world.render.attachNewNode("temp_lookat")
|
||||
temp_lookat.setPos(center)
|
||||
|
||||
# 获取当前朝向和目标朝向
|
||||
current_hpr = Vec3(self.world.cam.getHpr())
|
||||
|
||||
# 设置摄像机到目标位置
|
||||
self.world.cam.setPos(new_cam_pos)
|
||||
self.world.cam.lookAt(temp_lookat)
|
||||
target_hpr = Vec3(self.world.cam.getHpr())
|
||||
|
||||
# 恢复当前位置
|
||||
self.world.cam.setPos(current_cam_pos)
|
||||
self.world.cam.setHpr(current_hpr)
|
||||
|
||||
# 清理临时节点
|
||||
temp_lookat.removeNode()
|
||||
|
||||
# 使用一个简单的任务来平滑过渡
|
||||
class SmoothCameraMoveData:
|
||||
def __init__(self, start_pos, end_pos, start_hpr, end_hpr):
|
||||
self.start_pos = Point3(start_pos)
|
||||
self.end_pos = Point3(end_pos)
|
||||
self.start_hpr = Vec3(start_hpr)
|
||||
self.end_hpr = Vec3(end_hpr)
|
||||
self.elapsed_time = 0.0
|
||||
self.duration = 0.5 # 0.5秒的平滑过渡
|
||||
|
||||
self._smooth_camera_move_data = SmoothCameraMoveData(
|
||||
current_cam_pos, new_cam_pos, current_hpr, target_hpr
|
||||
)
|
||||
|
||||
taskMgr.remove("smoothCameraMoveTask")
|
||||
taskMgr.add(self._smoothCameraMoveTask, "smoothCameraMoveTask")
|
||||
|
||||
print(f"摄像机开始聚焦到节点: {self.selectedNode.getName()}")
|
||||
print(f"节点中心: {center}, 大小: {size:.2f}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"聚焦摄像机到选中节点失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _smoothCameraMoveTask(self, task):
|
||||
"""平滑摄像机移动任务"""
|
||||
try:
|
||||
|
||||
@ -326,6 +326,7 @@ class GUIManager:
|
||||
label.setTag("is_scene_element", "1")
|
||||
label.setTag("created_by_user", "1")
|
||||
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||
label.setTag("name",label_name)
|
||||
label.setName(label_name)
|
||||
|
||||
# 如果有GUI父节点,建立引用关系
|
||||
@ -416,6 +417,10 @@ class GUIManager:
|
||||
parent_gui_node = None
|
||||
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
||||
|
||||
font = None
|
||||
if hasattr(self.world,'getChineseFont'):
|
||||
font = self.world.getChineseFont()
|
||||
|
||||
entry = DirectEntry(
|
||||
text="",
|
||||
pos=gui_pos,
|
||||
@ -426,7 +431,9 @@ class GUIManager:
|
||||
numLines=1,
|
||||
width=12,
|
||||
focus=0,
|
||||
parent=parent_gui_node # 设置GUI父节点
|
||||
parent=parent_gui_node, # 设置GUI父节点
|
||||
text_font = font,
|
||||
frameSize=(-0.1,0.1,-0.05,0.05)
|
||||
)
|
||||
|
||||
# 设置节点标签
|
||||
@ -438,6 +445,7 @@ class GUIManager:
|
||||
entry.setTag("is_scene_element", "1")
|
||||
entry.setTag("created_by_user", "1")
|
||||
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||
entry.setTag("name",entry_name)
|
||||
entry.setName(entry_name)
|
||||
|
||||
# 如果有GUI父节点,建立引用关系
|
||||
@ -572,6 +580,7 @@ class GUIManager:
|
||||
image_node.setTag("tree_item_type", "GUI_IMAGE")
|
||||
image_node.setTag("created_by_user", "1")
|
||||
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||
image_node.setTag("name",image_name)
|
||||
image_node.setName(image_name)
|
||||
|
||||
# 如果有GUI父节点,建立引用关系
|
||||
@ -861,6 +870,7 @@ class GUIManager:
|
||||
image_node.setTag("is_scene_element", "1")
|
||||
image_node.setTag("tree_item_type", "GUI_3DIMAGE")
|
||||
image_node.setTag("created_by_user", "1")
|
||||
image_node.setTag("name",image_name)
|
||||
|
||||
# 添加到GUI元素列表
|
||||
self.gui_elements.append(image_node)
|
||||
@ -979,6 +989,7 @@ class GUIManager:
|
||||
video_screen.setTag("is_scene_element", "1")
|
||||
video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN")
|
||||
video_screen.setTag("created_by_user", "1")
|
||||
video_screen.setTag("name",screen_name)
|
||||
|
||||
# 设置视频路径标签
|
||||
if video_path and os.path.exists(video_path):
|
||||
@ -1496,6 +1507,7 @@ class GUIManager:
|
||||
video_screen.setTag("is_scene_element", "1")
|
||||
video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN")
|
||||
video_screen.setTag("created_by_user", "1")
|
||||
video_screen.setTag("name",screen_name)
|
||||
|
||||
video_screen.setTag("video_path", video_path if video_path else "")
|
||||
print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}")
|
||||
|
||||
@ -786,7 +786,6 @@ class SceneManager:
|
||||
"""为模型设置碰撞检测(增强版本)"""
|
||||
try:
|
||||
|
||||
|
||||
# 创建碰撞节点
|
||||
cNode = CollisionNode(f'modelCollision_{model.getName()}')
|
||||
|
||||
@ -969,24 +968,31 @@ class SceneManager:
|
||||
gui_info["panel_data"] = gui_node.getTag("info_panel_data")
|
||||
|
||||
if hasattr(self.world, 'script_manager') and self.world.script_manager:
|
||||
script_manager = self.world.script_manager
|
||||
# 获取挂载在此节点上的所有脚本
|
||||
scripts = script_manager.get_scripts_on_object(gui_node)
|
||||
if scripts:
|
||||
gui_info["scripts"] = []
|
||||
for script_component in scripts:
|
||||
script_name = script_component.script_name # 使用脚本组件的名称
|
||||
print(f"收集脚本信息: {script_name}")
|
||||
|
||||
# 获取脚本类的文件路径
|
||||
script_class = script_component.script_instance.__class__
|
||||
script_file = self._get_script_file_path(script_class, script_name)
|
||||
|
||||
gui_info["scripts"].append({
|
||||
"name": script_name,
|
||||
"file": script_file
|
||||
})
|
||||
print(f"脚本 {script_name} 来自文件: {script_file}")
|
||||
try:
|
||||
script_manager = self.world.script_manager
|
||||
scripts = script_manager.get_scripts_on_object(gui_node)
|
||||
if scripts:
|
||||
gui_info["scripts"] = []
|
||||
for script_component in scripts:
|
||||
try:
|
||||
script_name = script_manager.script_name
|
||||
#获取脚本路径
|
||||
script_class = script_component.script_instance.__class__
|
||||
script_file = self._get_script_file_path(script_class,script_name)
|
||||
#只有当脚本文件存在时才保存
|
||||
if script_file and os.path.exists(script_file):
|
||||
gui_info["scirpts"].append({
|
||||
"name":script_name,
|
||||
"file":script_file
|
||||
})
|
||||
print(f"收集脚本信息{script_name}from {script_file}")
|
||||
else:
|
||||
print(f"警告: 脚本文件不存在: {script_file}")
|
||||
except Exception as e:
|
||||
print(f"收集单个脚本信息失败{script_name},错误{e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"收集脚本信息失败{e}")
|
||||
|
||||
print(f"成功收集GUI元素信息: {gui_info}")
|
||||
return gui_info
|
||||
@ -1165,6 +1171,22 @@ class SceneManager:
|
||||
node.setTag("transform_scale", str(node.getScale()))
|
||||
print(f"保存节点 {node.getName()} 的变换信息")
|
||||
|
||||
# 保存父子关系信息 - 关键修改
|
||||
parent = node.getParent()
|
||||
if parent and not parent.isEmpty() and parent != self.world.render:
|
||||
# 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系
|
||||
if parent.getName() not in ["render", "aspect2d", "render2d"]:
|
||||
# 检查父节点是否也是场景中的模型
|
||||
is_parent_model = False
|
||||
for model in self.models:
|
||||
if model == parent:
|
||||
is_parent_model = True
|
||||
break
|
||||
|
||||
if is_parent_model:
|
||||
node.setTag("parent_name", parent.getName())
|
||||
print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}")
|
||||
|
||||
# 获取当前状态
|
||||
state = node.getState()
|
||||
|
||||
@ -1203,7 +1225,6 @@ class SceneManager:
|
||||
node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown"
|
||||
node.setTag("saved_gui_type", gui_type)
|
||||
|
||||
|
||||
# 保存GUI元素的通用属性
|
||||
if hasattr(node, 'getPythonTag'):
|
||||
# 保存任何Python标签数据
|
||||
@ -1403,11 +1424,17 @@ class SceneManager:
|
||||
processed_lights = []
|
||||
# 用于存储处理后的GUI元素,避免重复处理
|
||||
|
||||
#存储所有加载的节点,用于后续处理父子关系
|
||||
loaded_nodes = {} #name->nodePath映射
|
||||
|
||||
# 遍历场景中的所有节点
|
||||
def processNode(nodePath, depth=0):
|
||||
indent = " " * depth
|
||||
print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})")
|
||||
|
||||
#存储节点以便后续处理父子关系
|
||||
loaded_nodes[nodePath.getName()] = nodePath
|
||||
|
||||
# 跳过render节点的递归
|
||||
if nodePath.getName() == "render" and depth > 0:
|
||||
print(f"{indent}跳过重复的render节点")
|
||||
@ -1655,6 +1682,10 @@ class SceneManager:
|
||||
print("\n开始处理场景节点...")
|
||||
processNode(scene)
|
||||
|
||||
#处理父子关系 - 在所有节点加载完成后设置正确的父子关系
|
||||
print("\n开始重建父子关系...")
|
||||
self._rebuildParentChildRelationships(loaded_nodes)
|
||||
|
||||
# 加载GUI信息并重新创建非3D的GUI元素
|
||||
gui_info_file = filename.replace('.bam', '_gui.json')
|
||||
if os.path.exists(gui_info_file):
|
||||
@ -1704,6 +1735,50 @@ class SceneManager:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _rebuildParentChildRelationships(self, loaded_nodes):
|
||||
try:
|
||||
parent_child_relations = []
|
||||
for node_name, node in loaded_nodes.items():
|
||||
if node.hasTag("parent_name"):
|
||||
parent_name = node.getTag("parent_name")
|
||||
if parent_name in loaded_nodes:
|
||||
parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组
|
||||
print(f"发现父子关系:{parent_name}->{node_name}")
|
||||
else:
|
||||
print(f"警告:节点{node_name}的父节点{parent_name}不存在")
|
||||
for child_node, parent_node in parent_child_relations:
|
||||
try:
|
||||
child_node.wrtReparentTo(parent_node)
|
||||
print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}")
|
||||
except Exception as e:
|
||||
print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}")
|
||||
|
||||
if not parent_child_relations:
|
||||
print("尝试从场景结构推断父子关系")
|
||||
self._inferParentChildRelationships(loaded_nodes)
|
||||
|
||||
print("父子关系重建完成")
|
||||
except Exception as e:
|
||||
print(f"重建父子关系时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"重建父子关系时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _inferParentChildRelationships(self, loaded_nodes):
|
||||
"""从场景结构推断父子关系"""
|
||||
try:
|
||||
# 这里可以添加更复杂的父子关系推断逻辑
|
||||
# 例如,根据节点名称、位置关系等进行推断
|
||||
# 目前保持简单,后续可以扩展
|
||||
print("父子关系推断完成(当前为空实现)")
|
||||
except Exception as e:
|
||||
print(f"推断父子关系时出错: {e}")
|
||||
|
||||
def _shouldSkipNodeInTree(self, nodePath):
|
||||
"""判断节点是否应该在场景树中跳过显示"""
|
||||
# 跳过render节点的递归
|
||||
@ -1733,6 +1808,7 @@ class SceneManager:
|
||||
"""根据保存的GUI数据重新创建GUI元素"""
|
||||
try:
|
||||
gui_manager = getattr(self.world, 'gui_manager', None)
|
||||
property_manager = getattr(self.world, 'property_panel', None)
|
||||
info_panel_manager = getattr(self.world,'info_panel_manager',None)
|
||||
if not gui_manager:
|
||||
print("GUI管理器未找到,无法重建GUI元素")
|
||||
@ -1753,6 +1829,20 @@ class SceneManager:
|
||||
"parent_name": gui_info.get("parent_name")
|
||||
}
|
||||
|
||||
valid_parents = set()
|
||||
for gui_info in gui_data:
|
||||
name = gui_info.get("name",f"gui_element_{gui_info.get('index',0)}")
|
||||
valid_parents.add(name)
|
||||
|
||||
if hasattr(self.world,'gui_elements'):
|
||||
for elem in self.world.gui_elements:
|
||||
if elem and not elem.isEmpty():
|
||||
valid_parents.add(elem.getName())
|
||||
|
||||
valid_parents.add("render")
|
||||
valid_parents.add("aspect2d")
|
||||
valid_parents.add("render2d")
|
||||
|
||||
pos = (0, 0, 0)
|
||||
for i, gui_info in enumerate(gui_data):
|
||||
try:
|
||||
@ -1774,6 +1864,10 @@ class SceneManager:
|
||||
print(f"跳过重复元素: {name}")
|
||||
continue
|
||||
|
||||
if parent_name and parent_name not in valid_parents:
|
||||
print(f"⚠️ 跳过元素 {name},因为其父级 {parent_name} 不存在")
|
||||
continue
|
||||
|
||||
processed_names.add(name)
|
||||
|
||||
print(f"重建GUI元素: {name} (类型: {gui_type})")
|
||||
@ -1863,14 +1957,31 @@ class SceneManager:
|
||||
size=absolute_scale,
|
||||
video_path=video_path
|
||||
)
|
||||
if video_path and new_element and hasattr(gui_manager, 'loadVideoFile'):
|
||||
# 延迟一帧执行,确保节点完全初始化
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
def load_video_task(task):
|
||||
gui_manager.loadVideoFile(new_element, video_path)
|
||||
return task.done
|
||||
if video_path and new_element:
|
||||
if video_path.startswith("http://") or video_path.startswith("https://"):
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
def load_video_stream_task(task):
|
||||
if hasattr(property_manager,'_loadVideoFromURLWithOpenCV_3D'):
|
||||
property_manager._loadVideoFromURLWithOpenCV_3D(new_element,video_path)
|
||||
return task.done
|
||||
|
||||
taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask')
|
||||
taskMgr.doMethodLater(0.5, load_video_stream_task, 'loadVideoStreamTask')
|
||||
else:
|
||||
if hasattr(gui_manager,'loadVideoFile'):
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
def load_video_file_task(task):
|
||||
gui_manager.loadVideoFile(new_element,video_path)
|
||||
return task.done
|
||||
taskMgr.doMethodLater(0.1,load_video_file_task,'loadVideoFileTask')
|
||||
|
||||
# if video_path and new_element and hasattr(gui_manager, 'loadVideoFile'):
|
||||
# # 延迟一帧执行,确保节点完全初始化
|
||||
# from direct.task.TaskManagerGlobal import taskMgr
|
||||
# def load_video_task(task):
|
||||
# gui_manager.loadVideoFile(new_element, video_path)
|
||||
# return task.done
|
||||
#
|
||||
# taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask')
|
||||
|
||||
elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'):
|
||||
new_element = gui_manager.createGUI2DVideoScreen(
|
||||
@ -1878,54 +1989,22 @@ class SceneManager:
|
||||
size=absolute_scale,
|
||||
video_path=video_path
|
||||
)
|
||||
elif gui_type in ["info_panel", "info_panel_3d"]:
|
||||
# 重建信息面板
|
||||
if info_panel_manager:
|
||||
try:
|
||||
if panel_data:
|
||||
# 从序列化数据重建面板
|
||||
import json
|
||||
panel_data_obj = json.loads(panel_data)
|
||||
new_element = info_panel_manager.recreatePanelFromData(panel_data_obj)
|
||||
else:
|
||||
# 创建新的面板
|
||||
if gui_type == "info_panel_3d":
|
||||
# 3D信息面板
|
||||
new_element = info_panel_manager.create3DInfoPanel(
|
||||
panel_id=panel_id,
|
||||
position=tuple(position) if len(position) >= 3 else (0, 0, 0),
|
||||
size=tuple(scale) if len(scale) >= 2 else (1.0, 0.6),
|
||||
visible=not tags.get("hidden", False)
|
||||
)
|
||||
else:
|
||||
# 2D信息面板
|
||||
pos_2d = (position[0], position[2]) if len(position) >= 3 else (0, 0)
|
||||
new_element = info_panel_manager.createInfoPanel(
|
||||
panel_id=panel_id,
|
||||
position=pos_2d,
|
||||
size=tuple(scale) if len(scale) >= 2 else (1.0, 0.6),
|
||||
visible=not tags.get("hidden", False)
|
||||
)
|
||||
|
||||
# 设置背景图片
|
||||
if bg_image_path and hasattr(info_panel_manager, 'setPanelBackgroundImage'):
|
||||
info_panel_manager.setPanelBackgroundImage(panel_id, bg_image_path)
|
||||
|
||||
# 更新面板内容
|
||||
if text:
|
||||
title, content = text.split('\n', 1) if '\n' in text else ("信息面板", text)
|
||||
if gui_type == "info_panel_3d":
|
||||
info_panel_manager.update3DPanelContent(panel_id, title=title,
|
||||
content=content)
|
||||
else:
|
||||
info_panel_manager.updatePanelContent(panel_id, title=title,
|
||||
content=content)
|
||||
except Exception as e:
|
||||
print(f"重建信息面板失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print("信息面板管理器未找到,无法重建信息面板")
|
||||
if video_path and new_element:
|
||||
if video_path.startswith("http://") or video_path.startswith("https://"):
|
||||
pass
|
||||
# from direct.task.TaskManagerGlobal import taskMgr
|
||||
# def load_2d_video_stream_task(task):
|
||||
# if hasattr(property_manager,'_loadVideoFromURLWithOpenCV'):
|
||||
# property_manager._loadVideoFromURLWithOpenCV(new_element,video_path)
|
||||
# return task.done
|
||||
# taskMgr.doMethodLater(0.1,load_2d_video_stream_task,'load2DVideoStreamTask')
|
||||
else:
|
||||
if hasattr(property_manager,'load2DVideoFile'):
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
def load_2d_video_file_task(task):
|
||||
property_manager.load2DVideoFile(new_element,video_path)
|
||||
return task.done
|
||||
taskMgr.doMethodLater(0.1,load_2d_video_file_task,'load2DVideoFileTask')
|
||||
|
||||
|
||||
# 如果创建成功,设置属性
|
||||
@ -1958,48 +2037,6 @@ class SceneManager:
|
||||
|
||||
created_elements[name] = new_element
|
||||
|
||||
# 重新挂载脚本(如果有的话)
|
||||
# 在 _recreateGUIElementsFromData 方法中找到重新挂载脚本的部分,替换为以下代码:
|
||||
|
||||
# 重新挂载脚本(如果有的话)
|
||||
# if "scripts" in gui_info and hasattr(self.world,
|
||||
# 'script_manager') and self.world.script_manager:
|
||||
# script_manager = self.world.script_manager
|
||||
# for script_info in gui_info["scripts"]:
|
||||
# script_name = script_info["name"]
|
||||
# script_file = script_info.get("file", "")
|
||||
#
|
||||
# print(f"尝试重新挂载脚本: {script_name} from {script_file}")
|
||||
#
|
||||
# # 检查脚本是否已加载
|
||||
# if script_name not in script_manager.loader.script_classes:
|
||||
# # 如果脚本未加载,尝试从保存的文件路径加载
|
||||
# if script_file and os.path.exists(script_file):
|
||||
# print(f"从文件加载脚本: {script_file}")
|
||||
# loaded_class = script_manager.load_script_from_file(script_file)
|
||||
# if loaded_class is None:
|
||||
# print(f"从文件加载脚本失败: {script_file}")
|
||||
# # 如果从文件加载失败,尝试在脚本目录中查找
|
||||
# script_path = self._find_script_in_directory(script_name)
|
||||
# if script_path:
|
||||
# print(f"从目录找到脚本并加载: {script_path}")
|
||||
# script_manager.load_script_from_file(script_path)
|
||||
# else:
|
||||
# # 如果没有文件路径或文件不存在,尝试在脚本目录中查找
|
||||
# script_path = self._find_script_in_directory(script_name)
|
||||
# if script_path:
|
||||
# print(f"从目录找到脚本并加载: {script_path}")
|
||||
# script_manager.load_script_from_file(script_path)
|
||||
# else:
|
||||
# print(f"找不到脚本文件: {script_name}")
|
||||
#
|
||||
# # 为元素添加脚本
|
||||
# script_component = script_manager.add_script_to_object(new_element, script_name)
|
||||
# if script_component:
|
||||
# print(f"成功为 {name} 添加脚本: {script_name}")
|
||||
# else:
|
||||
# print(f"为 {name} 添加脚本失败: {script_name}")
|
||||
|
||||
print(f"GUI元素重建成功: {name}")
|
||||
else:
|
||||
print(f"无法重建GUI元素: {name} (类型: {gui_type})")
|
||||
@ -2239,6 +2276,7 @@ class SceneManager:
|
||||
try:
|
||||
from RenderPipelineFile.rpcore import SpotLight
|
||||
from QPanda3D.Panda3DWorld import get_render_pipeline
|
||||
from panda3d.core import Vec3
|
||||
|
||||
# 创建聚光灯对象
|
||||
light = SpotLight()
|
||||
@ -2256,9 +2294,6 @@ class SceneManager:
|
||||
light.casts_shadows = True
|
||||
light.shadow_map_resolution = 256
|
||||
|
||||
# 设置位置
|
||||
light.setPos(light_node.getPos())
|
||||
|
||||
# 添加到渲染管线
|
||||
render_pipeline = get_render_pipeline()
|
||||
render_pipeline.add_light(light)
|
||||
@ -2276,6 +2311,8 @@ class SceneManager:
|
||||
print(f"重新创建聚光灯: {light_node.getName()}")
|
||||
except Exception as e:
|
||||
print(f"重新创建聚光灯失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _recreatePointLight(self, light_node):
|
||||
"""重新创建点光源"""
|
||||
@ -2318,6 +2355,8 @@ class SceneManager:
|
||||
print(f"重新创建点光源: {light_node.getName()}")
|
||||
except Exception as e:
|
||||
print(f"重新创建点光源失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _cleanupAuxiliaryNodes(self):
|
||||
"""清理场景中可能存在的辅助节点"""
|
||||
@ -2452,6 +2491,8 @@ class SceneManager:
|
||||
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
|
||||
light_np.setPos(*pos)
|
||||
|
||||
light_np.setTransform(TransformState.makeIdentity())
|
||||
|
||||
# 创建聚光灯对象
|
||||
light = SpotLight()
|
||||
light.direction = Vec3(0, 0, -1)
|
||||
@ -2461,7 +2502,6 @@ class SceneManager:
|
||||
light.radius = 1000
|
||||
light.casts_shadows = True
|
||||
light.shadow_map_resolution = 256
|
||||
light.setPos(*pos)
|
||||
|
||||
# 添加到渲染管线
|
||||
render_pipeline.add_light(light)
|
||||
@ -2558,11 +2598,12 @@ class SceneManager:
|
||||
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
|
||||
light_np.setPos(*pos)
|
||||
|
||||
# 确保变换矩阵有效
|
||||
light_np.setTransform(TransformState.makeIdentity())
|
||||
|
||||
# 创建点光源对象
|
||||
light = PointLight()
|
||||
|
||||
light.setPos(*pos)
|
||||
|
||||
light.energy = 5000
|
||||
light.radius = 1000
|
||||
light.inner_radius = 0.4
|
||||
|
||||
@ -21,6 +21,10 @@ class RotatorScript(ScriptBase):
|
||||
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
|
||||
|
||||
def update(self, dt):
|
||||
# 检查 gameObject 是否存在且不为空
|
||||
if not self.gameObject or self.gameObject.isEmpty():
|
||||
print("RotatorScript: gameObject is empty or None, skipping update")
|
||||
return
|
||||
"""每帧更新"""
|
||||
if not self.is_rotating:
|
||||
return
|
||||
|
||||
@ -105,9 +105,10 @@ class PropertyPanelManager:
|
||||
|
||||
def updatePropertyPanel(self, item):
|
||||
"""更新属性面板显示"""
|
||||
if not self._propertyLayout or not self._propertyLayout.parent():
|
||||
print("属性布局未设置或没有父部件!")
|
||||
return
|
||||
# if not self._propertyLayout or not self._propertyLayout.parent():
|
||||
# print("属性布局未设置或没有父部件!")
|
||||
# return
|
||||
#更健壮的有效性检查
|
||||
|
||||
self._cleanupAllReferences()
|
||||
self.clearPropertyPanel()
|
||||
@ -3384,6 +3385,12 @@ class PropertyPanelManager:
|
||||
video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0)
|
||||
path_label = QLabel(video_path)
|
||||
path_label.setWordWrap(True)
|
||||
if len(video_path)>30:
|
||||
display_path = video_path[:27]+"..."
|
||||
path_label.setToolTip(video_path)
|
||||
else:
|
||||
display_path = video_path
|
||||
path_label.setText(display_path)
|
||||
path_label.setStyleSheet("color: #00AAFF;")
|
||||
video_info_layout.addWidget(path_label, 0, 1)
|
||||
elif os.path.exists(video_path):
|
||||
@ -8895,7 +8902,7 @@ except Exception as e:
|
||||
spinbox = QDoubleSpinBox()
|
||||
spinbox.setRange(min_val, max_val)
|
||||
spinbox.setDecimals(decimals)
|
||||
spinbox.setSingleStep(0.1)
|
||||
spinbox.setSingleStep(0.01)
|
||||
return spinbox
|
||||
|
||||
def _addSphereParameters(self, model, layout, start_row):
|
||||
@ -8906,7 +8913,7 @@ except Exception as e:
|
||||
radius_label = QLabel("半径:")
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_radius = self._createCollisionSpinBox(0.01, 1000000, 2)
|
||||
self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -8938,9 +8945,9 @@ except Exception as e:
|
||||
current_row += 1
|
||||
|
||||
# 宽度、长度、高度
|
||||
self.collision_width = self._createCollisionSpinBox(0.01, 1000000, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.1, 1000000, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.1, 1000000, 2)
|
||||
self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -8988,7 +8995,7 @@ except Exception as e:
|
||||
radius_label = QLabel("半径:")
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 1000000, 2)
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -9016,7 +9023,7 @@ except Exception as e:
|
||||
height_label = QLabel("高度:")
|
||||
layout.addWidget(height_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -9885,7 +9892,7 @@ except Exception as e:
|
||||
radius_label.setVisible(True)
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_radius.setVisible(True)
|
||||
self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v))
|
||||
layout.addWidget(self.collision_radius, current_row, 1)
|
||||
@ -9902,9 +9909,9 @@ except Exception as e:
|
||||
layout.addWidget(size_label, current_row, 0)
|
||||
current_row += 1
|
||||
|
||||
self.collision_width = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
|
||||
width_label = QLabel("宽度:")
|
||||
width_label.setVisible(True)
|
||||
@ -9941,7 +9948,7 @@ except Exception as e:
|
||||
radius_label = QLabel("半径:")
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v))
|
||||
layout.addWidget(self.collision_capsule_radius, current_row, 1)
|
||||
current_row += 1
|
||||
@ -9949,7 +9956,7 @@ except Exception as e:
|
||||
height_label = QLabel("高度:")
|
||||
layout.addWidget(height_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v))
|
||||
layout.addWidget(self.collision_capsule_height, current_row, 1)
|
||||
current_row += 1
|
||||
|
||||
@ -1367,6 +1367,27 @@ class CustomConsoleDockWidget(QWidget):
|
||||
""")
|
||||
toolbar.addWidget(self.timestampBtn)
|
||||
|
||||
self.fpsLabel = QLabel("FPS:0.0")
|
||||
self.fpsLabel.setStyleSheet("""
|
||||
QLabel {
|
||||
background-color: #2d2d44;
|
||||
color: #80ff80;
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
""")
|
||||
self.fpsLabel.setMinimumWidth(100)
|
||||
self.fpsLabel.setAlignment(Qt.AlignCenter)
|
||||
toolbar.addWidget(self.fpsLabel)
|
||||
|
||||
# 帧率更新定时器
|
||||
self.fpsTimer = QTimer()
|
||||
self.fpsTimer.timeout.connect(self.updateFPS)
|
||||
self.fpsTimer.start(1000) # 每秒更新一次
|
||||
|
||||
toolbar.addStretch()
|
||||
layout.addLayout(toolbar)
|
||||
|
||||
@ -1408,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget):
|
||||
# 添加欢迎信息
|
||||
self.addMessage("🎮 编辑器控制台已启动", "INFO")
|
||||
|
||||
def updateFPS(self):
|
||||
try:
|
||||
if hasattr(self.world,'clock'):
|
||||
fps = self.world.clock.getAverageFrameRate()
|
||||
self.fpsLabel.setText(f"FPS:{fps:.1f}")
|
||||
|
||||
# 根据帧率设置颜色
|
||||
if fps >= 50:
|
||||
color = "#80ff80" # 绿色 - 优秀
|
||||
elif fps >= 30:
|
||||
color = "#ffff80" # 黄色 - 一般
|
||||
else:
|
||||
color = "#ff8080" # 红色 - 较差
|
||||
|
||||
self.fpsLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: #2d2d44;
|
||||
color: {color};
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}}
|
||||
""")
|
||||
except Exception as e:
|
||||
pass # 静默处理错误,避免影响控制台功能
|
||||
|
||||
def setupConsoleRedirect(self):
|
||||
"""设置控制台重定向"""
|
||||
import sys
|
||||
@ -1890,7 +1939,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
elif is_dragged_3d_scene:
|
||||
if is_target_3d_scene:
|
||||
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
|
||||
return False
|
||||
return True
|
||||
elif is_target_2d_gui:
|
||||
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
|
||||
print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系")
|
||||
@ -1899,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
elif is_target_3d_gui:
|
||||
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下")
|
||||
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
||||
return False
|
||||
@ -2089,7 +2138,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
return
|
||||
|
||||
# 默认选中场景根节点,通常是第一个顶级节点
|
||||
next_item_to_select = self.topLevelItem(0)
|
||||
#next_item_to_select = self.topLevelItem(0)
|
||||
|
||||
# 3. 执行删除循环
|
||||
deleted_count = 0
|
||||
@ -2174,25 +2223,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
# 4. 删除操作完成后,更新UI ---
|
||||
if deleted_count > 0:
|
||||
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
|
||||
|
||||
# 检查预备选择的节点是否还有效 (例如,父节点可能也一同被删了)
|
||||
# 如果next_item_to_select在树中找不到了,就退回到选择根节点
|
||||
if next_item_to_select and self.indexFromItem(next_item_to_select).isValid():
|
||||
new_selection_item = next_item_to_select
|
||||
else:
|
||||
# 如果之前的父节点也一并被删除了,就默认选择场景根节点
|
||||
new_selection_item = self.topLevelItem(0)
|
||||
|
||||
if new_selection_item:
|
||||
# 设置UI树的选择
|
||||
self.setCurrentItem(new_selection_item)
|
||||
# 获取对应的Panda3D节点
|
||||
new_panda_node = new_selection_item.data(0, Qt.UserRole)
|
||||
# 调用您提供的函数来更新选择状态和属性面板
|
||||
#self.update_selection_and_properties(new_panda_node, new_selection_item)
|
||||
else:
|
||||
# 如果连根节点都没有了(例如清空场景),则清空选择
|
||||
self.update_selection_and_properties(None, None)
|
||||
self.update_selection_and_properties(None, None)
|
||||
|
||||
def delete_item(self, panda_node):
|
||||
"""删除指定节点 panda3D(node)- 优化和修复版本"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user