diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 98f9815c..af9861ed 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,7 +17,7 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.9555555556]]] + sun_azimuth: [[[0.5000000000,0.5000000000]]] sun_altitude: [[[0.5000000000,1.0000000000]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: diff --git a/core/event_handler.py b/core/event_handler.py index ae684c7f..c919aeae 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -1,3 +1,5 @@ +from inspect import getargs + from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState, DepthTestAttrib, ColorAttrib) @@ -119,6 +121,9 @@ class EventHandler: if not evt: print("事件为空") return + if self.world.currentTool == "地形编辑": + self._handleTerrainEdit(evt,"add") + return # 获取鼠标点击的位置 x = evt.get('x', 0) @@ -260,7 +265,154 @@ class EventHandler: print(f"清理碰撞检测节点失败: {str(e)}") print("=== 鼠标左键事件处理结束 ===\n") - + + def mousePressEventRight(self,evt): + """处理鼠标右键按下事件""" + print(f"当前工具: {self.world.currentTool}") + + # 检查是否是地形编辑模式 + if self.world.currentTool == "地形编辑": + self._handleTerrainEdit(evt, "subtract") # 降低地形 + return + + # 其他右键处理逻辑可以在这里添加 + print("鼠标右键事件处理") + + def _handleTerrainEdit(self, evt, operation): + """处理地形编辑""" + try: + x = evt.get('x', 0) + y = evt.get('y', 0) + + winWidth, winHeight = self.world.getWindowSize() + + mx = 2.0 * x / float(winWidth) - 1.0 + my = 1.0 - 2.0 * y / float(winHeight) + + nearPoint = Point3() + farPoint = Point3() + self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) + + worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) + worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) + + picker = CollisionTraverser() + queue = CollisionHandlerQueue() + + pickerNode = CollisionNode('terrain_edit_ray') + pickerNP = self.world.cam.attachNewNode(pickerNode) + + from panda3d.core import BitMask32 + # 确保我们检测所有碰撞体 + pickerNode.setFromCollideMask(BitMask32.bit(2)) + + # 使用相机坐标系的点创建射线 + direction = farPoint - nearPoint + direction.normalize() + pickerNode.addSolid(CollisionRay(nearPoint, direction)) + + picker.addCollider(pickerNP, queue) + picker.traverse(self.world.render) + print(f"地形碰撞检测结果数量: {queue.getNumEntries()}") + + # 添加调试信息,显示所有场景中的碰撞体 + print("场景中的碰撞体:") + + def show_colliders(node, level=0): + indent = " " * level + if not node.isEmpty(): + if isinstance(node.node(), CollisionNode): + print(f"{indent}CollisionNode: {node.getName()}") + for child in node.getChildren(): + show_colliders(child, level + 1) + + show_colliders(self.world.render) + + # 射线检测结果处理 + hitPos = None + hitNode = None + hitTerrainInfo = None + + if queue.getNumEntries() > 0: + queue.sortEntries() + # 遍历所有碰撞结果,找到地形节点 + for i in range(queue.getNumEntries()): + entry = queue.getEntry(i) + collided_node = entry.getIntoNodePath() + print(f"碰撞到节点: {collided_node.getName()}") + + # 检查是否是地形节点 + for terrain_info in self.world.terrain_manager.terrains: + terrain_node = terrain_info['node'] + if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node): + hitPos = entry.getSurfacePoint(self.world.render) + hitNode = collided_node + hitTerrainInfo = terrain_info + print(f"找到地形节点: {terrain_node.getName()}") + break + + if hitPos: + break + + if hitPos and hitTerrainInfo: + # 修改地形高度 + x_pos, y_pos = hitPos.getX(), hitPos.getY() + + # 使用 getattr 获取地形编辑参数 + radius = getattr(self.world, 'terrain_edit_radius', 3.0) + strength = getattr(self.world, 'terrain_edit_strength', 0.3) + + print(f"准备编辑地形: 位置({x_pos:.2f}, {y_pos:.2f}), 半径{radius}, 强度{strength}, 操作{operation}") + + # 添加更多调试信息 + print(f"地形信息: {hitTerrainInfo}") + if 'heightmap' in hitTerrainInfo: + print(f"高度图路径: {hitTerrainInfo['heightmap']}") + + try: + # 检查 terrain_manager 是否有 modifyTerrainHeight 方法 + if not hasattr(self.world.terrain_manager, 'modifyTerrainHeight'): + print("✗ 错误: terrain_manager 没有 modifyTerrainHeight 方法") + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + return + + # 调用地形编辑方法 + success = self.world.terrain_manager.modifyTerrainHeight( + hitTerrainInfo, x_pos, y_pos, radius=radius, strength=strength, operation=operation) + + if success: + print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})") + # 显示射线 + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + + # 强制刷新地形显示 + if 'terrain' in hitTerrainInfo: + hitTerrainInfo['terrain'].generate() + print("✓ 地形已刷新") + else: + print("✗ 地形编辑失败") + # 即使编辑失败也显示射线 + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + except Exception as e: + print(f"✗ 地形编辑过程中出现异常: {e}") + import traceback + traceback.print_exc() + # 即使编辑失败也显示射线 + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + else: + print("没有检测到地形碰撞") + # 显示射线(无碰撞) + self.showClickRay(worldNearPoint, worldFarPoint) + + # 清理碰撞检测节点 + pickerNP.removeNode() + print("地形编辑处理完成") + + except Exception as e: + print(f"地形编辑处理出错: {e}") + import traceback + traceback.print_exc() + def _handleSelectionClick(self, hitNode): """处理选择工具的点击事件""" print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") diff --git a/core/selection.py b/core/selection.py index 063239a6..b9c15de4 100644 --- a/core/selection.py +++ b/core/selection.py @@ -53,14 +53,14 @@ class SelectionSystem: # 高亮相关 self.gizmoHighlightAxis = None self.gizmo_colors = { - "x": (1, 0, 0, 0), # 红色 - "y": (0, 1, 0, 0), # 绿色 - "z": (0, 0, 1, 0) # 蓝色 + "x": (1, 0, 0, 1.0), # 红色 + "y": (0, 1, 0, 1.0), # 绿色 + "z": (0, 0, 1, 1.00) # 蓝色 } self.gizmo_highlight_colors = { - "x": (1, 1, 0, 0), # 黄色高亮 - "y": (1, 1, 0, 0), # 黄色高亮 - "z": (1, 1, 0, 0) # 黄色高亮 + "x": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 + "y": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 + "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 } print("✓ 选择和变换系统初始化完成") @@ -465,63 +465,74 @@ class SelectionSystem: axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis] + # 设置坐标轴主节点的渲染属性 + if self.gizmo: + self.gizmo.setLightOff() # 禁用光照 + self.gizmo.setShaderOff() # 禁用着色器 + self.gizmo.setFogOff() # 禁用雾效 + self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级,数值越大越优先 + self.gizmo.setDepthWrite(False) # 禁用深度写入 + self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见 + + # 设置各轴节点的渲染属性 for axis_node in axis_nodes: if axis_node: - #禁用光照和阴影 axis_node.setLightOff() axis_node.setShaderOff() axis_node.setFogOff() - #设置渲染层级,确保大多数对象之前渲染 - axis_node.setBin("fixed",30) - #axis_node.setDepthWrite(False) - #axis_node.setDepthTest(True) + axis_node.setBin("fixed", 40) # 与主节点相同优先级 + axis_node.setDepthWrite(False) # 禁用深度写入 + axis_node.setDepthTest(False) # 禁用深度测试 + # 设置旋转轴节点的渲染属性 for axis_rotnode in axis_Rotnodes: if axis_rotnode: axis_rotnode.setLightOff() axis_rotnode.setShaderOff() axis_rotnode.setFogOff() - axis_rotnode.setBin("fixed",30) - #axis_rotnode.setDepthWrite(False) - #axis_rotnode.setDepthTest(True) + axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级 + axis_rotnode.setDepthWrite(False) # 禁用深度写入 + axis_rotnode.setDepthTest(False) # 禁用深度测试 + # 收集所有handle节点 arrow_nodes = [] if self.gizmoXAxis: x_handle = self.gizmoXAxis.find("x_handle") - if x_handle: + if x_handle and not x_handle.isEmpty(): arrow_nodes.append(x_handle) if self.gizmoYAxis: y_handle = self.gizmoYAxis.find("y_handle") - if y_handle: + if y_handle and not y_handle.isEmpty(): arrow_nodes.append(y_handle) if self.gizmoZAxis: z_handle = self.gizmoZAxis.find("z_handle") - if z_handle: + if z_handle and not z_handle.isEmpty(): arrow_nodes.append(z_handle) rot_nodes = [] if self.gizmoRotXAxis: x_rHandle = self.gizmoRotXAxis.find("x_handle") - if x_rHandle: + if x_rHandle and not x_rHandle.isEmpty(): rot_nodes.append(x_rHandle) if self.gizmoRotYAxis: y_rHandle = self.gizmoRotYAxis.find("y_handle") - if y_rHandle: + if y_rHandle and not y_rHandle.isEmpty(): rot_nodes.append(y_rHandle) if self.gizmoRotZAxis: z_rHandle = self.gizmoRotZAxis.find("z_handle") - if z_rHandle: + if z_rHandle and not z_rHandle.isEmpty(): rot_nodes.append(z_rHandle) + # 设置handle节点的渲染属性 for arrow_node in arrow_nodes: if arrow_node: arrow_node.setLightOff() arrow_node.setShaderOff() arrow_node.setFogOff() - arrow_node.setBin("fixed",31) - #arrow_node.setDepthWrite(False) - #arrow_node.setDepthTest(False) - #启用透明度S + arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 + arrow_node.setDepthWrite(False) + arrow_node.setDepthTest(False) + # 启用透明度支持 arrow_node.setTransparency(TransparencyAttrib.MAlpha) for rot_node in rot_nodes: @@ -529,20 +540,12 @@ class SelectionSystem: rot_node.setLightOff() rot_node.setShaderOff() rot_node.setFogOff() - rot_node.setBin("fixed",31) - #rot_node.setDepthWrite(False) - #rot_node.setDepthTest(False) - #启用透明度S + rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 + rot_node.setDepthWrite(False) + rot_node.setDepthTest(False) + # 启用透明度支持 rot_node.setTransparency(TransparencyAttrib.MAlpha) - if self.gizmo: - self.gizmo.setLightOff() - self.gizmo.setShaderOff() - self.gizmo.setFogOff() - self.gizmo.setBin("fixed",29) - # self.gizmo.setDepthWrite(False) - #self.gizmo.setDepthTest(False) - except Exception as e: print(f"设置坐标轴渲染属性失败: {str(e)}") @@ -647,7 +650,7 @@ class SelectionSystem: is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - #安区地更新朝向 + # 安区地更新朝向 if is_scale_tool: #self.gizmo.setHpr(self.gizmoTarget.getHpr()) @@ -799,12 +802,27 @@ 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) + # # 设置材质属性 - 使用自发光确保在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]*20, 1.0), + min(color[1]*20, 1.0), + min(color[2]*20, 1.0), + color[3] + ) + + mat.setBaseColor(adjusted_color) + # mat.setDiffuse(adjusted_color * 0.8) # 稍微降低漫反射亮度 + # mat.setAmbient(adjusted_color * 0.3) # 设置环境光反射 + # mat.setSpecular(Vec4(0.3, 0.3, 0.3, 1.0)) # 适度的镜面反射 + # mat.setShininess(25.0) # 适中的高光强度 + mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 # 应用材质 handle_node.setMaterial(mat, 1) @@ -820,9 +838,9 @@ class SelectionSystem: handle_node.setShaderOff() # 禁用着色器 handle_node.setFogOff() # 禁用雾效果 - handle_node.setBin("fixed",31) - #arrow_node.setDepthWrite(False) - #arrow_node.setDepthTest(True) + handle_node.setBin("fixed",41) + handle_node.setDepthWrite(False) + handle_node.setDepthTest(False) # 保存材质引用以便后续修改 if axis == "x": @@ -835,9 +853,9 @@ class SelectionSystem: axis_node.setLightOff() axis_node.setShaderOff() axis_node.setFogOff() - axis_node.setBin("fixed", 30) + axis_node.setBin("fixed", 40) axis_node.setDepthWrite(False) - axis_node.setDepthTest(True) + axis_node.setDepthTest(False) except Exception as e: print(f"设置坐标轴颜色失败: {str(e)}") @@ -889,12 +907,28 @@ 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) + # # 设置材质属性 - 使用自发光确保在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), + min(color[2], 1.0), + color[3] + ) + + 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) @@ -910,9 +944,9 @@ class SelectionSystem: handle_node.setShaderOff() # 禁用着色器 handle_node.setFogOff() # 禁用雾效果 - handle_node.setBin("fixed",31) - #arrow_node.setDepthWrite(False) - #arrow_node.setDepthTest(True) + handle_node.setBin("fixed",41) + handle_node.setDepthWrite(False) + handle_node.setDepthTest(False) # 保存材质引用以便后续修改 if axis == "x": @@ -925,9 +959,9 @@ class SelectionSystem: axis_node.setLightOff() axis_node.setShaderOff() axis_node.setFogOff() - axis_node.setBin("fixed", 30) - #axis_node.setDepthWrite(False) - #axis_node.setDepthTest(True) + axis_node.setBin("fixed", 40) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(False) except Exception as e: print(f"设置坐标轴颜色失败: {str(e)}") @@ -964,6 +998,8 @@ class SelectionSystem: # 稍微放大以增强视觉效果 axis_nodes[axis].setScale(1.1) + self.gizmoHighlightAxis = axis + except Exception as e: print(f"鼠标进入坐标轴处理失败: {e}") @@ -990,6 +1026,9 @@ class SelectionSystem: if axis in axis_nodes and axis_nodes[axis]: axis_nodes[axis].setScale(1.0) + if self.gizmoHighlightAxis == axis: + self.gizmoHighlightAxis = None + except Exception as e: print(f"鼠标离开坐标轴处理失败: {e}") @@ -1435,8 +1474,12 @@ class SelectionSystem: print("拖拽更新失败: 没有坐标轴起始位置") return - is_scale_tool = self.world.tool_manager.isScaleTool() - is_rotate_tool = self.world.tool_manager.isRotateTool() + 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 + + is_gui_element = (hasattr(self.gizmoTarget,'getTag') and + self.gizmoTarget.getTag("is_gui_element") == "1") + # 计算鼠标移动距离(屏幕像素) mouseDeltaX = mouseX - self.dragStartMousePos[0] @@ -1446,22 +1489,35 @@ class SelectionSystem: scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01 start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1)) - target_hpr = self.gizmoTarget.getHpr() - - if self.dragGizmoAxis == "x": - new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z) - elif self.dragGizmoAxis == "y": - new_scale = Vec3(start_scale.x,start_scale.y*scale_factor,start_scale.z) - elif self.dragGizmoAxis == "z": - z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY)*0.01 - new_scale = Vec3(start_scale.x,start_scale.y,start_scale.z*z_scale_factor) + if is_gui_element: + if self.dragGizmoAxis == "x": + new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z) + elif self.dragGizmoAxis == "y": + new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) + elif self.dragGizmoAxis == "z": + new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale.y * scale_factor, + start_scale.z * scale_factor) else: - new_scale = Vec3(start_scale.x * scale_factor, - start_scale * scale_factor, - start_scale.z * scale_factor) - #应用新缩放值 + # 普通3D模型的缩放处理 + if self.dragGizmoAxis == "x": + new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z) + elif self.dragGizmoAxis == "y": + new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) + elif self.dragGizmoAxis == "z": + z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY) * 0.01 + new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale.y * scale_factor, + start_scale.z * scale_factor) + + # 应用新缩放值 self.gizmoTarget.setScale(new_scale) - #实时更新属性面板 + # 安全地更新属性面板 + #self._safeUpdatePropertyPanel() self.world.property_panel.refreshModelValues(self.gizmoTarget) return elif is_rotate_tool: @@ -1481,6 +1537,7 @@ class SelectionSystem: start_hpr.y + rotation_amount, start_hpr.z + rotation_amount) self.gizmoTarget.setHpr(new_hpr) + #self._safeUpdatePropertyPanel() self.world.property_panel.refreshModelValues(self.gizmoTarget) return @@ -1647,6 +1704,7 @@ class SelectionSystem: # 实时更新属性面板 self.world.property_panel.refreshModelValues(self.gizmoTarget) + #self._safeUpdatePropertyPanel() # 每次拖拽都输出调试信息(但限制频率) if not hasattr(self, '_last_drag_debug_time'): @@ -1663,6 +1721,39 @@ class SelectionSystem: import traceback traceback.print_exc() + def _safeUpdatePropertyPanel(self): + """安全地更新属性面板""" + try: + # 检查属性面板是否存在且有效 + if (hasattr(self.world, 'property_panel') and + self.world.property_panel is not None): + + # 检查目标节点是否有效 + if (self.gizmoTarget is not None and + not self.gizmoTarget.isEmpty()): + + # 检查是否有refreshModelValues方法 + if hasattr(self.world.property_panel, 'refreshModelValues'): + self.world.property_panel.refreshModelValues(self.gizmoTarget) + # 如果是GUI元素,可能需要特殊的处理 + elif (hasattr(self.gizmoTarget, 'getTag') and + self.gizmoTarget.getTag("is_gui_element") == "1" and + hasattr(self.world.property_panel, 'updateGUIPropertyPanel')): + # 对于GUI元素,可能需要重新构建整个面板 + if (hasattr(self.world, 'treeWidget') and + self.world.treeWidget is not None): + current_item = self.world.treeWidget.currentItem() + if current_item is not None: + self.world.property_panel.updatePropertyPanel(current_item) + except RuntimeError as e: + if "wrapped C/C++ object" in str(e): + # 忽略控件已被删除的错误 + print("警告: 属性面板控件已被删除,跳过更新") + else: + print(f"更新属性面板时出错: {e}") + except Exception as e: + print(f"更新属性面板失败: {e}") + def stopGizmoDrag(self): """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") @@ -1690,19 +1781,34 @@ class SelectionSystem: def updateSelection(self, nodePath): """更新选择状态""" print(f"\n=== 更新选择状态 ===") - print(f"新选择的节点: {nodePath.getName() if nodePath else 'None'}") + + # 如果正在删除节点,避免更新选择 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,跳过选择更新") + print("=== 选择状态更新完成 ===\n") + return + + node_name = "None" + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + print(f"新选择的节点: {node_name}") self.selectedNode = nodePath # 添加兼容性属性 self.selectedObject = nodePath - if nodePath: - print(f"开始为节点 {nodePath.getName()} 创建选择框和坐标轴...") + + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + print(f"开始为节点 {node_name} 创建选择框和坐标轴...") # 创建选择框 print("创建选择框...") self.createSelectionBox(nodePath) if self.selectionBox: - print(f"✓ 选择框创建成功: {self.selectionBox.getName()}") + box_name = "Unknown" + if self.selectionBox and not self.selectionBox.isEmpty(): + box_name = self.selectionBox.getName() + print(f"✓ 选择框创建成功: {box_name}") else: print("× 选择框创建失败") @@ -1710,11 +1816,14 @@ class SelectionSystem: print("创建坐标轴...") self.createGizmo(nodePath) if self.gizmo: - print(f"✓ 坐标轴创建成功: {self.gizmo.getName()}") + gizmo_name = "Unknown" + if self.gizmo and not self.gizmo.isEmpty(): + gizmo_name = self.gizmo.getName() + print(f"✓ 坐标轴创建成功: {gizmo_name}") else: print("× 坐标轴创建失败") - print(f"✓ 选中了节点: {nodePath.getName()}") + print(f"✓ 选中了节点: {node_name}") else: print("清除选择...") self.clearSelectionBox() @@ -1732,10 +1841,10 @@ class SelectionSystem: return self.selectedNode is not None def checkAndClearIfTargetDeleted(self): - if self.gizmoTarget and self.gizmoTarget.isEmpty(): + if (self.gizmoTarget and self.gizmoTarget.isEmpty()): self.clearGizmo() - if self.selectionBoxTarget and self.selectionBoxTarget.isEmpty(): + if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()): self.clearSelectionBox() def setupGizmoCollision(self): diff --git a/core/terrain_manager.py b/core/terrain_manager.py new file mode 100644 index 00000000..dd940091 --- /dev/null +++ b/core/terrain_manager.py @@ -0,0 +1,472 @@ +# core/terrain_manager.py +import time +from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath +from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight +import os + + +class TerrainManager: + """地形管理类""" + + def __init__(self, world): + self.world = world + self.terrains = [] + + # core/terrain_manager.py + + def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): + """从高度图创建地形""" + # 检查文件是否存在 + if not os.path.exists(heightmap_path): + print(f"错误: 高度图文件 {heightmap_path} 不存在") + return None + + try: + # 创建GeoMipTerrain对象 + terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) + + # 加载高度图 + height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) + + # 检查并调整图像尺寸为2的幂次方加1 + width, height = height_image.getXSize(), height_image.getYSize() + print(f"原始图像尺寸: {width}x{height}") + + # 找到最接近的有效尺寸 + valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] + target_size = 129 # 默认尺寸 + + # 选择最接近的尺寸 + max_dim = max(width, height) + for size in valid_sizes: + if size >= max_dim: + target_size = size + break + else: + target_size = valid_sizes[-1] # 使用最大尺寸 + + # 如果需要,调整图像尺寸 + if width != target_size or height != target_size: + print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") + # 使用正确的图像缩放方法 + resized_image = PNMImage(target_size, target_size) + resized_image.quickFilterFrom(height_image) + height_image = resized_image + + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) + + # 设置地形参数 + terrain.setBruteforce(True) # 使用LOD + + terrain.setBlockSize(32) + # terrain.setNearFarThreshold(50.0,200.0) + + # 生成地形 + terrain.generate() + + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的地形节点") + return None + + node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" + terrain_node.setName(node_name) + + center_offset = (target_size - 1) / 2 + terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) + + # 设置缩放 + terrain_node.setScale(scale[0], scale[1], scale[2]) + + # 将地形添加到场景中 + terrain_node.reparentTo(self.world.render) + + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + terrain_node.setPythonTag("selectable", True) + + # 保存地形信息(包括高度图的副本) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': heightmap_path, + 'heightfield': height_image, # 保存高度图副本 + 'scale': scale, + 'name': node_name + } + + self.terrains.append(terrain_info) + + # 更新场景树(再次检查节点是否有效) + if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, + 'updateSceneTree'): + try: + self.world.scene_manager.updateSceneTree() + except Exception as e: + print(f"警告: 更新场景树时出错: {e}") + + print(f"✓ 成功从 {heightmap_path} 创建地形") + return terrain_info + + except Exception as e: + print(f"创建地形时出错: {e}") + import traceback + traceback.print_exc() + return None + + def createFlatTerrain(self, size=(0.3, 0.3), resolution=129): + """创建平面地形""" + try: + # 确保分辨率是2的幂次方加1 (如129, 257, 513等) + valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] + if resolution not in valid_resolutions: + # 找到最接近的有效分辨率 + closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) + print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") + resolution = closest_res + + # 创建GeoMipTerrain对象 + terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) + + # 创建一个平面高度图,尺寸必须是2^n+1 + height_image = PNMImage(resolution, resolution) + height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) + + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) + terrain.setBruteforce(True) # 设置LOD + + # 设置地形参数 + terrain.setBlockSize(32) # 设置块大小 + + # 生成地形 + terrain.generate() + + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的平面地形节点") + return None + + node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain_node.setName(node_name) + + center_offset = (resolution - 1) / 2 + terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) + + # 将地形添加到场景中 + terrain_node.reparentTo(self.world.render) + + # 为地形添加碰撞体 + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + # 保存地形信息(包括高度图) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': None, + 'heightfield': height_image, # 保存高度图 + 'scale': (size[0], size[1], 50), + 'name': node_name + } + + self.terrains.append(terrain_info) + + # 更新场景树(再次检查节点是否有效) + if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, + 'updateSceneTree'): + try: + self.world.scene_manager.updateSceneTree() + except Exception as e: + print(f"警告: 更新场景树时出错: {e}") + + print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}") + return terrain_info + + except Exception as e: + print(f"创建平面地形时出错: {e}") + import traceback + traceback.print_exc() + return None + + def _applyTerrainMaterial(self, terrain_node): + """为地形应用材质""" + try: + # 创建材质 + material = Material() + material.setAmbient((0.5, 0.5, 0.5, 1.0)) # 环境光反射 + material.setDiffuse((1, 1, 1, 1.0)) # 漫反射(绿色调) + material.setSpecular((0.2, 0.2, 0.2, 1.0)) # 镜面反射 + material.setShininess(5.0) # 高光强度 + + # 应用材质到地形 + terrain_node.setMaterial(material) + + # 启用材质 + terrain_node.setAttrib(ColorAttrib.makeDefault()) + + print("✓ 地形材质已应用") + except Exception as e: + print(f"应用地形材质时出错: {e}") + + def updateTerrain(self): + """更新所有地形的LOD""" + for terrain_info in self.terrains: + terrain = terrain_info['terrain'] + if hasattr(terrain, 'update'): + terrain.update() + + def setTerrainLODThreshold(self, terrain_info, threshold): + """设置地形LOD阈值""" + if terrain_info in self.terrains: + terrain = terrain_info['terrain'] + # GeoMipTerrain可能没有setNearFarThreshold方法,使用其他方式设置LOD + print("注意: GeoMipTerrain可能不支持直接设置LOD阈值") + + def getTerrainHeight(self, terrain_info, x, y): + """获取地形上指定点的高度""" + if terrain_info in self.terrains: + terrain = terrain_info['terrain'] + if hasattr(terrain, 'getElevation'): + return terrain.getElevation(x, y) + return 0 + + # core/terrain_manager.py + + def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"): + """修改地形高度""" + if terrain_info not in self.terrains: + return False + + try: + terrain = terrain_info['terrain'] + terrain_node = terrain_info['node'] # 获取地形节点 + + # 直接使用我们保存的高度图数据 + if 'heightfield' not in terrain_info: + print("地形信息中没有保存高度图数据") + return False + + heightmap = terrain_info['heightfield'] + if not heightmap: + print("无法获取地形高度图数据") + return False + + # 获取高度图尺寸 + width = heightmap.getXSize() + height = heightmap.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 正确的坐标转换方法 + # 1. 将世界坐标转换为相对于地形中心的坐标 + relative_x = x - terrain_pos.getX() + relative_y = y - terrain_pos.getY() + + # 2. 考虑地形的缩放 + scaled_x = relative_x / terrain_scale.getX() + scaled_y = relative_y / terrain_scale.getY() + + # 3. 转换为高度图坐标 + center_offset = (width - 1) / 2 + center_x = int(scaled_x + center_offset) + center_y = int(scaled_y + center_offset) + + print(f"坐标转换详情:") + print(f" 世界坐标: ({x:.2f}, {y:.2f})") + print(f" 地形位置: ({terrain_pos.getX():.2f}, {terrain_pos.getY():.2f})") + print(f" 地形缩放: ({terrain_scale.getX():.2f}, {terrain_scale.getY():.2f})") + print(f" 相对坐标: ({relative_x:.2f}, {relative_y:.2f})") + print(f" 缩放坐标: ({scaled_x:.2f}, {scaled_y:.2f})") + print(f" 中心偏移: {center_offset}") + print(f" 高度图坐标: ({center_x}, {center_y})") + print(f" 高度图尺寸: {width} x {height}") + + # 检查中心点是否在有效范围内 + if not (0 <= center_x < width and 0 <= center_y < height): + print(f"❌ 中心点坐标超出范围: ({center_x}, {center_y})") + print(f"❌ 有效范围: (0-{width - 1}, 0-{height - 1})") + return False + + # 修改半径范围内的点 + modified = False + + # 使用正确的半径计算(考虑地形缩放) + effective_radius = max(1, int(radius / max(terrain_scale.getX(), terrain_scale.getY()))) + + print(f"有效编辑半径: {effective_radius}") + + # 限制编辑半径不超过地形尺寸的1/4,避免性能问题 + effective_radius = min(effective_radius, width // 4, height // 4) + + for dx in range(-effective_radius, effective_radius + 1): + for dy in range(-effective_radius, effective_radius + 1): + # 检查距离 + distance = (dx * dx + dy * dy) ** 0.5 + if distance <= effective_radius: + # 计算影响权重 + weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 + + # 计算目标点坐标 + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 获取当前高度 + current_height = heightmap.getRed(target_x, target_y) + + # 根据操作类型修改高度 + if operation == "add": + new_height = min(1.0, current_height + strength * weight * 0.01) + elif operation == "subtract": + new_height = max(0.0, current_height - strength * weight * 0.01) + elif operation == "set": + # 平坦化操作,设置为中等高度 + new_height = 0.5 + else: + new_height = current_height + + # 设置新高度 + heightmap.setRed(target_x, target_y, new_height) + heightmap.setGreen(target_x, target_y, new_height) + heightmap.setBlue(target_x, target_y, new_height) + modified = True + + # 如果有修改,则重新生成地形 + if modified: + # 重新设置高度图并生成地形 + terrain.setHeightfield(heightmap) + terrain.generate() + + # 关键修复:重新创建碰撞体,因为在生成地形时会丢失原有碰撞体 + from panda3d.core import BitMask32, CollisionNode, CollisionPlane, Plane, Point3 + # 移除旧的地形碰撞体(如果存在) + for child in terrain_node.getChildren(): + if isinstance(child.node(), CollisionNode) and child.getName().startswith("terrain_collision_"): + child.removeNode() + + # 创建新的地形碰撞体 + # 使用更简单的方法:直接给地形节点添加碰撞体 + terrain_node.setCollideMask(BitMask32.bit(2)) # 确保地形节点本身具有碰撞掩码 + + # 为地形的每个子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + 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 + + except Exception as e: + print(f"修改地形高度时出错: {e}") + import traceback + traceback.print_exc() + return False + + def deleteTerrain(self, terrain_info): + """删除地形""" + if terrain_info in self.terrains: + # 从场景中移除地形节点 + terrain_node = terrain_info['node'] + if terrain_node and not terrain_node.isEmpty(): + terrain_node.removeNode() + + # 从列表中移除 + self.terrains.remove(terrain_info) + + # 更新场景树 + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + self.world.scene_manager.updateSceneTree() + + print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}") + + import gc + gc.collect() + + return True + return False + + def getTerrainByName(self, name): + """根据名称查找地形""" + for terrain_info in self.terrains: + if terrain_info.get('name') == name: + return terrain_info + return None + + def getAllTerrainNames(self): + """获取所有地形名称""" + return [terrain_info.get('name', f'Terrain_{i}') for i, terrain_info in enumerate(self.terrains)] + + def setTerrainColor(self, terrain_info, color): + """设置地形颜色""" + try: + if terrain_info in self.terrains: + terrain_node = terrain_info['node'] + if terrain_node: + # 创建新材质 + material = Material() + material.setAmbient((color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, 1.0)) + material.setDiffuse((color[0], color[1], color[2], 1.0)) + material.setSpecular((0.2, 0.2, 0.2, 1.0)) + material.setShininess(5.0) + + # 应用材质 + terrain_node.setMaterial(material) + print(f"✓ 地形颜色已更新: {color}") + return True + return False + except Exception as e: + print(f"设置地形颜色时出错: {e}") + return False + + def setTerrainTexture(self, terrain_info, texture_path): + """为地形设置纹理""" + try: + if terrain_info in self.terrains: + terrain_node = terrain_info['node'] + if terrain_node and os.path.exists(texture_path): + # 加载纹理 + texture = self.world.loader.loadTexture(texture_path) + if texture: + # 应用纹理 + terrain_node.setTexture(texture, 1) + print(f"✓ 地形纹理已应用: {texture_path}") + return True + else: + print(f"✗ 无法加载纹理: {texture_path}") + else: + print(f"✗ 纹理文件不存在: {texture_path}") + return False + except Exception as e: + print(f"应用地形纹理时出错: {e}") + return False diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 0c87a889..dad88a63 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -10,9 +10,9 @@ GUI元素管理模块 from direct.gui.DirectGui import * from panda3d.core import * -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit, - QDoubleSpinBox, QPushButton, QDialogButtonBox, - QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout) +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit, + QDoubleSpinBox, QPushButton, QDialogButtonBox, + QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout, QGridLayout, QSpinBox) from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt # 尝试导入 QtWebEngineWidgets,如果失败则设置为 None @@ -147,6 +147,112 @@ class GUIManager: print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") return entry + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + """创建2D GUI图片""" + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib,Texture,CardMaker,CardMaker,Vec3 + + # 将3D坐标转换为2D屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + #使用CardMaker创建一个更可靠的图片框架 + cm = CardMaker('gui-2d-image') + cm.setFrame(-size,size,-size,size) + + image_node = self.world.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(gui_pos) + image_node.setBin('fixed',0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setColor(1,1,1,1) + + # 设置透明度支持 + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + try: + texture = self.world.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture,1) + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + image_node.setColor(1,1,1,1) + else: + print(f"⚠️ 无法加载2D图片纹理: {image_path}") + except Exception as e: + print(f"❌ 加载2D图片纹理失败: {e}") + + # 为GUI元素添加标识 + image_node.setTag("gui_type", "2d_image") + image_node.setTag("gui_id", f"2d_image_{len(self.gui_elements)}") + image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}") + if image_path: + image_node.setTag("image_path", image_path) + image_node.setTag("is_gui_element", "1") + + self.gui_elements.append(image_node) + + # 安全地调用updateSceneTree + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + + print(f"✓ 创建2D GUI图片 (逻辑位置: {pos}, 屏幕位置: {gui_pos})") + return image_node + + def constrain2DPosition(self,gui_element,new_x=None,new_z=None): + """限制2dGUI元素位置在屏幕范围内""" + try: + from panda3d.core import Vec3 + + bounds = gui_element.getTightBounds() + element_width=0 + element_height=0 + + if bounds: + min_point,max_point = bounds + element_width = (max_point.getX() - min_point.getX())/2 + element_height = (max_point.getZ()-min_point.getZ())/2 + + #获取当前缩放 + scale = gui_element.getScale() + if hasattr(scale,'getX'): + scale_x = scale.getX() + scale_z = scale.getZ() if hasattr(scale,'getZ') else scale_x + else: + scale_x = scale_z = scale if isinstance(scale,(int,float)) else 1.0 + + actual_width = element_width * scale_x + actual_height = element_height * scale_z + + screen_width = 1.9 + screen_height = 0.9 + + min_x = -screen_width + actual_width + max_x = screen_width - actual_width + min_z = -screen_height + actual_height + max_z = screen_height - actual_height + + #获取当前位置 + current_pos = gui_element.getPos() + x = new_x if new_x is not None else current_pos.getX() + z = new_z if new_z is not None else current_pos.getZ() + + #应用边界限制 + x = max(min_x,min(max_x,x)) + z = max(min_z,min(max_z,z)) + + return Vec3(x,current_pos.getY(),z) + except Exception as e: + print(f"约束2D位置时出错: {e}") + # 出错时返回原始值 + current_pos = gui_element.getPos() + x = new_x if new_x is not None else current_pos.getX() + z = new_z if new_z is not None else current_pos.getZ() + return Vec3(x, current_pos.getY(), z) + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): """创建3D空间文本""" @@ -436,25 +542,34 @@ class GUIManager: elif property_name == "color": # 添加颜色处理 if isinstance(value, (list, tuple)) and len(value) >= 3: - # 更新材质颜色 - if not gui_element.hasMaterial(): - material = Material(f"text-material-{gui_element.getName()}") - material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - gui_element.setMaterial(material, 1) - else: - material = gui_element.getMaterial() - material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - gui_element.setMaterial(material, 1) + # 对于2D GUI元素(button和label),使用frameColor属性 + if gui_type in ["button", "label"]: + # 设置背景颜色 + gui_element['frameColor'] = (value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0) + print(f"成功更新2D GUI背景颜色: {gui_type} -> {value}") + + # 对于3D元素,使用材质颜色 + elif gui_type in ["3d_text", "3d_image", "virtual_screen"]: + # 更新材质颜色 + if not gui_element.hasMaterial(): + material = Material(f"text-material-{gui_element.getName()}") + material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + gui_element.setMaterial(material, 1) + else: + material = gui_element.getMaterial() + material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + gui_element.setMaterial(material, 1) + # 更新 TextNode 的文本颜色 - if isinstance(gui_element.node(), TextNode): - gui_element.node().setTextColor( - Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - # if gui_type in ["3d_text", "virtual_screen"]: - # gui_element.setColor(*value) - # elif gui_type in ["button", "label"]: - # gui_element['text_fg'] = value + if isinstance(gui_element.node(), TextNode): + gui_element.node().setTextColor( + Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + print(f"成功更新3D GUI颜色: {gui_type} -> {value}") + + else: + print(f"警告: 未知的GUI类型 {gui_type},无法设置颜色") elif property_name == "position": if isinstance(value, (list, tuple)) and len(value) >= 3: @@ -497,6 +612,9 @@ class GUIManager: elif gui_type == "3d_image": image_path = gui_element.getTag("image_path") self.createGUI3DImage(new_pos,image_path,size=(2,2)) + elif gui_type == "2d_image": + image_path = gui_element.getTag("image_path") + self.createGUI2DImage(new_pos, image_path, size=0.2) elif gui_type == "virtual_screen": self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本") @@ -745,7 +863,20 @@ class GUIManager: text_font=self.world.getChineseFont() if self.world.getChineseFont() else None ) y_pos -= spacing - + + # 2D图片工具 + btn_2d_image = DirectButton( + parent=self.guiEditPanel, + text="2D图片", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["2d_image"], + frameColor=(0.8, 0.6, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + # 虚拟屏幕工具 btn_screen = DirectButton( parent=self.guiEditPanel, @@ -944,6 +1075,8 @@ class GUIManager: element = self.createGUI3DText(pos, f"3D文本_{len(self.gui_elements)}") elif gui_type == "3d_image": element = self.createGUI3DImage(pos) + elif gui_type == "2d_image": + element = self.createGUI2DImage(pos) elif gui_type == "virtual_screen": element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}") else: @@ -1039,54 +1172,198 @@ class GUIManager: # 位置属性 if hasattr(gui_element, 'getPos'): + # 根据GUI类型设置组名 + if gui_type in ["button", "label", "entry", "2d_image"]: + transform_group = QGroupBox("变换 Rect Transform") + else: + transform_group = QGroupBox("变换 Transform") + + transform_layout = QGridLayout() + pos = gui_element.getPos() # 根据GUI类型决定位置编辑方式 - if gui_type in ["button", "label", "entry"]: + if gui_type in ["button", "label", "entry","2d_image"]: # 2D GUI组件使用屏幕坐标 logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 logical_z = pos.getZ() / 0.1 - + + transform_layout.addWidget(QLabel("屏幕位置"), 0, 0) + + x_label = QLabel("X") + z_label = QLabel("z") + x_label.setAlignment(Qt.Aligncenter) + z_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(x_label, 0, 1) + transform_layout.addWidget(z_label, 0, 2) + xPos = QDoubleSpinBox() xPos.setRange(-50, 50) xPos.setValue(logical_x) - xPos.valueChanged.connect(lambda v: self.editGUI2DPosition(gui_element, "x", v)) - layout.addRow("屏幕位置 X:", xPos) - + xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) + transform_layout.addWidget(xPos, 1, 1) + zPos = QDoubleSpinBox() zPos.setRange(-50, 50) zPos.setValue(logical_z) - zPos.valueChanged.connect(lambda v: self.editGUI2DPosition(gui_element, "z", v)) - layout.addRow("屏幕位置 Z:", zPos) - + zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) + transform_layout.addWidget(zPos, 1, 2) + # 显示实际屏幕坐标(只读) + transform_layout.addWidget(QLabel("实际坐标"), 2, 0) + actualXLabel = QLabel(f"{pos.getX():.3f}") actualXLabel.setStyleSheet("color: gray; font-size: 10px;") - layout.addRow("实际屏幕 X:", actualXLabel) - actualZLabel = QLabel(f"{pos.getZ():.3f}") actualZLabel.setStyleSheet("color: gray; font-size: 10px;") - layout.addRow("实际屏幕 Z:", actualZLabel) + + transform_layout.addWidget(actualXLabel, 3, 1) + transform_layout.addWidget(actualZLabel, 3, 2) + # 添加宽度和高度控件(对于2D图像) + if gui_type == "2d_image": + + # 添加排序控制组 + sort_group = QGroupBox("渲染顺序") + sort_layout = QGridLayout() + + # 获取当前的sort值,如果没有设置则默认为0 + current_sort = int(gui_element.getTag("sort") or "0") + + sort_layout.addWidget(QLabel("层级:"), 0, 0) + + sort_spin = QSpinBox() + sort_spin.setRange(-1000, 1000) # 设置合理的范围 + sort_spin.setValue(current_sort) + + # 创建更新排序的函数 + def updateSort(value): + try: + # 设置标签 + gui_element.setTag("sort", str(value)) + # 应用sort到节点 - 使用fixed bin和指定的值 + gui_element.setBin("fixed", value) + print(f"✓ 更新2D图像渲染顺序: {value}") + except Exception as e: + print(f"✗ 更新2D图像渲染顺序失败: {e}") + + sort_spin.valueChanged.connect(updateSort) + sort_layout.addWidget(sort_spin, 0, 1) + + # 添加说明标签 + sort_help = QLabel("(数值越大越靠前)") + sort_help.setStyleSheet("font-size: 10px; color: gray;") + sort_layout.addWidget(sort_help, 1, 0, 1, 2) + + sort_group.setLayout(sort_layout) + layout.addWidget(sort_group) + + scale = gui_element.getScale() + width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, + (tuple, list)) else scale + height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, + (tuple, list)) and len( + scale) > 1 else scale + + # 宽度控件 + transform_layout.addWidget(QLabel("宽度"), 4, 0) + widthSpinBox = QDoubleSpinBox() + widthSpinBox.setRange(0.1, 10) + widthSpinBox.setSingleStep(0.1) + widthSpinBox.setValue(width) + widthSpinBox.valueChanged.connect( + lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + transform_layout.addWidget(widthSpinBox, 4, 1) + + # 高度控件 + transform_layout.addWidget(QLabel("高度"), 4, 2) + heightSpinBox = QDoubleSpinBox() + heightSpinBox.setRange(0.1, 10) + heightSpinBox.setSingleStep(0.1) + heightSpinBox.setValue(height) + heightSpinBox.valueChanged.connect( + lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + transform_layout.addWidget(heightSpinBox, 4, 3) else: # 3D GUI组件使用世界坐标 + transform_layout.addWidget(QLabel("位置"), 0, 0) + + # X, Y, Z 标签居中 + x_label = QLabel("X") + y_label = QLabel("Y") + z_label = QLabel("Z") + x_label.setAlignment(Qt.AlignCenter) + y_label.setAlignment(Qt.AlignCenter) + z_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(x_label, 0, 1) + transform_layout.addWidget(y_label, 0, 2) + transform_layout.addWidget(z_label, 0, 3) + + # 位置数值输入框 xPos = QDoubleSpinBox() - xPos.setRange(-1000, 1000) + xPos.setRange(-100, 100) xPos.setValue(pos.getX()) - xPos.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "position", [v, pos.getY(), pos.getZ()])) - layout.addRow("位置 X:", xPos) - + xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) + transform_layout.addWidget(xPos, 1, 1) + yPos = QDoubleSpinBox() - yPos.setRange(-1000, 1000) + yPos.setRange(-100, 100) yPos.setValue(pos.getY()) - yPos.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "position", [pos.getX(), v, pos.getZ()])) - layout.addRow("位置 Y:", yPos) - + yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) + transform_layout.addWidget(yPos, 1, 2) + zPos = QDoubleSpinBox() - zPos.setRange(-1000, 1000) + zPos.setRange(-100, 100) zPos.setValue(pos.getZ()) - zPos.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "position", [pos.getX(), pos.getY(), v])) - layout.addRow("位置 Z:", zPos) + zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) + transform_layout.addWidget(zPos, 1, 3) + + # 缩放属性 + scale = gui_element.getScale() + transform_layout.addWidget(QLabel("缩放"), 2, 0) + + # X, Y, Z 缩放标签居中 + sx_label = QLabel("X") + sy_label = QLabel("Y") + sz_label = QLabel("Z") + sx_label.setAlignment(Qt.AlignCenter) + sy_label.setAlignment(Qt.AlignCenter) + sz_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(sx_label, 2, 1) + transform_layout.addWidget(sy_label, 2, 2) + transform_layout.addWidget(sz_label, 2, 3) + + # 缩放数值输入框 + scale_x = QDoubleSpinBox() + scale_x.setRange(0.01, 10) + scale_x.setSingleStep(0.1) + scale_x.setValue( + scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) + scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + transform_layout.addWidget(scale_x, 3, 1) + + scale_y = QDoubleSpinBox() + scale_y.setRange(0.01, 10) + scale_y.setSingleStep(0.1) + scale_y.setValue( + scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( + scale) > 1 else scale) + scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) + transform_layout.addWidget(scale_y, 3, 2) + + scale_z = QDoubleSpinBox() + scale_z.setRange(0.01, 10) + scale_z.setSingleStep(0.1) + scale_z.setValue( + scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( + scale) > 2 else scale) + scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + transform_layout.addWidget(scale_z, 3, 3) + transform_group.setLayout(transform_layout) + self._propertyLayout.addWidget(transform_group) # 缩放属性 if hasattr(gui_element, 'getScale'): @@ -1136,24 +1413,24 @@ class GUIManager: 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 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 @@ -1190,7 +1467,164 @@ class GUIManager: except Exception as e: print(f"❌ 更新纹理时出错: {e}") - # 替换现有的 createCesiumView 方法 + def update2DImageTexture(self, gui_element, image_path): + """更新2D图片纹理""" + try: + # 加载新纹理 + new_texture = self.world.loader.loadTexture(image_path) + if new_texture: + new_texture.setMagfilter(Texture.FT_linear) + new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) + + new_texture.setFormat(Texture.F_rgba) + new_texture.setWrapU(Texture.WM_clamp) + new_texture.setWrapV(Texture.WM_clamp) + + # 应用纹理到模型 + gui_element.setTexture(new_texture, 1) + + # 更新标签 + gui_element.setTag("texture_path", image_path) + gui_element.setTag("image_path", image_path) + + print(f"✅ 2D图像纹理已更新为: {image_path}") + return True + else: + print(f"❌ 无法加载2D图片纹理: {image_path}") + return False + except Exception as e: + print(f"❌ 更新2D图片纹理时出错: {e}") + return False + + # 在gui_manager.py或其他相关文件中添加以下方法 + + def editGUI2DPosition(self, gui_element, axis, value): + """编辑2D GUI元素位置并应用边界约束""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry", "2d_image"]: + # 2D元素使用屏幕坐标,需要转换 + current_pos = gui_element.getPos() + + if axis == "x": + # 转换逻辑坐标到屏幕坐标 + screen_x = value * 0.1 + # 应用边界约束 + constrained_pos = self.constrain2DPosition(gui_element, new_x=screen_x) + new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ()) + elif axis == "z": + screen_z = value * 0.1 + # 应用边界约束 + constrained_pos = self.constrain2DPosition(gui_element, new_z=screen_z) + new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ()) + else: + return False + + gui_element.setPos(*new_pos) + print(f"✓ 更新2D GUI元素位置: {axis}={value} (约束后位置: {new_pos})") + return True + else: + print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") + return False + except Exception as e: + print(f"✗ 更新2D GUI元素位置失败: {e}") + import traceback + traceback.print_exc() + return False + + def editGUI3DPosition(self, gui_element, axis, value): + """编辑3D GUI元素位置""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["3d_text", "3d_image"]: + current_pos = gui_element.getPos() + + if axis == "x": + new_pos = (value, current_pos.getY(), current_pos.getZ()) + elif axis == "y": + new_pos = (current_pos.getX(), value, current_pos.getZ()) + elif axis == "z": + new_pos = (current_pos.getX(), current_pos.getY(), value) + else: + return False + + gui_element.setPos(*new_pos) + print(f"✓ 更新3D GUI元素位置: {axis}={value}") + return True + else: + print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}") + return False + except Exception as e: + print(f"✗ 更新3D GUI元素位置失败: {e}") + import traceback + traceback.print_exc() + return False + + def editGUIScale(self, gui_element, axis, value): + """编辑GUI元素缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 确保缩放值不为0 + if value == 0: + value = 0.01 + + if gui_type in ["3d_text", "3d_image"]: + # 3D元素处理 + if axis == "x": + new_scale = (value, current_scale.getY(), current_scale.getZ()) + elif axis == "y": + new_scale = (current_scale.getX(), value, current_scale.getZ()) + elif axis == "z": + new_scale = (current_scale.getX(), current_scale.getY(), value) + else: + return False + + gui_element.setScale(*new_scale) + elif gui_type == "2d_image": + # 2D图像特殊处理 - 分别控制宽度和高度 + if axis == "x": + # X轴控制宽度 + gui_element.setScale(value, current_scale.getZ() if hasattr(current_scale, 'getZ') + else current_scale[1] if isinstance(current_scale, (list, tuple)) + else current_scale) + elif axis == "z": + # Z轴控制高度 + gui_element.setScale(current_scale.getX() if hasattr(current_scale, 'getX') + else current_scale[0] if isinstance(current_scale, (list, tuple)) + else current_scale, value) + else: + # 其他情况使用统一缩放 + gui_element.setScale(value) + gui_element.setTransparency(TransparencyAttrib.MAlpha) + else: + # 其他2D元素处理 + if axis in ["x", "z"]: # 对于2D图像,x和z分别代表宽度和高度 + # 保持原有缩放比例,仅调整指定轴 + if axis == "x": + gui_element.setScale(value, + current_scale.getZ() if hasattr(current_scale, 'getZ') + else current_scale[1] if isinstance(current_scale, (list, tuple)) + else current_scale) + elif axis == "z": + gui_element.setScale( + current_scale.getX() if hasattr(current_scale, 'getX') + else current_scale[0] if isinstance(current_scale, (list, tuple)) + else current_scale, value) + else: + # 对于其他2D元素,使用统一缩放 + gui_element.setScale(value) + + print(f"✓ 更新GUI元素缩放: {axis}={value}") + return True + except Exception as e: + print(f"✗ 更新GUI元素缩放失败: {e}") + import traceback + traceback.print_exc() + return False def createCesiumView(self, main_window=None): """创建 Cesium 视图窗口(离线版本)""" diff --git a/main.py b/main.py index 2ca74757..610c7abf 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,7 @@ from core.vr_manager import VRManager from core.vr_input_handler import VRInputHandler from core.alvr_streamer import ALVRStreamer from gui.gui_manager import GUIManager +from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager from project.project_manager import ProjectManager from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget @@ -84,6 +85,11 @@ class MyWorld(CoreWorld): self.script_manager.start_system() #self.material_editor = None + self.terrain_manager = TerrainManager(self) + + self.terrain_edit_radius = 3.0 + self.terrain_edit_strength=0.3 + self.terrain_edit_operation = "add" print("✓ MyWorld 初始化完成") @@ -164,6 +170,17 @@ class MyWorld(CoreWorld): """树形控件的兼容性设置器""" self.interface_manager.treeWidget = value + #保留terrains属性以兼容现有代码 + @property + def terrains(self): + """地形列表的兼容性属性""" + return self.terrain_manager.terrains + + @terrains.setter + def terrains(self,value): + """地形列表的兼容性设置器""" + self.terrain_manager.terrains = value + # ==================== GUI管理功能代理 ==================== # GUI元素创建方法 - 代理到gui_manager @@ -187,11 +204,15 @@ class MyWorld(CoreWorld): """创建3D图片""" return self.gui_manager.createGUI3DImage(pos,text,size) - def createSpotLight(self,pos=(-20,0,5)): + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + """创建2D GUI图片""" + return self.gui_manager.createGUI2DImage(pos, image_path, size) + + def createSpotLight(self,pos=(0,0,5)): """创建聚光灯""" return self.scene_manager.createSpotLight(pos) - def createPointLight(self,pos=(20,0,5)): + def createPointLight(self,pos=(0,0,5)): """创建点光源""" return self.scene_manager.createPointLight(pos) @@ -686,6 +707,46 @@ class MyWorld(CoreWorld): else: return self.scene_manager.load_cesium_tileset(url,position) + # ==================== 地形管理功能代理 ==================== + # 地形创建方法 - 代理到terrain_manager + def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): + """从高度图创建地形""" + return self.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale) + + def createFlatTerrain(self, size=(0.3, 0.3), resolution=256): + """创建平面地形""" + return self.terrain_manager.createFlatTerrain(size, resolution) + + def updateTerrain(self): + """更新所有地形的LOD""" + return self.terrain_manager.updateTerrain() + + def getTerrainHeight(self, terrain_info, x, y): + """获取地形上指定点的高度""" + return self.terrain_manager.getTerrainHeight(terrain_info, x, y) + + def setTerrainEditParameters(self,radius=None,strength=None,operation=None): + if radius is not None: + self.terrain_edit_radius = float(radius) + if strength is not None: + self.terrain_edit_strenght = float(strength) + if operation is not None: + self.terrain_edit_operation = operation + print(f"地形编辑参数已更新: 半径={self.terrain_edit_radius}, 强度={self.terrain_edit_strength}, 操作={self.terrain_edit_operation}") + def getTerrainEditParameters(self): + """获取当前地形编辑参数""" + return { + 'radius': self.terrain_edit_radius, + 'strength': self.terrain_edit_strength, + 'operation': self.terrain_edit_operation + } + + def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"): + """修改地形高度的便捷方法""" + if hasattr(self, 'terrain_manager'): + return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation) + return False + # ==================== 项目管理功能代理 ==================== # 以下函数代理到project_manager模块的对应功能 @@ -716,17 +777,6 @@ def buildPackage(appw): return world.project_manager.buildPackage(appw) -# def open_material_editor(self): -# """打开材质编辑器作为子窗口""" -# if not self.material_editor: -# self.material_editor = MaterialEditor() -# # 设置为子窗口 -# self.material_editor.setParent(self) -# self.material_editor.setWindowFlags(Qt.Window) -# -# self.material_editor.show() -# - if __name__ == "__main__": world = MyWorld() diff --git a/scene/scene_manager.py b/scene/scene_manager.py index a3ae792a..3c419e7b 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -667,20 +667,20 @@ class SceneManager: cNode = CollisionNode(f'modelCollision_{model.getName()}') # 设置碰撞掩码 cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位作为模型的碰撞掩码 - + # 获取模型的边界 bounds = model.getBounds() center = bounds.getCenter() radius = bounds.getRadius() - + # 添加碰撞球体 cSphere = CollisionSphere(center, radius) cNode.addSolid(cSphere) - + # 将碰撞节点附加到模型上 cNodePath = model.attachNewNode(cNode) #cNodePath.hide() - # cNodePath.show() # 取消注释可以显示碰撞体,用于调试 + #cNodePath.show() # 取消注释可以显示碰撞体,用于调试 # ==================== 场景树管理 ==================== @@ -943,34 +943,27 @@ class SceneManager: render_pipeline = get_render_pipeline() - # 创建一个挂载节点(你控制的) - light_np = NodePath("SpotlightAttachNode") + # 为每个灯光创建独立的节点 + light_np = NodePath(f"Spotlight_{len(self.Spotlight)}") light_np.reparentTo(self.world.render) - #light_np.setPos(*pos) - - self.half_energy = 5000 - self.lamp_fov = 70 - self.lamp_radius = 1000 + light_np.setPos(*pos) + # 创建独立的灯光对象 light = SpotLight() light.direction = Vec3(0, 0, -1) # 光照方向 - light.fov = self.lamp_fov # 光源角度(类似手电筒) + light.fov = self.lamp_fov if hasattr(self, 'lamp_fov') else 70 # 光源角度 light.set_color_from_temperature(5 * 1000.0) # 色温(K) - light.energy = self.half_energy # 光照强度 - light.radius = self.lamp_radius # 影响范围 + light.energy = self.half_energy if hasattr(self, 'half_energy') else 5000 # 光照强度 + light.radius = self.lamp_radius if hasattr(self, 'lamp_radius') else 1000 # 影响范围 light.casts_shadows = True # 是否投射阴影 light.shadow_map_resolution = 256 # 阴影分辨率 light.setPos(*pos) - #light_np.setPos(*pos) - #light_np = render_pipeline.add_light(light, parent=self.world.render) render_pipeline.add_light(light) # 添加到渲染管线 - light_name = f"Spotlight_{len(self.Spotlight)}" - - light_np.setName(light_name) # 设置唯一名称 - #light_np.reparentTo(self.world.render) # 挂载到场景根节点 + #light_name = f"Spotlight_{len(self.Spotlight)}" + #light_np.setName(light_name) # 设置唯一名称 light_np.setTag("light_type", "spot_light") light_np.setTag("is_scene_element", "1") light_np.setTag("light_energy", str(light.energy)) @@ -982,7 +975,7 @@ class SceneManager: if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() - #print("nikan"+light_np.getHpr()) + return light,light_np def createPointLight(self, pos=(0, 0, 0)): from RenderPipelineFile.rpcore import PointLight, RenderPipeline @@ -990,14 +983,14 @@ class SceneManager: render_pipeline = get_render_pipeline() - # 创建一个挂载节点(你控制的) - light_np = NodePath("PointlightAttachNode") + # 为每个灯光创建独立的节点 + light_np = NodePath(f"Pointlight_{len(self.Pointlight)}") light_np.reparentTo(self.world.render) + light_np.setPos(*pos) - + # 创建独立的灯光对象 light = PointLight() light.setPos(*pos) - light_np.setPos(*pos) light.energy = 5000 light.radius = 1000 light.inner_radius = 0.4 @@ -1005,30 +998,27 @@ class SceneManager: light.casts_shadows = True # 是否投射阴影 light.shadow_map_resolution = 256 # 阴影分辨率 - render_pipeline.add_light(light) # 添加到渲染管线 - - light_name = f"Pointlight{len(self.Pointlight)}" - - light_np.setName(light_name) # 设置唯一名称 - - #light_np = NodePath(f"PointLight_{len(self.Pointlight)}") - #light_np.reparentTo(self.world.render) - #light_np.setPos(*pos) + # 添加到渲染管线 + render_pipeline.add_light(light) + # 设置标签和Python标签 light_np.setTag("light_type", "point_light") light_np.setTag("is_scene_element", "1") light_np.setTag("light_energy", str(light.energy)) - # 保存光源对象引用(重要!用于属性面板) + # 保存光源对象引用(重要!用于属性面板和删除操作) light_np.setPythonTag("rp_light_object", light) + # 添加到灯光列表 self.Pointlight.append(light_np) + # 更新场景树 if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() - return light,light_np - + print(f"创建点光源: {light_np.getName()}") + return light, light_np + # ==================== GLB 转换方法 ==================== def _shouldConvertToGLB(self, filepath): @@ -1486,42 +1476,66 @@ except Exception as e: normal = GeomVertexWriter(vdata, 'normal') color = GeomVertexWriter(vdata, 'color') - # 定义立方体顶点(稍微大一些,便于识别) + # 定义立方体顶点 size = 1.0 vertices = [ - (-size, -size, -size), (size, -size, -size), (size, size, -size), (-size, size, -size), - (-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size) + # 前面 (Z+) + (-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size), + # 后面 (Z-) + (-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size), + # 左面 (X-) + (-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size), + # 右面 (X+) + (size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size), + # 上面 (Y+) + (-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size), + # 下面 (Y-) + (-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size) ] - # 使用更鲜明的颜色 - for vert in vertices: + normals = [ + # 前面法线 + (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), + # 后面法线 + (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), + # 左面法线 + (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), + # 右面法线 + (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), + # 上面法线 + (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), + # 下面法线 + (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0) + ] + + # 青色 + face_colors = [ + (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色 + (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色 + (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色 + (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色 + (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色 + (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色 + ] + + for i, vert in enumerate(vertices): vertex.addData3f(*vert) - normal.addData3f(0, 0, 1) - color.addData4f(0.0, 1.0, 1.0, 1.0) # 青色 + normal.addData3f(*normals[i]) + color.addData4f(*face_colors[i]) # 创建几何体 geom = Geom(vdata) - # 创建面 + # 创建面(每个面两个三角形) prim = GeomTriangles(Geom.UHStatic) - # 底面 - prim.addVertices(0, 1, 2) - prim.addVertices(0, 2, 3) - # 顶面 - prim.addVertices(4, 7, 6) - prim.addVertices(4, 6, 5) - # 前面 - prim.addVertices(0, 4, 5) - prim.addVertices(0, 5, 1) - # 后面 - prim.addVertices(2, 6, 7) - prim.addVertices(2, 7, 3) - # 左面 - prim.addVertices(0, 3, 7) - prim.addVertices(0, 7, 4) - # 右面 - prim.addVertices(1, 5, 6) - prim.addVertices(1, 6, 2) + + # 每个面4个顶点,生成2个三角形 + for face in range(6): # 6个面 + base_index = face * 4 + # 第一个三角形 + prim.addVertices(base_index, base_index + 1, base_index + 2) + # 第二个三角形 + prim.addVertices(base_index, base_index + 2, base_index + 3) prim.closePrimitive() geom.addPrimitive(prim) @@ -1532,7 +1546,10 @@ except Exception as e: # 添加到场景 cube_node = parent_node.attachNewNode(geom_node) - cube_node.setScale(5) # 适当大小 + cube_node.setScale(5) # 设置合适的大小 + + # 设置双面渲染 + cube_node.setTwoSided(True) # 添加材质 material = Material() diff --git a/test_axis_fix.py b/test_axis_fix.py deleted file mode 100644 index 8331e606..00000000 --- a/test_axis_fix.py +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -坐标轴修复验证脚本 -用于测试鼠标悬停高亮和拖动一致性的修复效果 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel -from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld -from panda3d.core import * - -class AxisTestWorld(Panda3DWorld): - def __init__(self): - super().__init__() - - print("=== 坐标轴修复验证测试 ===") - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 调整相机位置 - self.cam.setPos(8, -15, 8) - self.cam.lookAt(0, 0, 0) - - # Qt部件引用 - self.qtWidget = None - - # 基础光照 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - # 创建测试立方体 - self.createTestCube() - - # 设置当前工具 - self.currentTool = "移动" - - # 模拟选择系统和工具管理器 - from core.selection import SelectionSystem - self.selection = SelectionSystem(self) - - # 简单的工具管理器模拟 - class SimpleToolManager: - def isScaleTool(self): - return False - def isRotateTool(self): - return False - - self.tool_manager = SimpleToolManager() - - # 模拟属性面板 - class SimplePropertyPanel: - def refreshModelValues(self, node): - print(f"属性更新: {node.getName()} 位置={node.getPos()}") - - self.property_panel = SimplePropertyPanel() - - # 自动选中立方体 - self.selectedNode = self.testCube - self.selection.updateSelection(self.testCube) - - print("测试说明:") - print("1. 鼠标悬停在坐标轴上应该变为黄色高亮") - print("2. 点击并拖拽坐标轴,立方体应该沿对应轴移动") - print("3. 拖动行为应该与鼠标移动方向一致") - - def setQtWidget(self, widget): - """设置Qt部件引用""" - self.qtWidget = widget - - def getWindowSize(self): - """获取准确的窗口尺寸""" - if self.qtWidget: - width, height = self.qtWidget.getActualSize() - if width > 0 and height > 0: - return width, height - - if hasattr(self, 'win') and self.win: - width = self.win.getXSize() - height = self.win.getYSize() - return width, height - - return 800, 600 - - def createTestCube(self): - """创建测试立方体""" - from panda3d.core import CardMaker - - cm = CardMaker('cube') - cm.setFrame(-1, 1, -1, 1) - - self.testCube = self.render.attachNewNode("testCube") - - # 6个面,不同颜色 - faces = [ - (Point3(0, 1, 0), Vec3(0, 0, 0), (1, 0.5, 0.5, 1)), # 前面 - (Point3(0, -1, 0), Vec3(0, 0, 180), (0.5, 1, 0.5, 1)), # 后面 - (Point3(-1, 0, 0), Vec3(0, 0, 90), (0.5, 0.5, 1, 1)), # 左面 - (Point3(1, 0, 0), Vec3(0, 0, -90), (1, 1, 0.5, 1)), # 右面 - (Point3(0, 0, 1), Vec3(-90, 0, 0), (1, 0.5, 1, 1)), # 顶面 - (Point3(0, 0, -1), Vec3(90, 0, 0), (0.5, 1, 1, 1)) # 底面 - ] - - for pos, hpr, color in faces: - face = self.testCube.attachNewNode(cm.generate()) - face.setPos(pos) - face.setHpr(hpr) - face.setColor(*color) - - print(f"✓ 创建测试立方体,位置: {self.testCube.getPos()}") - return self.testCube - - def mousePressEventLeft(self, evt): - """处理鼠标左键按下事件""" - if not evt: - return - - x = evt.get('x', 0) - y = evt.get('y', 0) - - # 检查坐标轴点击 - if self.selection.gizmo: - gizmoAxis = self.selection.checkGizmoClick(x, y) - if gizmoAxis: - print(f"✓ 坐标轴点击检测成功: {gizmoAxis}") - self.selection.startGizmoDrag(gizmoAxis, x, y) - return - else: - print("× 坐标轴点击检测失败") - - def mouseReleaseEventLeft(self, evt): - """处理鼠标左键释放事件""" - if self.selection.isDraggingGizmo: - self.selection.stopGizmoDrag() - - def mouseMoveEvent(self, evt): - """处理鼠标移动事件""" - if not evt: - return - - x = evt.get('x', 0) - y = evt.get('y', 0) - - # 处理坐标轴拖拽 - if self.selection.isDraggingGizmo: - self.selection.updateGizmoDrag(x, y) - return - - # 更新坐标轴高亮 - if self.selection.gizmo and not self.selection.isDraggingGizmo: - self.selection.updateGizmoHighlight(x, y) - - -class TestWidget(QPanda3DWidget): - """测试部件""" - def __init__(self, world, parent=None): - super().__init__(world, parent) - self.world = world - - if hasattr(world, 'setQtWidget'): - world.setQtWidget(self) - - def getActualSize(self): - return (self.width(), self.height()) - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton: - self.world.mousePressEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseReleaseEvent(self, event): - if event.button() == Qt.LeftButton: - self.world.mouseReleaseEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseMoveEvent(self, event): - self.world.mouseMoveEvent({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - -def main(): - print("启动坐标轴修复验证测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("坐标轴修复验证测试") - mainWindow.setGeometry(100, 100, 900, 700) - - # 创建布局 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加说明 - label = QLabel("坐标轴修复验证测试 - 测试鼠标悬停高亮和拖动一致性") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 12px;") - layout.addWidget(label) - - # 创建世界和部件 - world = AxisTestWorld() - pandaWidget = TestWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - mainWindow.show() - - print("\n修复验证项目:") - print("1. ✓ 鼠标悬停时坐标轴应该正确高亮为黄色") - print("2. ✓ 点击坐标轴应该能正确开始拖拽") - print("3. ✓ 拖拽方向应该与鼠标移动方向一致") - print("4. ✓ 减少了控制台调试输出") - - return app.exec_() - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/ui/interface_manager.py b/ui/interface_manager.py index 9797f3c2..bca64749 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu +from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu, QStyle from PyQt5.QtCore import Qt from PyQt5.sip import delete from panda3d.core import GeomNode, ModelRoot @@ -137,6 +137,22 @@ class InterfaceManager: def deleteNode(self, nodePath, item): """删除节点""" try: + item_data = item.data(0,Qt.UserRole+1) + if item_data == "terrain": + 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: + terrain_to_remove = terrain_info + break + if terrain_to_remove: + self.world.terrain_manager.deleteTerrain(terrain_to_remove) + print(f"成功删除地形节点:{nodePath.getName()}") + self.updateSceneTree() + self.world.property_panel.clearPropertyPanel() + self.world.selection.updateSelection(None) + return + # 从场景中移除 self.world.property_panel.removeActorForModel(nodePath) @@ -199,15 +215,6 @@ class InterfaceManager: cameraItem.setData(0, Qt.UserRole, self.world.cam) print("添加相机节点") - # # 添加模型节点组 - # modelsItem = QTreeWidgetItem(sceneRoot, ['模型']) - # print(f"模型列表中的模型数量: {len(self.world.models)}") - # - # # 添加GUI元素节点组 - # guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素']) - # - # lightItem = QTreeWidgetItem(sceneRoot,['灯光']) - BLACK_LIST = {'','**','temp','collision'} from panda3d.core import CollisionNode @@ -216,6 +223,9 @@ class InterfaceManager: return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name=="" def addNodeToTree(node,parentItem,force = False): + if node.isEmpty(): + return + if not force and should_skip(node): return nodeItem = QTreeWidgetItem(parentItem,[node.getName()]) @@ -224,52 +234,53 @@ class InterfaceManager: for child in node.getChildren(): addNodeToTree(child,nodeItem,force = False) - # 递归添加节点及其子节点 - # def addNodeToTree(node, parentItem): - # print(f"\n处理节点: {node.getName()}") - # # 创建节点项 - # nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) - # nodeItem.setData(0, Qt.UserRole, node) - # print(f"添加节点: {node.getName()}") - # - # # 递归处理所有子节点 - # for child in node.getChildren(): - # # 检查是否是有效的模型节点 - # if (isinstance(child.node(), GeomNode) or - # child.hasTag("file") or - # child.getName() == "RootNode" or - # isinstance(child.node(), ModelRoot)): - # print(f"处理子节点: {child.getName()}") - # addNodeToTree(child, nodeItem) - # else: - # print(f"跳过节点: {child.getName()}") - for model in self.world.models: - addNodeToTree(model, sceneRoot,force=True) + if not model.isEmpty(): + addNodeToTree(model, sceneRoot,force=True) # 添加所有GUI元素 for gui in self.world.gui_elements: - gui_type = gui.getTag("gui_type") or "unknown" - gui_text = gui.getTag("gui_text") or "GUI元素" - item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"]) - item.setData(0, Qt.UserRole, gui) + # 检查是否是有效的GUI节点(具有getTag方法的NodePath) + if hasattr(gui, 'getTag') and hasattr(gui, 'getName'): + gui_type = gui.getTag("gui_type") or "unknown" + gui_text = gui.getTag("text") or gui.getTag("gui_text") or "GUI元素" + item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"]) + item.setData(0, Qt.UserRole, gui) + else: + # 跳过非GUI节点(如QDockWidget等Qt对象) + continue + + # 添加地板节点 + if hasattr(self.world, 'ground') and self.world.ground: + groundItem = QTreeWidgetItem(sceneRoot, ['地板']) + groundItem.setData(0, Qt.UserRole, self.world.ground) + #添加灯光节点 for light in self.world.Spotlight + self.world.Pointlight: - addNodeToTree(light, sceneRoot, force=True) + if not light.isEmpty: + addNodeToTree(light, sceneRoot, force=True) #添加 Cesium tilesets if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'): for i , tileset_info in enumerate(self.world.scene_manager.tilesets): tileset_node = tileset_info['node'] - tileset_url = tileset_info['url'] - tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"]) - tileset_item.setData(0,Qt.UserRole,tileset_node) - addNodeToTree(tileset_node,tileset_item,force=True) + if not tileset_node.isEmpty(): + tileset_url = tileset_info['url'] + tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"]) + tileset_item.setData(0,Qt.UserRole,tileset_node) + addNodeToTree(tileset_node,tileset_item,force=True) + #添加地形节点 + if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains: + for terrain_info in self.world.terrain_manager.terrains: + terrain_node = terrain_info['node'] + terrain_name = terrain_info.get('name','未知地形') + terrain_item = QTreeWidgetItem(sceneRoot,[f"地形:{terrain_name}"]) + terrain_item.setData(0,Qt.UserRole,terrain_node) + terrain_item.setData(0,Qt.UserRole+1,"terrain") # 标记为地形节点 + addNodeToTree(terrain_node,terrain_item,force=True) + + - # 添加地板节点 - if hasattr(self.world, 'ground') and self.world.ground: - groundItem = QTreeWidgetItem(sceneRoot, ['地板']) - groundItem.setData(0, Qt.UserRole, self.world.ground) # 展开所有节点 #self.treeWidget.expandAll() diff --git a/ui/main_window.py b/ui/main_window.py index cf4e5f4d..5b8457ab 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -12,7 +12,8 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout, - QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget,QDialog) + QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog, + QSpinBox) from PyQt5.QtCore import Qt, QDir, QTimer from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget @@ -115,6 +116,13 @@ class MainWindow(QMainWindow): 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('地形编辑模式') + # GUI菜单 self.guiMenu = menubar.addMenu('GUI') self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式') @@ -275,6 +283,10 @@ class MainWindow(QMainWindow): self.create3DImageTool.setText("3D图片") self.toolbar.addWidget(self.create3DImageTool) + self.create2DImageTool = QToolButton() + self.create2DImageTool.setText("2D图片") + self.toolbar.addWidget(self.create2DImageTool) + self.createSpotLight = QToolButton() self.createSpotLight.setText("聚光灯") self.toolbar.addWidget(self.createSpotLight) @@ -299,6 +311,21 @@ class MainWindow(QMainWindow): self.addModelTool.clicked.connect(self.onAddModelClicked) self.toolbar.addWidget(self.addModelTool) + #地形 + # 地形工具 + self.createFlatTerrainTool = QToolButton() + self.createFlatTerrainTool.setText("平面地形") + self.toolbar.addWidget(self.createFlatTerrainTool) + + self.createHeightmapTerrainTool = QToolButton() + 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("选择") @@ -458,14 +485,25 @@ class MainWindow(QMainWindow): self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) + + #连接地形创建事件 + self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain) + self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain) + self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode) # 连接工具栏GUI创建按钮事件 self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton()) self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel()) self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText()) self.create3DImageTool.clicked.connect(lambda: self.world.createGUI3DImage()) + self.create2DImageTool.clicked.connect(lambda: self.world.createGUI2DImage()) self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight()) self.createPointLight.clicked.connect(lambda :self.world.createPointLight()) + + # 连接工具栏地形创建按钮事件 + self.createFlatTerrainTool.clicked.connect(self.onCreateFlatTerrain) + self.createHeightmapTerrainTool.clicked.connect(self.onCreateHeightmapTerrain) + self.terrainEditTool.clicked.connect(self.onTerrainEditMode) # 连接树节点点击信号 # self.treeWidget.itemClicked.connect(self.world.onTreeItemClicked) @@ -955,7 +993,158 @@ class MainWindow(QMainWindow): print(f"⚠️ 关闭应用程序时出错: {e}") event.accept() # 即使出错也要关闭 + def onCreateFlatTerrain(self): + """创建平面地形""" + dialog = QDialog(self) + dialog.setWindowTitle("创建平面地形") + dialog.setModal(True) + dialog.resize(300,200) + layout = QVBoxLayout(dialog) + + width_layout = QHBoxLayout() + width_layout.addWidget(QLabel("宽度:")) + width_spin = QDoubleSpinBox() + width_spin.setRange(0,10000) + width_spin.setValue(0.3) + width_layout.addWidget(width_spin) + layout.addLayout(width_layout) + + # 高度 + height_layout = QHBoxLayout() + height_layout.addWidget(QLabel("高度:")) + height_spin = QDoubleSpinBox() + height_spin.setRange(0, 10000) + height_spin.setValue(0.3) + height_layout.addWidget(height_spin) + layout.addLayout(height_layout) + + # 分辨率 + resolution_layout = QHBoxLayout() + resolution_layout.addWidget(QLabel("分辨率:")) + resolution_spin = QSpinBox() + resolution_spin.setRange(16, 2048) + resolution_spin.setValue(256) + resolution_spin.setSingleStep(16) + resolution_layout.addWidget(resolution_spin) + layout.addLayout(resolution_layout) + + # 按钮 + button_layout = QHBoxLayout() + ok_button = QPushButton("创建") + cancel_button = QPushButton("取消") + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) + + # 连接信号 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框 + if dialog.exec_() == QDialog.Accepted: + width = width_spin.value() + height = height_spin.value() + resolution = resolution_spin.value() + + # 调用世界对象创建地形 + terrain_info = self.world.createFlatTerrain((width, height), resolution) + if terrain_info: + QMessageBox.information(self, "成功", "平面地形创建成功!") + else: + QMessageBox.warning(self, "错误", "平面地形创建失败!") + + def onCreateHeightmapTerrain(self): + """从高度图创建地形""" + file_path,_=QFileDialog.getOpenFileName( + self, + "选择高度图文件", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)" + ) + + if file_path: + #创建对话框获取地形参数 + dialog = QDialog(self) + dialog.setWindowTitle("设置地形参数") + dialog.setModal(True) + dialog.resize(300,250) + + layout = QVBoxLayout(dialog) + + x_scale_layout = QHBoxLayout() + x_scale_layout.addWidget(QLabel("X缩放:")) + x_scale_spin = QDoubleSpinBox() + x_scale_spin.setRange(0.1,1000) + x_scale_spin.setValue(0.3) + x_scale_spin.setSingleStep(10) + x_scale_layout.addWidget(x_scale_spin) + layout.addLayout(x_scale_layout) + + # Y缩放 + y_scale_layout = QHBoxLayout() + y_scale_layout.addWidget(QLabel("Y缩放:")) + y_scale_spin = QDoubleSpinBox() + y_scale_spin.setRange(0.1, 1000) + y_scale_spin.setValue(0.3) + y_scale_spin.setSingleStep(10) + y_scale_layout.addWidget(y_scale_spin) + layout.addLayout(y_scale_layout) + + # Z缩放 + z_scale_layout = QHBoxLayout() + z_scale_layout.addWidget(QLabel("Z缩放:")) + z_scale_spin = QDoubleSpinBox() + z_scale_spin.setRange(0.1, 1000) + z_scale_spin.setValue(50) + z_scale_spin.setSingleStep(5) + z_scale_layout.addWidget(z_scale_spin) + layout.addLayout(z_scale_layout) + + # 按钮 + button_layout = QHBoxLayout() + ok_button = QPushButton("创建") + cancel_button = QPushButton("取消") + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) + + # 连接信号 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框 + if dialog.exec_() == QDialog.Accepted: + x_scale = x_scale_spin.value() + y_scale = y_scale_spin.value() + z_scale = z_scale_spin.value() + + # 调用世界对象创建地形 + terrain_info = self.world.createTerrainFromHeightMap( + file_path, + (x_scale, y_scale, z_scale) + ) + if terrain_info: + QMessageBox.information(self, "成功", "高度图地形创建成功!") + 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() @@ -965,4 +1154,4 @@ def setup_main_window(world): main_window = MainWindow(world) main_window.show() - return app, main_window \ No newline at end of file + return app, main_window \ No newline at end of file diff --git a/ui/property_panel.py b/ui/property_panel.py index f195c5ae..b15098d6 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -1,14 +1,17 @@ +import os from collections import deque -from traceback import print_exc +from traceback import print_exc, print_last from types import new_class from typing import Hashable from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, - QVBoxLayout, QGroupBox, QGridLayout) + QVBoxLayout, QGroupBox, QGridLayout, QSpinBox) from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor +from idna import check_label +from jinja2.compiler import has_safe_repr from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup from scene import util @@ -22,6 +25,14 @@ class PropertyPanelManager: self._propertyLayout = None self._actor_cache={} + # 初始化地形编辑参数 + if not hasattr(self.world, 'terrain_edit_radius'): + self.world.terrain_edit_radius = 3.0 + if not hasattr(self.world, 'terrain_edit_strength'): + self.world.terrain_edit_strength = 0.3 + if not hasattr(self.world, 'terrain_edit_operation'): # 这里原来是 terrain_edit_opertaion + self.world.terrain_edit_operation = "add" + # 定义紧凑样式 self.compact_style = """ QDoubleSpinBox { @@ -90,6 +101,7 @@ class PropertyPanelManager: print("属性布局未设置或没有父部件!") return + self._cleanupAllReferences() self.clearPropertyPanel() # 应用紧凑样式到属性面板容器 @@ -141,27 +153,16 @@ class PropertyPanelManager: # 获取节点对象 model = item.data(0, Qt.UserRole) - if model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset": + if self._isTerrainNode(model,item): + self._showTerrainProperties(model,item) + elif model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset": self._showCesiumTilesetProperties(model,item) - # 检查是否是GUI元素 elif model and hasattr(model, 'getTag') and model.getTag("gui_type"): - # gui_type = model.getTag("gui_type") - # if gui_type == "3d_image": - # self._updateGUIImagePropertyPanel(model) - # else: - # self.updateGUIPropertyPanel(model) - # pass self.updateGUIPropertyPanel(model) - pass elif model and hasattr(model, 'getTag') and model.getTag("light_type"): self.updateLightPropertyPanel(model) - pass - # 如果找到模型,显示其属性 elif model: self._updateModelPropertyPanel(model) - pass - # 显示脚本属性 - # self._updateScriptPropertyPanel(model) self._propertyLayout.addStretch() @@ -172,6 +173,373 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() + def _isTerrainNode(self,node,item): + """检查是否是地形节点""" + item_data = item.data(0,Qt.UserRole+1) + if item_data == "terrain": + return True + + if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains: + for terrain_info in self.world.terrain_manager.terrains: + if terrain_info['node'] == node: + return True + return False + + def _showTerrainProperties(self,terrain_node,item): + """显示地形属性面板""" + try: + terrain_info = None + if hasattr(self.world,'terrain_manager'): + for info in self.world.terrain_manager.terrains: + if info['node'] == terrain_node: + terrain_info = info + break + if not terrain_info: + no_info_label = QLabel("未找到地形信息") + no_info_label.setStyleSheet("color:red;font-weight:bold;") + self._propertyLayout.addWidget(no_info_label) + return + + info_group = QGroupBox("地形信息") + info_layout = QGridLayout() + info_layout.addWidget(QLabel("名称:"),0,0) + name_label = QLabel(terrain_info.get('name','未知')) + info_layout.addWidget(name_label,0,1) + + info_layout.addWidget(QLabel("类型:"),1,0) + type_label = QLabel("高度图地形"if terrain_info.get('heightmap') else "平面地形") + info_layout.addWidget(type_label,1,1) + + if terrain_info.get('heightmap'): + info_layout.addWidget(QLabel("高度图:"),2,0) + heightmap_label = QLabel(os.path.basename(terrain_info['heightmap'])) + heightmap_label.setWordWrap(True) + info_layout.addWidget(heightmap_label,2,1) + info_group.setLayout(info_layout) + self._propertyLayout.addWidget(info_group) + + #变换属性 + self._updateTerrainTransformPanel(terrain_node) + #地形编辑控制面板 + self._createTerrainEditPanel(terrain_info) + #材质属性 + self._updateTerrainMaterialPanel(terrain_node,terrain_info) + + #删除按钮 + delete_btn = QPushButton("删除地形") + delete_btn.setStyleSheet(""" + QPushButton{ + background-color:#ff4444; + color:white; + border:none; + padding:8px; + border-radius:4px; + margin-top:10px + } + QPushButton:hover{ + background-color:#ff6666; + } + """) + delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) + self._propertyLayout.addWidget(delete_btn) + except Exception as e: + print(f"显示地形属性时出错: {e}") + import traceback + traceback.print_exc() + + def _updateTerrainTransformPanel(self,terrain_node): + """更新地形变化属性面板""" + try: + transform_group = QGroupBox("变换 Transform") + transform_layout = QGridLayout() + + pos = terrain_node.getPos() + scale = terrain_node.getScale() + + transform_layout.addWidget(QLabel("位置"),0,0) + + x_label = QLabel("X") + y_label = QLabel("Y") + z_label = QLabel("Z") + x_label.setAlignment(Qt.AlignCenter) + y_label.setAlignment(Qt.AlignCenter) + z_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(x_label,0,1) + transform_layout.addWidget(y_label,0,2) + transform_layout.addWidget(z_label,0,3) + + self.pos_x = QDoubleSpinBox() + self.pos_y = QDoubleSpinBox() + self.pos_z = QDoubleSpinBox() + + for pos_widget in [self.pos_x,self.pos_y,self.pos_z]: + pos_widget.setRange(-1000000.0,1000000.0) + self.pos_x.setValue(pos.getX()) + self.pos_y.setValue(pos.getY()) + self.pos_z.setValue(pos.getZ()) + + def updateXPosition(value): + terrain_node.setX(value) + self.refreshModelValues(terrain_node) + self.pos_x.valueChanged.connect(updateXPosition) + + def updateYPosition(value): + terrain_node.setY(value) + self.refreshModelValues(terrain_node) + self.pos_y.valueChanged.connect(updateYPosition) + + def updateZPosition(value): + terrain_node.setZ(value) + self.refreshModelValues(terrain_node) + self.pos_z.valueChanged.connect(updateZPosition) + + transform_layout.addWidget(self.pos_x,1,1) + transform_layout.addWidget(self.pos_y,1,2) + transform_layout.addWidget(self.pos_z,1,3) + + transform_layout.addWidget(QLabel("旋转"),2,0) + self.rot_h = QDoubleSpinBox() + self.rot_p = QDoubleSpinBox() + self.rot_r = QDoubleSpinBox() + + for rot_widget in [self.rot_h,self.rot_p,self.rot_r]: + rot_widget.setRange(-360,360) + + self.rot_h.setValue(terrain_node.getH()) + self.rot_p.setValue(terrain_node.getP()) + self.rot_r.setValue(terrain_node.getR()) + + self.rot_h.valueChanged.connect(lambda v:terrain_node.setH(v)) + self.rot_p.valueChanged.connect(lambda v:terrain_node.setP(v)) + self.rot_r.valueChanged.connect(lambda v:terrain_node.setR(v)) + + h_label = QLabel("H") + p_label = QLabel("P") + r_label = QLabel("R") + h_label.setAlignment(Qt.AlignCenter) + p_label.setAlignment(Qt.AlignCenter) + r_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(h_label,2,1) + transform_layout.addWidget(p_label,2,2) + transform_layout.addWidget(r_label,2,3) + transform_layout.addWidget(self.rot_h,3,1) + transform_layout.addWidget(self.rot_p,3,2) + transform_layout.addWidget(self.rot_r,3,3) + + transform_layout.addWidget(QLabel("缩放"),4,0) + self.scale_x = QDoubleSpinBox() + self.scale_y = QDoubleSpinBox() + self.scale_z = QDoubleSpinBox() + + for scale_widget in [self.scale_x,self.scale_y,self.scale_z]: + scale_widget.setRange(-1000,1000) + scale_widget.setSingleStep(0.1) + self.scale_x.setValue(scale.getX()) + 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)) + + x_label2 = QLabel("X") + y_label2 = QLabel("Y") + z_label2 = QLabel("Z") + x_label2.setAlignment(Qt.AlignCenter) + y_label2.setAlignment(Qt.AlignCenter) + z_label2.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(x_label2,4,1) + transform_layout.addWidget(y_label2,4,2) + transform_layout.addWidget(z_label2,4,3) + transform_layout.addWidget(self.scale_x,5,1) + transform_layout.addWidget(self.scale_y,5,2) + transform_layout.addWidget(self.scale_z,5,3) + + transform_group.setLayout(transform_layout) + self._propertyLayout.addWidget(transform_group) + except Exception as e: + print(f"更新地形变换面板时出错: {e}") + + def _createTerrainEditPanel(self, terrain_info): + try: + edit_group = QGroupBox("地形编辑") + edit_layout = QGridLayout() + + # 编辑半径 + edit_layout.addWidget(QLabel("编辑半径:"), 0, 0) + self.terrain_radius_spin = QDoubleSpinBox() + self.terrain_radius_spin.setRange(0.1, 50.0) + self.terrain_radius_spin.setSingleStep(0.5) + self.terrain_radius_spin.setValue(getattr(self.world, 'terrain_edit_radius', 3.0)) + self.terrain_radius_spin.valueChanged.connect(self._onTerrainRadiusChanged) + edit_layout.addWidget(self.terrain_radius_spin, 0, 1) + + # 编辑强度 + edit_layout.addWidget(QLabel("编辑强度:"), 1, 0) + self.terrain_strength_spin = QDoubleSpinBox() + self.terrain_strength_spin.setRange(0.01, 50) + self.terrain_strength_spin.setSingleStep(0.1) + self.terrain_strength_spin.setValue(getattr(self.world, 'terrain_edit_strength', 0.3)) + self.terrain_strength_spin.valueChanged.connect(self._onTerrainStrengthChanged) + edit_layout.addWidget(self.terrain_strength_spin, 1, 1) + + edit_layout.addWidget(QLabel("操作类型:"), 2, 0) + self.terrain_operation_combo = QComboBox() + self.terrain_operation_combo.addItems(["升高地形", "降低地形", "平坦化"]) + current_op = getattr(self.world, 'terrain_edit_operation', "add") + if current_op == "add": + self.terrain_operation_combo.setCurrentText("升高地形") + elif current_op == "subtract": + self.terrain_operation_combo.setCurrentText("降低地形") + else: + self.terrain_operation_combo.setCurrentText("平坦化") + self.terrain_operation_combo.currentTextChanged.connect(self._onTerrainOperationChanged) + edit_layout.addWidget(self.terrain_operation_combo, 2, 1) + + help_label = QLabel("在地形编辑模式下,使用鼠标左键升高地形,右键降低地形") + help_label.setWordWrap(True) + help_label.setStyleSheet("font-size:10px;color:gray;") + edit_layout.addWidget(help_label, 3, 0, 1, 2) + + edit_group.setLayout(edit_layout) + self._propertyLayout.addWidget(edit_group) + + # 确保初始值被正确设置到 world 对象上 + self._onTerrainRadiusChanged(self.terrain_radius_spin.value()) + self._onTerrainStrengthChanged(self.terrain_strength_spin.value()) + self._onTerrainOperationChanged(self.terrain_operation_combo.currentText()) + except Exception as e: + print(f"创建地形编辑面板时出错: {e}") + + def _onTerrainRadiusChanged(self,value): + """地形编辑半径改变""" + if hasattr(self.world,'terrain_edit_radius'): + self.world.terrain_edit_radius = float(value) + + def _onTerrainStrengthChanged(self,value): + """地形编辑强度改变""" + if hasattr(self.world,'terrain_edit_strength'): + self.world.terrain_edit_strength = float(value) + + def _onTerrainOperationChanged(self, text): + """地形编辑操作类型改变""" + operation_map = { + "升高地形": "add", + "降低地形": "subtract", + "平坦化": "set" + } + if hasattr(self.world, 'terrain_edit_operation'): + # 正确设置地形编辑操作属性 + self.world.terrain_edit_operation = operation_map.get(text, "add") + print(f"地形编辑操作已设置为: {self.world.terrain_edit_operation}") + + def _updateTerrainMaterialPanel(self,terrain_node,terrain_info): + """更新地形材质属性面板""" + try: + material_group = QGroupBox("材质属性") + material_layout = QGridLayout() + #颜色设置 + material_layout.addWidget(QLabel("颜色:"),0,0) + color_button = QPushButton("选择颜色") + color_button.clicked.connect(lambda:self._selectTerrainColor(terrain_info)) + #纹理设置 + material_layout.addWidget(QLabel("纹理:"),1,0) + texture_button = QPushButton("选择纹理") + texture_button.clicked.connect(lambda :self._selectTerrainTexture(terrain_info)) + material_layout.addWidget(texture_button,1,1) + + material_group.setLayout(material_layout) + self._propertyLayout.addWidget(material_group) + except Exception as e: + print(f"更新材质面板时出错{e}") + + def _selectTerrainColor(self,terrain_info): + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + current_color = QColor(255,255,255) + + color = QColorDialog.getColor(current_color,None,"选择地形颜色") + if color.isValid(): + r,g,b = color.red()/255.0,color.green()/255.0,color.blue()/255.0 + if hasattr(self.world,'terrain_manager'): + self.world.terrain_manager.setTerrainColor(terrain_info,(r,g,b)) + except Exception as e: + print(f"选择地形颜色时出错: {e}") + + def _selectTerrainTexture(self,terrain_info): + """选择地形纹理""" + try: + from PyQt5.QtWidgets import QFileDialog + file_path,_ = QFileDialog.getOpenFileName( + None, + "选择地形纹理", + "", + "图像文件(*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + + if file_path and os.path.exists(file_path): + if hasattr(self.world,'terrain_manager'): + success = self.world.terrain_manager.setTerrainTexture(terrain_info,file_path) + if success: + print(f"地形纹理已应用{file_path}") + else: + print(f"应用地形纹理失败") + except Exception as e: + print(f"选择地形纹理时出错: {e}") + + def _deleteTerrain(self, terrain_info, item): + """删除地形""" + try: + from PyQt5.QtWidgets import QMessageBox + + reply = QMessageBox.question( + None, + '确认删除', + f'确定要删除地形 "{terrain_info.get("name", "未知地形")}" 吗?', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + # 调用 interface_manager 中已经实现的 deleteNode 方法 + if hasattr(self.world, 'interface_manager') and self.world.interface_manager.treeWidget: + self.world.interface_manager.deleteNode(terrain_info['node'], item) + else: + # 如果 interface_manager 不可用,使用原来的删除逻辑 + if hasattr(self.world, 'terrain_manager'): + success = self.world.terrain_manager.deleteTerrain(terrain_info) + if success: + # 更新场景树 + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, + 'updateSceneTree'): + self.world.scene_manager.updateSceneTree() + + # 清空属性面板 + self.clearPropertyPanel() + + print(f"✓ 地形已删除: {terrain_info.get('name', '未知')}") + else: + print("✗ 删除地形失败") + + except Exception as e: + print(f"删除地形时出错: {e}") + + def _cleanupAllReferences(self): + """清理所有控件引用""" + # 清理变换控件引用 + self._cleanupTransformControls() + + # 清理其他可能的控件引用 + other_controls = ['scale_x', 'scale_y', 'scale_z', 'pos_x', 'pos_y', 'pos_z'] + for name in other_controls: + if hasattr(self, name): + setattr(self, name, None) + def _setUserVisible(self,node,visible): node.setPythonTag("user_visible",visible) self._syncEffectiveVisibility(node) @@ -226,54 +594,213 @@ class PropertyPanelManager: print(f"切换模型可见性失败: {str(e)}") def refreshModelValues(self, nodePath): - """实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)""" + """刷新模型值显示""" if not nodePath or self._propertyLayout is None: return - parent = nodePath.getParent() - render = self.world.render # 世界根节点 + # 检查面板是否仍然有效 + if not self._isPropertyPanelValid(): + return - # ---------------- 相对位置 ---------------- - relPos = nodePath.getPos(parent) if parent else nodePath.getPos() - if hasattr(self, '_xSpin') and self._xSpin: - self._xSpin.blockSignals(True) - self._xSpin.setValue(relPos.x) - self._xSpin.blockSignals(False) - if hasattr(self, '_ySpin') and self._ySpin: - self._ySpin.blockSignals(True) - self._ySpin.setValue(relPos.y) - self._ySpin.blockSignals(False) - if hasattr(self, '_zSpin') and self._zSpin: - self._zSpin.blockSignals(True) - self._zSpin.setValue(relPos.z) - self._zSpin.blockSignals(False) + try: + # 检查是否是GUI元素 + is_gui_element = (hasattr(nodePath, 'getTag') and + nodePath.getTag("is_gui_element") == "1") - # ---------------- 世界位置 ---------------- - worldPos = nodePath.getPos(render) - for axis, attr in zip(('x', 'y', 'z'), ('_worldXSpin', '_worldYSpin', '_worldZSpin')): - spin = getattr(self, attr, None) - if spin: - spin.blockSignals(True) - spin.setValue(getattr(worldPos, axis)) - spin.blockSignals(False) + if is_gui_element: + # 对于GUI元素,更新所有相关属性 + self._refreshGUIElementValues(nodePath) + else: + # 对于普通3D模型,更新位置、旋转、缩放 + self._refreshModelValues(nodePath) - # ---------------- 旋转 ---------------- - hpr = nodePath.getHpr() - for idx, (attr, val) in enumerate(zip(('_hSpin', '_pSpin', '_rSpin'), hpr)): - spin = getattr(self, attr, None) - if spin: - spin.blockSignals(True) - spin.setValue(val) - spin.blockSignals(False) + except Exception as e: + print(f"刷新模型值显示失败: {e}") - # ---------------- 缩放 ---------------- - scale = nodePath.getScale() - for axis, attr in zip(('x', 'y', 'z'), ('_xScaleSpin', '_yScaleSpin', '_zScaleSpin')): - spin = getattr(self, attr, None) - if spin: - spin.blockSignals(True) - spin.setValue(getattr(scale, axis)) - spin.blockSignals(False) + def _refreshGUIElementValues(self, gui_element): + """刷新GUI元素值显示""" + try: + # 更新位置属性 + if hasattr(self, 'pos_x') and self.pos_x: + pos = gui_element.getPos() + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry", "2d_image"]: + # 2D GUI组件使用屏幕坐标 + logical_x = pos.getX() / 0.1 + logical_z = pos.getZ() / 0.1 + + self._safeUpdateSpinBox('pos_x', logical_x) + self._safeUpdateSpinBox('pos_z', logical_z) + else: + # 3D GUI组件使用世界坐标 + self._safeUpdateSpinBox('pos_x', pos.getX()) + self._safeUpdateSpinBox('pos_y', pos.getY()) + self._safeUpdateSpinBox('pos_z', pos.getZ()) + + # 更新缩放属性 + if hasattr(self, 'scale_x') and self.scale_x: + scale = gui_element.getScale() + self._safeUpdateSpinBox('scale_x', scale.getX()) + if hasattr(self, 'scale_y') and self.scale_y: + self._safeUpdateSpinBox('scale_y', scale.getY()) + if hasattr(self, 'scale_z') and self.scale_z: + self._safeUpdateSpinBox('scale_z', scale.getZ()) + + # 更新旋转属性(如果存在) + if hasattr(self, 'rot_x') and self.rot_x: + hpr = gui_element.getHpr() + self._safeUpdateSpinBox('rot_x', hpr.getX()) + self._safeUpdateSpinBox('rot_y', hpr.getY()) + self._safeUpdateSpinBox('rot_z', hpr.getZ()) + + except Exception as e: + print(f"刷新GUI元素值显示失败: {e}") + + def _refreshModelValues(self, nodePath): + """刷新普通模型值显示""" + try: + parent = nodePath.getParent() + render = self.world.render + relPos = nodePath.getPos(parent) if parent else nodePath.getPos() + + # 安全地更新位置控件 + self._safeUpdateSpinBox('pos_x', relPos.getX()) + self._safeUpdateSpinBox('pos_y', relPos.getY()) + self._safeUpdateSpinBox('pos_z', relPos.getZ()) + + # 安全地更新世界位置控件 + worldPos = nodePath.getPos(render) + self._safeUpdateSpinBox('world_pos_x', worldPos.getX()) + self._safeUpdateSpinBox('world_pos_y', worldPos.getY()) + self._safeUpdateSpinBox('world_pos_z', worldPos.getZ()) + + # 安全地更新旋转控件(如果存在) + if hasattr(self, 'rot_x') or hasattr(self, 'rot_y') or hasattr(self, 'rot_z'): + hpr = nodePath.getHpr() + self._safeUpdateSpinBox('rot_x', hpr.getX()) + self._safeUpdateSpinBox('rot_y', hpr.getY()) + self._safeUpdateSpinBox('rot_z', hpr.getZ()) + + # 安全地更新缩放控件(如果存在) + if hasattr(self, 'scale_x') or hasattr(self, 'scale_y') or hasattr(self, 'scale_z'): + scale = nodePath.getScale() + self._safeUpdateSpinBox('scale_x', scale.getX()) + self._safeUpdateSpinBox('scale_y', scale.getY()) + self._safeUpdateSpinBox('scale_z', scale.getZ()) + + except Exception as e: + print(f"刷新模型值显示失败: {e}") + + def _isPropertyPanelValid(self): + """检查属性面板是否仍然有效""" + try: + # 检查布局是否仍然存在且有效 + if not self._propertyLayout: + return False + + # 检查父控件是否仍然存在 + parent = self._propertyLayout.parent() + if not parent: + return False + + # 检查父控件是否仍然在窗口中 + return parent.isVisible() + except: + return False + + def _safeUpdateSpinBox(self, attr_name, value): + """安全地更新数值框""" + try: + spinbox = getattr(self, attr_name, None) + if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见 + # 检查对象是否仍然有效 + spinbox.blockSignals(True) + spinbox.setValue(value) + spinbox.blockSignals(False) + except RuntimeError as e: + # 对象已被删除 + setattr(self, attr_name, None) + print(f"警告: 数值框 {attr_name} 已被删除: {e}") + except Exception as e: + print(f"更新数值框 {attr_name} 时出错: {e}") + + # 在创建位置和变换控件时,增加安全检查 + def _createTransformControls(self, nodePath): + """创建变换控制控件""" + try: + # 清理旧的引用 + self._cleanupTransformControls() + + # 创建新的控件引用 + transform_layout = QGridLayout() + + # 位置控件 + transform_layout.addWidget(QLabel("相对位置"), 0, 0) + + # 创建并设置 X, Y, Z 标签居中 + x_label = QLabel("X") + y_label = QLabel("Y") + z_label = QLabel("Z") + x_label.setAlignment(Qt.AlignCenter) + y_label.setAlignment(Qt.AlignCenter) + z_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(x_label, 0, 1) + transform_layout.addWidget(y_label, 0, 2) + transform_layout.addWidget(z_label, 0, 3) + + # 位置数值输入框 + self.pos_x = self._createSafeSpinBox(-1000, 1000) + self.pos_y = self._createSafeSpinBox(-1000, 1000) + self.pos_z = self._createSafeSpinBox(-1000, 1000) + + transform_layout.addWidget(self.pos_x, 1, 1) + transform_layout.addWidget(self.pos_y, 1, 2) + transform_layout.addWidget(self.pos_z, 1, 3) + + # 世界位置 (只读) + transform_layout.addWidget(QLabel("世界位置"), 2, 0) + self.world_pos_x = self._createSafeSpinBox(-10000, 10000, True) # 只读 + self.world_pos_y = self._createSafeSpinBox(-10000, 10000, True) + self.world_pos_z = self._createSafeSpinBox(-10000, 10000, True) + + transform_layout.addWidget(self.world_pos_x, 2, 1) + transform_layout.addWidget(self.world_pos_y, 2, 2) + transform_layout.addWidget(self.world_pos_z, 2, 3) + + return transform_layout + except Exception as e: + print(f"创建变换控件失败: {e}") + return None + + def _createSafeSpinBox(self, min_val, max_val, read_only=False): + """创建安全的数值框""" + try: + spinbox = QDoubleSpinBox() + spinbox.setRange(min_val, max_val) + spinbox.setSingleStep(0.1) + if read_only: + spinbox.setReadOnly(True) + spinbox.setStyleSheet("background-color: #f0f0f0;") + return spinbox + except Exception as e: + print(f"创建数值框失败: {e}") + return None + + def _cleanupTransformControls(self): + """清理变换控件引用""" + control_names = ['pos_x', 'pos_y', 'pos_z', 'world_pos_x', 'world_pos_y', 'world_pos_z'] + for name in control_names: + if hasattr(self, name): + control = getattr(self, name) + if control: + try: + # 断开所有信号连接 + control.valueChanged.disconnect() + except: + pass + setattr(self, name, None) def _refreshWorldPos(self,model): if not hasattr(self,'worldXSpin'): return @@ -282,8 +809,223 @@ class PropertyPanelManager: self._worldYSpin.setValue(world.y) self._worldZSpin.setValue(world.z) + def _showCesiumTilesetProperties(self, nodePath, item): + """显示 Cesium tileset 属性""" + from PyQt5.QtWidgets import QLabel, QDoubleSpinBox, QPushButton, QGroupBox, QFormLayout + from PyQt5.QtCore import Qt + + print(f"显示 Cesium tileset 属性: {nodePath.getName()}") + + # 标题 + title = QLabel("Cesium 3D Tiles") + title.setStyleSheet("font-size: 14px; font-weight: bold; margin: 10px 0;") + self._propertyLayout.addWidget(title) + + # URL 信息 + if nodePath.hasTag("tileset_url"): + url_label = QLabel("URL:") + url_value = QLabel(nodePath.getTag("tileset_url")) + url_value.setWordWrap(True) + url_value.setStyleSheet("font-size: 9px; color: #666;") + self._propertyLayout.addWidget(url_label) + self._propertyLayout.addWidget(url_value) + + # 位置控制 + pos_group = QGroupBox("位置") + pos_layout = QFormLayout() + + # X 坐标 + x_spin = QDoubleSpinBox() + x_spin.setRange(-10000, 10000) + x_spin.setSingleStep(1.0) + x_spin.setDecimals(2) + pos = nodePath.getPos() + x_spin.setValue(pos.getX()) + x_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'x', v)) + pos_layout.addRow("X:", x_spin) + + # Y 坐标 + y_spin = QDoubleSpinBox() + y_spin.setRange(-10000, 10000) + y_spin.setSingleStep(1.0) + y_spin.setDecimals(2) + y_spin.setValue(pos.getY()) + y_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'y', v)) + pos_layout.addRow("Y:", y_spin) + + # Z 坐标 + z_spin = QDoubleSpinBox() + z_spin.setRange(-10000, 10000) + z_spin.setSingleStep(1.0) + z_spin.setDecimals(2) + z_spin.setValue(pos.getZ()) + z_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'z', v)) + pos_layout.addRow("Z:", z_spin) + + pos_group.setLayout(pos_layout) + self._propertyLayout.addWidget(pos_group) + + # 缩放控制 + scale_group = QGroupBox("缩放") + scale_layout = QFormLayout() + + scale_spin = QDoubleSpinBox() + scale_spin.setRange(0.01, 1000) + scale_spin.setSingleStep(0.1) + scale_spin.setDecimals(2) + scale_spin.setValue(nodePath.getScale().getX()) + scale_spin.valueChanged.connect(lambda v: self._updateTilesetScale(nodePath, v)) + scale_layout.addRow("缩放:", scale_spin) + + scale_group.setLayout(scale_layout) + self._propertyLayout.addWidget(scale_group) + + # 删除按钮 + delete_btn = QPushButton("删除 Tileset") + delete_btn.setStyleSheet(""" + QPushButton { + background-color: #ff4444; + color: white; + border: none; + padding: 8px; + border-radius: 4px; + margin-top: 10px; + } + QPushButton:hover { + background-color: #ff6666; + } + """) + delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) + self._propertyLayout.addWidget(delete_btn) + + # 添加弹性空间 + self._propertyLayout.addStretch() + + def _createPositionControl(self, label, nodePath, axis): + """创建位置控制控件""" + from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox + + layout = QHBoxLayout() + + axis_label = QLabel(label) + axis_label.setFixedWidth(20) + layout.addWidget(axis_label) + + spinbox = QDoubleSpinBox() + spinbox.setRange(-10000, 10000) + spinbox.setSingleStep(1.0) + spinbox.setDecimals(2) + + # 获取当前坐标值 + pos = nodePath.getPos() + if axis == 'x': + spinbox.setValue(pos.getX()) + elif axis == 'y': + spinbox.setValue(pos.getY()) + elif axis == 'z': + spinbox.setValue(pos.getZ()) + + # 连接值变化信号 + def onValueChanged(value): + self._updateTilesetPosition(nodePath, axis, value) + + spinbox.valueChanged.connect(onValueChanged) + layout.addWidget(spinbox) + + return layout + + def _createScaleControl(self, nodePath): + """创建缩放控制控件""" + from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox + + layout = QHBoxLayout() + + scale_label = QLabel("缩放:") + scale_label.setFixedWidth(40) + layout.addWidget(scale_label) + + spinbox = QDoubleSpinBox() + spinbox.setRange(0.01, 1000) + spinbox.setSingleStep(0.1) + spinbox.setDecimals(2) + spinbox.setValue(nodePath.getScale().getX()) # 假设均匀缩放 + + def onScaleChanged(value): + self._updateTilesetScale(nodePath, value) + + spinbox.valueChanged.connect(onScaleChanged) + layout.addWidget(spinbox) + + return layout + + def _updateTilesetPosition(self, nodePath, axis, value): + """更新 tileset 位置""" + try: + pos = nodePath.getPos() + if axis == 'x': + nodePath.setPos(value, pos.getY(), pos.getZ()) + elif axis == 'y': + nodePath.setPos(pos.getX(), value, pos.getZ()) + elif axis == 'z': + nodePath.setPos(pos.getX(), pos.getY(), value) + print(f"更新 {nodePath.getName()} 位置: {axis} = {value}") + except Exception as e: + print(f"更新位置失败: {e}") + + def _updateTilesetScale(self, nodePath, value): + """更新 tileset 缩放""" + try: + nodePath.setScale(value) + print(f"更新 {nodePath.getName()} 缩放: {value}") + except Exception as e: + print(f"更新缩放失败: {e}") + + def _deleteCesiumTileset(self, nodePath, item): + """删除 Cesium tileset""" + try: + from PyQt5.QtWidgets import QMessageBox + + reply = QMessageBox.question( + None, + '确认删除', + f'确定要删除 Cesium tileset "{nodePath.getName()}" 吗?', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + # 从场景中移除 + nodePath.removeNode() + + # 从 tilesets 列表中移除 + if hasattr(self.world, 'scene_manager'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + if tileset_info['node'] == nodePath: + tilesets_to_remove.append(i) + + # 从后往前删除,避免索引问题 + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + + # 更新场景树 + self.world.scene_manager.updateSceneTree() + + # 清空属性面板 + self.clearPropertyPanel() + + print(f"成功删除 Cesium tileset: {nodePath.getName()}") + + except Exception as e: + print(f"删除 Cesium tileset 失败: {str(e)}") + def _updateModelPropertyPanel(self, model): """更新模型属性面板""" + + if hasattr(model,'getTag') and model.getTag("is_gui_element") == "1": + self.updateGUIPropertyPanel(model) + return + # 获取父节点 parent = model.getParent() @@ -516,48 +1258,6 @@ class PropertyPanelManager: model.setScale(current_scale.getX(), current_scale.getY(), value) self.refreshModelValues(model) - def refreshModelValues(self,nodePath): - if not nodePath or self._propertyLayout is None: - return - parent = nodePath.getParent() - render = self.world.render - relPos = nodePath.getPos(parent) if parent else nodePath.getPos() - if hasattr(self,'pos_x') and self.pos_x: - self.pos_x.blockSignals(True) - self.pos_x.setValue(relPos.getX()) - self.pos_x.blockSignals(False) - if hasattr(self,'pos_y') and self.pos_y: - self.pos_y.blockSignals(True) - self.pos_y.setValue(relPos.getY()) - self.pos_y.blockSignals(False) - if hasattr(self,'pos_z') and self.pos_z: - self.pos_z.blockSignals(True) - self.pos_z.setValue(relPos.getZ()) - self.pos_z.blockSignals(False) - - worldPos = nodePath.getPos(render) - for axis,attr in zip(('x','y','z'),('world_pos_x','world_pos_y','world_pos_z')): - spin = getattr(self,attr,None) - if spin: - spin.blockSignals(True) - spin.setValue(getattr(worldPos,axis)) - spin.blockSignals(False) - - hpr = nodePath.getHpr() - for idx,(attr,val) in enumerate(zip(('rot_h','rot_p','rot_r'),hpr)): - spin = getattr(self,attr,None) - if spin: - spin.blockSignals(True) - spin.setValue(val) - spin.blockSignals(False) - - scale = nodePath.getScale() - for axis,attr in zip(('x','y','z'),('scale_x','scale_y','scale_z')): - spin = getattr(self,attr,None) - if spin: - spin.blockSignals(True) - spin.setValue(getattr(scale,axis)) - spin.blockSignals(False) def updateGUIPropertyPanel(self, gui_element): @@ -597,8 +1297,8 @@ class PropertyPanelManager: # 变换属性组(合并位置和变换) if hasattr(gui_element, 'getPos'): - # 根据GUI类型设置组名 - if gui_type in ["button", "label", "entry"]: + # 根据GUI类型设置组名—— + if gui_type in ["button", "label", "entry","2d_image"]: transform_group = QGroupBox("变换 Rect Transform") else: transform_group = QGroupBox("变换 Transform") @@ -608,7 +1308,7 @@ class PropertyPanelManager: pos = gui_element.getPos() # 根据GUI类型决定位置编辑方式 - if gui_type in ["button", "label", "entry"]: + if gui_type in ["button", "label", "entry","2d_image"]: # 2D GUI组件使用屏幕坐标 logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 logical_z = pos.getZ() / 0.1 @@ -625,17 +1325,18 @@ class PropertyPanelManager: transform_layout.addWidget(x_label, 0, 1) transform_layout.addWidget(z_label, 0, 2) - xPos = QDoubleSpinBox() - xPos.setRange(-50, 50) - xPos.setValue(logical_x) - xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) - transform_layout.addWidget(xPos, 1, 1) + self.pos_x = QDoubleSpinBox() + self.pos_x.setRange(-50, 50) + self.pos_x.setValue(logical_x) + self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) - zPos = QDoubleSpinBox() - zPos.setRange(-50, 50) - zPos.setValue(logical_z) - zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) - transform_layout.addWidget(zPos, 1, 2) + transform_layout.addWidget(self.pos_x, 1, 1) + + self.pos_z = QDoubleSpinBox() + self.pos_z.setRange(-50, 50) + self.pos_z.setValue(logical_z) + self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) + transform_layout.addWidget(self.pos_z, 1, 2) # 显示实际屏幕坐标(只读) transform_layout.addWidget(QLabel("实际坐标"), 2, 0) @@ -648,9 +1349,42 @@ class PropertyPanelManager: transform_layout.addWidget(actualXLabel, 3, 1) transform_layout.addWidget(actualZLabel, 3, 2) + if gui_type == "2d_image": + scale = gui_element.getScale() + width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, + (tuple, list)) else scale + height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, + (tuple, list)) and len( + scale) > 1 else scale + + transform_layout.addWidget(QLabel("宽度"),4,0) + self.scale_x = QDoubleSpinBox() + self.scale_x.setRange(0.1,10) + self.scale_x.setSingleStep(0.1) + self.scale_x.setValue(width) + self.scale_x.valueChanged.connect(lambda v:self.editGUIScale(gui_element,"x",v)) + transform_layout.addWidget(self.scale_x,4,1) + + transform_layout.addWidget(QLabel("高度"),4,2) + self.scale_z = QDoubleSpinBox() + self.scale_z.setRange(0.1,10) + self.scale_z.setSingleStep(0.1) + self.scale_z.setValue(height) + self.scale_z.valueChanged.connect(lambda v:self.editGUIScale(gui_element,"z",v)) + transform_layout.addWidget(self.scale_z,4,3) + + else: + transform_layout.addWidget(QLabel("缩放"),4,0) + scaleSpinBox = QDoubleSpinBox() + scaleSpinBox.setRange(0.01,10) + scaleSpinBox.setSingleStep(0.1) + scaleSpinBox.setValue(gui_element.getScale().getX()*2) + scaleSpinBox.valueChanged.connect(lambda v:self._update2DImageScale(gui_element,v)) + transform_layout.addWidget(scaleSpinBox,4,1) + else: # 3D GUI组件使用世界坐标 - transform_layout.addWidget(QLabel("世界位置"), 0, 0) + transform_layout.addWidget(QLabel("位置"), 0, 0) # X, Y, Z 标签居中 x_label = QLabel("X") @@ -664,65 +1398,105 @@ class PropertyPanelManager: transform_layout.addWidget(y_label, 0, 2) transform_layout.addWidget(z_label, 0, 3) - xPos = QDoubleSpinBox() - xPos.setRange(-1000, 1000) - xPos.setValue(pos.getX()) - xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", - [v, pos.getY(), pos.getZ()])) - transform_layout.addWidget(xPos, 1, 1) + # 位置数值输入框 + self.pos_x = QDoubleSpinBox() + self.pos_x.setRange(-100, 100) + self.pos_x.setValue(pos.getX()) + self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) + transform_layout.addWidget(self.pos_x, 1, 1) - yPos = QDoubleSpinBox() - yPos.setRange(-1000, 1000) - yPos.setValue(pos.getY()) - yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", - [pos.getX(), v, pos.getZ()])) - transform_layout.addWidget(yPos, 1, 2) + self.pos_y = QDoubleSpinBox() + self.pos_y.setRange(-100, 100) + self.pos_y.setValue(pos.getY()) + self.pos_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) + transform_layout.addWidget(self.pos_y, 1, 2) - zPos = QDoubleSpinBox() - zPos.setRange(-1000, 1000) - zPos.setValue(pos.getZ()) - zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUIElement(gui_element, "position", - [pos.getX(), pos.getY(), v])) - transform_layout.addWidget(zPos, 1, 3) + self.pos_z = QDoubleSpinBox() + self.pos_z.setRange(-100, 100) + self.pos_z.setValue(pos.getZ()) + self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) + transform_layout.addWidget(self.pos_z, 1, 3) - if hasattr(gui_element, 'getScale'): + # 缩放属性 scale = gui_element.getScale() + transform_layout.addWidget(QLabel("缩放"), 2, 0) - row_offset = 4 if gui_type in ["button", "label", "entry"] else 2 + # X, Y, Z 缩放标签居中 + sx_label = QLabel("X") + sy_label = QLabel("Y") + sz_label = QLabel("Z") + sx_label.setAlignment(Qt.AlignCenter) + sy_label.setAlignment(Qt.AlignCenter) + sz_label.setAlignment(Qt.AlignCenter) - transform_layout.addWidget(QLabel("缩放"), row_offset, 0) + transform_layout.addWidget(sx_label, 2, 1) + transform_layout.addWidget(sy_label, 2, 2) + transform_layout.addWidget(sz_label, 2, 3) - # X缩放 - transform_layout.addWidget(QLabel("长:"), row_offset, 1) - scaleXSpinBox = QDoubleSpinBox() - scaleXSpinBox.setRange(0.01, 1000) - scaleXSpinBox.setSingleStep(0.1) - scaleXSpinBox.setValue(scale.getX()) - scaleXSpinBox.valueChanged.connect(lambda v: self._onScaleValueChanged(scaleXSpinBox, v)) - scaleXSpinBox.valueChanged.connect(lambda v: self._updateGUIScaleX(gui_element, v)) - transform_layout.addWidget(scaleXSpinBox, row_offset, 2) + # 缩放数值输入框 + self.scale_x = QDoubleSpinBox() + self.scale_x.setRange(0.01, 10) + self.scale_x.setSingleStep(0.1) + self.scale_x.setValue( + scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) + self.scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + transform_layout.addWidget(self.scale_x, 3, 1) - row_offset += 1 - transform_layout.addWidget(QLabel("宽:"), row_offset, 1) - scaleYSpinBox = QDoubleSpinBox() - scaleYSpinBox.setRange(0.01, 1000) - scaleYSpinBox.setSingleStep(0.1) - scaleYSpinBox.setValue(scale.getY()) - scaleYSpinBox.valueChanged.connect(lambda v: self._onScaleValueChanged(scaleYSpinBox, v)) - scaleYSpinBox.valueChanged.connect(lambda v: self._updateGUIScaleZ(gui_element, v)) - transform_layout.addWidget(scaleYSpinBox, row_offset, 2) + self.scale_y = QDoubleSpinBox() + self.scale_y.setRange(0.01, 10) + self.scale_y.setSingleStep(0.1) + self.scale_y.setValue( + scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( + scale) > 1 else scale) + self.scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) + transform_layout.addWidget(self.scale_y, 3, 2) + + self.scale_z = QDoubleSpinBox() + self.scale_z.setRange(0.01, 10) + self.scale_z.setSingleStep(0.1) + self.scale_z.setValue( + scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( + scale) > 2 else scale) + self.scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + transform_layout.addWidget(self.scale_z, 3, 3) - # scaleSpinBox = QDoubleSpinBox() - # scaleSpinBox.setRange(0.01, 10) - # scaleSpinBox.setSingleStep(0.1) - # scaleSpinBox.setValue(scale.getX()) - # scaleSpinBox.valueChanged.connect( - # lambda v: self.world.gui_manager.editGUIElement(gui_element, "scale", v)) - # transform_layout.addWidget(scaleSpinBox, row_offset, 1) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) + # 为2D图像添加Sort属性 + if gui_type == "2d_image": + sort_group = QGroupBox("显示顺序") + sort_layout = QGridLayout() + + # 获取当前sort值,如果没有设置则默认为0 + current_sort = int(gui_element.getTag("sort") or "0") + + sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) + sort_spin = QSpinBox() + sort_spin.setRange(-1000, 1000) + sort_spin.setValue(current_sort) + + def updateSort(value): + try: + # 设置标签 + gui_element.setTag("sort", str(value)) + # 应用sort到节点 + gui_element.setBin("fixed", value) + print(f"✓ 更新2D图像渲染顺序: {value}") + except Exception as e: + print(f"✗ 更新2D图像渲染顺序失败: {e}") + + sort_spin.valueChanged.connect(updateSort) + sort_layout.addWidget(sort_spin, 0, 1) + + # sort_help = QLabel("数值越小越先渲染\n负数在背景,正数在前景") + # sort_help.setStyleSheet("font-size: 10px; color: gray;") + # sort_layout.addWidget(sort_help, 1, 0, 1, 2) + + sort_group.setLayout(sort_layout) + self._propertyLayout.addWidget(sort_group) + # 外观属性组 - 添加字体颜色选择 if gui_type in ["button", "label", "3d_text"]: appearance_group = QGroupBox("外观属性") @@ -793,6 +1567,49 @@ class PropertyPanelManager: image_group.setLayout(image_layout) self._propertyLayout.addWidget(image_group) + if gui_type == "2d_image": + image_group = QGroupBox("2D图片设置") + image_layout = QGridLayout() + + # 当前图片路径标签 + current_image_label = QLabel("当前图片:") + image_layout.addWidget(current_image_label, 0, 0) + + # 显示当前贴图路径(简化显示) + current_texture_path = gui_element.getTag("texture_path") or gui_element.getTag("image_path") or "未设置" + #texture_label = QLabel(current_texture_path) + #texture_label.setWordWrap(True) + #image_layout.addWidget(texture_label, 0, 1) + + # 选择图片按钮 + select_texture_button = QPushButton("选择图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 2) + + def onSelect2DTexture(): + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择图片", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + if file_path: + # 应用新纹理到 2D Image + success = self.update2DImageTexture(gui_element, file_path) + if success: + # 保存路径到 Tag + gui_element.setTag("texture_path", file_path) + gui_element.setTag("image_path", file_path) + # 更新显示 + #texture_label.setText(file_path) + # 可选:刷新场景树或其他 UI + self.world.scene_manager.updateSceneTree() + + select_texture_button.clicked.connect(onSelect2DTexture) + + image_group.setLayout(image_layout) + self._propertyLayout.addWidget(image_group) + # 添加弹性空间 self._propertyLayout.addStretch() @@ -803,6 +1620,180 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() + # 在gui_manager.py或其他相关文件中添加以下方法 + + def editGUI2DPosition(self, gui_element, axis, value): + """编辑2D GUI元素位置""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry", "2d_image"]: + # 2D元素使用屏幕坐标,需要转换 + current_pos = gui_element.getPos() + + if axis == "x": + # 转换逻辑坐标到屏幕坐标 + screen_x = value * 0.1 + new_pos = (screen_x, current_pos.getY(), current_pos.getZ()) + elif axis == "z": + screen_z = value * 0.1 + new_pos = (current_pos.getX(), current_pos.getY(), screen_z) + else: + return False + self.refreshModelValues(gui_element) + gui_element.setPos(*new_pos) + print(f"✓ 更新2D GUI元素位置: {axis}={value}") + return True + else: + print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") + return False + except Exception as e: + print(f"✗ 更新2D GUI元素位置失败: {e}") + import traceback + traceback.print_exc() + return False + + def editGUI3DPosition(self, gui_element, axis, value): + """编辑3D GUI元素位置""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["3d_text", "3d_image"]: + current_pos = gui_element.getPos() + + if axis == "x": + new_pos = (value, current_pos.getY(), current_pos.getZ()) + elif axis == "y": + new_pos = (current_pos.getX(), value, current_pos.getZ()) + elif axis == "z": + new_pos = (current_pos.getX(), current_pos.getY(), value) + else: + return False + + gui_element.setPos(*new_pos) + print(f"✓ 更新3D GUI元素位置: {axis}={value}") + return True + else: + print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}") + return False + except Exception as e: + print(f"✗ 更新3D GUI元素位置失败: {e}") + import traceback + traceback.print_exc() + return False + + def editGUIScale(self, gui_element, axis, value): + """编辑GUI元素缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 确保缩放值不为0 + if value == 0: + value = 0.01 + + if gui_type in ["3d_text", "3d_image","2d_image"]: + # 3D元素处理 + if axis == "x": + new_scale = (value, current_scale.getY(), current_scale.getZ()) + elif axis == "y": + new_scale = (current_scale.getX(), value, current_scale.getZ()) + elif axis == "z": + new_scale = (current_scale.getX(), current_scale.getY(), value) + else: + return False + + gui_element.setScale(*new_scale) + else: + # 2D元素处理 + if axis in ["x", "z"]: # 对于2D图像,x和z分别代表宽度和高度 + # 保持原有缩放比例,仅调整指定轴 + if axis == "x": + gui_element.setScale(value, + current_scale.getZ() if hasattr(current_scale, 'getZ') else current_scale[ + 1] if isinstance(current_scale, (list, tuple)) else current_scale) + elif axis == "z": + gui_element.setScale( + current_scale.getX() if hasattr(current_scale, 'getX') else current_scale[0] if isinstance( + current_scale, (list, tuple)) else current_scale, value) + else: + # 对于其他2D元素,使用统一缩放 + gui_element.setScale(value) + + print(f"✓ 更新GUI元素缩放: {axis}={value}") + return True + except Exception as e: + print(f"✗ 更新GUI元素缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update2DImageWidth(self,gui_element,width): + try: + current_scale = gui_element.getScale() + width_scaled = width/2 + height_scaled = current_scale.getZ() + + gui_element.setScale(width_scaled,current_scale.getY(),height_scaled) + + if hasattr(gui_element,'_height_spinbox'): + gui_element._height_spinbox.blockSignals(True) + gui_element._height_spinbox.setValue(height_scaled*2) + gui_element._height_spinbox.blockSignals(False) + + print(f"✓ 更新2D图片宽度: {width}") + except Exception as e: + print(f"✗ 更新2D图片宽度失败: {e}") + + def _update2DImageHeight(self, gui_element, height): + """更新2D图片高度""" + try: + # 获取当前缩放 + current_scale = gui_element.getScale() + # 保持宽度不变,只更新高度(Z轴) + height_scaled = height / 2 # 转换为内部缩放值 + width_scaled = current_scale.getX() + + gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) + + # 更新宽度控件的值 + if hasattr(gui_element, '_width_spinbox'): + gui_element._width_spinbox.blockSignals(True) + gui_element._width_spinbox.setValue(width_scaled * 2) + gui_element._width_spinbox.blockSignals(False) + + print(f"✓ 更新2D图片高度: {height}") + except Exception as e: + print(f"✗ 更新2D图片高度失败: {e}") + + def _update2DImageScale(self,gui_element,scale): + try: + scaled_value = scale/2 + gui_element.setScale(scaled_value,0,scaled_value) + print(f"✓ 更新2D图片缩放: {scale}") + except Exception as e: + print(f"✗ 更新2D图片缩放失败: {e}") + + def _update3DImageScale(self, gui_element, axis, scale): + """更新3D元素缩放""" + try: + current_scale = gui_element.getScale() + + if axis == 'x': + new_scale = (scale, current_scale.getY(), current_scale.getZ()) + # elif axis == 'y': + # new_scale = (current_scale.getX(), scale, current_scale.getZ()) + elif axis == 'z': + new_scale = (current_scale.getX(), current_scale.getY(), scale) + else: + return + + gui_element.setScale(*new_scale) + print(f"✓ 更新3D元素轴缩放 {axis}: {scale}") + except Exception as e: + print(f"✗ 更新3D元素轴缩放失败: {e}") + + def _selectGUIColor(self, gui_element): """选择GUI元素的字体颜色""" from PyQt5.QtWidgets import QColorDialog @@ -894,26 +1885,7 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _updateGUIScaleX(self, gui_element, scale_x): - """更新GUI元素X轴缩放""" - try: - gui_type = gui_element.getTag("gui_type") - current_scale = gui_element.getScale() - - # 对于不同的GUI类型使用不同的缩放方法 - if gui_type in ["3d_text", "3d_image"]: - # 对于3D元素,直接设置缩放 - new_scale = (scale_x, current_scale.getY(), current_scale.getZ()) - gui_element.setScale(*new_scale) - else: - # 对于2D元素,保持原有的缩放方法 - gui_element.setScale(scale_x) - - print(f"✓ 更新GUI元素X轴缩放: {scale_x}") - except Exception as e: - print(f"✗ 更新GUI元素X轴缩放失败: {e}") - - def _updateGUIScaleZ(self, gui_element, scale_z): + def _updateGUIScaleY(self, gui_element, scale_y): """更新GUI元素Y轴缩放""" try: gui_type = gui_element.getTag("gui_type") @@ -922,16 +1894,35 @@ class PropertyPanelManager: # 对于不同的GUI类型使用不同的缩放方法 if gui_type in ["3d_text", "3d_image"]: # 对于3D元素,直接设置缩放 - new_scale = (current_scale.getX(), current_scale.getZ(), scale_z) + new_scale = (current_scale.getX(), scale_y, current_scale.getZ()) gui_element.setScale(*new_scale) else: # 对于2D元素,保持原有的缩放方法 - gui_element.setScale(scale_z) + gui_element.setScale(scale_y) - print(f"✓ 更新GUI元素Y轴缩放: {scale_z}") + print(f"✓ 更新GUI元素Y轴缩放: {scale_y}") except Exception as e: print(f"✗ 更新GUI元素Y轴缩放失败: {e}") + def _updateGUIScaleZ(self, gui_element, scale_z): + """更新GUI元素Z轴缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 对于不同的GUI类型使用不同的缩放方法 + if gui_type in ["3d_text", "3d_image"]: + # 对于3D元素,直接设置缩放 + new_scale = (current_scale.getX(), current_scale.getY(), scale_z) + gui_element.setScale(*new_scale) + else: + # 对于2D元素,使用单一缩放值(保持宽高比) + gui_element.setScale(scale_z) + + print(f"✓ 更新GUI元素Z轴缩放: {scale_z}") + except Exception as e: + print(f"✗ 更新GUI元素Z轴缩放失败: {e}") + def update3DImageTexture(self,nodepath,texture_path): try: tex = self.world.loader.loadTexture(texture_path) @@ -945,6 +1936,42 @@ class PropertyPanelManager: print(f"[错误] 更新 3D 图片纹理失败: {e}") return False + def update2DImageTexture(self, gui_element, image_path): + try: + new_texture = self.world.loader.loadTexture(image_path) + if new_texture: + if hasattr(gui_element,'setTexture'): + gui_element.setTexture(new_texture,1) + else: + from direct.gui.DirectGui import DirectFrame + if isinstance(gui_element,DirectFrame): + gui_element['frameTexture']=None + gui_element['frameTexture']=new_texture + else: + print("❌ 不支持的GUI元素类型,无法更新纹理") + return False + gui_element.setTag("image_path",image_path) + + if not gui_element.hasMaterial(): + from panda3d.core import Material,LColor + mat = Material() + mat.setName(f"image-material-{id(gui_element)}") + mat.setBaseColor(LColor(1,1,1,1)) + mat.setDiffuse(LColor(1,1,1,1)) + mat.setAmbient(LColor(0.5,0.5,0.5,1)) + mat.setSpecular(LColor(0.1,0.1,0.1,1.0)) + mat.setShininess(10.0) + gui_element.setMaterial(mat,1) + print(f"✅ 2D图像纹理已更新为: {image_path}") + return True + else: + print(f"❌ 无法加载2D图片纹理: {image_path}") + return False + except Exception as e: + print(f"❌ 更新2D图片纹理时出错: {e}") + return False + + def _updateScriptPropertyPanel(self, game_object): """更新脚本属性面板""" # 获取对象上的脚本 @@ -4604,7 +5631,7 @@ class PropertyPanelManager: # 如果都没有动画 if not has_animation: - no_anim_label = QLabel("此模型无动画") + no_anim_label = QLabel("无法识别动画") no_anim_label.setStyleSheet("color:#888;font-style:italic;") animation_layout.addWidget(no_anim_label, 0, 0)