forked from Rowland/EG
addRender #26
4
.idea/EG.iml
generated
4
.idea/EG.iml
generated
@ -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>
|
||||
|
||||
@ -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
@ -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
0
Resources/c/a
Normal file
0
Resources/c/b/a.txt
Normal file
0
Resources/c/b/a.txt
Normal file
0
Resources/c/b/b
Normal file
0
Resources/c/b/b
Normal file
0
Resources/c/b/b.txt
Normal file
0
Resources/c/b/b.txt
Normal file
1054
core/InfoPanelManager.py
Normal file
1054
core/InfoPanelManager.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
1824
gui/gui_manager.py
1824
gui/gui_manager.py
File diff suppressed because it is too large
Load Diff
26
main.py
26
main.py
@ -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)
|
||||
|
||||
@ -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
4
terminal
Normal file
@ -0,0 +1,4 @@
|
||||
# 检查当前分支和状态
|
||||
git status
|
||||
git branch -v
|
||||
git log --oneline --graph --all -10
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
2371
ui/property_panel.py
2371
ui/property_panel.py
File diff suppressed because it is too large
Load Diff
390
ui/widgets.py
390
ui/widgets.py
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user