diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 199088f8..98f9815c 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.0000000000]]] + sun_azimuth: [[[0.5000000000,0.9555555556]]] sun_altitude: [[[0.5000000000,1.0000000000]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: diff --git a/core/RotationHandleFull.fbx b/core/RotationHandleFull.fbx new file mode 100755 index 00000000..8c42b1b9 Binary files /dev/null and b/core/RotationHandleFull.fbx differ diff --git a/core/RotationHandleQuarter.fbx b/core/RotationHandleQuarter.fbx new file mode 100755 index 00000000..d12b04bc Binary files /dev/null and b/core/RotationHandleQuarter.fbx differ diff --git a/core/UniformScaleHandle.fbx b/core/UniformScaleHandle.fbx new file mode 100755 index 00000000..dc0bfb89 Binary files /dev/null and b/core/UniformScaleHandle.fbx differ diff --git a/core/event_handler.py b/core/event_handler.py index 8308734d..ae684c7f 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -398,12 +398,8 @@ class EventHandler: if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo: x = evt.get('x', 0) y = evt.get('y', 0) - # 只在前5次调用时输出调试信息,避免刷屏 - if not hasattr(self.world, '_highlight_debug_count'): - self.world._highlight_debug_count = 0 - if self.world._highlight_debug_count < 5: - print(f"更新坐标轴高亮: 鼠标({x}, {y}), 坐标轴存在={bool(self.world.selection.gizmo)}") - self.world._highlight_debug_count += 1 + # 减少高亮调试输出,只在需要时输出 + # 已静默处理,避免控制台刷屏 self.world.selection.updateGizmoHighlight(x, y) # 调用CoreWorld的父类方法处理基础的相机旋转 diff --git a/core/selection.py b/core/selection.py index 22ff8ef5..063239a6 100644 --- a/core/selection.py +++ b/core/selection.py @@ -11,7 +11,7 @@ from PIL.ImageChops import lighter from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, - TransparencyAttrib, Vec4) + TransparencyAttrib, Vec4, CollisionCapsule) from direct.task.TaskManagerGlobal import taskMgr import math @@ -38,6 +38,9 @@ class SelectionSystem: self.gizmoXAxis = None # X轴 self.gizmoYAxis = None # Y轴 self.gizmoZAxis = None # Z轴 + self.gizmoRotXAxis = None + self.gizmoRotYAxis = None + self.gizmoRotZAxis = None self.axis_length = 5.0 # 坐标轴长度(增加到5.0) # 拖拽相关状态 @@ -312,6 +315,8 @@ class SelectionSystem: self._setupGizmoRendering() + self.setupGizmoCollision() + # 现在才显示坐标轴 self.gizmo.show() @@ -329,41 +334,120 @@ class SelectionSystem: if not self.gizmo: return - model_paths = [ - "core/TranslateArrowHandle.fbx", - "EG/core/TranslateArrowHandle.fbx", - ] - arrow_model = None + 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 + + if is_scale_tool: + model_paths = [ + "core/UniformScaleHandle.fbx", + ] + elif is_rotate_tool: + model_paths = [ + "core/RotationHandleQuarter.fbx", + ] + else: + model_paths = [ + "core/TranslateArrowHandle.fbx", + ] + + # model_paths = [ + # "core/TranslateArrowHandle.fbx", + # "EG/core/TranslateArrowHandle.fbx", + # ] + gizmo_model = None + gizmoRot_model = None for path in model_paths: try: - arrow_model = self.world.loader.loadModel(path) - if arrow_model: - print(f"成功加载模型: {path}") - break + if is_rotate_tool: + gizmo_model = self.world.loader.loadModel("core/TranslateArrowHandle.fbx") + gizmoRot_model = self.world.loader.loadModel(path) + else: + gizmo_model = self.world.loader.loadModel(path) + if gizmo_model: + print(f"成功加载模型: {path}") + break except: continue + + x_rHandle = None + y_rHandle = None + z_rHandle = None + + if is_rotate_tool: + self.gizmoRotXAxis = self.gizmo.attachNewNode("gizmo_rot_x_axis") + x_rHandle = gizmoRot_model.copyTo(self.gizmoRotXAxis) + x_rHandle.setName("x_handle") + self.gizmoRotYAxis = self.gizmo.attachNewNode("gizmo_rot_y_axis") + y_rHandle = gizmoRot_model.copyTo(self.gizmoRotYAxis) + y_rHandle.setName("y_handle") + self.gizmoRotZAxis = self.gizmo.attachNewNode("gizmo_rot_z_axis") + z_rHandle = gizmoRot_model.copyTo(self.gizmoRotZAxis) + z_rHandle.setName("z_handle") + self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis") - x_arrow = arrow_model.copyTo(self.gizmoXAxis) - x_arrow.setName("x_arrow") - x_arrow.setHpr(0,-90,0) - x_arrow.setScale(0.1,0.05,0.05) - x_arrow.setPos(0,0,0) + x_handle = gizmo_model.copyTo(self.gizmoXAxis) + x_handle.setName("x_handle") self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis") - y_arrow = arrow_model.copyTo(self.gizmoYAxis) - y_arrow.setName("y_arrow") - y_arrow.setHpr(90,0,0) - y_arrow.setScale(0.1,0.05,0.05) - y_arrow.setPos(0,0,0) + y_handle = gizmo_model.copyTo(self.gizmoYAxis) + y_handle.setName("y_handle") - # 创建Z轴(蓝色) self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis") - z_arrow = arrow_model.copyTo(self.gizmoZAxis) - z_arrow.setName("z_arrow") - # 旋转箭头使其指向Z轴正方向 - z_arrow.setHpr(0, 0, -90) # 根据需要调整旋转 - z_arrow.setScale(0.1,0.05,0.05) - z_arrow.setPos(0, 0, 0) + z_handle = gizmo_model.copyTo(self.gizmoZAxis) + z_handle.setName("z_handle") + + if is_scale_tool: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.6,0.03,0.03) + x_handle.setPos(2.2,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.6,0.03,0.03) + y_handle.setPos(0,2.2,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.6,0.03,0.03) + z_handle.setPos(0,0,2.2) + elif is_rotate_tool: + x_rHandle.setHpr(0,0,90) + x_rHandle.setScale(0.025,0.0125,0.0125) + x_rHandle.setPos(0,0,0) + + y_rHandle.setHpr(0,0,0) + y_rHandle.setScale(0.025,0.0125,0.0125) + y_rHandle.setPos(0,0,0) + + z_rHandle.setHpr(-90,0,0) + z_rHandle.setScale(0.025,0.0125,0.0125) + z_rHandle.setPos(0,0,0) + + x_handle.setHpr(0, -90, 0) + x_handle.setScale(0.1, 0.05, 0.05) + x_handle.setPos(0, 0, 0) + + y_handle.setHpr(90, 0, 0) + y_handle.setScale(0.1, 0.05, 0.05) + y_handle.setPos(0, 0, 0) + + z_handle.setHpr(0, 0, -90) + z_handle.setScale(0.1, 0.05, 0.05) + z_handle.setPos(0, 0, 0) + + self.setGizmoRotAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoRotAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoRotAxisColor("z", self.gizmo_colors["z"]) + else: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.1,0.05,0.05) + x_handle.setPos(0,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.1,0.05,0.05) + y_handle.setPos(0,0,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.1,0.05,0.05) + z_handle.setPos(0,0,0) # 设置初始颜色 self.setGizmoAxisColor("x", self.gizmo_colors["x"]) @@ -379,6 +463,7 @@ class SelectionSystem: def _setupGizmoRendering(self): try: axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] + axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis] for axis_node in axis_nodes: if axis_node: @@ -388,21 +473,45 @@ class SelectionSystem: axis_node.setFogOff() #设置渲染层级,确保大多数对象之前渲染 axis_node.setBin("fixed",30) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) + #axis_node.setDepthWrite(False) + #axis_node.setDepthTest(True) + + 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) + arrow_nodes = [] if self.gizmoXAxis: - x_arrow = self.gizmoXAxis.find("x_arrow") - if x_arrow: - arrow_nodes.append(x_arrow) + x_handle = self.gizmoXAxis.find("x_handle") + if x_handle: + arrow_nodes.append(x_handle) if self.gizmoYAxis: - y_arrow = self.gizmoYAxis.find("y_arrow") - if y_arrow: - arrow_nodes.append(y_arrow) + y_handle = self.gizmoYAxis.find("y_handle") + if y_handle: + arrow_nodes.append(y_handle) if self.gizmoZAxis: - z_arrow = self.gizmoZAxis.find("z_arrow") - if z_arrow: - arrow_nodes.append(z_arrow) + z_handle = self.gizmoZAxis.find("z_handle") + if z_handle: + arrow_nodes.append(z_handle) + + rot_nodes = [] + if self.gizmoRotXAxis: + x_rHandle = self.gizmoRotXAxis.find("x_handle") + if x_rHandle: + rot_nodes.append(x_rHandle) + if self.gizmoRotYAxis: + y_rHandle = self.gizmoRotYAxis.find("y_handle") + if y_rHandle: + rot_nodes.append(y_rHandle) + if self.gizmoRotZAxis: + z_rHandle = self.gizmoRotZAxis.find("z_handle") + if z_rHandle: + rot_nodes.append(z_rHandle) for arrow_node in arrow_nodes: if arrow_node: @@ -410,17 +519,30 @@ class SelectionSystem: arrow_node.setShaderOff() arrow_node.setFogOff() arrow_node.setBin("fixed",31) - arrow_node.setDepthWrite(False) - arrow_node.setDepthTest(False) + #arrow_node.setDepthWrite(False) + #arrow_node.setDepthTest(False) #启用透明度S arrow_node.setTransparency(TransparencyAttrib.MAlpha) + + for rot_node in rot_nodes: + if rot_node: + 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.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) + # self.gizmo.setDepthWrite(False) + #self.gizmo.setDepthTest(False) + except Exception as e: print(f"设置坐标轴渲染属性失败: {str(e)}") @@ -448,12 +570,49 @@ class SelectionSystem: self.clearGizmo() return task.done + 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 + was_scale_tool = getattr(self,'_last_tool_scale_state',False) + was_rotate_tool =getattr(self,'_last_tool_rotate_state',False) + + tool_changed = (is_scale_tool!=was_scale_tool) or (is_rotate_tool != was_rotate_tool) + + if tool_changed: + self._last_tool_scale_state = is_scale_tool + self._last_tool_rotate_state = is_rotate_tool + + if self.gizmoXAxis: + self.gizmoXAxis.removeNode() + self.gizmoXAxis = None + if self.gizmoYAxis: + self.gizmoYAxis.removeNode() + self.gizmoYAxis = None + if self.gizmoZAxis: + self.gizmoZAxis.removeNode() + self.gizmoZAxis = None + if self.gizmoRotXAxis: + self.gizmoRotXAxis.removeNode() + self.gizmoRotXAxis = None + if self.gizmoRotYAxis: + self.gizmoRotYAxis.removeNode() + self.gizmoRotYAxis = None + if self.gizmoRotZAxis: + self.gizmoRotZAxis.removeNode() + self.gizmoRotZAxis = None + + self.createGizmoGeometry() + + self.setGizmoAxisColor("x",self.gizmo_colors["x"]) + self.setGizmoAxisColor("y",self.gizmo_colors["y"]) + self.setGizmoAxisColor("z",self.gizmo_colors["z"]) + + self.setupGizmoCollision() + light_object = self.gizmoTarget.getPythonTag("rp_light_object") if light_object: light_pos = light_object.pos self.gizmo.setPos(light_object.pos) self.gizmoTarget.setPos(light_pos) - else: # 只在必要时更新位置和朝向 self._updateGizmoPositionAndOrientation() @@ -486,13 +645,28 @@ class SelectionSystem: self.gizmo.setPos(center) self._last_gizmo_bounds_update = current_time - # 更新朝向 - parent_node = self.gizmoTarget.getParent() - if parent_node and parent_node != self.world.render: - parent_hpr = parent_node.getHpr() - self.gizmo.setHpr(parent_hpr) + 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()) + self.gizmo.setQuat(self.gizmoTarget.getQuat(self.world.render)) else: - self.gizmo.setHpr(0, 0, 0) + parent_node = self.gizmoTarget.getParent() + if parent_node and parent_node != self.world.render: + parent_hpr = parent_node.getHpr() + self.gizmo.setHpr(parent_hpr) + else: + self.gizmo.setHpr(0,0,0) + + # 更新朝向 + # parent_node = self.gizmoTarget.getParent() + # if parent_node and parent_node != self.world.render: + # parent_hpr = parent_node.getHpr() + # self.gizmo.setHpr(parent_hpr) + # else: + # self.gizmo.setHpr(0, 0, 0) def _updateGizmoScreenSize(self): """动态调整坐标轴大小,保持固定的屏幕大小""" @@ -589,6 +763,97 @@ class SelectionSystem: # except: # pass + + def setGizmoRotAxisColor(self, axis, color): + """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" + try: + from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib + + # 获取对应的轴节点 + axis_nodes = { + "x": self.gizmoRotXAxis, + "y": self.gizmoRotYAxis, + "z": self.gizmoRotZAxis + } + + if axis not in axis_nodes or not axis_nodes[axis]: + return + + axis_node = axis_nodes[axis] + + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node + + #如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node: + children = axis_node.getChildren() + if children.getNumPath()>0: + handle_node = children[0] + + if not handle_node: + print(f"未找到{axis}轴的处理模型") + return + + # 创建或获取材质 + 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) + + # 应用材质 + handle_node.setMaterial(mat, 1) + + + # 设置透明度 + if color[3] < 1.0: + handle_node.setTransparency(TransparencyAttrib.MAlpha) + else: + handle_node.setTransparency(TransparencyAttrib.MNone) + + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 + + handle_node.setBin("fixed",31) + #arrow_node.setDepthWrite(False) + #arrow_node.setDepthTest(True) + + # 保存材质引用以便后续修改 + if axis == "x": + self.xMat = mat + elif axis == "y": + self.yMat = mat + elif axis == "z": + self.zMat = mat + + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 30) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(True) + + except Exception as e: + print(f"设置坐标轴颜色失败: {str(e)}") + # 回退到简单颜色设置 + try: + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) + except: + pass + def setGizmoAxisColor(self, axis, color): """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" try: @@ -606,17 +871,19 @@ class SelectionSystem: axis_node = axis_nodes[axis] - # 查找箭头模型节点 - arrow_node = None - if axis == "x": - arrow_node = axis_node.find("x_arrow") - elif axis == "y": - arrow_node = axis_node.find("y_arrow") - elif axis == "z": - arrow_node = axis_node.find("z_arrow") + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node - if not arrow_node: - print(f"未找到{axis}轴的箭头模型") + #如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node: + children = axis_node.getChildren() + if children.getNumPath()>0: + handle_node = children[0] + + if not handle_node: + print(f"未找到{axis}轴的处理模型") return # 创建或获取材质 @@ -630,20 +897,20 @@ class SelectionSystem: mat.set_roughness(1) # 应用材质 - arrow_node.setMaterial(mat, 1) + handle_node.setMaterial(mat, 1) # 设置透明度 if color[3] < 1.0: - arrow_node.setTransparency(TransparencyAttrib.MAlpha) + handle_node.setTransparency(TransparencyAttrib.MAlpha) else: - arrow_node.setTransparency(TransparencyAttrib.MNone) + handle_node.setTransparency(TransparencyAttrib.MNone) - arrow_node.setLightOff() # 禁用光照影响 - arrow_node.setShaderOff() # 禁用着色器 - arrow_node.setFogOff() # 禁用雾效果 + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 - arrow_node.setBin("fixed",31) + handle_node.setBin("fixed",31) #arrow_node.setDepthWrite(False) #arrow_node.setDepthTest(True) @@ -920,10 +1187,23 @@ class SelectionSystem: # 获取坐标轴中心的世界坐标 gizmo_world_pos = self.gizmo.getPos(self.world.render) + #获取坐标轴的世界朝向(考虑旋转) + gizmo_world_quat = self.gizmo.getQuat(self.world.render) + + #计算各轴在世界坐标系中的实际方向向量 + x_axis_world = gizmo_world_quat.xform(Vec3(1,0,0)) + y_axis_world = gizmo_world_quat.xform(Vec3(0,1,0)) + z_axis_world = gizmo_world_quat.xform(Vec3(0,0,1)) + + x_end = gizmo_world_pos + x_axis_world * self.axis_length + y_end = gizmo_world_pos + y_axis_world * self.axis_length + z_end = gizmo_world_pos + z_axis_world * self.axis_length + + # 计算各轴端点的世界坐标 - x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) - y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) - z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) + # x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) + # y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) + # z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) # 将3D坐标投影到屏幕坐标 def worldToScreen(worldPos): @@ -1019,7 +1299,8 @@ class SelectionSystem: if not self.gizmo or self.isDraggingGizmo: return - # 使用统一的检测方法 + # 使用碰撞检测方法 + #hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) # 简化稳定性检测逻辑 @@ -1037,6 +1318,11 @@ class SelectionSystem: # 高亮新的轴 if hoveredAxis: self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + else: + # 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色 + for axis_name in ["x", "y", "z"]: + if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色 + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) self.gizmoHighlightAxis = hoveredAxis @@ -1074,17 +1360,34 @@ class SelectionSystem: return self.isDraggingGizmo = True - # 使用当前高亮的轴,如果有的话 + + # 使用当前高亮的轴,如果有的话;否则使用传入的轴 if self.gizmoHighlightAxis: self.dragGizmoAxis = self.gizmoHighlightAxis - else: + elif axis and axis in self.gizmo_colors: self.dragGizmoAxis = axis + else: + # 如果没有明确指定轴,尝试通过鼠标位置检测 + self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + + # 如果仍然无法确定拖拽轴,则取消拖拽 + if not self.dragGizmoAxis: + print("开始拖拽失败: 无法确定拖拽轴") + self.isDraggingGizmo = False + return + self.dragStartMousePos = (mouseX, mouseY) # 保存开始拖拽时目标节点的位置和坐标轴的位置 self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 + # 添加对缩放的支持:保存初始缩放值 + if self.world.tool_manager.isScaleTool(): + self.gizmoTargetStartScale = self.gizmoTarget.getScale() + elif self.world.tool_manager.isRotateTool(): + self.gizmoTargetStartHpr = self.gizmoTarget.getHpr() + # 确保正在拖动的轴保持高亮状态 if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: # 先将所有轴恢复为正常颜色 @@ -1094,13 +1397,15 @@ class SelectionSystem: # 然后将当前拖动的轴设置为高亮颜色 self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis]) - self.gizmoHighlightAxis = self.dragGizmoAxis - elif axis and axis in self.gizmo_colors: - for axis_name in self.gizmo_colors.keys(): - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - - self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) - self.gizmoHighlightAxis = axis + # elif axis and axis in self.gizmo_colors: + # for axis_name in self.gizmo_colors.keys(): + # if axis_name != axis: + # self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + # + # self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) + # self.dragGizmoAxis = axis + # + # self.gizmoHighlightAxis = self.dragGizmoAxis print( f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") @@ -1130,10 +1435,55 @@ class SelectionSystem: print("拖拽更新失败: 没有坐标轴起始位置") return + is_scale_tool = self.world.tool_manager.isScaleTool() + is_rotate_tool = self.world.tool_manager.isRotateTool() + # 计算鼠标移动距离(屏幕像素) mouseDeltaX = mouseX - self.dragStartMousePos[0] mouseDeltaY = mouseY - self.dragStartMousePos[1] + if is_scale_tool: + 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) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale * scale_factor, + start_scale.z * scale_factor) + #应用新缩放值 + self.gizmoTarget.setScale(new_scale) + #实时更新属性面板 + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + elif is_rotate_tool: + rotation_speed = 0.5 + rotation_amount = (mouseDeltaX + mouseDeltaY) * rotation_speed + start_hpr = getattr(self,'gizmoTargetStartHpr',self.gizmoTarget.getHpr()) + + if self.dragGizmoAxis == "x": + new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z) + elif self.dragGizmoAxis == "y": + new_hpr = Vec3(start_hpr.x,start_hpr.y+rotation_amount,start_hpr.z) + elif self.dragGizmoAxis == "z": + new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount) + else: + # 默认绕所有轴旋转 + new_hpr = Vec3(start_hpr.x + rotation_amount, + start_hpr.y + rotation_amount, + start_hpr.z + rotation_amount) + self.gizmoTarget.setHpr(new_hpr) + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + # 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影 gizmo_world_pos = self.gizmoStartPos @@ -1155,13 +1505,30 @@ class SelectionSystem: print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}") return - # 确定轴向量的变换上下文 + world_axis_vector = local_axis_vector + if parent_node and parent_node != self.world.render: - transform_mat = parent_node.getMat(self.world.render) - world_axis_vector = transform_mat.xformVec(local_axis_vector) + try: + if parent_node.getTransform().hasMat(): + transform_mat = parent_node.getMat(self.world.render) + if not transform_mat.isSingular(): + world_axis_vector = transform_mat.xformVec(local_axis_vector) + else: + print("警告: 检测到奇异变换矩阵,使用默认轴向量") + else: + print("警告: 父节点没有有效的变换矩阵,使用默认轴向量") + except Exception as e: + print(f"变换计算出错: {e},使用默认轴向量") else: world_axis_vector = local_axis_vector + # 确定轴向量的变换上下文 + # if parent_node and parent_node != self.world.render: + # transform_mat = parent_node.getMat(self.world.render) + # world_axis_vector = transform_mat.xformVec(local_axis_vector) + # else: + # world_axis_vector = local_axis_vector + #axis_end = gizmo_world_pos + world_axis_vector # 投影到屏幕空间 @@ -1184,21 +1551,6 @@ class SelectionSystem: axis_start_screen = worldToScreen(gizmo_world_pos) axis_end_world = gizmo_world_pos + world_axis_vector axis_end_screen = worldToScreen(axis_end_world) - #gizmo_screen = worldToScreen(gizmo_world_pos) - #axis_screen = worldToScreen(axis_end) - - # if not gizmo_screen: - # print("拖拽更新失败: 坐标轴中心不在屏幕内") - # return - # if not axis_screen: - # print("拖拽更新失败: 坐标轴端点不在屏幕内") - # return - # - # # 计算轴在屏幕空间的方向向量 - # screen_axis_dir = ( - # axis_screen[0] - gizmo_screen[0], - # axis_screen[1] - gizmo_screen[1] - # ) if not axis_start_screen or not axis_end_screen: print("拖拽更新失败: 无法获取轴线屏幕坐标") @@ -1209,7 +1561,6 @@ class SelectionSystem: axis_end_screen[1] - axis_start_screen[1] ) - # 归一化屏幕轴方向 import math length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) @@ -1243,50 +1594,36 @@ class SelectionSystem: current_node = self.gizmoTarget.getParent() while current_node and current_node != self.world.render: - node_scale = current_node.getScale() - avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 - total_scale_factor *= avg_scale - current_node = current_node.getParent() + try: + if not current_node.isEmpty(): + node_scale = current_node.getScale() + if node_scale.x > 0 and node_scale.y >0 and node_scale.z >0 : + avg_scale = (node_scale.x + node_scale.y + node_scale.z)/3.0 + total_scale_factor *= avg_scale + #avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 + #total_scale_factor *= avg_scale + current_node = current_node.getParent() + else: + break + except: + break + if total_scale_factor > 0: movement_distance = movement_distance / total_scale_factor currentPos = self.gizmoTargetStartPos - # scale_adjustment = 1.0 - # if parent_node and parent_node!= self.world.render: - # current_node = parent_node - # total_scale = 1.0 - # while current_node and current_node != self.world.render: - # node_scale = current_node.getScale() - # avg_scale = (node_scale.x+node_scale.y + node_scale.z)/3.0 - # total_scale *= avg_scale - # current_node = current_node.getParent() - # if total_scale>0: - # scale_adjustment = 1.0 / total_scale - # # parent_scale = parent_node.getScale() - # # avg_scale = (parent_scale.x+parent_scale.y+parent_scale.z)/3.0 - # # if avg_scale>0: - # # scale_adjustment = 1.0 / avg_scale - # - # - # fixed_pixel_to_world_ratio = 0.01 # 1像素 = 0.01世界单位 - # scale_factor = fixed_pixel_to_world_ratio * scale_adjustment - # - # movement_distance = projected_distance * scale_factor - # # 获取当前位置并只修改选中轴的坐标 - # currentPos = self.gizmoTargetStartPos - # 根据拖拽的轴,只修改对应的坐标分量 if self.dragGizmoAxis == "x": newPos = Vec3(currentPos.x + movement_distance, currentPos.y, currentPos.z) - print(f"X轴移动:{currentPos.x} -> {newPos.x}") + #print(f"X轴移动:{currentPos.x} -> {newPos.x}") elif self.dragGizmoAxis == "y": newPos = Vec3(currentPos.x, currentPos.y + movement_distance, currentPos.z) - print(f"Y轴移动:{currentPos.y} -> {newPos.y}") + #print(f"Y轴移动:{currentPos.y} -> {newPos.y}") elif self.dragGizmoAxis == "z": newPos = Vec3(currentPos.x, currentPos.y, currentPos.z + movement_distance) - print(f"Z轴移动:{currentPos.z} -> {newPos.z}") + #print(f"Z轴移动:{currentPos.z} -> {newPos.z}") else: print(f"未知轴: {self.dragGizmoAxis}") return @@ -1329,10 +1666,10 @@ class SelectionSystem: def stopGizmoDrag(self): """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") - if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: - self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_colors[self.dragGizmoAxis]) - # 不要将 gizmoHighlightAxis 设置为 None,保持当前高亮轴的状态 - # self.gizmoHighlightAxis = None + + # 恢复所有轴的颜色 + for axis_name in ["x", "y", "z"]: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) self.isDraggingGizmo = False self.dragGizmoAxis = None @@ -1341,6 +1678,13 @@ class SelectionSystem: self.gizmoTargetStartPos = None self.gizmoStartPos = None + if hasattr(self, 'gizmoTargetStartScale'): + delattr(self, 'gizmoTargetStartScale') + if hasattr(self, 'gizmoTargetStartHpr'): + delattr(self, 'gizmoTargetStartHpr') + + # 重置高亮轴 + self.gizmoHighlightAxis = None # ==================== 选择管理 ==================== def updateSelection(self, nodePath): @@ -1393,3 +1737,164 @@ class SelectionSystem: if self.selectionBoxTarget and self.selectionBoxTarget.isEmpty(): self.clearSelectionBox() + + def setupGizmoCollision(self): + if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis: + return + + # 清除现有的碰撞节点 + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + # 查找并移除所有现有的碰撞节点 + collision_nodes = axis_node.findAllMatches("**/gizmo_collision_*") + for collision_node in collision_nodes: + collision_node.removeNode() + + # 为每个轴创建碰撞体 + self.createAxisCollision("x", self.gizmoXAxis) + self.createAxisCollision("y", self.gizmoYAxis) + self.createAxisCollision("z", self.gizmoZAxis) + + def createAxisCollision(self, axis_name, axis_node): + # 为单个轴创建碰撞体 + try: + handle_node = axis_node.find(f"{axis_name}_handle") + if not handle_node or handle_node.isEmpty(): + children = axis_node.getChildren() + if children.getNumPaths() > 0: + handle_node = children[0] + else: + print(f"警告: 未找到 {axis_name} 轴的 handle 节点") + return + + collision_node = CollisionNode(f"gizmo_collision_{axis_name}") + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 设置为into对象 + collision_node.setFromCollideMask(BitMask32.allOff()) # 不作为from对象 + + # 调整碰撞尺寸以匹配实际的轴长度和坐标轴缩放 + scale_factor = self.gizmo.getScale().x if self.gizmo else 1.0 + axis_length = 2.0 * scale_factor + radius = 0.3 * scale_factor + + # 根据轴的类型创建合适的碰撞体 + if axis_name == "x": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(axis_length, 0, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "y": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, axis_length, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "z": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, 0, axis_length), + radius + ) + collision_node.addSolid(capsule) + + # 将碰撞节点附加到handle节点,使其与可视化几何体保持一致 + collision_np = handle_node.attachNewNode(collision_node) + + # 设置标签以便识别 + collision_np.setTag("gizmo_axis", axis_name) + collision_np.setTag("pickable", "1") + + collision_np.hide() # 隐藏碰撞体,只用于检测 + + print(f"✓ 成功创建 {axis_name} 轴碰撞体") + + except Exception as e: + print(f"创建{axis_name}轴碰撞体失败: {e}") + import traceback + traceback.print_exc() + + def detectGizmoAxisWithCollision(self, mouseX, mouseY): + # 使用碰撞体检测鼠标是否悬停在坐标轴上 + if not self.gizmo: + return None + + try: + ray = CollisionRay() + + win_width, win_height = self.world.getWindowSize() + + mouse_x_ndc = (mouseX / win_width) * 2.0 - 1.0 + mouse_y_ndc = 1.0 - (mouseY / win_height) * 2.0 + + ray.setFromLens(self.world.cam.node(), mouse_x_ndc, mouse_y_ndc) + + traverser = CollisionTraverser("gizmo_traverser") + handler = CollisionHandlerQueue() + + # 创建射线节点 + ray_node = CollisionNode('mouseRay') + ray_node.addSolid(ray) + ray_node.setFromCollideMask(BitMask32.bit(1)) # 射线作为from对象 + ray_node.setIntoCollideMask(BitMask32.allOff()) # 射线不作为into对象 + ray_np = self.world.render.attachNewNode(ray_node) + + # 为所有轴的碰撞体设置正确的掩码并添加到遍历器 + collision_found = False + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + collision_node_path = axis_node.find("**/gizmo_collision_*") + if not collision_node_path.isEmpty(): + collision_node = collision_node_path.node() + collision_node.setFromCollideMask(BitMask32.allOff()) # 碰撞体不作为from对象 + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 碰撞体作为into对象 + collision_found = True + + if not collision_found: + ray_np.removeNode() + return None + + # 执行碰撞检测 - 这里是关键修复点 + traverser.addCollider(ray_np, handler) + traverser.traverse(self.world.render) + + ray_np.removeNode() + + # 检查是否有碰撞 + if handler.getNumEntries() > 0: + handler.sortEntries() + closest_entry = handler.getEntry(0) + + # 获取碰撞的对象 + collided_object = closest_entry.getIntoNodePath() + axis_tag = collided_object.getTag("gizmo_axis") + + if axis_tag in ["x", "y", "z"]: + return axis_tag + + return None + + except Exception as e: + print(f"使用碰撞体检测坐标轴失败: {e}") + import traceback + traceback.print_exc() + return None + + def debugGizmoCollision(self): + print("===碰撞体调试信息===") + for axis_name in ["x","y","z"]: + axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis") + if axis_node: + handle_node = axis_node.find(f"{axis_name}_handle") + collision_node = axis_node.find("**/gizmo_collision_*") + print(f"{axis_name.upper()}轴:") + print(f" - 轴节点: {axis_node}") + print(f" - Handle节点: {handle_node}") + print(f" - 碰撞节点: {collision_node}") + if not collision_node.isEmpty(): + print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") + else: + print(f"{axis_name.upper()}轴节点不存在") \ No newline at end of file diff --git a/core/tool_manager.py b/core/tool_manager.py index c9344f69..4f5f4cdf 100644 --- a/core/tool_manager.py +++ b/core/tool_manager.py @@ -150,49 +150,3 @@ class ToolManager: except Exception as e: print(f"❌ 启动插件配置器失败: {e}") return False - - def cleanup_processes(self): - """清理所有启动的进程""" - try: - # 清理插件配置器进程 - if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process: - if self._plugin_configurator_process.poll() is None: - print("🔄 正在关闭插件配置器...") - self._plugin_configurator_process.terminate() - try: - # 等待进程结束,最多等待5秒 - self._plugin_configurator_process.wait(timeout=5) - print("✅ 插件配置器已正常关闭") - except subprocess.TimeoutExpired: - print("⚠️ 插件配置器未响应,强制关闭...") - self._plugin_configurator_process.kill() - self._plugin_configurator_process.wait() - print("✅ 插件配置器已强制关闭") - self._plugin_configurator_process = None - - # 清理材质编辑器进程(如果存在) - if hasattr(self, '_material_editor_process') and self._material_editor_process: - if self._material_editor_process.poll() is None: - print("🔄 正在关闭材质编辑器...") - self._material_editor_process.terminate() - try: - self._material_editor_process.wait(timeout=5) - print("✅ 材质编辑器已正常关闭") - except subprocess.TimeoutExpired: - print("⚠️ 材质编辑器未响应,强制关闭...") - self._material_editor_process.kill() - self._material_editor_process.wait() - print("✅ 材质编辑器已强制关闭") - self._material_editor_process = None - - except Exception as e: - print(f"⚠️ 清理进程时出错: {e}") - - def get_plugin_configurator_status(self): - """获取插件配置器的运行状态""" - if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process: - if self._plugin_configurator_process.poll() is None: - return "运行中" - else: - return "已停止" - return "未启动" diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 550f0bb3..0c87a889 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -15,6 +15,14 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit, QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout) from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt +# 尝试导入 QtWebEngineWidgets,如果失败则设置为 None +try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + WEB_ENGINE_AVAILABLE = True +except ImportError: + QWebEngineView = None + WEB_ENGINE_AVAILABLE = False + print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") class GUIManager: @@ -139,37 +147,159 @@ class GUIManager: print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") return entry - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): """创建3D空间文本""" - from panda3d.core import TextNode - + from panda3d.core import TextNode,Material,Vec4,ColorAttrib,TransparencyAttrib + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') textNode.setText(text) textNode.setAlign(TextNode.ACenter) if self.world.getChineseFont(): textNode.setFont(self.world.getChineseFont()) - + + textNode.setTextColor(Vec4(1,1,1,1)) + textNodePath = self.world.render.attachNewNode(textNode) textNodePath.setPos(*pos) - textNodePath.setScale(size) - textNodePath.setColor(1, 1, 0, 1) - textNodePath.setBillboardAxis() # 让文本总是面向相机 - + textNodePath.setScale(size,size,size) + #textNodePath.setBillboardAxis() # 让文本总是面向相机 + + # 为3D文本创建默认材质 + material = Material(f"text-material-{len(self.gui_elements)}") + material.setBaseColor(Vec4(1, 1, 1, 1)) # 白色 + material.setDiffuse(Vec4(1, 1, 1, 1)) + material.setAmbient(Vec4(0.5, 0.5, 0.5, 1)) + material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + #material.setEmission(0,0,0,1) + + textNodePath.setMaterial(material, 1) + + textNodePath.setTransparency(TransparencyAttrib.MAlpha) + textNodePath.setAttrib(ColorAttrib.makeFlat(Vec4(1, 1, 1, 1))) + textNodePath.setLightOff() + # 为GUI元素添加标识 textNodePath.setTag("gui_type", "3d_text") textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") textNodePath.setTag("gui_text", text) textNodePath.setTag("is_gui_element", "1") - + + textNodePath.setDepthWrite(True) # 确保深度写入 + textNodePath.setDepthTest(True) # 启用深度测试 + textNodePath.setBin("fixed", 0) # 设置渲染层级,避免被遮挡 + + # if hasattr(self, 'render_pipeline') and self.render_pipeline: + # try: + # self.render_pipeline.set_effect( + # textNodePath, + # "effects/default.yaml", + # { + # "normal_mapping": False, + # "render_gbuffer": False, + # "alpha_testing": True, + # "parallax_mapping": False, + # "render_shadow": False, + # "render_envmap": False + # }, + # 50 + # ) + # except Exception as e: + # print(f"⚠️ PBR效果应用失败: {e}") + self.gui_elements.append(textNodePath) # 安全地调用updateSceneTree if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() - + print(f"✓ 创建3D文本: {text} (世界位置: {pos})") return textNodePath + def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib + + # 参数类型检查和转换 + if isinstance(size, (list, tuple)): + if len(size) >= 2: + x_size, y_size = float(size[0]), float(size[1]) + else: + x_size = y_size = float(size[0]) if size else 1.0 + else: + x_size = y_size = float(size) + + # 创建卡片 + cm = CardMaker('gui_3d_image') + cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) + + # 创建3D图像节点 + image_node = self.world.render.attachNewNode(cm.generate()) + image_node.setPos(*pos) + + # 设置面向摄像机 + #image_node.setBillboardAxis() # 让图像总是面向相机 + + # 创建支持贴图的材质 + # mat = Material() + # mat.setName("GUI3DImageMaterial") + # color = LColor(1, 1, 1, 1) + # mat.set_base_color(color) + # mat.set_roughness(0.5) + # mat.set_metallic(0.0) + # image_node.set_material(mat) + + # 为3D图像创建独立的材质 + material = Material(f"image-material-{len(self.gui_elements)}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + image_node.setMaterial(material, 1) + + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + self.update3DImageTexture(image_node, image_path) + + # 应用PBR效果(如果可用) + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.set_effect( + image_node, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": False, + "render_envmap": True, + "disable_children_effects": True + }, + 50 + ) + print("✓ GUI 3D图像PBR效果已应用") + except Exception as e: + print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + + # 为GUI元素添加标识(效仿3D文本方法) + image_node.setTag("gui_type", "3d_image") + image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + if image_path: + image_node.setTag("gui_image_path", image_path) + image_node.setTag("is_gui_element", "1") + + self.gui_elements.append(image_node) + + # 更新场景树 + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + + print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") + return image_node def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): @@ -262,15 +392,16 @@ class GUIManager: except Exception as e: print(f"删除GUI元素失败: {str(e)}") return False - + + # 在 gui_manager.py 中确保 editGUIElement 方法正确处理文本颜色 def editGUIElement(self, gui_element, property_name, value): """编辑GUI元素属性""" try: from panda3d.core import TextNode - + gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") - + if property_name == "text": if gui_type in ["button", "label"]: gui_element['text'] = value @@ -285,7 +416,7 @@ class GUIManager: print(f"成功更新3D文本: {value}") else: print(f"警告: {gui_type}节点类型为{type(gui_element.node())},不是TextNode类型") - + elif gui_type == "virtual_screen": # 对于虚拟屏幕,需要找到TextNode子节点 print(f"虚拟屏幕有 {gui_element.getNumChildren()} 个子节点") @@ -297,36 +428,53 @@ class GUIManager: text_found = True print(f"成功更新虚拟屏幕文本: {value}") break - + if not text_found: print(f"警告: 在{gui_type}中未找到TextNode子节点") - + gui_element.setTag("gui_text", value) - + + 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) + # 更新 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 + elif property_name == "position": if isinstance(value, (list, tuple)) and len(value) >= 3: gui_element.setPos(*value[:3]) - + elif property_name == "scale": if isinstance(value, (int, float)): gui_element.setScale(value) elif isinstance(value, (list, tuple)) and len(value) >= 3: gui_element.setScale(*value[:3]) - - elif property_name == "color": - if isinstance(value, (list, tuple)) and len(value) >= 3: - if gui_type in ["button", "label"]: - gui_element['frameColor'] = value - else: - gui_element.setColor(*value) - + print(f"编辑GUI元素 {gui_type}: {property_name} = {value}") return True - + except Exception as e: print(f"编辑GUI元素失败: {str(e)}") + import traceback + traceback.print_exc() return False - + def duplicateGUIElement(self, gui_element): """复制GUI元素""" try: @@ -346,6 +494,9 @@ class GUIManager: self.createGUIEntry(new_pos, gui_text + "_副本") elif gui_type == "3d_text": self.createGUI3DText(new_pos, gui_text + "_副本") + elif gui_type == "3d_image": + image_path = gui_element.getTag("image_path") + self.createGUI3DImage(new_pos,image_path,size=(2,2)) elif gui_type == "virtual_screen": self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本") @@ -581,6 +732,19 @@ class GUIManager: text_font=self.world.getChineseFont() if self.world.getChineseFont() else None ) y_pos -= spacing + + #3D图片工具 + btn_image = DirectButton( + parent = self.guiEditPanel, + text="3D图片", + pos=(0,0,y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["3d_image"], + frameColor=(0.2,0.8,0.8,1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing # 虚拟屏幕工具 btn_screen = DirectButton( @@ -594,6 +758,43 @@ class GUIManager: text_font=self.world.getChineseFont() if self.world.getChineseFont() else None ) y_pos -= spacing + + #Cesium 集成工具 (仅在Webengine 可用时显示) + if WEB_ENGINE_AVAILABLE: + label_cesium = DirectLabel( + parent=self.guiEditPanel, + text="Cesium 集成", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= 0.08 + + # 切换 Cesium 视图按钮 + btn_toggle_cesium = DirectButton( + parent=self.guiEditPanel, + text="切换地图视图", + pos=(0, 0, y_pos), + scale=0.04, + command=self.toggleCesiumView, + frameColor=(0.2, 0.8, 0.6, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 刷新 Cesium 视图按钮 + btn_refresh_cesium = DirectButton( + parent=self.guiEditPanel, + text="刷新地图", + pos=(0, 0, y_pos), + scale=0.04, + command=self.refreshCesiumView, + frameColor=(0.6, 0.8, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing # 分隔线 y_pos -= 0.1 @@ -741,6 +942,8 @@ class GUIManager: element = self.createGUIEntry(pos, f"输入框_{len(self.gui_elements)}") elif gui_type == "3d_text": element = self.createGUI3DText(pos, f"3D文本_{len(self.gui_elements)}") + elif gui_type == "3d_image": + element = self.createGUI3DImage(pos) elif gui_type == "virtual_screen": element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}") else: @@ -950,4 +1153,510 @@ class GUIManager: print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})") except Exception as e: - print(f"编辑2D GUI位置失败: {str(e)}") \ No newline at end of file + print(f"编辑2D GUI位置失败: {str(e)}") + + def update3DImageTexture(self, model_nodepath, image_path): + from panda3d.core import Texture + + 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) + + # 应用纹理到模型 + model_nodepath.setTexture(new_texture, 1) + + # 更新标签 + model_nodepath.setTag("gui_image_path", image_path) + + # 确保材质设置正确 + if not model_nodepath.has_material(): + from panda3d.core import Material, LColor + mat = Material() + mat.setName(f"image-material-{id(model_nodepath)}") + 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) + model_nodepath.setMaterial(mat, 1) + + print(f"✅ 3D图像纹理已更新为: {image_path}") + else: + print(f"❌ 无法加载纹理: {image_path}") + except Exception as e: + print(f"❌ 更新纹理时出错: {e}") + + # 替换现有的 createCesiumView 方法 + + def createCesiumView(self, main_window=None): + """创建 Cesium 视图窗口(离线版本)""" + if not WEB_ENGINE_AVAILABLE: + print("❌ 无法创建Cesium视图: Web引擎不可用") + return None + + try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWidgets import QDockWidget + from PyQt5.QtCore import QUrl + import os + + # 尝试获取主窗口引用 + if main_window is None: + print("🔍 尝试获取主窗口引用...") + + # 检查各种可能的主窗口引用 + if hasattr(self.world, 'interface_manager'): + print(f" - interface_manager 存在: {self.world.interface_manager}") + if hasattr(self.world.interface_manager, 'main_window'): + main_window = self.world.interface_manager.main_window + print(f" - interface_manager.main_window: {main_window}") + + if main_window is None and hasattr(self.world, 'main_window'): + main_window = self.world.main_window + print(f" - world.main_window: {main_window}") + + # 如果仍然没有主窗口,尝试从树形控件获取 + if main_window is None and self.world.treeWidget: + try: + main_window = self.world.treeWidget.window() + print(f" - 从 treeWidget 获取窗口: {main_window}") + except: + pass + + if main_window is None: + print("✗ 无法获取主窗口引用") + return None + else: + print(f"✅ 使用传入的主窗口引用: {main_window}") + + # 检查主窗口是否有效 + if not hasattr(main_window, 'addDockWidget'): + print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") + return None + + # 检查是否已经存在 Cesium 视图 + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + print("⚠ Cesium 视图已经存在") + # 将其前置显示 + element.show() + element.raise_() + return element + + # 创建停靠窗口 + print(f"🔧 创建 Cesium 停靠窗口,父窗口: {main_window}") + cesium_dock = QDockWidget("Cesium 地图视图(离线)", main_window) + cesium_dock.setObjectName("CesiumView") + + # 创建 Web 视图 + self.cesium_view = QWebEngineView() + + # 使用本地 HTML 文件(离线模式) + local_html_path = os.path.abspath("./cesium_offline.html") + if os.path.exists(local_html_path): + print(f"🌐 加载离线 Cesium: file://{local_html_path}") + self.cesium_view.load(QUrl(f"file://{local_html_path}")) + else: + print("⚠️ 离线文件不存在,使用在线版本") + self.cesium_view.load(QUrl("http://localhost:8080/Apps/HelloWorld.html")) + + # 设置内容 + cesium_dock.setWidget(self.cesium_view) + + # 添加到主窗口 + print("📍 将 Cesium 视图添加到主窗口") + main_window.addDockWidget(Qt.RightDockWidgetArea, cesium_dock) + + # 添加到GUI元素列表以便管理 + self.gui_elements.append(cesium_dock) + + print("✓ Cesium 离线视图已创建并集成到项目中") + return cesium_dock + + except Exception as e: + print(f"✗ 创建 Cesium 视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def toggleCesiumView(self): + """切换 Cesium 视图显示状态""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法切换 Cesium 视图") + return None + + try: + # 查找现有的 Cesium 视图 + cesium_dock = None + cesium_index = -1 + for i, element in enumerate(self.gui_elements): + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + cesium_dock = element + cesium_index = i + break + + # 如果存在则移除,否则创建 + if cesium_dock: + # 获取主窗口引用以正确移除停靠窗口 + main_window = None + if (hasattr(self.world, 'interface_manager') and + hasattr(self.world.interface_manager, 'main_window') and + self.world.interface_manager.main_window): + main_window = self.world.interface_manager.main_window + elif hasattr(self.world, 'main_window') and self.world.main_window: + main_window = self.world.main_window + + if main_window and hasattr(main_window, 'removeDockWidget'): + main_window.removeDockWidget(cesium_dock) + + # 从列表中移除 + if cesium_index >= 0: + self.gui_elements.pop(cesium_index) + + print("✓ Cesium 视图已隐藏") + return None + else: + return self.createCesiumView() + + except Exception as e: + print(f"✗ 切换 Cesium 视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def refreshCesiumView(self): + """刷新 Cesium 视图""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法刷新 Cesium 视图") + return False + + try: + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + web_view = element.widget() + if isinstance(web_view, QWebEngineView): + web_view.reload() + print("✓ Cesium 视图已刷新") + return True + print("⚠ 未找到 Cesium 视图") + return False + except Exception as e: + print(f"✗ 刷新 Cesium 视图失败: {str(e)}") + return False + + def updateCesiumURL(self, url): + """更新 Cesium 视图的 URL""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法更新 Cesium URL") + return False + + try: + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + web_view = element.widget() + if isinstance(web_view, QWebEngineView): + from PyQt5.QtCore import QUrl + web_view.load(QUrl(url)) + print(f"✓ Cesium URL 已更新为: {url}") + return True + print("⚠ 未找到 Cesium 视图") + return False + except Exception as e: + print(f"✗ 更新 Cesium URL 失败: {str(e)}") + return False + + # 在 GUIManager 类中添加以下方法 + + def addModelToCesium(self, model_id, model_url, longitude, latitude, height=0, scale=1.0): + """向 Cesium 添加模型""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法操作 Cesium") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 转义特殊字符以防止 JavaScript 语法错误 + escaped_model_id = str(model_id).replace("'", "\\'") + escaped_model_url = str(model_url).replace("'", "\\'").replace("\\", "/") + + # 构造 JavaScript 调用 + js_code = f""" + (function() {{ + if (window.CesiumAPI && typeof window.CesiumAPI.addModel === 'function') {{ + try {{ + var result = window.CesiumAPI.addModel( + '{escaped_model_id}', + '{escaped_model_url}', + {{ + longitude: {longitude}, + latitude: {latitude}, + height: {height} + }}, + {scale} + ); + console.log('Cesium 添加模型结果:', result); + return result || {{success: true, message: 'Model added'}}; + }} catch (error) {{ + console.error('JavaScript 错误:', error); + return {{success: false, message: 'JavaScript error: ' + error.message}}; + }} + }} else {{ + console.error('CesiumAPI.addModel 不可用'); + return {{success: false, message: 'CesiumAPI.addModel not available'}}; + }} + }})(); + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功在 Cesium 中添加模型: {model_id}") + else: + print(f"✗ 在 Cesium 中添加模型失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送添加模型请求: {model_id}") + except Exception as callback_error: + print(f"✗ 处理回调结果时出错: {callback_error}") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 添加模型到 Cesium 失败: {e}") + import traceback + traceback.print_exc() + return False + + # 添加新的方法来集成 Panda3D 场景中的 Cesium Tiles + def addCesiumTilesetToScene(self, tileset_name, tileset_url, position=(0, 0, 0)): + """在 Panda3D 场景中添加 Cesium 3D Tiles""" + try: + # 使用场景管理器加载 tileset + tileset_node = self.world.scene_manager.load_cesium_tileset(tileset_url, position) + + if tileset_node: + # 添加到 GUI 元素列表以便管理 + self.gui_elements.append({ + 'type': 'cesium_tileset', + 'name': tileset_name, + 'node': tileset_node, + 'url': tileset_url + }) + + print(f"✓ 在场景中添加 Cesium tileset: {tileset_name}") + return tileset_node + else: + print(f"✗ 在场景中添加 Cesium tileset 失败: {tileset_name}") + return None + + except Exception as e: + print(f"✗ 在场景中添加 Cesium tileset 出错: {e}") + return None + + def removeModelFromCesium(self, model_id): + """从 Cesium 移除模型""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 构造 JavaScript 调用 + js_code = f""" + if (window.CesiumAPI && typeof window.CesiumAPI.removeModel === 'function') {{ + var result = window.CesiumAPI.removeModel('{model_id}'); + result; + }} else {{ + {{success: false, message: 'CesiumAPI.removeModel not available'}}; + }} + """ + + # 定义回调函数处理结果 + def handle_result(result): + if result and isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功从 Cesium 中移除模型: {model_id}") + else: + print(f"✗ 从 Cesium 中移除模型失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送移除模型请求: {model_id} (无法获取详细结果)") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 从 Cesium 移除模型失败: {e}") + return False + + def updateCesiumModelPosition(self, model_id, longitude, latitude, height=0): + """更新 Cesium 中模型的位置""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 使用更安全的 JavaScript 字符串构造方式 + escaped_model_id = model_id.replace("'", "\\'") + + # 构造 JavaScript 调用 + js_code = f""" + (function() {{ + if (window.CesiumAPI && typeof window.CesiumAPI.updateModelPosition === 'function') {{ + try {{ + var result = window.CesiumAPI.updateModelPosition( + '{escaped_model_id}', + {{ + longitude: {longitude}, + latitude: {latitude}, + height: {height} + }} + ); + return result || {{success: true, message: 'Position updated'}}; + }} catch (error) {{ + return {{success: false, message: 'JavaScript error: ' + error.message}}; + }} + }} else {{ + return {{success: false, message: 'CesiumAPI.updateModelPosition not available'}}; + }} + }})(); + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功更新 Cesium 中模型位置: {model_id}") + else: + print(f"✗ 更新 Cesium 中模型位置失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送更新模型位置请求: {model_id}") + except Exception as callback_error: + print(f"✗ 处理回调结果时出错: {callback_error}") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 更新 Cesium 中模型位置失败: {e}") + return False + + def getAllCesiumModels(self): + """获取 Cesium 中所有模型的列表""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return None + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return None + + # 构造 JavaScript 调用 + js_code = """ + if (window.CesiumAPI && typeof window.CesiumAPI.getAllModels === 'function') { + var result = window.CesiumAPI.getAllModels(); + JSON.stringify(result); + } else { + JSON.stringify({success: false, message: 'CesiumAPI.getAllModels not available'}); + } + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, str): + import json + result = json.loads(result) + + if result and result.get('success', False): + models = result.get('models', []) + print(f"✓ Cesium 中的模型列表: {models}") + return models + else: + print(f"✗ 获取 Cesium 模型列表失败: {result.get('message', 'Unknown error')}") + return [] + except Exception as e: + print(f"✗ 解析 Cesium 模型列表结果失败: {e}") + return [] + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code) + return None # 异步操作,实际结果通过回调处理 + + except Exception as e: + print(f"✗ 获取 Cesium 模型列表失败: {e}") + return None + + # 添加一个便捷方法来加载本地模型文件 + def addLocalModelToCesium(self, model_id, local_model_path, longitude, latitude, height=0, scale=1.0): + """向 Cesium 添加本地模型文件""" + try: + # 将本地路径转换为相对路径或 URL + import os + if os.path.exists(local_model_path): + # 如果 Cesium 服务器可以访问该路径,可以直接使用 + # 否则需要将模型文件放在 Cesium 的静态资源目录中 + model_url = local_model_path.replace('\\', '/') # 确保使用正斜杠 + return self.addModelToCesium(model_id, model_url, longitude, latitude, height, scale) + else: + print(f"✗ 模型文件不存在: {local_model_path}") + return False + except Exception as e: + print(f"✗ 添加本地模型失败: {e}") + return False diff --git a/main.py b/main.py index 065ef800..2ca74757 100644 --- a/main.py +++ b/main.py @@ -183,6 +183,10 @@ class MyWorld(CoreWorld): """创建3D空间文本""" return self.gui_manager.createGUI3DText(pos, text, size) + def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)): + """创建3D图片""" + return self.gui_manager.createGUI3DImage(pos,text,size) + def createSpotLight(self,pos=(-20,0,5)): """创建聚光灯""" return self.scene_manager.createSpotLight(pos) @@ -673,6 +677,15 @@ class MyWorld(CoreWorld): "streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None } + def loadCesiumTileset(self,tileset_url,position=(0,0,0)): + return self.scene_manager.load_cesium_tileset(tileset_url,position) + + def addCesiumTileset(self,name,url,position=(0,0,0)): + if hasattr(self,'gui_manager') and self.gui_manager: + return self.gui_manager.addCesiumTilesetToScene(name,url,position) + else: + return self.scene_manager.load_cesium_tileset(url,position) + # ==================== 项目管理功能代理 ==================== # 以下函数代理到project_manager模块的对应功能 diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 75671fe2..a3ae792a 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -12,11 +12,57 @@ from panda3d.core import ( MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, BitMask32, TransparencyAttrib,LColor ) +import json +import aiohttp +import asyncio +from pathlib import Path from panda3d.egg import EggData, EggVertexPool from direct.actor.Actor import Actor from QPanda3D.Panda3DWorld import get_render_pipeline from scene import util +class CesiumIntegration: + def __init__(self, scene_manager): + self.scene_manager = scene_manager + self.world = scene_manager.world + self.tilesets = {} + + def add_tileset(self,name,url,position=(0,0,0)): + try: + tileset_node = self.scene_manager.load_cesium_tileset(url,position) + + if tileset_node: + self.tilesets[name] = { + 'node':tileset_node, + 'url':url, + 'position':position + } + print(f"✓ 添加 Cesium tileset: {name}") + return tileset_node + else: + print(f"✗ 添加 Cesium tileset 失败: {name}") + return None + except Exception as e: + print(f"✗ 添加 Cesium tileset 出错: {e}") + return None + + def remove_tileset(self, name): + """移除 tileset""" + if name in self.tilesets: + tileset_info = self.tilesets[name] + tileset_info['node'].removeNode() + del self.tilesets[name] + print(f"✓ 移除 Cesium tileset: {name}") + return True + return False + + def get_tileset(self, name): + """获取 tileset""" + return self.tilesets.get(name, None) + + def list_tilesets(self): + """列出所有 tilesets""" + return list(self.tilesets.keys()) class SceneManager: """场景管理器 - 统一管理场景中的所有元素""" @@ -33,6 +79,8 @@ class SceneManager: self.Spotlight = [] self.Pointlight = [] + self.tilesets = [] #来存储tilesets + self.cesium_integration = CesiumIntegration(self) print("✓ 场景管理系统初始化完成") @@ -1242,3 +1290,267 @@ except Exception as e: print(f"[PyAssimp转换] 转换过程出错: {e}") return False + def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)): + try: + print(f"加载 Cesium 3D Tiles: {tileset_url}") + + # 创建一个容器节点来管理tileset + node_name = f"cesium_tileset_{len(self.tilesets)}" + tileset_node = self.world.render.attachNewNode(node_name) + tileset_node.setPos(*position) + + #添加标签以便场景树识别 + tileset_node.setTag("is_scene_element","1") + tileset_node.setTag("element_type","cesium_tileset") + tileset_node.setTag("tileset_url",tileset_url) + tileset_node.setTag("file",f"tileset_{len(self.tilesets)}") + + # 存储tileset信息 + tileset_info = { + 'url': tileset_url, + 'node': tileset_node, + 'position': position, + 'tiles': {} + } + + self.tilesets.append(tileset_info) + + # 创建一个临时的可视化占位符,让用户能看到节点已添加 + self._create_placeholder_geometry(tileset_node) + + # 异步加载tileset数据 + self._load_tileset_async(tileset_url, tileset_info) + + # 更新场景树 + self.updateSceneTree() + print(f"✓ Cesium 3D Tiles 加载请求已发送") + return tileset_node + + except Exception as e: + print(f"❌ 加载 Cesium 3D Tiles 失败: {e}") + import traceback + traceback.print_exc() + return None + + def _load_tileset_async(self, tileset_url, tileset_info): + """异步加载 tileset 数据""" + + async def load_tileset(): + try: + async with aiohttp.ClientSession() as session: + async with session.get(tileset_url) as response: + if response.status == 200: + tileset_data = await response.json() + self._parse_tileset(tileset_data, tileset_info) + print(f"✓ Tileset 数据加载完成") + else: + print(f"✗ Tileset 加载失败: {response.status}") + except Exception as e: + print(f"✗ Tileset 加载出错: {e}") + + # 在 Panda3D 的任务系统中运行异步任务 + task = asyncio.ensure_future(load_tileset()) + self._current_asyncio_task = task # 保存任务引用 + self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True) + + def _check_async_task(self, panda3d_task): + # 检查 asyncio 任务是否完成 + if hasattr(self, '_current_asyncio_task'): + if self._current_asyncio_task.done(): + try: + self._current_asyncio_task.result() + except Exception as e: + print(f"异步任务出错:{e}") + # 返回 Panda3D 任务管理器的完成状态 + return panda3d_task.done # 注意是 done 而不是 DONE + # 返回 Panda3D 任务管理器的继续状态 + return panda3d_task.cont # 注意是 cont 而不是 CONTINUE + + def _parse_tileset(self,tileset_data,tileset_info): + try: + root = tileset_data.get('root',{}) + self._parse_tile(root,tileset_info['node'],tileset_info) + print("✓ Tileset 解析完成") + except Exception as e: + print(f"✗ Tileset 解析出错: {e}") + + def _parse_tile(self, tile_data, parent_node, tileset_info): + try: + # 获取tileID + tile_id = f"tile_{len(tileset_info['tiles'])}" + print(f"创建tile节点: {tile_id}") + # 创建tile节点 + tile_node = parent_node.attachNewNode(tile_id) + + tileset_info['tiles'][tile_id] = { + 'node': tile_node, + 'data': tile_data, + 'loaded': False + } + + # 如果有内容,创建占位几何体 + if 'content' in tile_data: + print(f"为tile {tile_id} 创建几何体") + self._create_tile_geometry(tile_node) + # 递归解析子tiles + children = tile_data.get('children', []) + print(f"Tile {tile_id} 有 {len(children)} 个子节点") + for child_data in children: + self._parse_tile(child_data, tile_node, tileset_info) + except Exception as e: + print(f"✗ Tile 解析出错: {e}") + import traceback + traceback.print_exc() + + def _create_tile_geometry(self,parent_node): + """为 tile 创建占位几何体""" + try: + # 创建一个简单的立方体作为占位符 + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + + format = GeomVertexFormat.getV3n3c4() + vdata = GeomVertexData('tile_cube', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + color = GeomVertexWriter(vdata, 'color') + + # 定义立方体顶点 + vertices = [ + (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5), + (-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5) + ] + + for vert in vertices: + vertex.addData3f(*vert) + normal.addData3f(0, 0, 1) + color.addData4f(0.2, 0.6, 0.8, 1.0) + + # 创建几何体 + 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) + + prim.closePrimitive() + geom.addPrimitive(prim) + + # 创建几何节点 + geom_node = GeomNode('tile_geometry') + geom_node.addGeom(geom) + + # 添加到场景 + cube_node = parent_node.attachNewNode(geom_node) + cube_node.setScale(1000) # 放大以便观察 + + # 添加材质 + material = Material() + material.setBaseColor((0.2, 0.6, 0.8, 1.0)) + material.setSpecular((0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + cube_node.setMaterial(material) + + except Exception as e: + print(f"✗ 创建 tile 几何体出错: {e}") + + def _create_placeholder_geometry(self, parent_node): + """创建一个简单的占位符几何体,让用户能看到节点""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + + # 创建简单的立方体作为占位符 + format = GeomVertexFormat.getV3n3c4() + vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + 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) + ] + + # 使用更鲜明的颜色 + for vert in vertices: + vertex.addData3f(*vert) + normal.addData3f(0, 0, 1) + color.addData4f(0.0, 1.0, 1.0, 1.0) # 青色 + + # 创建几何体 + 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) + + prim.closePrimitive() + geom.addPrimitive(prim) + + # 创建几何节点 + geom_node = GeomNode('tileset_placeholder') + geom_node.addGeom(geom) + + # 添加到场景 + cube_node = parent_node.attachNewNode(geom_node) + cube_node.setScale(5) # 适当大小 + + # 添加材质 + material = Material() + material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色 + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) + cube_node.setMaterial(material) + + # 添加标识标签 + cube_node.setTag("element_type", "cesium_placeholder") + + print("✓ 占位符几何体创建完成") + return cube_node + except Exception as e: + print(f"✗ 创建占位符几何体出错: {e}") + import traceback + traceback.print_exc() + return None + + + diff --git a/ui/interface_manager.py b/ui/interface_manager.py index 2fc489db..9797f3c2 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -74,6 +74,10 @@ class InterfaceManager: duplicateAction = menu.addAction("复制") duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath)) + elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset": + deleteAction = menu.addAction("删除 Cesium Tileset") + deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) + else: # 为模型节点或其子节点添加删除选项 parentItem = item.parent() @@ -88,6 +92,40 @@ class InterfaceManager: # 显示菜单 menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) + def deleteCesiumTileset(self, nodePath, item): + """删除 Cesium tileset""" + try: + # 从场景中移除 + 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] + + # 从树形控件中移除 + parentItem = item.parent() + if parentItem: + parentItem.removeChild(item) + + print(f"成功删除 Cesium tileset: {nodePath.getName()}") + + # 清空属性面板和选择框 + self.world.property_panel.clearPropertyPanel() + self.world.selection.updateSelection(None) + + # 更新场景树 + self.updateSceneTree() + + except Exception as e: + print(f"删除 Cesium tileset 失败: {str(e)}") + def isModelOrChild(self, item): """检查是否是模型节点或其子节点""" while item and item.parent(): @@ -161,14 +199,14 @@ 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,['灯光']) + # # 添加模型节点组 + # modelsItem = QTreeWidgetItem(sceneRoot, ['模型']) + # print(f"模型列表中的模型数量: {len(self.world.models)}") + # + # # 添加GUI元素节点组 + # guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素']) + # + # lightItem = QTreeWidgetItem(sceneRoot,['灯光']) BLACK_LIST = {'','**','temp','collision'} @@ -207,17 +245,26 @@ class InterfaceManager: # print(f"跳过节点: {child.getName()}") for model in self.world.models: - addNodeToTree(model, modelsItem,force=True) + 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(guiItem, [f"{gui_type}: {gui_text}"]) + item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"]) item.setData(0, Qt.UserRole, gui) - + #添加灯光节点 for light in self.world.Spotlight + self.world.Pointlight: - addNodeToTree(light, lightItem, force=True) + 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 hasattr(self.world, 'ground') and self.world.ground: diff --git a/ui/main_window.py b/ui/main_window.py index a853f494..cf4e5f4d 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -12,7 +12,7 @@ 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) + QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget,QDialog) from PyQt5.QtCore import Qt, QDir, QTimer from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget @@ -92,18 +92,45 @@ class MainWindow(QMainWindow): self.scaleAction = self.toolsMenu.addAction('缩放工具') self.sunsetAction = self.toolsMenu.addAction('光照编辑') self.pluginAction = self.toolsMenu.addAction('图形编辑') - + + # 创建菜单 + self.createMenu = menubar.addMenu('创建') + self.createEnptyaddAction = self.createMenu.addAction('空对象') + self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象') + + self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI') + self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本') + + self.createGUIaddMenu = self.createMenu.addMenu('GUI') + self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') + self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') + self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') + self.createGUIaddMenu.addSeparator() + self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') + self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图') + self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图') + self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图') + + self.createLightaddMenu = self.createMenu.addMenu('光源') + self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') + self.createPointLightAction = self.createLightaddMenu.addAction('点光源') + # GUI菜单 self.guiMenu = menubar.addMenu('GUI') self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式') self.guiMenu.addSeparator() - self.createButtonAction = self.guiMenu.addAction('创建按钮') - self.createLabelAction = self.guiMenu.addAction('创建标签') - self.createEntryAction = self.guiMenu.addAction('创建输入框') + # self.createButtonAction = self.guiMenu.addAction('创建按钮') + # self.createLabelAction = self.guiMenu.addAction('创建标签') + # self.createEntryAction = self.guiMenu.addAction('创建输入框') + self.guiMenu.addAction(self.createButtonAction) + self.guiMenu.addAction(self.createLabelAction) + self.guiMenu.addAction(self.createEntryAction) self.guiMenu.addSeparator() - self.create3DTextAction = self.guiMenu.addAction('创建3D文本') - self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕') - + # self.create3DTextAction = self.guiMenu.addAction('创建3D文本') + self.guiMenu.addAction(self.create3DTextAction) + # self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕') + self.guiMenu.addAction(self.createVirtualScreenAction) + # 脚本菜单 self.scriptMenu = menubar.addMenu('脚本') self.createScriptAction = self.scriptMenu.addAction('创建脚本...') @@ -115,6 +142,10 @@ class MainWindow(QMainWindow): self.toggleHotReloadAction.setChecked(True) # 默认启用 self.scriptMenu.addSeparator() self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器') + + self.cesiumMenu = menubar.addMenu('Cesium') + self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') + self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset) # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') @@ -131,6 +162,7 @@ class MainWindow(QMainWindow): # self.leftDock.setMinimumWidth(300) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock) + # 创建右侧停靠窗口(属性窗口) self.rightDock = QDockWidget("属性", self) self.rightDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) @@ -239,6 +271,10 @@ class MainWindow(QMainWindow): self.create3DTextTool.setText("3D文本") self.toolbar.addWidget(self.create3DTextTool) + self.create3DImageTool = QToolButton() + self.create3DImageTool.setText("3D图片") + self.toolbar.addWidget(self.create3DImageTool) + self.createSpotLight = QToolButton() self.createSpotLight.setText("聚光灯") self.toolbar.addWidget(self.createSpotLight) @@ -247,6 +283,22 @@ class MainWindow(QMainWindow): self.createPointLight.setText("点光灯") self.toolbar.addWidget(self.createPointLight) + # Cesium 工具按钮 + self.cesiumViewTool = QToolButton() + self.cesiumViewTool.setText("地图视图") + self.cesiumViewTool.clicked.connect(self.onCreateCesiumView) + self.toolbar.addWidget(self.cesiumViewTool) + + self.refreshCesiumTool = QToolButton() + self.refreshCesiumTool.setText("刷新地图") + self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView) + self.toolbar.addWidget(self.refreshCesiumTool) + + self.addModelTool = QToolButton() + self.addModelTool.setText("添加模型") + self.addModelTool.clicked.connect(self.onAddModelClicked) + self.toolbar.addWidget(self.addModelTool) + # 默认选择"选择"工具 self.selectTool.setChecked(True) self.world.setCurrentTool("选择") @@ -388,7 +440,11 @@ class MainWindow(QMainWindow): # 连接GUI编辑模式事件 self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode()) - + + # 连接创建事件 + # 连接光源创建按钮事件 + self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) + self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight()) # 连接GUI创建按钮事件 self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) @@ -396,11 +452,18 @@ class MainWindow(QMainWindow): self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) #self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) + self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) + self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) + self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) + self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) + self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) + self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) # 连接工具栏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.createSpotLight.clicked.connect(lambda :self.world.createSpotLight()) self.createPointLight.clicked.connect(lambda :self.world.createPointLight()) @@ -418,7 +481,225 @@ class MainWindow(QMainWindow): self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) - + + def onCreateCesiumView(self): + if hasattr(self.world,'gui_manager') and self.world.gui_manager: + self.world.gui_manager.createCesiumView() + else: + QMessageBox.warning(self,"错误","GUI管理其不可用") + + def onToggleCesiumView(self): + """切换 Cesium 视图显示状态""" + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.toggleCesiumView() + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + + def onRefreshCesiumView(self): + """刷新 Cesium 视图""" + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.refreshCesiumView() + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + + def onUpdateCesiumURL(self): + """更新 Cesium URL""" + url, ok = QInputDialog.getText(self, "更新 Cesium URL", "输入新的 URL:", + QLineEdit.Normal, "http://localhost:8080/Apps/HelloWorld.html") + if ok and url: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.updateCesiumURL(url) + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + + def onAddModelClicked(self): + """处理加入模型按钮点击事件""" + # 检查 Cesium 视图是否存在 + cesium_view_exists = False + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + for element in self.world.gui_manager.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + cesium_view_exists = True + break + + if not cesium_view_exists: + reply = QMessageBox.question( + self, + '提示', + 'Cesium 地图视图尚未打开,是否先打开地图视图?', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + self.onCreateCesiumView() + # 给一点时间让 Cesium 视图加载 + QTimer.singleShot(1000, self.showAddModelDialog) + return + else: + return + + self.showAddModelDialog() + + def showAddModelDialog(self): + """显示添加模型对话框""" + # 打开文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择 3D 模型文件", + "", + "3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)" + ) + + if file_path: + # 获取模型位置信息 + coords, ok = self.getModelCoordinates() + if ok: + longitude, latitude, height, scale = coords + + # 生成唯一的模型 ID + import uuid + model_id = f"model_{uuid.uuid4().hex[:8]}" + + try: + # 添加模型到 Cesium + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + success = self.world.gui_manager.addModelToCesium( + model_id, + file_path, + longitude, + latitude, + height, + scale + ) + + if success: + QMessageBox.information( + self, + "成功", + f"模型已成功添加到地图!\n模型ID: {model_id}" + ) + else: + QMessageBox.warning( + self, + "失败", + "添加模型失败,请检查控制台输出" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"添加模型时发生错误:\n{str(e)}" + ) + + def getModelCoordinates(self): + """获取模型坐标信息的对话框""" + # 创建对话框 + dialog = QDialog(self) + dialog.setWindowTitle("设置模型位置") + dialog.setModal(True) + dialog.resize(300, 200) + + layout = QVBoxLayout(dialog) + + # 经度 + lon_layout = QHBoxLayout() + lon_layout.addWidget(QLabel("经度:")) + lon_spin = QDoubleSpinBox() + lon_spin.setRange(-180, 180) + lon_spin.setValue(116.3975) # 默认北京位置 + lon_layout.addWidget(lon_spin) + layout.addLayout(lon_layout) + + # 纬度 + lat_layout = QHBoxLayout() + lat_layout.addWidget(QLabel("纬度:")) + lat_spin = QDoubleSpinBox() + lat_spin.setRange(-90, 90) + lat_spin.setValue(39.9085) # 默认北京位置 + lat_layout.addWidget(lat_spin) + layout.addLayout(lat_layout) + + # 高度 + height_layout = QHBoxLayout() + height_layout.addWidget(QLabel("高度(米):")) + height_spin = QDoubleSpinBox() + height_spin.setRange(-10000, 100000) + height_spin.setValue(0) + height_layout.addWidget(height_spin) + layout.addLayout(height_layout) + + # 缩放 + scale_layout = QHBoxLayout() + scale_layout.addWidget(QLabel("缩放:")) + scale_spin = QDoubleSpinBox() + scale_spin.setRange(0.001, 100000) + scale_spin.setValue(1.0) + scale_spin.setSingleStep(0.1) + scale_layout.addWidget(scale_spin) + layout.addLayout(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: + return ( + lon_spin.value(), + lat_spin.value(), + height_spin.value(), + scale_spin.value() + ), True + else: + return None, False + + def onLoadCesiumTileset(self): + url,ok = QInputDialog.getText( + self, + "加载 Cesium 3D Tiles", + "输入 tileset.json URL:", + QLineEdit.Normal, + "https://assets.ion.cesium.com/96128/tileset.json" + ) + + if ok and url: + try: + # 生成唯一的 tileset 名称 + import uuid + tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" + + # 加载 tileset + if hasattr(self.world, 'addCesiumTileset'): + success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0)) + if success: + QMessageBox.information( + self, + "成功", + f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}" + ) + else: + QMessageBox.warning( + self, + "失败", + "加载 Cesium 3D Tiles 失败" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}" + ) + + def onToolChanged(self, button): """工具切换事件处理""" if button.isChecked(): @@ -428,7 +709,7 @@ class MainWindow(QMainWindow): else: self.world.setCurrentTool(None) print("工具栏: 取消选择工具") - + # ==================== 脚本管理事件处理 ==================== def refreshScriptsList(self): diff --git a/ui/property_panel.py b/ui/property_panel.py index eb13630f..f195c5ae 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -22,6 +22,25 @@ class PropertyPanelManager: self._propertyLayout = None self._actor_cache={} + # 定义紧凑样式 + self.compact_style = """ + QDoubleSpinBox { + min-width: 45px; + } + QPushButton { + min-width: 10px; + } + QComboBox { + min-width: 60px; + } + QLineEdit { + min-width: 60px; + } + QCheckBox { + min-width: 20px; + } + """ + def setPropertyLayout(self, layout): """设置属性面板布局引用""" print("开始设置属性布局") @@ -59,6 +78,10 @@ class PropertyPanelManager: # 当节点被拖拽后,需要根据新父节点的状态来更新可见性 self._syncEffectiveVisibility(node) + self._syncSceneVisibility() + def _syncSceneVisibility(self): + scene_root = self.world.render + self._syncEffectiveVisibility(scene_root) def updatePropertyPanel(self, item): @@ -69,6 +92,10 @@ class PropertyPanelManager: self.clearPropertyPanel() + # 应用紧凑样式到属性面板容器 + if self._propertyLayout.parent(): + self._propertyLayout.parent().setStyleSheet(self.compact_style) + itemText = item.text(0) # 如果点击的是场景根节点,显示提示信息 @@ -114,10 +141,18 @@ class PropertyPanelManager: # 获取节点对象 model = item.data(0, Qt.UserRole) + if model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset": + self._showCesiumTilesetProperties(model,item) # 检查是否是GUI元素 - if model and hasattr(model, 'getTag') and model.getTag("gui_type"): - self.updateGUIPropertyPanel(model) - pass + 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 @@ -368,19 +403,27 @@ class PropertyPanelManager: self.scale_y = QDoubleSpinBox() self.scale_z = QDoubleSpinBox() - # 设置缩放控件属性 - for scale_widget in [self.scale_x, self.scale_y, self.scale_z]: - scale_widget.setRange(0.01, 100) - scale_widget.setSingleStep(0.1) + current_scale = model.getScale() - self.scale_x.setValue(model.getScale().getX()) - self.scale_y.setValue(model.getScale().getY()) - self.scale_z.setValue(model.getScale().getZ()) + # 设置缩放控件属性 + for i, (scale_widget, scale_value) in enumerate(zip([self.scale_x, self.scale_y, self.scale_z], + [current_scale.getX(), current_scale.getY(), + current_scale.getZ()])): + scale_widget.setRange(-1000, 1000) + scale_widget.setSingleStep(0.1) + # 如果缩放值为0,设置为一个很小的非零值 + if scale_value == 0: + scale_value = 0.01 if scale_value >= 0 else -0.01 + scale_widget.setValue(scale_value) + + self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value)) + self.scale_y.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_y, value)) + self.scale_z.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_z, value)) # 连接缩放变化事件 - self.scale_x.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ())) - self.scale_y.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ())) - self.scale_z.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v)) + self.scale_x.valueChanged.connect(lambda value: self._updateXScale(model, value)) + self.scale_y.valueChanged.connect(lambda value: self._updateYScale(model, value)) + self.scale_z.valueChanged.connect(lambda value: self._updateZScale(model, value)) # 创建并设置 X, Y, Z 标签居中 x_label3 = QLabel("X") @@ -407,6 +450,72 @@ class PropertyPanelManager: # 材质属性组 self._updateModelMaterialPanel(model) + def _onScaleValueChanged(self, scale_widget, value): + """确保缩放值不为0""" + if value == 0: + # 设置为一个很小的非零值,保持原有符号 + if hasattr(scale_widget, 'value') and scale_widget.value() > 0: + scale_widget.setValue(0.01) + else: + scale_widget.setValue(-0.01) + + def _updateXScale(self, model, value): + """更新X轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的X轴缩放 + current_scale = model.getScale() + model.setScale(value, current_scale.getY(), current_scale.getZ()) + self.refreshModelValues(model) + + def _updateYScale(self, model, value): + """更新Y轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的Y轴缩放 + current_scale = model.getScale() + model.setScale(current_scale.getX(), value, current_scale.getZ()) + self.refreshModelValues(model) + + def _updateZScale(self, model, value): + """更新Z轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的Z轴缩放 + current_scale = model.getScale() + 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 @@ -451,7 +560,6 @@ class PropertyPanelManager: spin.blockSignals(False) - def updateGUIPropertyPanel(self, gui_element): """更新GUI元素属性面板""" gui_type = gui_element.getTag("gui_type") @@ -467,6 +575,7 @@ class PropertyPanelManager: # typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") gui_info_layout.addWidget(typeValue, 0, 1) + # 修改 updateGUIPropertyPanel 中的文本属性部分 # 文本属性(如果适用) if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: gui_info_layout.addWidget(QLabel("文本:"), 1, 0) @@ -477,7 +586,8 @@ class PropertyPanelManager: success = self.world.gui_manager.editGUIElement(gui_element, "text", text) if success: # 更新场景树显示的名称 - self.world.scene_manager.updateSceneTree() + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + self.world.scene_manager.updateSceneTree() textEdit.textChanged.connect(updateText) gui_info_layout.addWidget(textEdit, 1, 1) @@ -575,7 +685,6 @@ class PropertyPanelManager: [pos.getX(), pos.getY(), v])) transform_layout.addWidget(zPos, 1, 3) - # 缩放属性 if hasattr(gui_element, 'getScale'): scale = gui_element.getScale() @@ -583,17 +692,51 @@ class PropertyPanelManager: transform_layout.addWidget(QLabel("缩放"), row_offset, 0) - 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) + # 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) + + 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) + + # 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) + # 外观属性组 - 添加字体颜色选择 + if gui_type in ["button", "label", "3d_text"]: + appearance_group = QGroupBox("外观属性") + appearance_layout = QGridLayout() + + # 字体颜色选择 + appearance_layout.addWidget(QLabel("字体颜色:"), 0, 0) + colorButton = QPushButton("选择颜色") + colorButton.clicked.connect(lambda checked, elem=gui_element: self._selectGUIColor(elem)) + appearance_layout.addWidget(colorButton, 0, 1) + + appearance_group.setLayout(appearance_layout) + self._propertyLayout.addWidget(appearance_group) + # 外观属性组 if gui_type in ["button", "label"]: appearance_group = QGroupBox("外观属性") @@ -608,6 +751,200 @@ class PropertyPanelManager: appearance_group.setLayout(appearance_layout) self._propertyLayout.addWidget(appearance_group) + if gui_type == "3d_image": + image_group = QGroupBox("图片设置") + image_layout = QGridLayout() + + # 当前图片路径标签 + current_image_label = QLabel("当前图片:") + image_layout.addWidget(current_image_label, 0, 0) + + # 显示当前贴图路径(简化显示) + current_texture_path = gui_element.getTag("texture_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 onSelectTexture(): + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择图片", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + if file_path: + # 应用新纹理到 3D Image + success = self.world.gui_manager.update3DImageTexture(gui_element, file_path) + if success: + # 保存路径到 Tag + gui_element.setTag("texture_path", file_path) + # 更新显示 + texture_label.setText(file_path) + # 可选:刷新场景树或其他 UI + self.world.scene_manager.updateSceneTree() + + select_texture_button.clicked.connect(onSelectTexture) + + image_group.setLayout(image_layout) + self._propertyLayout.addWidget(image_group) + + # 添加弹性空间 + self._propertyLayout.addStretch() + + # 强制更新布局 + if self._propertyLayout: + self._propertyLayout.update() + propertyWidget = self._propertyLayout.parentWidget() + if propertyWidget: + propertyWidget.update() + + def _selectGUIColor(self, gui_element): + """选择GUI元素的字体颜色""" + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + from panda3d.core import Vec4 + + # 获取当前颜色(如果已设置) + current_color = QColor(255, 255, 255) # 默认白色 + + # 尝试获取当前设置的颜色 + gui_type = gui_element.getTag("gui_type") + try: + if gui_type == "3d_text": + if gui_element.hasMaterial(): + material = gui_element.getMaterial() + base_color = material.getBaseColor() + current_color = QColor( + int(base_color.getX() * 255), + int(base_color.getY() * 255), + int(base_color.getZ() * 255), + int(base_color.getW() * 255) + ) + else: + # 从节点颜色获取 + node_color = gui_element.getColor() + current_color = QColor( + int(node_color.getX() * 255), + int(node_color.getY() * 255), + int(node_color.getZ() * 255), + int(node_color.getW() * 255) + ) + # current_color_obj = gui_element.getColor() + # current_color = QColor( + # int(current_color_obj[0] * 255), + # int(current_color_obj[1] * 255), + # int(current_color_obj[2] * 255) + # ) + # 对于其他类型的元素,可以添加类似的获取当前颜色的逻辑 + except: + pass # 使用默认颜色 + + color = QColorDialog.getColor(current_color, None, "选择字体颜色") + if color.isValid(): + r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 + self._updateGUITextColor(gui_element, (r, g, b, 1.0)) + + def _updateGUITextColor(self, gui_element, color): + """更新GUI元素的字体颜色""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry"]: + # 对于DirectGUI元素,使用text_fg属性 + gui_element['text_fg'] = color + print(f"✓ 更新DirectGUI元素字体颜色: {gui_type}") + + elif gui_type == "3d_text": + # # 对于3D文本元素,直接设置颜色 + # gui_element.setColor(*color) + # print(f"✓ 更新3D文本字体颜色: {gui_type}") + + from panda3d.core import Material,Vec4 + if not gui_element.hasMaterial(): + material = Material(f"text-material-{gui_element.getName()}") + material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) + material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) + material.setAmbient(Vec4(color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, color[3])) + material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + gui_element.setMaterial(material, 1) + else: + # 更新现有材质 + material = gui_element.getMaterial() + material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) + material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) + gui_element.setMaterial(material, 1) + print(f"✓ 更新3D文本材质颜色: {color}") + + gui_element.setColor(*color) + + elif gui_type == "3d_image": + # 对于3D图片,如果有文本标签的话 + # 这里可以根据需要添加特定处理 + pass + + print(f"✓ 更新GUI元素字体颜色: {gui_type}, 颜色: {color}") + except Exception as e: + print(f"✗ 更新GUI元素字体颜色失败: {e}") + 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): + """更新GUI元素Y轴缩放""" + 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.getZ(), scale_z) + gui_element.setScale(*new_scale) + else: + # 对于2D元素,保持原有的缩放方法 + gui_element.setScale(scale_z) + + print(f"✓ 更新GUI元素Y轴缩放: {scale_z}") + except Exception as e: + print(f"✗ 更新GUI元素Y轴缩放失败: {e}") + + def update3DImageTexture(self,nodepath,texture_path): + try: + tex = self.world.loader.loadTexture(texture_path) + if tex: + nodepath.setTexture(tex,1) + return True + else: + print(f"[警告] 无法加载贴图: {texture_path}") + return False + except Exception as e: + print(f"[错误] 更新 3D 图片纹理失败: {e}") + return False + def _updateScriptPropertyPanel(self, game_object): """更新脚本属性面板""" # 获取对象上的脚本 diff --git a/ui/widgets.py b/ui/widgets.py index 625c4b66..52f97d1d 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -15,7 +15,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout, QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, QFileDialog, QMessageBox, QAbstractItemView) from PyQt5.QtCore import Qt, QUrl -from PyQt5.QtGui import QDrag, QPainter, QPixmap +from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush from PyQt5.sip import wrapinstance from QPanda3D.QPanda3DWidget import QPanda3DWidget @@ -319,118 +319,268 @@ class CustomTreeWidget(QTreeWidget): self.setHeaderHidden(True) # 启用多选和拖拽 self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) + self.setDropIndicatorShown(True) # 启用拖放指示线 def setupDragDrop(self): """设置拖拽功能""" # 使用自定义拖拽模式 self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop self.setDefaultDropAction(Qt.DropAction.MoveAction) - + self.setDragEnabled(True) + self.setAcceptDrops(True) + def dropEvent(self, event): - """处理拖放事件""" - # 获取拖动的项和目标项 dragged_item = self.currentItem() target_item = self.itemAt(event.pos()) - + if not dragged_item or not target_item: event.ignore() return - - # 获取节点引用 + + if not self.isValidParentChild(dragged_item, target_item): + event.ignore() + return + dragged_node = dragged_item.data(0, Qt.UserRole) - amtarget_node = target_item.data(0, Qt.UserRole) - - # 如果目标是模型根节点,使用 render 作为新父节点 - if target_item.text(0) == "模型": - target_node = self.world.render - else: - target_node = target_item.data(0, Qt.UserRole) - + target_node = target_item.data(0, Qt.UserRole) + if not dragged_node or not target_node: event.ignore() return - # 检查是否是有效的父子关系 - if self.isValidParentChild(dragged_item, target_item): - # 保存当前的世界坐标 - world_pos = dragged_node.getPos(self.world.render) - - # 更新场景图中的父子关系 - dragged_node.wrtReparentTo(target_node) - - # 接受拖放事件,更新树形控件 - super().dropEvent(event) - - #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item) - # 更新属性面板 - self.world.updatePropertyPanel(dragged_item) - self.world.property_panel._syncEffectiveVisibility(dragged_node) + # # 检查是否是有效的父子关系 + # if self.isValidParentChild(dragged_item, target_item): + # # 保存当前的世界坐标 + # world_pos = dragged_node.getPos(self.world.render) + # + # # 更新场景图中的父子关系 + # dragged_node.wrtReparentTo(target_node) + # + # # 接受拖放事件,更新树形控件 + # super().dropEvent(event) + # + # #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item) + # # 更新属性面板 + # self.world.updatePropertyPanel(dragged_item) + # self.world.property_panel._syncEffectiveVisibility(dragged_node) - else: - event.ignore() + print(f"dragged_node: {dragged_node}, target_node: {target_node}") + + # 记录拖拽前的父节点 + old_parent_item = dragged_item.parent() + old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None + + # 执行Qt默认拖拽 + super().dropEvent(event) + + # 检查拖拽后的父节点 + new_parent_item = dragged_item.parent() + new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None + + # 同步Panda3D场景图的父子关系 + try: + # 检查是否是跨层级拖拽(父节点发生变化) + if old_parent_node != new_parent_node: + print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") + + # 保存世界坐标位置 + world_pos = dragged_node.getPos(self.world.render) + world_hpr = dragged_node.getHpr(self.world.render) + world_scale = dragged_node.getScale(self.world.render) + + # 重新父化到新的父节点 + if new_parent_node: + dragged_node.reparentTo(new_parent_node) + else: + # 如果新父节点为None,重新父化到render + dragged_node.reparentTo(self.world.render) + + # 恢复世界坐标位置 + dragged_node.setPos(self.world.render, world_pos) + dragged_node.setHpr(self.world.render, world_hpr) + dragged_node.setScale(self.world.render, world_scale) + + print(f"✅ Panda3D父子关系已更新") + else: + print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") + + except Exception as e: + print(f"⚠️ 同步Panda3D场景图失败: {e}") + # 不影响Qt树的更新,继续执行 + + # 事后验证:确保节点仍在"场景"根节点下 + self._ensureUnderSceneRoot(dragged_item) + self.world.property_panel._syncEffectiveVisibility(dragged_node) + event.accept() + + # try: + # world_pos = dragged_node.getPos(self.world.render) + # + # parent_of_dragged = dragged_node.getParent() + # target_node.wrtReparentTo(parent_of_dragged) + # + # # 拖动节点到目标节点下 + # dragged_node.wrtReparentTo(target_node) + # dragged_node.setPos(self.world.render, world_pos) + # + # # 更新 Qt 树控件 + # super().dropEvent(event) + # + # # 更新属性面板 + # self.world.updatePropertyPanel(dragged_item) + # + # event.accept() + # + # except Exception as e: + # print(f"重设父节点失败: {e}") + # event.ignore() + + def _ensureUnderSceneRoot(self, item): + """确保节点在场景根节点下,如果不是则自动修正""" + if not item: + return + + # 检查是否成为了顶级节点 + if not item.parent(): + # 如果节点名称不是"场景",说明意外成为了顶级节点 + if item.text(0) != "场景": + print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...") + + # 找到场景根节点 + scene_root = None + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.text(0) == "场景": + scene_root = top_item + break + + if scene_root: + # 将节点移回场景根节点下 + self.takeTopLevelItem(self.indexOfTopLevelItem(item)) + scene_root.addChild(item) + print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下") + def isValidParentChild(self, dragged_item, target_item): - """检查是否是有效的父子关系""" - # 不能拖放到自己上 + """检查是否是有效的父子关系(防止循环)""" + + # 1. 禁止拖放到自身 if dragged_item == target_item: return False - - # 不能拖放到自己的子节点上 - parent = target_item - while parent: - if parent == dragged_item: - return False - parent = parent.parent() - - # 检查目标项 - if target_item.text(0) == "场景": - return False # 不能拖放到场景根节点 - - # 允许拖放到模型根节点或其他模型节点 - if target_item.text(0) == "模型": - return True - - # 检查目标项的父节点 - target_parent = target_item.parent() - if not target_parent: + + # 2. 禁止拖到根节点之外(根节点本身除外) + target_root = self._getRootNode(target_item) + if target_root != "场景": + print(f"❌ 目标节点 {target_item.text(0)} 不在场景下") return False - - # 允许在模型节点下的任何位置调整父子关系 - while target_parent: - if target_parent.text(0) == "模型": - return True - target_parent = target_parent.parent() - - return False - + + # 3. 禁止拖拽"场景"根节点 + dragged_root = self._getRootNode(dragged_item) + if dragged_item.text(0) == "场景" or dragged_root != "场景": + print(f"❌ 禁止拖拽场景根节点或根节点外的节点") + return False + + # 4. Qt 树循环检查 + current = target_item + while current: + if current == dragged_item: + print(f"❌ Qt 树检测:{target_item.text(0)} 是 {dragged_item.text(0)} 的后代") + return False + current = current.parent() + + return True + + def _getRootNode(self, item): + """获取树中节点的根节点文本""" + current = item + while current.parent(): + current = current.parent() + return current.text(0) + def dragEnterEvent(self, event): """处理拖入事件""" if event.source() == self: event.accept() else: event.ignore() - + def dragMoveEvent(self, event): """处理拖动事件""" - if event.source() == self: - event.accept() - else: + if event.source() != self: event.ignore() - + return + + # 获取当前拖拽的项目和目标位置 + target_item = self.itemAt(event.pos()) + selected_items = self.selectedItems() + + # 检查是否拖拽到多选区域内的项目 + if target_item and target_item in selected_items: + event.ignore() + return + + # 检查其他禁止条件 + if target_item and selected_items: + for dragged_item in selected_items: + if not self.isValidParentChild(dragged_item, target_item): + event.ignore() + return + + super().dragMoveEvent(event) + event.accept() + def keyPressEvent(self, event): """处理键盘按键事件""" if event.key() == Qt.Key_Delete: - currentItem = self.currentItem() - if currentItem and currentItem.parent(): - # 检查是否是模型节点或其子节点 - if self.world.interface_manager.isModelOrChild(currentItem): - nodePath = currentItem.data(0, Qt.UserRole) - if nodePath: - print("正在删除节点...") - self.world.interface_manager.deleteNode(nodePath, currentItem) - print("删除完成") + # currentItem = self.currentItem() + # if currentItem and currentItem.parent(): + # # 检查是否是模型节点或其子节点 + # if self.world.interface_manager.isModelOrChild(currentItem): + # nodePath = currentItem.data(0, Qt.UserRole) + # if nodePath: + # print("正在删除节点...") + # self.world.interface_manager.deleteNode(nodePath, currentItem) + # print("删除完成") + selected_items = self.selectedItems() + if selected_items: + # 执行删除操作 + self.delete_items(selected_items) + else: + # 没有选中任何项目,执行默认操作 + super().keyPressEvent(event) else: - super().keyPressEvent(event) \ No newline at end of file + super().keyPressEvent(event) + + def delete_items(self, selected_items): + """删除选中的项目""" + if not selected_items: + return + + # 准备确认对话框的内容 + item_count = len(selected_items) + if item_count == 1: + item_names = f'"{selected_items[0].text(0)}"' + title = "确认删除" + message = f"确定要删除节点 {item_names} 吗?" + else: + item_names = "、".join([f'"{item.text(0)}"' for item in selected_items[:3]]) + if item_count > 3: + item_names += f" 等 {item_count} 个节点" + title = "确认批量删除" + message = f"确定要删除以下 {item_count} 个节点吗?\n\n{item_names}" + + # 创建确认对话框 + reply = QMessageBox.question( + self, + title, + message, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No # 默认选择"取消",防止误删 + ) + + # 只有用户确认后才执行删除 + if reply == QMessageBox.Yes: + pass + print(f"✅ 已删除 {item_count} 个节点") \ No newline at end of file