1
0
forked from Rowland/EG

addRender #26

Merged
Hector merged 11 commits from addRender into main 2025-09-10 01:13:50 +00:00
21 changed files with 6164 additions and 978 deletions

4
.idea/EG.iml generated
View File

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

View File

@ -53,7 +53,7 @@ class Panda3DWorld(ShowBase):
Panda3DWorld : A class to handle all panda3D world manipulation
"""
def __init__(self, width=1380, height=729, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
name="qpanda3D"):
global _global_world_instance

File diff suppressed because one or more lines are too long

View File

@ -116,6 +116,7 @@ class InternalLightManager(object):
source.set_slot(slot)
def remove_light(self, light):
print("111111111111111111111111111111111111111111111111")
assert light is not None
if not light.has_slot():
print("ERROR: Could not detach light, light was not attached!")

0
Resources/c/a Normal file
View File

0
Resources/c/b/a.txt Normal file
View File

0
Resources/c/b/b Normal file
View File

0
Resources/c/b/b.txt Normal file
View File

1054
core/InfoPanelManager.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -114,7 +114,7 @@ class EventHandler:
def mousePressEventLeft(self, evt):
"""处理鼠标左键按下事件"""
print("\n=== 开始处理鼠标左键事件 ===")
print(f"当前工具: {self.world.currentTool}")
#print(f"当前工具: {self.world.currentTool}")
if not evt:
print("事件为空")
@ -126,7 +126,7 @@ class EventHandler:
# 获取鼠标点击的位置
x = evt.get('x', 0)
y = evt.get('y', 0)
print(f"鼠标点击位置: ({x}, {y})")
#print(f"鼠标点击位置: ({x}, {y})")
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
@ -134,20 +134,20 @@ class EventHandler:
# 直接使用 x, y 创建鼠标位置
mx = 2.0 * x / float(winWidth) - 1.0
my = 1.0 - 2.0 * y / float(winHeight)
print(f"转换后的坐标: ({mx}, {my})")
#print(f"转换后的坐标: ({mx}, {my})")
# 创建射线
nearPoint = Point3()
farPoint = Point3()
self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint)
print(f"相机坐标系射线起点: {nearPoint}")
print(f"相机坐标系射线终点: {farPoint}")
#print(f"相机坐标系射线起点: {nearPoint}")
#print(f"相机坐标系射线终点: {farPoint}")
# 将相机坐标系的点转换到世界坐标系
worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint)
worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint)
print(f"世界坐标系射线起点: {worldNearPoint}")
print(f"世界坐标系射线终点: {worldFarPoint}")
#print(f"世界坐标系射线起点: {worldNearPoint}")
#print(f"世界坐标系射线终点: {worldFarPoint}")
# 进行射线检测
picker = CollisionTraverser()

View File

@ -63,7 +63,49 @@ class SelectionSystem:
"z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮
}
self._current_cursor = None
self._default_cursor = None
print("✓ 选择和变换系统初始化完成")
# ==================== 光标设置 ====================
def _setCursor(self,cursor_type):
try:
from PyQt5.QtCore import Qt
if self._current_cursor == cursor_type:
return
if hasattr(self.world,'main_window') and self.world.main_window:
main_window = self.world.main_window
else:
from PyQt5.QtWidgets import QApplication
main_window = QApplication.activeWindow()
if not main_window:
windows = QApplication.topLevelWindows()
for window in windows:
if hasattr(window,'isVisible') and window.isVisible():
main_window = window
break
if main_window:
if cursor_type == "crosshair":
main_window.setCursor(Qt.CrossCursor)
elif cursor_type == "size_hor":
main_window.setCursor(Qt.SizeHorCursor)
elif cursor_type == "size_ver":
main_window.setCursor(Qt.SizeVerCursor)
elif cursor_type == "size_all":
main_window.setCursor(Qt.SizeAllCursor)
elif cursor_type == "pointing_hand":
main_window.setCursor(Qt.PointingHandCursor)
else:
main_window.unsetCursor()
self._current_cursor = cursor_type
#print(f"光标已设置:{cursor_type}")
self._current_cursor = cursor_type
else:
print("警告:无法获取主窗口,光标设置失败")
except Exception as e:
print(f"设置光标失败{e}")
def _resetCursor(self):
self._setCursor("default")
# ==================== 选择框系统 ====================
@ -108,6 +150,26 @@ class SelectionSystem:
if not self.selectionBox or not self.selectionBoxTarget:
return
if self.selectionBoxTarget.isEmpty():
return
minPoint = Point3()
maxPoint = Point3()
try:
has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render)
if not has_bounds:
return
except:
return
# 检查边界框的有效性
if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or
abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or
abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10):
print("警告: 检测到无效的边界框,跳过选择框更新")
return
# 检查是否需要重新计算边界框
if not hasattr(self, '_bounds_cache'):
self._bounds_cache = {}
@ -337,19 +399,24 @@ class SelectionSystem:
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False
import os
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if is_scale_tool:
model_paths = [
"core/UniformScaleHandle.fbx",
os.path.join(base_dir,"core/UniformScaleHandle.fbx"),
]
elif is_rotate_tool:
model_paths = [
"core/RotationHandleQuarter.fbx",
os.path.join(base_dir,"core/RotationHandleQuarter.fbx"),
]
else:
model_paths = [
"core/TranslateArrowHandle.fbx",
os.path.join(base_dir, "core/TranslateArrowHandle.fbx"),
]
arrow_path = os.path.join(base_dir, "core/TranslateArrowHandle.fbx")
# model_paths = [
# "core/TranslateArrowHandle.fbx",
# "EG/core/TranslateArrowHandle.fbx",
@ -359,7 +426,7 @@ class SelectionSystem:
for path in model_paths:
try:
if is_rotate_tool:
gizmo_model = self.world.loader.loadModel("core/TranslateArrowHandle.fbx")
gizmo_model = self.world.loader.loadModel(arrow_path)
gizmoRot_model = self.world.loader.loadModel(path)
else:
gizmo_model = self.world.loader.loadModel(path)
@ -640,12 +707,25 @@ class SelectionSystem:
if current_time - self._last_gizmo_bounds_update > 0.2: # 每0.2秒计算一次边界框
minPoint = Point3()
maxPoint = Point3()
if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
# 计算中心点
center = Point3((minPoint.x + maxPoint.x) * 0.5,
(minPoint.y + maxPoint.y) * 0.5,
(minPoint.z + maxPoint.z) * 0.5)
self.gizmo.setPos(center)
# 添加异常处理
try:
if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
# 检查边界框的有效性
if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and
abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10):
# 计算中心点
center = Point3((minPoint.x + maxPoint.x) * 0.5,
(minPoint.y + maxPoint.y) * 0.5,
(minPoint.z + maxPoint.z) * 0.5)
self.gizmo.setPos(center)
except Exception as e:
print(f"更新Gizmo位置时出错: {e}")
# if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
# # 计算中心点
# center = Point3((minPoint.x + maxPoint.x) * 0.5,
# (minPoint.y + maxPoint.y) * 0.5,
# (minPoint.z + maxPoint.z) * 0.5)
# self.gizmo.setPos(center)
self._last_gizmo_bounds_update = current_time
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
@ -703,7 +783,7 @@ class SelectionSystem:
self.gizmo.setScale(scale_factor)
# 限制缩放范围,避免过大或过小
min_scale = 0.08
min_scale = 0.001
max_scale = 100.0
final_scale = max(min_scale, min(max_scale, scale_factor))
@ -733,6 +813,7 @@ class SelectionSystem:
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
self.gizmoStartPos = None
self._resetCursor()
# def setGizmoAxisColor(self, axis, color):
@ -889,15 +970,18 @@ class SelectionSystem:
axis_node = axis_nodes[axis]
if axis_node.isEmpty():
return
handle_node = None
handle_node = axis_node.find("x_handle") if axis == "x" else handle_node
handle_node = axis_node.find("y_handle") if axis == "y" else handle_node
handle_node = axis_node.find("z_handle") if axis == "z" else handle_node
#如果找不到特定名称的节点,尝试查找任何子节点
if not handle_node:
# 如果找不到特定名称的节点,尝试查找任何子节点
if not handle_node or handle_node.isEmpty():
children = axis_node.getChildren()
if children.getNumPath()>0:
if children.getNumPaths() > 0:
handle_node = children[0]
if not handle_node:
@ -907,15 +991,6 @@ class SelectionSystem:
# 创建或获取材质
mat = Material()
# # 设置材质属性 - 使用自发光确保在RenderPipeline下可见
# mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
# mat.setDiffuse(Vec4(0, 0, 0, 1))
# #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光
# mat.setEmission(Vec4(1,1,1,1.0)) # 自发光
# mat.set_roughness(1)
# 设置材质属性 - 使用更自然的颜色,避免过亮的自发光
# 将颜色值控制在合理范围内
adjusted_color = Vec4(
min(color[0], 1.0),
min(color[1], 1.0),
@ -924,16 +999,11 @@ class SelectionSystem:
)
mat.setBaseColor(adjusted_color)
#mat.setDiffuse(adjusted_color * 1) # 稍微降低漫反射亮度
#mat.setAmbient(adjusted_color * 0.4) # 设置环境光反射
# mat.setSpecular(Vec4(0.4, 0.4, 0.4, 1.0)) # 适度的镜面反射
# mat.setShininess(1.0) # 适中的高光强度
mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
# 应用材质
handle_node.setMaterial(mat, 1)
# 设置透明度
if color[3] < 1.0:
handle_node.setTransparency(TransparencyAttrib.MAlpha)
@ -1336,6 +1406,7 @@ class SelectionSystem:
def updateGizmoHighlight(self, mouseX, mouseY):
"""更新坐标轴高亮状态"""
if not self.gizmo or self.isDraggingGizmo:
self._resetCursor()
return
# 使用碰撞检测方法
@ -1357,15 +1428,19 @@ class SelectionSystem:
# 高亮新的轴
if hoveredAxis:
self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis])
self._setCursor("pointing_hand")
else:
# 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色
for axis_name in ["x", "y", "z"]:
if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
self._resetCursor()
self.gizmoHighlightAxis = hoveredAxis
self._last_detected_axis = hoveredAxis
elif hoveredAxis is None:
self._resetCursor()
def _detectHoveredAxis(self, mouseX, mouseY):
"""检测鼠标悬停的轴 - 提取为独立方法"""
@ -1445,6 +1520,13 @@ class SelectionSystem:
# self.dragGizmoAxis = axis
#
# self.gizmoHighlightAxis = self.dragGizmoAxis
# 设置拖拽光标
if self.dragGizmoAxis == "x":
self._setCursor("size_all") # 水平调整光标
elif self.dragGizmoAxis == "y":
self._setCursor("size_all") # 垂直调整光标
elif self.dragGizmoAxis == "z":
self._setCursor("size_all") # 全向调整光标
print(
f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})")
@ -1487,6 +1569,9 @@ class SelectionSystem:
if is_scale_tool:
scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01
scale_factor = max(0.001, scale_factor)
start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1))
if is_gui_element:
@ -1514,6 +1599,12 @@ class SelectionSystem:
start_scale.y * scale_factor,
start_scale.z * scale_factor)
new_scale = Vec3(
max(0.001,new_scale.x),
max(0.001,new_scale.y),
max(0.001,new_scale.z)
)
# 应用新缩放值
self.gizmoTarget.setScale(new_scale)
# 安全地更新属性面板
@ -1528,7 +1619,7 @@ class SelectionSystem:
if self.dragGizmoAxis == "x":
new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z)
elif self.dragGizmoAxis == "y":
new_hpr = Vec3(start_hpr.x,start_hpr.y+rotation_amount,start_hpr.z)
new_hpr = Vec3(start_hpr.x,start_hpr.y-rotation_amount,start_hpr.z)
elif self.dragGizmoAxis == "z":
new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount)
else:
@ -1776,6 +1867,7 @@ class SelectionSystem:
# 重置高亮轴
self.gizmoHighlightAxis = None
self._resetCursor()
# ==================== 选择管理 ====================
def updateSelection(self, nodePath):

View File

@ -13,6 +13,15 @@ class TerrainManager:
self.terrains = []
# core/terrain_manager.py
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)):
"""从高度图创建地形"""
@ -381,9 +390,6 @@ class TerrainManager:
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}")
print(f"✓ 重新设置了地形碰撞体")
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
return modified

View File

@ -33,38 +33,71 @@ class CoreWorld(Panda3DWorld):
self.mouseRightPressed = False
# 初始化世界
self._setupResourcePaths()
self._setupCamera()
self._setupLighting()
self._setupGround()
self._loadFont()
#self.load_and_play_glb_model()
def load_and_play_glb_model(self):
"""加载 glTF 模型并播放动画"""
def _setupResourcePaths(self):
"""设置Panda3D资源搜索路径确保能正确找到Resources文件夹中的模型和贴图"""
try:
from direct.actor.Actor import Actor
# 使用 Actor 类加载 glTF 模型
self.model = Actor("/home/tiger/cube.glb")
print("模型加载成功!")
self.model.reparentTo(self.render)
self.model.setPos(0, 10, 0)
self.model.setScale(10)
# 找出所有 AnimBundleNode
print(f"开始寻找动画AnimationBundleNode...")
for np in self.model.findAllMatches("**/+AnimBundleNode"):
print(f"找到AnimBundleNode: {np.getName()}")
bundle = np.node().getBundle()
for i in range(bundle.getNumAnimations()):
anim_name = bundle.getAnimation(i).getName()
print("动画名:", anim_name)
# 这里不能直接 play需要手动把 AnimControl 绑定到节点
import os
from panda3d.core import getModelPath, DSearchPath, Filename
# 获取项目根目录
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
resources_dir = os.path.join(project_root, "Resources")
# 确保Resources目录存在
if not os.path.exists(resources_dir):
os.makedirs(resources_dir, exist_ok=True)
print(f"✓ 创建Resources目录: {resources_dir}")
# 添加Resources目录到Panda3D模型搜索路径
model_path = getModelPath()
resources_filename = Filename.from_os_specific(resources_dir)
# 检查路径是否已存在,避免重复添加
if not model_path.findFile(resources_filename):
model_path.appendDirectory(resources_filename)
print(f"✓ 添加Resources到模型搜索路径: {resources_dir}")
# 同时添加各个子目录到搜索路径
subdirs = ['models', 'textures', 'animations', 'icons', 'materials']
for subdir in subdirs:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not model_path.findFile(subdir_filename):
model_path.appendDirectory(subdir_filename)
print(f"✓ 添加子目录到搜索路径: {subdir}")
else:
# 创建不存在的子目录
os.makedirs(subdir_path, exist_ok=True)
subdir_filename = Filename.from_os_specific(subdir_path)
model_path.appendDirectory(subdir_filename)
print(f"✓ 创建并添加子目录: {subdir}")
# 设置纹理搜索路径
from panda3d.core import getTexturePath
texture_path = getTexturePath()
if not texture_path.findFile(resources_filename):
texture_path.appendDirectory(resources_filename)
for subdir in ['textures', 'materials', 'icons']:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not texture_path.findFile(subdir_filename):
texture_path.appendDirectory(subdir_filename)
print(f"✓ 资源路径设置完成")
print(f" 项目根目录: {project_root}")
print(f" Resources目录: {resources_dir}")
except Exception as e:
print(f"模型加载失败: {e}")
return None
print(f"⚠️ 设置资源路径失败: {e}")
def diagnose_fbx_loading(self, fbx_path):
"""诊断FBX加载状态"""
@ -399,14 +432,14 @@ class CoreWorld(Panda3DWorld):
def mousePressEventRight(self, evt):
"""处理鼠标右键按下事件"""
print("右键按下")
#print("右键按下")
self.mouseRightPressed = True
self.lastMouseX = evt['x']
self.lastMouseY = evt['y']
def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件"""
print("右键释放")
#print("右键释放")
self.mouseRightPressed = False
def mouseMoveEvent(self, evt):

