diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index a1862356..9c9622cd 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -5,7 +5,7 @@ control_points: clouds: - cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000]]] + cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000],[0.4932562620,0.9526462396]]] color_correction: camera_iso: [[[0.4708024067,0.2757660168]]] camera_shutter: [[[0.5134061147,0.0552217053]]] @@ -17,8 +17,8 @@ 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.5000000000]]] - sun_altitude: [[[0.5000000000,0.9666666667]]] + sun_azimuth: [[[0.5000000000,0.4833333333]]] + sun_altitude: [[[0.5000000000,0.9777777778]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: fog_ramp_size: [[[0.5510597303,0.7409470752]]] diff --git a/Resources/models/DancingTwerk.glb b/Resources/models/DancingTwerk.glb new file mode 100644 index 00000000..1567bf93 Binary files /dev/null and b/Resources/models/DancingTwerk.glb differ diff --git a/Resources/models/Haqijingzhu.glb b/Resources/models/Haqijingzhu.glb new file mode 100644 index 00000000..0b5e1df0 Binary files /dev/null and b/Resources/models/Haqijingzhu.glb differ diff --git a/Resources/models/JQB_auto_converted.glb b/Resources/models/JQB_auto_converted.glb new file mode 100644 index 00000000..89df9e5b Binary files /dev/null and b/Resources/models/JQB_auto_converted.glb differ diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py index 232e2139..35000ffe 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -145,7 +145,7 @@ class InfoPanelManager(DirectObject): text_scale=0.045, text_fg=content_color, text_align=TextNode.ALeft, - text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行 + text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行 pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05), parent=panel_node, relief=None, @@ -529,9 +529,7 @@ class InfoPanelManager(DirectObject): -size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05 ) - # 设置一个非常大的换行值,几乎不自动换行 - panel_data['content_label']['text_wordwrap'] = 500 - print(f"更新面板换行: 设置为500(几乎不换行)") + panel_data['content_label']['text_wordwrap'] = 0 # 如果有背景图片,也需要更新其大小 if 'bg_image' in panel_data and panel_data['bg_image']: @@ -575,8 +573,9 @@ class InfoPanelManager(DirectObject): if 'content_size' in properties: panel_data['content_label']['text_scale'] = properties['content_size'] props['content_size'] = properties['content_size'] + current_size = props.get('size',(1.0,0.6)) # 当字体大小改变时,仍然保持较大的换行值 - panel_data['content_label']['text_wordwrap'] = 500 + panel_data['content_label']['text_wordwrap'] = 0 # 更新背景图片 if 'bg_image' in properties: diff --git a/core/selection.py b/core/selection.py index 3e6b6a73..8a3302aa 100644 --- a/core/selection.py +++ b/core/selection.py @@ -2761,4 +2761,23 @@ class SelectionSystem: # 清理其他资源 self.clearSelectionBox() - self.clearGizmo() \ No newline at end of file + self.clearGizmo() + def clearSelection(self): + """清除当前选择""" + try: + self.selectedNode = None + self.selectedObject = None + self.clearSelectionBox() + self.clearGizmo() + + # 清除树形控件中的选择 + if (hasattr(self.world, 'interface_manager') and + self.world.interface_manager and + hasattr(self.world.interface_manager, 'treeWidget') and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + + print("已清除选择") + except Exception as e: + print(f"清除选择失败: {e}") + diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 32a6c721..d99c33d9 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -24,80 +24,80 @@ except ImportError: WEB_ENGINE_AVAILABLE = False print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") - def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): - from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib - - # 参数类型检查和转换 - if isinstance(size, (list, tuple)): - if len(size) >= 2: - x_size, y_size = float(size[0]), float(size[1]) - else: - x_size = y_size = float(size[0]) if size else 1.0 - else: - x_size = y_size = float(size) - - # 创建卡片 - cm = CardMaker('gui_3d_image') - cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) - - # 创建3D图像节点 - image_node = self.world.render.attachNewNode(cm.generate()) - image_node.setPos(*pos) - - # 为3D图像创建独立的材质 - material = Material(f"image-material-{len(self.gui_elements)}") - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setDiffuse(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) - material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 - image_node.setMaterial(material, 1) - - image_node.setTransparency(TransparencyAttrib.MAlpha) - - # 如果提供了图像路径,则加载纹理 - if image_path: - self.update3DImageTexture(image_node, image_path) - - # 应用PBR效果(如果可用) - try: - if hasattr(self, 'render_pipeline') and self.render_pipeline: - self.render_pipeline.set_effect( - image_node, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": False, - "render_envmap": True, - "disable_children_effects": True - }, - 50 - ) - print("✓ GUI 3D图像PBR效果已应用") - except Exception as e: - print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") - - # 为GUI元素添加标识(效仿3D文本方法) - image_node.setTag("gui_type", "3d_image") - image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") - image_node.setTag("is_scene_element", "1") - image_node.setTag("tree_item_type", "GUI_3DIMAGE") - 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 createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + # from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib + # + # # 参数类型检查和转换 + # if isinstance(size, (list, tuple)): + # if len(size) >= 2: + # x_size, y_size = float(size[0]), float(size[1]) + # else: + # x_size = y_size = float(size[0]) if size else 1.0 + # else: + # x_size = y_size = float(size) + # + # # 创建卡片 + # cm = CardMaker('gui_3d_image') + # cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) + # + # # 创建3D图像节点 + # image_node = self.world.render.attachNewNode(cm.generate()) + # image_node.setPos(*pos) + # + # # 为3D图像创建独立的材质 + # material = Material(f"image-material-{len(self.gui_elements)}") + # material.setBaseColor(LColor(1, 1, 1, 1)) + # material.setDiffuse(LColor(1, 1, 1, 1)) + # material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + # material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + # material.setShininess(10.0) + # material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + # image_node.setMaterial(material, 1) + # + # image_node.setTransparency(TransparencyAttrib.MAlpha) + # + # # 如果提供了图像路径,则加载纹理 + # if image_path: + # self.update3DImageTexture(image_node, image_path) + # + # # 应用PBR效果(如果可用) + # try: + # if hasattr(self, 'render_pipeline') and self.render_pipeline: + # self.render_pipeline.set_effect( + # image_node, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": False, + # "render_envmap": True, + # "disable_children_effects": True + # }, + # 50 + # ) + # print("✓ GUI 3D图像PBR效果已应用") + # except Exception as e: + # print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + # + # # 为GUI元素添加标识(效仿3D文本方法) + # image_node.setTag("gui_type", "3d_image") + # image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + # image_node.setTag("is_scene_element", "1") + # image_node.setTag("tree_item_type", "GUI_3DIMAGE") + # if image_path: + # image_node.setTag("gui_image_path", image_path) + # image_node.setTag("is_gui_element", "1") + # + # self.gui_elements.append(image_node) + # + # # 更新场景树 + # if hasattr(self.world, 'updateSceneTree'): + # self.world.updateSceneTree() + # + # print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") + # return image_node class GUIManager: """GUI元素管理系统类""" diff --git a/main.py b/main.py index 77e82536..bd204899 100644 --- a/main.py +++ b/main.py @@ -895,6 +895,22 @@ class MyWorld(CoreWorld): except Exception as e: print(f"创建默认自动朝向巡检路线失败: {e}") + def _serializeNode(self, node): + """序列化节点数据""" + try: + return self.world.scene_manager.serializeNode(node) + except Exception as e: + print(f"序列化节点失败: {e}") + return None + + def _deserializeNode(self, node_data, parent_node): + """反序列化节点数据""" + try: + return self.world.scene_manager.deserializeNode(node_data, parent_node) + except Exception as e: + print(f"反序列化节点失败: {e}") + return None + # ==================== 项目管理功能代理 ==================== # 以下函数代理到project_manager模块的对应功能 diff --git a/scene/scene_manager.py b/scene/scene_manager.py index b2ce3739..1ad2d9ab 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -7,6 +7,7 @@ """ import os +import time from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QTreeWidgetItem @@ -165,16 +166,16 @@ class SceneManager: model.setTag("converted_to_glb", "true") # 特殊处理FBX模型 - if filepath.lower().endswith('.fbx'): - print("检测到FBX模型,应用特殊处理...") - - # 将模型缩放设置为原来的1/100 - model.setScale(0.01) - print("设置模型缩放为 0.01 (原始大小的1/100)") - - # 设置模型旋转为 (0, 90, 0) - model.setHpr(0, 90, 0) - print("设置模型旋转为 (0, 90, 0)") + # if filepath.lower().endswith('.fbx'): + # print("检测到FBX模型,应用特殊处理...") + # + # # 将模型缩放设置为原来的1/100 + # model.setScale(0.01) + # print("设置模型缩放为 0.01 (原始大小的1/100)") + # + # # 设置模型旋转为 (0, 90, 0) + # model.setHpr(0, 90, 0) + # print("设置模型旋转为 (0, 90, 0)") # # 可选的单位转换(主要针对FBX # if apply_unit_conversion and filepath.lower().endswith('.fbx'): @@ -2294,6 +2295,9 @@ class SceneManager: light.casts_shadows = True light.shadow_map_resolution = 256 + light_pos = light_node.getPos() + light.setPos(light_pos) + # 添加到渲染管线 render_pipeline = get_render_pipeline() render_pipeline.add_light(light) @@ -3456,4 +3460,729 @@ except Exception as e: return None + def serializeNode(self, node): + """序列化节点为字典数据""" + try: + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + + # 特殊处理不同类型的节点 + if hasattr(node.node(), 'getClassType'): + node_class = node.node().getClassType().getName() + node_data['node_class'] = node_class + + # 递归序列化子节点 + for child in node.getChildren(): + # 跳过辅助节点 + if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')): + child_data = self.serializeNode(child) + if child_data: + node_data['children'].append(child_data) + + return node_data + + except Exception as e: + print(f"序列化节点 {node.getName()} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def deserializeNode(self, node_data, parent_node): + """从字典数据反序列化节点""" + try: + # 创建新节点 + node_name = node_data.get('name', 'node') + new_node = parent_node.attachNewNode(node_name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + new_node.setTag(tag_key, tag_value) + + # 根据节点类型进行特殊处理 + node_type = node_data.get('type', '') + node_class = node_data.get('node_class', '') + + # 特殊处理光源节点 + if 'light_type' in node_data.get('tags', {}): + light_type = node_data['tags']['light_type'] + if light_type == 'spot_light': + self._recreateSpotLight(new_node) + elif light_type == 'point_light': + self._recreatePointLight(new_node) + + # 递归创建子节点 + for child_data in node_data.get('children', []): + self.deserializeNode(child_data, new_node) + + return new_node + + except Exception as e: + print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def serializeNodeForCopy(self, node): + """序列化节点用于复制操作,完整保存视觉属性""" + try: + if not node or node.isEmpty(): + return None + + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + try: + if hasattr(node, 'getTagKeys'): + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + except Exception as e: + print(f"获取标签时出错: {e}") + + # 保存视觉属性 + try: + # 保存颜色属性 + if hasattr(node, 'getColor'): + color = node.getColor() + node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW()) + + # 保存材质属性 + if hasattr(node, 'getMaterial'): + material = node.getMaterial() + if material: + material_data = {} + material_data['base_color'] = ( + material.getBaseColor().getX(), + material.getBaseColor().getY(), + material.getBaseColor().getZ(), + material.getBaseColor().getW() + ) + material_data['ambient'] = ( + material.getAmbient().getX(), + material.getAmbient().getY(), + material.getAmbient().getZ(), + material.getAmbient().getW() + ) + material_data['diffuse'] = ( + material.getDiffuse().getX(), + material.getDiffuse().getY(), + material.getDiffuse().getZ(), + material.getDiffuse().getW() + ) + material_data['specular'] = ( + material.getSpecular().getX(), + material.getSpecular().getY(), + material.getSpecular().getZ(), + material.getSpecular().getW() + ) + material_data['shininess'] = material.getShininess() + node_data['material'] = material_data + + except Exception as e: + print(f"保存视觉属性时出错: {e}") + + # 根据节点类型保存特定信息 + if node.hasTag("tree_item_type"): + node_type = node.getTag("tree_item_type") + node_data['node_type'] = node_type + + # 保存特定类型节点的额外信息 + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + # 保存光源特定信息 + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + node_data['light_data'] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else None, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light, + 'casts_shadows') else True, + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr( + rp_light, 'shadow_map_resolution') else 256 + } + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]: + # 保存GUI元素特定信息 + node_data['gui_data'] = self._serializeGUIData(node) + elif node_type == "IMPORTED_MODEL_NODE": + # 保存模型特定信息 + node_data['model_data'] = self._serializeModelData(node) + + return node_data + + except Exception as e: + print(f"序列化节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _serializeGUIData(self, node): + """序列化GUI元素数据""" + try: + gui_data = {} + + # 保存GUI相关的通用属性 + if node.hasTag("gui_type"): + gui_data['gui_type'] = node.getTag("gui_type") + + # 保存文本内容(如果有的话) + if node.hasTag("text"): + gui_data['text'] = node.getTag("text") + + # 保存其他GUI相关标签 + gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size'] + for tag in gui_tags: + if node.hasTag(tag): + gui_data[tag] = node.getTag(tag) + + return gui_data + except Exception as e: + print(f"序列化GUI数据失败: {e}") + return {} + + def _serializeModelData(self, node): + """序列化模型数据,包括材质信息""" + try: + model_data = {} + + # 保存模型相关的标签 + model_tags = ['model_path', 'file', 'element_type'] + for tag in model_tags: + if node.hasTag(tag): + model_data[tag] = node.getTag(tag) + + # 保存材质信息 + try: + # 获取模型的材质信息 + if hasattr(node, 'getState'): + state = node.getState() + if state: + # 保存基础颜色信息 + from panda3d.core import ColorAttrib + color_attrib = state.getColor() + if color_attrib: + model_data['base_color'] = ( + color_attrib.getColor().getX(), + color_attrib.getColor().getY(), + color_attrib.getColor().getZ(), + color_attrib.getColor().getW() + ) + + # 保存其他材质属性 + from panda3d.core import MaterialAttrib + material_attrib = state.getAttrib(MaterialAttrib.getClassType()) + if material_attrib: + material = material_attrib.getMaterial() + if material: + # 保存基础颜色 + base_color = material.getBaseColor() + model_data['material_base_color'] = ( + base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW() + ) + + # 保存环境光颜色 + ambient_color = material.getAmbient() + model_data['material_ambient_color'] = ( + ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(), + ambient_color.getW() + ) + + # 保存漫反射颜色 + diffuse_color = material.getDiffuse() + model_data['material_diffuse_color'] = ( + diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(), + diffuse_color.getW() + ) + + # 保存高光颜色 + specular_color = material.getSpecular() + model_data['material_specular_color'] = ( + specular_color.getX(), specular_color.getY(), specular_color.getZ(), + specular_color.getW() + ) + + # 保存粗糙度和金属度等参数 + model_data['material_roughness'] = material.getRoughness() + model_data['material metallic'] = material.getMetallic() + + except Exception as e: + print(f"保存材质信息时出错: {e}") + + return model_data + except Exception as e: + print(f"序列化模型数据失败: {e}") + return {} + + def recreateNodeFromData(self, node_data, parent_node): + """根据数据重建节点,并确保在场景树中显示""" + try: + if not node_data or not parent_node or parent_node.isEmpty(): + return None + + print(f"正在重建节点 {node_data}") + node_type = node_data.get('node_type', '') + original_name = node_data.get('name', 'node') + + # 生成唯一名称 + unique_name = self._generateUniqueName(original_name, parent_node) + + # 根据节点类型调用相应的重建方法 + new_node = None + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + new_node = self._recreateLightFromData(node_data, parent_node, unique_name) + elif node_type == "CESIUM_TILESET_NODE": + new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIRTUAL_SCREEN"]: + new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) + elif node_type == "IMPORTED_MODEL_NODE": + new_node = self._recreateModelFromData(node_data, parent_node, unique_name) + else: + # 创建普通节点 + new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name) + + # 如果成功创建节点,确保它在场景树中显示 + if new_node: + # 尝试更新场景树以显示新节点 + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + # 查找父节点在场景树中的对应项 + parent_item = self._findTreeItemForNode(parent_node) + if parent_item: + # 添加新节点到场景树 + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE") + except Exception as e: + print(f"添加节点到场景树时出错: {e}") + + return new_node + + except Exception as e: + print(f"重建节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _findTreeItemForNode(self, node): + """根据节点查找对应的场景树项""" + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + # 遍历场景树查找匹配的节点项 + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + result = self._findTreeItemForNodeRecursive(item, node) + if result: + return result + return None + except Exception as e: + print(f"查找场景树项时出错: {e}") + return None + + def _findTreeItemForNodeRecursive(self, item, target_node): + """递归查找场景树项""" + try: + # 检查当前项是否匹配 + item_node = getattr(item, 'node_path', None) + if not item_node: + item_node = getattr(item, 'node', None) + + if item_node and item_node == target_node: + return item + + # 递归检查子项 + for i in range(item.childCount()): + child_item = item.child(i) + result = self._findTreeItemForNodeRecursive(child_item, target_node) + if result: + return result + + return None + except Exception as e: + print(f"递归查找场景树项时出错: {e}") + return None + + def _recreateLightFromData(self, node_data, parent_node, name): + """根据数据重建光源""" + try: + light_type = node_data.get('tags', {}).get('light_type', 'spot_light') + + # 创建光源 + if light_type == 'spot_light': + light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0))) + else: # point_light + light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0))) + + if light_node: + # 设置名称 + light_node.setName(name) + + # 恢复其他属性 + light_data = node_data.get('light_data', {}) + rp_light = light_node.getPythonTag("rp_light_object") + if rp_light and light_data: + if 'energy' in light_data: + rp_light.energy = light_data['energy'] + if 'radius' in light_data: + rp_light.radius = light_data['radius'] + if 'fov' in light_data and hasattr(rp_light, 'fov'): + rp_light.fov = light_data['fov'] + if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'): + rp_light.inner_radius = light_data['inner_radius'] + if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'): + rp_light.casts_shadows = light_data['casts_shadows'] + if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'): + rp_light.shadow_map_resolution = light_data['shadow_map_resolution'] + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name', 'light_type']: + light_node.setTag(tag_key, str(tag_value)) + + return light_node + + except Exception as e: + print(f"重建光源失败: {e}") + return None + + def _recreateTilesetFromData(self, node_data, parent_node, name): + """根据数据重建Tileset""" + try: + tileset_url = node_data.get('tileset_url', '') + if not tileset_url: + return None + + # 使用现有方法加载tileset + position = node_data.get('pos', (0, 0, 0)) + tileset_node = self.load_cesium_tileset(tileset_url, position) + + if tileset_node: + # 设置名称 + tileset_node.setName(name) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + tileset_node.setTag(tag_key, str(tag_value)) + + return tileset_node + + except Exception as e: + print(f"重建Tileset失败: {e}") + return None + + def _recreateGUIFromData(self, node_data, parent_node, name): + """根据数据重建GUI元素""" + try: + gui_data = node_data.get('gui_data', {}) + #gui_type = gui_data.get('gui_type', '') + gui_type = node_data.get("tags").get("gui_type", "") + + print(f"正在重建GUI元素: {gui_type}") + print(f"正在重建GUI元素: {node_data}") + + # 根据GUI类型调用相应的创建方法 + new_gui_element = None + + if gui_type == "button" and hasattr(self.world, 'createGUIButton'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + print(pos,text,size) + new_gui_element = self.world.createGUIButton(pos,text,size) + elif gui_type == "label" and hasattr(self.world, 'createGUILabel'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUILabel(pos,text,size) + elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUIEntry(pos,text,size) + elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('image_path', '') + size = node_data.get('size', 1) + new_gui_element = self.world.createGUI2DImage(pos, image_path, size) + elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'): + print("正在创建3D文本!!!") + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags', {}).get('gui_text', '') + scale = node_data.get('scale', 1) + if isinstance(scale, (list, tuple)): + scale = scale[0] if len(scale) > 0 else 1 + print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}") + new_gui_element = self.world.createGUI3DText(pos, text, scale) + elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('image_path', '') + scale = node_data.get('scale', (1, 1)) + if isinstance(scale, (int, float)): + scale = (scale, scale) + elif isinstance(scale, (list, tuple)) and len(scale) >= 2: + scale = (scale[0], scale[1]) + else: + scale = (1, 1) + print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") + new_gui_element = self.world.createGUI3DImage(pos, image_path, scale) + + if new_gui_element: + # 设置名称和变换 + if hasattr(new_gui_element, 'setName'): + new_gui_element.setName(name) + + # 设置位置、旋转、缩放 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + if hasattr(new_gui_element, 'setPos'): + new_gui_element.setPos(*pos) + if hasattr(new_gui_element, 'setHpr'): + new_gui_element.setHpr(*hpr) + if hasattr(new_gui_element, 'setScale'): + new_gui_element.setScale(*scale) + + # 恢复文本内容 + if 'text' in gui_data and hasattr(new_gui_element, 'setText'): + new_gui_element.setText(gui_data['text']) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']: + new_gui_element.setTag(tag_key, str(tag_value)) + + print(f"GUI元素重建成功: {name}") + + return new_gui_element + + except Exception as e: + print(f"重建GUI元素失败: {e}") + import traceback + traceback.print_exc() + return None + + def _recreateModelFromData(self, node_data, parent_node, name): + """根据数据重建模型,保持材质""" + try: + model_data = node_data.get('model_data', {}) + model_path = model_data.get('model_path', model_data.get('file', '')) + + if not model_path or not os.path.exists(model_path): + # 如果原始模型文件不存在,创建一个基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + # 导入模型,保持原有参数 + model = self.importModel( + model_path, + apply_unit_conversion=False, # 已经处理过的模型不需要再次转换 + normalize_scales=False, # 保持原有缩放 + auto_convert_to_glb=False # 已经处理过的模型不需要再次转换 + ) + + if model: + # 设置名称 + model.setName(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + model.setPos(*pos) + model.setHpr(*hpr) + model.setScale(*scale) + + # 恢复材质信息 + try: + self._restoreModelMaterial(model, model_data) + except Exception as e: + print(f"恢复模型材质时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + model.setTag(tag_key, str(tag_value)) + + # 添加到模型列表 + if model not in self.models: + self.models.append(model) + + return model + + except Exception as e: + print(f"重建模型失败: {e}") + # 出错时创建基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + def _restoreModelMaterial(self, model, model_data): + """恢复模型材质""" + try: + # 恢复基础颜色 + if 'base_color' in model_data: + from panda3d.core import ColorAttrib + base_color = model_data['base_color'] + color = (base_color[0], base_color[1], base_color[2], base_color[3]) + model.setColor(color) + + # 恢复复杂材质属性 + if any(key.startswith('material_') for key in model_data.keys()): + from panda3d.core import Material + + # 创建新材质或获取现有材质 + material = Material() + + # 恢复基础颜色 + if 'material_base_color' in model_data: + base_color = model_data['material_base_color'] + material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3])) + + # 恢复环境光颜色 + if 'material_ambient_color' in model_data: + ambient_color = model_data['material_ambient_color'] + material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3])) + + # 恢复漫反射颜色 + if 'material_diffuse_color' in model_data: + diffuse_color = model_data['material_diffuse_color'] + material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])) + + # 恢复高光颜色 + if 'material_specular_color' in model_data: + specular_color = model_data['material_specular_color'] + material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3])) + + # 恢复粗糙度和金属度 + if 'material_roughness' in model_data: + material.setRoughness(model_data['material_roughness']) + + if 'material_metallic' in model_data: + material.setMetallic(model_data['material_metallic']) + + # 应用材质到模型 + model.setMaterial(material) + + except Exception as e: + print(f"恢复材质失败: {e}") + + def _createBasicNodeFromData(self, node_data, parent_node, name): + """创建基本节点,保持视觉属性""" + try: + new_node = parent_node.attachNewNode(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复视觉属性 + try: + # 恢复颜色 + if 'color' in node_data: + color_data = node_data['color'] + new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3]) + + # 恢复材质 + if 'material' in node_data: + from panda3d.core import Material + material_data = node_data['material'] + material = Material() + + if 'base_color' in material_data: + bc = material_data['base_color'] + material.setBaseColor((bc[0], bc[1], bc[2], bc[3])) + + if 'ambient' in material_data: + ac = material_data['ambient'] + material.setAmbient((ac[0], ac[1], ac[2], ac[3])) + + if 'diffuse' in material_data: + dc = material_data['diffuse'] + material.setDiffuse((dc[0], dc[1], dc[2], dc[3])) + + if 'specular' in material_data: + sc = material_data['specular'] + material.setSpecular((sc[0], sc[1], sc[2], sc[3])) + + if 'shininess' in material_data: + material.setShininess(material_data['shininess']) + + new_node.setMaterial(material) + + except Exception as e: + print(f"恢复视觉属性时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + new_node.setTag(tag_key, str(tag_value)) + + return new_node + + except Exception as e: + print(f"创建基本节点失败: {e}") + return None + + def _generateUniqueName(self, base_name, parent_node): + """生成唯一节点名称""" + try: + # 移除可能的数字后缀 + import re + import time + name_base = re.sub(r'_\d+$', '', base_name) + + # 查找现有同名节点 + counter = 1 + unique_name = base_name + while True: + # 检查父节点下是否已存在同名子节点 + existing_node = parent_node.find(unique_name) + if existing_node.isEmpty(): + break + unique_name = f"{name_base}_{counter}" + counter += 1 + if counter > 1000: # 防止无限循环 + unique_name = f"{name_base}_{int(time.time())}" + break + + return unique_name + except: + return f"{base_name}_{int(time.time())}" + diff --git a/ui/main_window.py b/ui/main_window.py index 4e9139b4..4fb010b5 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -19,6 +19,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction QSpinBox, QFrame) from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import OrthographicLens from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget from ui.icon_manager import get_icon_manager, get_icon @@ -37,6 +38,10 @@ class MainWindow(QMainWindow): self.world = world self.world.main_window = self # 关键:让world对象能访问主窗口 + #剪切板相关属性 + self.clipboard = [] + self.clipboard_mode = None + # 初始化图标管理器并打印调试信息 self.icon_manager = get_icon_manager() print("🔧 图标管理器初始化完成") @@ -808,6 +813,238 @@ class MainWindow(QMainWindow): # 统一连接信号到处理方法 self.connectCreateMenuActions() + def setupViewMenuActions(self): + """设置视图菜单动作""" + # 连接视图菜单事件 + self.viewPerspectiveAction.triggered.connect(self.onViewPerspective) + self.viewTopAction.triggered.connect(self.onViewTop) + self.viewFrontAction.triggered.connect(self.onViewFront) + self.viewOrthographicAction = self.viewMenu.addAction('正交视图') # 添加正交视图动作 + self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) + self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接 + + # 保存原始相机设置 + self._original_camera_fov = 80 + self._original_camera_pos = (0, -50, 20) + self._original_camera_lookat = (0, 0, 0) + + self._grid_visible = False + def onViewPerspective(self): + """切换到透视视图""" + try: + lens = self.world.cam.node().getLens() + lens.setFov(self._original_camera_fov) + except Exception as e: + print(f"切换到透视视图失败{e}") + + def onViewOrthographic(self): + """切换到正交视图""" + try: + # 保存当前相机设置(如果是透视模式) + lens = self.world.cam.node().getLens() + if not hasattr(self, '_saved_perspective_settings'): + self._saved_perspective_settings = { + 'fov': lens.getFov()[0], + 'pos': self.world.cam.getPos(), + 'hpr': self.world.cam.getHpr() + } + + # 获取窗口尺寸 + win_width, win_height = self.world.getWindowSize() + aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 + + # 修改现有镜头为正交投影 + if not isinstance(lens, OrthographicLens): + # 保存当前镜头类型 + self._original_lens = lens + + # 创建正交镜头并替换现有镜头 + ortho_lens = OrthographicLens() + ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 + ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 + + # 应用正交镜头 + self.world.cam.node().setLens(ortho_lens) + else: + # 如果已经是正交镜头,则调整其参数 + film_height = 20 + film_width = film_height * aspect_ratio + lens.setFilmSize(film_width, film_height) + + print("切换到正交视图") + except Exception as e: + print(f"切换正交视图失败: {e}") + + def onViewTop(self): + """切换到俯视图(正交)""" + try: + # 保存当前设置 + self._saveCurrentCameraSettings() + + # 设置正交投影 + self._setupOrthographicLens() + + # 设置摄像机位置(从上方俯视) + self.world.cam.setPos(0, 0, 30) + self.world.cam.lookAt(0, 0, 0) + self.world.cam.setHpr(0, -90, 0) # 朝下看 + + # 更新菜单项文本 + self._updateViewMenuText() + + print("切换到俯视图") + except Exception as e: + print(f"切换俯视图失败: {e}") + + def onViewFront(self): + """切换到前视图(正交)""" + try: + # 保存当前设置 + self._saveCurrentCameraSettings() + + # 设置正交投影 + self._setupOrthographicLens() + + # 设置摄像机位置(从前方向看) + self.world.cam.setPos(0, -30, 0) + self.world.cam.lookAt(0, 0, 0) + self.world.cam.setHpr(0, 0, 0) # 正面朝向 + + # 更新菜单项文本 + self._updateViewMenuText() + + print("切换到前视图") + except Exception as e: + print(f"切换前视图失败: {e}") + + def onViewGrid(self): + """切换网格显示/隐藏""" + try: + # 切换网格显示状态 + self._grid_visible = not self._grid_visible + + # 查找网格节点 + grid_node = self.world.render.find("**/grid") + if grid_node.isEmpty(): + # 如果网格不存在则创建 + self._createGridView() + grid_node = self.world.render.find("**/grid") + + # 设置网格可见性 + if not grid_node.isEmpty(): + if self._grid_visible: + grid_node.show() + self.viewGridAction.setText("隐藏网格") + print("网格已显示") + else: + grid_node.hide() + self.viewGridAction.setText("显示网格") + print("网格已隐藏") + else: + print("网格节点未找到") + + except Exception as e: + print(f"切换网格显示失败: {e}") + def _createGridView(self): + """创建网格视图""" + try: + from panda3d.core import LineSegs,Vec3 + grid_node = self.world.render.attachNewNode("grid") + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(0.3,0.3,0.3,1.0) + grid_size = 20 + grid_step = 1 + + for i in range(-grid_size,grid_size+1,grid_step): + lines.moveTo(Vec3(-grid_size,i,0)) + lines.drawTo(Vec3(grid_size,i,0)) + + grid_node.attachNewNode(lines.create()) + # 添加中心轴线(红色X轴,绿色Y轴) + axis_lines = LineSegs() + axis_lines.setThickness(2.0) + # X轴(红色) + axis_lines.setColor(1.0,0.0,0.0,1.0) + axis_lines.moveTo(Vec3(0,0,0)) + axis_lines.drawTo(Vec3(grid_size,0,0)) + # Y轴(绿色) + axis_lines.setColor(0.0, 1.0, 0.0, 1.0) + axis_lines.moveTo(Vec3(0, 0, 0)) + axis_lines.drawTo(Vec3(0, grid_size, 0)) + + grid_node.attachNewNode(axis_lines.create()) + + print("网格已创建") + except Exception as e: + print(f"创建网格失败{e}") + + def _saveCurrentCameraSettings(self): + """保存当前相机设置""" + try: + lens = self.world.cam.node().getLens() + self._saved_camera_settings = { + 'lens_type': 'perspective' if not isinstance(lens, OrthographicLens) else 'orthographic', + 'fov': lens.getFov()[0] if hasattr(lens, 'getFov') else None, + 'film_size': (lens.getFilmSize()[0], lens.getFilmSize()[1]) if hasattr(lens, 'getFilmSize') else None, + 'pos': self.world.cam.getPos(), + 'hpr': self.world.cam.getHpr() + } + except Exception as e: + print(f"保存相机设置失败: {e}") + + def _setupOrthographicLens(self): + """设置正交镜头""" + try: + win_width, win_height = self.world.getWindowSize() + aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 + + from panda3d.core import OrthographicLens + ortho_lens = OrthographicLens() + ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 + ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 + + self.world.cam.node().setLens(ortho_lens) + except Exception as e: + print(f"设置正交镜头失败: {e}") + + def _updateViewMenuText(self): + """更新视图菜单文本""" + try: + lens = self.world.cam.node().getLens() + from panda3d.core import OrthographicLens + + # 更新正交/透视视图动作文本 + if isinstance(lens, OrthographicLens): + self.viewOrthographicAction.setText("切换到透视视图") + self.viewOrthographicAction.triggered.disconnect() + self.viewOrthographicAction.triggered.connect(self.onViewPerspective) + else: + self.viewOrthographicAction.setText("切换到正交视图") + self.viewOrthographicAction.triggered.disconnect() + self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) + except Exception as e: + print(f"更新视图菜单文本失败: {e}") + + # 如果需要在窗口大小改变时调整正交镜头,可以添加以下方法 + def _onWindowResized(self): + """窗口大小改变时的处理""" + try: + lens = self.world.cam.node().getLens() + from panda3d.core import OrthographicLens + + # 如果当前是正交镜头,需要根据新窗口大小调整 + if isinstance(lens, OrthographicLens): + win_width, win_height = self.world.getWindowSize() + if win_height != 0: + aspect_ratio = win_width / win_height + film_height = 20 + film_width = film_height * aspect_ratio + lens.setFilmSize(film_width, film_height) + except Exception as e: + print(f"窗口大小调整失败: {e}") + + def connectCreateMenuActions(self): """统一连接创建菜单的信号到处理方法""" # 连接到world对象的创建方法 @@ -1564,6 +1801,9 @@ class MainWindow(QMainWindow): self.buildAction.triggered.connect(lambda: buildPackage(self)) self.exitAction.triggered.connect(QApplication.instance().quit) + #添加保存项目快捷键盘 + self.saveAction.setShortcut(QKeySequence.Save) + # 连接工具事件 self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑")) self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑")) @@ -1593,6 +1833,21 @@ class MainWindow(QMainWindow): lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0)) print("已连接点击信号") + self.undoAction.triggered.connect(self.onUndo) + self.redoAction.triggered.connect(self.onRedo) + self.cutAction.triggered.connect(self.onCut) + self.copyAction.triggered.connect(self.onCopy) + self.pasteAction.triggered.connect(self.onPaste) + + self.undoAction.setShortcut(QKeySequence.Undo) + self.redoAction.setShortcut(QKeySequence.Redo) + self.cutAction.setShortcut(QKeySequence.Cut) + self.copyAction.setShortcut(QKeySequence.Copy) + self.pasteAction.setShortcut(QKeySequence.Paste) + + #连接视图菜单事件 + self.setupViewMenuActions() + # 连接工具切换信号 #self.toolGroup.buttonClicked.connect(self.onToolChanged) @@ -1603,6 +1858,288 @@ class MainWindow(QMainWindow): # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + def onCopy(self): + """复制操作""" + try: + selected_item = self.treeWidget.currentItem() + if not selected_item: + QMessageBox.information(self, "提示", "请先选择要复制的节点") + return + + # 获取选中的节点 + selected_node = getattr(selected_item, 'node_path', None) + if not selected_node: + selected_node = getattr(selected_item, 'node', None) + + if not selected_node and hasattr(self.world, 'selection'): + selected_node = getattr(self.world.selection, 'selectedNode', None) + + if not selected_node or selected_node.isEmpty(): + QMessageBox.warning(self, "错误", "无法获取选中节点") + return + + # 检查是否是根节点 + if selected_node.getName() == "render": + QMessageBox.warning(self, "错误", "不能复制根节点") + return + + # 序列化节点数据 + node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) + if not node_data: + QMessageBox.warning(self, "错误", "无法序列化选中节点") + return + + # 存储到剪切板 + self.clipboard = [node_data] + self.clipboard_mode = "copy" + + QMessageBox.information(self, "成功", "节点已复制到剪切板") + + except Exception as e: + QMessageBox.critical(self, "错误", f"复制操作失败: {str(e)}") + + def onCut(self): + """剪切操作""" + try: + selected_item = self.treeWidget.currentItem() + if not selected_item: + QMessageBox.information(self, "提示", "请先选择要剪切的节点") + return + + # 获取选中的节点 + selected_node = getattr(selected_item, 'node_path', None) + if not selected_node: + selected_node = getattr(selected_item, 'node', None) + + if not selected_node and hasattr(self.world, 'selection'): + selected_node = getattr(self.world.selection, 'selectedNode', None) + + if not selected_node or selected_node.isEmpty(): + QMessageBox.warning(self, "错误", "无法获取选中节点") + return + + # 检查是否是根节点或特殊节点 + if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]: + QMessageBox.warning(self, "错误", "不能剪切根节点或系统节点") + return + + # 序列化节点数据 + node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) + if not node_data: + QMessageBox.warning(self, "错误", "无法序列化选中节点") + return + + # 存储到剪切板 + self.clipboard = [node_data] + self.clipboard_mode = "cut" + + # 删除原节点 + self.treeWidget.delete_items([selected_item]) + + QMessageBox.information(self, "成功", "节点已剪切到剪切板") + + except Exception as e: + QMessageBox.critical(self, "错误", f"剪切操作失败: {str(e)}") + + def onPaste(self): + """粘贴操作""" + try: + if not self.clipboard: + QMessageBox.information(self, "提示", "剪切板为空") + return + + # 获取粘贴目标节点 + parent_item = self.treeWidget.currentItem() + parent_node = None + + # 如果选中了节点,将其作为父节点 + if parent_item: + parent_node = getattr(parent_item, 'node_path', None) + if not parent_node: + parent_node = getattr(parent_item, 'node', None) + + # 确保获取到有效的父节点 + if parent_node and not parent_node.isEmpty(): + print(f"将粘贴到选中的节点: {parent_node.getName()}") + else: + parent_node = None + + # 如果没有选中有效节点,默认粘贴到render节点下 + if not parent_node: + print("未选中有效节点,将粘贴到根节点下") + # 查找render节点 + for i in range(self.treeWidget.topLevelItemCount()): + item = self.treeWidget.topLevelItem(i) + if item.text(0) == "render": + parent_item = item + break + + # 如果找到了render节点项,获取对应的节点 + if parent_item: + parent_node = getattr(parent_item, 'node_path', None) + if not parent_node: + parent_node = getattr(parent_item, 'node', None) + + # 如果仍然没有找到父节点项,直接使用world.render + if not parent_node: + parent_node = self.world.render + + # 检查父节点有效性 + if not parent_node or parent_node.isEmpty(): + QMessageBox.warning(self, "错误", "无法获取有效的父节点") + return + + # 检查目标节点是否为允许的父节点类型 + parent_name = parent_node.getName() + if parent_name in ["camera", "ambientLight", "directionalLight"]: + QMessageBox.warning(self, "错误", "不能粘贴到该类型节点下") + return + + # 粘贴节点 + pasted_nodes = [] + for node_data in self.clipboard: + print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}") + new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node) + if new_node: + pasted_nodes.append(new_node) + print(f"成功粘贴节点: {new_node.getName()}") + else: + print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}") + + # 如果是剪切操作,清空剪切板 + if self.clipboard_mode == "cut": + self.clipboard.clear() + self.clipboard_mode = None + + QMessageBox.information(self, "成功", f"已粘贴 {len(pasted_nodes)} 个节点") + + except Exception as e: + QMessageBox.critical(self, "错误", f"粘贴操作失败: {str(e)}") + + def _serializeNode(self, node): + """序列化节点数据""" + try: + if not node or node.isEmpty(): + return None + + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + try: + # 使用更安全的方式获取标签 + if hasattr(node, 'getTagKeys'): + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + except Exception as e: + print(f"获取标签时出错: {e}") + + # 递归序列化子节点(跳过辅助节点) + try: + if hasattr(node, 'getChildren'): + for child in node.getChildren(): + # 跳过辅助节点 + child_name = child.getName() if hasattr(child, 'getName') else "" + if not child_name.startswith(('gizmo', 'selectionBox', 'grid')): + child_data = self._serializeNode(child) + if child_data: + node_data['children'].append(child_data) + except Exception as e: + print(f"序列化子节点时出错: {e}") + + return node_data + + except Exception as e: + print(f"序列化节点失败: {e}") + return None + + def _deserializeNode(self, node_data, parent_node): + """反序列化节点数据""" + try: + if not node_data or not parent_node or parent_node.isEmpty(): + return None + + # 创建新节点 + node_name = node_data.get('name', 'node') + new_node = parent_node.attachNewNode(node_name) + + # 设置变换 + try: + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + except Exception as e: + print(f"设置变换时出错: {e}") + + # 恢复标签 + try: + for tag_key, tag_value in node_data.get('tags', {}).items(): + new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串 + except Exception as e: + print(f"恢复标签时出错: {e}") + + # 递归创建子节点 + try: + for child_data in node_data.get('children', []): + self._deserializeNode(child_data, new_node) + except Exception as e: + print(f"创建子节点时出错: {e}") + + return new_node + + except Exception as e: + print(f"反序列化节点失败: {e}") + return None + + def _deleteNode(self, node, tree_item): + """删除节点""" + try: + if not node or node.isEmpty(): + return + + # 特殊处理选中节点 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node: + self.world.selection.clearSelection() + + # 从场景中删除节点 + node.removeNode() + + # 从树形控件中删除项目 + if tree_item: + try: + parent = tree_item.parent() + if parent: + parent.removeChild(tree_item) + else: + index = self.treeWidget.indexOfTopLevelItem(tree_item) + if index >= 0: + self.treeWidget.takeTopLevelItem(index) + except Exception as e: + print(f"从树形控件删除项目时出错: {e}") + + except Exception as e: + print(f"删除节点失败: {e}") + + # 添加撤销/重做功能的基础实现 + def onUndo(self): + """撤销操作""" + QMessageBox.information(self, "提示", "撤销功能将在后续版本中实现") + + def onRedo(self): + """重做操作""" + QMessageBox.information(self, "提示", "重做功能将在后续版本中实现") + def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: