diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index abf9d58d..199088f8 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.4777777778]]] + sun_azimuth: [[[0.5000000000,0.0000000000]]] sun_altitude: [[[0.5000000000,1.0000000000]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: diff --git a/core/TranslateArrowHandle.fbx b/core/TranslateArrowHandle.fbx new file mode 100755 index 00000000..29725650 Binary files /dev/null and b/core/TranslateArrowHandle.fbx differ diff --git a/core/selection.py b/core/selection.py index 6fdc36fb..22ff8ef5 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) + TransparencyAttrib, Vec4) from direct.task.TaskManagerGlobal import taskMgr import math @@ -50,14 +50,14 @@ class SelectionSystem: # 高亮相关 self.gizmoHighlightAxis = None self.gizmo_colors = { - "x": (1*10, 0, 0, 1), # 红色 - "y": (0, 1*10, 0, 1), # 绿色 - "z": (0, 0, 1*10, 1) # 蓝色 + "x": (1, 0, 0, 0), # 红色 + "y": (0, 1, 0, 0), # 绿色 + "z": (0, 0, 1, 0) # 蓝色 } self.gizmo_highlight_colors = { - "x": (1.5*20, 1.5*20, 0, 1), # 黄色高亮 - "y": (1.5*20, 1.5*20, 0, 1), # 黄色高亮 - "z": (1.5*20, 1.5*20, 0, 1) # 黄色高亮 + "x": (1, 1, 0, 0), # 黄色高亮 + "y": (1, 1, 0, 0), # 黄色高亮 + "z": (1, 1, 0, 0) # 黄色高亮 } print("✓ 选择和变换系统初始化完成") @@ -217,6 +217,9 @@ class SelectionSystem: return task.cont self._last_selection_box_update = current_time + #检查目标节点是否已被删除 + self.checkAndClearIfTargetDeleted() + if not self.selectionBox or not self.selectionBoxTarget: return task.done # 结束任务 @@ -300,11 +303,18 @@ class SelectionSystem: # 只调用一次几何体创建 self.createGizmoGeometry() - # 只调用一次颜色设置 + #只调用一次颜色设置 self.setGizmoAxisColor("x", self.gizmo_colors["x"]) self.setGizmoAxisColor("y", self.gizmo_colors["y"]) self.setGizmoAxisColor("z", self.gizmo_colors["z"]) + self._updateGizmoScreenSize() + + self._setupGizmoRendering() + + # 现在才显示坐标轴 + self.gizmo.show() + # 只启动一次更新任务 taskMgr.add(self.updateGizmoTask, "updateGizmo") @@ -319,321 +329,100 @@ class SelectionSystem: if not self.gizmo: return - # 创建X轴(红色) - x_lines = LineSegs("x_axis") - x_lines.setThickness(6.0) - x_lines.moveTo(0, 0, 0) - x_lines.drawTo(self.axis_length, 0, 0) - # 创建X轴箭头 - x_lines.moveTo(self.axis_length - 0.5, -0.2, 0) - x_lines.drawTo(self.axis_length, 0, 0) - x_lines.drawTo(self.axis_length - 0.5, 0.2, 0) - x_geom = x_lines.create() - self.gizmoXAxis = self.gizmo.attachNewNode(x_geom) - self.gizmoXAxis.setName("gizmo_x_axis") - #self.gizmoXAxis.setLightOff() - # 创建Y轴(绿色) - y_lines = LineSegs("y_axis") - y_lines.setThickness(6.0) - y_lines.moveTo(0, 0, 0) - y_lines.drawTo(0, self.axis_length, 0) - # 创建Y轴箭头 - y_lines.moveTo(-0.2, self.axis_length - 0.5, 0) - y_lines.drawTo(0, self.axis_length, 0) - y_lines.drawTo(0.2, self.axis_length - 0.5, 0) - y_geom = y_lines.create() - self.gizmoYAxis = self.gizmo.attachNewNode(y_geom) - self.gizmoYAxis.setName("gizmo_y_axis") - #self.gizmoYAxis.setLightOff() + model_paths = [ + "core/TranslateArrowHandle.fbx", + "EG/core/TranslateArrowHandle.fbx", + ] + arrow_model = None + for path in model_paths: + try: + arrow_model = self.world.loader.loadModel(path) + if arrow_model: + print(f"成功加载模型: {path}") + break + except: + continue + 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) + + 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) # 创建Z轴(蓝色) - z_lines = LineSegs("z_axis") - z_lines.setThickness(6.0) - z_lines.moveTo(0, 0, 0) - z_lines.drawTo(0, 0, self.axis_length) - # 创建Z轴箭头 - z_lines.moveTo(-0.2, 0, self.axis_length - 0.5) - z_lines.drawTo(0, 0, self.axis_length) - z_lines.drawTo(0.2, 0, self.axis_length - 0.5) - z_geom = z_lines.create() - self.gizmoZAxis = self.gizmo.attachNewNode(z_geom) - self.gizmoZAxis.setName("gizmo_z_axis") - #self.gizmoZAxis.setLightOff() + 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) - # 确保坐标轴不被光照影响 - #self.gizmo.setLightOff() - - # 使用最强的渲染设置,确保坐标轴绝对不会被遮挡 - self.gizmo.setBin("gui-popup", 0) # 使用最高的GUI渲染层 - self.gizmo.setDepthTest(False) # 完全禁用深度测试 - self.gizmo.setDepthWrite(False) # 禁用深度写入 - self.gizmo.setTwoSided(True) # 双面渲染 - - # 创建强制前景渲染状态 - from panda3d.core import RenderModeAttrib, TransparencyAttrib - foreground_state = RenderState.make( - DepthTestAttrib.make(DepthTestAttrib.MNone), # 完全不进行深度测试 - TransparencyAttrib.make(TransparencyAttrib.MAlpha) # 启用透明度混合 - ) - #self.gizmo.setState(foreground_state) - - # 对每个坐标轴设置独立的最高渲染优先级 - self.gizmoXAxis.setBin("gui-popup", 10) - self.gizmoXAxis.setDepthTest(False) - self.gizmoXAxis.setDepthWrite(False) - self.gizmoXAxis.setLightOff() - self.gizmoXAxis.setState(foreground_state) - - self.gizmoYAxis.setBin("gui-popup", 20) - self.gizmoYAxis.setDepthTest(False) - self.gizmoYAxis.setDepthWrite(False) - self.gizmoYAxis.setLightOff() - self.gizmoYAxis.setState(foreground_state) - - self.gizmoZAxis.setBin("gui-popup", 30) - self.gizmoZAxis.setDepthTest(False) - self.gizmoZAxis.setDepthWrite(False) - self.gizmoZAxis.setLightOff() - self.gizmoZAxis.setState(foreground_state) - - - - # 初始化高亮状态 - self.gizmoHighlightAxis = None - - # 立即设置初始颜色,确保创建时就有正确的颜色 - self.setGizmoAxisColor("z", self.gizmo_colors["z"]) + # 设置初始颜色 self.setGizmoAxisColor("x", self.gizmo_colors["x"]) self.setGizmoAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoAxisColor("z", self.gizmo_colors["z"]) - - print(f"✓ 坐标轴几何体创建完成,长度={self.axis_length}") - - # 为 RenderPipeline 环境设置正确的渲染状态 - self._setupRenderPipelineCompatibleGizmo() - - self._setupEmissiveMaterials() + #设置渲染属性,解决模型遮挡和阴影问题 self._setupGizmoRendering() except Exception as e: print(f"创建坐标轴几何体失败: {str(e)}") - def _setupEmissiveMaterials(self): - try: - from panda3d.core import Material,Vec4 - materials ={ - "x":(Vec4(1,0,0,1),Vec4(2.0,0,0,1)), - "y":(Vec4(0,1,0,1),Vec4(0,2.0,0,1)), - "z":(Vec4(0,0,1,1),Vec4(0,0,2.0,1)) - } - axis_nodes ={ - "x":self.gizmoXAxis, - "y":self.gizmoYAxis, - "z":self.gizmoZAxis - } - for axis,(base_color,emission_color) in materials.items(): - if axis_nodes[axis]: - material=Material(f"gizmo_{axis}_material") - material.setBaseColor(base_color) - material.setEmission(emission_color) - material.setRoughness(1.0) - material.setMetallic(0.0) - axis_nodes[axis].setMaterial(material) - except Exception as e: - print(f"自发光材质设置失败: {str(e)}") - def _setupGizmoRendering(self): - """设置坐标轴渲染属性""" try: - # 设置渲染优先级,确保在最前面显示 - self.gizmo.setBin("gui-popup", 1000) - self.gizmo.setDepthTest(False) - self.gizmo.setDepthWrite(False) - self.gizmo.setLightOff() + axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] - # 为每个轴设置独立的渲染属性 - for i, axis_node in enumerate([self.gizmoXAxis, self.gizmoYAxis, self.gizmoZAxis]): + for axis_node in axis_nodes: if axis_node: - axis_node.setBin("gui-popup", 1001 + i) - axis_node.setDepthTest(False) - axis_node.setDepthWrite(False) + #禁用光照和阴影 axis_node.setLightOff() - - except Exception as e: - print(f"设置坐标轴渲染失败!!!!!!: {str(e)}") - - def _setupRenderPipelineCompatibleGizmo(self): - """为 RenderPipeline 环境设置兼容的坐标轴渲染 - 激进修复版本""" - try: - from panda3d.core import (ShaderAttrib, MaterialAttrib, RenderModeAttrib, - AntialiasAttrib, TransparencyAttrib, CullFaceAttrib, - RescaleNormalAttrib, TextureAttrib) - - # 第一步:完全隔离坐标轴,避免被任何系统处理 - self._isolateGizmoFromRenderPipeline() - - # 第二步:使用最简单的渲染方式 - self._setupMinimalGizmoRendering() - - # 第三步:强制设置每个轴的独立渲染 - self._forceAxisIndependentRendering() - - - except Exception as e: - # 使用最后的备用方案 - self._setupUltimateGizmoFallback() - - def _isolateGizmoFromRenderPipeline(self): - """完全隔离坐标轴,避免被 RenderPipeline 处理""" - try: - # 设置所有可能的隔离标签 - isolation_tags = [ - ("no_shadow", "1"), - ("no_lighting", "1"), - ("no_material", "1"), - ("no_shader", "1"), - ("no_fog", "1"), - ("no_texture", "1"), - ("gui_element", "1"), - ("bypass_rp", "1"), - ("fixed_pipeline", "1"), - ("ignore_all", "1") - ] - - for tag, value in isolation_tags: - self.gizmo.setTag(tag, value) - - # 完全禁用所有高级功能,使用最高优先级 - self.gizmo.setShaderOff(10000) - self.gizmo.setLightOff(10000) - self.gizmo.setFogOff(10000) - self.gizmo.setMaterialOff(10000) - self.gizmo.setTextureOff(10000) - - # 禁用所有可能的渲染特性 - from panda3d.core import RenderModeAttrib, CullFaceAttrib - # self.gizmo.setRenderModeWireframe() - # self.gizmo.setTwoSided(True) - self.gizmo.setRenderMode(RenderModeAttrib.MFilled) - self.gizmo.setTwoSided(True) - - self.gizmo.setColorScale(2.0,2.0,2.0,1.0) - for axis_node in [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis]: - if axis_node: - axis_node.setTag("emissive","1") - axis_node.setTag("unlit","1") - axis_node.setColorScale(2.0,2.0,2.0,1.0) - - except Exception as e: - print(f" ❌ 隔离失败: {e}") - - def _setupMinimalGizmoRendering(self): - """设置最简单的渲染方式""" - try: - from panda3d.core import (ShaderAttrib, MaterialAttrib, TextureAttrib, - CullFaceAttrib, RescaleNormalAttrib) - - # 使用最高优先级的 GUI 渲染 bin - self.gizmo.setBin("gui-popup", 10000) - self.gizmo.setDepthTest(False) - self.gizmo.setDepthWrite(False) - - # 创建最简单的渲染状态 - minimal_state = RenderState.make( - ShaderAttrib.makeOff(10000), # 完全禁用着色器 - MaterialAttrib.makeOff(), # 禁用材质 - TextureAttrib.makeOff(), # 禁用纹理 - CullFaceAttrib.make(CullFaceAttrib.MCullNone), # 禁用面剔除 - RescaleNormalAttrib.makeOff() # 禁用法线重缩放 - ) - self.gizmo.setState(minimal_state, 10000) - - except Exception as e: - print(f" ❌ 最简渲染设置失败: {e}") - - def _forceAxisIndependentRendering(self): - """强制设置每个轴的独立渲染""" - try: - # 轴配置 - axis_configs = [ - (self.gizmoXAxis, "X轴", (1.0, 0.0, 0.0, 1.0), 0), - (self.gizmoYAxis, "Y轴", (0.0, 1.0, 0.0, 1.0), 0), - (self.gizmoZAxis, "Z轴", (0.0, 0.0, 1.0, 1.0), 0) - ] - - for axis_node, name, color, priority in axis_configs: - if axis_node: - # 每个轴都完全独立设置 - self._setupSingleAxisRendering(axis_node, name, color, 0) - - - except Exception as e: - print(f" ❌ 独立轴渲染设置失败: {e}") - - def _setupSingleAxisRendering(self, axis_node, name, color, priority): - """为单个轴设置完全独立的渲染""" - try: - from panda3d.core import (LVecBase4f, RenderState, ColorAttrib, - TransparencyAttrib, LColor, AntialiasAttrib, - RenderModeAttrib, CullFaceAttrib, AuxBitplaneAttrib, - LightRampAttrib) - - # 转换颜色为LColor并增加亮度 - base_color = LColor(*color) - emissive_color = LColor(base_color[0], base_color[1], base_color[2], 1.0) - - # 完全禁用所有高级渲染功能 - axis_node.clearShader() - axis_node.clearTexture() - axis_node.clearMaterial() - axis_node.setLightOff() - axis_node.setFogOff() - axis_node.setAttrib(RenderModeAttrib.make(RenderModeAttrib.MWireframe, 6.0)) - axis_node.setAttrib(AntialiasAttrib.make(AntialiasAttrib.MLine)) - axis_node.setBin("gui-popup", 0) - axis_node.setDepthTest(False) - axis_node.setDepthWrite(False) - axis_node.setTwoSided(True) - - # 强制设置自发光颜色 - axis_node.setColor(*color) - axis_node.setColorScale(1.0, 1.0, 1.0, 1.0) # 增加整体亮度 - - except Exception as e: - print(" ❌ {} 轴渲染设置失败: {}".format(name, str(e))) - raise e - - def _setupUltimateGizmoFallback(self): - """最后的备用方案 - 最简单的渲染""" - try: - # 最简单的设置 - self.gizmo.setLightOff() - self.gizmo.setFogOff() - self.gizmo.setBin("gui-popup", 20000) - self.gizmo.setDepthTest(False) - self.gizmo.setDepthWrite(False) - - # 直接设置颜色,不使用复杂的渲染状态 + axis_node.setShaderOff() + axis_node.setFogOff() + #设置渲染层级,确保大多数对象之前渲染 + axis_node.setBin("fixed",30) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(False) + arrow_nodes = [] if self.gizmoXAxis: - self.gizmoXAxis.setColor(1, 0, 0, 1) - self.gizmoXAxis.setLightOff() - self.gizmoXAxis.setBin("gui-popup", 20001) - self.gizmoXAxis.setDepthTest(False) - + x_arrow = self.gizmoXAxis.find("x_arrow") + if x_arrow: + arrow_nodes.append(x_arrow) if self.gizmoYAxis: - self.gizmoYAxis.setColor(0, 1, 0, 1) - self.gizmoYAxis.setLightOff() - self.gizmoYAxis.setBin("gui-popup", 20002) - self.gizmoYAxis.setDepthTest(False) - + y_arrow = self.gizmoYAxis.find("y_arrow") + if y_arrow: + arrow_nodes.append(y_arrow) if self.gizmoZAxis: - self.gizmoZAxis.setColor(0, 0, 1, 1) - self.gizmoZAxis.setLightOff() - self.gizmoZAxis.setBin("gui-popup", 20003) - self.gizmoZAxis.setDepthTest(False) + z_arrow = self.gizmoZAxis.find("z_arrow") + if z_arrow: + arrow_nodes.append(z_arrow) + for arrow_node in arrow_nodes: + if arrow_node: + arrow_node.setLightOff() + arrow_node.setShaderOff() + arrow_node.setFogOff() + arrow_node.setBin("fixed",31) + arrow_node.setDepthWrite(False) + arrow_node.setDepthTest(False) + #启用透明度S + arrow_node.setTransparency(TransparencyAttrib.MAlpha) + if self.gizmo: + self.gizmo.setLightOff() + self.gizmo.setShaderOff() + self.gizmo.setFogOff() + self.gizmo.setBin("fixed",29) + self.gizmo.setDepthWrite(False) + self.gizmo.setDepthTest(False) except Exception as e: - print(f"❌ 最后备用方案也失败: {e}") + print(f"设置坐标轴渲染属性失败: {str(e)}") def updateGizmoTask(self, task): """坐标轴更新任务 - 包含固定大小功能""" @@ -648,6 +437,9 @@ class SelectionSystem: return task.cont self._last_gizmo_update = current_time + #检查目标节点是否已被删除 + self.checkAndClearIfTargetDeleted() + if not self.gizmo or not self.gizmoTarget: return task.done @@ -665,25 +457,6 @@ class SelectionSystem: else: # 只在必要时更新位置和朝向 self._updateGizmoPositionAndOrientation() - # # 更新坐标轴位置,始终在目标节点中心 - # minPoint = Point3() - # maxPoint = Point3() - # if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - # # 计算中心点 - # center = Point3((minPoint.x + maxPoint.x) * 0.5, - # (minPoint.y + maxPoint.y) * 0.5, - # (minPoint.z + maxPoint.z) * 0.5) - # self.gizmo.setPos(center) - # - # # 【关键修复】:更新坐标轴朝向以跟踪父节点的变化 - # 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) # 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小 self._updateGizmoScreenSize() @@ -753,8 +526,8 @@ class SelectionSystem: self.gizmo.setScale(scale_factor) # 限制缩放范围,避免过大或过小 - min_scale = 0.1 - max_scale = 10.0 + min_scale = 0.08 + max_scale = 100.0 final_scale = max(min_scale, min(max_scale, scale_factor)) if final_scale != scale_factor: @@ -785,34 +558,122 @@ class SelectionSystem: self.gizmoStartPos = None - def setGizmoAxisColor(self, axis, color): - """设置坐标轴颜色 - RenderPipeline 兼容版本""" - try: - from panda3d.core import AntialiasAttrib,TransparencyAttrib + # def setGizmoAxisColor(self, axis, color): + # """设置坐标轴颜色 - RenderPipeline 兼容版本""" + # try: + # from panda3d.core import AntialiasAttrib,TransparencyAttrib + # + # axis_nodes = { + # "x": self.gizmoXAxis, + # "y": self.gizmoYAxis, + # "z": self.gizmoZAxis + # } + # + # if axis in axis_nodes and axis_nodes[axis]: + # axis_node = axis_nodes[axis] + # + # axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) + # axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3]) + # axis_node.setShaderOff(10000) + # axis_node.setLightOff() + # axis_node.setMaterialOff() + # axis_node.setTextureOff() + # axis_node.setFogOff() + # + # except Exception as e: + # print(f"设置坐标轴颜色失败: {str(e)}") + # # 回退到简单的颜色设置 + # try: + # if axis in axis_nodes and axis_nodes[axis]: + # axis_nodes[axis].setColor(*color) + # except: + # pass + def setGizmoAxisColor(self, axis, color): + """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" + try: + from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib + + # 获取对应的轴节点 axis_nodes = { "x": self.gizmoXAxis, "y": self.gizmoYAxis, "z": self.gizmoZAxis } - if axis in axis_nodes and axis_nodes[axis]: - axis_node = axis_nodes[axis] + if axis not in axis_nodes or not axis_nodes[axis]: + return - axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) - axis_node.setColorScale(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) - axis_node.setShaderOff(10000) - axis_node.setLightOff(10000) - axis_node.setMaterialOff(10000) - axis_node.setTextureOff(1000) - axis_node.setFogOff(10000) + 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") + + if not arrow_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) + + # 应用材质 + arrow_node.setMaterial(mat, 1) + + + # 设置透明度 + if color[3] < 1.0: + arrow_node.setTransparency(TransparencyAttrib.MAlpha) + else: + arrow_node.setTransparency(TransparencyAttrib.MNone) + + arrow_node.setLightOff() # 禁用光照影响 + arrow_node.setShaderOff() # 禁用着色器 + arrow_node.setFogOff() # 禁用雾效果 + + arrow_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) + axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) except: pass @@ -1050,38 +911,10 @@ class SelectionSystem: # ==================== 高亮和交互 ==================== - def updateGizmoHighlight(self, mouseX, mouseY): - """更新坐标轴高亮状态""" - if not self.gizmo or self.isDraggingGizmo: - return - - import time - current_time = time.time() - if not hasattr(self,'_last_highlight_time'): - self._last_highlight_time = 0 - - if current_time - self._last_highlight_time<0.05: - return - self._last_highlight_time = current_time - - hoveredAxis = self._detectHoveredAxis(mouseX, mouseY) - - if not hasattr(self,'_hover_stability_counter'): - self._hover_stability_counter = {} - self._last_detected_axis = None - - if hoveredAxis !=self._last_detected_axis: - self._hover_stability_counter[hoveredAxis]=1 - self._last_detected_axis = hoveredAxis - else: - self._hover_stability_counter[hoveredAxis] = self._hover_stability_counter.get(hoveredAxis,0)+1 - - if self._hover_stability_counter.get(hoveredAxis,0)>=2: - if hoveredAxis != self.gizmoHighlightAxis: - self._updateAxisHighlight(hoveredAxis) - - # 检测鼠标悬停的轴(使用相同的检测逻辑但不输出调试信息) - hoveredAxis = None + def detectGizmoAxisAtMouse(self, mouseX, mouseY): + """统一的坐标轴检测方法 - 同时用于高亮和点击检测""" + if not self.gizmo or not self.gizmoTarget: + return None try: # 获取坐标轴中心的世界坐标 @@ -1094,29 +927,29 @@ class SelectionSystem: # 将3D坐标投影到屏幕坐标 def worldToScreen(worldPos): - try: - # 转换为相机坐标系 - camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) + try: + # 转换为相机坐标系 + camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) - # 检查点是否在相机前方 - if camPos.getY() <= 0: - return None + # 检查点是否在相机前方 + if camPos.getY() <= 0: + return None - # 使用相机lens进行投影 - screenPos = Point2() - lens = self.world.cam.node().getLens() + # 使用相机lens进行投影 + screenPos = Point2() + lens = self.world.cam.node().getLens() - if lens.project(camPos, screenPos): - # 获取准确的窗口尺寸 - winWidth, winHeight = self.world.getWindowSize() + if lens.project(camPos, screenPos): + # 获取准确的窗口尺寸 + winWidth, winHeight = self.world.getWindowSize() - # 转换为像素坐标 - winX = (screenPos.x + 1) * 0.5 * winWidth - winY = (1 - screenPos.y) * 0.5 * winHeight - return (winX, winY) - return None - except: - return None + # 转换为像素坐标 + winX = (screenPos.x + 1) * 0.5 * winWidth + winY = (1 - screenPos.y) * 0.5 * winHeight + return (winX, winY) + return None + except: + return None # 获取各坐标轴的屏幕投影 gizmo_screen = worldToScreen(gizmo_world_pos) @@ -1124,70 +957,90 @@ class SelectionSystem: y_screen = worldToScreen(y_end) z_screen = worldToScreen(z_end) - # 只要坐标轴中心在屏幕内,就进行检测 - if gizmo_screen: - click_threshold = 25 + # 如果坐标轴中心不在屏幕内,返回None + if not gizmo_screen: + return None - def isNearLine(mousePos, start, end, threshold): - import math - A = mousePos[1] - start[1] - B = start[0] - mousePos[0] - C = (end[1] - start[1]) * mousePos[0] + (start[0] - end[0]) * mousePos[1] + end[0] * start[1] - start[0] * end[1] + # 设置检测阈值 + click_threshold = 35 # 统一使用25像素的检测阈值 - length = math.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2) - if length == 0: - return False + # 更准确的点到线段距离计算方法 + def distanceToLineSegment(mousePos, start, end): + import math + mx, my = mousePos + x1, y1 = start + x2, y2 = end - distance = abs(C) / length - t = ((mousePos[0] - start[0]) * (end[0] - start[0]) + - (mousePos[1] - start[1]) * (end[1] - start[1])) / (length * length) + # 线段向量 + dx = x2 - x1 + dy = y2 - y1 - return distance < threshold and 0 <= t <= 1 + # 线段长度平方 + length_sq = dx * dx + dy * dy - mouse_pos = (mouseX, mouseY) + if length_sq == 0: + # 线段退化为点 + return math.sqrt((mx - x1) ** 2 + (my - y1) ** 2) - # 分别检测每个轴,为在屏幕外的轴端点提供替代方案 - # 按优先级检测轴(Z > X > Y) + # 投影参数 + t = max(0, min(1, ((mx - x1) * dx + (my - y1) * dy) / length_sq)) - # 对于轴端点在屏幕外的情况,使用较短的轴段进行检测 - def getAxisScreenPoint(axis_name, axis_screen_end): - if axis_screen_end: - return axis_screen_end - # 如果端点在屏幕外,使用轴长度的一半作为检测点 - if axis_name == "x": - half_end = gizmo_world_pos + Vec3(self.axis_length * 0.5, 0, 0) - elif axis_name == "y": - half_end = gizmo_world_pos + Vec3(0, self.axis_length * 0.5, 0) - elif axis_name == "z": - half_end = gizmo_world_pos + Vec3(0, 0, self.axis_length * 0.5) - return worldToScreen(half_end) + # 投影点坐标 + proj_x = x1 + t * dx + proj_y = y1 + t * dy - # 获取有效的检测点(优先使用完整轴,备用使用半轴) - z_detect_point = getAxisScreenPoint("z", z_screen) - x_detect_point = getAxisScreenPoint("x", x_screen) - y_detect_point = getAxisScreenPoint("y", y_screen) + # 返回点到投影点的距离 + return math.sqrt((mx - proj_x) ** 2 + (my - proj_y) ** 2) - if z_detect_point and isNearLine(mouse_pos, gizmo_screen, z_detect_point, click_threshold): - hoveredAxis = "z" - elif x_detect_point and isNearLine(mouse_pos, gizmo_screen, x_detect_point, click_threshold): - hoveredAxis = "x" - elif y_detect_point and isNearLine(mouse_pos, gizmo_screen, y_detect_point, click_threshold): - hoveredAxis = "y" + mouse_pos = (mouseX, mouseY) + + # 检测各个轴 - 按优先级检测(Z > X > Y) + axes_to_check = [ + ("z", z_screen), + ("x", x_screen), + ("y", y_screen) + ] + + for axis_name, axis_end in axes_to_check: + if axis_end: + distance = distanceToLineSegment(mouse_pos, gizmo_screen, axis_end) + if distance < click_threshold: + return axis_name + + # 如果没有检测到,返回None + return None except Exception as e: - pass # 静默处理错误,避免频繁输出 + # 静默处理错误,避免频繁输出 + return None - # 如果高亮状态发生变化 - if hoveredAxis != self.gizmoHighlightAxis: - # 恢复之前高亮的轴 - if self.gizmoHighlightAxis: - self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) + def updateGizmoHighlight(self, mouseX, mouseY): + """更新坐标轴高亮状态""" + if not self.gizmo or self.isDraggingGizmo: + return - # 高亮新的轴 - if hoveredAxis: - self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + # 使用统一的检测方法 + hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) - self.gizmoHighlightAxis = hoveredAxis + # 简化稳定性检测逻辑 + if not hasattr(self, '_last_detected_axis'): + self._last_detected_axis = None + + # 如果检测结果发生变化,立即更新高亮 + if hoveredAxis != self._last_detected_axis: + # 更新轴的高亮状态 + if hoveredAxis != self.gizmoHighlightAxis: + # 恢复之前高亮的轴 + if self.gizmoHighlightAxis: + self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) + + # 高亮新的轴 + if hoveredAxis: + self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + + self.gizmoHighlightAxis = hoveredAxis + + self._last_detected_axis = hoveredAxis def _detectHoveredAxis(self, mouseX, mouseY): """检测鼠标悬停的轴 - 提取为独立方法""" @@ -1221,14 +1074,36 @@ class SelectionSystem: return self.isDraggingGizmo = True - self.dragGizmoAxis = axis + # 使用当前高亮的轴,如果有的话 + if self.gizmoHighlightAxis: + self.dragGizmoAxis = self.gizmoHighlightAxis + else: + self.dragGizmoAxis = axis self.dragStartMousePos = (mouseX, mouseY) # 保存开始拖拽时目标节点的位置和坐标轴的位置 self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 - print(f"开始拖拽 {axis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") + # 确保正在拖动的轴保持高亮状态 + if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: + # 先将所有轴恢复为正常颜色 + for axis_name in self.gizmo_colors.keys(): + if axis_name != self.dragGizmoAxis: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + + # 然后将当前拖动的轴设置为高亮颜色 + 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 + + print( + f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") except Exception as e: print(f"开始坐标轴拖拽失败: {str(e)}") @@ -1282,33 +1157,23 @@ class SelectionSystem: # 确定轴向量的变换上下文 if parent_node and parent_node != self.world.render: - # 子节点:使用父节点的局部坐标系 - # print(f"子节点拖拽 - 父节点: {parent_node.getName()}, 父节点旋转: {parent_node.getHpr()}") - # transform_context = parent_node transform_mat = parent_node.getMat(self.world.render) world_axis_vector = transform_mat.xformVec(local_axis_vector) else: world_axis_vector = local_axis_vector - # 顶级模型:使用世界坐标系 - # print(f"顶级模型拖拽 - 使用世界坐标系") - # transform_context = self.world.render - axis_end = gizmo_world_pos + world_axis_vector + + #axis_end = gizmo_world_pos + world_axis_vector # 投影到屏幕空间 def worldToScreen(worldPos): try: - # 先转换为相机坐标系 camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) - - # 检查是否在相机前方 if camPos.getY() <= 0: return None screenPos = Point2() if self.world.cam.node().getLens().project(camPos, screenPos): - # 获取准确的窗口尺寸 winWidth, winHeight = self.world.getWindowSize() - winX = (screenPos.x + 1) * 0.5 * winWidth winY = (1 - screenPos.y) * 0.5 * winHeight return (winX, winY) @@ -1316,28 +1181,45 @@ class SelectionSystem: except Exception as e: print(f"世界坐标转屏幕坐标失败: {e}") return None + 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) - 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 gizmo_screen: - print("拖拽更新失败: 坐标轴中心不在屏幕内") - return - if not axis_screen: - print("拖拽更新失败: 坐标轴端点不在屏幕内") + if not axis_start_screen or not axis_end_screen: + print("拖拽更新失败: 无法获取轴线屏幕坐标") return - # 计算轴在屏幕空间的方向向量 screen_axis_dir = ( - axis_screen[0] - gizmo_screen[0], - axis_screen[1] - gizmo_screen[1] + axis_end_screen[0] - axis_start_screen[0], + axis_end_screen[1] - axis_start_screen[1] ) + # 归一化屏幕轴方向 import math length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) if length > 0: - screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) + #screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) + screen_axis_dir = ( + screen_axis_dir[0] / length, + screen_axis_dir[1] / length + ) + else: print("拖拽更新失败: 屏幕轴方向长度为0") return @@ -1346,29 +1228,54 @@ class SelectionSystem: projected_distance = (mouseDeltaX * screen_axis_dir[0] + mouseDeltaY * screen_axis_dir[1]) - 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 + cam_pos = self.world.cam.getPos(self.world.render) + distance_to_object = (cam_pos - gizmo_world_pos).length() + lens = self.world.cam.node().getLens() + fov = lens.getFov()[0] + winWidth,winHeight = self.world.getWindowSize() - fixed_pixel_to_world_ratio = 0.01 # 1像素 = 0.01世界单位 - scale_factor = fixed_pixel_to_world_ratio * scale_adjustment + pixels_to_world_units = (2*distance_to_object*math.tan(math.radians(fov/2)))/winWidth + + movement_distance = projected_distance * pixels_to_world_units + + total_scale_factor = 1.0 + 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() + + if total_scale_factor > 0: + movement_distance = movement_distance / total_scale_factor - movement_distance = projected_distance * 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": @@ -1411,7 +1318,7 @@ class SelectionSystem: import time current_time = time.time() if current_time - self._last_drag_debug_time > 0.1: # 每0.1秒最多输出一次 - print(f"拖拽更新成功 - 轴:{self.dragGizmoAxis}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}") + #print(f"拖拽更新成功 - 轴:{self.dragGizmoAxis}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}") self._last_drag_debug_time = current_time except Exception as e: @@ -1422,6 +1329,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 self.isDraggingGizmo = False self.dragGizmoAxis = None @@ -1475,3 +1386,10 @@ class SelectionSystem: def hasSelection(self): """检查是否有选中的节点""" return self.selectedNode is not None + + def checkAndClearIfTargetDeleted(self): + if self.gizmoTarget and self.gizmoTarget.isEmpty(): + self.clearGizmo() + + if self.selectionBoxTarget and self.selectionBoxTarget.isEmpty(): + self.clearSelectionBox() diff --git a/main.py b/main.py index 3329c223..065ef800 100644 --- a/main.py +++ b/main.py @@ -369,9 +369,6 @@ class MyWorld(CoreWorld): def updatePropertyPanel(self, item): """更新属性面板显示 - 代理到property_panel""" return self.property_panel.updatePropertyPanel(item) - - # def addAnimationPanel(self,originmodel,filepath): - # return self.property_panel._addAnimationPanel(originmodel,filepath) def updateGUIPropertyPanel(self, gui_element): """更新GUI元素属性面板 - 代理到property_panel""" @@ -380,6 +377,9 @@ class MyWorld(CoreWorld): def removeActorForModel(self,model): return self.property_panel.removeActorForModel( model) + def updateNodeVisibilityAfterDrag(self,item): + return self.property_panel.updateNodeVisibilityAfterDrag(item) + # ==================== 工具管理代理 ==================== def setCurrentTool(self, tool): diff --git a/scene/scene_manager.py b/scene/scene_manager.py index f814efec..75671fe2 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -631,6 +631,7 @@ class SceneManager: # 将碰撞节点附加到模型上 cNodePath = model.attachNewNode(cNode) + #cNodePath.hide() # cNodePath.show() # 取消注释可以显示碰撞体,用于调试 # ==================== 场景树管理 ==================== diff --git a/ui/interface_manager.py b/ui/interface_manager.py index d45c3458..2fc489db 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -1,5 +1,6 @@ from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu from PyQt5.QtCore import Qt +from PyQt5.sip import delete from panda3d.core import GeomNode, ModelRoot @@ -26,8 +27,11 @@ class InterfaceManager: def onTreeItemClicked(self, item, column): """处理树形控件项目点击事件""" if not item: + self.world.selection.updateSelection(None) return + self.world.property_panel.updatePropertyPanel(item) + # 获取节点对象 nodePath = item.data(0, Qt.UserRole) if nodePath: @@ -36,7 +40,7 @@ class InterfaceManager: self.world.selection.updateSelection(nodePath) # 更新属性面板 - self.world.property_panel.updatePropertyPanel(item) + #self.world.property_panel.updatePropertyPanel(item) print(f"树形控件点击: {item.text(0)}") else: @@ -77,6 +81,9 @@ class InterfaceManager: if self.isModelOrChild(item): deleteAction = menu.addAction("删除") deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) + else: + deleteAction = menu.addAction("删除") + deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) # 显示菜单 menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) @@ -94,12 +101,33 @@ class InterfaceManager: try: # 从场景中移除 self.world.property_panel.removeActorForModel(nodePath) + + if hasattr(nodePath,'getPythonTag'): + light_object = nodePath.getPythonTag('rp_light_object') + if light_object and hasattr(self.world,'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + + if hasattr(self.world,'selection'): + if self.world.selection.selectedNode == nodePath: + self.world.selection.clearSelectionBox() + self.world.selection.clearGizmo() + self.world.selection.selectedNode = None + self.world.selection.selectedObject = None + + if nodePath in self.world.Spotlight: + self.world.Spotlight.remove(nodePath) + if nodePath in self.world.Pointlight: + self.world.Pointlight.remove(nodePath) + nodePath.removeNode() + if hasattr(self.world,'selection'): + self.world.selection.checkAndClearIfTargetDeleted() + # 如果是模型根节点,从模型列表中移除 - if item.parent().text(0) == "模型": - if nodePath in self.world.models: - self.world.models.remove(nodePath) + #if item.parent().text(0) == "模型": + if nodePath in self.world.models: + self.world.models.remove(nodePath) # 从树形控件中移除 parentItem = item.parent() @@ -239,4 +267,35 @@ class InterfaceManager: item.setExpanded(True) for i in range(item.childCount()): - self._restore_expanded(item.child(i),path) \ No newline at end of file + self._restore_expanded(item.child(i),path) + + + def syncVisibilityDownward(self): + from collections import deque + + if not self.world.models: + return + q = deque() + + for root in self.world.models: + q.append(root) + + while q: + node = q.popleft() + + visible = node.getPythonTag("visible") + if visible is None: + visible = True + if not visible: + stack = [node] + while stack: + cur = stack.pop() + cur.hide() + + for child in cur.getChildren(): + stack.append(child) + continue + node.show() + + for child in node.getChildren(): + q.append(child) diff --git a/ui/property_panel.py b/ui/property_panel.py index 1684aa7c..eb13630f 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -1,3 +1,4 @@ +from collections import deque from traceback import print_exc from types import new_class from typing import Hashable @@ -50,6 +51,16 @@ class PropertyPanelManager: if item.widget(): item.widget().deleteLater() + def updateNodeVisibilityAfterDrag(self, item): + """拖拽结束后更新节点的可见性状态""" + node = item.data(0, Qt.UserRole) + if not node: + return + + # 当节点被拖拽后,需要根据新父节点的状态来更新可见性 + self._syncEffectiveVisibility(node) + + def updatePropertyPanel(self, item): """更新属性面板显示""" if not self._propertyLayout or not self._propertyLayout.parent(): @@ -68,15 +79,34 @@ class PropertyPanelManager: self._propertyLayout.addWidget(tipLabel) return + model = item.data(0, Qt.UserRole) + + user_visible = True + if model: + user_visible = model.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + model.setPythonTag("user_visible", True) + self.name_group = QGroupBox("物体名称") name_layout = QHBoxLayout() self.active_check = QCheckBox() - self.active_check.setChecked(True) # 默认激活 + # 根据模型的实际可见性状态设置复选框 + self.active_check.setChecked(user_visible) self.name_input = QLineEdit(itemText) name_layout.addWidget(self.active_check) name_layout.addWidget(self.name_input) self.name_group.setLayout(name_layout) self._propertyLayout.addWidget(self.name_group) + + if model: + try: + self.active_check.stateChanged.disconnect() + except TypeError: + pass + self.active_check.stateChanged.connect( + lambda state,m = model:self._setUserVisible(m,state == Qt.Checked) + ) # nameLabel = QLabel("名称:") # nameEdit = QLineEdit(itemText) # self._propertyLayout.addRow(nameLabel, nameEdit) @@ -107,6 +137,59 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() + def _setUserVisible(self,node,visible): + node.setPythonTag("user_visible",visible) + self._syncEffectiveVisibility(node) + + def _syncEffectiveVisibility(self, start_node): + """广度优先,确保父隐藏则子一定隐藏""" + # 获取起始节点的父节点 + parent_node = start_node.getParent() + + # 确定父节点的有效可见性 + parent_effective_visible = True + if parent_node: + parent_effective_visible = parent_node.getPythonTag("effective_visible") + if parent_effective_visible is None: + parent_effective_visible = True + + q = deque([(start_node, parent_effective_visible)]) # (node, parent_effective_visible) + + while q: + node, parent_eff = q.popleft() + user = node.getPythonTag("user_visible") + if user is None: + user = True + eff = parent_eff and user + node.setPythonTag("effective_visible", eff) + + # 特殊处理:检查是否为碰撞体节点 + + if node.getName().startswith("modelCollision_"): + node.hide() + else: + if eff: + node.show() + else: + node.hide() + + for child in node.getChildren(): + q.append((child, eff)) + + def _toggleModelVisibility(self, model, state): + """切换模型可见性状态""" + try: + # 用我们自己维护的可见性接口,而不是直接 show/hide + visible = (state == Qt.Checked) + self._setUserVisible(model, visible) + + collision_nodes = model.findAllMatches("**/modelCollision_*") + for collision_node in collision_nodes: + collision_node.hide() + + except Exception as e: + print(f"切换模型可见性失败: {str(e)}") + def refreshModelValues(self, nodePath): """实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)""" if not nodePath or self._propertyLayout is None: @@ -185,16 +268,31 @@ class PropertyPanelManager: # 设置位置控件属性 for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: - pos_widget.setRange(-1000, 1000) + pos_widget.setRange(-1000000.0, 1000000.0) self.pos_x.setValue(relativePos.getX()) self.pos_y.setValue(relativePos.getY()) self.pos_z.setValue(relativePos.getZ()) # 连接位置变化事件 - self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v)) - self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v)) - self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v)) + # self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v)) + # self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v)) + # self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v)) + + def updateXPosition(value): + model.setX(value) + self.refreshModelValues(model) + self.pos_x.valueChanged.connect(updateXPosition) + + def updateYPosition(value): + model.setY(value) + self.refreshModelValues(model) + self.pos_y.valueChanged.connect(updateYPosition) + + def updateZPosition(value): + model.setZ(value) + self.refreshModelValues(model) + self.pos_z.valueChanged.connect(updateZPosition) # 创建并设置 X, Y, Z 标签居中 x_label1 = QLabel("X") @@ -219,7 +317,7 @@ class PropertyPanelManager: # 设置世界位置控件属性 for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]: - world_pos_widget.setRange(-1000, 1000) + world_pos_widget.setRange(-1000000.0, 1000000.0) world_pos_widget.setReadOnly(True) self.world_pos_x.setValue(worldPos.getX()) @@ -238,7 +336,7 @@ class PropertyPanelManager: # 设置旋转控件属性 for rot_widget in [self.rot_h, self.rot_p, self.rot_r]: - rot_widget.setRange(-180, 180) + rot_widget.setRange(-360, 360) self.rot_h.setValue(model.getH()) self.rot_p.setValue(model.getP()) @@ -4167,14 +4265,6 @@ class PropertyPanelManager: # 忽略 Actor 加载错误,很多模型都不是角色动画 print(f"[信息] 该模型不包含骨骼动画: {actor_error}") - # 只有在没有骨骼动画时才检测非骨骼动画 - if not has_skeletal_anim: - non_skeletal_anims = self._detectNonSkeletalAnimations(origin_model) - if non_skeletal_anims and self._validateNonSkeletalAnimations(origin_model, non_skeletal_anims): - self._buildNonSkeletalUI(origin_model, non_skeletal_anims, animation_layout) - has_animation = True - print(f"[信息] 检测到非骨骼动画: {list(non_skeletal_anims.keys())}") - # 如果都没有动画 if not has_animation: no_anim_label = QLabel("此模型无动画") @@ -4846,434 +4936,7 @@ except Exception as e: origin_model.setPythonTag("anim_speed",speed) print(f"[动画] 速度设为: {speed} ({display_name})") - def _detectNonSkeletalAnimations(self, origin_model): - """检测模型中的非骨骼动画""" - animations = {} - try: - print(f"[调试] 开始检测非骨骼动画: {origin_model}") - - # 1. 精确检测 AnimBundle (非骨骼动画) - bundle_nodes = origin_model.findAllMatches("**/+AnimBundleNode") - print(f"[调试] 找到 AnimBundleNode 数量: {bundle_nodes.getNumPaths()}") - - if not bundle_nodes.isEmpty(): - for i, bundle_node in enumerate(bundle_nodes): - try: - bundle = bundle_node.node().getBundle() - print(f"[调试] AnimBundle #{i}: {bundle}") - - if bundle and hasattr(bundle, 'getNumFrames'): - num_frames = bundle.getNumFrames() - print(f"[调试] Bundle 帧数: {num_frames}") - - # 只有真正有多帧的才认为是动画 - if num_frames > 1: - anim_names = [] - - # 尝试获取动画名称 - try: - if hasattr(bundle, 'getName') and bundle.getName(): - anim_names.append(bundle.getName()) - else: - anim_names.append(f"Animation_{i}") - print(f"[调试] 检测到有效帧动画: {anim_names}, {num_frames} 帧") - except Exception as name_error: - anim_names.append(f"Animation_{i}") - print(f"[调试] 使用默认动画名: Animation_{i}") - - animations['transform'] = { - 'bundle': bundle, - 'names': anim_names, - 'node': bundle_node, - 'frames': num_frames - } - else: - print(f"[调试] Bundle 只有 {num_frames} 帧,不认为是动画") - - except Exception as bundle_error: - print(f"[调试] 处理 bundle 失败: {bundle_error}") - - # 2. 仅对 GLB 文件进行特殊检测 - filepath = origin_model.getTag("model_path") - if filepath and filepath.lower().endswith('.glb') and not animations: - print(f"[调试] GLB 文件特殊检测: {filepath}") - - # 检查是否有任何动画相关的节点名称 - all_nodes = origin_model.findAllMatches("**") - anim_indicators = [] - - for node in all_nodes: - node_name = node.getName().lower() - # 查找典型的动画节点命名模式 - if any(keyword in node_name for keyword in ['anim', 'key', 'frame', 'action', 'timeline']): - anim_indicators.append(node.getName()) - print(f"[调试] 发现可能的动画节点: {node.getName()}") - - # 只有在明确找到动画指示器时才创建动画条目 - if anim_indicators: - animations['glb_keyframe'] = { - 'bundle': None, - 'names': ['GLB_Animation'], - 'node': origin_model, - 'type': 'glb_manual', - 'indicators': anim_indicators - } - print(f"[调试] 创建 GLB 动画条目,基于指示器: {anim_indicators}") - - except Exception as e: - print(f"检测非骨骼动画失败: {e}") - - print(f"[调试] 最终检测结果: {animations}") - return animations if animations else None - - def _validateNonSkeletalAnimations(self, origin_model, animations): - """验证检测到的非骨骼动画是否真的可以播放""" - try: - print(f"[验证] 开始验证非骨骼动画: {list(animations.keys())}") - - for anim_type, anim_data in animations.items(): - if anim_type == 'transform': - # 验证变换动画 - bundle = anim_data.get('bundle') - if bundle: - # 检查是否真的有可播放的动画数据 - if hasattr(bundle, 'getNumFrames'): - frames = bundle.getNumFrames() - if frames <= 1: - print(f"[验证] 变换动画帧数不足: {frames}") - return False - - # 检查是否有有效的动画通道 - try: - if hasattr(bundle, 'getNumChannels'): - channels = bundle.getNumChannels() - if channels == 0: - print(f"[验证] 无有效动画通道") - return False - except: - pass - - elif anim_type == 'glb_keyframe': - # 验证 GLB 动画指示器 - indicators = anim_data.get('indicators', []) - if not indicators: - print(f"[验证] GLB 动画无有效指示器") - return False - - print(f"[验证] 动画验证通过") - return True - - except Exception as e: - print(f"[验证] 动画验证失败: {e}") - return False - - def _buildNonSkeletalUI(self, origin_model, animations, layout): - """构建非骨骼动画控制UI""" - from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox, QTabWidget - - # 动画信息 - info_text = f"非骨骼动画数量: {len(animations)}" - info_label = QLabel(info_text) - info_label.setStyleSheet("color:#888;font-size:10px;") - layout.addWidget(QLabel("信息:"), 0, 0) - layout.addWidget(info_label, 0, 1, 1, 3) - - # 如果有多种类型的动画,使用标签页 - if len(animations) > 1: - tab_widget = QTabWidget() - - for anim_type, anim_data in animations.items(): - tab = QWidget() - tab_layout = QVBoxLayout(tab) - self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data) - tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type)) - - self._propertyLayout.addRow("动画类型:", tab_widget) - else: - # 只有一种类型,直接显示 - anim_type, anim_data = next(iter(animations.items())) - self._buildAnimationTypeUI(self._propertyLayout, origin_model, anim_type, anim_data) - - # 存储动画信息供控制使用 - if not hasattr(self, '_non_skeletal_cache'): - self._non_skeletal_cache = {} - self._non_skeletal_cache[origin_model] = animations - - def _buildAnimationTypeUI(self, layout, origin_model, anim_type, anim_data): - """为特定动画类型构建UI""" - from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox - - if anim_type == 'transform': - # 变换动画控制 - self.ns_transform_combo = QComboBox() - self.ns_transform_combo.addItems(anim_data['names']) - layout.addRow("变换动画:", self.ns_transform_combo) - - elif anim_type == 'texture': - # 纹理动画控制 - self.ns_texture_combo = QComboBox() - self.ns_texture_combo.addItems(anim_data['stages']) - layout.addRow("纹理动画:", self.ns_texture_combo) - - elif anim_type == 'material': - # 材质动画控制 - self.ns_material_combo = QComboBox() - self.ns_material_combo.addItems(anim_data['properties']) - layout.addRow("材质动画:", self.ns_material_combo) - - elif anim_type == 'lerp': - # Lerp动画控制 - self.ns_lerp_combo = QComboBox() - self.ns_lerp_combo.addItems(anim_data['intervals']) - layout.addRow("Lerp动画:", self.ns_lerp_combo) - - # 通用控制按钮 - btn_box = QWidget() - btn_lay = QHBoxLayout(btn_box) - for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")): - btn = QPushButton(txt) - btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c)) - btn_lay.addWidget(btn) - layout.addRow("控制:", btn_box) - - # 播放速度 - speed_spinbox = QDoubleSpinBox() - speed_spinbox.setRange(0.1, 5.0) - speed_spinbox.setSingleStep(0.1) - speed_spinbox.setValue(1.0) - speed_spinbox.valueChanged.connect(lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) - layout.addRow("播放速度:", speed_spinbox) - - def _getAnimTypeDisplayName(self, anim_type): - """获取动画类型的显示名称""" - names = { - 'transform': '变换动画', - 'glb_keyframe': 'GLB关键帧动画', - 'texture': '纹理动画', - 'material': '材质动画', - 'lerp': 'Lerp动画' - } - return names.get(anim_type, anim_type) - - def _controlNonSkeletalAnimation(self, origin_model, anim_type, command): - """控制非骨骼动画播放""" - try: - if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache: - return - - animations = self._non_skeletal_cache[origin_model] - if anim_type not in animations: - return - - anim_data = animations[anim_type] - - if anim_type == 'transform': - self._controlTransformAnimation(origin_model, anim_data, command) - elif anim_type == 'glb_keyframe': - self._controlGLBKeyframeAnimation(origin_model, anim_data, command) - elif anim_type == 'texture': - self._controlTextureAnimation(origin_model, anim_data, command) - elif anim_type == 'material': - self._controlMaterialAnimation(origin_model, anim_data, command) - elif anim_type == 'lerp': - self._controlLerpAnimation(origin_model, anim_data, command) - - print(f"[非骨骼动画] {anim_type} {command}") - - except Exception as e: - print(f"控制非骨骼动画失败: {e}") - - def _controlTransformAnimation(self, origin_model, anim_data, command): - """控制变换动画""" - try: - bundle = anim_data['bundle'] - bundle_node = anim_data['node'] - - print(f"[调试] 控制变换动画: {command}, Bundle: {bundle}") - - if command == 'play': - # 方法1: 通过 AnimBundle 直接播放 - if hasattr(bundle, 'play'): - bundle.play() - print(f"[动画] 通过 bundle.play() 播放") - # 方法2: 通过 AnimControl 播放 - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].play() - print(f"[动画] 通过 AnimControl 播放") - # 方法3: 通过启用动画节点 - else: - bundle_node.node().setPlayRate(1.0) - print(f"[动画] 设置播放速率为 1.0") - - elif command == 'pause': - if hasattr(bundle, 'pause'): - bundle.pause() - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].pause() - else: - bundle_node.node().setPlayRate(0.0) - - elif command == 'stop': - if hasattr(bundle, 'stop'): - bundle.stop() - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].stop() - else: - bundle_node.node().setPlayRate(0.0) - # 重置到第一帧 - if hasattr(bundle, 'setFrame'): - bundle.setFrame(0) - - elif command == 'loop': - if hasattr(bundle, 'loop'): - bundle.loop() - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].loop(True) - controls[0].play() - else: - bundle_node.node().setPlayRate(1.0) - - except Exception as e: - print(f"[错误] 控制变换动画失败: {e}") - import traceback - traceback.print_exc() - - def _controlGLBKeyframeAnimation(self, origin_model, anim_data, command): - """控制 GLB 关键帧动画""" - try: - print(f"[调试] 控制 GLB 动画: {command}") - - # 尝试通过 AnimControlCollection 控制 - from panda3d.core import AnimControlCollection - - # 方法1: 查找 AnimControlCollection - anim_collection = AnimControlCollection() - origin_model.getAnimControls(anim_collection) - - if anim_collection.getNumAnims() > 0: - for i in range(anim_collection.getNumAnims()): - anim_control = anim_collection.getAnim(i) - print(f"[调试] 找到动画控制: {anim_control.getName()}") - - if command == 'play': - anim_control.setPlayRate(1.0) - print(f"[GLB动画] 播放: {anim_control.getName()}") - elif command == 'pause': - anim_control.setPlayRate(0.0) - print(f"[GLB动画] 暂停: {anim_control.getName()}") - elif command == 'stop': - anim_control.setPlayRate(0.0) - anim_control.setFrame(0) - print(f"[GLB动画] 停止: {anim_control.getName()}") - elif command == 'loop': - anim_control.setPlayRate(1.0) - anim_control.loop(True) - print(f"[GLB动画] 循环: {anim_control.getName()}") - return - - # 方法2: 通过自动播放任务 - if command == 'play': - origin_model.setPlayRate(1.0) - print(f"[GLB动画] 设置播放速率为 1.0") - elif command == 'pause': - origin_model.setPlayRate(0.0) - print(f"[GLB动画] 暂停播放") - elif command == 'stop': - origin_model.setPlayRate(0.0) - print(f"[GLB动画] 停止播放") - elif command == 'loop': - origin_model.setPlayRate(1.0) - print(f"[GLB动画] 循环播放") - - except Exception as e: - print(f"[错误] 控制 GLB 动画失败: {e}") - - def _controlTextureAnimation(self, origin_model, anim_data, command): - """控制纹理动画""" - # 纹理动画通常通过 LerpInterval 实现 - print(f"[纹理动画] {command} - 功能待实现") - - def _controlMaterialAnimation(self, origin_model, anim_data, command): - """控制材质动画""" - # 材质动画通常通过修改材质属性实现 - print(f"[材质动画] {command} - 功能待实现") - - def _controlLerpAnimation(self, origin_model, anim_data, command): - """控制Lerp动画""" - # 通过 LerpInterval 控制 - print(f"[Lerp动画] {command} - 功能待实现") - - def _setNonSkeletalAnimationSpeed(self, origin_model, anim_type, speed): - """设置非骨骼动画播放速度""" - try: - if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache: - return - - animations = self._non_skeletal_cache[origin_model] - if anim_type not in animations: - return - - anim_data = animations[anim_type] - - if anim_type == 'transform': - bundle = anim_data['bundle'] - if hasattr(bundle, 'setPlayRate'): - bundle.setPlayRate(speed) - - print(f"[非骨骼动画] {anim_type} 速度设为: {speed}") - - except Exception as e: - print(f"设置非骨骼动画速度失败: {e}") - - def _detectPlayer(self,origin_model): - filepath = origin_model.getTag("model_path") - if filepath: - try: - actor = Actor(filepath) - if actor.getAnimNames(): - actor.cleanup();actor.removeNode() - return ("actor",None) - except: - pass - bundle_np = origin_model.find("**/+AnimBundleNode") - if not bundle_np.isEmpty(): - ctrl = bundle_np.node().getBundle().bind(origin_model.node(),PartGroup.PART_Whole) - return ("bundle",ctrl) - return None - - def _buildBundleUI(self,origin_model,ctrl): - from PyQt5.QtWidgets import QLabel,QPushButton,QHBoxLayout,QWidget,QSlider - - title = QLabel("骨骼动画控制") - title.setStyleSheet("color:#FF8C00;font-weight:bold;font-size:14px;margin-top:10px;") - self._propertyLayout.addRow(title) - - btn_box = QWidget() - lay = QHBoxLayout(btn_box) - for txt , fn in (("播放",ctrl.play), - ("暂停",ctrl.stop), - ("重置",lambda:ctrl.pose(0))): - btn = QPushButton(txt) - btn.clicked.connect(fn) - lay.addWidget(btn) - self._propertyLayout.addRow("控制:",btn_box) - - slider = QSlider() - slider.setOrientation(1) - slider.setRange(0,int(ctrl.getNumFrames())) - slider.valueChanged.connect(ctrl.pose) - self._propertyLayout.addRow("帧:",slider) - - self._actor_cache[origin_model] = ("bundle",ctrl) def _dispatchAnimCommand(self,origin_model,cmd): cache = self._actor_cache.get(origin_model) @@ -5308,20 +4971,6 @@ except Exception as e: elif isinstance(cmd,tuple) and cmd[0] == "speed": actor.setPlayRate(cmd[1], anim_name) - elif kind == "bundle": - ctrl = player - if cmd == "play": - ctrl.play() - elif cmd == "pause": - ctrl.stop() - elif cmd == "stop": - ctrl.stop() - ctrl.pose(0) - elif cmd == "loop": - ctrl.loop(True) - elif isinstance(cmd,tuple) and cmd[0] =="speed": - ctrl.setPlayRate(cmd[1]) - def removeActorForModel(self, model): """删除 model 对应的 Actor(如果存在)""" actor = self._actor_cache.pop(model, None) diff --git a/ui/widgets.py b/ui/widgets.py index 9a1c5dc7..625c4b66 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -339,6 +339,7 @@ class CustomTreeWidget(QTreeWidget): # 获取节点引用 dragged_node = dragged_item.data(0, Qt.UserRole) + amtarget_node = target_item.data(0, Qt.UserRole) # 如果目标是模型根节点,使用 render 作为新父节点 if target_item.text(0) == "模型": @@ -360,12 +361,17 @@ class CustomTreeWidget(QTreeWidget): # 接受拖放事件,更新树形控件 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() - + def isValidParentChild(self, dragged_item, target_item): """检查是否是有效的父子关系""" # 不能拖放到自己上