File diff suppressed because it is too large Load Diff

26
main.py
View File

@ -1,5 +1,6 @@
import warnings
from core.InfoPanelManager import InfoPanelManager
from demo.video_integration import VideoManager
warnings.filterwarnings("ignore", category=DeprecationWarning)
@ -39,7 +40,7 @@ from direct.task import Task
from direct.task.TaskManagerGlobal import taskMgr
from direct.showbase.ShowBase import ShowBase
from direct.showbase.DirectObject import DirectObject
from direct.showbase.ShowBaseGlobal import globalClock
from direct.showbase.ShowBaseGlobal import globalClock, aspect2d
import os
import json
import datetime
@ -97,6 +98,8 @@ class MyWorld(CoreWorld):
self.terrain_edit_strength=0.3
self.terrain_edit_operation = "add"
self.info_panel_manager = InfoPanelManager(self)
# 初始化碰撞管理器
from core.collision_manager import CollisionManager
self.collision_manager = CollisionManager(self)
@ -234,6 +237,27 @@ class MyWorld(CoreWorld):
"""创建2D GUI图片"""
return self.gui_manager.createGUI2DImage(pos, image_path, size)
def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
"""创建视频屏幕"""
return self.gui_manager.createVideoScreen(pos,size,video_path)
def create2DVideoScreen(self,pos=(0,0,0),size=0.2,video_path=None):
"""创建2D视频屏幕"""
return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path)
def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None):
"""创建360度视频"""
return self.gui_manager.createSphericalVideo(pos,radius,video_path)
def playSphericalVideo(self,spherical_video_node):
return self.gui_manager.playSphericalVideo(spherical_video_node)
def pauseSphericalVideo(self,spherical_video_node):
return self.gui_manager.pauseSphericalVideo(spherical_video_node)
def setSphericalVideoTime(self,spherical_video_node,time_seconds):
return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds)
def createSpotLight(self,pos=(0,0,5)):
"""创建聚光灯"""
return self.scene_manager.createSpotLight(pos)

View File

@ -87,10 +87,10 @@ class SceneManager:
print("✓ 场景管理系统初始化完成")
# ==================== 模型导入和处理 ====================
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
"""导入模型到场景 - 只在根节点下创建
"""导入模型到场景
Args:
filepath: 模型文件路径
apply_unit_conversion: 是否应用单位转换主要针对FBX文件
@ -98,16 +98,14 @@ class SceneManager:
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
"""
try:
print(f"\n💾 开始导入模型: {os.path.basename(filepath)}")
print(f"\n=== 开始导入模型: {filepath} ===")
print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}")
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
# 预处理文件路径和转换
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
# 检查是否需要转换为GLB
# 检查是否需要转换为GLB以获得更好的动画支持
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式尝试转换为GLB...")
converted_path = self._convertToGLBWithProgress(filepath)
@ -118,268 +116,206 @@ class SceneManager:
try:
from PyQt5.QtWidgets import QMessageBox
original_ext = os.path.splitext(original_filepath)[1].upper()
QMessageBox.information(None, "转换成功",
f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!")
QMessageBox.information(None, "转换成功",
f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!")
except:
pass
else:
print(f"⚠️ 转换失败,使用原始文件")
# 直接在render根节点下创建模型
print("--- 在根节点下创建模型实例 ---")
# 加载模型
# 总是重新加载模型以确保材质信息完整
# 不使用ModelPool缓存避免材质信息丢失问题
print("直接从文件加载模型...")
model = self.world.loader.loadModel(filepath)
if not model:
print("加载模型失败")
print("加载模型失败")
return None
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
# 将模型挂载到render根节点
# 将模型添加到场景
model.reparentTo(self.world.render)
# 设置标签和路径信息
# 保存原始路径和转换后的路径
model.setTag("model_path", filepath)
model.setTag("original_path", original_filepath)
model.setTag("file", model.getName())
model.setTag("is_model_root", "1")
model.setTag("is_scene_element", "1")
model.setTag("created_by_user", "1")
if filepath != original_filepath:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
# 应用处理选项
# 可选的单位转换主要针对FBX
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换厘米到米...")
self._applyUnitConversion(model, 0.01)
model.setTag("unit_conversion_applied", "true")
# 智能缩放标准化处理FBX子节点的大缩放值
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
self._normalizeModelScales(model)
model.setTag("scale_normalization_applied", "true")
# 调整模型位置到地面
self._adjustModelToGround(model)
# 创建并设置基础材质
print("设置材质...")
print("\n=== 开始设置材质 ===")
self._applyMaterialsToModel(model)
# 设置碰撞检测(重要!用于选择功能)
print("设置碰撞检测...")
print("\n=== 设置碰撞检测 ===")
self.setupCollision(model)
# 添加文件标签用于保存/加载
model.setTag("file", model_name)
model.setTag("is_model_root", "1")
# 记录应用的处理选项
if apply_unit_conversion:
model.setTag("unit_conversion_applied", "true")
if normalize_scales:
model.setTag("scale_normalization_applied", "true")
# 添加到模型列表
self.models.append(model)
print(f"✅ 创建模型成功: {model.getName()}")
# 更新场景树
self.updateSceneTree()
# 获取树形控件并添加到Qt树中
tree_widget = self._get_tree_widget()
if tree_widget:
# 找到根节点项
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
root_item = item
break
if root_item:
qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE")
if qt_item:
tree_widget.setCurrentItem(qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(model, qt_item)
print("✅ Qt树节点添加成功")
else:
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
else:
print("⚠️ 未找到根节点项无法添加到Qt树")
else:
print("⚠️ 无法访问树形控件")
print(f"🎉 模型导入完成")
print(f"=== 模型导入成功: {model_name} ===\n")
return model
except Exception as e:
print(f"❌ 导入模型过程失败: {str(e)}")
import traceback
traceback.print_exc()
print(f"导入模型失败: {str(e)}")
return None
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
# """导入带动画的FBX模型使用assimp
#
# Args:
# filepath: FBX文件路径
# scale: 缩放比例默认0.01,从厘米转换到米)
# auto_play: 是否自动播放第一个动画
#
# Returns:
# Actor对象如果加载失败返回None
# """
# try:
# print(f"\n=== 导入动画FBX模型: {filepath} ===")
# filepath = util.normalize_model_path(filepath)
#
# # 使用动画管理器加载FBX
# actor = self.animation_manager.load_fbx_with_animations(filepath, scale)
#
# if actor:
# # 设置模型名称
# model_name = os.path.basename(filepath)
# actor.setName(model_name)
#
# # 调整模型位置到地面
# self._adjustModelToGround(actor)
#
# # 设置碰撞检测
# self.setupCollision(actor)
#
# # 添加文件标签
# actor.setTag("file", model_name)
# actor.setTag("is_animated_model", "1")
#
# # 添加到模型列表
# self.models.append(actor)
#
# # 自动播放第一个动画
# if auto_play:
# available_anims = self.animation_manager.get_available_animations(actor)
# if available_anims:
# first_anim = available_anims[0]
# self.animation_manager.play_animation(actor, first_anim, loop=True)
# print(f"🎬 自动播放动画: {first_anim}")
#
# # 更新场景树
# self.updateSceneTree()
#
# print(f"=== 动画FBX模型导入成功: {model_name} ===\n")
# return actor
# else:
# print("❌ 动画FBX模型导入失败")
# return None
#
# except Exception as e:
# print(f"导入动画FBX模型失败: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
def apply_material(node_path, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {node_path.getName()}")
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
try:
print(f"{indent}处理节点: {node_path.getName()}")
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
if isinstance(node_path.node(), GeomNode):
print(f"{indent}发现GeomNode处理材质")
geom_node = node_path.node()
if isinstance(node_path.node(), GeomNode):
print(f"{indent}发现GeomNode处理材质")
geom_node = node_path.node()
# 检查所有几何体的状态
has_color = False
color = None
# 检查所有几何体的状态
has_color = False
color = None
# 首先检查节点自身的状态
node_state = node_path.getState()
if node_state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
node_material = mat_attrib.getMaterial()
if node_material and node_material.hasDiffuse():
color = node_material.getDiffuse()
has_color = True
print(f"{indent}从节点材质获取颜色: {color}")
# 首先检查节点自身的状态
node_state = node_path.getState()
if node_state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
node_material = mat_attrib.getMaterial()
if node_material and node_material.hasDiffuse():
color = node_material.getDiffuse()
has_color = True
print(f"{indent}从节点材质获取颜色: {color}")
# 检查FBX特有的属性
for tag_key in node_path.getTagKeys():
print(f"{indent}发现标签: {tag_key}")
if "color" in tag_key.lower() or "diffuse" in tag_key.lower():
tag_value = node_path.getTag(tag_key)
print(f"{indent}颜色相关标签: {tag_key} = {tag_value}")
# 检查FBX特有的属性
for tag_key in node_path.getTagKeys():
print(f"{indent}发现标签: {tag_key}")
if "color" in tag_key.lower() or "diffuse" in tag_key.lower():
tag_value = node_path.getTag(tag_key)
print(f"{indent}颜色相关标签: {tag_key} = {tag_value}")
# 如果还没找到颜色,检查几何体
if not has_color:
for i in range(geom_node.getNumGeoms()):
geom = geom_node.getGeom(i)
state = geom_node.getGeomState(i)
# 如果还没找到颜色,检查几何体
if not has_color:
for i in range(geom_node.getNumGeoms()):
try:
geom = geom_node.getGeom(i)
state = geom_node.getGeomState(i)
# 检查顶点颜色
vdata = geom.getVertexData()
format = vdata.getFormat()
for j in range(format.getNumColumns()):
column = format.getColumn(j)
# InternalName对象需要使用getName()转换为字符串
column_name = column.getName().getName()
if "color" in column_name.lower():
print(f"{indent}发现顶点颜色数据: {column_name}")
# 这里可以读取顶点颜色,但先记录发现
# 检查顶点颜色
vdata = geom.getVertexData()
if vdata:
format = vdata.getFormat()
if format:
for j in range(format.getNumColumns()):
try:
column = format.getColumn(j)
# InternalName对象需要使用getName()转换为字符串
column_name = column.getName().getName()
if "color" in column_name.lower():
print(f"{indent}发现顶点颜色数据: {column_name}")
except Exception:
continue
# 检查材质属性
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
orig_material = mat_attrib.getMaterial()
if orig_material:
if orig_material.hasBaseColor():
color = orig_material.getBaseColor()
has_color = True
print(f"{indent}从基础颜色获取: {color}")
break
elif orig_material.hasDiffuse():
color = orig_material.getDiffuse()
has_color = True
print(f"{indent}从漫反射颜色获取: {color}")
break
# 检查材质属性
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
orig_material = mat_attrib.getMaterial()
if orig_material:
if orig_material.hasBaseColor():
color = orig_material.getBaseColor()
has_color = True
print(f"{indent}从基础颜色获取: {color}")
break
elif orig_material.hasDiffuse():
color = orig_material.getDiffuse()
has_color = True
print(f"{indent}从漫反射颜色获取: {color}")
break
# 检查颜色属性
if not has_color and state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
color = color_attrib.getColor()
has_color = True
print(f"{indent}从颜色属性获取: {color}")
break
# 检查颜色属性
if not has_color and state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
color = color_attrib.getColor()
has_color = True
print(f"{indent}从颜色属性获取: {color}")
break
except Exception as geom_error:
print(f"{indent}处理几何体 {i} 时出错: {geom_error}")
continue
# 创建新材质
material = Material()
if has_color:
print(f"{indent}应用找到的颜色: {color}")
material.setDiffuse(color)
material.setBaseColor(color) # 同时设置基础颜色
node_path.setColor(color)
else:
print(f"{indent}使用默认颜色")
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
# 创建新材质
material = Material()
if has_color:
print(f"{indent}应用找到的颜色: {color}")
try:
material.setDiffuse(color)
material.setBaseColor(color) # 同时设置基础颜色
node_path.setColor(color)
except Exception as color_error:
print(f"{indent}设置颜色时出错: {color_error}")
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
else:
print(f"{indent}使用默认颜色")
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
# 设置其他材质属性
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
#material.set_metallic(1)
#material.set_roughness(0)
# 设置其他材质属性
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
# 应用材质
node_path.setMaterial(material)
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
# 应用材质
node_path.setMaterial(material)
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
except Exception as node_error:
print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}")
# 递归处理子节点
child_count = node_path.getNumChildren()
print(f"{indent}子节点数量: {child_count}")
for i in range(child_count):
child = node_path.getChild(i)
apply_material(child, depth + 1)
try:
child = node_path.getChild(i)
apply_material(child, depth + 1)
except Exception as child_error:
print(f"{indent}处理子节点 {i} 时出错: {child_error}")
continue
# 应用材质
print("\n开始递归应用材质...")
apply_material(model)
try:
apply_material(model)
except Exception as e:
print(f"应用材质时出错: {e}")
print("=== 材质设置完成 ===\n")
def _adjustModelToGround(self, model):
@ -429,6 +365,8 @@ class SceneManager:
print("模型已应用过单位转换,跳过")
return
# 获取当前边界用于后续位置调整
original_bounds = model.getBounds()
@ -734,7 +672,7 @@ class SceneManager:
# 根据调试设置决定是否显示碰撞体
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
cNodePath.show()
cNodePath.hide()
else:
cNodePath.hide()
@ -768,6 +706,27 @@ class SceneManager:
try:
print(f"\n=== 开始保存场景到: {filename} ===")
# 存储需要临时隐藏的节点,以便保存后恢复
nodes_to_restore = []
# 查找并隐藏所有坐标轴和选择框节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
# 隐藏坐标轴节点
for node in gizmo_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True)) # (节点, 原先是否可见)
node.hide()
print(f"临时隐藏坐标轴节点: {node.getName()}")
# 隐藏选择框节点
for node in selection_box_nodes:
if not node.isHidden():
nodes_to_restore.append((node, True))
node.hide()
print(f"临时隐藏选择框节点: {node.getName()}")
# 遍历所有模型,保存材质状态和变换信息
for model in self.models:
# 保存变换信息(关键!)
@ -802,12 +761,34 @@ class SceneManager:
if not color_attrib.isOff():
model.setTag("color", str(color_attrib.getColor()))
# 保存场景
success = self.world.render.writeBamFile(filename)
return success
try:
print("--- 打印当前场景图 (render) ---")
self.world.render.ls()
print("---------------------------------")
# 保存场景
success = self.world.render.writeBamFile(filename)
if success:
print(f"✓ 场景保存成功: {filename}")
else:
print("✗ 场景保存失败")
return success
finally:
# 恢复之前隐藏的节点
for item in nodes_to_restore:
node, was_visible = item
if was_visible and not node.isEmpty():
node.show()
print(f"恢复显示节点: {node.getName()}")
if nodes_to_restore:
print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示")
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def loadScene(self, filename):
@ -821,6 +802,9 @@ class SceneManager:
model.removeNode()
self.models.clear()
# 清理可能存在的辅助节点(坐标轴、选择框等)
self._cleanupAuxiliaryNodes()
# 加载场景
scene = self.world.loader.loadModel(filename)
if not scene:
@ -846,6 +830,16 @@ class SceneManager:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
# 跳过辅助节点(坐标轴和选择框)
if nodePath.getName().startswith(("gizmo", "selectionBox")):
print(f"{indent}跳过辅助节点: {nodePath.getName()}")
return
if nodePath.getName() in ['SceneRoot'] or \
any(keyword in nodePath.getName() for keyword in ["Skybox","skybox"]):
print(f"{indent}跳过环境节点:{nodePath.getName()}")
return
if isinstance(nodePath.node(), ModelRoot):
print(f"{indent}找到模型根节点!")
@ -940,8 +934,36 @@ class SceneManager:
except Exception as e:
print(f"加载场景时发生错误: {str(e)}")
import traceback
traceback.print_exc()
return False
def _cleanupAuxiliaryNodes(self):
"""清理场景中可能存在的辅助节点"""
try:
# 查找并移除所有坐标轴节点
gizmo_nodes = self.world.render.findAllMatches("**/gizmo*")
for node in gizmo_nodes:
if not node.isEmpty():
node.removeNode()
print(f"清理坐标轴节点: {node.getName()}")
# 查找并移除所有选择框节点
selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*")
for node in selection_box_nodes:
if not node.isEmpty():
node.removeNode()
print(f"清理选择框节点: {node.getName()}")
# 停止相关的更新任务
from direct.task.TaskManagerGlobal import taskMgr
taskMgr.remove("updateGizmo")
taskMgr.remove("updateSelectionBox")
print("辅助节点清理完成")
except Exception as e:
print(f"清理辅助节点时出错: {e}")
# ==================== 模型管理 ====================
def deleteModel(self, model):
@ -1052,10 +1074,7 @@ class SceneManager:
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
light.setPos(*pos)
# 添加到渲染管线
render_pipeline.add_light(light)
@ -1084,6 +1103,8 @@ class SceneManager:
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}")
import traceback
traceback.print_exc()
continue
# 处理创建结果
@ -1152,9 +1173,7 @@ class SceneManager:
# 创建点光源对象
light = PointLight()
# 设置光源的世界坐标位置
world_pos = light_np.getPos(self.world.render)
light.setPos(world_pos)
light.setPos(*pos)
light.energy = 5000
light.radius = 1000

