From d980330d9db48438cfecd01c7f3a69a88888ead1 Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Mon, 1 Sep 2025 09:06:16 +0800 Subject: [PATCH] =?UTF-8?q?8.28=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QPanda3D/Panda3DWorld.py | 2 +- core/event_handler.py | 14 +-- core/selection.py | 169 ++++++++++++++++++++++++-------- core/terrain_manager.py | 3 - core/world.py | 4 +- gui/gui_manager.py | 167 +++++++++++++++++++++++++++----- scene/scene_manager.py | 43 +++++---- ui/interface_manager.py | 201 ++++++++++++++++++++++++++++++++++----- ui/main_window.py | 35 +++---- ui/property_panel.py | 31 +----- ui/widgets.py | 25 +++-- 11 files changed, 519 insertions(+), 175 deletions(-) diff --git a/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py index a19f52c0..78193075 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QPanda3D/Panda3DWorld.py @@ -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 diff --git a/core/event_handler.py b/core/event_handler.py index 5b35345d..ff7b17c2 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -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() diff --git a/core/selection.py b/core/selection.py index b9c15de4..c93422de 100644 --- a/core/selection.py +++ b/core/selection.py @@ -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 = {} @@ -471,8 +533,8 @@ class SelectionSystem: self.gizmo.setShaderOff() # 禁用着色器 self.gizmo.setFogOff() # 禁用雾效 self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级,数值越大越优先 - self.gizmo.setDepthWrite(False) # 禁用深度写入 - self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见 + #self.gizmo.setDepthWrite(False) # 禁用深度写入 + #self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见 # 设置各轴节点的渲染属性 for axis_node in axis_nodes: @@ -481,8 +543,8 @@ class SelectionSystem: axis_node.setShaderOff() axis_node.setFogOff() axis_node.setBin("fixed", 40) # 与主节点相同优先级 - axis_node.setDepthWrite(False) # 禁用深度写入 - axis_node.setDepthTest(False) # 禁用深度测试 + #axis_node.setDepthWrite(False) # 禁用深度写入 + #axis_node.setDepthTest(False) # 禁用深度测试 # 设置旋转轴节点的渲染属性 for axis_rotnode in axis_Rotnodes: @@ -491,8 +553,8 @@ class SelectionSystem: axis_rotnode.setShaderOff() axis_rotnode.setFogOff() axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级 - axis_rotnode.setDepthWrite(False) # 禁用深度写入 - axis_rotnode.setDepthTest(False) # 禁用深度测试 + #axis_rotnode.setDepthWrite(False) # 禁用深度写入 + #axis_rotnode.setDepthTest(False) # 禁用深度测试 # 收集所有handle节点 arrow_nodes = [] @@ -530,8 +592,8 @@ class SelectionSystem: arrow_node.setShaderOff() arrow_node.setFogOff() arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 - arrow_node.setDepthWrite(False) - arrow_node.setDepthTest(False) + #arrow_node.setDepthWrite(False) + #arrow_node.setDepthTest(False) # 启用透明度支持 arrow_node.setTransparency(TransparencyAttrib.MAlpha) @@ -541,8 +603,8 @@ class SelectionSystem: rot_node.setShaderOff() rot_node.setFogOff() rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 - rot_node.setDepthWrite(False) - rot_node.setDepthTest(False) + #rot_node.setDepthWrite(False) + #rot_node.setDepthTest(False) # 启用透明度支持 rot_node.setTransparency(TransparencyAttrib.MAlpha) @@ -640,12 +702,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 @@ -733,6 +808,7 @@ class SelectionSystem: self.dragStartMousePos = None self.gizmoTargetStartPos = None self.gizmoStartPos = None + self._resetCursor() # def setGizmoAxisColor(self, axis, color): @@ -839,8 +915,8 @@ class SelectionSystem: handle_node.setFogOff() # 禁用雾效果 handle_node.setBin("fixed",41) - handle_node.setDepthWrite(False) - handle_node.setDepthTest(False) + #handle_node.setDepthWrite(False) + #handle_node.setDepthTest(False) # 保存材质引用以便后续修改 if axis == "x": @@ -854,8 +930,8 @@ class SelectionSystem: axis_node.setShaderOff() axis_node.setFogOff() axis_node.setBin("fixed", 40) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) + #axis_node.setDepthWrite(False) + #axis_node.setDepthTest(False) except Exception as e: print(f"设置坐标轴颜色失败: {str(e)}") @@ -889,15 +965,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 +986,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 +994,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) @@ -945,8 +1010,8 @@ class SelectionSystem: handle_node.setFogOff() # 禁用雾效果 handle_node.setBin("fixed",41) - handle_node.setDepthWrite(False) - handle_node.setDepthTest(False) + #handle_node.setDepthWrite(False) + #handle_node.setDepthTest(False) # 保存材质引用以便后续修改 if axis == "x": @@ -960,8 +1025,8 @@ class SelectionSystem: axis_node.setShaderOff() axis_node.setFogOff() axis_node.setBin("fixed", 40) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) + #axis_node.setDepthWrite(False) + #axis_node.setDepthTest(False) except Exception as e: print(f"设置坐标轴颜色失败: {str(e)}") @@ -1336,6 +1401,7 @@ class SelectionSystem: def updateGizmoHighlight(self, mouseX, mouseY): """更新坐标轴高亮状态""" if not self.gizmo or self.isDraggingGizmo: + self._resetCursor() return # 使用碰撞检测方法 @@ -1357,15 +1423,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 +1515,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 +1564,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 +1594,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) # 安全地更新属性面板 @@ -1776,6 +1862,7 @@ class SelectionSystem: # 重置高亮轴 self.gizmoHighlightAxis = None + self._resetCursor() # ==================== 选择管理 ==================== def updateSelection(self, nodePath): diff --git a/core/terrain_manager.py b/core/terrain_manager.py index 07ae3d7f..cfd85590 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -390,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 diff --git a/core/world.py b/core/world.py index d987d256..ab69cff0 100644 --- a/core/world.py +++ b/core/world.py @@ -399,14 +399,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): diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 95aaa827..60b58ec9 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -24,6 +24,78 @@ except ImportError: WEB_ENGINE_AVAILABLE = False print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") + def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib + + # 参数类型检查和转换 + if isinstance(size, (list, tuple)): + if len(size) >= 2: + x_size, y_size = float(size[0]), float(size[1]) + else: + x_size = y_size = float(size[0]) if size else 1.0 + else: + x_size = y_size = float(size) + + # 创建卡片 + cm = CardMaker('gui_3d_image') + cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) + + # 创建3D图像节点 + image_node = self.world.render.attachNewNode(cm.generate()) + image_node.setPos(*pos) + + # 为3D图像创建独立的材质 + material = Material(f"image-material-{len(self.gui_elements)}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + image_node.setMaterial(material, 1) + + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + self.update3DImageTexture(image_node, image_path) + + # 应用PBR效果(如果可用) + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.set_effect( + image_node, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": False, + "render_envmap": True, + "disable_children_effects": True + }, + 50 + ) + print("✓ GUI 3D图像PBR效果已应用") + except Exception as e: + print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + + # 为GUI元素添加标识(效仿3D文本方法) + image_node.setTag("gui_type", "3d_image") + image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + if image_path: + image_node.setTag("gui_image_path", image_path) + image_node.setTag("is_gui_element", "1") + + self.gui_elements.append(image_node) + + # 更新场景树 + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + + print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") + return image_node class GUIManager: """GUI元素管理系统类""" @@ -1887,28 +1959,9 @@ class GUIManager: if color.isValid(): r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 self.editGUIElement(gui_element, "color", [r, g, b, 1.0]) - - # def editGUI2DPosition(self, gui_element, axis, value): - # """编辑2D GUI元素位置""" - # try: - # current_pos = gui_element.getPos() - # - # if axis == "x": - # # 将逻辑坐标转换为屏幕坐标 - # new_screen_x = value * 0.1 - # gui_element.setPos(new_screen_x, current_pos.getY(), current_pos.getZ()) - # elif axis == "z": - # # 将逻辑坐标转换为屏幕坐标 - # new_screen_z = value * 0.1 - # gui_element.setPos(current_pos.getX(), current_pos.getY(), new_screen_z) - # - # print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})") - # - # except Exception as e: - # print(f"编辑2D GUI位置失败: {str(e)}") def update3DImageTexture(self, model_nodepath, image_path): - from panda3d.core import Texture + from panda3d.core import Texture, TextureStage try: # 加载新纹理 @@ -1918,14 +1971,21 @@ class GUIManager: new_texture.setMagfilter(Texture.FT_linear) new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) - # 应用纹理到模型 - model_nodepath.setTexture(new_texture, 1) + model_nodepath.clearTexture() + + # 为3D图像创建独立的纹理阶段 + image_stage = TextureStage("3d_image_texture") + image_stage.setSort(0) # 使用第一个纹理槽 + image_stage.setMode(TextureStage.MModulate) # 使用调制模式 + + # 应用纹理到模型,使用独立的纹理阶段 + model_nodepath.setTexture(image_stage, new_texture) # 更新标签 model_nodepath.setTag("gui_image_path", image_path) # 确保材质设置正确 - if not model_nodepath.has_material(): + if not model_nodepath.hasMaterial(): from panda3d.core import Material, LColor mat = Material() mat.setName(f"image-material-{id(model_nodepath)}") @@ -1936,11 +1996,72 @@ class GUIManager: mat.setShininess(10.0) model_nodepath.setMaterial(mat, 1) + # 保护子节点的纹理(特别是3D文本) + self._preserveChildNodeTextures(model_nodepath) + print(f"✅ 3D图像纹理已更新为: {image_path}") + return True else: print(f"❌ 无法加载纹理: {image_path}") + return False except Exception as e: print(f"❌ 更新纹理时出错: {e}") + return False + + def _preserveChildNodeTextures(self, parent_node): + """保护子节点的纹理不被父节点纹理影响""" + try: + # 遍历所有直接子节点 + for i in range(parent_node.getNumChildren()): + child = parent_node.getChild(i) + + # 检查子节点是否为3D文本或其他需要特殊处理的节点 + if self._is3DTextElement(child): + # 为3D文本创建独立的纹理阶段 + self._restore3DTextTexture(child) + elif self._isOtherSpecialElement(child): + # 为其他特殊元素恢复纹理 + self._restoreSpecialElementTexture(child) + + except Exception as e: + print(f"保护子节点纹理时出错: {e}") + + def _is3DTextElement(self, node): + """检查节点是否为3D文本元素""" + try: + return (hasattr(node, 'getTag') and + node.getTag("gui_type") == "3d_text") + except: + return False + + def _isOtherSpecialElement(self, node): + """检查节点是否为其他需要特殊处理的元素""" + try: + return (hasattr(node, 'getTag') and + node.getTag("gui_type") in ["3d_image", "button", "label"]) + except: + return False + + def _restore3DTextTexture(self, text_node): + """恢复3D文本的纹理设置""" + try: + from panda3d.core import TextureStage + + # 如果3D文本已经有字体纹理,确保它使用正确的纹理阶段 + if hasattr(text_node, 'getTexture') and text_node.getTexture(): + # 创建专门用于文本的纹理阶段 + text_stage = TextureStage("text_texture") + text_stage.setSort(10) # 使用较高的纹理槽索引 + text_stage.setMode(TextureStage.MModulate) + + # 重新应用文本纹理 + current_texture = text_node.getTexture() + text_node.setTexture(text_stage, current_texture) + + print(f"已恢复3D文本纹理: {text_node.getName()}") + + except Exception as e: + print(f"恢复3D文本纹理失败: {e}") def update2DImageTexture(self, gui_element, image_path): """更新2D图片纹理""" diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 472b2f16..5ba53bf0 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -87,7 +87,7 @@ class SceneManager: print("✓ 场景管理系统初始化完成") # ==================== 模型导入和处理 ==================== - + def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): """导入模型到场景 - 只在根节点下创建 @@ -98,15 +98,15 @@ class SceneManager: auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持 """ try: + + # 预处理文件路径和转换 + filepath = util.normalize_model_path(filepath) + original_filepath = filepath print(f"\n💾 开始导入模型: {os.path.basename(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 if auto_convert_to_glb and self._shouldConvertToGLB(filepath): print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") @@ -154,14 +154,27 @@ class SceneManager: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") + # # 应用处理选项 + # if apply_unit_conversion and filepath.lower().endswith('.fbx'): + # print("应用FBX单位转换(厘米到米)...") + # self._applyUnitConversion(model, 0.01) + # model.setTag("unit_conversion_applied", "true") + # + # if normalize_scales and filepath.lower().endswith('.fbx'): + # print("标准化FBX模型缩放层级...") + # self._normalizeModelScales(model) + # model.setTag("scale_normalization_applied", "true") + # 应用处理选项 - if apply_unit_conversion and filepath.lower().endswith('.fbx'): - print("应用FBX单位转换(厘米到米)...") + # 对于GLB文件,通常不需要单位转换,因为它们已经是标准单位 + if apply_unit_conversion and filepath.lower().endswith( + ('.fbx', '.obj')) and not filepath.lower().endswith('.glb'): + print("应用单位转换(厘米到米)...") self._applyUnitConversion(model, 0.01) model.setTag("unit_conversion_applied", "true") - if normalize_scales and filepath.lower().endswith('.fbx'): - print("标准化FBX模型缩放层级...") + if normalize_scales and filepath.lower().endswith(('.fbx', '.obj')): + print("标准化模型缩放层级...") self._normalizeModelScales(model) model.setTag("scale_normalization_applied", "true") @@ -429,6 +442,8 @@ class SceneManager: print("模型已应用过单位转换,跳过") return + + # 获取当前边界用于后续位置调整 original_bounds = model.getBounds() @@ -734,7 +749,7 @@ class SceneManager: # 根据调试设置决定是否显示碰撞体 if hasattr(self.world, 'debug_collision') and self.world.debug_collision: - cNodePath.show() + cNodePath.hide() else: cNodePath.hide() @@ -1053,9 +1068,7 @@ class SceneManager: 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) @@ -1152,9 +1165,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 diff --git a/ui/interface_manager.py b/ui/interface_manager.py index d2e8e4ac..3e802bfb 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -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): """更新场景树显示 - 实际实现""" diff --git a/ui/main_window.py b/ui/main_window.py index 27e974e1..873c84ec 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -314,6 +314,20 @@ 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.createTerrainMenu = self.createMenu.addMenu('地形') self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形') @@ -647,11 +661,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("选择") @@ -1429,22 +1438,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() diff --git a/ui/property_panel.py b/ui/property_panel.py index 8edc81b2..d50eb34b 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -340,9 +340,9 @@ class PropertyPanelManager: self.scale_y.setValue(scale.getY()) self.scale_z.setValue(scale.getZ()) - self.scale_x.valueChanged.connect(lambda v:terrain_node.setScaleX(v)) - self.scale_y.valueChanged.connect(lambda v:terrain_node.setScaleY(v)) - self.scale_z.valueChanged.connect(lambda v:terrain_node.setScaleZ(v)) + self.scale_x.valueChanged.connect(lambda v:self._updateXScale(terrain_node,v)) + self.scale_y.valueChanged.connect(lambda v:self._updateYScale(terrain_node,v)) + self.scale_z.valueChanged.connect(lambda v:self._updateZScale(terrain_node,v)) x_label2 = QLabel("X") y_label2 = QLabel("Y") @@ -1153,9 +1153,6 @@ class PropertyPanelManager: current_scale.getZ()])): scale_widget.setRange(-1000, 1000) scale_widget.setSingleStep(0.1) - # 如果缩放值为0,设置为一个很小的非零值 - if scale_value == 0: - scale_value = 0.01 if scale_value >= 0 else -0.01 scale_widget.setValue(scale_value) self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value)) @@ -1193,13 +1190,7 @@ class PropertyPanelManager: self._updateModelMaterialPanel(model) def _onScaleValueChanged(self, scale_widget, value): - """确保缩放值不为0""" - if value == 0: - # 设置为一个很小的非零值,保持原有符号 - if hasattr(scale_widget, 'value') and scale_widget.value() > 0: - scale_widget.setValue(0.01) - else: - scale_widget.setValue(-0.01) + pass def _updateXScale(self, model, value): """更新X轴缩放值""" @@ -1558,6 +1549,7 @@ class PropertyPanelManager: if success: # 保存路径到 Tag gui_element.setTag("texture_path", file_path) + gui_element.setTag("gui_image_path",file_path) # 更新显示 texture_label.setText(file_path) # 可选:刷新场景树或其他 UI @@ -1925,19 +1917,6 @@ class PropertyPanelManager: except Exception as e: print(f"✗ 更新GUI元素Z轴缩放失败: {e}") - def update3DImageTexture(self,nodepath,texture_path): - try: - tex = self.world.loader.loadTexture(texture_path) - if tex: - nodepath.setTexture(tex,1) - return True - else: - print(f"[警告] 无法加载贴图: {texture_path}") - return False - except Exception as e: - print(f"[错误] 更新 3D 图片纹理失败: {e}") - return False - def update2DImageTexture(self, gui_element, image_path): try: new_texture = self.world.loader.loadTexture(image_path) diff --git a/ui/widgets.py b/ui/widgets.py index f7364990..80dac573 100644 --- a/ui/widgets.py +++ b/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() @@ -936,6 +936,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)) # 设置内部拖拽数据 @@ -1910,24 +1911,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])