4
terminal Normal file
View File

@ -0,0 +1,4 @@
# 检查当前分支和状态
git status
git branch -v
git log --oneline --graph --all -10

View File

@ -1,3 +1,4 @@
from PIL.ImageChops import lighter
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu, QStyle
from PyQt5.QtCore import Qt
from PyQt5.sip import delete
@ -79,19 +80,113 @@ class InterfaceManager:
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))
#灯光节点添加特殊处理
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:
@ -137,9 +232,14 @@ class InterfaceManager:
def deleteNode(self, nodePath, item):
"""删除节点"""
try:
item_data = item.data(0,Qt.UserRole+1)
# 如果是灯光节点,直接调用灯光删除方法
if self.isLightNode(nodePath):
self.deleteLightNode(nodePath, item)
return
item_data = item.data(0, Qt.UserRole + 1)
if item_data == "terrain":
if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains:
if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains:
terrain_to_remove = None
for terrain_info in self.world.terrain_manager.terrains:
if terrain_info['node'] == nodePath:
@ -153,34 +253,38 @@ class InterfaceManager:
self.world.selection.updateSelection(None)
return
# 先递归删除所有子节点中的灯光
self._recursiveRemoveLights(nodePath)
# 从场景中移除
self.world.property_panel.removeActorForModel(nodePath)
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)
if hasattr(self.world,'selection'):
# 清除选择状态
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
if nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
if nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
# 强制删除节点及其所有子节点
if not nodePath.isEmpty():
# 先递归删除所有子节点
children = list(nodePath.getChildren())
for child in children:
try:
if not child.isEmpty():
child.removeNode()
except Exception as e:
print(f"删除子节点失败: {str(e)}")
nodePath.removeNode()
# 再删除节点本身
nodePath.removeNode()
if hasattr(self.world,'selection'):
if hasattr(self.world, 'selection'):
self.world.selection.checkAndClearIfTargetDeleted()
# 如果是模型根节点,从模型列表中移除
#if item.parent().text(0) == "模型":
if nodePath in self.world.models:
if hasattr(self.world, 'models') and nodePath in self.world.models:
self.world.models.remove(nodePath)
# 从树形控件中移除
@ -196,6 +300,51 @@ class InterfaceManager:
except Exception as e:
print(f"删除节点失败: {str(e)}")
import traceback
traceback.print_exc()
def _cleanupAllLightsInSubtree(self,parentNode):
try:
if parentNode.isEmpty():
return
#收集所有子节点
all_nodes = []
def collect_all_nodes(node):
if not node.isEmpty():
all_nodes.append(node)
for child in node.getChildren():
collect_all_nodes(child)
collect_all_nodes(parentNode)
ligths_processed = 0
for node in all_nodes:
if self.isLightNode(node):
if hasattr(node,'getPythonTag'):
light_object = 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"✓ 从渲染管线移除灯光: {node.getName()}")
except Exception as e:
print(f"⚠ 从渲染管线移除灯光失败: {str(e)}")
#从灯光列表中移除
try:
if node in self.world.Spotlight:
self.world.Spotlight.remove(node)
print(f"从Spotlight列表移除{node.getName()}")
if node in self.world.Pointlight:
self.world.Pointlight.remove(node)
print(f"从pointlight列表移除{node.getName()}")
except Exception as e:
print(f"从灯列表移除失败{str(e)}")
ligths_processed += 1
if ligths_processed>0:
print(f"清理{ligths_processed}个灯光节点")
except Exception as e:
print(f"清理节点树中的灯光失败{str(e)}")
def updateSceneTree(self):
"""更新场景树显示 - 实际实现"""
@ -282,9 +431,6 @@ class InterfaceManager:
terrain_item.setData(0,Qt.UserRole+1,"terrain") # 标记为地形节点
addNodeToTree(terrain_node,terrain_item,force=True)
# 展开所有节点
#self.treeWidget.expandAll()
self._restore_expanded()

View File

@ -17,6 +17,8 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog,
QSpinBox, QFrame)
from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint
from direct.showbase.ShowBaseGlobal import aspect2d
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget
class MainWindow(QMainWindow):
@ -41,6 +43,11 @@ class MainWindow(QMainWindow):
self.updateTimer.timeout.connect(self.updateScriptPanel)
self.updateTimer.start(500) # 每500毫秒更新一次
self.toolbarDragging = False
self.dragStartPos = QPoint(0, 0)
self.toolbarStartPos = QPoint(0, 0)
def setupCenterWidget(self):
"""设置窗口基本属性"""
self.setWindowTitle("引擎编辑器")
@ -178,21 +185,27 @@ class MainWindow(QMainWindow):
def toolbarMouseMoveEvent(self, event):
"""工具栏鼠标移动事件"""
if self.toolbarDragging and event.buttons() == Qt.LeftButton:
# 计算新位置
delta = event.globalPos() - self.dragStartPos
new_pos = self.toolbarStartPos + delta
try:
if self.toolbarDragging and event.buttons() == Qt.LeftButton:
# 计算新位置
delta = event.globalPos() - self.dragStartPos
new_pos = self.toolbarStartPos + delta
# 边界检测
panda_rect = self.pandaWidget.geometry()
toolbar_size = self.embeddedToolbar.size()
# 边界检测
panda_rect = self.pandaWidget.geometry()
toolbar_size = self.embeddedToolbar.size()
# 限制在Panda3D区域内
new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width())))
new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height())))
# 限制在Panda3D区域内
new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width())))
new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height())))
self.embeddedToolbar.move(new_pos)
event.accept()
except Exception as e:
print(f"工具栏鼠标移动事件出错")
import traceback
traceback.print_exc()
self.embeddedToolbar.move(new_pos)
event.accept()
def toolbarMouseReleaseEvent(self, event):
"""工具栏鼠标释放事件"""
@ -303,26 +316,26 @@ class MainWindow(QMainWindow):
self.createMenu = menubar.addMenu('创建')
self.setupCreateMenuActions() # 统一创建菜单动作
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
self.createGUIaddMenu.addSeparator()
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图')
self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图')
self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图')
self.createLightaddMenu = self.createMenu.addMenu('光源')
self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
# self.createGUIaddMenu = self.createMenu.addMenu('GUI')
# self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
# self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
# self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
# self.createGUIaddMenu.addSeparator()
# self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
# self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图')
# self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图')
# self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图')
#
# self.createLightaddMenu = self.createMenu.addMenu('光源')
# self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
# self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
#添加地形菜单
self.createTerrainMenu = self.createMenu.addMenu('地形')
self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形')
self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形')
self.createTerrainMenu.addSeparator()
self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式')
# self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式')
# GUI菜单
# GUI菜单 - 复用创建菜单的动作
@ -366,11 +379,42 @@ class MainWindow(QMainWindow):
self.addModelToCesiumAction = self.cesiumMenu.addAction('添加模型到地球')
self.addModelToCesiumAction.triggered.connect(self.onAddModelClicked)
self.infoPanelMenu = menubar.addMenu('信息面板')
# 创建示例面板动作
self.createSamplePanelAction = self.infoPanelMenu.addAction('创建示例面板')
self.createSamplePanelAction.triggered.connect(self.onCreateSampleInfoPanel)
# 添加更多面板创建选项
self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板')
self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel)
self.createSensorDataPanelAction = self.infoPanelMenu.addAction('创建传感器数据面板')
self.createSensorDataPanelAction.triggered.connect(self.onCreateSensorDataPanel)
self.createSceneInfoPanelAction = self.infoPanelMenu.addAction('创建场景信息面板')
self.createSceneInfoPanelAction.triggered.connect(self.onCreateSceneInfoPanel)
# 添加分隔符和批量创建选项
self.infoPanelMenu.addSeparator()
self.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板')
self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels)
#资源菜单
self.assetsMenu = menubar.addMenu('资源')
self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源')
self.refreshAssetsAction.triggered.connect(self.refreshAssetsView)
# 帮助菜单
self.helpMenu = menubar.addMenu('帮助')
self.aboutAction = self.helpMenu.addAction('关于')
def refreshAssetsView(self):
""""刷新资源视图"""
if hasattr(self,'fileView') and self.fileView:
self.fileView.refreshView()
print("资源视图已刷新")
def setupCreateMenuActions(self):
"""统一设置创建菜单的所有动作 - 避免重复代码"""
# 基础对象
@ -383,14 +427,17 @@ class MainWindow(QMainWindow):
# 3D GUI子菜单
self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
self.create3DImageAction = self.create3dGUIaddMenu.addAction('3D图片')
# GUI子菜单
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
self.createImageAction = self.createGUIaddMenu.addAction('创建图片')
self.createGUIaddMenu.addSeparator()
self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕')
self.create2DVideoScreen = self.createGUIaddMenu.addAction('创建2D视频屏幕')
self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频')
self.createGUIaddMenu.addSeparator()
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
@ -408,12 +455,15 @@ class MainWindow(QMainWindow):
# 连接到world对象的创建方法
# self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject)
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
self.create3DImageAction.triggered.connect(lambda: self.world.createGUI3DImage())
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
# self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
# self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
# self.createVirtualScreenAction.triggered.connect(self.world.createVirtualScreen)
self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage())
self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen)
self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
@ -430,10 +480,13 @@ class MainWindow(QMainWindow):
return {
'createEmpty': self.createEnptyaddAction,
'create3DText': self.create3DTextAction,
'create3DImage': self.create3DImageAction,
'createButton': self.createButtonAction,
'createLabel': self.createLabelAction,
'createEntry': self.createEntryAction,
'createImage': self.createImageAction,
'createVideoScreen': self.createVideoScreen,
'create2DVideoScreen':self.create2DVideoScreen,
'createSphericalVideo': self.createSphericalVideo,
'createVirtualScreen': self.createVirtualScreenAction,
'createSpotLight': self.createSpotLightAction,
@ -553,10 +606,10 @@ class MainWindow(QMainWindow):
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
# 创建底部停靠控制台
self.consoleDock = QDockWidget("控制台", self)
self.consoleView = CustomConsoleDockWidget(self.world)
self.consoleDock.setWidget(self.consoleView)
self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
# self.consoleDock = QDockWidget("控制台", self)
# self.consoleView = CustomConsoleDockWidget(self.world)
# self.consoleDock.setWidget(self.consoleView)
# self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
def setupToolbar(self):
"""创建工具栏"""
@ -644,11 +697,6 @@ class MainWindow(QMainWindow):
self.createHeightmapTerrainTool.setText("高度图地形")
self.toolbar.addWidget(self.createHeightmapTerrainTool)
self.terrainEditTool = QToolButton()
self.terrainEditTool.setText("地形编辑")
self.terrainEditTool.setCheckable(True)
self.toolbar.addWidget(self.terrainEditTool)
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
@ -785,13 +833,13 @@ class MainWindow(QMainWindow):
self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
# 连接创建事件 - 使用菜单动作而不是不存在的工具栏按钮
self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight())
self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight())
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
# self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight())
# self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight())
# self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
# self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
# self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
# self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
# self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
@ -799,7 +847,7 @@ class MainWindow(QMainWindow):
# 连接地形创建事件
self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain)
self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain)
self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode)
# self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode)
# 连接树节点点击信号
self.treeWidget.itemSelectionChanged.connect(
@ -1045,6 +1093,396 @@ class MainWindow(QMainWindow):
self.world.setCurrentTool(None)
print("工具栏: 取消选择工具")
# 在 MainWindow 类中添加以下方法
def onCreateSampleInfoPanel(self):
"""创建示例天气信息面板(模拟数据)"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
# 使用唯一的面板ID
import time
unique_id = f"weather_info_{int(time.time())}"
# 创建示例面板
weather_panel = info_manager.createInfoPanel(
panel_id=unique_id, # 使用唯一ID
position=(0, 0),
size=(1, 1),
bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景
border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框
title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 更新面板标题
info_manager.updatePanelContent(unique_id, title="北京天气")
# 添加到场景树
self.addInfoPanelToTree(weather_panel, "天气信息面板")
# 立即显示加载中信息
info_manager.updatePanelContent(unique_id, content="正在获取天气数据...")
info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) # 每10分钟更新一次
# # 立即显示示例数据
# sample_data = self.getSampleWeatherData()
# info_manager.updatePanelContent(unique_id, content=sample_data)
#
# # 注册数据源,定期更新示例数据
# info_manager.registerDataSource(unique_id, self.getSampleWeatherData, update_interval=2.0)
print("✓ 示例天气信息面板已创建")
except Exception as e:
print(f"✗ 创建示例天气信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}")
def getRealWeatherData(self):
"""获取真实天气数据"""
try:
import requests
import json
from datetime import datetime
# 请求天气数据
url = "https://wttr.in/Beijing?format=j1"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON数据
weather_data = response.json()
# 提取当前天气信息
current_condition = weather_data['current_condition'][0]
weather_desc = current_condition['weatherDesc'][0]['value']
temp_c = current_condition['temp_C']
feels_like = current_condition['FeelsLikeC']
humidity = current_condition['humidity']
pressure = current_condition['pressure']
visibility = current_condition['visibility']
wind_speed = current_condition['windspeedKmph']
wind_dir = current_condition['winddir16Point']
# 提取空气质量(如果可用)
air_quality = "N/A"
if 'air_quality' in weather_data and weather_data['air_quality']:
if 'us-epa-index' in current_condition:
air_quality_index = current_condition['air_quality_index']
air_quality = f"指数: {air_quality_index}"
# 获取更新时间
update_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 格式化显示内容
content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}"
return content
except requests.exceptions.Timeout:
return "错误: 获取天气数据超时"
except requests.exceptions.ConnectionError:
return "错误: 网络连接失败"
except requests.exceptions.HTTPError as e:
return f"HTTP错误: {e}"
except json.JSONDecodeError:
return "错误: 无法解析天气数据"
except KeyError as e:
return f"错误: 天气数据格式不正确 (缺少字段: {e})"
except Exception as e:
return f"获取天气数据失败: {str(e)}"
def getSampleWeatherData(self):
"""获取示例天气数据"""
try:
from datetime import datetime
import random
# 模拟天气数据
cities = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"]
conditions = ["晴天", "多云", "阴天", "小雨", "雷阵雨", "", ""]
city = random.choice(cities)
condition = random.choice(conditions)
temp = random.randint(-5, 35)
humidity = random.randint(30, 90)
wind_speed = round(random.uniform(0, 20), 1)
pressure = random.randint(980, 1030)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
return f"城市: {city}\n天气状况: {condition}\n温度: {temp}°C\n湿度: {humidity}%\n风速: {wind_speed} m/s\n气压: {pressure} hPa\n更新时间: {current_time}"
except Exception as e:
return f"获取示例数据失败: {str(e)}"
def onCreateSystemStatusPanel(self):
"""创建系统状态信息面板"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
panel = info_manager.createInfoPanel(
panel_id="system_status",
position=(1.4, 0.2),
size=(0.8, 1.2),
bg_color=(0.25, 0.15, 0.15, 0.95), # 红色背景
border_color=(0.7, 0.3, 0.3, 1.0), # 红色边框
title_color=(1.0, 0.5, 0.5, 1.0), # 浅红色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 添加到场景树
self.addInfoPanelToTree(panel, "系统状态信息面板")
# 立即显示初始数据
initial_data = self.getSystemStatusData()
info_manager.updatePanelContent("system_status", content=initial_data)
# 注册数据源每5秒更新一次
info_manager.registerDataSource("system_status", self.getSystemStatusData, update_interval=5.0)
except Exception as e:
print(f"✗ 创建系统状态信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建系统状态信息面板时出错: {str(e)}")
def getSystemStatusData(self):
"""
获取系统状态数据的回调函数
"""
try:
import psutil
import time
from datetime import datetime
# 获取系统信息
cpu_percent = psutil.cpu_percent(interval=0.1)
memory = psutil.virtual_memory()
memory_mb = round(memory.used / (1024 * 1024), 1)
memory_total_mb = round(memory.total / (1024 * 1024), 1)
memory_percent = memory.percent
# 网络状态
net_io = psutil.net_io_counters()
bytes_sent = round(net_io.bytes_sent / (1024 * 1024), 2) # MB
bytes_recv = round(net_io.bytes_recv / (1024 * 1024), 2) # MB
# 磁盘使用情况
disk = psutil.disk_usage('/')
disk_used_gb = round(disk.used / (1024 ** 3), 2)
disk_total_gb = round(disk.total / (1024 ** 3), 2)
disk_percent = round((disk.used / disk.total) * 100, 1)
# 时间戳
timestamp = datetime.now().strftime("%H:%M:%S")
return f"CPU使用率: {cpu_percent}%\n内存使用: {memory_mb}MB / {memory_total_mb}MB ({memory_percent}%)\n磁盘使用: {disk_used_gb}GB / {disk_total_gb}GB ({disk_percent}%)\n网络发送: {bytes_sent}MB\n网络接收: {bytes_recv}MB\n更新时间: {timestamp}"
except Exception as e:
return f"获取系统状态失败: {str(e)}"
def onCreateSensorDataPanel(self):
"""创建传感器数据信息面板"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
panel = info_manager.createInfoPanel(
panel_id="sensor_data",
position=(0.8, -0.2),
size=(0.8, 0.6),
bg_color=(0.15, 0.25, 0.15, 0.95), # 绿色背景
border_color=(0.3, 0.7, 0.3, 1.0), # 绿色边框
title_color=(0.5, 1.0, 0.5, 1.0), # 浅绿色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font
)
# 添加到场景树
self.addInfoPanelToTree(panel, "传感器数据信息面板")
# 立即显示初始数据
initial_data = self.getSensorData()
info_manager.updatePanelContent("sensor_data", content=initial_data)
# 注册数据源每2秒更新一次
info_manager.registerDataSource("sensor_data", self.getSensorData, update_interval=2.0)
# 绑定键盘事件
info_manager.accept("F3", info_manager.togglePanel, ["sensor_data"])
print("✓ 传感器数据信息面板已创建(按 F3 切换显示)")
except Exception as e:
print(f"✗ 创建传感器数据信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建传感器数据信息面板时出错: {str(e)}")
def getSensorData(self):
"""
获取传感器数据的回调函数模拟数据
"""
try:
import random
from datetime import datetime
# 模拟传感器数据
temperature = round(random.uniform(20, 35), 1)
humidity = round(random.uniform(30, 70), 1)
pressure = round(random.uniform(990, 1030), 1)
light_level = round(random.uniform(0, 1000), 1)
# 时间戳
timestamp = datetime.now().strftime("%H:%M:%S")
return f"温度: {temperature}°C\n湿度: {humidity}%\n气压: {pressure} hPa\n光照: {light_level} lux\n更新时间: {timestamp}"
except Exception as e:
return f"获取传感器数据失败: {str(e)}"
def onCreateSceneInfoPanel(self):
"""创建场景信息面板"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 创建面板
info_manager = self.world.info_panel_manager
info_manager.setParent(aspect2d)
panel = info_manager.createInfoPanel(
panel_id="scene_info",
position=(-0.8, 0.5),
size=(0.8, 0.6),
bg_color=(0.12, 0.12, 0.12, 0.95), # 深灰色背景
border_color=(0.4, 0.4, 0.4, 1.0), # 灰色边框
title_color=(0.2, 0.8, 1.0, 1.0), # 蓝色标题
content_color=(0.9, 0.9, 0.9, 1.0),
font=font
)
# 添加到场景树
self.addInfoPanelToTree(panel, "场景信息面板")
# 立即显示初始数据
initial_data = self.getSceneInfoData()
info_manager.updatePanelContent("scene_info", content=initial_data)
# 注册数据源每3秒更新一次
info_manager.registerDataSource("scene_info", self.getSceneInfoData, update_interval=3.0)
# 绑定键盘事件
info_manager.accept("F4", info_manager.togglePanel, ["scene_info"])
print("✓ 场景信息面板已创建(按 F4 切换显示)")
except Exception as e:
print(f"✗ 创建场景信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建场景信息面板时出错: {str(e)}")
def getSceneInfoData(self):
"""
获取场景信息数据的回调函数
"""
try:
# 获取场景信息
node_count = 0
texture_count = 0
light_count = 0
# 如果有场景管理器,获取实际数据
if hasattr(self.world, 'scene_graph'):
# 这里可以根据实际的场景结构来统计节点数
node_count = len([node for node in self.world.scene_graph.nodes]) if hasattr(self.world.scene_graph,
'nodes') else 0
# 统计光源数量
if hasattr(self.world, 'lights'):
light_count = len(self.world.lights)
# 统计纹理数量
if hasattr(self.world, 'textures'):
texture_count = len(self.world.textures)
# 当前时间
from datetime import datetime
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"场景节点数: {node_count}\n纹理数量: {texture_count}\n光源数量: {light_count}\nFPS: {self.world.clock.getAverageFrameRate():.1f}\n更新时间: {current_time}"
except Exception as e:
return f"获取场景信息失败: {str(e)}"
def onCreateAllInfoPanels(self):
"""创建所有信息面板"""
try:
self.onCreateSampleInfoPanel()
self.onCreateSystemStatusPanel()
self.onCreateSensorDataPanel()
self.onCreateSceneInfoPanel()
QMessageBox.information(self, "成功",
"所有信息面板已创建完成!\n快捷键:\nF1 - 示例面板\nF2 - 系统状态面板\nF3 - 传感器数据面板\nF4 - 场景信息面板")
except Exception as e:
QMessageBox.critical(self, "错误", f"创建信息面板时出错: {str(e)}")
def addInfoPanelToTree(self, panel, panel_name):
"""
将信息面板添加到场景树控件中
"""
if panel and self.treeWidget:
# 找到场景根节点
scene_root = None
for i in range(self.treeWidget.topLevelItemCount()):
item = self.treeWidget.topLevelItem(i)
if item.text(0) == "render":
scene_root = item
break
# 如果找不到场景根节点,使用第一个顶级节点
if not scene_root and self.treeWidget.topLevelItemCount() > 0:
scene_root = self.treeWidget.topLevelItem(0)
if scene_root:
tree_item = self.treeWidget.add_node_to_tree_widget(
node=panel,
parent_item=scene_root,
node_type="INFO_PANEL"
)
if tree_item:
self.treeWidget.setCurrentItem(tree_item)
self.treeWidget.update_selection_and_properties(panel, tree_item)
print(f"{panel_name}节点已添加到场景树")
return True
else:
print(f"⚠️ {panel_name}节点添加到场景树失败")
else:
print("❌ 未找到场景根节点")
return False
# ==================== 脚本管理事件处理 ====================
def refreshScriptsList(self):
@ -1426,22 +1864,6 @@ class MainWindow(QMainWindow):
else:
QMessageBox.warning(self, "错误", "高度图地形创建失败!")
def onTerrainEditMode(self):
"""地形编辑模式"""
# 检查当前是否已经处于地形编辑模式
if self.world.currentTool == "地形编辑":
# 退出地形编辑模式
self.world.setCurrentTool(None)
self.terrainEditTool.setChecked(False)
self.terrainEditTool.setText("地形编辑")
QMessageBox.information(self, "地形编辑", "已退出地形编辑模式")
else:
# 进入地形编辑模式
self.world.setCurrentTool("地形编辑")
self.terrainEditTool.setChecked(True)
self.terrainEditTool.setText("退出地形编辑")
QMessageBox.information(self, "地形编辑",
"已进入地形编辑模式\n\n使用鼠标左键抬高地形\n使用鼠标右键降低地形")
def setup_main_window(world):
"""设置主窗口的便利函数"""
app = QApplication.instance()

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,7 @@ from PyQt5.sip import wrapinstance
from panda3d.core import ModelRoot
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from scene import util
class NewProjectDialog(QDialog):
"""新建项目对话框"""
@ -150,8 +150,8 @@ class CustomPanda3DWidget(QPanda3DWidget):
for url in event.mimeData().urls():
filepath = url.toLocalFile()
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
# 使用关键字参数确保兼容性
self.world.importModel(filepath)
#self.world.addAnimationPanel(None,filepath)
event.acceptProposedAction()
else:
event.ignore()
@ -315,6 +315,9 @@ class CustomFileView(QTreeView):
print("不支持的文件类型")
super().mouseDoubleClickEvent(event)
from PyQt5.QtCore import QFileSystemWatcher,QTimer
import os
class CustomAssetsTreeWidget(QTreeWidget):
def __init__(self, world, parent=None):
if parent is None:
@ -325,6 +328,18 @@ class CustomAssetsTreeWidget(QTreeWidget):
self.setupUI()
self.setupDragDrop()
#添加文件系统监控器
self.file_watcher = QFileSystemWatcher()
self.file_watcher.directoryChanged.connect(self.onDirectoryChanged)
self.file_watcher.fileChanged.connect(self.onFileChanged)
self.refresh_timer = QTimer()
self.refresh_timer.setSingleShot(True)
self.refresh_timer.timeout.connect(self.refreshView)
#存储监控的目录
self.watched_directories = set()
# 默认加载项目根路径
self.load_file_tree()
# 设置右键菜单
@ -624,6 +639,9 @@ class CustomAssetsTreeWidget(QTreeWidget):
# 加载当前目录内容
self.load_directory_tree(self.current_path, root_item)
#添加目录到监控器
self.addWatchedDirectory(self.current_path)
# 展开根节点
root_item.setExpanded(True)
@ -631,6 +649,54 @@ class CustomAssetsTreeWidget(QTreeWidget):
error_item = QTreeWidgetItem(["❌ 无权限访问此目录"])
self.addTopLevelItem(error_item)
def addWatchedDirectory(self,directory):
"""添加监控目录"""
if os.path.exists(directory) and directory not in self.watched_directories:
if self.file_watcher.addPath(directory):
self.watched_directories.add(directory)
print(f"开始监控目录:{directory}")
try:
for item in os.listdir(directory):
item_path = os.path.join(directory,item)
if os.path.isdir(item_path):
self.addWatchedDirectory(item_path)
except Exception as e:
pass
else:
print(f"无法监控目录:{directory}")
def removeWatchedDirectory(self,directory):
"""移除监控目录"""
if directory in self.watched_directories:
if self.file_watcher.removePath(directory):
self.watched_directories.discard(directory)
print(f"停止监控目录:{directory}")
else:
print(f"无法停止监控目录:{directory}")
def onDirectoryChanged(self,path):
"""目录发生变化时处理"""
print(f"目录变化{path}")
if not self.refresh_timer.isActive():
self.refresh_timer.start(1000)
def onFileChanged(self,path):
"""目录发生变化时的处理"""
print(f"目录变化{path}")
if not self.refresh_timer.isActive():
self.refresh_timer.start(1000)
def refreshView(self):
"""刷新视图"""
print("刷新资源视图...")
try:
expanded_paths = self._saveExpandedState()
self.load_file_tree()
self._restoreExpandedState(expanded_paths)
print("资源视图刷新完成")
except Exception as e:
print(f"刷新资源视图失败{e}")
def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0):
"""递归加载目录树(类似左侧导航面板)"""
if current_depth >= max_depth:
@ -936,6 +1002,7 @@ 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))
# 设置内部拖拽数据
@ -1302,11 +1369,42 @@ class CustomTreeWidget(QTreeWidget):
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
self.initData()
self.setupUI() # 初始化界面
self.setupContextMenu() # 初始化右键菜单
self.setupDragDrop() # 设置拖拽功能
self.original_scales={}
def initData(self):
"""初始化变量"""
# 定义2D GUI元素类型
self.gui_2d_types = {
"GUI_BUTTON", # DirectButton
"GUI_LABEL", # DirectLabel
"GUI_ENTRY", # DirectEntry
"GUI_IMAGE",
"GUI_NODE" # 其他2D GUI容器
}
# 定义3D GUI元素类型
self.gui_3d_types = {
"GUI_3DTEXT", # 3D TextNode
"GUI_3DIMAGE",
"GUI_VIRTUAL_SCREEN" # Virtual Screen
}
# 定义3D场景节点类型可以接受3D GUI元素和其他3D场景元素
self.scene_3d_types = {
"SCENE_ROOT",
"SCENE_NODE",
"LIGHT_NODE",
"CAMERA_NODE",
"IMPORTED_MODEL_NODE",
"MODEL_NODE"
}
def setupUI(self):
"""初始化UI设置"""
self.setHeaderHidden(True)
@ -1356,12 +1454,14 @@ class CustomTreeWidget(QTreeWidget):
# 3D GUI菜单
create3dGUIMenu = createMenu.addMenu('3D GUI')
create3dGUIMenu.addAction(create_actions['create3DText'])
create3dGUIMenu.addAction(create_actions['create3DImage'])
# GUI菜单
createGUIMenu = createMenu.addMenu('GUI')
createGUIMenu.addAction(create_actions['createButton'])
createGUIMenu.addAction(create_actions['createLabel'])
createGUIMenu.addAction(create_actions['createEntry'])
createGUIMenu.addAction(create_actions['createImage'])
createGUIMenu.addSeparator()
createGUIMenu.addAction(create_actions['createVideoScreen'])
createGUIMenu.addAction(create_actions['createSphericalVideo'])
@ -1395,106 +1495,93 @@ class CustomTreeWidget(QTreeWidget):
else:
print("用户取消了菜单选择")
# 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分
def dropEvent(self, event):
dragged_item = self.currentItem()
target_item = self.itemAt(event.pos())
if not dragged_item or not target_item:
# 1. 获取所有被拖拽的项
dragged_items = self.selectedItems()
if not dragged_items:
event.ignore()
return
if not self.isValidParentChild(dragged_item, target_item):
event.ignore()
return
# 2. 在执行Qt的默认拖拽前记录所有拖拽项的原始状态
drag_info = []
for item in dragged_items:
panda_node = item.data(0, Qt.UserRole)
if not panda_node or panda_node.is_empty():
continue # 跳过无效节点
dragged_node = dragged_item.data(0, Qt.UserRole)
target_node = target_item.data(0, Qt.UserRole)
drag_info.append({
"item": item,
"node": panda_node,
"old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None
})
if not dragged_node or not target_node:
event.ignore()
return
# # 检查是否是有效的父子关系
# if self.isValidParentChild(dragged_item, target_item):
# # 保存当前的世界坐标
# world_pos = dragged_node.getPos(self.world.render)
#
# # 更新场景图中的父子关系
# dragged_node.wrtReparentTo(target_node)
#
# # 接受拖放事件,更新树形控件
# super().dropEvent(event)
#
# #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
# # 更新属性面板
# self.world.updatePropertyPanel(dragged_item)
# self.world.property_panel._syncEffectiveVisibility(dragged_node)
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
# 记录拖拽前的父节点
old_parent_item = dragged_item.parent()
old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None
# 执行Qt默认拖拽
# 3. 执行Qt的默认拖拽让UI树先行更新
# 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点
super().dropEvent(event)
# 检查拖拽后的父节点
new_parent_item = dragged_item.parent()
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
# 同步Panda3D场景图的父子关系
# 4. 遍历记录下的信息同步每一个Panda3D节点的状态
try:
# 检查是否是跨层级拖拽(父节点发生变化)
if old_parent_node != new_parent_node:
print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}")
for info in drag_info:
dragged_item = info["item"]
dragged_node = info["node"]
old_parent_node = info["old_parent_node"]
# 保存世界坐标位置
world_pos = dragged_node.getPos(self.world.render)
world_hpr = dragged_node.getHpr(self.world.render)
world_scale = dragged_node.getScale(self.world.render)
# 获取拖拽后的新父节点
new_parent_item = dragged_item.parent()
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
# 检查是否是2D GUI元素
dragged_type = dragged_item.data(0, Qt.UserRole + 1)
is_2d_gui = dragged_type in {"GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_NODE"}
# 仅当父节点实际发生变化时才执行重新父化
if old_parent_node != new_parent_node:
print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}")
# 重新父化到新的父节点
if new_parent_node:
if is_2d_gui:
# 2D GUI元素需要特殊处理
if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
dragged_node.reparentTo(new_parent_node)
# # 保存世界坐标位置
# world_pos = dragged_node.getPos(self.world.render)
# world_hpr = dragged_node.getHpr(self.world.render)
# world_scale = dragged_node.getScale(self.world.render)
# 检查是否是2D GUI元素
dragged_type = dragged_item.data(0, Qt.UserRole + 1)
is_2d_gui = dragged_type in self.gui_2d_types
# 重新父化到新的父节点
if new_parent_node:
if is_2d_gui:
# 2D GUI元素需要特殊处理
if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
dragged_node.wrtReparentTo(new_parent_node)
else:
# 目标是3D节点保持GUI特性重新父化到aspect2d
# from direct.showbase.ShowBase import aspect2d
dragged_node.wrtReparentTo(self.world.aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下")
else:
# 目标是3D节点保持GUI特性重新父化到aspect2d
from direct.showbase.ShowBase import aspect2d
dragged_node.reparentTo(aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下")
# 非GUI元素正常重新父化
dragged_node.wrtReparentTo(new_parent_node)
else:
# 非GUI元素正常重新父化
dragged_node.reparentTo(new_parent_node)
else:
# 如果新父节点为None根据元素类型决定父节点
if is_2d_gui:
from direct.showbase.ShowBase import aspect2d
dragged_node.reparentTo(aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d")
else:
dragged_node.reparentTo(self.world.render)
# 如果新父节点为None根据元素类型决定父节点
if is_2d_gui:
# from direct.showbase.ShowBase import aspect2d
dragged_node.wrtReparentTo(self.world.aspect2d)
print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d")
else:
dragged_node.wrtReparentTo(self.world.render)
# 恢复世界坐标位置对于2D GUI可能需要调整
if is_2d_gui:
# 2D GUI元素使用屏幕坐标系可能需要特殊处理
dragged_node.setPos(world_pos)
dragged_node.setHpr(world_hpr)
dragged_node.setScale(world_scale)
else:
dragged_node.setPos(self.world.render, world_pos)
dragged_node.setHpr(self.world.render, world_hpr)
dragged_node.setScale(self.world.render, world_scale)
# # 恢复世界坐标位置对于2D GUI可能需要调整
# if is_2d_gui:
# # 2D GUI元素使用屏幕坐标系可能需要特殊处理
# dragged_node.setPos(world_pos)
# dragged_node.setHpr(world_hpr)
# dragged_node.setScale(world_scale)
# else:
# dragged_node.setPos(self.world.render, world_pos)
# dragged_node.setHpr(self.world.render, world_hpr)
# dragged_node.setScale(self.world.render, world_scale)
print(f"✅ Panda3D父子关系已更新")
else:
print(f"同层级移动父节点未变化跳过Panda3D重新父化")
print(f"✅ Panda3D父子关系已更新")
else:
print(f"同层级移动父节点未变化跳过Panda3D重新父化")
except Exception as e:
print(f"⚠️ 同步Panda3D场景图失败: {e}")
@ -1505,28 +1592,6 @@ class CustomTreeWidget(QTreeWidget):
self.world.property_panel._syncEffectiveVisibility(dragged_node)
event.accept()
# try:
# world_pos = dragged_node.getPos(self.world.render)
#
# parent_of_dragged = dragged_node.getParent()
# target_node.wrtReparentTo(parent_of_dragged)
#
# # 拖动节点到目标节点下
# dragged_node.wrtReparentTo(target_node)
# dragged_node.setPos(self.world.render, world_pos)
#
# # 更新 Qt 树控件
# super().dropEvent(event)
#
# # 更新属性面板
# self.world.updatePropertyPanel(dragged_item)
#
# event.accept()
#
# except Exception as e:
# print(f"重设父节点失败: {e}")
# event.ignore()
def _ensureUnderSceneRoot(self, item):
"""确保节点在场景根节点下,如果不是则自动修正"""
if not item:
@ -1595,28 +1660,13 @@ class CustomTreeWidget(QTreeWidget):
target_type = target_item.data(0, Qt.UserRole + 1)
# 定义2D GUI元素类型
gui_2d_types = {
"GUI_BUTTON", # DirectButton
"GUI_LABEL", # DirectLabel
"GUI_ENTRY", # DirectEntry
"GUI_NODE" # 其他2D GUI容器
}
gui_2d_types = self.gui_2d_types
# 定义3D GUI元素类型
gui_3d_types = {
"GUI_3DTEXT", # 3D TextNode
"GUI_VIRTUAL_SCREEN" # Virtual Screen
}
gui_3d_types = self.gui_3d_types
# 定义3D场景节点类型可以接受3D GUI元素和其他3D场景元素
scene_3d_types = {
"SCENE_ROOT",
"SCENE_NODE",
"LIGHT_NODE",
"CAMERA_NODE",
"IMPORTED_MODEL_NODE",
"MODEL_NODE"
}
scene_3d_types = self.scene_3d_types
# 检查拖拽元素的类型
is_dragged_2d_gui = dragged_type in gui_2d_types
@ -1704,24 +1754,6 @@ class CustomTreeWidget(QTreeWidget):
# 出错时采用保守策略,禁止拖拽
return False
def _getNodeTypeDescription(self, node_type):
"""获取节点类型的描述文本(用于错误提示)"""
type_descriptions = {
"GUI_BUTTON": "2D按钮",
"GUI_LABEL": "2D标签",
"GUI_ENTRY": "2D输入框",
"GUI_NODE": "2D GUI容器",
"GUI_3DTEXT": "3D文本",
"GUI_VIRTUAL_SCREEN": "虚拟屏幕",
"SCENE_ROOT": "场景根节点",
"SCENE_NODE": "场景节点",
"LIGHT_NODE": "灯光节点",
"CAMERA_NODE": "相机节点",
"IMPORTED_MODEL_NODE": "导入模型",
"MODEL_NODE": "模型节点"
}
return type_descriptions.get(node_type, f"未知类型({node_type})")
def _getRootNode(self, item):
"""获取树中节点的根节点项"""
if not item:
@ -1842,18 +1874,45 @@ 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'):
self.world.render_pipeline.remove_light(light_object)
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()}")
# 从world列表中移除
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, '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()}")
# 从Panda3D场景中移除
panda_node.removeNode()
try:
if not panda_node.isEmpty():
panda_node.removeNode()
except Exception as e:
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
# 从Qt树中移除
parent_item = item.parent()
@ -1869,6 +1928,8 @@ class CustomTreeWidget(QTreeWidget):
except Exception as e:
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
import traceback
traceback.print_exc()
# 最终清理
# if hasattr(self.world, 'property_panel'):
@ -1912,24 +1973,30 @@ class CustomTreeWidget(QTreeWidget):
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==""
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):
def addNodeToTree(node, parentItem, force=False):
if not force and should_skip(node):
return
nodeItem = QTreeWidgetItem(parentItem,[node.getName()])
nodeItem.setData(0,Qt.UserRole, node)
return None
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)
addNodeToTree(child, nodeItem, force=False)
return nodeItem
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"
addNodeToTree(node, parent_item, force=True)
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])
@ -2018,10 +2085,7 @@ class CustomTreeWidget(QTreeWidget):
node_type = item.data(0, Qt.UserRole + 1)
# 场景根节点和普通场景节点可以作为父节点
if node_type in ["SCENE_ROOT",
"SCENE_NODE", "LIGHT_NODE", "CAMERA_NODE",
"IMPORTED_MODEL_NODE",
"GUI_3DTEXT"]:
if node_type in self.gui_3d_types and self.scene_3d_types:
return True
# # 模型节点也可以作为父节点
@ -2095,7 +2159,7 @@ class CustomTreeWidget(QTreeWidget):
node_type = item.data(0, Qt.UserRole + 1)
# GUI元素可以作为其他GUI元素的父节点
if node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_NODE"]:
if node_type in self.gui_2d_types:
return True
# 场景根节点和普通场景节点也可以作为父节点
@ -2111,7 +2175,7 @@ class CustomTreeWidget(QTreeWidget):
# 检查是否有GUI标签
if hasattr(node, 'getTag'):
return node.getTag("is_gui_element") == "1"
return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"]
# 检查是否是DirectGUI对象
try: