diff --git a/.idea/misc.xml b/.idea/misc.xml index a0ea9286..51b9fc17 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/QPanda3D/QPanda3DWidget.py b/QPanda3D/QPanda3DWidget.py index 25a6158c..61b531b1 100644 --- a/QPanda3D/QPanda3DWidget.py +++ b/QPanda3D/QPanda3DWidget.py @@ -32,9 +32,24 @@ class QPanda3DSynchronizer(QTimer): self.setInterval(int(dt)) self.timeout.connect(self.tick) + # def tick(self): + # taskMgr.step() + # self.qPanda3DWidget.update() + def tick(self): - taskMgr.step() - self.qPanda3DWidget.update() + try: + taskMgr.step() + self.qPanda3DWidget.update() + except AssertionError as e: + if "has_mat()" in str(e): + print("⚠️ 检测到变换矩阵错误,跳过此帧") + # 继续运行而不是崩溃 + else: + raise + except Exception as e: + print(f"❌ 渲染循环错误: {e}") + import traceback + traceback.print_exc() def get_panda_key_modifiers(evt): @@ -160,8 +175,8 @@ class QPanda3DWidget(QWidget): def resizeEvent(self, evt): width = evt.size().width() height = evt.size().height() - print(f"width:{width}") - print(f"height:{height}") + #print(f"width:{width}") + #print(f"height:{height}") from Panda3DWorld import resize_buffer #resize_buffer(width, height) diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 731e5beb..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.0000000000]]] - sun_altitude: [[[0.5000000000,0.9333333333]]] + 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/RenderPipelineFile/rpcore/light_manager.py b/RenderPipelineFile/rpcore/light_manager.py index b24ec006..fe9e4ceb 100644 --- a/RenderPipelineFile/rpcore/light_manager.py +++ b/RenderPipelineFile/rpcore/light_manager.py @@ -90,6 +90,10 @@ class LightManager(RPObject): def remove_light(self, light): """ Removes a light """ + print(f'333333333333333333333333333333,{light.casts_shadows}') + # from RenderPipelineFile.rpcore.pynative.internal_light_manager import InternalLightManager + # inter = InternalLightManager() + # inter.remove_light(light) self.internal_mgr.remove_light(light) self.pta_max_light_index[0] = self.internal_mgr.max_light_index diff --git a/RenderPipelineFile/rpcore/native/__init__.py b/RenderPipelineFile/rpcore/native/__init__.py index 3ba9d575..a2f219a3 100644 --- a/RenderPipelineFile/rpcore/native/__init__.py +++ b/RenderPipelineFile/rpcore/native/__init__.py @@ -80,6 +80,7 @@ if NATIVE_CXX_LOADED: RPObject.global_debug("CORE", "Using native core module") from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position else: + print(f'343434343434343434343434343') from rpcore import pynative as _native_module # pylint: disable=wrong-import-position RPObject.global_debug("CORE", "Using simulated python-wrapper module") diff --git a/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h b/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h index 979732ec..4c3e6964 100644 --- a/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h +++ b/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h @@ -161,7 +161,12 @@ public: // Update maximum index if (slot == _max_index) { - while (_max_index >= 0 && !_data[_max_index--]); + while (_max_index >= 0 && !_data[_max_index--]); + // 正确的修复代码 +// while (_max_index >= 0 && _data[_max_index] == NULL) { +// _max_index--; +// } + } } } diff --git a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py index 911ee147..eb4f5810 100644 --- a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py +++ b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py @@ -116,7 +116,25 @@ class InternalLightManager(object): source.set_slot(slot) def remove_light(self, light): - print("111111111111111111111111111111111111111111111111") + print(f'44444444444444444444444444') + print("\n" + "=" * 50) + print(f"DEBUG: Entering remove_light for light object: {light.casts_shadows}") + if light: + print(f" - Light's Slot: {light.get_slot() if light.has_slot() else 'No Slot'}") + print(f" - Does it cast shadows? light.get_casts_shadows() -> {light.get_casts_shadows()}") + else: + print(" - Light object is None!") + + print("\n --- State of Light System BEFORE removal ---") + if hasattr(self, '_lights') and hasattr(self._lights, '_data'): + # 打印出当前所有灯光对象,看看有没有异常 + print(f" - Light Data Array (_data):") + for i, l_obj in enumerate(self._lights._data): + print(f" [{i}]: {l_obj}") + + print(f"\n - Max Index (_max_index): {self._lights._max_index}") + print(f" - Num Entries (_num_entries): {self._lights._num_entries}") + print("=" * 50 + "\n") assert light is not None if not light.has_slot(): print("ERROR: Could not detach light, light was not attached!") @@ -133,7 +151,10 @@ class InternalLightManager(object): # 关键修复:先保存第一个source的slot,再清理 first_source = light.get_shadow_source(0) first_source_slot = first_source.get_slot() # 保存slot值 - + + # --- 在这里加上打印语句 --- + print(f"DEBUG: Removing shadow sources. Start Slot: {first_source_slot}, Count: {num_sources}") + # 先发送GPU移除命令(在清理之前) cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources) cmd_remove.push_int(first_source_slot) # 使用保存的slot值 diff --git a/RenderPipelineFile/rpcore/render_pipeline.py b/RenderPipelineFile/rpcore/render_pipeline.py index 0424783f..a7ac29df 100644 --- a/RenderPipelineFile/rpcore/render_pipeline.py +++ b/RenderPipelineFile/rpcore/render_pipeline.py @@ -208,6 +208,7 @@ class RenderPipeline(RPObject): def remove_light(self, light): """ Removes a previously attached light, check out the LightManager remove_light documentation for further information. """ + print(f'222222222222222222222222222,{light.casts_shadows}') self.light_mgr.remove_light(light) def load_ies_profile(self, filename): 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/Resources/models/Women_1.glb b/Resources/models/Women_1.glb new file mode 100644 index 00000000..79ebe8d2 Binary files /dev/null and b/Resources/models/Women_1.glb differ diff --git a/Resources/models/Women_2.glb b/Resources/models/Women_2.glb new file mode 100644 index 00000000..efba7b02 Binary files /dev/null and b/Resources/models/Women_2.glb differ diff --git a/Resources/models/women_1.glb b/Resources/models/women_1.glb new file mode 100644 index 00000000..f634ea92 Binary files /dev/null and b/Resources/models/women_1.glb differ diff --git a/Start_Run.py b/Start_Run.py new file mode 100644 index 00000000..8e6a1214 --- /dev/null +++ b/Start_Run.py @@ -0,0 +1,34 @@ +import os +import sys + +# 添加项目根目录到 Python 路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# 添加 RenderPipeline 到路径(注意路径名称应与实际目录一致) +render_pipeline_path = os.path.join(project_root, "RenderPipeline") +sys.path.insert(0, render_pipeline_path) + +# 添加 RenderPipelineFile 路径 +render_pipeline_file_path = os.path.join(project_root, "RenderPipelineFile") +sys.path.insert(0, render_pipeline_file_path) + +# 添加 icons 目录到路径 +icons_path = os.path.join(project_root, "icons") +sys.path.insert(0, icons_path) + +# 现在可以导入并运行主程序 +if __name__ == "__main__": + args = sys.argv[1:] + # args = "/home/tiger/桌面/Test1" + # args = "C:/Users/29381/Desktop/1" + print(f'Path is {args}') + # 将整个列表转换为字符串(包括方括号) + args_str = ''.join(args) + + from main import run + if args: + run(args_str) + # run(args) + else: + run() \ No newline at end of file diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py index dbe6dede..35000ffe 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -1,6 +1,7 @@ # 修改后的 InfoPanelManager.py from xml.sax.handler import property_encoding +from PyQt5.QtCore import Qt from direct.gui.DirectGui import DirectFrame, DirectLabel from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import TextNode, Vec4, NodePath @@ -144,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, @@ -182,12 +183,60 @@ class InfoPanelManager(DirectObject): panel_node.setTag("gui_type", "info_panel") panel_node.setTag("panel_id", panel_id) panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + panel_node.setTag("is_gui_element",'1') + panel_node.setTag("tree_item_type","INFO_PANEL") + panel_node.setTag("supports_3d_position_editing","1") + + # 如果有背景图片,保存背景图片路径 + if bg_image: + panel_node.setTag("bg_image_path", bg_image) + if not visible: panel_node.hide() + # 将面板添加到场景树 + #self._addPanelToSceneTree(panel_node, panel_id) + return panel_node + def _addPanelToSceneTree(self, panel_node, panel_id): + """ + 将信息面板添加到场景树中 + """ + try: + # 获取树形控件 + if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + # 查找根节点项 + root_item = None + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: + root_item = item + break + + if root_item: + # 使用现有的 add_node_to_tree_widget 方法添加节点 + qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL") + if qt_item: + print(f"✅ 信息面板 {panel_id} 已添加到场景树") + # 选中创建的节点 + tree_widget.setCurrentItem(qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(panel_node, qt_item) + else: + print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败") + else: + print("⚠️ 未找到场景树根节点,无法添加信息面板") + else: + print("⚠️ 无法访问场景树控件,信息面板未添加到场景树") + except Exception as e: + print(f"❌ 添加信息面板到场景树时出错: {e}") + import traceback + traceback.print_exc() + def setPanelBackgroundImage(self, panel_id, image_path): """ 为指定面板设置背景图片 @@ -317,11 +366,10 @@ class InfoPanelManager(DirectObject): return True + # 更新 registerDataSource 方法以更好地处理不同类型面板 def registerDataSource(self, panel_id, data_callback, update_interval=1.0): """ - 注册数据源,定期更新面板内容 - data_callback: 返回数据的回调函数 - update_interval: 更新间隔(秒) + 注册数据源,定期更新面板内容 - 改进版 """ if panel_id not in self.panels: print(f"面板 {panel_id} 不存在") @@ -335,7 +383,8 @@ class InfoPanelManager(DirectObject): data_source = { 'callback': data_callback, 'interval': update_interval, - 'stop': False + 'stop': False, + 'panel_type': '3d' if self._is3DPanel(panel_id) else '2d' if self._is2DPanel(panel_id) else 'unknown' } self.data_sources[panel_id] = data_source @@ -346,9 +395,10 @@ class InfoPanelManager(DirectObject): return True + # 在 InfoPanelManager 类中修复 _updateDataThread 方法 def _updateDataThread(self, panel_id): """ - 数据更新线程 + 数据更新线程 - 最终修复版 """ while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']: try: @@ -357,7 +407,18 @@ class InfoPanelManager(DirectObject): # 更新面板内容 if data and panel_id in self.panels: - self.updatePanelContent(panel_id, content=data) + panel_type = self.data_sources[panel_id].get('panel_type', 'unknown') + + if panel_type == '2d': + self.updatePanelContent(panel_id, content=data) + elif panel_type == '3d': + self.update3DPanelContent(panel_id, content=data) + else: + # 尝试自动检测 + if self._is2DPanel(panel_id): + self.updatePanelContent(panel_id, content=data) + elif self._is3DPanel(panel_id): + self.update3DPanelContent(panel_id, content=data) # 等待下次更新 interval = self.data_sources[panel_id]['interval'] @@ -365,8 +426,29 @@ class InfoPanelManager(DirectObject): except Exception as e: print(f"更新面板 {panel_id} 数据时出错: {e}") + import traceback + traceback.print_exc() time.sleep(1.0) # 出错时等待1秒再重试 + # 在 InfoPanelManager 类中添加以下方法 + def _is3DPanel(self, panel_id): + """ + 判断面板是否为3D面板 + """ + if panel_id not in self.panels: + return False + panel_data = self.panels[panel_id] + return 'content_node' in panel_data and 'content_label' not in panel_data + + def _is2DPanel(self, panel_id): + """ + 判断面板是否为2D面板 + """ + if panel_id not in self.panels: + return False + panel_data = self.panels[panel_id] + return 'content_label' in panel_data and 'content_node' not in panel_data + def unregisterDataSource(self, panel_id): """ 注销数据源 @@ -447,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']: @@ -493,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: @@ -657,10 +738,247 @@ class InfoPanelManager(DirectObject): return True + def create3DInfoPanel(self, panel_id, position=(0, 0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0), + visible=True, font=None, bg_image=None): + """ + 创建简化版3D信息面板 - 只显示文字,无面板背景,避免闪烁 + """ + # 如果面板已存在,先移除它 + if panel_id in self.panels: + self.removePanel(panel_id) + + # 确保父节点存在 + parent_node = self.parent if self.parent else self.world.render + + # 根据面板ID确定标题和内容 + title, content = self._getPanelContent(panel_id) + + # 创建主节点,便于统一管理 + panel_node = parent_node.attachNewNode(f"info_panel_3d_{panel_id}") + panel_node.setPos(position[0], position[1], position[2]) + + # 直接创建文字节点,不创建面板背景和边框 + from panda3d.core import TextNode + + # 创建标题文本 + title_text_node = TextNode(f'title_{panel_id}') + title_text_node.setText(title) + title_text_node.setTextColor(*title_color) + title_text_node.setAlign(TextNode.ACenter) + if font: + title_text_node.setFont(font) + + title_text = panel_node.attachNewNode(title_text_node) + title_text.setScale(0.06) + title_text.setPos(0, 0, size[1] / 4) # 将标题放在上方 + + # 创建内容文本 + content_text_node = TextNode(f'content_{panel_id}') + content_text_node.setText(content) + content_text_node.setTextColor(*content_color) + content_text_node.setAlign(TextNode.ALeft) + content_text_node.setWordwrap(size[0] * 2) # 根据面板宽度设置换行 + if font: + content_text_node.setFont(font) + + content_text = panel_node.attachNewNode(content_text_node) + content_text.setScale(0.045) + content_text.setPos(-size[0] / 2, 0, size[1] / 4 - 0.1) # 将内容放在标题下方 + + # 保存引用 + self.panels[panel_id] = { + 'node': panel_node, + 'title_text': title_text, + 'content_text': content_text, + 'title_node': title_text_node, + 'content_node': content_text_node, + 'properties': { + 'size': size, + 'position': position, + 'title_color': title_color, + 'content_color': content_color, + 'font': font + } + } + + # 设置GUI类型标记和支持3D编辑的标记 + panel_node.setTag("gui_type", "info_panel_3d") + panel_node.setTag("panel_id", panel_id) + panel_node.setTag("is_gui_element", "1") # 添加此标记确保节点被识别为GUI元素 + panel_node.setTag("is_scene_element", "1") # 添加此标记确保节点被识别为场景元素 + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + panel_node.setTag("tree_item_type", "INFO_PANEL_3D") # 添加树节点类型标记 + + # 如果有背景图片,保存背景图片路径 + if bg_image: + panel_node.setTag("bg_image_path", bg_image) + + if not visible: + panel_node.hide() + + # 将面板添加到场景树 + #self._addPanelToSceneTree(panel_node, panel_id) + + return panel_node + + def update3DPanelContent(self, panel_id, title=None, content=None): + """ + 更新3D面板内容 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + + if title is not None and 'title_node' in panel_data: + panel_data['title_node'].setText(title) + + if content is not None and 'content_node' in panel_data: + panel_data['content_node'].setText(content) + + return True + + def update3DPanelProperties(self, panel_id, **properties): + """ + 更新3D面板属性 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + props = panel_data['properties'] + + # 更新位置 + if 'position' in properties: + pos = properties['position'] + panel_data['node'].setPos(pos[0], pos[1], pos[2]) + props['position'] = pos + + # 更新大小 + if 'size' in properties: + size = properties['size'] + props['size'] = size + + # 由于3D面板使用CardMaker创建,需要重新创建几何体 + print("注意:3D面板大小调整需要重新创建面板几何体") + + # 更新背景颜色 + if 'bg_color' in properties: + bg_color = properties['bg_color'] + if 'panel_bg' in panel_data: + from panda3d.core import Material + material = Material() + material.setDiffuse(Vec4(*bg_color)) + material.setAmbient(Vec4(*bg_color[:3], 1.0)) + panel_data['panel_bg'].setMaterial(material, 1) + props['bg_color'] = bg_color + + # 更新边框颜色 + if 'border_color' in properties: + border_color = properties['border_color'] + from panda3d.core import Material + border_mat = Material() + border_mat.setDiffuse(Vec4(*border_color)) + for border in panel_data['borders'].values(): + border.setMaterial(border_mat, 1) + props['border_color'] = border_color + + # 更新标题颜色 + if 'title_color' in properties: + title_color = properties['title_color'] + if 'title_node' in panel_data: + panel_data['title_node'].setTextColor(*title_color) + props['title_color'] = title_color + + # 更新内容颜色 + if 'content_color' in properties: + content_color = properties['content_color'] + if 'content_node' in panel_data: + panel_data['content_node'].setTextColor(*content_color) + props['content_color'] = content_color + + # 更新标题 + if 'title' in properties: + if 'title_node' in panel_data: + panel_data['title_node'].setText(properties['title']) + + # 更新内容 + if 'content' in properties: + if 'content_node' in panel_data: + panel_data['content_node'].setText(properties['content']) + + # 更新字体大小 + if 'title_size' in properties: + if 'title_text' in panel_data: + panel_data['title_text'].setScale(properties['title_size']) + props['title_size'] = properties['title_size'] + + if 'content_size' in properties: + if 'content_text' in panel_data: + panel_data['content_text'].setScale(properties['content_size']) + props['content_size'] = properties['content_size'] + + return True + + def create3DHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, + position=(0, 0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), + border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), + content_color=(0.9, 0.9, 0.9, 1.0), + update_interval=30.0, font=None): + """ + 创建3D HTTP信息面板 + """ + # 创建面板 + domain = urlparse(url).netloc or url[:30] + title = f"HTTP数据: {domain}" + + panel_node = self.create3DInfoPanel( + panel_id=panel_id, + position=position, + size=size, + bg_color=bg_color, + border_color=border_color, + title_color=title_color, + content_color=content_color, + font=font + ) + + # 更新标题 + self.update3DPanelContent(panel_id, title=title) + + # 立即获取并显示数据 + content = fetchHTTPData(url, method, headers, data) + self.update3DPanelContent(panel_id, content=content) + + # 注册数据源,定期更新 + def http_data_callback(): + return fetchHTTPData(url, method, headers, data) + + self.registerDataSource(panel_id, http_data_callback, update_interval) + + # 保存HTTP请求信息,便于后续更新 + if panel_id not in self.data_sources: + self.data_sources[panel_id] = {} + self.data_sources[panel_id]['http_info'] = { + 'url': url, + 'method': method, + 'headers': headers, + 'data': data + } + + return panel_node + # 在 add_methods_to_property_panel 函数中添加以下方法 def add_methods_to_property_panel(property_panel_instance): # ... (原有代码保持不变) + import types def createHTTPInfoPanel(self, url, panel_id="http_info", method="GET", headers=None, data=None, position=(0.8, 0.0), size=(0.4, 0.3), update_interval=30.0): @@ -721,12 +1039,233 @@ class InfoPanelManager(DirectObject): print(f"✗ 更新HTTP信息面板失败: {e}") return False + def create3DRealtimeDataPanel(self, data_callback=None, update_interval=1.0): + """创建3D实时数据面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 创建3D实时数据面板 + panel_node = self.info_panel_manager.create3DInfoPanel( + panel_id="realtime_data_3d", + position=(0, 0, 0), + size=(0.35, 0.3), + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 设置标签 + panel_node.setTag("element_type", "info_panel_3d") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + # 如果提供了数据回调函数,则注册数据源 + if data_callback: + # 立即显示初始数据 + initial_data = data_callback() + self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=initial_data) + + # 注册数据源 + self.info_panel_manager.registerDataSource("realtime_data_3d", data_callback, update_interval) + else: + # 使用默认数据 + default_data = "等待数据..." + self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=default_data) + + print("✓ 已创建3D实时数据面板") + + return panel_node + + except Exception as e: + print(f"✗ 创建3D实时数据面板失败: {e}") + return None + + def create3DModelInfoPanel(self, model): + """为模型创建3D信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取模型信息 + model_name = model.getName() if hasattr(model, 'getName') else 'Unknown' + num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0 + + # 创建面板内容 + content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.create3DInfoPanel( + panel_id="model_info_3d", + position=(2, 0, 0), # 默认放在模型旁边 + size=(0.35, 0.25), + bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景 + border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框 + title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为模型特定信息 + self.info_panel_manager.update3DPanelContent("model_info_3d", + title="模型信息", + content=content) + + # 设置标签 + panel_node.setTag("element_type", "info_panel_3d") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建3D模型信息面板: {model_name}") + + return panel_node + + except Exception as e: + print(f"✗ 创建3D模型信息面板失败: {e}") + return None + + # 在绑定方法的部分添加3D面板方法 + property_panel_instance.create3DRealtimeDataPanel = types.MethodType(create3DRealtimeDataPanel, + property_panel_instance) + property_panel_instance.create3DModelInfoPanel = types.MethodType(create3DModelInfoPanel, + property_panel_instance) + # 将新方法绑定到实例 - import types # ... (原有绑定保持不变) property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance) property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance) + def serializePanelData(self, panel_id): + """序列化面板数据用于保存""" + if panel_id not in self.panels: + return None + + panel_data = self.panels[panel_id] + props = panel_data['properties'] + + # 获取面板类型 + panel_type = "2d" + if panel_data['node'].hasTag("gui_type"): + gui_type = panel_data['node'].getTag("gui_type") + if "3d" in gui_type.lower(): + panel_type = "3d" + + # 构建序列化数据 + serialized_data = { + 'panel_id': panel_id, + 'panel_type': panel_type, + 'position': props.get('position', (0, 0) if panel_type == "2d" else (0, 0, 0)), + 'size': props.get('size', (1.0, 0.6)), + 'bg_color': props.get('bg_color', (0.15, 0.15, 0.15, 0.9)), + 'border_color': props.get('border_color', (0.3, 0.3, 0.3, 1.0)), + 'title_color': props.get('title_color', (1.0, 1.0, 1.0, 1.0)), + 'content_color': props.get('content_color', (0.9, 0.9, 0.9, 1.0)), + 'title': panel_data['title_label'].getText() if 'title_label' in panel_data else "信息面板", + 'content': panel_data[ + 'content_label'].getText() if 'content_label' in panel_data else "" if 'content_node' not in panel_data else + panel_data['content_node'].getText(), + 'font_path': props.get('font', None), + 'bg_image': props.get('bg_image', None), + 'visible': not panel_data['node'].isHidden() + } + + # 添加HTTP面板特有数据 + if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]: + serialized_data['http_info'] = self.data_sources[panel_id]['http_info'] + + return serialized_data + + def getAllPanelData(self): + """获取所有面板的序列化数据""" + panel_data_list = [] + for panel_id in self.panels: + data = self.serializePanelData(panel_id) + if data: + panel_data_list.append(data) + return panel_data_list + + def recreatePanelFromData(self, panel_data): + """从序列化数据重新创建面板""" + try: + panel_id = panel_data['panel_id'] + panel_type = panel_data['panel_type'] + position = panel_data['position'] + size = panel_data['size'] + + # 重建面板 + if panel_type == "3d": + panel_node = self.create3DInfoPanel( + panel_id=panel_id, + position=position, + size=size, + bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), + border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), + title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), + content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), + visible=panel_data.get('visible', True), + font=panel_data.get('font_path', None), + bg_image=panel_data.get('bg_image', None) + ) + + # 更新内容 + self.update3DPanelContent( + panel_id, + title=panel_data.get('title', '信息面板'), + content=panel_data.get('content', '') + ) + else: + panel_node = self.createInfoPanel( + panel_id=panel_id, + position=(position[0], position[1]) if len(position) >= 2 else (0, 0), + size=size, + bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), + border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), + title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), + content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), + visible=panel_data.get('visible', True), + font=panel_data.get('font_path', None), + bg_image=panel_data.get('bg_image', None) + ) + + # 更新内容 + self.updatePanelContent( + panel_id, + title=panel_data.get('title', '信息面板'), + content=panel_data.get('content', '') + ) + + # 设置标签 + if panel_node: + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") + + # 如果是HTTP面板,重新注册数据源 + if 'http_info' in panel_data: + http_info = panel_data['http_info'] + self.createHTTPInfoPanel( + panel_id=panel_id, + url=http_info['url'], + method=http_info.get('method', 'GET'), + headers=http_info.get('headers', None), + data=http_info.get('data', None), + position=position, + size=size, + update_interval=self.data_sources.get(panel_id, {}).get('interval', + 30.0) if panel_id in self.data_sources else 30.0 + ) + + print(f"✓ 信息面板 {panel_id} 已重建") + return panel_node + + except Exception as e: + print(f"✗ 重建信息面板 {panel_data.get('panel_id', 'unknown')} 失败: {e}") + import traceback + traceback.print_exc() + return None + # 示例数据源函数 def getRealtimeData(): @@ -777,6 +1316,7 @@ def add_methods_to_property_panel(property_panel_instance): """ 为 property_panel 实例添加 DirectGUI 信息面板支持 """ + import types # 添加信息面板管理器作为类属性 if not hasattr(property_panel_instance, 'info_panel_manager'): @@ -982,7 +1522,6 @@ def add_methods_to_property_panel(property_panel_instance): property_panel_instance.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance) property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance) - def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5): """ 获取HTTP数据的通用函数 diff --git a/core/collision_manager.py b/core/collision_manager.py index 041571bf..6c4b3e73 100644 --- a/core/collision_manager.py +++ b/core/collision_manager.py @@ -309,40 +309,81 @@ class CollisionManager: CollisionPlane, CollisionPolygon, Plane, Vec3 ) - bounds = model.getBounds() - if bounds.isEmpty(): + # 获取考虑变换后的实际尺寸和中心 + transformed_info = self._getTransformedModelInfo(model) + if not transformed_info: # 默认小球体 return CollisionSphere(Point3(0, 0, 0), 1.0) - center = bounds.getCenter() - radius = bounds.getRadius() + center = transformed_info['center'] + radius = transformed_info['radius'] + actual_size = transformed_info['size'] + scale_factor = transformed_info['scale_factor'] # 自动选择最适合的形状 if shape_type == 'auto': - shape_type = self._determineOptimalShape(model, bounds) + shape_type = self._determineOptimalShape(model, transformed_info) if shape_type == 'sphere': - return CollisionSphere(center, kwargs.get('radius', radius)) + # 优化球形碰撞体 + sphere_radius = kwargs.get('radius', radius) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + sphere_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) + + return CollisionSphere(sphere_center, sphere_radius) elif shape_type == 'box': - # 创建包围盒 - min_point = bounds.getMin() - max_point = bounds.getMax() + # 优化盒型碰撞体 - 更精确的尺寸和位置控制(考虑缩放) + # 获取自定义尺寸,如果没有提供则使用变换后的实际尺寸 + width = kwargs.get('width', actual_size.x) + length = kwargs.get('length', actual_size.y) + height = kwargs.get('height', actual_size.z) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + box_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) + + # 计算盒子的最小和最大点(基于偏移后的中心) + half_width = width / 2 + half_length = length / 2 + half_height = height / 2 + + min_point = Point3(box_center.x - half_width, box_center.y - half_length, box_center.z - half_height) + max_point = Point3(box_center.x + half_width, box_center.y + half_length, box_center.z + half_height) return CollisionBox(min_point, max_point) elif shape_type == 'capsule': - # 创建胶囊体(适合角色) - height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z)) - radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3)) - point_a = Point3(center.x, center.y, bounds.getMin().z + radius) - point_b = Point3(center.x, center.y, bounds.getMax().z - radius) - return CollisionCapsule(point_a, point_b, radius) + # 优化胶囊体碰撞 - 更合理的比例和位置控制(考虑缩放) + # 使用变换后的实际高度,或自定义高度 + custom_height = kwargs.get('height', actual_size.z) + + # 更合理的半径计算:基于变换后模型宽度的平均值 + default_radius = min(actual_size.x, actual_size.y) / 2.5 # 稍微小于模型的宽度 + custom_radius = kwargs.get('radius', min(default_radius, custom_height * 0.4)) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + capsule_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) + + # 计算胶囊体的两个端点(确保半径不会超出高度) + effective_height = max(custom_height, custom_radius * 2.1) # 确保高度至少是半径的2倍多一点 + point_a = Point3(capsule_center.x, capsule_center.y, capsule_center.z - effective_height/2 + custom_radius) + point_b = Point3(capsule_center.x, capsule_center.y, capsule_center.z + effective_height/2 - custom_radius) + return CollisionCapsule(point_a, point_b, custom_radius) elif shape_type == 'plane': - # 创建平面(适合地面、墙面) + # 优化平面碰撞 - 支持位置偏移和更灵活的法向量 normal = kwargs.get('normal', Vec3(0, 0, 1)) - point = kwargs.get('point', center) - plane = Plane(normal, point) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + plane_point = kwargs.get('point', Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)) + + # 标准化法向量 + normal.normalize() + plane = Plane(normal, plane_point) return CollisionPlane(plane) elif shape_type == 'polygon': @@ -364,13 +405,13 @@ class CollisionManager: collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices]) return collision_poly else: - print("⚠️ 多边形至少需要3个顶点,回退到球体") + #print("⚠️ 多边形至少需要3个顶点,回退到球体") return CollisionSphere(center, radius) - def _determineOptimalShape(self, model, bounds): + def _determineOptimalShape(self, model, transformed_info): """根据模型特征自动确定最适合的碰撞体形状""" - # 获取模型尺寸比例 - size = bounds.getMax() - bounds.getMin() + # 获取变换后的模型尺寸比例 + size = transformed_info['size'] max_dim = max(size.x, size.y, size.z) min_dim = min(size.x, size.y, size.z) @@ -399,6 +440,87 @@ class CollisionManager: else: # 其他用包围盒 return 'box' + def _getTransformedModelInfo(self, model): + """获取考虑变换后的模型信息 + + Args: + model: 模型节点 + + Returns: + dict: 包含变换后的尺寸、中心、半径等信息 + """ + try: + # 获取原始包围盒 + bounds = model.getBounds() + if bounds.isEmpty(): + return None + + # 获取模型的变换矩阵 + transform = model.getTransform() + scale = model.getScale() + + # 计算缩放因子(取三轴缩放的平均值) + scale_factor = (abs(scale.x) + abs(scale.y) + abs(scale.z)) / 3.0 + + # 获取原始尺寸 + original_size = bounds.getMax() - bounds.getMin() + + # 应用缩放到尺寸 + actual_size = Vec3( + original_size.x * abs(scale.x), + original_size.y * abs(scale.y), + original_size.z * abs(scale.z) + ) + + # 获取变换后的中心点(在世界坐标系中) + original_center = bounds.getCenter() + if hasattr(model, 'getPos'): + # 模型在世界坐标系中的位置 + world_center = model.getPos(model.getParent() if model.getParent() else model) + center = Point3(world_center.x, world_center.y, world_center.z) + else: + # 如果无法获取世界位置,使用原始中心 + center = original_center + + # 计算变换后的半径(考虑缩放) + original_radius = bounds.getRadius() + transformed_radius = original_radius * scale_factor + + # 调试信息 + print(f"🔍 模型 {model.getName()} 变换信息:") + print(f" 原始尺寸: {original_size}") + print(f" 缩放因子: {scale}") + print(f" 变换后尺寸: {actual_size}") + print(f" 变换后半径: {transformed_radius:.2f}") + + return { + 'center': center, + 'radius': transformed_radius, + 'size': actual_size, + 'scale_factor': scale_factor, + 'original_size': original_size, + 'scale': scale, + 'transform': transform + } + + except Exception as e: + print(f"⚠️ 获取模型变换信息失败: {e}") + # 回退到原始包围盒 + bounds = model.getBounds() + if bounds.isEmpty(): + return None + + original_size = bounds.getMax() - bounds.getMin() + return { + 'center': bounds.getCenter(), + 'radius': bounds.getRadius(), + 'size': original_size, + 'scale_factor': 1.0, + 'original_size': original_size, + 'scale': Vec3(1, 1, 1), + 'transform': None + } + def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']): """创建鼠标射线""" # 组合掩码 diff --git a/core/event_handler.py b/core/event_handler.py index ff7b17c2..be0f6cc1 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -167,7 +167,7 @@ class EventHandler: picker.addCollider(pickerNP, queue) picker.traverse(self.world.render) - print(f"碰撞检测结果数量: {queue.getNumEntries()}") + #print(f"碰撞检测结果数量: {queue.getNumEntries()}") # 射线检测结果处理 hitPos = None @@ -184,13 +184,13 @@ class EventHandler: self.showClickRay(worldNearPoint, worldFarPoint, hitPos) # 优先检查是否点击了坐标轴 - print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") + #print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") if self.world.selection.gizmo: - print("准备检查坐标轴点击...") + #print("准备检查坐标轴点击...") try: gizmoAxis = self.world.selection.checkGizmoClick(x, y) if gizmoAxis: - print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") # 开始坐标轴拖拽 self.world.selection.startGizmoDrag(gizmoAxis, x, y) pickerNP.removeNode() @@ -203,16 +203,16 @@ class EventHandler: traceback.print_exc() print("继续处理模型选择...") - print("继续处理碰撞结果...") + #print("继续处理碰撞结果...") if hitPos and hitNode: - print(f"✓ 检测到碰撞,开始处理点击事件") - print(f"GUI编辑模式: {self.world.guiEditMode}") - print(f"当前工具: {self.world.currentTool}") + #print(f"✓ 检测到碰撞,开始处理点击事件") + #print(f"GUI编辑模式: {self.world.guiEditMode}") + #print(f"当前工具: {self.world.currentTool}") # 处理GUI编辑模式 if self.world.guiEditMode: - print("处理GUI编辑模式点击") + #print("处理GUI编辑模式点击") # 检查是否点击了GUI元素 clickedGUI = self.world.gui_manager.findClickedGUI(hitNode) if clickedGUI: @@ -411,103 +411,6 @@ class EventHandler: import traceback traceback.print_exc() - - def mousePressEventRight(self,evt): - """处理鼠标右键按下事件""" - print(f"当前工具: {self.world.currentTool}") - - # 检查是否是地形编辑模式 - if self.world.currentTool == "地形编辑": - self._handleTerrainEdit(evt, "subtract") # 降低地形 - return - - # 其他右键处理逻辑可以在这里添加 - print("鼠标右键事件处理") - - def _handleTerrainEdit(self,evt,operation): - try: - x = evt.get('x',0) - y = evt.get('y',0) - - winWidth,winHeight = self.world.getWindowSize() - - mx = 2.0 * x/float(winWidth) - 1.0 - my = 1.0 -2.0*y/float(winHeight) - - nearPoint = Point3() - farPoint = Point3() - self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - - worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) - worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) - - picker = CollisionTraverser() - queue = CollisionHandlerQueue() - - pickerNode = CollisionNode('terrain_edit_ray') - pickerNP = self.world.cam.attachNewNode(pickerNode) - - from panda3d.core import BitMask32 - pickerNode.setFromCollideMask(BitMask32.allOn()) # 检查所有碰撞 - - # 使用相机坐标系的点创建射线 - direction = farPoint - nearPoint - direction.normalize() - pickerNode.addSolid(CollisionRay(nearPoint, direction)) - - picker.addCollider(pickerNP, queue) - picker.traverse(self.world.render) - print(f"地形碰撞检测结果数量: {queue.getNumEntries()}") - - # 射线检测结果处理 - hitPos = None - hitNode = None - - if queue.getNumEntries() > 0: - # 遍历所有碰撞结果,找到地形节点 - for i in range(queue.getNumEntries()): - entry = queue.getEntry(i) - collided_node = entry.getIntoNodePath() - print(f"碰撞到节点: {collided_node.getName()}") - - # 检查是否是地形节点 - for terrain_info in self.world.terrain_manager.terrains: - terrain_node = terrain_info['node'] - if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node): - hitPos = entry.getSurfacePoint(self.world.render) - hitNode = collided_node - print(f"找到地形节点: {terrain_node.getName()}") - - # 修改地形高度 - x_pos, y_pos = hitPos.getX(), hitPos.getY() - success = self.world.modifyTerrainHeight( - terrain_info, x_pos, y_pos, radius=3.0, strength=0.3, operation=operation) - - if success: - print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})") - # 显示射线 - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - else: - print("✗ 地形编辑失败") - break - - if hitPos: - break - - if not hitPos: - print("没有检测到地形碰撞") - # 显示射线(无碰撞) - self.showClickRay(worldNearPoint, worldFarPoint) - - # 清理碰撞检测节点 - pickerNP.removeNode() - print("地形编辑处理完成") - - except Exception as e: - print(f"地形编辑处理出错: {e}") - import traceback - traceback.print_exc() - def _handleSelectionClick(self, hitNode): """处理选择工具的点击事件""" print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") @@ -541,43 +444,52 @@ class EventHandler: selectedModel = model print(f"找到父模型: {selectedModel.getName()}") break - + if selectedModel: break current = current.getParent() if selectedModel: - print(f"✓ 最终选中模型: {selectedModel.getName()}") + #print(f"✓ 最终选中模型: {selectedModel.getName()}") + self.world.selection.handleMouseClick(selectedModel) # 更新选择状态并显示选择框和坐标轴 self.world.selection.updateSelection(selectedModel) # 在树形控件中查找并选中对应的项 if self.world.interface_manager.treeWidget: - print("查找树形控件中的对应项...") + #print("查找树形控件中的对应项...") root = self.world.interface_manager.treeWidget.invisibleRootItem() foundItem = None - + for i in range(root.childCount()): sceneItem = root.child(i) if sceneItem.text(0) == "场景": - print(f"在场景节点下查找...") + #print(f"在场景节点下查找...") foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem) if foundItem: print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}") + try: + self.world.interface_manager.treeWidget.itemClicked.disconnect() + except TypeError: + pass + self.world.interface_manager.treeWidget.setCurrentItem(foundItem) - self.world.property_panel.updatePropertyPanel(foundItem) + + self.world.interface_manager.treeWidget.itemClicked.connect( + self.world.interface_manager.onTreeItemClicked) else: print("× 在树形控件中没有找到对应项") break - + if not foundItem: print("× 没有找到场景节点或对应的树形项") else: print("× 树形控件不存在") else: print("× 没有找到可选择的模型节点") + self.world.selection.updateSelection(None) def mouseReleaseEventLeft(self, evt): """处理鼠标左键释放事件""" diff --git a/core/patrol_system.py b/core/patrol_system.py new file mode 100644 index 00000000..ab951b47 --- /dev/null +++ b/core/patrol_system.py @@ -0,0 +1,495 @@ +from direct.showbase.ShowBaseGlobal import globalClock +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import Point3, Vec3 +import math + + +class PatrolSystem: + """巡检系统类""" + + def __init__(self, world): + """初始化巡检系统 + + Args: + world: 核心世界对象引用 + """ + self.world = world + + # 巡检状态 + self.is_patrolling = False + self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...] + self.current_patrol_index = 0 + self.patrol_task = None + + # 巡检参数 + self.patrol_speed = 5.0 # 巡检移动速度(单位/秒) + self.patrol_turn_speed = 90.0 # 转向速度(度/秒) + self.patrol_wait_timer = 0.0 + self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back" + + # 相机状态保存 + self.original_cam_pos = None + self.original_cam_hpr = None + + print("✓ 巡检系统初始化完成") + + def add_patrol_point(self, position, heading=None, wait_time=3.0): + if heading is None: + if self.patrol_points: + last_pos = self.patrol_points[-1][0] + direction_x = position[0] - last_pos.x + direction_y = position[1] - last_pos.y + direction_z = position[2] - last_pos.z + + import math + h=math.degrees(math.atan2(-direction_x,-direction_y)) + + distance_xy = math.sqrt(direction_x**2+direction_y**2) + p = math.degrees(math.atan2(direction_z,distance_xy)) + p = max(-89,min(89,p)) + + r=0 + + heading = (h,p,r) + + else: + # 使用当前相机朝向 + current_hpr = self.world.cam.getHpr() + heading = (current_hpr.x, current_hpr.y, current_hpr.z) + + pos = Point3(position[0], position[1], position[2]) + hpr = Vec3(heading[0], heading[1], heading[2]) + + self.patrol_points.append((pos, hpr, wait_time)) + print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}秒") + + # 在 PatrolSystem 类中添加以下方法 + + def add_auto_heading_patrol_point(self, position, wait_time=3.0): + """添加自动计算朝向的巡检点(朝向路径前进方向) + + Args: + position: 相机位置 (x, y, z) + wait_time: 在该点停留时间(秒) + """ + heading = None # 将自动计算朝向 + + # 复用原有的 add_patrol_point 方法 + self.add_patrol_point(position, heading, wait_time) + + def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0): + """添加朝向指定位置的巡检点 + + Args: + position: 相机位置 (x, y, z) + look_at_position: 相机朝向的目标位置 (x, y, z) + wait_time: 在该点停留时间(秒) + """ + import math + + # 计算从当前位置到目标位置的方向向量 + direction_x = look_at_position[0] - position[0] + direction_y = look_at_position[1] - position[1] + direction_z = look_at_position[2] - position[2] + + # 计算HPR朝向 + h = math.degrees(math.atan2(-direction_x, -direction_y)) + + distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2) + p = math.degrees(math.atan2(direction_z, distance_xy)) + p = max(-89, min(89, p)) # 限制pitch角度在合理范围内 + + r = 0 # roll通常为0 + + heading = (h, p, r) + self.add_patrol_point(position, heading, wait_time) + + def clear_patrol_points(self): + """清空所有巡检点""" + self.patrol_points = [] + print("✓ 巡检点已清空") + + def set_patrol_speed(self, move_speed, turn_speed=None): + """设置巡检速度 + + Args: + move_speed: 移动速度(单位/秒) + turn_speed: 转向速度(度/秒),如果为None则保持当前值 + """ + self.patrol_speed = move_speed + if turn_speed is not None: + self.patrol_turn_speed = turn_speed + print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}") + + def start_patrol(self): + """开始巡检""" + if not self.patrol_points: + print("✗ 没有设置巡检点,无法开始巡检") + return False + + if self.is_patrolling: + print("⚠ 巡检已在进行中") + return True + + # 保存当前相机状态 + self.original_cam_pos = Point3(self.world.cam.getPos()) + self.original_cam_hpr = Vec3(self.world.cam.getHpr()) + + # 重置巡检状态 + self.current_patrol_index = 0 + self.patrol_state = "moving" + self.patrol_wait_timer = 0.0 + self.is_patrolling = True + + # 启动巡检任务 + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") + + print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点") + return True + + def stop_patrol(self): + """停止巡检""" + if not self.is_patrolling: + print("⚠ 巡检未在进行中") + return False + + # 停止巡检任务 + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = None + + self.is_patrolling = False + self.patrol_state = "moving" + self.patrol_wait_timer = 0.0 + + print("✓ 巡检已停止") + return True + + def pause_patrol(self): + """暂停巡检""" + if not self.is_patrolling: + print("⚠ 巡检未在进行中") + return False + + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = None + + print("✓ 巡检已暂停") + return True + + def resume_patrol(self): + """恢复巡检""" + if self.is_patrolling: + print("⚠ 巡检已在进行中") + return False + + if not self.patrol_points: + print("✗ 没有设置巡检点") + return False + + self.is_patrolling = True + self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") + + print("✓ 巡检已恢复") + return True + + def reset_to_original_position(self): + """重置相机到原始位置""" + if self.original_cam_pos and self.original_cam_hpr: + self.world.cam.setPos(self.original_cam_pos) + self.world.cam.setHpr(self.original_cam_hpr) + print("✓ 相机已重置到原始位置") + return True + else: + print("✗ 没有保存的原始位置") + return False + + def _patrol_task(self, task): + """巡检主任务""" + try: + if not self.is_patrolling or not self.patrol_points: + return task.done + + # 获取当前巡检点 + current_point = self.patrol_points[self.current_patrol_index] + target_pos, target_hpr, wait_time = current_point + + # 根据当前状态执行不同操作 + if self.patrol_state == "moving": + self._handle_moving_state(target_pos) + elif self.patrol_state == "turning_to_target": + self._handle_turning_to_target_state(target_hpr) + elif self.patrol_state == "waiting": + self._handle_waiting_state(wait_time) + elif self.patrol_state == "turning_back": + self._handle_turning_back_state() + + return task.cont + + except Exception as e: + print(f"巡检任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def _handle_moving_state(self, target_pos): + """处理移动状态""" + current_pos = self.world.cam.getPos() + distance = (target_pos - current_pos).length() + + if distance < 0.1: # 到达目标点 + print(f"✓ 到达巡检点 {self.current_patrol_index + 1}") + self.patrol_state = "turning_to_target" + return + + # 计算移动方向和距离 + direction = target_pos - current_pos + direction.normalize() + + # 计算目标朝向(看向目标点) + target_hpr = self._look_at_to_hpr(direction) + current_hpr = self.world.cam.getHpr() + + # 平滑转向到目标朝向 + h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) + p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) + r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) + + # 计算本帧应转动的角度 + dt = globalClock.getDt() + turn_amount = self.patrol_turn_speed * dt + + # 逐步转向目标角度 + new_hpr = Vec3(current_hpr) + + if abs(h_diff) > turn_amount: + new_hpr.x += turn_amount if h_diff > 0 else -turn_amount + else: + new_hpr.x = target_hpr.x + + if abs(p_diff) > turn_amount: + new_hpr.y += turn_amount if p_diff > 0 else -turn_amount + else: + new_hpr.y = target_hpr.y + + if abs(r_diff) > turn_amount: + new_hpr.z += turn_amount if r_diff > 0 else -turn_amount + else: + new_hpr.z = target_hpr.z + + self.world.cam.setHpr(new_hpr) + + # 计算本帧应移动的距离 + move_distance = self.patrol_speed * dt + + # 如果移动距离大于剩余距离,则直接移动到目标点 + if move_distance >= distance: + self.world.cam.setPos(target_pos) + else: + # 否则按方向移动 + new_pos = current_pos + direction * move_distance + self.world.cam.setPos(new_pos) + + def _handle_turning_to_target_state(self, target_hpr): + """处理转向目标状态""" + # 检查是否需要朝向下一个点 + if target_hpr == "look_next": + # 计算朝向下一个点的方向 + next_index = (self.current_patrol_index + 1) % len(self.patrol_points) + next_point_pos = self.patrol_points[next_index][0] + + current_pos = self.world.cam.getPos() + direction = next_point_pos - current_pos + direction.normalize() + + # 计算目标朝向 + target_hpr = self._look_at_to_hpr(direction) + + current_hpr = self.world.cam.getHpr() + + # 计算角度差 + h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) + p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) + r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) + + # 检查是否已完成转向 + if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0: + print(f"✓ 完成转向,开始停留") + self.patrol_state = "waiting" + self.patrol_wait_timer = 0.0 + return + + # 计算本帧应转动的角度 + dt = globalClock.getDt() + turn_amount = self.patrol_turn_speed * dt + + # 逐步转向目标角度 + new_hpr = Vec3(current_hpr) + + if abs(h_diff) > turn_amount: + new_hpr.x += turn_amount if h_diff > 0 else -turn_amount + else: + new_hpr.x = target_hpr.x + + if abs(p_diff) > turn_amount: + new_hpr.y += turn_amount if p_diff > 0 else -turn_amount + else: + new_hpr.y = target_hpr.y + + if abs(r_diff) > turn_amount: + new_hpr.z += turn_amount if r_diff > 0 else -turn_amount + else: + new_hpr.z = target_hpr.z + + self.world.cam.setHpr(new_hpr) + + def _handle_waiting_state(self, wait_time): + """处理等待状态""" + self.patrol_wait_timer += globalClock.getDt() + + if self.patrol_wait_timer >= wait_time: + print(f"✓ 停留结束,准备转回原朝向") + self.patrol_state = "turning_back" + + # 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法 + + def _handle_turning_back_state(self): + """处理转回原朝向状态""" + # 直接完成转向状态,进入移动状态 + print(f"✓ 停留结束,开始移动到下一个点") + # 移动到下一个巡检点 + next_index = (self.current_patrol_index + 1) % len(self.patrol_points) + self.current_patrol_index = next_index + self.patrol_state = "moving" + return + + def _normalize_angle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _look_at_to_hpr(self, direction): + """将方向向量转换为HPR角度""" + # 简化的转换,实际应用中可能需要更精确的计算 + h = math.degrees(math.atan2(-direction.x, -direction.y)) + p = math.degrees(math.asin(direction.z)) + return Vec3(h, p, 0) + + def get_patrol_status(self): + """获取巡检状态信息""" + return { + "is_patrolling": self.is_patrolling, + "current_point": self.current_patrol_index, + "total_points": len(self.patrol_points), + "state": self.patrol_state, + "wait_timer": self.patrol_wait_timer + } + + def list_patrol_points(self): + """列出所有巡检点""" + if not self.patrol_points: + print("没有设置巡检点") + return + + print(f"巡检点列表 (共{len(self.patrol_points)}个):") + for i, (pos, hpr, wait_time) in enumerate(self.patrol_points): + current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else "" + print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) " + f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) " + f"停留:{wait_time}秒{current_marker}") + + def remove_patrol_point(self, index): + """移除指定索引的巡检点""" + if 0 <= index < len(self.patrol_points): + removed_point = self.patrol_points.pop(index) + print( + f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})") + + # 调整当前索引 + if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points: + self.current_patrol_index = len(self.patrol_points) - 1 + elif self.current_patrol_index >= len(self.patrol_points): + self.current_patrol_index = 0 + else: + print(f"✗ 无效的巡检点索引: {index}") + + def insert_patrol_point(self, index, position, heading=None, wait_time=3.0): + """在指定位置插入巡检点""" + if index < 0 or index > len(self.patrol_points): + print(f"✗ 无效的插入位置: {index}") + return + + if heading is None: + # 使用当前相机朝向 + current_hpr = self.world.cam.getHpr() + heading = (current_hpr.x, current_hpr.y, current_hpr.z) + + pos = Point3(position[0], position[1], position[2]) + hpr = Vec3(heading[0], heading[1], heading[2]) + + self.patrol_points.insert(index, (pos, hpr, wait_time)) + print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}秒") + + def update_patrol_point(self, index, position=None, heading=None, wait_time=None): + """更新指定巡检点的信息""" + if 0 <= index < len(self.patrol_points): + pos, hpr, wt = self.patrol_points[index] + + if position is not None: + pos = Point3(position[0], position[1], position[2]) + if heading is not None: + hpr = Vec3(heading[0], heading[1], heading[2]) + if wait_time is not None: + wt = wait_time + + self.patrol_points[index] = (pos, hpr, wt) + print(f"✓ 更新巡检点 {index + 1}") + else: + print(f"✗ 无效的巡检点索引: {index}") + + def goto_patrol_point(self, index): + """直接跳转到指定巡检点""" + if not self.patrol_points: + print("✗ 没有设置巡检点") + return False + + if 0 <= index < len(self.patrol_points): + pos, hpr, _ = self.patrol_points[index] + self.world.cam.setPos(pos) + self.world.cam.setHpr(hpr) + self.current_patrol_index = index + print(f"✓ 跳转到巡检点 {index + 1}") + return True + else: + print(f"✗ 无效的巡检点索引: {index}") + return False + + def cleanup(self): + """清理巡检系统资源""" + self.stop_patrol() + self.clear_patrol_points() + self.original_cam_pos = None + self.original_cam_hpr = None + print("✓ 巡检系统资源已清理") + + +# 使用示例和便捷函数 +def create_default_patrol_route(patrol_system): + """创建默认的巡检路线(示例)""" + # 清空现有巡检点 + patrol_system.clear_patrol_points() + + # 添加一些示例巡检点 + patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1:前方低位置 + patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2:中央高位置 + patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3:右侧位置 + patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4:左侧位置 + + print("✓ 默认巡检路线已创建") + diff --git a/core/script_system.py b/core/script_system.py index abb206d3..d9aa465c 100644 --- a/core/script_system.py +++ b/core/script_system.py @@ -253,7 +253,7 @@ class ScriptLoader: for component in components_to_remove: self.script_manager.remove_script_from_object(component.game_object, script_name) - + # 从sys.modules中移除 module = self.loaded_modules[script_name] if module.__name__ in sys.modules: @@ -294,6 +294,31 @@ class ScriptLoader: return len(changed_scripts) > 0 + def find_script_file(self, script_name: str) -> Optional[str]: + """根据脚本名称查找脚本文件路径""" + # 首先检查已加载的脚本 + if script_name in self.loaded_modules: + module = self.loaded_modules[script_name] + if hasattr(module, '__file__') and module.__file__: + return module.__file__ + + # 在已知的文件路径中查找 + for file_path in self.file_mtimes.keys(): + file_name = os.path.splitext(os.path.basename(file_path))[0] + if file_name == script_name: + return file_path + + # 在脚本目录中查找 + scripts_dir = self.script_manager.scripts_directory + if os.path.exists(scripts_dir): + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if base_name == script_name: + return os.path.join(scripts_dir, file_name) + + return None + class ScriptAPI: """脚本API - 提供给脚本使用的API接口""" @@ -671,18 +696,62 @@ class {class_name}(ScriptBase): return False script_components = self.object_scripts[game_object] + removed = False + for component in script_components[:]: # 复制列表以避免修改时出错 if component.script_instance.__class__.__name__ == script_name: # 从引擎移除 self.engine.remove_script_component(component) # 从对象脚本列表移除 script_components.remove(component) + removed = True print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}") - return True + + + if not script_components: + del self.object_scripts[game_object] + + # 更新节点上保存的脚本信息标签 + if removed: + self._update_node_script_tags_after_removal(game_object, script_name) - return False - + return removed + + def _update_node_script_tags_after_removal(self, game_object, removed_script_name): + """在移除脚本后更新节点标签""" + try: + # 获取对象上剩余的脚本 + remaining_scripts = self.get_scripts_on_object(game_object) + + if not remaining_scripts: + # 如果没有其他脚本,清除所有脚本标签 + if game_object.hasTag("has_scripts"): + game_object.clearTag("has_scripts") + if game_object.hasTag("scripts_info"): + game_object.clearTag("scripts_info") + print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签") + else: + # 如果还有其他脚本,更新脚本信息标签 + script_info_list = [] + for script_component in remaining_scripts: + script_name = script_component.script_name + script_class = script_component.script_instance.__class__ + script_file = self.loader.find_script_file(script_name) or "" + + script_info_list.append({ + "name": script_name, + "file": script_file + }) + + import json + game_object.setTag("has_scripts", "true") + game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) + print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本") + + except Exception as e: + print(f"更新节点标签失败: {e}") + def get_scripts_on_object(self, game_object) -> List[ScriptComponent]: """获取对象上的所有脚本""" return self.object_scripts.get(game_object, []) @@ -744,6 +813,36 @@ class {class_name}(ScriptBase): print("==================\n") + def save_object_scripts(self,game_object,node_data:dict): + try: + if game_object in self.object_scripts: + scripts_data = [] + for script_commponent in self.object_scripts[game_object]: + script_info = { + 'script_name':script_commponent.script_name, + 'enabled':script_commponent.enabled, + 'script_class':script_commponent.script_instance.__class__.__name__ + } + scripts_data.append(script_info) + if scripts_data: + node_data['scripts'] = scripts_data + print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}") + except Exception as e: + print(f"保存对象脚本信息失败: {e}") + traceback.print_exc() + + # def restore_object_scripts(self,game_object,node_data:dict): + # try: + # if 'scripts' in node_data: + # scripts_data = node_data['scripts'] + # restored_count = 0 + # for script_info in scripts_data: + # script_name = script_info.get('script_name') + # enabled = script_info.get('enabled',True) + # + # #检查脚本是否可用 + # if script_name in self.loader.script_classes: + #为 # 添加全局便捷函数,让脚本更容易使用API def get_script_api(): diff --git a/core/selection.py b/core/selection.py index 7cef719c..8a3302aa 100644 --- a/core/selection.py +++ b/core/selection.py @@ -8,6 +8,7 @@ - 射线检测和碰撞检测 """ from PIL.ImageChops import lighter +from direct.showbase.ShowBaseGlobal import globalClock from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, @@ -63,9 +64,21 @@ class SelectionSystem: "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 } + #性能优化相关 + self._optimized_node = False + self._last_update_time = 0 + self._cached_bounds = {} + self._gizmo_update_interval = 0.1 + self._selection_box_update_interval = 0.2 + self._current_cursor = None self._default_cursor = None + self._last_click_time = 0 + self._double_click_threshold = 0.3 + self._last_clicked_node = None + self._double_click_task = None + print("✓ 选择和变换系统初始化完成") # ==================== 光标设置 ==================== def _setCursor(self,cursor_type): @@ -112,7 +125,7 @@ class SelectionSystem: def createSelectionBox(self, nodePath): """为选中的节点创建选择框""" try: - print(f" 开始创建选择框,目标节点: {nodePath.getName()}") + #print(f" 开始创建选择框,目标节点: {nodePath.getName()}") # 如果已有选择框,先移除 if self.selectionBox: @@ -127,17 +140,17 @@ class SelectionSystem: # 创建选择框作为render的子节点,但会实时跟踪目标节点 self.selectionBox = self.world.render.attachNewNode("selectionBox") self.selectionBoxTarget = nodePath # 保存目标节点引用 - print(f" 选择框节点创建完成: {self.selectionBox}") + #print(f" 选择框节点创建完成: {self.selectionBox}") # 启动选择框更新任务 taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - print(" 选择框更新任务已启动") + #print(" 选择框更新任务已启动") # 初始更新选择框 - print(" 开始初始化选择框几何体...") - self.updateSelectionBoxGeometry() + #print(" 开始初始化选择框几何体...") + #self.updateSelectionBoxGeometry() - print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") + #print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") except Exception as e: print(f" ✗ 创建选择框失败: {str(e)}") @@ -212,7 +225,7 @@ class SelectionSystem: return # 获取边界框的最小和最大点(世界坐标) - print(f"世界边界框: min={minPoint}, max={maxPoint}") + #print(f"世界边界框: min={minPoint}, max={maxPoint}") # 创建线段对象 lines = LineSegs() @@ -271,51 +284,58 @@ class SelectionSystem: traceback.print_exc() def updateSelectionBoxTask(self, task): - """选择框更新任务""" + """选择框更新任务 - 平衡性能和实时性""" try: - if not hasattr(self,'_last_selection_box_update'): + update_interval = 0.1 + + if not hasattr(self, '_last_selection_box_update'): self._last_selection_box_update = 0 import time current_time = time.time() - if current_time - self._last_selection_box_update < 0.1: + if current_time - self._last_selection_box_update < update_interval: return task.cont self._last_selection_box_update = current_time - #检查目标节点是否已被删除 + # 检查目标节点是否已被删除 self.checkAndClearIfTargetDeleted() if not self.selectionBox or not self.selectionBoxTarget: - return task.done # 结束任务 + return task.done # 检查目标节点是否还存在 if self.selectionBoxTarget.isEmpty(): self.clearSelectionBox() return task.done - # 获取目标节点在世界坐标系中的当前边界框(使用正确的API) - currentMinPoint = Point3() - currentMaxPoint = Point3() - if not self.selectionBoxTarget.calcTightBounds(currentMinPoint, currentMaxPoint, self.world.render): - return task.cont + # 检查目标节点是否发生了变化(位置、旋转、缩放) + current_transform = self._getNodeTransformKey(self.selectionBoxTarget) - # 检查边界框是否发生变化(位置或大小) - if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or - self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint): - - # 更新选择框几何体 + if (not hasattr(self, '_last_transform_key') or + self._last_transform_key != current_transform): + # 节点发生了变化,更新选择框 self.updateSelectionBoxGeometry() + self._last_transform_key = current_transform - # 保存当前边界框信息 - self._lastMinPoint = currentMinPoint - self._lastMaxPoint = currentMaxPoint - - return task.cont # 继续任务 + return task.cont except Exception as e: print(f"选择框更新任务出错: {str(e)}") return task.done + def _getNodeTransformKey(self, node): + """获取节点变换的关键信息,用于快速比较""" + try: + # 获取节点的关键变换信息 + pos = node.getPos(self.world.render) + hpr = node.getHpr(self.world.render) + scale = node.getScale(self.world.render) + + # 返回一个可以比较的元组 + return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z) + except: + return None + def clearSelectionBox(self): """清除选择框""" if self.selectionBox: @@ -335,7 +355,7 @@ class SelectionSystem: def createGizmo(self, nodePath): """为选中的节点创建坐标轴工具 - 保留箭头版本""" try: - print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") + #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") # 如果已有坐标轴,先移除 if self.gizmo: @@ -350,6 +370,22 @@ class SelectionSystem: self.gizmo = self.world.render.attachNewNode("gizmo") self.gizmoTarget = nodePath + # 添加标识标签,便于识别 + self.gizmo.setTag("is_gizmo", "1") + if hasattr(nodePath, 'getName'): + self.gizmo.setTag("gizmo_target", nodePath.getName()) + + # 为各轴添加标签 + if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis: + self.gizmoXAxis.setTag("is_gizmo", "1") + self.gizmoXAxis.setTag("gizmo_axis", "x") + if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis: + self.gizmoYAxis.setTag("is_gizmo", "1") + self.gizmoYAxis.setTag("gizmo_axis", "y") + if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis: + self.gizmoZAxis.setTag("is_gizmo", "1") + self.gizmoZAxis.setTag("gizmo_axis", "z") + # 设置位置和朝向 minPoint = Point3() maxPoint = Point3() @@ -385,7 +421,7 @@ class SelectionSystem: # 只启动一次更新任务 taskMgr.add(self.updateGizmoTask, "updateGizmo") - print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") + #print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") except Exception as e: print(f"创建坐标轴失败: {str(e)}") @@ -431,7 +467,7 @@ class SelectionSystem: else: gizmo_model = self.world.loader.loadModel(path) if gizmo_model: - print(f"成功加载模型: {path}") + #print(f"成功加载模型: {path}") break except: continue @@ -877,7 +913,7 @@ class SelectionSystem: handle_node = children[0] if not handle_node: - print(f"未找到{axis}轴的处理模型") + #print(f"未找到{axis}轴的处理模型") return # 创建或获取材质 @@ -985,7 +1021,7 @@ class SelectionSystem: handle_node = children[0] if not handle_node: - print(f"未找到{axis}轴的处理模型") + # print(f"未找到{axis}轴的处理模型") return # 创建或获取材质 @@ -999,7 +1035,7 @@ class SelectionSystem: ) mat.setBaseColor(adjusted_color) - mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 # 应用材质 handle_node.setMaterial(mat, 1) @@ -1177,7 +1213,7 @@ class SelectionSystem: return self.checkGizmoClickFallback(mouseX, mouseY) # 计算点击阈值 - click_threshold = 30 # 增大检测范围 + click_threshold = 15 # 增大检测范围 # 检测各个轴,对于端点在屏幕外的轴提供回退方案 def getClickDetectionPoint(axis_name, original_screen_pos): @@ -1206,10 +1242,10 @@ class SelectionSystem: distance = self.distanceToLine( (mouseX, mouseY), center_screen, axis_screen ) - print(f"{axis_label}距离: {distance:.2f}") + #print(f"{axis_label}距离: {distance:.2f}") if distance < click_threshold: - print(f"✓ 点击了{axis_label}") + #print(f"✓ 点击了{axis_label}") return axis_name return None @@ -1657,14 +1693,12 @@ class SelectionSystem: if parent_node and parent_node != self.world.render: try: - if parent_node.getTransform().hasMat(): - transform_mat = parent_node.getMat(self.world.render) - if not transform_mat.isSingular(): - world_axis_vector = transform_mat.xformVec(local_axis_vector) - else: - print("警告: 检测到奇异变换矩阵,使用默认轴向量") + #获取变换矩阵 + transfrom_mat = parent_node.getMat(self.world.render) + if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat): + world_axis_vector = transfrom_mat.xformVec(local_axis_vector) else: - print("警告: 父节点没有有效的变换矩阵,使用默认轴向量") + print("警告: 检测到无效变换矩阵,使用默认轴向量") except Exception as e: print(f"变换计算出错: {e},使用默认轴向量") else: @@ -1812,6 +1846,23 @@ class SelectionSystem: import traceback traceback.print_exc() + def _isMatrixValid(self, matrix): + """检查矩阵是否有效,替代 isSingular 方法""" + try: + # 检查矩阵元素是否为有效数字 + for i in range(4): + for j in range(4): + element = matrix.getCell(i, j) + # 检查是否为 NaN 或无穷大 + if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf': + return False + # 检查是否过大 + if abs(element) > 1e10: + return False + return True + except: + return False + def _safeUpdatePropertyPanel(self): """安全地更新属性面板""" try: @@ -1871,58 +1922,110 @@ class SelectionSystem: # ==================== 选择管理 ==================== def updateSelection(self, nodePath): - """更新选择状态""" - print(f"\n=== 更新选择状态 ===") + try: + if self.selectedNode == nodePath: + return + print(f"\n=== 更新选择状态 ===") + + # 如果正在删除节点,避免更新选择 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,跳过选择更新") + print("=== 选择状态更新完成 ===\n") + return + + node_name = "None" + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + print(f"新选择的节点: {node_name}") + + self.selectedNode = nodePath + # 添加兼容性属性 + self.selectedObject = nodePath + + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + #print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + + # 创建选择框 + #print("创建选择框...") + self.createSelectionBox(nodePath) + if self.selectionBox: + box_name = "Unknown" + if self.selectionBox and not self.selectionBox.isEmpty(): + box_name = self.selectionBox.getName() + #print(f"✓ 选择框创建成功: {box_name}") + else: + print("× 选择框创建失败") + + # 创建坐标轴 + #print("创建坐标轴...") + self.createGizmo(nodePath) + if self.gizmo: + gizmo_name = "Unknown" + if self.gizmo and not self.gizmo.isEmpty(): + gizmo_name = self.gizmo.getName() + #print(f"✓ 坐标轴创建成功: {gizmo_name}") + else: + print("× 坐标轴创建失败") + + print(f"✓ 选中了节点: {node_name}") + else: + print("清除选择...") + self.clearSelectionBox() + self.clearGizmo() + print("✓ 取消选择") + + #当取消选择时,同步清空树形控件的选中状态 + if (hasattr(self.world,'interface_manager')and + self.world.interface_manager and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + print("✓ 树形控件选中状态已清空") - # 如果正在删除节点,避免更新选择 - if hasattr(self, '_deleting_node') and self._deleting_node: - print("正在删除节点,跳过选择更新") print("=== 选择状态更新完成 ===\n") - return + except Exception as e: + print(f"更新选择状态失败{str(e)}") + import traceback + traceback.print_exc() - node_name = "None" - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - print(f"新选择的节点: {node_name}") + def _updateSelectionVisuals(self, nodePath): + """更新选择的视觉效果(选择框和坐标轴)""" + try: + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - self.selectedNode = nodePath - # 添加兼容性属性 - self.selectedObject = nodePath + # 创建选择框 + print("创建选择框...") + self.createSelectionBox(nodePath) + if self.selectionBox: + box_name = "Unknown" + if self.selectionBox and not self.selectionBox.isEmpty(): + box_name = self.selectionBox.getName() + print(f"✓ 选择框创建成功: {box_name}") + else: + print("× 选择框创建失败") - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # 创建坐标轴 + print("创建坐标轴...") + self.createGizmo(nodePath) + if self.gizmo: + gizmo_name = "Unknown" + if self.gizmo and not self.gizmo.isEmpty(): + gizmo_name = self.gizmo.getName() + print(f"✓ 坐标轴创建成功: {gizmo_name}") + else: + print("× 坐标轴创建失败") - # 创建选择框 - print("创建选择框...") - self.createSelectionBox(nodePath) - if self.selectionBox: - box_name = "Unknown" - if self.selectionBox and not self.selectionBox.isEmpty(): - box_name = self.selectionBox.getName() - print(f"✓ 选择框创建成功: {box_name}") + print(f"✓ 选中了节点: {node_name}") else: - print("× 选择框创建失败") + print("清除选择...") + self.clearSelectionBox() + self.clearGizmo() + print("✓ 取消选择") - # 创建坐标轴 - print("创建坐标轴...") - self.createGizmo(nodePath) - if self.gizmo: - gizmo_name = "Unknown" - if self.gizmo and not self.gizmo.isEmpty(): - gizmo_name = self.gizmo.getName() - print(f"✓ 坐标轴创建成功: {gizmo_name}") - else: - print("× 坐标轴创建失败") - - print(f"✓ 选中了节点: {node_name}") - else: - print("清除选择...") - self.clearSelectionBox() - self.clearGizmo() - print("✓ 取消选择") - - print("=== 选择状态更新完成 ===\n") + except Exception as e: + print(f"更新选择视觉效果失败: {e}") def getSelectedNode(self): """获取当前选中的节点""" @@ -2010,7 +2113,7 @@ class SelectionSystem: collision_np.hide() # 隐藏碰撞体,只用于检测 - print(f"✓ 成功创建 {axis_name} 轴碰撞体") + #print(f"✓ 成功创建 {axis_name} 轴碰撞体") except Exception as e: print(f"创建{axis_name}轴碰撞体失败: {e}") @@ -2098,4 +2201,583 @@ class SelectionSystem: if not collision_node.isEmpty(): print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") else: - print(f"{axis_name.upper()}轴节点不存在") \ No newline at end of file + print(f"{axis_name.upper()}轴节点不存在") + + def focusCameraOnSelectedNodeAdvanced(self): + """高级版的摄像机聚焦功能,包含平滑动画效果""" + try: + if not self.selectedNode or self.selectedNode.isEmpty(): + print("没有选中的节点,无法聚焦") + return False + + # 获取选中节点的边界框 + minPoint = Point3() + maxPoint = Point3() + + if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): + print("无法计算选中节点的边界框,使用节点为位置作为替代方案") + node_pos = self.selectedNode.getPos(self.world.render) + optimal_distance = 10.0 + current_cam_pos = self.world.cam.getPos() + view_direction = node_pos - current_cam_pos + if view_direction.length()<0.001: + view_direction = Vec3(5,-5,2) + view_direction.normalize() + target_cam_pos = node_pos - (view_direction * optimal_distance) + + temp_node =self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(node_pos) + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + currrent_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) + print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}") + return True + + # 计算节点中心点和大小 + center = Point3( + (minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5 + ) + + # 计算节点的对角线长度 + size = (maxPoint - minPoint).length() + + # 如果节点太小,使用默认大小 + if size < 0.01: + size = 1.0 + + # 获取当前摄像机位置和朝向 + current_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + + # 计算观察方向 + view_direction = current_cam_pos - center + if view_direction.length() < 0.001: + view_direction = Vec3(5, -5, 2) + + view_direction.normalize() + + # 计算合适的观察距离 + optimal_distance = max(size * 2.0, 5.0) + + # 计算目标摄像机位置 + target_cam_pos = center + (view_direction * optimal_distance) + + # 计算目标朝向(不直接使用lookAt,而是计算目标HPR) + # 创建临时节点用于计算目标朝向 + temp_node = self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(center) + + # 创建另一个临时节点用于计算朝向 + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + # 清理临时节点 + temp_node.removeNode() + dummy_cam.removeNode() + + # 使用任务来实现平滑移动动画 + self._startCameraFocusAnimation(current_cam_pos, target_cam_pos, + current_cam_hpr, target_cam_hpr) + + print(f"开始聚焦到节点: {self.selectedNode.getName()}") + return True + + except Exception as e: + print(f"高级聚焦功能失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): + """启动摄像机聚焦动画""" + try: + # 创建动画任务 + class CameraFocusData: + def __init__(self, start_pos, end_pos, start_hpr, end_hpr): + self.start_pos = Point3(start_pos) # 确保是Point3类型 + self.end_pos = Point3(end_pos) # 确保是Point3类型 + self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型 + self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型 + self.elapsed_time = 0.0 + self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑 + + self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) + + # 移除之前的任务(如果存在) + taskMgr.remove("cameraFocusTask") + + # 添加新任务 + taskMgr.add(self._cameraFocusTask, "cameraFocusTask") + + except Exception as e: + print(f"启动摄像机聚焦动画失败: {e}") + + def _normalizeAngle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _cameraFocusTask(self, task): + """摄像机聚焦动画任务""" + try: + if not hasattr(self, '_camera_focus_data'): + return task.done + + data = self._camera_focus_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用更平滑的插值函数 + smooth_t = t * t * (3 - 2 * t) # 平滑步进插值 + + # 手动实现lerp功能 + def lerp_point3(start, end, factor): + return Point3( + start.x + (end.x - start.x) * factor, + start.y + (end.y - start.y) * factor, + start.z + (end.z - start.z) * factor + ) + + # 角度插值需要特殊处理,确保选择最短路径 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + # 再次规范化 + result_x = self._normalizeAngle(result_x) + result_y = self._normalizeAngle(result_y) + result_z = self._normalizeAngle(result_z) + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t) + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_camera_focus_data'): + delattr(self, '_camera_focus_data') + print("摄像机聚焦动画完成") + return task.done + + return task.cont + + except Exception as e: + print(f"摄像机聚焦任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def _smoothCameraMoveTask(self, task): + """平滑摄像机移动任务""" + try: + if not hasattr(self, '_smooth_camera_move_data'): + return task.done + + data = self._smooth_camera_move_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用平滑插值 + smooth_t = t * t * (3 - 2 * t) + + # 角度插值需要特殊处理 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = Point3( + data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t, + data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t, + data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t + ) + + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_smooth_camera_move_data'): + delattr(self, '_smooth_camera_move_data') + print("摄像机平滑移动完成") + return task.done + + return task.cont + + except Exception as e: + print(f"平滑摄像机移动任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def handleMouseClick(self, nodePath, mouseX=None, mouseY=None): + """处理鼠标点击事件 - 支持坐标轴双击聚焦""" + try: + # 如果正在删除节点,忽略鼠标点击 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,忽略鼠标点击") + return + + import time + current_time = time.time() + + # 检查是否点击了坐标轴 + is_gizmo_click = False + target_node = nodePath + + # 判断是否点击了坐标轴 + if (nodePath and hasattr(nodePath, 'getName') and + (nodePath.getName().startswith("gizmo") or + "gizmo" in nodePath.getName().lower() or + (hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))): + is_gizmo_click = True + # 如果有选中的模型,使用选中的模型作为聚焦目标 + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}") + + # 检查是否为双击(同一节点且在时间阈值内) + is_double_click = (self._last_clicked_node == target_node and + target_node is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 双击 detected + node_name = target_node.getName() if target_node else "None" + print(f"检测到双击节点: {node_name}") + + # 无论是点击模型还是坐标轴,都执行聚焦 + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + if self.selectedNode != target_node: + self.updateSelection(target_node) + else: + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") + + # 重置状态以避免三击等误触发 + self._last_click_time = 0 + self._last_clicked_node = None + else: + # 单击,更新状态 + self._last_click_time = current_time + self._last_clicked_node = target_node + + # 如果点击的是坐标轴,保持当前选择不变 + if is_gizmo_click: + print("坐标轴单击,保持当前选择") + else: + # 正常的单击选择 + self.updateSelection(nodePath) + + except Exception as e: + print(f"处理鼠标点击事件失败: {e}") + + def _onDoubleClick(self, nodePath): + """双击事件处理""" + try: + # 获取实际要聚焦的目标节点 + target_node = nodePath + + # 如果是坐标轴,确保使用关联的模型作为目标 + if (nodePath and hasattr(nodePath, 'hasTag') and + nodePath.hasTag("is_gizmo")): + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}") + else: + print("坐标轴双击,但没有关联的选中模型") + return + + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + # 更新选择(如果需要) + if self.selectedNode != target_node: + self.updateSelection(target_node) + + # 执行聚焦 + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") + + except Exception as e: + print(f"双击事件处理失败: {e}") + + # 添加一个更精确的双击检测方法 + + def checkDoubleClick(self, nodePath): + """检查是否为双击,返回布尔值""" + try: + import time + current_time = time.time() + + # 必须是同一节点且在时间阈值内 + is_double_click = (self._last_clicked_node == nodePath and + nodePath is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + return True + else: + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"双击检测失败: {e}") + return False + + # 添加一个定时重置方法,用于清除长时间未完成的双击状态 + + def _resetDoubleClickState(self): + """重置双击状态""" + self._last_click_time = 0 + self._last_clicked_node = None + + # 添加一个任务来自动重置双击状态 + + def _startDoubleClickResetTask(self): + """启动双击状态重置任务""" + if self._double_click_task: + taskMgr.remove(self._double_click_task) + + self._double_click_task = taskMgr.doMethodLater( + self._double_click_threshold * 2, # 等待2倍阈值时间 + self._resetDoubleClickStateTask, + "resetDoubleClickState" + ) + + def _resetDoubleClickStateTask(self, task): + """任务:重置双击状态""" + self._resetDoubleClickState() + self._double_click_task = None + return task.done + + # 修改 updateSelection 方法以集成双击检测 + + # def updateSelection(self, nodePath): + # """更新选择状态""" + # print(f"\n=== 更新选择状态 ===") + # + # # 如果正在删除节点,避免更新选择 + # if hasattr(self, '_deleting_node') and self._deleting_node: + # print("正在删除节点,跳过选择更新") + # print("=== 选择状态更新完成 ===\n") + # return + # + # node_name = "None" + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"新选择的节点: {node_name}") + # + # # 检查是否为双击 + # is_double_click = self.checkDoubleClick(nodePath) + # if is_double_click: + # print(f"检测到双击 {node_name},执行聚焦") + # # 启动聚焦(在下一帧执行,确保选择状态已更新) + # taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") + # + # self.selectedNode = nodePath + # # 添加兼容性属性 + # self.selectedObject = nodePath + # + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # + # # 创建选择框 + # print("创建选择框...") + # self.createSelectionBox(nodePath) + # if self.selectionBox: + # box_name = "Unknown" + # if self.selectionBox and not self.selectionBox.isEmpty(): + # box_name = self.selectionBox.getName() + # print(f"✓ 选择框创建成功: {box_name}") + # else: + # print("× 选择框创建失败") + # + # # 创建坐标轴 + # print("创建坐标轴...") + # self.createGizmo(nodePath) + # if self.gizmo: + # gizmo_name = "Unknown" + # if self.gizmo and not self.gizmo.isEmpty(): + # gizmo_name = self.gizmo.getName() + # print(f"✓ 坐标轴创建成功: {gizmo_name}") + # else: + # print("× 坐标轴创建失败") + # + # print(f"✓ 选中了节点: {node_name}") + # else: + # print("清除选择...") + # self.clearSelectionBox() + # self.clearGizmo() + # print("✓ 取消选择") + # + # print("=== 选择状态更新完成 ===\n") + + def _delayedFocusTask(self, task): + """延迟执行聚焦任务""" + try: + self.focusCameraOnSelectedNodeAdvanced() + except Exception as e: + print(f"延迟聚焦任务失败: {e}") + return task.done + + # 添加一个更智能的双击检测方法,考虑鼠标位置 + + def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None): + """检查是否为双击,同时考虑鼠标位置""" + try: + import time + current_time = time.time() + + # 如果没有提供鼠标位置,直接使用基本双击检测 + if mouse_x is None or mouse_y is None: + return self.checkDoubleClick(nodePath) + + # 检查节点和时间 + time_diff = current_time - self._last_click_time + is_same_node = (self._last_clicked_node == nodePath) + + # 如果是同一节点且在时间阈值内,认为是双击 + if is_same_node and time_diff < self._double_click_threshold: + # 重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + return True + else: + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"带位置的双击检测失败: {e}") + return False + + # 添加一个公共方法来设置双击阈值 + + def setDoubleClickThreshold(self, threshold_seconds): + """设置双击时间阈值""" + if threshold_seconds > 0: + self._double_click_threshold = threshold_seconds + print(f"双击阈值已设置为: {threshold_seconds} 秒") + else: + print("无效的双击阈值") + + # 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式) + + def triggerDoubleClickFocus(self, nodePath=None): + """手动触发双击聚焦""" + try: + target_node = nodePath if nodePath else self.selectedNode + if target_node and not target_node.isEmpty(): + print(f"手动触发聚焦到节点: {target_node.getName()}") + if self.selectedNode != target_node: + self.updateSelection(target_node) + self.focusCameraOnSelectedNodeAdvanced() + return True + else: + print("没有有效的目标节点进行聚焦") + return False + except Exception as e: + print(f"手动触发聚焦失败: {e}") + return False + + def cleanup(self): + """清理选择系统资源""" + # 清理双击任务 + if self._double_click_task: + taskMgr.remove(self._double_click_task) + self._double_click_task = None + + # 清理其他资源 + self.clearSelectionBox() + 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/core/terrain_manager.py b/core/terrain_manager.py index cfd85590..edbec218 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -1,9 +1,13 @@ # core/terrain_manager.py import time +import urllib + from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight import os +from scene import util + class TerrainManager: """地形管理类""" @@ -31,106 +35,151 @@ class TerrainManager: return None try: - # 创建GeoMipTerrain对象 - terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain = GeoMipTerrain(terrain_name) + print(f"🔆 开始创建高度图地形") - # 加载高度图 - height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) - - # 检查并调整图像尺寸为2的幂次方加1 - width, height = height_image.getXSize(), height_image.getYSize() - print(f"原始图像尺寸: {width}x{height}") - - # 找到最接近的有效尺寸 - valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] - target_size = 129 # 默认尺寸 - - # 选择最接近的尺寸 - max_dim = max(width, height) - for size in valid_sizes: - if size >= max_dim: - target_size = size - break - else: - target_size = valid_sizes[-1] # 使用最大尺寸 - - # 如果需要,调整图像尺寸 - if width != target_size or height != target_size: - print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") - # 使用正确的图像缩放方法 - resized_image = PNMImage(target_size, target_size) - resized_image.quickFilterFrom(height_image) - height_image = resized_image - - # 使用正确的方法设置高度图 - terrain.setHeightfield(height_image) - - # 设置地形参数 - terrain.setBruteforce(True) # 使用LOD - - terrain.setBlockSize(32) - # terrain.setNearFarThreshold(50.0,200.0) - - # 生成地形 - terrain.generate() - - # 获取地形节点 - terrain_node = terrain.getRoot() - - if terrain_node.isEmpty(): - print("错误:无法生成有效的地形节点") + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") return None - node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" - terrain_node.setName(node_name) + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None - center_offset = (target_size - 1) / 2 - terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) + created_terrains = [] + try: + parent_item, parent_node = target_parents[0] - # 设置缩放 - terrain_node.setScale(scale[0], scale[1], scale[2]) + # 创建GeoMipTerrain对象 + terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) - # 将地形添加到场景中 - terrain_node.reparentTo(self.world.render) + # 加载高度图 + height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) - from panda3d.core import BitMask32 - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 - # 为地形的所有子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) + # 检查并调整图像尺寸为2的幂次方加1 + width, height = height_image.getXSize(), height_image.getYSize() + print(f"原始图像尺寸: {width}x{height}") - # 添加材质 - self._applyTerrainMaterial(terrain_node) + # 找到最接近的有效尺寸 + valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] + target_size = 129 # 默认尺寸 - terrain_node.setPythonTag("selectable", True) + # 选择最接近的尺寸 + max_dim = max(width, height) + for size in valid_sizes: + if size >= max_dim: + target_size = size + break + else: + target_size = valid_sizes[-1] # 使用最大尺寸 - # 保存地形信息(包括高度图的副本) - terrain_info = { - 'terrain': terrain, - 'node': terrain_node, - 'heightmap': heightmap_path, - 'heightfield': height_image, # 保存高度图副本 - 'scale': scale, - 'name': node_name - } + # 如果需要,调整图像尺寸 + if width != target_size or height != target_size: + print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") + # 使用正确的图像缩放方法 + resized_image = PNMImage(target_size, target_size) + resized_image.quickFilterFrom(height_image) + height_image = resized_image - self.terrains.append(terrain_info) + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) - # 更新场景树(再次检查节点是否有效) - if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, - 'updateSceneTree'): - try: - self.world.scene_manager.updateSceneTree() - except Exception as e: - print(f"警告: 更新场景树时出错: {e}") + # 设置地形参数 + terrain.setBruteforce(True) # 使用LOD - print(f"✓ 成功从 {heightmap_path} 创建地形") - return terrain_info + terrain.setBlockSize(32) + # terrain.setNearFarThreshold(50.0,200.0) + + # 生成地形 + terrain.generate() + + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的地形节点") + return None + + terrain_node.setTag("is_scene_element", "1") + terrain_node.setTag("tree_item_type", "TERRAIN_NODE") + + node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" + terrain_node.setName(node_name) + + center_offset = (target_size - 1) / 2 + terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) + + # 设置缩放 + terrain_node.setScale(scale[0], scale[1], scale[2]) + + # 将地形添加到场景中 + terrain_node.reparentTo(parent_node) + + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + terrain_node.setPythonTag("selectable", True) + + # 保存地形信息(包括高度图的副本) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': heightmap_path, + 'heightfield': height_image, # 保存高度图副本 + 'scale': scale, + 'name': node_name + } + + self.terrains.append(terrain_info) + + print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: + created_terrains.append((terrain_node, qt_item)) + else: + created_terrains.append((terrain_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}") + return None + + # 处理创建结果 + if not created_terrains: + print("❌ 没有成功创建任何高度图地形") + return None + + # 选中最后创建的光源 + if created_terrains: + last_light_np, last_qt_item = created_terrains[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形") + + # 返回值处理 + if len(created_terrains) == 1: + return created_terrains[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 except Exception as e: - print(f"创建地形时出错: {e}") + print(f"❌ 创建高度图地形过程失败: {str(e)}") import traceback traceback.print_exc() return None @@ -138,84 +187,129 @@ class TerrainManager: def createFlatTerrain(self, size=(0.3, 0.3), resolution=129): """创建平面地形""" try: - # 确保分辨率是2的幂次方加1 (如129, 257, 513等) - valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] - if resolution not in valid_resolutions: - # 找到最接近的有效分辨率 - closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) - print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") - resolution = closest_res + print(f"🔆 开始创建平面地形") - # 创建GeoMipTerrain对象 - terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain = GeoMipTerrain(terrain_name) - - # 创建一个平面高度图,尺寸必须是2^n+1 - height_image = PNMImage(resolution, resolution) - height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) - - # 使用正确的方法设置高度图 - terrain.setHeightfield(height_image) - terrain.setBruteforce(True) # 设置LOD - - # 设置地形参数 - terrain.setBlockSize(32) # 设置块大小 - - # 生成地形 - terrain.generate() - - # 获取地形节点 - terrain_node = terrain.getRoot() - - if terrain_node.isEmpty(): - print("错误:无法生成有效的平面地形节点") + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") return None - node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain_node.setName(node_name) + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None - center_offset = (resolution - 1) / 2 - terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) + created_terrains = [] - # 将地形添加到场景中 - terrain_node.reparentTo(self.world.render) + try: + parent_item, parent_node = target_parents[0] + # 确保分辨率是2的幂次方加1 (如129, 257, 513等) + valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] + if resolution not in valid_resolutions: + # 找到最接近的有效分辨率 + closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) + print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") + resolution = closest_res - # 为地形添加碰撞体 - from panda3d.core import BitMask32 - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 - # 为地形的所有子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) + # 创建GeoMipTerrain对象 + terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) - # 添加材质 - self._applyTerrainMaterial(terrain_node) + # 创建一个平面高度图,尺寸必须是2^n+1 + height_image = PNMImage(resolution, resolution) + height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) - # 保存地形信息(包括高度图) - terrain_info = { - 'terrain': terrain, - 'node': terrain_node, - 'heightmap': None, - 'heightfield': height_image, # 保存高度图 - 'scale': (size[0], size[1], 50), - 'name': node_name - } + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) + terrain.setBruteforce(True) # 设置LOD - self.terrains.append(terrain_info) + # 设置地形参数 + terrain.setBlockSize(32) # 设置块大小 - # 更新场景树(再次检查节点是否有效) - if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, - 'updateSceneTree'): - try: - self.world.scene_manager.updateSceneTree() - except Exception as e: - print(f"警告: 更新场景树时出错: {e}") + # 生成地形 + terrain.generate() - print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}") - return terrain_info + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的平面地形节点") + return None + + terrain_node.setTag("is_scene_element", "1") + terrain_node.setTag("tree_item_type", "TERRAIN_NODE") + + node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain_node.setName(node_name) + + center_offset = (resolution - 1) / 2 + terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) + + # 将地形添加到场景中 + terrain_node.reparentTo(parent_node) + + # 为地形添加碰撞体 + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + # 保存地形信息(包括高度图) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': None, + 'heightfield': height_image, # 保存高度图 + 'scale': (size[0], size[1], 50), + 'name': node_name + } + + self.terrains.append(terrain_info) + + print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: + created_terrains.append((terrain_node, qt_item)) + else: + created_terrains.append((terrain_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}") + return None + + # 处理创建结果 + if not created_terrains: + print("❌ 没有成功创建任何平面地形") + return None + + # 选中最后创建的光源 + if created_terrains: + last_light_np, last_qt_item = created_terrains[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形") + + # 返回值处理 + if len(created_terrains) == 1: + return created_terrains[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 except Exception as e: - print(f"创建平面地形时出错: {e}") + print(f"❌ 创建平面地形过程失败: {str(e)}") import traceback traceback.print_exc() return None @@ -390,7 +484,6 @@ class TerrainManager: print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}") print(f"✓ 重新设置了地形碰撞体") - return modified except Exception as e: @@ -405,14 +498,17 @@ class TerrainManager: # 从场景中移除地形节点 terrain_node = terrain_info['node'] if terrain_node and not terrain_node.isEmpty(): - terrain_node.removeNode() + tree_widget = self._get_tree_widget() + if tree_widget: + tree_widget.delete_items(tree_widget.selectedItems()) + # terrain_node.removeNode() + # + # # 从列表中移除 + # self.terrains.remove(terrain_info) - # 从列表中移除 - self.terrains.remove(terrain_info) - - # 更新场景树 - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - self.world.scene_manager.updateSceneTree() + # # 更新场景树 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}") @@ -461,6 +557,7 @@ class TerrainManager: if terrain_info in self.terrains: terrain_node = terrain_info['node'] if terrain_node and os.path.exists(texture_path): + texture_path = util.normalize_model_path(texture_path) # 加载纹理 texture = self.world.loader.loadTexture(texture_path) if texture: @@ -476,3 +573,88 @@ class TerrainManager: except Exception as e: print(f"应用地形纹理时出错: {e}") return False + + def saveTerrainData(self, terrain_info, filename): + """保存地形数据到文件""" + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + # 保存高度图到文件 + if heightfield: + # 创建保存路径 + terrain_dir = os.path.join(os.path.dirname(filename), "terrains") + if not os.path.exists(terrain_dir): + os.makedirs(terrain_dir) + + # 生成唯一的地形文件名 + terrain_filename = f"terrain_{terrain_info.get('name', 'unnamed')}_{int(time.time())}.png" + terrain_path = os.path.join(terrain_dir, terrain_filename) + + # 保存高度图 + heightfield.write(Filename.fromOsSpecific(terrain_path)) + + # 保存地形信息到标签 + terrain_node.setTag("terrain_heightmap_path", terrain_path) + terrain_node.setTag("terrain_scale", str(terrain_info['scale'])) + + print(f"✓ 地形数据已保存: {terrain_path}") + return terrain_path + return None + except Exception as e: + print(f"保存地形数据时出错: {e}") + return None + + def _recreateTerrain(self, terrain_node): + """重新创建地形""" + try: + print(f"重新创建地形: {terrain_node.getName()}") + + # 获取保存的地形信息 + heightmap_path = terrain_node.getTag("terrain_heightmap_path") if terrain_node.hasTag( + "terrain_heightmap_path") else None + scale_str = terrain_node.getTag("terrain_scale") if terrain_node.hasTag("terrain_scale") else "(1, 1, 1)" + + # 解析缩放信息 + try: + scale_str = scale_str.strip("()") + scale_values = [float(x.strip()) for x in scale_str.split(",")] + scale = (scale_values[0], scale_values[1], scale_values[2]) if len(scale_values) >= 3 else (1, 1, 1) + except: + scale = (1, 1, 1) + + # 恢复位置信息 + if terrain_node.hasTag("transform_pos"): + try: + pos_str = terrain_node.getTag("transform_pos") + pos_str = pos_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') + pos_values = [float(x.strip()) for x in pos_str.split(',')] + if len(pos_values) >= 3: + terrain_node.setPos(pos_values[0], pos_values[1], pos_values[2]) + except Exception as e: + print(f"恢复地形位置失败: {e}") + + # 如果有高度图路径,重新创建地形 + if heightmap_path and os.path.exists(heightmap_path): + # 使用现有方法重新创建地形 + new_terrain = self.createTerrainFromHeightMap(heightmap_path, scale) + if new_terrain: + # 删除旧的地形节点 + if not terrain_node.isEmpty(): + terrain_node.removeNode() + return new_terrain + + # 如果没有高度图或创建失败,创建一个平面地形作为替代 + print("使用平面地形作为替代") + new_terrain = self.createFlatTerrain(size=(scale[0], scale[1]), resolution=129) + if new_terrain: + # 删除旧的地形节点 + if not terrain_node.isEmpty(): + terrain_node.removeNode() + return new_terrain + + return terrain_node + except Exception as e: + print(f"重新创建地形失败: {e}") + return terrain_node + diff --git a/core/tool_manager.py b/core/tool_manager.py index 4f5f4cdf..a0906215 100644 --- a/core/tool_manager.py +++ b/core/tool_manager.py @@ -5,6 +5,7 @@ class ToolManager: """初始化工具管理器""" self.world = world self.currentTool = "选择" # 默认工具为选择工具 + print(f"当前工具: {self.currentTool}") def setCurrentTool(self, tool): """设置当前工具""" diff --git a/core/world.py b/core/world.py index 4860ad50..0f4ec28a 100644 --- a/core/world.py +++ b/core/world.py @@ -288,6 +288,8 @@ class CoreWorld(Panda3DWorld): self.cam.setPos(0, -50, 20) self.cam.lookAt(0, 0, 0) self.camLens.setFov(80) + self.cam.setTag("is_scene_element", "1") + self.cam.setTag("tree_item_type", "CAMERA_NODE") print("✓ 相机设置完成") def _setupLighting(self): @@ -319,18 +321,61 @@ class CoreWorld(Panda3DWorld): # 创建地板节点 self.ground = self.render.attachNewNode(cm.generate()) self.ground.setP(-90) - self.ground.setZ(-0.1) + self.ground.setZ(-1.0) self.ground.setColor(0.8, 0.8, 0.8, 1) + # self.ground.setTag("is_scene_element", "1") + # self.ground.setTag("tree_item_type", "SCENE_NODE") # 创建支持贴图的材质 mat = Material() mat.setName("GroundMaterial") color = LColor(1, 1, 1, 0.8) mat.set_base_color(color) - mat.set_roughness(0.5) # 设置合适的初始粗糙度 + mat.set_roughness(1) # 设置合适的初始粗糙度 mat.set_metallic(0.5) # 设置较低的初始金属性 self.ground.set_material(mat) + #创建第二个相同的地面,位置稍有偏移 + self.ground2 = self.render.attachNewNode(cm.generate()) + self.ground2.setH(-90) + self.ground2.setZ(-1.0) + self.ground2.setX(50) # 在X轴方向偏移 + self.ground2.setZ(49) # 在X轴方向偏移 + self.ground2.setColor(0.8, 0.8, 0.8, 1) + self.ground2.set_material(mat) + + # 创建第三个相同的地面,位置在另一个方向 + self.ground3 = self.render.attachNewNode(cm.generate()) + self.ground3.setH(90) + self.ground3.setZ(-1.0) + self.ground3.setX(-50) # 在X轴负方向偏移 + self.ground3.setZ(49) # 在X轴负方向偏移 + self.ground3.setColor(0.8, 0.8, 0.8, 1) + self.ground3.set_material(mat) + + self.ground4 = self.render.attachNewNode(cm.generate()) + # self.ground3.setR(90) + self.ground4.setZ(-1.0) + self.ground4.setY(50) # 在X轴负方向偏移 + self.ground4.setZ(49) # 在X轴负方向偏移 + self.ground4.setColor(0.8, 0.8, 0.8, 1) + self.ground4.set_material(mat) + + self.ground5 = self.render.attachNewNode(cm.generate()) + self.ground5.setP(180) + self.ground5.setZ(-1) + self.ground5.setY(-50) # 在X轴负方向偏移 + self.ground5.setZ(49) # 在X轴负方向偏移 + self.ground5.setColor(0.8, 0.8, 0.8, 1) + self.ground5.set_material(mat) + + self.ground6 = self.render.attachNewNode(cm.generate()) + self.ground6.setP(90) + self.ground6.setZ(-1) + self.ground6.setZ(99) # 在X轴负方向偏移 + self.ground6.setColor(0.8, 0.8, 0.8, 1) + self.ground6.set_material(mat) + # 应用默认PBR效果,确保支持贴图 try: if hasattr(self, 'render_pipeline') and self.render_pipeline: @@ -347,6 +392,72 @@ class CoreWorld(Panda3DWorld): }, 50 ) + # 为其他两个地面也应用相同的效果 + self.render_pipeline.set_effect( + self.ground2, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground3, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground4, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground5, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground6, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) print("✓ 地板PBR效果已应用") else: print("⚠️ RenderPipeline未初始化,地板将使用基础渲染") diff --git a/demo/test_gizmo_drag.py b/demo/test_gizmo_drag.py index 81718bc3..4a1b39d2 100644 --- a/demo/test_gizmo_drag.py +++ b/demo/test_gizmo_drag.py @@ -359,7 +359,7 @@ class GizmoDragTestWorld(Panda3DWorld): distance = distance_to_line( (mouseX, mouseY), center_screen, axis_screen ) - print(f"{axis_label}距离: {distance:.2f}") + #print(f"{axis_label}距离: {distance:.2f}") if distance < click_threshold: print(f"✓ 点击了{axis_label}") diff --git a/gui/gui_manager.py b/gui/gui_manager.py index babc94c2..cfd10728 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -24,78 +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)}") - 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元素管理系统类""" @@ -124,7 +126,7 @@ class GUIManager: # ==================== GUI元素创建方法 ==================== - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=(0.1,0.1,0.1)): """创建2D GUI按钮 - 支持多选创建和GUI父子关系,优化版本""" try: from direct.gui.DirectGui import DirectButton @@ -180,17 +182,35 @@ class GUIManager: text_font=self.world.getChineseFont() if self.world.getChineseFont() else None, rolloverSound=None, clickSound=None, - parent=parent_gui_node # 设置GUI父节点 + parent=parent_gui_node ) + if not hasattr(button,'_tags'): + button._tags = {} + + # button._tags["gui_type"] = "button" + # button._tags["gui_id"] = f"button_{len(self.gui_elements)}" + # button._tags["gui_text"] = text + # button._tags["is_gui_element"] = "1" + # button._tags["is_scene_element"] = "1" + # button._tags["saved_gui_type"] = "button" + # button._tags["gui_element_type"] = "button" + # button._tags["created_by_user"] = "1" + # button._tags["name"] = button_name + # button.setName(button_name) + # 设置节点标签 button.setTag("gui_type", "button") button.setTag("gui_id", f"button_{len(self.gui_elements)}") button.setTag("gui_text", text) button.setTag("is_gui_element", "1") - button.setTag("is_scene_element", "1") + button.setTag("is_scene_element", "1") # 确保这个标签被设置 + button.setTag("tree_item_type", "GUI_BUTTON") + button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性 + button.setTag("gui_element_type", "button") button.setTag("created_by_user", "1") button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + button.setTag("name", button_name) button.setName(button_name) # 如果有GUI父节点,建立引用关系 @@ -200,6 +220,7 @@ class GUIManager: # 添加到GUI元素列表 self.gui_elements.append(button) + button.reparentTo(self.world.aspect2d) print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}") @@ -221,11 +242,11 @@ class GUIManager: return None # 选中最后创建的按钮并更新场景树 - if created_buttons: - last_button, last_qt_item = created_buttons[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - tree_widget.update_selection_and_properties(last_button, last_qt_item) + # if created_buttons: + # last_button, last_qt_item = created_buttons[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_button, last_qt_item) print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮") @@ -301,9 +322,11 @@ class GUIManager: label.setTag("gui_id", f"label_{len(self.gui_elements)}") label.setTag("gui_text", text) label.setTag("is_gui_element", "1") + label.setTag("tree_item_type", "GUI_LABEL") label.setTag("is_scene_element", "1") label.setTag("created_by_user", "1") label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + label.setTag("name",label_name) label.setName(label_name) # 如果有GUI父节点,建立引用关系 @@ -334,11 +357,11 @@ class GUIManager: return None # 选中最后创建的标签并更新场景树 - if created_labels: - last_label, last_qt_item = created_labels[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - tree_widget.update_selection_and_properties(last_label, last_qt_item) + # if created_labels: + # last_label, last_qt_item = created_labels[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_label, last_qt_item) print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签") @@ -394,6 +417,10 @@ class GUIManager: parent_gui_node = None print(f"📎 挂载到3D父节点: {parent_item.text(0)}") + font = None + if hasattr(self.world,'getChineseFont'): + font = self.world.getChineseFont() + entry = DirectEntry( text="", pos=gui_pos, @@ -404,7 +431,9 @@ class GUIManager: numLines=1, width=12, focus=0, - parent=parent_gui_node # 设置GUI父节点 + parent=parent_gui_node, # 设置GUI父节点 + text_font = font, + frameSize=(-0.1,0.1,-0.05,0.05) ) # 设置节点标签 @@ -412,9 +441,11 @@ class GUIManager: entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") entry.setTag("gui_placeholder", placeholder) entry.setTag("is_gui_element", "1") + entry.setTag("tree_item_type", "GUI_ENTRY") entry.setTag("is_scene_element", "1") entry.setTag("created_by_user", "1") entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + entry.setTag("name",entry_name) entry.setName(entry_name) # 如果有GUI父节点,建立引用关系 @@ -445,11 +476,11 @@ class GUIManager: return None # 选中最后创建的输入框并更新场景树 - if created_entries: - last_entry, last_qt_item = created_entries[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - tree_widget.update_selection_and_properties(last_entry, last_qt_item) + # if created_entries: + # last_entry, last_qt_item = created_entries[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_entry, last_qt_item) print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框") @@ -509,7 +540,11 @@ class GUIManager: cm = CardMaker('gui-2d-image') cm.setFrame(-size, size, -size, size) - image_node = self.world.aspect2d.attachNewNode(cm.generate()) + # image_node = self.world.aspect2d.attachNewNode(cm.generate()) + if parent_gui_node: + image_node = parent_gui_node.attachNewNode(cm.generate()) + else: + image_node = self.world.aspect2d.attachNewNode(cm.generate()) image_node.setPos(gui_pos) image_node.setBin('fixed', 0) image_node.setDepthWrite(False) @@ -522,6 +557,7 @@ class GUIManager: # 如果提供了图像路径,则加载纹理 if image_path: try: + image_node.setTag("image_path", image_path) texture = self.world.loader.loadTexture(image_path) if texture: image_node.setTexture(texture, 1) @@ -541,8 +577,10 @@ class GUIManager: image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}") image_node.setTag("is_gui_element", "1") image_node.setTag("is_scene_element", "1") + image_node.setTag("tree_item_type", "GUI_IMAGE") image_node.setTag("created_by_user", "1") image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + image_node.setTag("name",image_name) image_node.setName(image_name) # 如果有GUI父节点,建立引用关系 @@ -573,11 +611,11 @@ class GUIManager: return None # 选中最后创建的按钮并更新场景树 - if created_2dimage: - last_button, last_qt_item = created_2dimage[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - tree_widget.update_selection_and_properties(last_button, last_qt_item) + # if created_2dimage: + # last_button, last_qt_item = created_2dimage[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_button, last_qt_item) print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮") @@ -692,7 +730,9 @@ class GUIManager: textNodePath.setTag("gui_text", text) textNodePath.setTag("is_gui_element", "1") textNodePath.setTag("is_scene_element", "1") + textNodePath.setTag("tree_item_type", "GUI_3DTEXT") textNodePath.setTag("created_by_user", "1") + textNodePath.setTag("name", text_name) # 添加到GUI元素列表 self.gui_elements.append(textNodePath) @@ -717,11 +757,11 @@ class GUIManager: return None # 选中最后创建的文本并更新场景树 - if created_texts: - last_text, last_qt_item = created_texts[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - tree_widget.update_selection_and_properties(last_text, last_qt_item) + # if created_texts: + # last_text, last_qt_item = created_texts[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_text, last_qt_item) print(f"🎉 总共创建了 {len(created_texts)} 个3D文本") @@ -740,6 +780,7 @@ class GUIManager: def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): """创建3D空间图片""" try: + from panda3d.core import TextNode from PyQt5.QtCore import Qt @@ -779,7 +820,8 @@ class GUIManager: 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 = self.world.render.attachNewNode(cm.generate()) + image_node = parent_node.attachNewNode(cm.generate()) image_node.setPos(*pos) # 为3D图像创建独立的材质 @@ -824,10 +866,12 @@ class GUIManager: image_node.setTag("gui_type", "3d_image") image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") if image_path: - image_node.setTag("gui_image_path", image_path) + image_node.setTag("image_path", image_path) image_node.setTag("is_gui_element", "1") image_node.setTag("is_scene_element", "1") + image_node.setTag("tree_item_type", "GUI_3DIMAGE") image_node.setTag("created_by_user", "1") + image_node.setTag("name",image_name) # 添加到GUI元素列表 self.gui_elements.append(image_node) @@ -852,11 +896,11 @@ class GUIManager: return None # 选中最后创建的文本并更新场景树 - if created_3dimage: - last_image, last_qt_item = created_3dimage[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - tree_widget.update_selection_and_properties(last_image, last_qt_item) + # if created_3dimage: + # last_image, last_qt_item = created_3dimage[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_image, last_qt_item) print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本") @@ -872,7 +916,7 @@ class GUIManager: traceback.print_exc() return None - def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): + def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None): """创建3D视频播放屏幕 - 添加占位符纹理支持""" try: from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage @@ -895,7 +939,7 @@ class GUIManager: size = float(size) except (ValueError, TypeError): print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}") - size = 0.2 + size = 0.2*5 print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}") @@ -944,7 +988,9 @@ class GUIManager: video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}") video_screen.setTag("is_gui_element", "1") video_screen.setTag("is_scene_element", "1") + video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") + video_screen.setTag("name",screen_name) # 设置视频路径标签 if video_path and os.path.exists(video_path): @@ -1042,12 +1088,12 @@ class GUIManager: return None # 选中最后创建的视频屏幕 - if created_videoscreens: - last_screen_np, last_qt_item = created_videoscreens[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + # if created_videoscreens: + # last_screen_np, last_qt_item = created_videoscreens[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕") @@ -1125,6 +1171,7 @@ class GUIManager: # 检查是否有播放方法 if hasattr(movie_texture, 'play'): try: + self.loadVideoFile(video_screen, video_screen.getTag("video_path")) movie_texture.play() print(f"▶️ 继续播放视频: {video_screen.getName()}") return True @@ -1135,9 +1182,7 @@ class GUIManager: print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}") return False else: - print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") - self._debugVideoScreenTextures(video_screen) - return False + self.loadVideoFile(video_screen,video_screen.getTag("video_path")) except Exception as e: print(f"❌ 播放视频失败: {e}") import traceback @@ -1264,7 +1309,6 @@ class GUIManager: def loadVideoFile(self, video_screen, video_path): """为视频屏幕加载新的视频文件""" try: - from panda3d.core import Texture, TextureStage import os if not os.path.exists(video_path): @@ -1422,8 +1466,6 @@ class GUIManager: print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}") size = 0.2 - print(f"📺 开始创建2D视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}") - # 获取树形控件 tree_widget = self._get_tree_widget() if not tree_widget: @@ -1450,7 +1492,7 @@ class GUIManager: frameColor=(1, 1, 1, 1), # 默认背景色 pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标 parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d, - suppressMouse=True + suppressMouse=True, ) video_screen.setName(screen_name) @@ -1458,19 +1500,18 @@ class GUIManager: # 设置透明度支持 video_screen.setTransparency(TransparencyAttrib.MAlpha) - # 设置2D视频屏幕特有的标签 + #设置2D视频屏幕特有的标签 video_screen.setTag("gui_type", "2d_video_screen") video_screen.setTag("gui_id", f"2d_video_screen_{len(self.gui_elements)}") video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}") video_screen.setTag("is_gui_element", "1") video_screen.setTag("is_scene_element", "1") + video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") + video_screen.setTag("name",screen_name) - # 设置视频路径标签 - if video_path and os.path.exists(video_path): - video_screen.setTag("video_path", video_path) - else: - video_screen.setTag("video_path", "") + video_screen.setTag("video_path", video_path if video_path else "") + print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}") # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") @@ -1543,12 +1584,12 @@ class GUIManager: return None # 选中最后创建的视频屏幕 - if created_videoscreens: - last_screen_np, last_qt_item = created_videoscreens[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + # if created_videoscreens: + # last_screen_np, last_qt_item = created_videoscreens[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕") @@ -1569,7 +1610,11 @@ class GUIManager: try: import os - if not os.path.exists(video_path): + video_screen.setTag("video_path",video_path) + path = video_screen.getTag("video_path") + print(f"🔧 更新2D视频屏幕标签 - video_path: {path}") + + if not video_path or not os.path.exists(video_path): print(f"❌ 2D视频文件不存在: {video_path}") return False @@ -1581,7 +1626,6 @@ class GUIManager: # 保存视频纹理引用 video_screen.setPythonTag("movie_texture", movie_texture) - video_screen.setTag("video_path", video_path) print(f"✅ 成功加载新2D视频: {video_path}") return True @@ -1776,6 +1820,7 @@ class GUIManager: # 设置标签以便识别和管理 sphere_np.setTag("gui_type", "spherical_video") sphere_np.setTag("is_gui_element", "1") + sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO") sphere_np.setTag("video_path", video_path or "") sphere_np.setTag("original_radius", str(radius)) @@ -1808,12 +1853,12 @@ class GUIManager: return None # 选中最后创建的球形视频 - if created_spherical_videos: - last_sphere_np, last_qt_item = created_spherical_videos[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item) + # if created_spherical_videos: + # last_sphere_np, last_qt_item = created_spherical_videos[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item) print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频") @@ -1988,6 +2033,7 @@ class GUIManager: virtual_screen.setTag("gui_text", text) virtual_screen.setTag("is_gui_element", "1") virtual_screen.setTag("is_scene_element", "1") + virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen") virtual_screen.setTag("created_by_user", "1") # 添加到GUI元素列表 @@ -2013,12 +2059,12 @@ class GUIManager: return None # 选中最后创建的虚拟屏幕 - if created_screens: - last_screen_np, last_qt_item = created_screens[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + # if created_screens: + # last_screen_np, last_qt_item = created_screens[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕") @@ -2070,7 +2116,9 @@ class GUIManager: pass return None + # 暂无滑块功能 def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3): + pass """创建2D GUI滑块""" from direct.gui.DirectGui import DirectSlider @@ -2105,19 +2153,22 @@ class GUIManager: """删除GUI元素""" try: if gui_element in self.gui_elements: - # 移除GUI元素 - if hasattr(gui_element, 'removeNode'): - gui_element.removeNode() - elif hasattr(gui_element, 'destroy'): - gui_element.destroy() - - # 从列表中移除 - self.gui_elements.remove(gui_element) + # # 移除GUI元素 + # if hasattr(gui_element, 'removeNode'): + # gui_element.removeNode() + # elif hasattr(gui_element, 'destroy'): + # gui_element.destroy() + # + # # 从列表中移除 + # self.gui_elements.remove(gui_element) # 更新场景树 # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() + tree_widget = self._get_tree_widget() + if tree_widget: + tree_widget.delete_items(tree_widget.selectedItems()) + # if hasattr(self.world, 'updateSceneTree'): + # self.world.updateSceneTree() print(f"删除GUI元素: {gui_element}") return True @@ -2910,7 +2961,7 @@ class GUIManager: heightSpinBox.valueChanged.connect( lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) transform_layout.addWidget(heightSpinBox, 4, 3) - + else: # 3D GUI组件使用世界坐标 transform_layout.addWidget(QLabel("位置"), 0, 0) @@ -2990,11 +3041,11 @@ class GUIManager: transform_layout.addWidget(scale_z, 3, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) - + # 缩放属性 if hasattr(gui_element, 'getScale'): scale = gui_element.getScale() - + scaleSpinBox = QDoubleSpinBox() scaleSpinBox.setRange(0.01, 100) scaleSpinBox.setSingleStep(0.1) @@ -3207,7 +3258,7 @@ class GUIManager: try: gui_type = gui_element.getTag("gui_type") - if gui_type in ["3d_text", "3d_image", "video_screen","info_panel"]: + if gui_type in ["3d_text", "3d_image", "video_screen","info_panel","info_panel_3d"]: current_pos = gui_element.getPos() if axis == "x": @@ -3220,7 +3271,7 @@ class GUIManager: return False gui_element.setPos(*new_pos) - print(f"✓ 更新3D GUI元素位置: {axis}={value}") + #print(f"✓ 更新3D GUI元素位置: {axis}={value}") return True else: print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}") @@ -3241,7 +3292,7 @@ class GUIManager: if value == 0: value = 0.01 - if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel"]: + if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel","info_panel_3d"]: # 3D元素处理 if axis == "x": new_scale = (value, current_scale.getY(), current_scale.getZ()) diff --git a/icons/logo.png b/icons/logo.png new file mode 100644 index 00000000..361b35c7 Binary files /dev/null and b/icons/logo.png differ diff --git a/main.py b/main.py index 137b8566..bd204899 100644 --- a/main.py +++ b/main.py @@ -22,6 +22,7 @@ from core.script_system import ScriptManager from core.vr_manager import VRManager from core.vr_input_handler import VRInputHandler from core.alvr_streamer import ALVRStreamer +from core.patrol_system import PatrolSystem from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager @@ -55,7 +56,16 @@ class MyWorld(CoreWorld): # 初始化选择和变换系统 self.selection = SelectionSystem(self) - + + # 绑定F键用于聚焦选中节点 + self.accept("f", self.onFocusKeyPressed) + self.accept("F", self.onFocusKeyPressed) # 大写F + + #初始化巡检系统 + self.patrol_system = PatrolSystem(self) + self.accept("p",self.onPatrolKeyPressed) + self.accept("P",self.onPatrolKeyPressed) + # 初始化事件处理系统 self.event_handler = EventHandler(self) @@ -225,7 +235,7 @@ class MyWorld(CoreWorld): """创建2D GUI文本输入框""" return self.gui_manager.createGUIEntry(pos, placeholder, size) - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): """创建3D空间文本""" return self.gui_manager.createGUI3DText(pos, text, size) @@ -233,9 +243,9 @@ class MyWorld(CoreWorld): """创建3D图片""" return self.gui_manager.createGUI3DImage(pos,text,size) - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1): """创建2D GUI图片""" - return self.gui_manager.createGUI2DImage(pos, image_path, size) + return self.gui_manager.createGUI2DImage(pos, image_path, size*0.2) def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None): """创建视频屏幕""" @@ -492,10 +502,6 @@ class MyWorld(CoreWorld): """异步导入模型""" return self.scene_manager.importModelAsync(filepath) - def loadAnimatedModel(self, model_path, anims=None): - """加载带动画的模型""" - return self.scene_manager.loadAnimatedModel(model_path, anims) - # 材质和几何体处理方法 - 代理到scene_manager def processMaterials(self, model): """处理模型材质""" @@ -814,6 +820,97 @@ class MyWorld(CoreWorld): """获取碰撞统计""" return self.collision_manager.getCollisionStatistics() + def setupKeyboardEvents(self): + """设置键盘事件""" + try: + # 绑定 F 键用于聚焦选中节点 + self.accept("f", self.onFocusKeyPressed) + self.accept("F", self.onFocusKeyPressed) # 大写F + + print("✓ 键盘事件绑定完成") + + except Exception as e: + print(f"设置键盘事件失败: {e}") + + def onFocusKeyPressed(self): + """处理 F 键按下事件""" + try: + #print("检测到 F 键按下") + + # 检查是否有选中的节点 + if hasattr(self, 'selection') and self.selection.selectedNode: + #print(f"当前选中节点: {self.selection.selectedNode.getName()}") + # 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本) + # self.selection.focusCameraOnSelectedNode() # 无动画版本 + self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本 + else: + print("当前没有选中任何节点") + + except Exception as e: + print(f"处理 F 键事件失败: {e}") + + def onPatrolKeyPressed(self): + """处理 P 键按下事件 - 控制巡检系统""" + try: + print("检测到 P 键按下") + + if not self.patrol_system.is_patrolling: + # 如果巡检系统没有点,创建默认巡检路线 + if not self.patrol_system.patrol_points: + self.createDefaultPatrolRoute() + + # 开始巡检 + if self.patrol_system.start_patrol(): + print("✓ 巡检已开始") + else: + print("✗ 巡检启动失败") + else: + # 停止巡检 + if self.patrol_system.stop_patrol(): + print("✓ 巡检已停止") + else: + print("✗ 巡检停止失败") + + except Exception as e: + print(f"处理 P 键事件失败: {e}") + + def createDefaultPatrolRoute(self): + """创建默认巡检路线(使用自动朝向)""" + try: + # 清空现有巡检点 + self.patrol_system.clear_patrol_points() + + # 添加巡检点,使用None表示朝向下一个点 + self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5) + + # 最后一个点可以指定特定的朝向,或者也设为None继续循环 + self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5) + + print("✓ 默认自动朝向巡检路线已创建") + self.patrol_system.list_patrol_points() + + 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模块的对应功能 @@ -830,6 +927,7 @@ def createNewProject(parent_window): def saveProject(appw): """保存项目 - 代理到project_manager""" + world = appw.centralWidget().world return world.project_manager.saveProject(appw) @@ -838,19 +936,26 @@ def openProject(appw): world = appw.centralWidget().world return world.project_manager.openProject(appw) +def openProjectForPath(project_path, appw): + """打开项目 - 代理到project_manager""" + world = appw.centralWidget().world + return world.project_manager.openProjectForPath(project_path, appw) + def buildPackage(appw): """打包项目 - 代理到project_manager""" world = appw.centralWidget().world return world.project_manager.buildPackage(appw) - -if __name__ == "__main__": +def run(args = None): world = MyWorld() # 使用新的UI模块创建主窗口 from ui.main_window import setup_main_window - - app, main_window = setup_main_window(world) - + print(f'Path is {args}') + app, main_window = setup_main_window(world, args) + # 启动应用程序 - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/project/project_manager.py b/project/project_manager.py index e54a9995..31b2df70 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -9,7 +9,6 @@ import os import sys import json -import re import datetime import subprocess import shutil @@ -47,7 +46,9 @@ class ProjectManager: project_path = dialog.projectPath project_name = dialog.projectName - full_project_path = os.path.join(project_path, project_name) + # full_project_path = os.path.join(project_path, project_name) + full_project_path = os.path.normpath(os.path.join(project_path, project_name)) + print(f"full_project_path: {full_project_path}") try: # 创建项目文件夹结构 @@ -72,13 +73,13 @@ class ProjectManager: json.dump(project_config, f, ensure_ascii=False, indent=4) print(f"项目配置文件已创建: {config_file}") - + # 清空当前场景 self._clearCurrentScene() - + # 自动保存初始场景 scene_file = os.path.join(scenes_path, "scene.bam") - if self.world.scene_manager.saveScene(scene_file): + if self.world.scene_manager.saveScene(scene_file, project_path): # 更新配置文件中的场景路径 project_config["scene_file"] = os.path.relpath(scene_file, full_project_path) with open(config_file, "w", encoding="utf-8") as f: @@ -122,57 +123,157 @@ class ProjectManager: if not project_path: return False - + # 检查是否是有效的项目文件夹 config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!") return False - + # 读取项目配置 with open(config_file, "r", encoding="utf-8") as f: project_config = json.load(f) - + # 检查场景文件 scene_file = os.path.join(project_path, "scenes", "scene.bam") - if not os.path.exists(scene_file): - QMessageBox.warning(parent_window, "警告", "没有找到场景文件!") - return False - - # 加载场景 - if self.world.scene_manager.loadScene(scene_file): - # 更新项目配置 - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - project_config["scene_file"] = os.path.relpath(scene_file, project_path) - - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - # 更新项目状态 - self.current_project_path = project_path - self.project_config = project_config - - # 保存当前项目路径到主窗口 - parent_window.current_project_path = project_path - - # 更新窗口标题 - project_name = os.path.basename(project_path) - self.updateWindowTitle(parent_window, project_name) - - # 更新文件浏览器 - if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): - parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) - - QMessageBox.information(parent_window, "成功", "项目加载成功!") - return True - else: - QMessageBox.warning(parent_window, "错误", "加载场景失败!") - return False - + if os.path.exists(scene_file): + # 加载场景 + if self.world.scene_manager.loadScene(scene_file): + # 更新项目配置 + project_config["scene_file"] = os.path.relpath(scene_file, project_path) + + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + # 更新项目状态 + self.current_project_path = project_path + self.project_config = project_config + + # 保存当前项目路径到主窗口 + parent_window.current_project_path = project_path + + # 更新窗口标题 + project_name = os.path.basename(project_path) + self.updateWindowTitle(parent_window, project_name) + + # 更新文件浏览器 + if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): + parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) + + QMessageBox.information(parent_window, "成功", "项目加载成功!") + return True + # 检查场景文件 + # scene_file = os.path.join(project_path, "scenes", "scene.bam") + # if not os.path.exists(scene_file): + # QMessageBox.warning(parent_window, "警告", "没有找到场景文件!") + # return False + # + # # 加载场景 + # if self.world.scene_manager.loadScene(scene_file): + # # 更新项目配置 + # project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # project_config["scene_file"] = os.path.relpath(scene_file, project_path) + # + # with open(config_file, "w", encoding="utf-8") as f: + # json.dump(project_config, f, ensure_ascii=False, indent=4) + # + # # 更新项目状态 + # self.current_project_path = project_path + # self.project_config = project_config + # + # # 保存当前项目路径到主窗口 + # parent_window.current_project_path = project_path + # + # # 更新窗口标题 + # project_name = os.path.basename(project_path) + # self.updateWindowTitle(parent_window, project_name) + # + # # 更新文件浏览器 + # if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): + # parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) + # + # QMessageBox.information(parent_window, "成功", "项目加载成功!") + # return True + # else: + # QMessageBox.warning(parent_window, "错误", "加载场景失败!") + # return False + except Exception as e: QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}") return False - + + def openProjectForPath(self, project_path, parent_window=None): + """通过路径打开项目 + + Args: + project_path: 项目路径 + parent_window: 父窗口对象(可选) + """ + try: + if not project_path: + return False + # 检查是否是有效的项目文件夹 + config_file = os.path.join(project_path, "project.json") + if not os.path.exists(config_file): + if parent_window: + QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}") + else: + print("警告: 选择的不是有效的项目文件夹!") + return False + + # 读取项目配置 + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 检查场景文件 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 加载场景 + if self.world.scene_manager.loadScene(scene_file): + # 更新项目配置 + project_config["scene_file"] = os.path.relpath(scene_file, project_path) + + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + # 更新项目状态 + self.current_project_path = project_path + self.project_config = project_config + + # 如果有父窗口,更新相关UI元素 + if parent_window: + # 保存当前项目路径到主窗口 + parent_window.current_project_path = project_path + + # 更新窗口标题 + project_name = os.path.basename(project_path) + self.updateWindowTitle(parent_window, project_name) + + # 更新文件浏览器 + if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): + parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) + + QMessageBox.information(parent_window, "成功", "项目加载成功!") + + print(f"项目 '{project_path}' 加载成功!") + return True + else: + if parent_window: + QMessageBox.warning(parent_window, "错误", "加载场景失败!") + else: + print("错误: 加载场景失败!") + return False + + except Exception as e: + error_msg = f"加载项目时发生错误:{str(e)}" + if parent_window: + QMessageBox.critical(parent_window, "错误", error_msg) + else: + print(error_msg) + return False + def saveProject(self, parent_window): """保存项目""" try: @@ -201,7 +302,7 @@ class ProjectManager: return False # 保存场景 - if self.world.scene_manager.saveScene(scene_file): + if self.world.scene_manager.saveScene(scene_file, project_path): # 更新项目配置文件 config_file = os.path.join(project_path, "project.json") if os.path.exists(config_file): diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 69ee1d49..1ad2d9ab 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -7,20 +7,24 @@ """ import os +import time from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeWidgetItem from panda3d.core import ( ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, - MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, - BitMask32, TransparencyAttrib,LColor + MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, + BitMask32, TransparencyAttrib, LColor, TransformState ) import json import aiohttp import asyncio +import inspect from pathlib import Path from panda3d.egg import EggData, EggVertexPool from direct.actor.Actor import Actor from QPanda3D.Panda3DWorld import get_render_pipeline +from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq from scene import util class CesiumIntegration: @@ -97,20 +101,29 @@ class SceneManager: normalize_scales: 是否标准化子节点缩放(推荐开启) auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持 """ + try: print(f"\n=== 开始导入模型: {filepath} ===") - print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") - print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") + #print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") + #print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") filepath = util.normalize_model_path(filepath) original_filepath = filepath + # 在加载前设置忽略未知属性 + from panda3d.core import ConfigVariableBool + ConfigVariableBool("model-cache-ignore-unknown-properties").setValue(True) + + # 清除可能存在的模型缓存 + from panda3d.core import ModelPool + ModelPool.releaseAllModels() + # 检查是否需要转换为GLB以获得更好的动画支持 if auto_convert_to_glb and self._shouldConvertToGLB(filepath): - print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") + #print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") converted_path = self._convertToGLBWithProgress(filepath) if converted_path: - print(f"✅ 转换成功: {converted_path}") + #print(f"✅ 转换成功: {converted_path}") filepath = converted_path # 显示成功消息 try: @@ -125,7 +138,7 @@ class SceneManager: # 总是重新加载模型以确保材质信息完整 # 不使用ModelPool缓存,避免材质信息丢失问题 - print("直接从文件加载模型...") + #print("直接从文件加载模型...") model = self.world.loader.loadModel(filepath) if not model: print("加载模型失败") @@ -133,10 +146,18 @@ class SceneManager: # 设置模型名称 model_name = os.path.basename(filepath) + # 确保名称有效 + if not model_name: + model_name = "imported_model" model.setName(model_name) - # 将模型添加到场景 model.reparentTo(self.world.render) + + # 设置模型名称 + model_name = os.path.basename(filepath) + model.setName(model_name) + + # 保存原始路径和转换后的路径 model.setTag("model_path", filepath) model.setTag("original_path", original_filepath) @@ -144,18 +165,31 @@ class SceneManager: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") - # 可选的单位转换(主要针对FBX) - if apply_unit_conversion and filepath.lower().endswith('.fbx'): - print("应用FBX单位转换(厘米到米)...") - self._applyUnitConversion(model, 0.01) + # 特殊处理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)") - # 智能缩放标准化(处理FBX子节点的大缩放值) - if normalize_scales and filepath.lower().endswith('.fbx'): - print("标准化FBX模型缩放层级...") - self._normalizeModelScales(model) + # # 可选的单位转换(主要针对FBX + # if apply_unit_conversion and filepath.lower().endswith('.fbx'): + # #print("应用FBX单位转换(厘米到米)...") + # self._applyUnitConversion(model, 0.01) + # + # # 智能缩放标准化(处理FBX子节点的大缩放值) + # if normalize_scales and filepath.lower().endswith('.fbx'): + # #print("标准化FBX模型缩放层级...") + # self._normalizeModelScales(model) # 调整模型位置到地面 - self._adjustModelToGround(model) + model.setPos(0,0,0) + #self._adjustModelToGround(model) # 创建并设置基础材质 print("\n=== 开始设置材质 ===") @@ -168,6 +202,8 @@ class SceneManager: # 添加文件标签用于保存/加载 model.setTag("file", model_name) model.setTag("is_model_root", "1") + model.setTag("is_scene_element", "1") + model.setTag("tree_item_type", "IMPORTED_MODEL_NODE") # 记录应用的处理选项 if apply_unit_conversion: @@ -179,7 +215,29 @@ class SceneManager: self.models.append(model) # 更新场景树 - self.updateSceneTree() + # 获取树形控件并添加到Qt树中 + tree_widget = self._get_tree_widget() + if tree_widget: + # 找到根节点项 + root_item = None + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: + root_item = item + break + + if root_item: + qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE") + if qt_item: + #tree_widget.setCurrentItem(qt_item) + # 更新选择和属性面板 + #tree_widget.update_selection_and_properties(model, qt_item) + print("✅ Qt树节点添加成功") + else: + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + else: + print("⚠️ 未找到根节点项,无法添加到Qt树") + #self.updateSceneTree() print(f"=== 模型导入成功: {model_name} ===\n") return model @@ -188,17 +246,158 @@ class SceneManager: print(f"导入模型失败: {str(e)}") return None + def _fixModelStructure(self, model): + """修复模型结构""" + try: + # 使用正确的方式查找动画相关节点 + character_nodes = model.findAllMatches("**/+Character") + anim_bundle_nodes = model.findAllMatches("**/+AnimBundleNode") + + if character_nodes.getNumPaths() > 0 or anim_bundle_nodes.getNumPaths() > 0: + print(f"检测到模型{model.getName()}包含角色相节点:") + if character_nodes.getNumPaths() > 0: + print(f"CharacterNode数量:{character_nodes.getNumPaths()}") + if anim_bundle_nodes.getNumPaths() > 0: + print(f"AnimBundleNode数量: {anim_bundle_nodes.getNumPaths()}") + + model.setTag("fixed_structure", "true") + return True + except Exception as e: + print(f"修复模型结构时出错: {e}") + return False + + def _validateAndFixAllTransforms(self, model): + """递归验证并修复模型中所有节点的变换矩阵""" + try: + fixed_count = 0 + + # 先处理根节点 + if not self._validateAndFixTransform(model): + fixed_count += 1 + + # 递归处理所有子节点 + def process_children(node, depth=0): + nonlocal fixed_count + for i in range(node.getNumChildren()): + try: + child = node.getChild(i) + if not self._validateAndFixTransform(child): + fixed_count += 1 + # 递归处理孙节点 + process_children(child, depth + 1) + except Exception as e: + print(f"处理子节点时出错 (深度 {depth}): {e}") + continue + + process_children(model) + + if fixed_count > 0: + print(f"共修复了 {fixed_count} 个节点的变换") + + return True + except Exception as e: + print(f"验证所有变换时出错: {e}") + return False + + def _validateAndFixTransform(self, node_path): + """验证并修复单个节点的变换矩阵""" + try: + node_name = node_path.getName() + + # 获取当前变换状态 + original_pos = node_path.getPos() + original_hpr = node_path.getHpr() + original_scale = node_path.getScale() + + # 检查位置是否包含无效值 + if not original_pos.isFinite(): + print(f"警告: 节点 {node_name} 位置包含无效值 {original_pos},重置为 (0,0,0)") + node_path.setPos(0, 0, 0) + return False + + # 检查旋转是否包含无效值 + if not original_hpr.isFinite(): + print(f"警告: 节点 {node_name} 旋转包含无效值 {original_hpr},重置为 (0,0,0)") + node_path.setHpr(0, 0, 0) + return False + + # 检查缩放是否包含无效值或为零 + if not original_scale.isFinite(): + print(f"警告: 节点 {node_name} 缩放包含无效值 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + # 检查缩放是否为零或接近零 + min_scale = 1e-10 + if (abs(original_scale.x) < min_scale or + abs(original_scale.y) < min_scale or + abs(original_scale.z) < min_scale): + print(f"警告: 节点 {node_name} 缩放接近零 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + # 检查缩放是否过大(防止异常大的缩放) + max_scale = 1000000 # 100万倍作为上限 + if (abs(original_scale.x) > max_scale or + abs(original_scale.y) > max_scale or + abs(original_scale.z) > max_scale): + print(f"警告: 节点 {node_name} 缩放过异常 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + return True + + except Exception as e: + print(f"验证/修复节点 {node_path.getName()} 变换时出错: {e}") + # 只在出现严重错误时才重置变换 + try: + node_path.setPos(0, 0, 0) + node_path.setHpr(0, 0, 0) + node_path.setScale(1, 1, 1) + except: + pass + return False + + def _applyModelScale(self, model, scale_factor): + """应用模型特定缩放 + + Args: + model: 要缩放的模型 + scale_factor: 缩放因子 + """ + try: + print(f"应用模型缩放因子: {scale_factor}") + + # 获取当前边界用于后续位置调整 + original_bounds = model.getBounds() + + # 应用缩放 + model.setScale(scale_factor) + + # 重新调整位置(因为缩放会影响边界) + if original_bounds and not original_bounds.isEmpty(): + new_bounds = model.getBounds() + min_point = new_bounds.getMin() + ground_offset = -min_point.getZ() + model.setZ(ground_offset) + print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") + + print(f"模型缩放完成,缩放因子: {scale_factor}") + + except Exception as e: + print(f"应用模型缩放失败: {str(e)}") + def _applyMaterialsToModel(self, model): """递归应用材质到模型的所有GeomNode""" def apply_material(node_path, depth=0): indent = " " * depth try: - print(f"{indent}处理节点: {node_path.getName()}") - print(f"{indent}节点类型: {node_path.node().__class__.__name__}") + #print(f"{indent}处理节点: {node_path.getName()}") + #print(f"{indent}节点类型: {node_path.node().__class__.__name__}") if isinstance(node_path.node(), GeomNode): - print(f"{indent}发现GeomNode,处理材质") + #print(f"{indent}发现GeomNode,处理材质") geom_node = node_path.node() # 检查所有几何体的状态 @@ -210,40 +409,23 @@ class SceneManager: if node_state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) node_material = mat_attrib.getMaterial() - if node_material and node_material.hasDiffuse(): - color = node_material.getDiffuse() - has_color = True - print(f"{indent}从节点材质获取颜色: {color}") + if node_material: + if node_material.hasBaseColor(): + color = node_material.getBaseColor() + has_color = True + #print(f"{indent}从节点材质获取基础颜色: {color}") + elif node_material.hasDiffuse(): + color = node_material.getDiffuse() + has_color = True + #print(f"{indent}从节点材质获取漫反射颜色: {color}") - # 检查FBX特有的属性 - for tag_key in node_path.getTagKeys(): - print(f"{indent}发现标签: {tag_key}") - if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): - tag_value = node_path.getTag(tag_key) - print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") - - # 如果还没找到颜色,检查几何体 + # 检查几何体材质 if not has_color: for i in range(geom_node.getNumGeoms()): try: geom = geom_node.getGeom(i) state = geom_node.getGeomState(i) - # 检查顶点颜色 - vdata = geom.getVertexData() - if vdata: - format = vdata.getFormat() - if format: - for j in range(format.getNumColumns()): - try: - column = format.getColumn(j) - # InternalName对象需要使用getName()转换为字符串 - column_name = column.getName().getName() - if "color" in column_name.lower(): - print(f"{indent}发现顶点颜色数据: {column_name}") - except Exception: - continue - # 检查材质属性 if state.hasAttrib(MaterialAttrib.getClassType()): mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) @@ -252,12 +434,12 @@ class SceneManager: if orig_material.hasBaseColor(): color = orig_material.getBaseColor() has_color = True - print(f"{indent}从基础颜色获取: {color}") + #print(f"{indent}从几何体材质获取基础颜色: {color}") break elif orig_material.hasDiffuse(): color = orig_material.getDiffuse() has_color = True - print(f"{indent}从漫反射颜色获取: {color}") + #print(f"{indent}从几何体材质获取漫反射颜色: {color}") break # 检查颜色属性 @@ -266,7 +448,7 @@ class SceneManager: if not color_attrib.isOff(): color = color_attrib.getColor() has_color = True - print(f"{indent}从颜色属性获取: {color}") + #print(f"{indent}从颜色属性获取: {color}") break except Exception as geom_error: print(f"{indent}处理几何体 {i} 时出错: {geom_error}") @@ -274,17 +456,26 @@ class SceneManager: # 创建新材质 material = Material() - if has_color: - print(f"{indent}应用找到的颜色: {color}") + if has_color and color: + #print(f"{indent}应用找到的颜色: {color}") try: - material.setDiffuse(color) - material.setBaseColor(color) # 同时设置基础颜色 - node_path.setColor(color) + # 确保颜色值有效 + if (color.getX() == color.getX() and color.getY() == color.getY() and + color.getZ() == color.getZ() and color.getW() == color.getW()): + material.setBaseColor(color) + material.setDiffuse(color) + node_path.setColor(color) + else: + print(f"{indent}⚠️ 颜色值无效,使用默认颜色") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) except Exception as color_error: print(f"{indent}设置颜色时出错: {color_error}") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) material.setDiffuse((0.8, 0.8, 0.8, 1.0)) else: print(f"{indent}使用默认颜色") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) material.setDiffuse((0.8, 0.8, 0.8, 1.0)) # 设置其他材质属性 @@ -293,15 +484,20 @@ class SceneManager: material.setShininess(32.0) # 应用材质 - node_path.setMaterial(material) - print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") + try: + node_path.setMaterial(material, 1) # 1表示强制应用 + #print(f"{indent}材质应用成功") + except Exception as mat_error: + print(f"{indent}⚠️ 应用材质时出错: {mat_error}") + + #print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") except Exception as node_error: print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") # 递归处理子节点 child_count = node_path.getNumChildren() - print(f"{indent}子节点数量: {child_count}") + #print(f"{indent}子节点数量: {child_count}") for i in range(child_count): try: child = node_path.getChild(i) @@ -311,7 +507,7 @@ class SceneManager: continue # 应用材质 - print("\n开始递归应用材质...") + #print("\n开始递归应用材质...") try: apply_material(model) except Exception as e: @@ -321,7 +517,7 @@ class SceneManager: def _adjustModelToGround(self, model): """智能调整模型到地面,但保持原有缩放结构""" try: - print("调整模型位置到地面...") + #print("调整模型位置到地面...") # 获取模型的边界框 bounds = model.getBounds() @@ -341,9 +537,9 @@ class SceneManager: # 设置模型位置:X,Y居中,Z调整到地面 model.setPos(0, 0, ground_offset) - print(f"模型边界: 最小点{min_point}, 中心{center}") - print(f"地面偏移: {ground_offset}") - print(f"最终位置: {model.getPos()}") + #print(f"模型边界: 最小点{min_point}, 中心{center}") + #print(f"地面偏移: {ground_offset}") + #print(f"最终位置: {model.getPos()}") except Exception as e: print(f"调整模型位置失败: {str(e)}") @@ -373,6 +569,18 @@ class SceneManager: # 应用缩放 model.setScale(scale_factor) + # 应用缩放(添加异常处理) + try: + model.setScale(scale_factor) + except Exception as e: + print(f"直接设置缩放失败: {e},尝试使用变换状态") + try: + model.setTransform(TransformState.makeScale(scale_factor)) + except Exception as e2: + print(f"使用变换状态设置缩放也失败: {e2}") + + # 应用缩放后验证变换 + self._validateAndFixTransform(model) # 重新调整位置(因为缩放会影响边界) if original_bounds and not original_bounds.isEmpty(): new_bounds = model.getBounds() @@ -531,59 +739,6 @@ class SceneManager: except Exception as e: print(f"异步加载模型失败: {str(e)}") - def loadAnimatedModel(self, model_path, anims=None, auto_play=True): - """加载带动画的模型 - - Args: - model_path: 模型文件路径 - anims: 动画字典,格式为 {"动画名": "动画文件路径"} - auto_play: 是否自动播放第一个动画 - """ - try: - print(f"🎬 加载动画模型: {model_path}") - - # 如果没有指定动画,尝试自动检测 - if anims is None: - anims = self._detectAnimations(model_path) - - # 创建Actor对象 - actor = Actor(model_path, anims) - if actor: - actor.reparentTo(self.world.render) - - # 设置碰撞检测 - self.setupCollision(actor) - - # 获取可用的动画列表 - available_anims = actor.getAnimNames() - print(f"📋 检测到动画: {available_anims}") - - # 自动播放第一个动画 - if auto_play and available_anims: - first_anim = available_anims[0] - actor.loop(first_anim) - print(f"▶️ 自动播放动画: {first_anim}") - - # 添加动画控制标签 - actor.setTag("animated", "true") - actor.setTag("current_anim", first_anim) - actor.setTag("available_anims", str(available_anims)) - - # 调整模型位置(让它站在地面上) - self._adjustModelPosition(actor) - - self.models.append(actor) - # 更新场景树 - self.updateSceneTree() - - print(f"✅ 动画模型加载完成: {actor.getName()}") - return actor - except Exception as e: - print(f"❌ 加载动画模型失败: {str(e)}") - import traceback - traceback.print_exc() - return None - # ==================== 材质和几何体处理 ==================== def processMaterials(self, model): @@ -631,6 +786,7 @@ class SceneManager: def setupCollision(self, model): """为模型设置碰撞检测(增强版本)""" try: + # 创建碰撞节点 cNode = CollisionNode(f'modelCollision_{model.getName()}') @@ -701,11 +857,233 @@ class SceneManager: # ==================== 场景保存和加载 ==================== - def saveScene(self, filename): - """保存场景到BAM文件""" + def _collectGUIElementInfo(self, gui_node): + """收集GUI元素的信息用于保存""" + try: + # 获取GUI元素类型 + gui_type = "unknown" + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_type"): + gui_type = gui_node.getTag("gui_type") + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("saved_gui_type"): + gui_type = gui_node.getTag("saved_gui_type") + else: + # 尝试从节点名称推断类型 + name_lower = gui_node.getName().lower() + if "button" in name_lower: + gui_type = "button" + elif "label" in name_lower: + gui_type = "label" + elif "entry" in name_lower: + gui_type = "entry" + elif "image" in name_lower: + gui_type = "2d_image" + elif "videoscreen" in name_lower: + if "2d" in name_lower: + gui_type = "2d_video_screen" + else: + gui_type = "video_screen" + elif "info_panel" in name_lower: + if "3d" in name_lower: + gui_type = "info_panel_3d" + else: + gui_type = "info_panel" + else: + # 如果无法识别类型,跳过该元素 + print(f"跳过无法识别类型的GUI元素: {gui_node.getName()}") + return None + + gui_info = { + "name": gui_node.getName(), + "type": gui_type, + "position": list(gui_node.getPos()), + "rotation": list(gui_node.getHpr()), + "scale": list(gui_node.getScale()), + "tags": {}, + "parent_name":None + } + + parent = gui_node.getParent() + if parent and not parent.isEmpty(): + parent_name = parent.getName() + if parent_name not in ["render","aspect2d","render2d"]: + gui_info["parent_name"] = parent_name + + # 收集所有标签(仅对NodePath类型的对象) + if hasattr(gui_node, 'getTagNames'): + for tag in gui_node.getTagNames(): + gui_info["tags"][tag] = gui_node.getTag(tag) + elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象 + # DirectGUI对象使用不同的方法存储标签 + if hasattr(gui_node, '_tags'): + gui_info["tags"] = gui_node._tags.copy() + + # 根据类型收集特定信息 + if gui_type == "button": + if hasattr(gui_node, 'get'): # DirectButton + gui_info["text"] = gui_node.get() + elif hasattr(gui_node, 'getText'): # 其他类型 + gui_info["text"] = gui_node.getText() + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type == "label": + if hasattr(gui_node, 'getText'): + gui_info["text"] = gui_node.getText() + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type == "entry": + if hasattr(gui_node, 'get'): + gui_info["text"] = gui_node.get() + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type == "2d_image": + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): + gui_info["image_path"] = gui_node.getTag("image_path") + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_image_path"): + gui_info["image_path"] = gui_node.getTag("gui_image_path") + elif gui_type == "3d_text": + if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif hasattr(gui_node,'node') and hasattr(gui_node.node(),'getText'): + gui_info["text"] = gui_node.node().getText() + elif gui_type == "3d_image": + if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"): + gui_info["image_path"] = gui_node.getTag("gui_image_path") + elif gui_type in ["video_screen", "2d_video_screen"]: + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): + gui_info["video_path"] = gui_node.getTag("video_path") + gui_info["video_path"] = gui_node.getTag("video_path") + elif gui_type == "virtual_screen": + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type in ["info_panel", "info_panel_3d"]: + # 收集信息面板的特定信息 + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_id"): + gui_info["panel_id"] = gui_node.getTag("panel_id") + + # 收集背景图片信息 + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("bg_image_path"): + gui_info["bg_image_path"] = gui_node.getTag("bg_image_path") + + # 如果是信息面板,收集面板数据 + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("info_panel_data"): + gui_info["panel_data"] = gui_node.getTag("info_panel_data") + + if hasattr(self.world, 'script_manager') and self.world.script_manager: + try: + script_manager = self.world.script_manager + scripts = script_manager.get_scripts_on_object(gui_node) + if scripts: + gui_info["scripts"] = [] + for script_component in scripts: + try: + script_name = script_manager.script_name + #获取脚本路径 + script_class = script_component.script_instance.__class__ + script_file = self._get_script_file_path(script_class,script_name) + #只有当脚本文件存在时才保存 + if script_file and os.path.exists(script_file): + gui_info["scirpts"].append({ + "name":script_name, + "file":script_file + }) + print(f"收集脚本信息{script_name}from {script_file}") + else: + print(f"警告: 脚本文件不存在: {script_file}") + except Exception as e: + print(f"收集单个脚本信息失败{script_name},错误{e}") + continue + except Exception as e: + print(f"收集脚本信息失败{e}") + + print(f"成功收集GUI元素信息: {gui_info}") + return gui_info + except Exception as e: + print(f"收集GUI元素信息失败: {e}") + import traceback + traceback.print_exc() + return None + + def _get_script_file_path(self, script_class, script_name): + """ + 获取脚本文件路径的可靠方法 + """ + script_file = "" + + # 方法1: 使用 inspect.getfile + try: + script_file = inspect.getfile(script_class) + if script_file and os.path.exists(script_file): + return script_file + except: + pass + + # 方法2: 使用 __file__ 属性 + try: + if hasattr(script_class, '__file__') and script_class.__file__: + script_file = script_class.__file__ + if script_file and os.path.exists(script_file): + return script_file + except: + pass + + # 方法3: 使用模块的 __file__ 属性 + try: + module = inspect.getmodule(script_class) + if module and hasattr(module, '__file__') and module.__file__: + script_file = module.__file__ + if script_file and os.path.exists(script_file): + return script_file + except: + pass + + # 方法4: 从脚本管理器中查找 + try: + if hasattr(self.world, 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + # 查找脚本类对应的文件路径 + for file_path, file_mtime in script_manager.loader.file_mtimes.items(): + # 检查文件名是否匹配脚本名 + file_name = os.path.splitext(os.path.basename(file_path))[0] + if file_name == script_name: + if os.path.exists(file_path): + return file_path + except: + pass + + # 方法5: 在脚本目录中查找 + try: + if hasattr(self.world, 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + scripts_dir = script_manager.scripts_directory + + # 查找匹配的脚本文件 + if os.path.exists(scripts_dir): + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if base_name == script_name: + full_path = os.path.join(scripts_dir, file_name) + if os.path.exists(full_path): + return full_path + except: + pass + + print(f"警告: 无法获取脚本 {script_name} 的文件路径") + return script_file + + def saveScene(self, filename,project_path): + """保存场景到BAM文件 - 完整版,支持GUI元素,地形""" try: print(f"\n=== 开始保存场景到: {filename} ===") + # 确保文件路径是规范化的 + filename = os.path.normpath(filename) + + # 确保目录存在 + directory = os.path.dirname(filename) + if directory and not os.path.exists(directory): + os.makedirs(directory) + # 存储需要临时隐藏的节点,以便保存后恢复 nodes_to_restore = [] @@ -727,19 +1105,91 @@ class SceneManager: node.hide() print(f"临时隐藏选择框节点: {node.getName()}") - # 遍历所有模型,保存材质状态和变换信息 - for model in self.models: - # 保存变换信息(关键!) - model.setTag("transform_pos", str(model.getPos())) - model.setTag("transform_hpr", str(model.getHpr())) - model.setTag("transform_scale", str(model.getScale())) - print(f"保存模型 {model.getName()} 的变换信息:") - print(f" 位置: {model.getPos()}") - print(f" 旋转: {model.getHpr()}") - print(f" 缩放: {model.getScale()}") + # 收集所有需要保存的节点 + all_nodes = [] + all_nodes.extend(self.models) + all_nodes.extend(self.Spotlight) + all_nodes.extend(self.Pointlight) + + # 添加GUI元素节点 + gui_elements = [] + if hasattr(self.world, 'gui_elements'): + # 过滤掉空的或重复的GUI元素 + unique_gui_elements = [] + seen_names = set() + + for elem in self.world.gui_elements: + if elem and not elem.isEmpty(): + if not elem.isEmpty() and elem.getName() not in seen_names: + unique_gui_elements.append(elem) + seen_names.add(elem.getName()) + gui_elements = unique_gui_elements + + print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}") + all_nodes.extend(gui_elements) + + # 创建用于保存GUI信息的JSON文件路径 + gui_info_file = filename.replace('.bam', '_gui.json') + + print(self.world.gui_elements) + # 收集GUI元素信息(排除3D文本和3D图像) + gui_data = [] + for gui_node in gui_elements: + gui_info = self._collectGUIElementInfo(gui_node) + if gui_info: + gui_data.append(gui_info) + print(f"添加GUI信息{gui_info['name']}") + + # 保存GUI信息到JSON文件(确保即使没有GUI元素也创建有效的空JSON数组) + try: + import json + with open(gui_info_file, 'w', encoding='utf-8') as f: + json.dump(gui_data, f, ensure_ascii=False, indent=2) + print(f"✓ GUI信息已保存到: {gui_info_file}") + except Exception as e: + print(f"✗ 保存GUI信息失败: {e}") + import traceback + traceback.print_exc() + + # 添加tilesets节点 + for tileset_info in self.tilesets: + if tileset_info.get('node') and not tileset_info['node'].isEmpty(): + all_nodes.append(tileset_info['node']) + + # 添加Cesium tilesets节点 + for tileset_name, tileset_info in self.cesium_integration.tilesets.items(): + if tileset_info.get('node') and not tileset_info['node'].isEmpty(): + all_nodes.append(tileset_info['node']) + + # 保存所有节点的信息 + for node in all_nodes: + if node.isEmpty(): + continue + + # 保存变换信息 + node.setTag("transform_pos", str(node.getPos())) + node.setTag("transform_hpr", str(node.getHpr())) + node.setTag("transform_scale", str(node.getScale())) + print(f"保存节点 {node.getName()} 的变换信息") + + # 保存父子关系信息 - 关键修改 + parent = node.getParent() + if parent and not parent.isEmpty() and parent != self.world.render: + # 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系 + if parent.getName() not in ["render", "aspect2d", "render2d"]: + # 检查父节点是否也是场景中的模型 + is_parent_model = False + for model in self.models: + if model == parent: + is_parent_model = True + break + + if is_parent_model: + node.setTag("parent_name", parent.getName()) + print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}") # 获取当前状态 - state = model.getState() + state = node.getState() # 如果有材质属性,保存为标签 if state.hasAttrib(MaterialAttrib.getClassType()): @@ -747,26 +1197,86 @@ class SceneManager: material = mat_attrib.getMaterial() if material: # 保存材质属性到标签 - model.setTag("material_ambient", str(material.getAmbient())) - model.setTag("material_diffuse", str(material.getDiffuse())) - model.setTag("material_specular", str(material.getSpecular())) - model.setTag("material_emission", str(material.getEmission())) - model.setTag("material_shininess", str(material.getShininess())) + node.setTag("material_ambient", str(material.getAmbient())) + node.setTag("material_diffuse", str(material.getDiffuse())) + node.setTag("material_specular", str(material.getSpecular())) + node.setTag("material_emission", str(material.getEmission())) + node.setTag("material_shininess", str(material.getShininess())) if material.hasBaseColor(): - model.setTag("material_basecolor", str(material.getBaseColor())) + node.setTag("material_basecolor", str(material.getBaseColor())) - # 如果有颜色属性,保存为标签 - if state.hasAttrib(ColorAttrib.getClassType()): - color_attrib = state.getAttrib(ColorAttrib.getClassType()) - if not color_attrib.isOff(): - model.setTag("color", str(color_attrib.getColor())) + # 保存特定类型节点的额外信息 + if node.hasTag("light_type"): + # 保存光源特定信息 + light_obj = node.getPythonTag("rp_light_object") + if light_obj: + node.setTag("light_energy", str(light_obj.energy)) + node.setTag("light_radius", str(getattr(light_obj, 'radius', 0))) + if hasattr(light_obj, 'fov'): + node.setTag("light_fov", str(light_obj.fov)) + elif node.hasTag("element_type"): + element_type = node.getTag("element_type") + if element_type == "cesium_tileset": + # 保存tileset特定信息 + if node.hasTag("tileset_url"): + node.setTag("saved_tileset_url", node.getTag("tileset_url")) + elif node.hasTag("gui_type") or node.hasTag("is_gui_element"): + # 保存GUI元素特定信息 + gui_type = node.getTag("gui_type") if node.hasTag("gui_type") else \ + node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown" + node.setTag("saved_gui_type", gui_type) + + # 保存GUI元素的通用属性 + if hasattr(node, 'getPythonTag'): + # 保存任何Python标签数据 + for tag_name in node.getPythonTagKeys(): + try: + tag_value = node.getPythonTag(tag_name) + node.setTag(f"python_tag_{tag_name}", str(tag_value)) + except: + pass + elif node.hasTag("element_type") and node.getTag("element_type") == "info_panel": + # 保存信息面板特定信息 + print(f"保存信息面板信息: {node.getName()}") + panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName() + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.serializePanelData(panel_id) + if panel_data: + import json + node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False)) + + if hasattr(self.world,'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + scripts = script_manager.get_scripts_on_object(node) + if scripts: + node.setTag("has_scripts", "true") + script_info_list = [] + for script_component in scripts: + script_name = script_component.script_name + print(f"保存脚本信息: {script_name}") + + # 获取脚本类的文件路径 + script_class = script_component.script_instance.__class__ + script_file = self._get_script_file_path(script_class, script_name) + + script_info_list.append({ + "name": script_name, + "file": script_file + }) + + # 将脚本信息保存为JSON字符串 + import json + node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) + print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本") try: print("--- 打印当前场景图 (render) ---") self.world.render.ls() print("---------------------------------") + + self.take_screenshot(project_path) # 保存场景 - success = self.world.render.writeBamFile(filename) + success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename)) if success: print(f"✓ 场景保存成功: {filename}") @@ -774,6 +1284,7 @@ class SceneManager: print("✗ 场景保存失败") return success + finally: # 恢复之前隐藏的节点 for item in nodes_to_restore: @@ -791,29 +1302,139 @@ class SceneManager: traceback.print_exc() return False + def take_screenshot(self, projectpath): + """ + 截图并保存到指定的完整路径 + + Args: + full_path (str): 完整的文件保存路径,包括文件名和扩展名 + + Returns: + bool: 截图是否成功 + """ + try: + from panda3d.core import Filename + import os + + print(f"\n=== 截图保存: {projectpath} ===") + + # 确保目录存在 + directory = os.path.dirname(projectpath) + if directory and not os.path.exists(directory): + os.makedirs(directory) + print(f"创建目录: {directory}") + + # 规范化路径 + filename = os.path.basename(os.path.normpath(projectpath)) + filename = f'{filename}.png' + print(f'project_path: {projectpath}') + print(f'project_name: {filename}') + full_path = os.path.normpath(os.path.join(projectpath, filename)) + p3d_filename = Filename.from_os_specific(full_path) + # 使用 Panda3D 的截图功能 + success = self.world.win.saveScreenshot(p3d_filename) + + if success: + print(f"✅ 成功截图并保存到: {full_path}") + return True + else: + print(f"❌ 截图保存失败: {full_path}") + return False + + except Exception as e: + print(f"保存截图时发生错误: {str(e)}") + import traceback + traceback.print_exc() + return False + def loadScene(self, filename): """从BAM文件加载场景""" try: print(f"\n=== 开始加载场景: {filename} ===") + # 确保文件路径是规范化的 + filename = os.path.normpath(filename) + + # 检查文件是否存在 + if not os.path.exists(filename): + print(f"场景文件不存在: {filename}") + return False + + tree_widget = self._get_tree_widget() # 清除当前场景 print("\n清除当前场景...") for model in self.models: - model.removeNode() - self.models.clear() + tree_widget.delete_item(model) - # 清理可能存在的辅助节点(坐标轴、选择框等) + # 清除灯光 + for light_node in self.Spotlight: + tree_widget.delete_item(light_node) + + for light_node in self.Pointlight: + tree_widget.delete_item(light_node) + + for terrain in self.world.terrain_manager.terrains: + tree_widget.delete_item(terrain) + + # 清除tilesets + for tileset_info in self.tilesets: + tree_widget.delete_item(tileset_info['node']) + + for light in self.Spotlight: + if not light.isEmpty(): + light.removeNode() + self.Spotlight.clear() + + for light in self.Pointlight: + if not light.isEmpty(): + light.removeNode() + self.Pointlight.clear() + + # 清理tilesets + for tileset_info in self.tilesets: + if tileset_info['node'] and not tileset_info['node'].isEmpty(): + tileset_info['node'].removeNode() + self.tilesets.clear() + + # 清理Cesium tilesets + for tileset_name, tileset_info in list(self.cesium_integration.tilesets.items()): + if tileset_info['node'] and not tileset_info['node'].isEmpty(): + tileset_info['node'].removeNode() + self.cesium_integration.tilesets.clear() + + for gui in self.world.gui_elements: + if not gui.isEmpty(): + gui.removeNode() + self.world.gui_elements.clear() + + if hasattr(self.world,'info_panel_manager'): + self.world.info_panel_manager.removeAllPanels() + + # 清理可能存在的辅助节点 self._cleanupAuxiliaryNodes() # 加载场景 - scene = self.world.loader.loadModel(filename) + scene = self.world.loader.loadModel(Filename.fromOsSpecific(filename)) if not scene: + print("场景加载失败") return False + tree_widget.create_model_items(scene) # 遍历场景中的所有模型节点 + # 用于存储处理后的灯光节点,避免重复处理 + processed_lights = [] + # 用于存储处理后的GUI元素,避免重复处理 + + #存储所有加载的节点,用于后续处理父子关系 + loaded_nodes = {} #name->nodePath映射 + + # 遍历场景中的所有节点 def processNode(nodePath, depth=0): indent = " " * depth - print(f"{indent}处理节点: {nodePath.getName()}") + print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") + + #存储节点以便后续处理父子关系 + loaded_nodes[nodePath.getName()] = nodePath # 跳过render节点的递归 if nodePath.getName() == "render" and depth > 0: @@ -830,68 +1451,68 @@ class SceneManager: print(f"{indent}跳过相机节点: {nodePath.getName()}") return - # 跳过辅助节点(坐标轴和选择框) + # 跳过辅助节点 if nodePath.getName().startswith(("gizmo", "selectionBox")): print(f"{indent}跳过辅助节点: {nodePath.getName()}") return if nodePath.getName() in ['SceneRoot'] or \ - any(keyword in nodePath.getName() for keyword in ["Skybox","skybox"]): + any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]): print(f"{indent}跳过环境节点:{nodePath.getName()}") return - if isinstance(nodePath.node(), ModelRoot): - print(f"{indent}找到模型根节点!") + # 检查是否是用户创建的场景元素 + is_scene_element = ( + nodePath.hasTag("is_scene_element") or + nodePath.hasTag("is_model_root") or + nodePath.hasTag("light_type") or + nodePath.hasTag("gui_type") or # 检查gui_type标签 + nodePath.hasTag("is_gui_element") or + nodePath.hasTag("saved_gui_type") or + (nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel") + ) + + # 特殊处理:检查节点名称是否包含GUI相关关键词 + is_potential_gui = any(keyword in nodePath.getName().lower() for keyword in + ["gui", "button", "label", "entry", "image", "video", "screen", "text"]) + + if is_scene_element or is_potential_gui: + print(f"{indent}找到场景元素节点: {nodePath.getName()}") + + # 如果是潜在的GUI元素但没有标签,添加基本标签 + if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")): + print(f"{indent}为潜在GUI元素添加标签: {nodePath.getName()}") + nodePath.setTag("is_gui_element", "1") + nodePath.setTag("is_scene_element", "1") + # 尝试从名称推断类型 + name_lower = nodePath.getName().lower() + if "button" in name_lower: + nodePath.setTag("gui_type", "button") + elif "label" in name_lower: + nodePath.setTag("gui_type", "label") + elif "entry" in name_lower: + nodePath.setTag("gui_type", "entry") + elif "image" in name_lower: + nodePath.setTag("gui_type", "image") + elif "video" in name_lower or "screen" in name_lower: + nodePath.setTag("gui_type", "video_screen") + else: + nodePath.setTag("gui_type", "unknown") # 清除现有材质状态 nodePath.clearMaterial() nodePath.clearColor() - # 创建新材质 - material = Material() - - # 从标签恢复材质属性 - def parseColor(color_str): - """解析颜色字符串为Vec4""" - try: - # 移除LVecBase4f标记,只保留数值 - color_str = color_str.replace('LVecBase4f', '').strip('()') - r, g, b, a = map(float, color_str.split(',')) - return Vec4(r, g, b, a) - except: - return Vec4(1, 1, 1, 1) # 默认白色 - - if nodePath.hasTag("material_ambient"): - material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) - if nodePath.hasTag("material_diffuse"): - material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) - if nodePath.hasTag("material_specular"): - material.setSpecular(parseColor(nodePath.getTag("material_specular"))) - if nodePath.hasTag("material_emission"): - material.setEmission(parseColor(nodePath.getTag("material_emission"))) - if nodePath.hasTag("material_shininess"): - material.setShininess(float(nodePath.getTag("material_shininess"))) - if nodePath.hasTag("material_basecolor"): - material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) - - # 应用材质 - nodePath.setMaterial(material) - - # 恢复颜色属性 - if nodePath.hasTag("color"): - nodePath.setColor(parseColor(nodePath.getTag("color"))) - - # 恢复变换信息(关键!) + # 恢复变换信息 def parseVec3(vec_str): """解析向量字符串为Vec3""" try: - # 移除LVecBase3f标记,只保留数值 vec_str = vec_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') x, y, z = map(float, vec_str.split(',')) return Vec3(x, y, z) except Exception as e: print(f"解析向量失败: {vec_str}, 错误: {e}") - return Vec3(0, 0, 0) # 默认值 + return Vec3(0, 0, 0) if nodePath.hasTag("transform_pos"): pos = parseVec3(nodePath.getTag("transform_pos")) @@ -908,13 +1529,152 @@ class SceneManager: nodePath.setScale(scale) print(f"{indent}恢复缩放: {scale}") - # 将模型重新挂载到render下 - nodePath.wrtReparentTo(self.world.render) + if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true": + if hasattr(self.world,'script_manager') and self.world.script_manager: + try: + import json + scripts_info = json.loads(nodePath.getTag("scripts_info")) + print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本") - # 为加载的模型设置碰撞检测 - self.setupCollision(nodePath) + script_manager = self.world.script_manager + for script_info in scripts_info: + script_name = script_info["name"] + script_file = script_info.get("file","") - self.models.append(nodePath) + print(f"尝试重新挂载脚本{script_name}from {script_file}") + + if script_name not in script_manager.loader.script_classes: + if script_file and os.path.exists(script_file): + print(f"从文件加载脚本:{script_file}") + loaded_class = script_manager.load_script_from_file(script_file) + if loaded_class is None: + print(f"从文件加载脚本失败{script_file}") + script_path = self._find_scrip_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载{script_path}") + script_manager.load_script_from_file(script_path) + else: + script_path = self._find_script_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载: {script_path}") + script_manager.load_script_from_file(script_path) + else: + print(f"找不到脚本文件: {script_name}") + if script_name in script_manager.loader.script_classes: + script_component = script_manager.add_script_to_object(nodePath,script_name) + if script_component: + print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}") + else: + print(f"为 {nodePath.getName()} 添加脚本失败: {script_name}") + else: + print(f"脚本 {script_name} 不可用,跳过挂载") + except Exception as e: + print(f"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() + + + # 恢复材质属性 + def parseColor(color_str): + """解析颜色字符串为Vec4""" + try: + color_str = color_str.replace('LVecBase4f', '').strip('()') + r, g, b, a = map(float, color_str.split(',')) + return Vec4(r, g, b, a) + except: + return Vec4(1, 1, 1, 1) + + # 创建并恢复材质 + material = Material() + material_changed = False + + if nodePath.hasTag("material_ambient"): + material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) + material_changed = True + + if nodePath.hasTag("material_diffuse"): + material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) + material_changed = True + + if nodePath.hasTag("material_specular"): + material.setSpecular(parseColor(nodePath.getTag("material_specular"))) + material_changed = True + + if nodePath.hasTag("material_emission"): + material.setEmission(parseColor(nodePath.getTag("material_emission"))) + material_changed = True + + if nodePath.hasTag("material_shininess"): + material.setShininess(float(nodePath.getTag("material_shininess"))) + material_changed = True + + if nodePath.hasTag("material_basecolor"): + material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) + material_changed = True + + if material_changed: + nodePath.setMaterial(material) + + # 恢复颜色属性 + if nodePath.hasTag("color"): + nodePath.setColor(parseColor(nodePath.getTag("color"))) + + # 处理特定类型的节点 + if nodePath.hasTag("light_type"): + light_type = nodePath.getTag("light_type") + print(f"{indent}检测到光源类型: {light_type}") + + # 检查是否已经处理过这个灯光 + if nodePath not in processed_lights: + # 重新创建RP光源对象 + if light_type == "spot_light": + self._recreateSpotLight(nodePath) + elif light_type == "point_light": + self._recreatePointLight(nodePath) + # 标记为已处理 + processed_lights.append(nodePath) + + elif nodePath.hasTag("element_type"): + element_type = nodePath.getTag("element_type") + if element_type == "cesium_tileset": + tileset_url = nodePath.getTag("saved_tileset_url") if nodePath.hasTag( + "saved_tileset_url") else "" + tileset_info = { + 'url': tileset_url, + 'node': nodePath, + 'position': nodePath.getPos(), + 'tiles': {} + } + self.tilesets.append(tileset_info) + self.cesium_integration.tilesets[nodePath.getName()] = tileset_info + + # 将节点重新挂载到render下(如果需要) + # 注意:GUI元素可能需要挂载到特定的父节点上 + if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"): + # GUI元素通常应该挂载到aspect2d或特定的GUI父节点上 + # 这里我们先保持原挂载关系 + pass + else: + # 其他节点确保挂载到render下 + if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render", + "aspect2d", + "render2d"]: + nodePath.wrtReparentTo(self.world.render) + + # 为模型节点设置碰撞检测 + if nodePath.hasTag("is_model_root"): + print(f"J{indent}处理模型节点{nodePath.getName()}") + + #self._validateAndFixAllTransforms(nodePath) + + self._fixModelStructure(nodePath) + + # if self.world.property_panel._hasCollision(nodePath): + # print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") + # else: + # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") + # self.setupCollision(nodePath) + self.models.append(nodePath) # 递归处理子节点 for child in nodePath.getChildren(): @@ -923,11 +1683,49 @@ class SceneManager: print("\n开始处理场景节点...") processNode(scene) + #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 + print("\n开始重建父子关系...") + self._rebuildParentChildRelationships(loaded_nodes) + + # 加载GUI信息并重新创建非3D的GUI元素 + gui_info_file = filename.replace('.bam', '_gui.json') + if os.path.exists(gui_info_file): + try: + with open(gui_info_file, 'r', encoding='utf-8') as f: + content = f.read().strip() + if content: # 检查文件是否为空 + import json + gui_data = json.loads(content) + print(f"✓ 成功加载GUI信息文件: {gui_info_file}") + print(f" 发现 {len(gui_data)} 个GUI元素需要重建") + + # 使用gui_manager重新创建GUI元素 + self._recreateGUIElementsFromData(gui_data) + else: + print("ℹ️ GUI信息文件为空") + except json.JSONDecodeError as e: + print(f"✗ GUI信息文件格式错误: {e}") + except Exception as e: + print(f"✗ 加载GUI信息失败: {e}") + import traceback + traceback.print_exc() + else: + print("ℹ️ 未找到GUI信息文件") + # 移除临时场景节点 - scene.removeNode() + if not scene.isEmpty(): + scene.removeNode() # 更新场景树 - self.updateSceneTree() + #self.updateSceneTree() + #self._get_tree_widget().create_model_items(scene) + + print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}") + if len(self.world.gui_elements) > 0: + print("GUI元素列表:") + for i, elem in enumerate(self.world.gui_elements): + print( + f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})") print("=== 场景加载完成 ===\n") return True @@ -938,6 +1736,632 @@ class SceneManager: traceback.print_exc() return False + def _rebuildParentChildRelationships(self, loaded_nodes): + try: + parent_child_relations = [] + for node_name, node in loaded_nodes.items(): + if node.hasTag("parent_name"): + parent_name = node.getTag("parent_name") + if parent_name in loaded_nodes: + parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组 + print(f"发现父子关系:{parent_name}->{node_name}") + else: + print(f"警告:节点{node_name}的父节点{parent_name}不存在") + for child_node, parent_node in parent_child_relations: + try: + child_node.wrtReparentTo(parent_node) + print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}") + except Exception as e: + print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}") + + if not parent_child_relations: + print("尝试从场景结构推断父子关系") + self._inferParentChildRelationships(loaded_nodes) + + print("父子关系重建完成") + except Exception as e: + print(f"重建父子关系时出错: {e}") + import traceback + traceback.print_exc() + + + except Exception as e: + print(f"重建父子关系时出错: {e}") + import traceback + traceback.print_exc() + + def _inferParentChildRelationships(self, loaded_nodes): + """从场景结构推断父子关系""" + try: + # 这里可以添加更复杂的父子关系推断逻辑 + # 例如,根据节点名称、位置关系等进行推断 + # 目前保持简单,后续可以扩展 + print("父子关系推断完成(当前为空实现)") + except Exception as e: + print(f"推断父子关系时出错: {e}") + + def _shouldSkipNodeInTree(self, nodePath): + """判断节点是否应该在场景树中跳过显示""" + # 跳过render节点的递归 + if nodePath.getName() == "render": + return True + + # 跳过光源节点 + if nodePath.getName() in ["alight", "dlight"]: + return True + + # 跳过相机节点 + if nodePath.getName() in ["camera", "cam"]: + return True + + # 跳过3D文本和3D图像节点 + if (hasattr(nodePath.node(), "hasTag") and + nodePath.node().hasTag("gui_type") and + nodePath.node().getTag("gui_type") in ["3d_text", "3d_image"]): + return True + + # 跳过辅助节点 + if nodePath.getName().startswith(("gizmo", "selectionBox")): + return True + + return False + def _recreateGUIElementsFromData(self, gui_data): + """根据保存的GUI数据重新创建GUI元素""" + try: + gui_manager = getattr(self.world, 'gui_manager', None) + property_manager = getattr(self.world, 'property_panel', None) + info_panel_manager = getattr(self.world,'info_panel_manager',None) + if not gui_manager: + print("GUI管理器未找到,无法重建GUI元素") + return + print(f"开始重建 {len(gui_data)} 个GUI元素...") + + processed_names = set() + created_elements = {} + #存储原始的缩放和位置信息,用于后续计算 + element_original_data = {} + + # 第一遍:收集所有元素信息 + for i, gui_info in enumerate(gui_data): + name = gui_info.get("name", f"gui_element_{i}") + element_original_data[name] = { + "scale": gui_info.get("scale", [1, 1, 1]), + "position": gui_info.get("position", [0, 0, 0]), + "parent_name": gui_info.get("parent_name") + } + + valid_parents = set() + for gui_info in gui_data: + name = gui_info.get("name",f"gui_element_{gui_info.get('index',0)}") + valid_parents.add(name) + + if hasattr(self.world,'gui_elements'): + for elem in self.world.gui_elements: + if elem and not elem.isEmpty(): + valid_parents.add(elem.getName()) + + valid_parents.add("render") + valid_parents.add("aspect2d") + valid_parents.add("render2d") + + pos = (0, 0, 0) + for i, gui_info in enumerate(gui_data): + try: + gui_type = gui_info.get("type", "unknown") + name = gui_info.get("name", f"gui_element_{i}") + position = gui_info.get("position", [0, 0, 0]) + scale = gui_info.get("scale", [1, 1, 1]) + tags = gui_info.get("tags", {}) + text = gui_info.get("text", "") + image_path = gui_info.get("image_path", "") + video_path = gui_info.get("video_path", "") + bg_image_path = gui_info.get("bg_image_path", "") # 背景图片路径 + panel_id = gui_info.get("panel_id", name) # 信息面板ID + panel_data = gui_info.get("panel_data", None) # 面板数据 + parent_name = gui_info.get("parent_name") + + # 检查是否已经处理过同名元素 + if name in processed_names: + print(f"跳过重复元素: {name}") + continue + + if parent_name and parent_name not in valid_parents: + print(f"⚠️ 跳过元素 {name},因为其父级 {parent_name} 不存在") + continue + + processed_names.add(name) + + print(f"重建GUI元素: {name} (类型: {gui_type})") + print(f" 位置: {position}") + print(f" 缩放: {scale}") + print(f" 文本: {text}") + print(f" 图像路径: {image_path}") + print(f" 背景图片路径: {bg_image_path}") + print(f"视频路径:{video_path}") + + absolute_position = list(position) + absolute_scale = list(scale) + + if parent_name and parent_name in element_original_data: + parent_data = element_original_data[parent_name] + parent_scale = parent_data["scale"] + + if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", + "2d_video_screen"]: + # 位置需要乘以父级缩放来得到绝对位置 + for j in range(min(len(absolute_position), len(parent_scale))): + absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + # 缩放需要乘以父级缩放来得到绝对缩放 + for j in range(min(len(absolute_scale), len(parent_scale))): + absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + print(f" 绝对位置: {absolute_position}") + print(f" 绝对缩放: {absolute_scale}") + + # 根据类型创建相应的GUI元素 + new_element = None + + if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'): + new_element = gui_manager.createGUIButton( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'): + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + new_element = gui_manager.createGUILabel( + pos=tuple(absolute_position), + text=text, + size=scale_value + ) + elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'): + new_element = gui_manager.createGUIEntry( + pos=tuple(absolute_position), + placeholder=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.2 + new_element = gui_manager.createGUI2DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=scale_value*0.2 + ) + elif gui_type == "3d_text" and hasattr(gui_manager,'createGUI3DText'): + size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 + new_element = gui_manager.createGUI3DText( + pos=tuple(absolute_position), + text=text, + size=size + ) + elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): + # 处理3D图像 + # 根据缩放值的数量处理尺寸 + if len(absolute_scale) >= 3: + size = (absolute_scale[0] * 2, absolute_scale[1] * 2) + elif len(absolute_scale) >= 2: + size = (absolute_scale[0] * 2, absolute_scale[1] * 2) + elif len(scale) >= 1: + size = (absolute_scale[0] * 2, absolute_scale[0] * 2) + else: + size = (1.0, 1.0) + + new_element = gui_manager.createGUI3DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=size + ) + elif gui_type == "video_screen" and hasattr(gui_manager,'createVideoScreen'): + new_element = gui_manager.createVideoScreen( + pos=tuple(absolute_position), + size=absolute_scale, + video_path=video_path + ) + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + from direct.task.TaskManagerGlobal import taskMgr + def load_video_stream_task(task): + if hasattr(property_manager,'_loadVideoFromURLWithOpenCV_3D'): + property_manager._loadVideoFromURLWithOpenCV_3D(new_element,video_path) + return task.done + + taskMgr.doMethodLater(0.5, load_video_stream_task, 'loadVideoStreamTask') + else: + if hasattr(gui_manager,'loadVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr + def load_video_file_task(task): + gui_manager.loadVideoFile(new_element,video_path) + return task.done + taskMgr.doMethodLater(0.1,load_video_file_task,'loadVideoFileTask') + + # if video_path and new_element and hasattr(gui_manager, 'loadVideoFile'): + # # 延迟一帧执行,确保节点完全初始化 + # from direct.task.TaskManagerGlobal import taskMgr + # def load_video_task(task): + # gui_manager.loadVideoFile(new_element, video_path) + # return task.done + # + # taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask') + + elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'): + new_element = gui_manager.createGUI2DVideoScreen( + pos=tuple(absolute_position), + size=absolute_scale, + video_path=video_path + ) + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + pass + # from direct.task.TaskManagerGlobal import taskMgr + # def load_2d_video_stream_task(task): + # if hasattr(property_manager,'_loadVideoFromURLWithOpenCV'): + # property_manager._loadVideoFromURLWithOpenCV(new_element,video_path) + # return task.done + # taskMgr.doMethodLater(0.1,load_2d_video_stream_task,'load2DVideoStreamTask') + else: + if hasattr(property_manager,'load2DVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr + def load_2d_video_file_task(task): + property_manager.load2DVideoFile(new_element,video_path) + return task.done + taskMgr.doMethodLater(0.1,load_2d_video_file_task,'load2DVideoFileTask') + + + # 如果创建成功,设置属性 + if new_element: + # 如果返回的是列表(多选创建),取第一个 + if isinstance(new_element, list): + new_element = new_element[0] + + # 设置名称 + new_element.setName(name) + + # 设置变换 + new_element.setPos(*position) + + if len(scale) >= 3: + new_element.setScale(scale[0], scale[1], scale[2]) + elif len(scale) >= 1: + new_element.setScale(scale[0]) + + # 设置标签 + # 对于NodePath对象 + if hasattr(new_element, 'setTag'): + for tag_name, tag_value in tags.items(): + # 跳过变换标签,因为我们已经设置了 + if tag_name not in ["transform_pos", "transform_hpr", "transform_scale"]: + new_element.setTag(tag_name, tag_value) + # 对于DirectGUI对象,使用自定义标签存储 + elif hasattr(new_element, '_tags'): + new_element._tags.update(tags) + + created_elements[name] = new_element + + print(f"GUI元素重建成功: {name}") + else: + print(f"无法重建GUI元素: {name} (类型: {gui_type})") + + except Exception as e: + print(f"重建GUI元素失败 {name}: {e}") + import traceback + traceback.print_exc() + continue + + # 第二遍:设置父子级关系并更新Qt树 + print("开始设置父子级关系...") + try: + # 创建父子级关系映射 + parent_child_map = {} + for gui_info in gui_data: + name = gui_info.get("name") + parent_name = gui_info.get("parent_name") + + if name and parent_name and parent_name in created_elements: + parent_child_map[name] = parent_name + print(f"父子级关系映射: {parent_name} -> {name}") + + # 按正确的顺序设置父子级关系并更新Qt树 + tree_widget = self._get_tree_widget() + if tree_widget: + # 先将所有元素添加到Qt树中 + qt_tree_items = {} + for name, element in created_elements.items(): + # 尝试在Qt树中找到对应的项,如果找不到则创建 + qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) + if qt_item: + qt_tree_items[name] = qt_item + + # 然后设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] + + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + + # 更新Qt树显示 + if child_name in qt_tree_items and parent_name in qt_tree_items: + child_item = qt_tree_items[child_name] + parent_item = qt_tree_items[parent_name] + + # 从当前位置移除子项 + if child_item.parent(): + child_item.parent().removeChild(child_item) + else: + # 如果是顶级项,从树中移除 + index = tree_widget.indexOfTopLevelItem(child_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) + + # 将子项添加到新的父项下 + parent_item.addChild(child_item) + print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") + else: + print(f"元素 {child_name} 不支持 reparentTo 操作") + else: + print(f"元素未找到: 父级={parent_name}, 子级={child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + else: + # 如果没有tree_widget,只设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] + + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + + except Exception as e: + print(f"设置父子级关系时出错: {e}") + # 第三遍:重新挂载脚本 + print("开始重新挂载脚本...") + for gui_info in gui_data: + try: + name = gui_info.get("name") + if name in created_elements and "scripts" in gui_info: + new_element = created_elements[name] + + # 重新挂载脚本(如果有的话) + if "scripts" in gui_info and hasattr(self.world, + 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + for script_info in gui_info["scripts"]: + script_name = script_info["name"] + script_file = script_info.get("file", "") + + print(f"尝试重新挂载脚本: {script_name} from {script_file}") + + # 检查脚本是否已加载 + if script_name not in script_manager.loader.script_classes: + # 如果脚本未加载,尝试从保存的文件路径加载 + if script_file and os.path.exists(script_file): + print(f"从文件加载脚本: {script_file}") + loaded_class = script_manager.load_script_from_file(script_file) + if loaded_class is None: + print(f"从文件加载脚本失败: {script_file}") + # 如果从文件加载失败,尝试在脚本目录中查找 + script_path = self._find_script_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载: {script_path}") + script_manager.load_script_from_file(script_path) + else: + # 如果没有文件路径或文件不存在,尝试在脚本目录中查找 + script_path = self._find_script_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载: {script_path}") + script_manager.load_script_from_file(script_path) + else: + print(f"找不到脚本文件: {script_name}") + + # 为元素添加脚本 + script_component = script_manager.add_script_to_object(new_element, script_name) + if script_component: + print(f"成功为 {name} 添加脚本: {script_name}") + else: + print(f"为 {name} 添加脚本失败: {script_name}") + except Exception as e: + print(f"重新挂载脚本失败 {gui_info.get('name', 'unknown')}: {e}") + continue + + print("GUI元素重建完成") + + except Exception as e: + print(f"重建GUI元素时发生错误: {e}") + import traceback + traceback.print_exc() + + def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name): + """在Qt树中查找或创建指定元素对应的项""" + try: + # 首先尝试查找现有的项 + existing_item = self._findQtTreeItem(tree_widget, target_element) + if existing_item: + return existing_item + + # 如果找不到,创建新的项 + # 找到场景根节点 + scene_root = None + for i in range(tree_widget.topLevelItemCount()): + top_item = tree_widget.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + scene_root = top_item + break + + if not scene_root: + print("无法找到场景根节点") + return None + + # 创建新的Qt树项 + new_item = QTreeWidgetItem(scene_root, [element_name]) + new_item.setData(0, Qt.UserRole, target_element) + new_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") # 或根据元素类型设置适当的类型 + + print(f"为元素 {element_name} 创建了新的Qt树项") + return new_item + + except Exception as e: + print(f"查找或创建Qt树项失败: {e}") + return None + + def _findQtTreeItem(self, tree_widget, target_element): + """在Qt树中查找指定元素对应的项""" + try: + def search_recursive(parent_item): + # 检查当前项 + if parent_item: + item_element = parent_item.data(0, Qt.UserRole) + if item_element == target_element: + return parent_item + + # 递归检查子项 + for i in range(parent_item.childCount()): + child_item = parent_item.child(i) + result = search_recursive(child_item) + if result: + return result + return None + + # 从根节点开始搜索 + root = tree_widget.invisibleRootItem() + for i in range(root.childCount()): + top_item = root.child(i) + result = search_recursive(top_item) + if result: + return result + + return None + except Exception as e: + print(f"查找Qt树项失败: {e}") + return None + def _find_script_in_directory(self, script_name): + """在脚本目录中查找脚本文件""" + try: + if hasattr(self.world, 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + scripts_dir = script_manager.scripts_directory + + if os.path.exists(scripts_dir): + # 首先精确匹配 + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if base_name == script_name: + return os.path.join(scripts_dir, file_name) + + # 如果没有精确匹配,尝试模糊匹配 + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if script_name.lower() in base_name.lower() or base_name.lower() in script_name.lower(): + return os.path.join(scripts_dir, file_name) + except Exception as e: + print(f"查找脚本文件时出错: {e}") + + return None + + def _recreateSpotLight(self, light_node): + """重新创建聚光灯""" + try: + from RenderPipelineFile.rpcore import SpotLight + from QPanda3D.Panda3DWorld import get_render_pipeline + from panda3d.core import Vec3 + + # 创建聚光灯对象 + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = 70 + light.set_color_from_temperature(5 * 1000.0) + + # 恢复保存的属性 + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + 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) + + # 保存光源对象引用 + light_node.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.Spotlight.append(light_node) + + # 确保灯光节点有正确的标签,以便在场景树更新时被识别 + if not light_node.hasTag("is_scene_element"): + light_node.setTag("is_scene_element", "1") + + print(f"重新创建聚光灯: {light_node.getName()}") + except Exception as e: + print(f"重新创建聚光灯失败: {str(e)}") + import traceback + traceback.print_exc() + + def _recreatePointLight(self, light_node): + """重新创建点光源""" + try: + from RenderPipelineFile.rpcore import PointLight + from QPanda3D.Panda3DWorld import get_render_pipeline + + # 创建点光源对象 + light = PointLight() + + # 恢复保存的属性 + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5 * 1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + # 设置位置 + light.setPos(light_node.getPos()) + + # 添加到渲染管线 + render_pipeline = get_render_pipeline() + render_pipeline.add_light(light) + + # 保存光源对象引用 + light_node.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.Pointlight.append(light_node) + + # 确保灯光节点有正确的标签,以便在场景树更新时被识别 + if not light_node.hasTag("is_scene_element"): + light_node.setTag("is_scene_element", "1") + + print(f"重新创建点光源: {light_node.getName()}") + except Exception as e: + print(f"重新创建点光源失败: {str(e)}") + import traceback + traceback.print_exc() + def _cleanupAuxiliaryNodes(self): """清理场景中可能存在的辅助节点""" try: @@ -970,9 +2394,14 @@ class SceneManager: """删除模型""" try: if model in self.models: - model.removeNode() - self.models.remove(model) - self.updateSceneTree() + tree_widget = self._get_tree_widget() + if not tree_widget: + return False + + tree_widget.delete_items(tree_widget.selectedItems()) + # model.removeNode() + # self.models.remove(model) + # self.updateSceneTree() print(f"删除模型: {model.getName()}") return True except Exception as e: @@ -981,14 +2410,15 @@ class SceneManager: def clearAllModels(self): """清除所有模型""" - try: - for model in self.models: - model.removeNode() - self.models.clear() - self.updateSceneTree() - print("清除所有模型完成") - except Exception as e: - print(f"清除所有模型失败: {str(e)}") + pass + # try: + # for model in self.models: + # model.removeNode() + # self.models.clear() + # self.updateSceneTree() + # print("清除所有模型完成") + # except Exception as e: + # print(f"清除所有模型失败: {str(e)}") def getModels(self): """获取模型列表""" @@ -1065,6 +2495,8 @@ class SceneManager: light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.setPos(*pos) + light_np.setTransform(TransformState.makeIdentity()) + # 创建聚光灯对象 light = SpotLight() light.direction = Vec3(0, 0, -1) @@ -1074,7 +2506,6 @@ class SceneManager: light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 - light.setPos(*pos) # 添加到渲染管线 render_pipeline.add_light(light) @@ -1082,6 +2513,7 @@ class SceneManager: # 设置节点属性和标签 light_np.setTag("light_type", "spot_light") light_np.setTag("is_scene_element", "1") + light_np.setTag("tree_item_type", "LIGHT_NODE") light_np.setTag("light_energy", str(light.energy)) light_np.setTag("created_by_user", "1") @@ -1170,11 +2602,12 @@ class SceneManager: light_np.reparentTo(parent_node) # 挂载到父节点而不是render light_np.setPos(*pos) + # 确保变换矩阵有效 + light_np.setTransform(TransformState.makeIdentity()) + # 创建点光源对象 light = PointLight() - light.setPos(*pos) - light.energy = 5000 light.radius = 1000 light.inner_radius = 0.4 @@ -1188,6 +2621,7 @@ class SceneManager: # 设置节点属性和标签 light_np.setTag("light_type", "point_light") light_np.setTag("is_scene_element", "1") + light_np.setTag("tree_item_type", "LIGHT_NODE") light_np.setTag("light_energy", str(light.energy)) light_np.setTag("created_by_user", "1") @@ -1248,92 +2682,6 @@ class SceneManager: pass return None - def _importModelSingle(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): - """传统单一模型导入方法(兼容性保留)""" - try: - print(f"\n=== 使用传统模式导入模型: {filepath} ===") - - filepath = util.normalize_model_path(filepath) - original_filepath = filepath - - # 检查是否需要转换为GLB - if auto_convert_to_glb and self._shouldConvertToGLB(filepath): - print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") - converted_path = self._convertToGLBWithProgress(filepath) - if converted_path: - print(f"✅ 转换成功: {converted_path}") - filepath = converted_path - try: - from PyQt5.QtWidgets import QMessageBox - original_ext = os.path.splitext(original_filepath)[1].upper() - QMessageBox.information(None, "转换成功", - f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!") - except: - pass - else: - print(f"⚠️ 转换失败,使用原始文件") - - # 加载模型 - print("直接从文件加载模型...") - model = self.world.loader.loadModel(filepath) - if not model: - print("加载模型失败") - return None - - # 设置模型名称 - model_name = os.path.basename(filepath) - model.setName(model_name) - - # 将模型添加到场景 - model.reparentTo(self.world.render) - - # 设置标签和路径信息 - model.setTag("model_path", filepath) - model.setTag("original_path", original_filepath) - model.setTag("file", model_name) - model.setTag("is_model_root", "1") - model.setTag("is_scene_element", "1") - model.setTag("created_by_user", "1") - - if filepath != original_filepath: - model.setTag("converted_from", os.path.splitext(original_filepath)[1]) - model.setTag("converted_to_glb", "true") - - # 应用处理选项 - if apply_unit_conversion and filepath.lower().endswith('.fbx'): - print("应用FBX单位转换(厘米到米)...") - self._applyUnitConversion(model, 0.01) - model.setTag("unit_conversion_applied", "true") - - if normalize_scales and filepath.lower().endswith('.fbx'): - print("标准化FBX模型缩放层级...") - self._normalizeModelScales(model) - model.setTag("scale_normalization_applied", "true") - - # 调整模型位置到地面 - self._adjustModelToGround(model) - - # 创建并设置基础材质 - print("\n=== 开始设置材质 ===") - self._applyMaterialsToModel(model) - - # 设置碰撞检测(重要!用于选择功能) - print("\n=== 设置碰撞检测 ===") - self.setupCollision(model) - - # 添加到模型列表 - self.models.append(model) - - # 更新场景树 - self.updateSceneTree() - - print(f"=== 模型导入成功: {model_name} ===\n") - return model - - except Exception as e: - print(f"导入模型失败: {str(e)}") - return None - # def createSpotLight(self, pos=(0, 0, 0)): # """创建聚光灯 - 使用统一的create_item方法""" # try: @@ -1770,43 +3118,96 @@ except Exception as e: return False def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)): + """ + 加载 Cesium 3D Tileset - 采用新的创建逻辑,支持多选和更完善的UI交互。 + """ try: - print(f"加载 Cesium 3D Tiles: {tileset_url}") + from panda3d.core import NodePath + print(f"🗺️ 开始加载 Cesium 3D Tiles: {tileset_url}") - # 创建一个容器节点来管理tileset - node_name = f"cesium_tileset_{len(self.tilesets)}" - tileset_node = self.world.render.attachNewNode(node_name) - tileset_node.setPos(*position) + # 1. 获取UI控件和目标父节点 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None - #添加标签以便场景树识别 - tileset_node.setTag("is_scene_element","1") - tileset_node.setTag("element_type","cesium_tileset") - tileset_node.setTag("tileset_url",tileset_url) - tileset_node.setTag("file",f"tileset_{len(self.tilesets)}") + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点来附加Tileset") + return None - # 存储tileset信息 - tileset_info = { - 'url': tileset_url, - 'node': tileset_node, - 'position': position, - 'tiles': {} - } + created_tilesets = [] - self.tilesets.append(tileset_info) + # 2. 遍历所有选中的父节点,并为其创建Tileset + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + node_name = f"cesium_tileset_{len(self.tilesets)}" - # 创建一个临时的可视化占位符,让用户能看到节点已添加 - self._create_placeholder_geometry(tileset_node) + # 创建一个容器节点来管理tileset,并挂载到父节点 + tileset_node = parent_node.attachNewNode(node_name) + tileset_node.setPos(*position) - # 异步加载tileset数据 - self._load_tileset_async(tileset_url, tileset_info) + # 添加标签以便场景识别和保存 + tileset_node.setTag("is_scene_element", "1") + tileset_node.setTag("tree_item_type", "CESIUM_TILESET_NODE") + tileset_node.setTag("element_type", "cesium_tileset") + tileset_node.setTag("tileset_url", tileset_url) + # 使用唯一名称作为文件标识,代替索引 + tileset_node.setTag("file", node_name) - # 更新场景树 - self.updateSceneTree() - print(f"✓ Cesium 3D Tiles 加载请求已发送") - return tileset_node + # 存储tileset核心信息 + tileset_info = { + 'url': tileset_url, + 'node': tileset_node, + 'position': position, + 'tiles': {} # 用于后续管理瓦片 + } + self.tilesets.append(tileset_info) + + # 创建一个临时的可视化占位符,让用户能看到节点已添加 + self._create_placeholder_geometry(tileset_node) + + # 异步加载tileset的实际数据 + self._load_tileset_async(tileset_url, tileset_info) + + print(f"✅ 为 {parent_item.text(0)} 加载 Tileset 成功: {node_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(tileset_node, parent_item, "CESIUM_TILESET_NODE") + if qt_item: + created_tilesets.append((tileset_node, qt_item)) + else: + created_tilesets.append((tileset_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 加载 Tileset 失败: {str(e)}") + continue # 继续尝试为下一个父节点创建 + + # 3. 处理创建结果 + if not created_tilesets: + print("❌ 没有成功加载任何 Tileset") + return None + + # 选中最后创建的Tileset并更新UI + if created_tilesets: + last_tileset_node, last_qt_item = created_tilesets[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择状态和属性面板 + tree_widget.update_selection_and_properties(last_tileset_node, last_qt_item) + + print(f"🎉 总共加载了 {len(created_tilesets)} 个 Cesium Tileset 实例") + + # 4. 返回值处理 + if len(created_tilesets) == 1: + return created_tilesets[0][0] # 单个实例返回NodePath + else: + return [node for node, _ in created_tilesets] # 多个实例返回NodePath列表 except Exception as e: - print(f"❌ 加载 Cesium 3D Tiles 失败: {e}") + print(f"❌ 加载 Cesium 3D Tiles 过程失败: {str(e)}") import traceback traceback.print_exc() return None @@ -2059,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/scene/util.py b/scene/util.py index 678beba0..06ad4b23 100644 --- a/scene/util.py +++ b/scene/util.py @@ -25,9 +25,9 @@ class CrossPlatformPathHandler: def normalize_model_path(self, filepath): """标准化模型文件路径""" try: - print(f"\n=== 路径标准化处理 ===") - print(f"原始路径: {filepath}") - print(f"当前系统: {self.system}") + #print(f"\n=== 路径标准化处理 ===") + #print(f"原始路径: {filepath}") + #print(f"当前系统: {self.system}") # 步骤1: 检查原始路径是否存在 if self._check_file_exists(filepath): @@ -54,10 +54,6 @@ class CrossPlatformPathHandler: def _check_file_exists(self, filepath): """检查文件是否存在""" exists = os.path.exists(filepath) - if exists: - print(f"✓ 文件存在: {filepath}") - else: - print(f"⚠️ 文件不存在: {filepath}") return exists def _panda3d_normalize(self, filepath): diff --git a/scripts/RotatorScript.py b/scripts/RotatorScript.py index cea56986..bdc49495 100644 --- a/scripts/RotatorScript.py +++ b/scripts/RotatorScript.py @@ -21,6 +21,10 @@ class RotatorScript(ScriptBase): self.log(f"旋转速度: {self.rotation_speed_y}度/秒") def update(self, dt): + # 检查 gameObject 是否存在且不为空 + if not self.gameObject or self.gameObject.isEmpty(): + print("RotatorScript: gameObject is empty or None, skipping update") + return """每帧更新""" if not self.is_rotating: return diff --git a/threejs_panel.html b/threejs_panel.html new file mode 100644 index 00000000..ee6ef4c1 --- /dev/null +++ b/threejs_panel.html @@ -0,0 +1,106 @@ + + + + + Three.js Panel + + + +
+

场景信息面板

+

FPS: 0

+

对象数: 0

+
+
+ + + + + diff --git a/ui/icon_manager.py b/ui/icon_manager.py new file mode 100644 index 00000000..944c3aa6 --- /dev/null +++ b/ui/icon_manager.py @@ -0,0 +1,322 @@ +""" +图标管理工具 + +负责统一管理应用程序中的所有图标: +- 图标路径解析 +- 图标缓存 +- 图标预加载 +- 图标错误处理 +""" +import os +import sys +from typing import Dict, Optional +from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtCore import QSize + + +class IconManager: + """图标管理器类""" + + def __init__(self): + """初始化图标管理器""" + self.icon_cache: Dict[str, QIcon] = {} + self.icon_directory = self._get_icon_directory() + self.default_icon = None + + # 预定义的图标映射 + self.icon_map = { + # 主窗口图标 + 'app_logo': 'logo.png', + + # 工具栏图标 + 'select_tool': 'select_tool.png', + 'move_tool': 'move_tool.png', + 'rotate_tool': 'rotate_tool.png', + 'scale_tool': 'scale_tool.png', + + # 菜单图标(如果有的话) + 'new_file': 'new_file.png', + 'open_file': 'open_file.png', + 'save_file': 'save_file.png', + 'exit': 'exit.png', + + # 对象类型图标 + 'object_3d': 'object_3d.png', + 'light': 'light.png', + 'camera': 'camera.png', + 'terrain': 'terrain.png', + 'script': 'script.png', + + # 状态图标 + 'success': 'success.png', + 'warning': 'warning.png', + 'error': 'error.png', + 'info': 'info.png', + } + + # 初始化默认图标 + self._create_default_icon() + + # 预加载常用图标 + self._preload_icons() + + def _get_icon_directory(self) -> str: + """获取图标目录的绝对路径""" + # 获取当前文件的目录(ui目录) + current_dir = os.path.dirname(os.path.abspath(__file__)) + # 获取项目根目录(ui的父目录) + project_root = os.path.dirname(current_dir) + # 拼接icons目录路径 + icon_dir = os.path.join(project_root, "icons") + + print(f"🔍 图标目录路径: {icon_dir}") + + # 检查目录是否存在 + if not os.path.exists(icon_dir): + print(f"⚠️ 图标目录不存在: {icon_dir}") + # 尝试创建目录 + try: + os.makedirs(icon_dir, exist_ok=True) + print(f"✅ 已创建图标目录: {icon_dir}") + except Exception as e: + print(f"❌ 创建图标目录失败: {e}") + + return icon_dir + + def _create_default_icon(self): + """创建默认图标""" + # 创建一个简单的默认图标 + pixmap = QPixmap(16, 16) + pixmap.fill() # 填充为白色 + self.default_icon = QIcon(pixmap) + + def _preload_icons(self): + """预加载常用图标""" + print("🔄 开始预加载图标...") + + for icon_name, file_name in self.icon_map.items(): + icon_path = os.path.join(self.icon_directory, file_name) + if os.path.exists(icon_path): + try: + icon = QIcon(icon_path) + self.icon_cache[icon_name] = icon + print(f"✅ 已加载图标: {icon_name} -> {file_name}") + except Exception as e: + print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}") + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + + print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标") + + def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon: + """ + 获取图标 + + Args: + icon_name: 图标名称(可以是预定义名称或文件名) + size: 图标尺寸 + + Returns: + QIcon对象 + """ + # 首先检查缓存 + if icon_name in self.icon_cache: + icon = self.icon_cache[icon_name] + if size: + # 如果指定了尺寸,返回指定尺寸的图标 + pixmap = icon.pixmap(size) + return QIcon(pixmap) + return icon + + # 如果不在缓存中,尝试从映射中获取 + if icon_name in self.icon_map: + file_name = self.icon_map[icon_name] + icon_path = os.path.join(self.icon_directory, file_name) + else: + # 直接使用文件名 + icon_path = os.path.join(self.icon_directory, icon_name) + if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + icon_path += '.png' # 默认添加.png扩展名 + + # 尝试加载图标 + if os.path.exists(icon_path): + try: + icon = QIcon(icon_path) + # 缓存图标 + self.icon_cache[icon_name] = icon + print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}") + + if size: + pixmap = icon.pixmap(size) + return QIcon(pixmap) + return icon + except Exception as e: + print(f"❌ 加载图标失败: {icon_path}, 错误: {e}") + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + + # 返回默认图标 + return self.default_icon + + def get_icon_path(self, icon_name: str) -> str: + """ + 获取图标文件的完整路径 + + Args: + icon_name: 图标名称 + + Returns: + 图标文件的完整路径 + """ + if icon_name in self.icon_map: + file_name = self.icon_map[icon_name] + else: + file_name = icon_name + if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + file_name += '.png' + + icon_path = os.path.join(self.icon_directory, file_name) + + if os.path.exists(icon_path): + return icon_path + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + return "" + + def has_icon(self, icon_name: str) -> bool: + """ + 检查图标是否存在 + + Args: + icon_name: 图标名称 + + Returns: + 是否存在 + """ + return bool(self.get_icon_path(icon_name)) + + def add_icon(self, icon_name: str, icon_path: str) -> bool: + """ + 添加新图标到缓存 + + Args: + icon_name: 图标名称 + icon_path: 图标文件路径 + + Returns: + 是否添加成功 + """ + try: + if os.path.exists(icon_path): + icon = QIcon(icon_path) + self.icon_cache[icon_name] = icon + print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}") + return True + else: + print(f"❌ 图标文件不存在: {icon_path}") + return False + except Exception as e: + print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}") + return False + + def refresh_cache(self): + """刷新图标缓存""" + print("🔄 刷新图标缓存...") + self.icon_cache.clear() + self._preload_icons() + + def get_available_icons(self) -> list: + """获取所有可用的图标列表""" + available_icons = [] + + # 添加预定义的图标 + available_icons.extend(self.icon_map.keys()) + + # 扫描图标目录中的所有图标文件 + if os.path.exists(self.icon_directory): + for file_name in os.listdir(self.icon_directory): + if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + icon_name = os.path.splitext(file_name)[0] + if icon_name not in available_icons: + available_icons.append(icon_name) + + return sorted(available_icons) + + def get_cache_info(self) -> dict: + """获取缓存信息""" + return { + 'cached_icons': len(self.icon_cache), + 'icon_directory': self.icon_directory, + 'available_icons': len(self.get_available_icons()), + 'cache_keys': list(self.icon_cache.keys()) + } + + def debug_info(self): + """打印调试信息""" + print("=" * 50) + print("📋 图标管理器调试信息") + print("=" * 50) + + info = self.get_cache_info() + print(f"图标目录: {info['icon_directory']}") + print(f"目录存在: {os.path.exists(info['icon_directory'])}") + print(f"缓存图标数: {info['cached_icons']}") + print(f"可用图标数: {info['available_icons']}") + + if info['cache_keys']: + print("\n已缓存的图标:") + for key in info['cache_keys']: + print(f" - {key}") + + print("\n图标目录内容:") + if os.path.exists(self.icon_directory): + for file_name in os.listdir(self.icon_directory): + file_path = os.path.join(self.icon_directory, file_name) + size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0 + print(f" - {file_name} ({size} bytes)") + else: + print(" 目录不存在") + + print("=" * 50) + + +# 全局图标管理器实例 +_icon_manager = None + + +def get_icon_manager() -> IconManager: + """获取全局图标管理器实例""" + global _icon_manager + if _icon_manager is None: + _icon_manager = IconManager() + return _icon_manager + + +def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon: + """便捷函数:获取图标""" + return get_icon_manager().get_icon(icon_name, size) + + +def get_icon_path(icon_name: str) -> str: + """便捷函数:获取图标路径""" + return get_icon_manager().get_icon_path(icon_name) + + +def has_icon(icon_name: str) -> bool: + """便捷函数:检查图标是否存在""" + return get_icon_manager().has_icon(icon_name) + + +if __name__ == "__main__": + # 测试代码 + print("🧪 测试图标管理器...") + + manager = IconManager() + manager.debug_info() + + # 测试获取图标 + logo_icon = manager.get_icon('app_logo') + print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}") + + move_tool_icon = manager.get_icon('move_tool') + print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}") \ No newline at end of file diff --git a/ui/icon_manager_gui.py b/ui/icon_manager_gui.py new file mode 100644 index 00000000..bda6524a --- /dev/null +++ b/ui/icon_manager_gui.py @@ -0,0 +1,405 @@ +""" +图标管理器GUI工具 + +提供图形界面来管理和查看图标: +- 显示所有可用图标 +- 图标预览 +- 图标信息 +- 图标刷新 +""" +import os +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QListWidget, QListWidgetItem, QLabel, QGroupBox, + QTextEdit, QSplitter, QDialog, QDialogButtonBox, + QFileDialog, QMessageBox, QScrollArea, QGridLayout) +from PyQt5.QtGui import QIcon, QPixmap, QFont +from PyQt5.QtCore import Qt, QSize + +from ui.icon_manager import get_icon_manager + + +class IconPreviewWidget(QWidget): + """图标预览控件""" + + def __init__(self): + super().__init__() + self.setupUI() + + def setupUI(self): + """设置UI""" + layout = QVBoxLayout(self) + + # 图标显示区域 + self.icon_label = QLabel() + self.icon_label.setAlignment(Qt.AlignCenter) + self.icon_label.setStyleSheet(""" + QLabel { + border: 2px dashed #8b5cf6; + border-radius: 8px; + background-color: #2d2d44; + color: #e0e0ff; + min-height: 100px; + margin: 10px; + } + """) + self.icon_label.setText("选择图标查看预览") + layout.addWidget(self.icon_label) + + # 图标信息 + self.info_label = QLabel() + self.info_label.setStyleSheet(""" + QLabel { + background-color: #252538; + color: #e0e0ff; + padding: 10px; + border-radius: 4px; + font-family: monospace; + } + """) + self.info_label.setText("图标信息将在此显示") + layout.addWidget(self.info_label) + + def showIcon(self, icon_name: str, icon: QIcon): + """显示图标""" + if not icon.isNull(): + # 显示不同尺寸的图标 + sizes = [16, 24, 32, 48, 64] + pixmaps = [] + + # 创建组合图标显示 + for size in sizes: + pixmap = icon.pixmap(QSize(size, size)) + if not pixmap.isNull(): + pixmaps.append((size, pixmap)) + + if pixmaps: + # 创建合成图片显示多个尺寸 + total_width = sum(size for size, _ in pixmaps) + 20 * (len(pixmaps) - 1) + max_height = max(size for size, _ in pixmaps) + + combined_pixmap = QPixmap(total_width, max_height + 40) + combined_pixmap.fill(Qt.transparent) + + from PyQt5.QtGui import QPainter, QPen + painter = QPainter(combined_pixmap) + + x = 0 + for size, pixmap in pixmaps: + # 绘制图标 + y = (max_height - size) // 2 + painter.drawPixmap(x, y, pixmap) + + # 绘制尺寸标签 + painter.setPen(QPen(Qt.white)) + painter.drawText(x, max_height + 15, f"{size}x{size}") + + x += size + 20 + + painter.end() + + self.icon_label.setPixmap(combined_pixmap) + else: + self.icon_label.setText("无法加载图标") + else: + self.icon_label.setText("图标无效") + + # 更新信息 + info_text = f"图标名称: {icon_name}\n" + info_text += f"图标有效: {'是' if not icon.isNull() else '否'}\n" + + # 获取图标管理器信息 + icon_manager = get_icon_manager() + icon_path = icon_manager.get_icon_path(icon_name) + if icon_path: + info_text += f"文件路径: {icon_path}\n" + if os.path.exists(icon_path): + size = os.path.getsize(icon_path) + info_text += f"文件大小: {size} bytes\n" + + # 获取可用尺寸 + if not icon.isNull(): + available_sizes = icon.availableSizes() + if available_sizes: + sizes_text = ", ".join(f"{s.width()}x{s.height()}" for s in available_sizes) + info_text += f"可用尺寸: {sizes_text}\n" + + self.info_label.setText(info_text) + + +class IconManagerDialog(QDialog): + """图标管理器对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.icon_manager = get_icon_manager() + self.setupUI() + self.loadIcons() + + def setupUI(self): + """设置UI""" + self.setWindowTitle("图标管理器") + self.setModal(False) + self.resize(800, 600) + + # 设置样式 + self.setStyleSheet(""" + QDialog { + background-color: #1e1e2e; + color: #e0e0ff; + } + QListWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + alternate-background-color: #2d2d44; + } + QListWidget::item { + padding: 8px; + border-bottom: 1px solid #3a3a4a; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QGroupBox { + background-color: #252538; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; + } + QGroupBox::title { + subline-offset: -2px; + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + QTextEdit { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + font-family: monospace; + } + """) + + layout = QVBoxLayout(self) + + # 顶部按钮栏 + button_layout = QHBoxLayout() + + self.refresh_btn = QPushButton("刷新图标") + self.refresh_btn.clicked.connect(self.refreshIcons) + button_layout.addWidget(self.refresh_btn) + + self.add_icon_btn = QPushButton("添加图标") + self.add_icon_btn.clicked.connect(self.addIcon) + button_layout.addWidget(self.add_icon_btn) + + self.debug_btn = QPushButton("调试信息") + self.debug_btn.clicked.connect(self.showDebugInfo) + button_layout.addWidget(self.debug_btn) + + button_layout.addStretch() + layout.addLayout(button_layout) + + # 主分割器 + splitter = QSplitter(Qt.Horizontal) + + # 左侧:图标列表 + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + list_group = QGroupBox("可用图标") + list_layout = QVBoxLayout(list_group) + + self.icon_list = QListWidget() + self.icon_list.itemSelectionChanged.connect(self.onIconSelected) + list_layout.addWidget(self.icon_list) + + left_layout.addWidget(list_group) + splitter.addWidget(left_widget) + + # 右侧:图标预览 + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + preview_group = QGroupBox("图标预览") + preview_layout = QVBoxLayout(preview_group) + + self.preview_widget = IconPreviewWidget() + preview_layout.addWidget(self.preview_widget) + + right_layout.addWidget(preview_group) + splitter.addWidget(right_widget) + + # 设置分割器比例 + splitter.setSizes([300, 500]) + layout.addWidget(splitter) + + # 底部按钮 + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.rejected.connect(self.close) + layout.addWidget(button_box) + + def loadIcons(self): + """加载图标列表""" + self.icon_list.clear() + + available_icons = self.icon_manager.get_available_icons() + + for icon_name in available_icons: + item = QListWidgetItem() + + # 获取图标 + icon = self.icon_manager.get_icon(icon_name, QSize(24, 24)) + + # 设置图标和文本 + if not icon.isNull(): + item.setIcon(icon) + item.setText(f"🎨 {icon_name}") + else: + item.setText(f"❌ {icon_name}") + + item.setData(Qt.UserRole, icon_name) + self.icon_list.addItem(item) + + print(f"📊 加载了 {len(available_icons)} 个图标") + + def onIconSelected(self): + """当选择图标时""" + current_item = self.icon_list.currentItem() + if current_item: + icon_name = current_item.data(Qt.UserRole) + icon = self.icon_manager.get_icon(icon_name) + self.preview_widget.showIcon(icon_name, icon) + + def refreshIcons(self): + """刷新图标""" + print("🔄 刷新图标缓存...") + self.icon_manager.refresh_cache() + self.loadIcons() + QMessageBox.information(self, "完成", "图标缓存已刷新") + + def addIcon(self): + """添加新图标""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择图标文件", + "", + "图像文件 (*.png *.jpg *.jpeg *.svg *.ico);;所有文件 (*)" + ) + + if file_path: + # 获取文件名作为图标名称 + file_name = os.path.basename(file_path) + icon_name = os.path.splitext(file_name)[0] + + # 复制文件到图标目录 + import shutil + target_path = os.path.join(self.icon_manager.icon_directory, file_name) + + try: + shutil.copy2(file_path, target_path) + + # 添加到缓存 + success = self.icon_manager.add_icon(icon_name, target_path) + + if success: + QMessageBox.information(self, "成功", f"图标 '{icon_name}' 已添加") + self.loadIcons() + else: + QMessageBox.warning(self, "失败", "添加图标失败") + + except Exception as e: + QMessageBox.critical(self, "错误", f"复制文件失败:\n{str(e)}") + + def showDebugInfo(self): + """显示调试信息""" + debug_dialog = QDialog(self) + debug_dialog.setWindowTitle("图标管理器调试信息") + debug_dialog.resize(600, 400) + + layout = QVBoxLayout(debug_dialog) + + text_edit = QTextEdit() + text_edit.setFont(QFont("Consolas", 10)) + + # 获取调试信息 + info = self.icon_manager.get_cache_info() + debug_text = "图标管理器调试信息\n" + debug_text += "=" * 50 + "\n\n" + debug_text += f"图标目录: {info['icon_directory']}\n" + debug_text += f"目录存在: {os.path.exists(info['icon_directory'])}\n" + debug_text += f"缓存图标数: {info['cached_icons']}\n" + debug_text += f"可用图标数: {info['available_icons']}\n\n" + + debug_text += "已缓存的图标:\n" + for key in info['cache_keys']: + debug_text += f" - {key}\n" + + debug_text += "\n图标目录内容:\n" + if os.path.exists(info['icon_directory']): + for file_name in os.listdir(info['icon_directory']): + file_path = os.path.join(info['icon_directory'], file_name) + if os.path.isfile(file_path): + size = os.path.getsize(file_path) + debug_text += f" - {file_name} ({size} bytes)\n" + else: + debug_text += " 目录不存在\n" + + text_edit.setPlainText(debug_text) + layout.addWidget(text_edit) + + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.rejected.connect(debug_dialog.close) + layout.addWidget(button_box) + + debug_dialog.exec_() + + +def show_icon_manager(parent=None): + """显示图标管理器对话框""" + dialog = IconManagerDialog(parent) + dialog.show() + return dialog + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + import sys + + app = QApplication(sys.argv) + + # 设置全局样式 + app.setStyleSheet(""" + QApplication { + background-color: #1e1e2e; + color: #e0e0ff; + } + """) + + dialog = IconManagerDialog() + dialog.show() + + sys.exit(app.exec_()) \ No newline at end of file diff --git a/ui/interface_manager.py b/ui/interface_manager.py index 3e802bfb..e5e0c626 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -25,10 +25,39 @@ class InterfaceManager: # 更新场景树 self.world.scene_manager.updateSceneTree() + def onTreeWidgetClicked(self, index): + """处理树形控件点击事件(包括空白区域)""" + # 检查点击的是否是空白区域 + if not self.treeWidget.itemFromIndex(index): # 点击的是空白区域 + self.world.selection.updateSelection(None) + self.world.property_panel.clearPropertyPanel() + print("点击树形控件空白区域,清除选中状态") + + def onTreeCurrentItemChanged(self, current, previous): + """处理树形控件当前选中项改变事件""" + # 当 current 为 None 时,表示点击了空白区域 + if current is None: + self.world.selection.updateSelection(None) + print("点击空白区域,清除选中状态") + # 当 current 不为 None 时,表示选中了某个项目 + else: + # 更新选择状态 + nodePath = current.data(0, Qt.UserRole) + if nodePath: + self.world.selected_np = nodePath + self.world.selection.updateSelection(nodePath) + self.world.property_panel.updatePropertyPanel(current) + print(f"树形控件选中项改变: {current.text(0)}") + def onTreeItemClicked(self, item, column): """处理树形控件项目点击事件""" - if not item: + print(f"树形控件点击事件触发,item: {item}, column: {column}") + + # 检查是否点击了空白区域 + # 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象 + if not item or (item.text(0) == "" and item.data(0, Qt.UserRole) is None): self.world.selection.updateSelection(None) + print("点击空白区域,清除选中状态") return self.world.property_panel.updatePropertyPanel(item) @@ -39,187 +68,90 @@ class InterfaceManager: # 更新选择状态 self.world.selected_np = nodePath self.world.selection.updateSelection(nodePath) - - # 更新属性面板 - #self.world.property_panel.updatePropertyPanel(item) - print(f"树形控件点击: {item.text(0)}") else: # 如果没有节点对象,清除选择 self.world.selection.updateSelection(None) - #self.world.property_panel.clearPropertyPanel() - - def showTreeContextMenu(self, position): - """显示树形控件的右键菜单""" - item = self.treeWidget.itemAt(position) - if not item: - return - - # 获取节点对象 - nodePath = item.data(0, Qt.UserRole) - if not nodePath: - return - - # 创建菜单 - menu = QMenu() - - # 检查是否是GUI元素 - if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"): - # GUI元素菜单 - editAction = menu.addAction("编辑") - editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath)) - - deleteAction = menu.addAction("删除GUI元素") - deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath)) - - duplicateAction = menu.addAction("复制") - duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath)) - - elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset": - deleteAction = menu.addAction("删除 Cesium Tileset") - deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) - - else: - #灯光节点添加特殊处理 - if self.isLightNode(nodePath): - deleteAction = menu.addAction("删除灯光") - deleteAction.triggered.connect(lambda:self.deleteLightNode(nodePath,item)) - else: - deleteAction = menu.addAction("删除") - deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) - - # 显示菜单 - menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) - - def isLightNode(self, nodePath): - try: - if not nodePath or nodePath.isEmpty(): - return False - - # 修复:统一使用 rp_light_object - if hasattr(nodePath, 'getPythonTag'): - light_object = nodePath.getPythonTag('rp_light_object') - if light_object is not None: - return True - - if hasattr(nodePath, 'getTag'): - light_type = nodePath.getTag('light_type') - if light_type in ["spot_light", "point_light"]: - return True - - if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight: - return True - if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight: - return True - - return False - except Exception as e: - print(f"判断节点是否是灯光节点失败: {str(e)}") - return False - - def deleteLightNode(self, nodePath, item): - """专门处理灯光节点的删除""" - try: - print(f"开始删除灯光节点: {nodePath.getName()}") - - # 从RenderPipeline中移除灯光(如果存在) - if hasattr(nodePath, 'getPythonTag'): - light_object = nodePath.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - print("从RenderPipeline移除灯光") - self.world.render_pipeline.remove_light(light_object) - nodePath.clearPythonTag('rp_light_object') - - if hasattr(self.world,'Spotlight') and nodePath in self.world.Spotlight: - self.world.Spotlight.remove(nodePath) - print("从Spotlight列表中删除") - if hasattr(self.world,'Pointlight') and nodePath in self.world.Pointlight: - self.world.Pointlight.remove(nodePath) - print("从Pointlight列表中移除") - - if hasattr(self.world,'selection'): - if self.world.selection.selectedNode == nodePath: - self.world.selection.clearSelectionBox() - self.world.selection.clearGizmo() - self.world.selection.selectedNode = None - self.world.selection.selectedObject = None - - print(f"移除节点{nodePath.getName()}") - nodePath.removeNode() - - parentItem = item.parent() - if parentItem: - parentItem.removeChild(item) - - print(f"成功删除灯光节点{nodePath.getName()}") - - if hasattr(self.world,'property_panel'): - self.world.property_panel.clearPropertyPanel() - if hasattr(self.world,'selection'): - self.world.selection.updateSelection(None) - - except Exception as e: - print(f"删除灯光节点失败: {str(e)}") - - def _recursiveRemoveLights(self, nodePath): - """递归删除节点及其子节点中的所有灯光""" - if nodePath.isEmpty(): - return - - # 先递归处理所有子节点 - for child in nodePath.getChildren(): - self._recursiveRemoveLights(child) - - # 然后处理当前节点 - if self.isLightNode(nodePath): - print(f"删除子灯光节点: {nodePath.getName()}") - - # 从RenderPipeline中移除灯光 - if hasattr(nodePath, 'getPythonTag'): - light_object = nodePath.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) - nodePath.clearPythonTag('rp_light_object') - - # 从灯光列表中移除 - if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight: - self.world.Spotlight.remove(nodePath) - if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight: - self.world.Pointlight.remove(nodePath) - - def deleteCesiumTileset(self, nodePath, item): - """删除 Cesium tileset""" - try: - # 从场景中移除 - nodePath.removeNode() - - # 从 tilesets 列表中移除 - if hasattr(self.world, 'scene_manager'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - if tileset_info['node'] == nodePath: - tilesets_to_remove.append(i) - - # 从后往前删除,避免索引问题 - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - - # 从树形控件中移除 - parentItem = item.parent() - if parentItem: - parentItem.removeChild(item) - - print(f"成功删除 Cesium tileset: {nodePath.getName()}") - - # 清空属性面板和选择框 self.world.property_panel.clearPropertyPanel() - self.world.selection.updateSelection(None) + print("点击了无数据项,清除选中状态") - # 更新场景树 - self.updateSceneTree() - - except Exception as e: - print(f"删除 Cesium tileset 失败: {str(e)}") + # def showTreeContextMenu(self, position): + # """显示树形控件的右键菜单""" + # item = self.treeWidget.itemAt(position) + # if not item: + # return + # + # # 获取节点对象 + # nodePath = item.data(0, Qt.UserRole) + # if not nodePath: + # return + # + # # 创建菜单 + # menu = QMenu() + # + # # 检查是否是GUI元素 + # if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"): + # # GUI元素菜单 + # editAction = menu.addAction("编辑") + # editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath)) + # + # deleteAction = menu.addAction("删除GUI元素") + # deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath)) + # + # duplicateAction = menu.addAction("复制") + # duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath)) + # + # elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset": + # deleteAction = menu.addAction("删除 Cesium Tileset") + # deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) + # + # else: + # # 为模型节点或其子节点添加删除选项 + # parentItem = item.parent() + # if parentItem: + # if self.isModelOrChild(item): + # deleteAction = menu.addAction("删除") + # deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) + # else: + # deleteAction = menu.addAction("删除") + # deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item)) + # + # # 显示菜单 + # menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) + # + # def deleteCesiumTileset(self, nodePath, item): + # """删除 Cesium tileset""" + # try: + # # 从场景中移除 + # nodePath.removeNode() + # + # # 从 tilesets 列表中移除 + # if hasattr(self.world, 'scene_manager'): + # tilesets_to_remove = [] + # for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + # if tileset_info['node'] == nodePath: + # tilesets_to_remove.append(i) + # + # # 从后往前删除,避免索引问题 + # for i in reversed(tilesets_to_remove): + # del self.world.scene_manager.tilesets[i] + # + # # 从树形控件中移除 + # parentItem = item.parent() + # if parentItem: + # parentItem.removeChild(item) + # + # print(f"成功删除 Cesium tileset: {nodePath.getName()}") + # + # # 清空属性面板和选择框 + # self.world.property_panel.clearPropertyPanel() + # self.world.selection.updateSelection(None) + # + # # 更新场景树 + # self.updateSceneTree() + # + # except Exception as e: + # print(f"删除 Cesium tileset 失败: {str(e)}") def isModelOrChild(self, item): """检查是否是模型节点或其子节点""" @@ -248,9 +180,9 @@ class InterfaceManager: if terrain_to_remove: self.world.terrain_manager.deleteTerrain(terrain_to_remove) print(f"成功删除地形节点:{nodePath.getName()}") - self.updateSceneTree() - self.world.property_panel.clearPropertyPanel() - self.world.selection.updateSelection(None) + # self.updateSceneTree() + # self.world.property_panel.clearPropertyPanel() + # self.world.selection.updateSelection(None) return # 先递归删除所有子节点中的灯光 @@ -408,10 +340,19 @@ class InterfaceManager: groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") #添加灯光节点 - for light in self.world.Spotlight + self.world.Pointlight: - if not light.isEmpty: + for light in self.world.Spotlight: + if light: addNodeToTree(light, sceneRoot, force=True) + for light in self.world.Pointlight: + if light: + addNodeToTree(light, sceneRoot, force=True) + + # for light in self.world.Spotlight + self.world.Pointlight: + # if not light.isEmpty: + # print(f"33333333333333333333333333333{light}") + # addNodeToTree(light, sceneRoot, force=True) + #添加 Cesium tilesets if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'): for i , tileset_info in enumerate(self.world.scene_manager.tilesets): diff --git a/ui/main_window.py b/ui/main_window.py index bce4d014..4fb010b5 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -6,29 +6,154 @@ - 停靠窗口设置 - 事件连接和信号处理 """ - +import os import sys -from PyQt5.QtGui import QKeySequence, QIcon +from PyQt5.QtGui import QKeySequence, QIcon, QPalette, QColor +from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout, QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog, QSpinBox, QFrame) -from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint +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 +try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + WEB_ENGINE_AVAILABLE = True +except ImportError: + QWebEngineView = None + WEB_ENGINE_AVAILABLE = False class MainWindow(QMainWindow): """主窗口类""" - + def __init__(self, world): super().__init__() self.world = world self.world.main_window = self # 关键:让world对象能访问主窗口 + #剪切板相关属性 + self.clipboard = [] + self.clipboard_mode = None + + # 初始化图标管理器并打印调试信息 + self.icon_manager = get_icon_manager() + print("🔧 图标管理器初始化完成") + self.icon_manager.debug_info() + + self.setStyleSheet(""" + QMainWindow { + background-color: #1e1e2e; + } + QMenuBar { + background-color: #252538; + color: #e0e0ff; + border-bottom: 1px solid #3a3a4a; + } + QMenuBar::item { + background-color: transparent; + padding: 4px 8px; + } + QMenuBar::item:selected { + background-color: rgba(139, 92, 246, 100); + } + QMenuBar::item:pressed { + background-color: rgba(139, 92, 246, 150); + } + QMenu { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QMenu::item { + padding: 4px 20px; + } + QMenu::item:selected { + background-color: rgba(139, 92, 246, 100); + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QComboBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px 8px; + } + QComboBox::drop-down { + border: none; + } + QComboBox QAbstractItemView { + background-color: #2d2d44; + color: #e0e0ff; + selection-background-color: rgba(139, 92, 246, 100); + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + QSpinBox, QDoubleSpinBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + QScrollBar:vertical { + background-color: #252538; + width: 15px; + border: none; + } + QScrollBar::handle:vertical { + background-color: #3a3a4a; + border-radius: 4px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background-color: #8b5cf6; + } + QScrollBar:horizontal { + background-color: #252538; + width: 15px; + border: none; + } + QScrollBar::handle:horizontal { + background-color: #3a3a4a; + border-radius: 4px; + min-height: 20px; + } + QScrollBar::handle:horizontal:hover { + background-color: #8b5cf6; + } + """) + # 设置 QMessageBox 样式表 + self.setupMessageBoxStyles() + self.setupCenterWidget() # 创建中间部分Panda3D self.setupMenus() # 创建菜单栏 self.setupDockWindows() @@ -47,11 +172,184 @@ class MainWindow(QMainWindow): self.dragStartPos = QPoint(0, 0) self.toolbarStartPos = QPoint(0, 0) + def setupMessageBoxStyles(self): + """设置 QMessageBox 的全局样式""" + # 设置 QMessageBox 的样式表 + msg_box_style = """ + QMessageBox { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QMessageBox QLabel { + color: #e0e0ff; + } + QMessageBox QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + min-width: 80px; + } + QMessageBox QPushButton:hover { + background-color: #7c3aed; + } + QMessageBox QPushButton:pressed { + background-color: #6d28d9; + } + QMessageBox QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + """ + + # 应用全局样式 + self.setStyleSheet(self.styleSheet() + msg_box_style) + + def createStyledInputDialog(self, parent, title, label, mode=QLineEdit.Normal, text=""): + """创建带有统一主题样式的 QInputDialog""" + dialog = QInputDialog(parent) + dialog.setWindowTitle(title) + dialog.setLabelText(label) + dialog.setTextEchoMode(mode) + dialog.setTextValue(text) + + # 设置样式表 + dialog.setStyleSheet(""" + QInputDialog { + background-color: #252538; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + font-weight: 500; + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 6px; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + min-width: 80px; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + """) + + return dialog + + def createStyledFileDialog(self, parent, caption, directory="", filter=""): + """创建带有统一主题样式的 QFileDialog""" + dialog = QFileDialog(parent) + dialog.setWindowTitle(caption) + + # 设置样式表 + dialog.setStyleSheet(""" + QFileDialog { + background-color: #252538; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + } + QListView { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QListView::item:hover { + background-color: #3a3a4a; + } + QListView::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QTreeView { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QTreeView::item:hover { + background-color: #3a3a4a; + } + QTreeView::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QComboBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px 8px; + } + QComboBox::drop-down { + border: none; + } + QComboBox QAbstractItemView { + background-color: #2d2d44; + color: #e0e0ff; + selection-background-color: rgba(139, 92, 246, 100); + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + """) + + return dialog def setupCenterWidget(self): """设置窗口基本属性""" self.setWindowTitle("引擎编辑器") - + # 使用图标管理器设置窗口图标 + app_icon = get_icon('app_logo') + if not app_icon.isNull(): + self.setWindowIcon(app_icon) + print("✅ 应用图标设置成功") + else: + print("⚠️ 应用图标设置失败,使用默认图标") # 使用自定义的 Panda3D 部件作为中央部件 self.pandaWidget = CustomPanda3DWidget(self.world) self.setCentralWidget(self.pandaWidget) @@ -69,33 +367,75 @@ class MainWindow(QMainWindow): int(screen.height() / 2 - self.height() / 2), ) + @staticmethod + def get_icon_path(icon_name): + """获取图标文件的完整路径""" + # 假设 icons 文件夹在项目根目录下 + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + icon_path = os.path.join(project_root, "icons", icon_name) + + # 检查文件是否存在,如果不存在则返回默认值或None + if not os.path.exists(icon_path): + print(f"警告: 图标文件不存在: {icon_path}") + return "" # 返回空字符串,QIcon会处理空路径 + + return icon_path + def setupEmbeddedToolbar(self): """创建Unity风格的内嵌工具栏""" # 创建工具栏容器 self.embeddedToolbar = QFrame(self.pandaWidget) self.embeddedToolbar.setObjectName("UnityToolbar") + # self.embeddedToolbar.setStyleSheet(""" + # QFrame#UnityToolbar { + # background-color: rgba(240, 240, 240, 180); + # border: 1px solid rgba(200, 200, 200, 200); + # border-radius: 4px; + # } + # QToolButton { + # background-color: rgba(250, 250, 250, 150); + # border: none; + # color: #333333; + # padding: 5px; + # border-radius: 3px; + # } + # QToolButton:hover { + # background-color: rgba(220, 220, 220, 200); + # } + # QToolButton:checked { + # background-color: rgba(100, 150, 220, 180); + # color: white; + # } + # QToolButton:pressed { + # background-color: rgba(80, 130, 200, 200); + # } + # """) self.embeddedToolbar.setStyleSheet(""" QFrame#UnityToolbar { - background-color: rgba(240, 240, 240, 180); - border: 1px solid rgba(200, 200, 200, 200); - border-radius: 4px; + background-color: rgba(40, 40, 60, 200); /* 深蓝灰色背景 */ + border: 1px solid rgba(80, 80, 120, 200); /* 深蓝灰边框 */ + border-radius: 6px; + padding: 4px; } QToolButton { - background-color: rgba(250, 250, 250, 150); - border: none; - color: #333333; - padding: 5px; - border-radius: 3px; + background-color: rgba(60, 60, 90, 180); /* 稍亮的深蓝灰 */ + border: 1px solid rgba(100, 100, 150, 150); + color: #e0e0ff; /* 浅蓝白色文字 */ + padding: 6px 8px; + border-radius: 4px; + font-weight: 500; } QToolButton:hover { - background-color: rgba(220, 220, 220, 200); + background-color: rgba(80, 80, 130, 200); /* 悬停时更亮 */ + border: 1px solid rgba(139, 92, 246, 150); /* 紫色边框 */ } QToolButton:checked { - background-color: rgba(100, 150, 220, 180); + background-color: rgba(139, 92, 246, 180); /* 紫色背景 */ color: white; + border: 1px solid rgba(180, 140, 255, 200); } QToolButton:pressed { - background-color: rgba(80, 130, 200, 200); + background-color: rgba(120, 75, 220, 200); /* 按下时稍暗的紫色 */ } """) @@ -109,8 +449,11 @@ class MainWindow(QMainWindow): # 选择工具 self.selectTool = QToolButton() - self.selectTool.setIcon(QIcon("icons/select_tool.png")) # 使用图标资源 - self.selectTool.setText('选择') + select_icon = get_icon('select_tool', QSize(16, 16)) + if not select_icon.isNull(): + self.selectTool.setIcon(select_icon) + else: + self.selectTool.setText('选择') # 如果没有图标则显示文字 self.selectTool.setIconSize(QSize(16, 16)) self.selectTool.setCheckable(True) self.selectTool.setToolTip("选择工具 (Q)") @@ -120,8 +463,10 @@ class MainWindow(QMainWindow): # 移动工具 self.moveTool = QToolButton() - self.moveTool.setIcon(QIcon("icons/move_tool.png")) - self.moveTool.setText('移动') + icon_path = self.get_icon_path("move_tool.png") + if icon_path and os.path.exists(icon_path): + self.moveTool.setIcon(QIcon(icon_path)) + self.moveTool.setText("移动") self.moveTool.setIconSize(QSize(16, 16)) self.moveTool.setCheckable(True) self.moveTool.setToolTip("移动工具 (W)") @@ -131,8 +476,10 @@ class MainWindow(QMainWindow): # 旋转工具 self.rotateTool = QToolButton() - self.rotateTool.setIcon(QIcon("icons/rotate_tool.png")) - self.rotateTool.setText('旋转') + rotate_icon = get_icon('rotate_tool', QSize(16, 16)) + if not rotate_icon.isNull(): + self.rotateTool.setIcon(rotate_icon) + self.rotateTool.setText("旋转") self.rotateTool.setIconSize(QSize(16, 16)) self.rotateTool.setCheckable(True) self.rotateTool.setToolTip("旋转工具 (E)") @@ -142,8 +489,10 @@ class MainWindow(QMainWindow): # 缩放工具 self.scaleTool = QToolButton() - self.scaleTool.setIcon(QIcon("icons/scale_tool.png")) - self.scaleTool.setText('缩放') + scale_icon = get_icon('scale_tool', QSize(16, 16)) + if not scale_icon.isNull(): + self.scaleTool.setIcon(scale_icon) + self.scaleTool.setText("缩放") self.scaleTool.setIconSize(QSize(16, 16)) self.scaleTool.setCheckable(True) self.scaleTool.setToolTip("缩放工具 (R)") @@ -309,8 +658,12 @@ class MainWindow(QMainWindow): self.moveAction = self.toolsMenu.addAction('移动工具') self.rotateAction = self.toolsMenu.addAction('旋转工具') self.scaleAction = self.toolsMenu.addAction('缩放工具') + self.toolsMenu.addSeparator() self.sunsetAction = self.toolsMenu.addAction('光照编辑') self.pluginAction = self.toolsMenu.addAction('图形编辑') + # self.toolsMenu.addSeparator() + # self.iconManagerAction = self.toolsMenu.addAction('图标管理器') + # self.iconManagerAction.triggered.connect(self.onOpenIconManager) # 统一创建菜单 - 关键修改 self.createMenu = menubar.addMenu('创建') @@ -384,19 +737,29 @@ class MainWindow(QMainWindow): self.createSamplePanelAction = self.infoPanelMenu.addAction('创建示例面板') self.createSamplePanelAction.triggered.connect(self.onCreateSampleInfoPanel) # 添加更多面板创建选项 - self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板') - self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel) + # self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板') + # self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel) + # + # self.createSensorDataPanelAction = self.infoPanelMenu.addAction('创建传感器数据面板') + # self.createSensorDataPanelAction.triggered.connect(self.onCreateSensorDataPanel) + # + # self.createSceneInfoPanelAction = self.infoPanelMenu.addAction('创建场景信息面板') + # self.createSceneInfoPanelAction.triggered.connect(self.onCreateSceneInfoPanel) - self.createSensorDataPanelAction = self.infoPanelMenu.addAction('创建传感器数据面板') - self.createSensorDataPanelAction.triggered.connect(self.onCreateSensorDataPanel) - - self.createSceneInfoPanelAction = self.infoPanelMenu.addAction('创建场景信息面板') - self.createSceneInfoPanelAction.triggered.connect(self.onCreateSceneInfoPanel) + self.infoPanelMenu.addSeparator() + self.create3DSamplePanelAction = self.infoPanelMenu.addAction('创建3D实例面板') + self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel) + # 添加网页浏览器菜单项 + self.webBrowserAction = self.infoPanelMenu.addAction("信息面板") + self.webBrowserAction.triggered.connect(self.openWebBrowser) + # + # self.create3DSystemStatusPanelAction = self.infoPanelMenu.addAction('创建3D系统状态面板') + # self.create3DSystemStatusPanelAction.triggered.connect(self.onCreate3DSystemStatusPanel) # 添加分隔符和批量创建选项 - self.infoPanelMenu.addSeparator() - self.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板') - self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels) + # self.infoPanelMenu.addSeparator() + # self.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板') + # self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels) @@ -450,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对象的创建方法 @@ -497,6 +1092,53 @@ class MainWindow(QMainWindow): """创建停靠窗口""" # 创建左侧停靠窗口(层级窗口) self.leftDock = QDockWidget("层级", self) + self.leftDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + QTreeView { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QTreeView::item:hover { + background-color: #3a3a4a; + } + QTreeView::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + """) self.treeWidget = CustomTreeWidget(self.world) self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用 self.leftDock.setWidget(self.treeWidget) @@ -504,6 +1146,64 @@ class MainWindow(QMainWindow): # 创建右侧停靠窗口(属性窗口) self.rightDock = QDockWidget("属性", self) + self.rightDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + QScrollArea { + background-color: #1e1e2e; + border: none; + } + QWidget#PropertyContainer { + background-color: #1e1e2e; + } + QLabel { + color: #e0e0ff; + } + QGroupBox { + background-color: #252538; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ + } + QGroupBox::title { + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + """) # 创建属性面板的主容器和布局 self.propertyContainer = QWidget() @@ -552,9 +1252,73 @@ class MainWindow(QMainWindow): # 创建脚本管理停靠窗口 self.scriptDock = QDockWidget("脚本管理", self) + self.scriptDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + QScrollArea { + background-color: #1e1e2e; + border: none; + } + QWidget#PropertyContainer { + background-color: #1e1e2e; + } + QLabel { + color: #e0e0ff; + } + QGroupBox { + background-color: #252538; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ + } + QGroupBox::title { + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + """) # 创建脚本面板的主容器和布局(与属性面板相同结构) self.scriptContainer = QWidget() + # 设置脚本容器的背景色 + self.scriptContainer.setStyleSheet(""" + QWidget#ScriptContainer { + background-color: #1e1e2e; + } + """) self.scriptContainer.setObjectName("ScriptContainer") self.scriptLayout = QVBoxLayout(self.scriptContainer) @@ -571,9 +1335,6 @@ class MainWindow(QMainWindow): self.setupScriptPanel(self.scriptLayout) self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock) - # 将右侧停靠窗口设为标签形式 - self.tabifyDockWidget(self.rightDock, self.scriptDock) - # # 创建底部停靠窗口(资源窗口) # self.bottomDock = QDockWidget("资源", self) # self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea) @@ -599,17 +1360,181 @@ class MainWindow(QMainWindow): # self.bottomDock.setWidget(self.fileView) # self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) + # 创建底部停靠窗口(资源窗口) self.bottomDock = QDockWidget("资源", self) + self.bottomDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + """) + self.fileView = CustomAssetsTreeWidget(self.world) + # 为资源树添加样式 + self.fileView.setStyleSheet(""" + QTreeWidget { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QTreeWidget::item:hover { + background-color: #3a3a4a; + } + QTreeWidget::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QHeaderView::section { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + padding: 4px; + } + """) self.bottomDock.setWidget(self.fileView) self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) # 创建底部停靠控制台 - # self.consoleDock = QDockWidget("控制台", self) - # self.consoleView = CustomConsoleDockWidget(self.world) - # self.consoleDock.setWidget(self.consoleView) - # self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) + self.consoleDock = QDockWidget("控制台", self) + self.consoleDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + """) + self.consoleView = CustomConsoleDockWidget(self.world) + # 为控制台添加样式 + self.consoleView.setStyleSheet(""" + QTextEdit { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + font-family: 'Consolas', 'Monaco', monospace; + } + """) + self.consoleDock.setWidget(self.consoleView) + self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) + + # 将右侧停靠窗口设为标签形式 + # self.tabifyDockWidget(self.rightDock, self.scriptDock) + # # 将底部的两个窗口也标签化 + # self.tabifyDockWidget(self.bottomDock, self.consoleDock) + # 设定默认显示的标签 + self.bottomDock.raise_() + self.rightDock.raise_() + self.scriptDock.raise_() + self.consoleDock.raise_() + self.leftDock.raise_() + # ========================================================================= + # ↓↓↓ 新增代码:为停靠窗口的标签栏(QTabBar)设置统一样式 ↓↓↓ + # ========================================================================= + # 这段样式会应用到主窗口内的所有 QTabBar,特别是停靠区域的标签栏。 + tab_bar_style = """ + /* QTabBar 的整体样式 */ + QTabBar { + qproperty-drawBase: 0; /* 移除标签栏底部的线条 */ + } + + QTabBar::tab { + background-color: #2d2d44; /* 未选中标签的背景色,与标题栏一致 */ + color: #c0c0e0; /* 未选中标签的文字颜色 */ + border: 1px solid #3a3a4a; /* 边框颜色 */ + border-bottom: none; /* 移除底部边框 */ + border-top-left-radius: 6px; + border-top-right-radius: 6px; + padding: 8px 16px; /* 内边距,让文字不拥挤 */ + font-weight: 500; + margin-right: 2px; /* 标签之间的间距 */ + } + + /* 鼠标悬停在标签上时的样式 */ + QTabBar::tab:hover { + background-color: #3a3a4a; /* 悬停时使用稍亮的背景色 */ + } + + /* 当前选中的标签页的样式 */ + QTabBar::tab:selected { + background-color: #252538; /* 选中时使用 Dock 背景色 */ + color: #ffffff; /* 选中时使用更亮的文字颜色 */ + font-weight: bold; /* 字体加粗 */ + border-color: #3a3a4a; + border-bottom: 2px solid #8b5cf6; /* 选中标签的底部高亮线 */ + } + + /* 未选中的标签 */ + QTabBar::tab:!selected { + margin-top: 2px; /* 未选中标签稍微下移,创建层次感 */ + } + + /* 标签栏底部的线条 */ + QTabBar::tab-bar { + alignment: left; + border: none; /* 移除标签栏边框 */ + } + """ + + # 获取主窗口现有的样式表,并附加我们新的样式规则 + # 这样可以避免覆盖掉其他可能存在的全局样式 + existing_style = self.styleSheet() + self.setStyleSheet(existing_style + tab_bar_style) def setupToolbar(self): """创建工具栏""" @@ -752,6 +1677,31 @@ class MainWindow(QMainWindow): # 脚本列表 self.scriptsList = QListWidget() + self.scriptsList.setStyleSheet(""" + QListWidget { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + alternate-background-color: #252538; + selection-background-color: rgba(139, 92, 246, 100); + selection-color: white; + } + QListWidget::item { + padding: 6px 8px; + border-bottom: 1px solid #2d2d44; + } + QListWidget::item:last-child { + border-bottom: none; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: rgba(139, 92, 246, 120); + color: white; + } + """) self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick) scriptsLayout.addWidget(self.scriptsList) @@ -795,6 +1745,32 @@ class MainWindow(QMainWindow): # 已挂载脚本列表 self.mountedScriptsList = QListWidget() + self.mountedScriptsList.setStyleSheet(""" + QListWidget { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + alternate-background-color: #252538; + selection-background-color: rgba(139, 92, 246, 100); + selection-color: white; + max-height: 120px; + } + QListWidget::item { + padding: 4px 8px; + border-bottom: 1px solid #2d2d44; + } + QListWidget::item:last-child { + border-bottom: none; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: rgba(139, 92, 246, 120); + color: white; + } + """) self.mountedScriptsList.setMaximumHeight(100) mountLayout.addWidget(QLabel("已挂载脚本:")) mountLayout.addWidget(self.mountedScriptsList) @@ -825,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("图形编辑")) @@ -854,15 +1833,312 @@ 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) + #self.toolGroup.buttonClicked.connect(self.onToolChanged) # 连接脚本菜单事件 - self.createScriptAction.triggered.connect(self.onCreateScriptDialog) - self.loadScriptAction.triggered.connect(self.onLoadScriptFile) - self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) - self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) - self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + # self.createScriptAction.triggered.connect(self.onCreateScriptDialog) + # self.loadScriptAction.triggered.connect(self.onLoadScriptFile) + # self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) + # 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): @@ -887,13 +2163,21 @@ class MainWindow(QMainWindow): def onUpdateCesiumURL(self): """更新 Cesium URL""" - url, ok = QInputDialog.getText(self, "更新 Cesium URL", "输入新的 URL:", - QLineEdit.Normal, "http://localhost:8080/Apps/HelloWorld.html") - if ok and url: - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - self.world.gui_manager.updateCesiumURL(url) - else: - QMessageBox.warning(self, "错误", "GUI 管理器不可用") + dialog = self.createStyledInputDialog( + self, + "更新 Cesium URL", + "输入新的 URL:", + QLineEdit.Normal, + "http://localhost:8080/Apps/HelloWorld.html" + ) + if dialog.exec_() == QDialog.Accepted: + url = dialog.textValue() + if url: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.updateCesiumURL(url) + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + def onAddModelClicked(self): """处理加入模型按钮点击事件""" @@ -927,53 +2211,56 @@ class MainWindow(QMainWindow): def showAddModelDialog(self): """显示添加模型对话框""" # 打开文件选择对话框 - file_path, _ = QFileDialog.getOpenFileName( + dialog = self.createStyledFileDialog( self, "选择 3D 模型文件", "", "3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)" ) - if file_path: - # 获取模型位置信息 - coords, ok = self.getModelCoordinates() - if ok: - longitude, latitude, height, scale = coords + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + # 获取模型位置信息 + coords, ok = self.getModelCoordinates() + if ok: + longitude, latitude, height, scale = coords - # 生成唯一的模型 ID - import uuid - model_id = f"model_{uuid.uuid4().hex[:8]}" + # 生成唯一的模型 ID + import uuid + model_id = f"model_{uuid.uuid4().hex[:8]}" - try: - # 添加模型到 Cesium - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - success = self.world.gui_manager.addModelToCesium( - model_id, - file_path, - longitude, - latitude, - height, - scale + try: + # 添加模型到 Cesium + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + success = self.world.gui_manager.addModelToCesium( + model_id, + file_path, + longitude, + latitude, + height, + scale + ) + + if success: + QMessageBox.information( + self, + "成功", + f"模型已成功添加到地图!\n模型ID: {model_id}" + ) + else: + QMessageBox.warning( + self, + "失败", + "添加模型失败,请检查控制台输出" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"添加模型时发生错误:\n{str(e)}" ) - if success: - QMessageBox.information( - self, - "成功", - f"模型已成功添加到地图!\n模型ID: {model_id}" - ) - else: - QMessageBox.warning( - self, - "失败", - "添加模型失败,请检查控制台输出" - ) - except Exception as e: - QMessageBox.critical( - self, - "错误", - f"添加模型时发生错误:\n{str(e)}" - ) def getModelCoordinates(self): """获取模型坐标信息的对话框""" @@ -1046,7 +2333,7 @@ class MainWindow(QMainWindow): return None, False def onLoadCesiumTileset(self): - url,ok = QInputDialog.getText( + dialog = self.createStyledInputDialog( self, "加载 Cesium 3D Tiles", "输入 tileset.json URL:", @@ -1054,46 +2341,133 @@ class MainWindow(QMainWindow): "https://assets.ion.cesium.com/96128/tileset.json" ) - if ok and url: - try: - # 生成唯一的 tileset 名称 - import uuid - tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" - - # 加载 tileset - if hasattr(self.world, 'addCesiumTileset'): - success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0)) - if success: - QMessageBox.information( - self, - "成功", - f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}" - ) - else: - QMessageBox.warning( - self, - "失败", - "加载 Cesium 3D Tiles 失败" - ) - except Exception as e: - QMessageBox.critical( - self, - "错误", - f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}" - ) + if dialog.exec_() == QDialog.Accepted: + url = dialog.textValue() + if url: + try: + # 生成唯一的 tileset 名称 + import uuid + tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" + # 加载 tileset + if hasattr(self.world, 'addCesiumTileset'): + success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0)) + if success: + QMessageBox.information( + self, + "成功", + f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}" + ) + else: + QMessageBox.warning( + self, + "失败", + "加载 Cesium 3D Tiles 失败" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}" + ) def onToolChanged(self, button): """工具切换事件处理""" if button.isChecked(): - tool_name = button.text() - self.world.setCurrentTool(tool_name) - print(f"工具栏: 选择了 {tool_name} 工具") + tool_name = button.text().strip() # 添加strip()去除空格 + if tool_name: # 确保工具名称不为空 + self.world.setCurrentTool(tool_name) + print(f"工具栏: 选择了 {tool_name} 工具") + else: + print("工具栏: 选择了空工具名称") else: self.world.setCurrentTool(None) print("工具栏: 取消选择工具") - # 在 MainWindow 类中添加以下方法 + def openWebBrowser(self): + if not WEB_ENGINE_AVAILABLE: + return None + try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWidgets import QDockWidget + from PyQt5.QtCore import QUrl + import os + + main_window = self.world.main_window + + # 尝试获取主窗口引用 + if main_window is None: + print("🔍 尝试获取主窗口引用...") + + # 检查各种可能的主窗口引用 + if hasattr(self.world, 'interface_manager'): + print(f" - interface_manager 存在: {self.world.interface_manager}") + if hasattr(self.world.interface_manager, 'main_window'): + main_window = self.world.interface_manager.main_window + print(f" - interface_manager.main_window: {main_window}") + + if main_window is None and hasattr(self.world, 'main_window'): + main_window = self.world.main_window + print(f" - world.main_window: {main_window}") + + # 如果仍然没有主窗口,尝试从树形控件获取 + if main_window is None and self.world.treeWidget: + try: + main_window = self.world.treeWidget.window() + print(f" - 从 treeWidget 获取窗口: {main_window}") + except: + pass + + if main_window is None: + print("✗ 无法获取主窗口引用") + return None + else: + print(f"✅ 使用传入的主窗口引用: {main_window}") + + # 检查主窗口是否有效 + if not hasattr(main_window, 'addDockWidget'): + print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") + return None + + # 检查是否已经存在浏览器视图 + for element in self.world.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "WebBrowserView": + print("⚠ 浏览器视图已经存在") + # 将其前置显示 + element.show() + element.raise_() + return element + + # 创建停靠窗口 + print(f"🔧 创建浏览器停靠窗口,父窗口: {main_window}") + browser_dock = QDockWidget("信息面板", main_window) + browser_dock.setObjectName("WebBrowserView") + + # 创建 Web 视图 + self.web_view = QWebEngineView() + + # 加载百度网页 + #print("🌐 加载百度网页: https://www.baidu.com") + self.web_view.load(QUrl("https://www.bootstrapmb.com/item/15762/preview")) + + # 设置内容 + browser_dock.setWidget(self.web_view) + + # 添加到主窗口 + print("📍 将浏览器视图添加到主窗口") + main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock) + + # 添加到GUI元素列表以便管理 + self.world.gui_elements.append(browser_dock) + + print("✓ 网页浏览器视图已创建并集成到项目中") + return browser_dock + + except Exception as e: + print(f"✗ 创建浏览器视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None def onCreateSampleInfoPanel(self): """创建示例天气信息面板(模拟数据)""" @@ -1113,13 +2487,14 @@ class MainWindow(QMainWindow): # 创建示例面板 weather_panel = info_manager.createInfoPanel( panel_id=unique_id, # 使用唯一ID - position=(0, 0), - size=(1, 1), + position=(1.32, 0.68), + size=(1, 0.6), bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景 border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框 title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 content_color=(0.95, 0.95, 0.95, 1.0), - font=font + font=font, + bg_image="/home/tiger/图片/内部信息框2@2x.png" ) # 更新面板标题 @@ -1131,7 +2506,7 @@ class MainWindow(QMainWindow): # 立即显示加载中信息 info_manager.updatePanelContent(unique_id, content="正在获取天气数据...") - info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) # 每10分钟更新一次 + info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) # # 立即显示示例数据 # sample_data = self.getSampleWeatherData() @@ -1225,6 +2600,142 @@ class MainWindow(QMainWindow): except Exception as e: return f"获取示例数据失败: {str(e)}" + # 在 main_window.py 中修改 onCreate3DSampleInfoPanel 方法 + def onCreate3DSampleInfoPanel(self): + """创建3D示例天气信息面板(修复透明度问题)""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(self.world.render) + + # 使用唯一的面板ID + import time + unique_id = f"weather_info_3d_{int(time.time())}" + + # 创建3D示例面板 - 修复透明度问题 + weather_panel = info_manager.create3DInfoPanel( + panel_id=unique_id, + position=(2, 0, 2), # 调整Z坐标避免与其他对象重叠 + size=(5, 5), + bg_color=(0.15, 0.25, 0.35, 0.85), # 设置合适的透明度值 + border_color=(0.3, 0.5, 0.7, 1.0), + title_color=(0.7, 0.9, 1.0, 1.0), + content_color=(0.95, 0.95, 0.95, 1.0), + font=font + ) + + # 重要:手动设置面板的透明度渲染模式 + # if weather_panel: + # # 确保面板支持透明度 + # weather_panel.setTransparency(True) + # # 设置合适的渲染顺序,确保透明对象正确渲染 + # weather_panel.setBin("transparent", 0) + # # 启用深度写入,但保持透明度 + # weather_panel.setDepthWrite(False) + + # 更新面板标题 + info_manager.update3DPanelContent(unique_id, title="3D北京天气") + + # 添加到场景树 + self.addInfoPanelToTree(weather_panel, "3D天气信息面板") + + # 显示加载中信息 + info_manager.update3DPanelContent(unique_id, content="正在获取天气数据...") + + info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) + + print("✓ 3D示例天气信息面板已创建") + + except Exception as e: + print(f"✗ 创建3D示例天气信息面板失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}") + + def onCreate3DSystemStatusPanel(self): + """创建3D系统状态信息面板""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(self.world.render) + + # 使用唯一的面板ID + import time + unique_id = f"system_status_3d_{int(time.time())}" + + panel = info_manager.create3DInfoPanel( + panel_id=unique_id, + position=(2, 0, 0), + size=(0.8, 1.2), + bg_color=(0.25, 0.15, 0.15, 0.95), # 红色背景 + border_color=(0.7, 0.3, 0.3, 1.0), # 红色边框 + title_color=(1.0, 0.5, 0.5, 1.0), # 浅红色标题 + content_color=(0.95, 0.95, 0.95, 1.0), + font=font + ) + + # 添加到场景树 + self.addInfoPanelToTree(panel, "3D系统状态信息面板") + + # 立即显示初始数据 + initial_data = self.getSystemStatusData() + info_manager.update3DPanelContent(unique_id, content=initial_data) + + # 注册数据源,每5秒更新一次 + info_manager.registerDataSource(unique_id, self.getSystemStatusData, update_interval=5.0) + + except Exception as e: + print(f"✗ 创建3D系统状态信息面板失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "错误", f"创建3D系统状态信息面板时出错: {str(e)}") + + # 更新 addInfoPanelToTree 方法以支持3D面板 + def addInfoPanelToTree(self, panel, panel_name): + """ + 将信息面板添加到场景树控件中 + """ + if panel and self.treeWidget: + # 找到场景根节点 + scene_root = None + for i in range(self.treeWidget.topLevelItemCount()): + item = self.treeWidget.topLevelItem(i) + if item.text(0) == "render": + scene_root = item + break + + # 如果找不到场景根节点,使用第一个顶级节点 + if not scene_root and self.treeWidget.topLevelItemCount() > 0: + scene_root = self.treeWidget.topLevelItem(0) + + if scene_root: + # 根据面板类型确定节点类型 + node_type = "INFO_PANEL_3D" if "3d" in panel_name.lower() else "INFO_PANEL" + + tree_item = self.treeWidget.add_node_to_tree_widget( + node=panel, + parent_item=scene_root, + node_type=node_type + ) + if tree_item: + self.treeWidget.setCurrentItem(tree_item) + self.treeWidget.update_selection_and_properties(panel, tree_item) + print(f"✓ {panel_name}节点已添加到场景树") + return True + else: + print(f"⚠️ {panel_name}节点添加到场景树失败") + else: + print("❌ 未找到场景根节点") + return False + def onCreateSystemStatusPanel(self): """创建系统状态信息面板""" try: @@ -1449,40 +2960,6 @@ class MainWindow(QMainWindow): except Exception as e: QMessageBox.critical(self, "错误", f"创建信息面板时出错: {str(e)}") - def addInfoPanelToTree(self, panel, panel_name): - """ - 将信息面板添加到场景树控件中 - """ - if panel and self.treeWidget: - # 找到场景根节点 - scene_root = None - for i in range(self.treeWidget.topLevelItemCount()): - item = self.treeWidget.topLevelItem(i) - if item.text(0) == "render": - scene_root = item - break - - # 如果找不到场景根节点,使用第一个顶级节点 - if not scene_root and self.treeWidget.topLevelItemCount() > 0: - scene_root = self.treeWidget.topLevelItem(0) - - if scene_root: - tree_item = self.treeWidget.add_node_to_tree_widget( - node=panel, - parent_item=scene_root, - node_type="INFO_PANEL" - ) - if tree_item: - self.treeWidget.setCurrentItem(tree_item) - self.treeWidget.update_selection_and_properties(panel, tree_item) - print(f"✓ {panel_name}节点已添加到场景树") - return True - else: - print(f"⚠️ {panel_name}节点添加到场景树失败") - else: - print("❌ 未找到场景根节点") - return False - # ==================== 脚本管理事件处理 ==================== def refreshScriptsList(self): @@ -1572,17 +3049,21 @@ class MainWindow(QMainWindow): def onCreateScriptDialog(self): """菜单创建脚本事件""" - script_name, ok = QInputDialog.getText(self, "创建脚本", "输入脚本名称:") - if ok and script_name.strip(): - try: - success = self.world.createScript(script_name.strip(), "basic") - if success: - QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!") - self.refreshScriptsList() - else: - QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!") - except Exception as e: - QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}") + dialog = self.createStyledInputDialog(self, "创建脚本", "输入脚本名称:") + + if dialog.exec_() == QDialog.Accepted: + script_name = dialog.textValue() + if script_name.strip(): + try: + success = self.world.createScript(script_name.strip(), "basic") + if success: + QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!") + self.refreshScriptsList() + else: + QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!") + except Exception as e: + QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}") + def onLoadScript(self): """加载脚本按钮事件""" @@ -1603,19 +3084,26 @@ class MainWindow(QMainWindow): def onLoadScriptFile(self): """加载脚本文件菜单事件""" - file_path, _ = QFileDialog.getOpenFileName( - self, "选择脚本文件", "", "Python文件 (*.py)" + dialog = self.createStyledFileDialog( + self, + "选择脚本文件", + "", + "Python文件 (*.py)" ) - if file_path: - try: - success = self.world.loadScript(file_path) - if success: - QMessageBox.information(self, "成功", "脚本文件加载成功!") - self.refreshScriptsList() - else: - QMessageBox.warning(self, "错误", "脚本文件加载失败!") - except Exception as e: - QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}") + + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + try: + success = self.world.loadScript(file_path) + if success: + QMessageBox.information(self, "成功", "脚本文件加载成功!") + self.refreshScriptsList() + else: + QMessageBox.warning(self, "错误", "脚本文件加载失败!") + except Exception as e: + QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}") + def onReloadAllScripts(self): """重载所有脚本事件""" @@ -1701,6 +3189,16 @@ class MainWindow(QMainWindow): except Exception as e: QMessageBox.critical(self, "错误", f"卸载脚本时出错: {str(e)}") + def onOpenIconManager(self): + """打开图标管理器""" + try: + from ui.icon_manager_gui import show_icon_manager + self.icon_manager_dialog = show_icon_manager(self) + print("🎨 图标管理器已打开") + except Exception as e: + print(f"❌ 打开图标管理器失败: {e}") + QMessageBox.warning(self, "错误", f"打开图标管理器失败:\n{str(e)}") + def closeEvent(self, event): """处理窗口关闭事件""" try: @@ -1734,6 +3232,43 @@ class MainWindow(QMainWindow): dialog.setWindowTitle("创建平面地形") dialog.setModal(True) dialog.resize(300,200) + # 设置对话框样式 + dialog.setStyleSheet(""" + QDialog { + background-color: #252538; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + font-weight: 500; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + min-width: 80px; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QDoubleSpinBox, QSpinBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + """) layout = QVBoxLayout(dialog) @@ -1791,86 +3326,91 @@ class MainWindow(QMainWindow): def onCreateHeightmapTerrain(self): """从高度图创建地形""" - file_path,_=QFileDialog.getOpenFileName( + dialog = self.createStyledFileDialog( self, "选择高度图文件", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)" ) - if file_path: - #创建对话框获取地形参数 - dialog = QDialog(self) - dialog.setWindowTitle("设置地形参数") - dialog.setModal(True) - dialog.resize(300,250) + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + #创建对话框获取地形参数 + dialog = QDialog(self) + dialog.setWindowTitle("设置地形参数") + dialog.setModal(True) + dialog.resize(300,250) - layout = QVBoxLayout(dialog) + layout = QVBoxLayout(dialog) - x_scale_layout = QHBoxLayout() - x_scale_layout.addWidget(QLabel("X缩放:")) - x_scale_spin = QDoubleSpinBox() - x_scale_spin.setRange(0.1,1000) - x_scale_spin.setValue(0.3) - x_scale_spin.setSingleStep(10) - x_scale_layout.addWidget(x_scale_spin) - layout.addLayout(x_scale_layout) + x_scale_layout = QHBoxLayout() + x_scale_layout.addWidget(QLabel("X缩放:")) + x_scale_spin = QDoubleSpinBox() + x_scale_spin.setRange(0.1,1000) + x_scale_spin.setValue(0.3) + x_scale_spin.setSingleStep(10) + x_scale_layout.addWidget(x_scale_spin) + layout.addLayout(x_scale_layout) - # Y缩放 - y_scale_layout = QHBoxLayout() - y_scale_layout.addWidget(QLabel("Y缩放:")) - y_scale_spin = QDoubleSpinBox() - y_scale_spin.setRange(0.1, 1000) - y_scale_spin.setValue(0.3) - y_scale_spin.setSingleStep(10) - y_scale_layout.addWidget(y_scale_spin) - layout.addLayout(y_scale_layout) + # Y缩放 + y_scale_layout = QHBoxLayout() + y_scale_layout.addWidget(QLabel("Y缩放:")) + y_scale_spin = QDoubleSpinBox() + y_scale_spin.setRange(0.1, 1000) + y_scale_spin.setValue(0.3) + y_scale_spin.setSingleStep(10) + y_scale_layout.addWidget(y_scale_spin) + layout.addLayout(y_scale_layout) - # Z缩放 - z_scale_layout = QHBoxLayout() - z_scale_layout.addWidget(QLabel("Z缩放:")) - z_scale_spin = QDoubleSpinBox() - z_scale_spin.setRange(0.1, 1000) - z_scale_spin.setValue(50) - z_scale_spin.setSingleStep(5) - z_scale_layout.addWidget(z_scale_spin) - layout.addLayout(z_scale_layout) + # Z缩放 + z_scale_layout = QHBoxLayout() + z_scale_layout.addWidget(QLabel("Z缩放:")) + z_scale_spin = QDoubleSpinBox() + z_scale_spin.setRange(0.1, 1000) + z_scale_spin.setValue(50) + z_scale_spin.setSingleStep(5) + z_scale_layout.addWidget(z_scale_spin) + layout.addLayout(z_scale_layout) - # 按钮 - button_layout = QHBoxLayout() - ok_button = QPushButton("创建") - cancel_button = QPushButton("取消") - button_layout.addWidget(ok_button) - button_layout.addWidget(cancel_button) - layout.addLayout(button_layout) + # 按钮 + button_layout = QHBoxLayout() + ok_button = QPushButton("创建") + cancel_button = QPushButton("取消") + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) - # 连接信号 - ok_button.clicked.connect(dialog.accept) - cancel_button.clicked.connect(dialog.reject) + # 连接信号 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) - # 显示对话框 - if dialog.exec_() == QDialog.Accepted: - x_scale = x_scale_spin.value() - y_scale = y_scale_spin.value() - z_scale = z_scale_spin.value() + # 显示对话框 + if dialog.exec_() == QDialog.Accepted: + x_scale = x_scale_spin.value() + y_scale = y_scale_spin.value() + z_scale = z_scale_spin.value() - # 调用世界对象创建地形 - terrain_info = self.world.createTerrainFromHeightMap( - file_path, - (x_scale, y_scale, z_scale) - ) - if terrain_info: - QMessageBox.information(self, "成功", "高度图地形创建成功!") - else: - QMessageBox.warning(self, "错误", "高度图地形创建失败!") + # 调用世界对象创建地形 + terrain_info = self.world.createTerrainFromHeightMap( + file_path, + (x_scale, y_scale, z_scale) + ) + if terrain_info: + QMessageBox.information(self, "成功", "高度图地形创建成功!") + else: + QMessageBox.warning(self, "错误", "高度图地形创建失败!") -def setup_main_window(world): +def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() if app is None: app = QApplication(sys.argv) - + main_window = MainWindow(world) main_window.show() - + from main import openProjectForPath + if path: + openProjectForPath(path,main_window) + return app, main_window \ No newline at end of file diff --git a/ui/property_panel.py b/ui/property_panel.py index fe7c776b..cf0942f6 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -7,14 +7,14 @@ from typing import Hashable from PyQt5.QtGui import QColor from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, - QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog) + QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox) from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor from direct.gui import DirectGui from idna import check_label from jinja2.compiler import has_safe_repr -from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup +from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3 from scene import util from direct.gui.DirectGui import DirectLabel, DirectFrame from panda3d.core import TextNode @@ -27,7 +27,7 @@ class PropertyPanelManager: """初始化属性面板管理器""" self.world = world self._propertyLayout = None - self._actor_cache={} + self._actor_cache = {} self._spherical_video_controls = {} # 初始化地形编辑参数 @@ -37,6 +37,9 @@ class PropertyPanelManager: self.world.terrain_edit_strength = 0.3 if not hasattr(self.world, 'terrain_edit_operation'): # 这里原来是 terrain_edit_opertaion self.world.terrain_edit_operation = "add" + + # 初始化碰撞参数加载标志位 + self._loading_collision_params = False # 定义紧凑样式 self.compact_style = """ @@ -95,16 +98,17 @@ class PropertyPanelManager: # 当节点被拖拽后,需要根据新父节点的状态来更新可见性 self._syncEffectiveVisibility(node) self._syncSceneVisibility() + def _syncSceneVisibility(self): scene_root = self.world.render self._syncEffectiveVisibility(scene_root) - def updatePropertyPanel(self, item): """更新属性面板显示""" - if not self._propertyLayout or not self._propertyLayout.parent(): - print("属性布局未设置或没有父部件!") - return + # if not self._propertyLayout or not self._propertyLayout.parent(): + # print("属性布局未设置或没有父部件!") + # return + #更健壮的有效性检查 self._cleanupAllReferences() self.clearPropertyPanel() @@ -138,6 +142,11 @@ class PropertyPanelManager: # 根据模型的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) self.name_input = QLineEdit(itemText) + + self.name_input.returnPressed.connect( + lambda: self.world.treeWidget.update_item_name(self.name_input.text(), item) + ) + name_layout.addWidget(self.active_check) name_layout.addWidget(self.name_input) self.name_group.setLayout(name_layout) @@ -149,7 +158,7 @@ class PropertyPanelManager: except TypeError: pass self.active_check.stateChanged.connect( - lambda state,m = model:self._setUserVisible(m,state == Qt.Checked) + lambda state, m=model: self._setUserVisible(m, state == Qt.Checked) ) # nameLabel = QLabel("名称:") # nameEdit = QLineEdit(itemText) @@ -158,12 +167,12 @@ class PropertyPanelManager: # 获取节点对象 model = item.data(0, Qt.UserRole) - if self._isTerrainNode(model,item): - self._showTerrainProperties(model,item) - elif model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset": - self._showCesiumTilesetProperties(model,item) + if self._isTerrainNode(model, item): + self._showTerrainProperties(model, item) + elif model and hasattr(model, 'getTag') and model.getTag("element_type") == "cesium_tileset": + self._showCesiumTilesetProperties(model, item) elif model and hasattr(model, 'getTag') and model.getTag("gui_type"): - self.updateGUIPropertyPanel(model) + self.updateGUIPropertyPanel(model, item) elif model and hasattr(model, 'getTag') and model.getTag("light_type"): self.updateLightPropertyPanel(model) elif model: @@ -171,6 +180,9 @@ class PropertyPanelManager: self._propertyLayout.addStretch() + # 重置碰撞相关标志,确保下次选择时正常显示 + self._just_added_collision = False + # 强制更新布局 if self._propertyLayout: self._propertyLayout.update() @@ -178,23 +190,23 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() - def _isTerrainNode(self,node,item): + def _isTerrainNode(self, node, item): """检查是否是地形节点""" - item_data = item.data(0,Qt.UserRole+1) + item_data = item.data(0, Qt.UserRole + 1) if item_data == "terrain": return True - if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains: + if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: for terrain_info in self.world.terrain_manager.terrains: if terrain_info['node'] == node: return True return False - def _showTerrainProperties(self,terrain_node,item): + def _showTerrainProperties(self, terrain_node, item): """显示地形属性面板""" try: terrain_info = None - if hasattr(self.world,'terrain_manager'): + if hasattr(self.world, 'terrain_manager'): for info in self.world.terrain_manager.terrains: if info['node'] == terrain_node: terrain_info = info @@ -207,52 +219,52 @@ class PropertyPanelManager: info_group = QGroupBox("地形信息") info_layout = QGridLayout() - info_layout.addWidget(QLabel("名称:"),0,0) - name_label = QLabel(terrain_info.get('name','未知')) - info_layout.addWidget(name_label,0,1) + info_layout.addWidget(QLabel("名称:"), 0, 0) + name_label = QLabel(terrain_info.get('name', '未知')) + info_layout.addWidget(name_label, 0, 1) - info_layout.addWidget(QLabel("类型:"),1,0) - type_label = QLabel("高度图地形"if terrain_info.get('heightmap') else "平面地形") - info_layout.addWidget(type_label,1,1) + info_layout.addWidget(QLabel("类型:"), 1, 0) + type_label = QLabel("高度图地形" if terrain_info.get('heightmap') else "平面地形") + info_layout.addWidget(type_label, 1, 1) if terrain_info.get('heightmap'): - info_layout.addWidget(QLabel("高度图:"),2,0) + info_layout.addWidget(QLabel("高度图:"), 2, 0) heightmap_label = QLabel(os.path.basename(terrain_info['heightmap'])) heightmap_label.setWordWrap(True) - info_layout.addWidget(heightmap_label,2,1) + info_layout.addWidget(heightmap_label, 2, 1) info_group.setLayout(info_layout) self._propertyLayout.addWidget(info_group) - #变换属性 + # 变换属性 self._updateTerrainTransformPanel(terrain_node) - #地形编辑控制面板 + # 地形编辑控制面板 self._createTerrainEditPanel(terrain_info) - #材质属性 - self._updateTerrainMaterialPanel(terrain_node,terrain_info) + # 材质属性 + self._updateTerrainMaterialPanel(terrain_node, terrain_info) - #删除按钮 - delete_btn = QPushButton("删除地形") - delete_btn.setStyleSheet(""" - QPushButton{ - background-color:#ff4444; - color:white; - border:none; - padding:8px; - border-radius:4px; - margin-top:10px - } - QPushButton:hover{ - background-color:#ff6666; - } - """) - delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) - self._propertyLayout.addWidget(delete_btn) + # #删除按钮 + # delete_btn = QPushButton("删除地形") + # delete_btn.setStyleSheet(""" + # QPushButton{ + # background-color:#ff4444; + # color:white; + # border:none; + # padding:8px; + # border-radius:4px; + # margin-top:10px + # } + # QPushButton:hover{ + # background-color:#ff6666; + # } + # """) + # delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) + # self._propertyLayout.addWidget(delete_btn) except Exception as e: print(f"显示地形属性时出错: {e}") import traceback traceback.print_exc() - def _updateTerrainTransformPanel(self,terrain_node): + def _updateTerrainTransformPanel(self, terrain_node): """更新地形变化属性面板""" try: transform_group = QGroupBox("变换 Transform") @@ -261,7 +273,7 @@ class PropertyPanelManager: pos = terrain_node.getPos() scale = terrain_node.getScale() - transform_layout.addWidget(QLabel("位置"),0,0) + transform_layout.addWidget(QLabel("位置"), 0, 0) x_label = QLabel("X") y_label = QLabel("Y") @@ -270,16 +282,16 @@ class PropertyPanelManager: y_label.setAlignment(Qt.AlignCenter) z_label.setAlignment(Qt.AlignCenter) - transform_layout.addWidget(x_label,0,1) - transform_layout.addWidget(y_label,0,2) - transform_layout.addWidget(z_label,0,3) + transform_layout.addWidget(x_label, 0, 1) + transform_layout.addWidget(y_label, 0, 2) + transform_layout.addWidget(z_label, 0, 3) self.pos_x = QDoubleSpinBox() self.pos_y = QDoubleSpinBox() self.pos_z = QDoubleSpinBox() - for pos_widget in [self.pos_x,self.pos_y,self.pos_z]: - pos_widget.setRange(-1000000.0,1000000.0) + for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: + pos_widget.setRange(-1000000.0, 1000000.0) self.pos_x.setValue(pos.getX()) self.pos_y.setValue(pos.getY()) self.pos_z.setValue(pos.getZ()) @@ -287,37 +299,40 @@ class PropertyPanelManager: def updateXPosition(value): terrain_node.setX(value) self.refreshModelValues(terrain_node) + self.pos_x.valueChanged.connect(updateXPosition) def updateYPosition(value): terrain_node.setY(value) self.refreshModelValues(terrain_node) + self.pos_y.valueChanged.connect(updateYPosition) def updateZPosition(value): terrain_node.setZ(value) self.refreshModelValues(terrain_node) + self.pos_z.valueChanged.connect(updateZPosition) - transform_layout.addWidget(self.pos_x,1,1) - transform_layout.addWidget(self.pos_y,1,2) - transform_layout.addWidget(self.pos_z,1,3) + transform_layout.addWidget(self.pos_x, 1, 1) + transform_layout.addWidget(self.pos_y, 1, 2) + transform_layout.addWidget(self.pos_z, 1, 3) - transform_layout.addWidget(QLabel("旋转"),2,0) + transform_layout.addWidget(QLabel("旋转"), 2, 0) self.rot_x = QDoubleSpinBox() self.rot_y = QDoubleSpinBox() self.rot_z = QDoubleSpinBox() - for rot_widget in [self.rot_x,self.rot_y,self.rot_z]: - rot_widget.setRange(-360,360) + for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: + rot_widget.setRange(-360, 360) self.rot_x.setValue(terrain_node.getH()) self.rot_y.setValue(terrain_node.getP()) self.rot_z.setValue(terrain_node.getR()) - self.rot_x.valueChanged.connect(lambda v:terrain_node.setH(v)) - self.rot_y.valueChanged.connect(lambda v:terrain_node.setP(v)) - self.rot_z.valueChanged.connect(lambda v:terrain_node.setR(v)) + self.rot_x.valueChanged.connect(lambda v: terrain_node.setH(v)) + self.rot_y.valueChanged.connect(lambda v: terrain_node.setP(v)) + self.rot_z.valueChanged.connect(lambda v: terrain_node.setR(v)) h_label = QLabel("H") p_label = QLabel("P") @@ -326,28 +341,28 @@ class PropertyPanelManager: p_label.setAlignment(Qt.AlignCenter) r_label.setAlignment(Qt.AlignCenter) - transform_layout.addWidget(h_label,2,1) - transform_layout.addWidget(p_label,2,2) - transform_layout.addWidget(r_label,2,3) - transform_layout.addWidget(self.rot_x,3,1) - transform_layout.addWidget(self.rot_y,3,2) - transform_layout.addWidget(self.rot_z,3,3) + transform_layout.addWidget(h_label, 2, 1) + transform_layout.addWidget(p_label, 2, 2) + transform_layout.addWidget(r_label, 2, 3) + transform_layout.addWidget(self.rot_x, 3, 1) + transform_layout.addWidget(self.rot_y, 3, 2) + transform_layout.addWidget(self.rot_z, 3, 3) - transform_layout.addWidget(QLabel("缩放"),4,0) + transform_layout.addWidget(QLabel("缩放"), 4, 0) self.scale_x = QDoubleSpinBox() self.scale_y = QDoubleSpinBox() self.scale_z = QDoubleSpinBox() - for scale_widget in [self.scale_x,self.scale_y,self.scale_z]: - scale_widget.setRange(-1000,1000) + for scale_widget in [self.scale_x, self.scale_y, self.scale_z]: + scale_widget.setRange(-1000, 1000) scale_widget.setSingleStep(0.1) self.scale_x.setValue(scale.getX()) self.scale_y.setValue(scale.getY()) self.scale_z.setValue(scale.getZ()) - self.scale_x.valueChanged.connect(lambda v:self._updateXScale(terrain_node,v)) - self.scale_y.valueChanged.connect(lambda v:self._updateYScale(terrain_node,v)) - self.scale_z.valueChanged.connect(lambda v:self._updateZScale(terrain_node,v)) + self.scale_x.valueChanged.connect(lambda v: self._updateXScale(terrain_node, v)) + self.scale_y.valueChanged.connect(lambda v: self._updateYScale(terrain_node, v)) + self.scale_z.valueChanged.connect(lambda v: self._updateZScale(terrain_node, v)) x_label2 = QLabel("X") y_label2 = QLabel("Y") @@ -356,12 +371,12 @@ class PropertyPanelManager: y_label2.setAlignment(Qt.AlignCenter) z_label2.setAlignment(Qt.AlignCenter) - transform_layout.addWidget(x_label2,4,1) - transform_layout.addWidget(y_label2,4,2) - transform_layout.addWidget(z_label2,4,3) - transform_layout.addWidget(self.scale_x,5,1) - transform_layout.addWidget(self.scale_y,5,2) - transform_layout.addWidget(self.scale_z,5,3) + transform_layout.addWidget(x_label2, 4, 1) + transform_layout.addWidget(y_label2, 4, 2) + transform_layout.addWidget(z_label2, 4, 3) + transform_layout.addWidget(self.scale_x, 5, 1) + transform_layout.addWidget(self.scale_y, 5, 2) + transform_layout.addWidget(self.scale_z, 5, 3) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) @@ -419,14 +434,14 @@ class PropertyPanelManager: except Exception as e: print(f"创建地形编辑面板时出错: {e}") - def _onTerrainRadiusChanged(self,value): + def _onTerrainRadiusChanged(self, value): """地形编辑半径改变""" - if hasattr(self.world,'terrain_edit_radius'): + if hasattr(self.world, 'terrain_edit_radius'): self.world.terrain_edit_radius = float(value) - def _onTerrainStrengthChanged(self,value): + def _onTerrainStrengthChanged(self, value): """地形编辑强度改变""" - if hasattr(self.world,'terrain_edit_strength'): + if hasattr(self.world, 'terrain_edit_strength'): self.world.terrain_edit_strength = float(value) def _onTerrainOperationChanged(self, text): @@ -441,46 +456,46 @@ class PropertyPanelManager: self.world.terrain_edit_operation = operation_map.get(text, "add") print(f"地形编辑操作已设置为: {self.world.terrain_edit_operation}") - def _updateTerrainMaterialPanel(self,terrain_node,terrain_info): + def _updateTerrainMaterialPanel(self, terrain_node, terrain_info): """更新地形材质属性面板""" try: material_group = QGroupBox("材质属性") material_layout = QGridLayout() - #颜色设置 - material_layout.addWidget(QLabel("颜色:"),0,0) + # 颜色设置 + material_layout.addWidget(QLabel("颜色:"), 0, 0) color_button = QPushButton("选择颜色") - color_button.clicked.connect(lambda:self._selectTerrainColor(terrain_info)) - #纹理设置 - material_layout.addWidget(QLabel("纹理:"),1,0) + color_button.clicked.connect(lambda: self._selectTerrainColor(terrain_info)) + # 纹理设置 + material_layout.addWidget(QLabel("纹理:"), 1, 0) texture_button = QPushButton("选择纹理") - texture_button.clicked.connect(lambda :self._selectTerrainTexture(terrain_info)) - material_layout.addWidget(texture_button,1,1) + texture_button.clicked.connect(lambda: self._selectTerrainTexture(terrain_info)) + material_layout.addWidget(texture_button, 1, 1) material_group.setLayout(material_layout) self._propertyLayout.addWidget(material_group) except Exception as e: print(f"更新材质面板时出错{e}") - def _selectTerrainColor(self,terrain_info): + def _selectTerrainColor(self, terrain_info): try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor - current_color = QColor(255,255,255) + current_color = QColor(255, 255, 255) - color = QColorDialog.getColor(current_color,None,"选择地形颜色") + color = QColorDialog.getColor(current_color, None, "选择地形颜色") if color.isValid(): - r,g,b = color.red()/255.0,color.green()/255.0,color.blue()/255.0 - if hasattr(self.world,'terrain_manager'): - self.world.terrain_manager.setTerrainColor(terrain_info,(r,g,b)) + r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 + if hasattr(self.world, 'terrain_manager'): + self.world.terrain_manager.setTerrainColor(terrain_info, (r, g, b)) except Exception as e: print(f"选择地形颜色时出错: {e}") - def _selectTerrainTexture(self,terrain_info): + def _selectTerrainTexture(self, terrain_info): """选择地形纹理""" try: from PyQt5.QtWidgets import QFileDialog - file_path,_ = QFileDialog.getOpenFileName( + file_path, _ = QFileDialog.getOpenFileName( None, "选择地形纹理", "", @@ -488,8 +503,8 @@ class PropertyPanelManager: ) if file_path and os.path.exists(file_path): - if hasattr(self.world,'terrain_manager'): - success = self.world.terrain_manager.setTerrainTexture(terrain_info,file_path) + if hasattr(self.world, 'terrain_manager'): + success = self.world.terrain_manager.setTerrainTexture(terrain_info, file_path) if success: print(f"地形纹理已应用{file_path}") else: @@ -520,12 +535,12 @@ class PropertyPanelManager: success = self.world.terrain_manager.deleteTerrain(terrain_info) if success: # 更新场景树 - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, - 'updateSceneTree'): - self.world.scene_manager.updateSceneTree() - - # 清空属性面板 - self.clearPropertyPanel() + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, + # 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + # + # # 清空属性面板 + # self.clearPropertyPanel() print(f"✓ 地形已删除: {terrain_info.get('name', '未知')}") else: @@ -553,13 +568,27 @@ class PropertyPanelManager: if hasattr(self, '_current_info_panel'): del self._current_info_panel + # 清理碰撞相关控件引用 + collision_controls = [ + 'collision_status_text', 'collision_shape_combo', 'collision_shape_label', + 'collision_visibility_button', 'collision_button', 'collision_layout', 'collision_group', + 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', + 'collision_radius', 'collision_width', 'collision_length', 'collision_height', + 'collision_capsule_radius', 'collision_capsule_height', + 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' + ] + for name in collision_controls: + if hasattr(self, name): + delattr(self, name) + # 清理其他可能的控件引用 other_controls = ['scale_x', 'scale_y', 'scale_z', 'pos_x', 'pos_y', 'pos_z'] for name in other_controls: if hasattr(self, name): setattr(self, name, None) - def _setUserVisible(self,node,visible): - node.setPythonTag("user_visible",visible) + + def _setUserVisible(self, node, visible): + node.setPythonTag("user_visible", visible) self._syncEffectiveVisibility(node) def _syncEffectiveVisibility(self, start_node): @@ -740,6 +769,7 @@ class PropertyPanelManager: if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见 # 检查对象是否仍然有效 spinbox.blockSignals(True) + spinbox.setKeyboardTracking(False) # 确保禁用键盘跟踪 spinbox.setValue(value) spinbox.blockSignals(False) except RuntimeError as e: @@ -762,42 +792,79 @@ class PropertyPanelManager: # 位置控件 transform_layout.addWidget(QLabel("相对位置"), 0, 0) - # 创建并设置 X, Y, Z 标签居中 - x_label = QLabel("X") - y_label = QLabel("Y") - z_label = QLabel("Z") - x_label.setAlignment(Qt.AlignCenter) - y_label.setAlignment(Qt.AlignCenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(y_label, 0, 2) - transform_layout.addWidget(z_label, 0, 3) - - # 位置数值输入框 - self.pos_x = self._createSafeSpinBox(-1000, 1000) - self.pos_y = self._createSafeSpinBox(-1000, 1000) - self.pos_z = self._createSafeSpinBox(-1000, 1000) - + # X坐标 + transform_layout.addWidget(QLabel("X:"), 1, 0) + self.pos_x = QLineEdit() + self.pos_x.setText(str(round(nodePath.getX(), 6))) + self.pos_x.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'x')) transform_layout.addWidget(self.pos_x, 1, 1) - transform_layout.addWidget(self.pos_y, 1, 2) - transform_layout.addWidget(self.pos_z, 1, 3) - # 世界位置 (只读) - transform_layout.addWidget(QLabel("世界位置"), 2, 0) - self.world_pos_x = self._createSafeSpinBox(-10000, 10000, True) # 只读 - self.world_pos_y = self._createSafeSpinBox(-10000, 10000, True) - self.world_pos_z = self._createSafeSpinBox(-10000, 10000, True) + # Y坐标 + transform_layout.addWidget(QLabel("Y:"), 1, 2) + self.pos_y = QLineEdit() + self.pos_y.setText(str(round(nodePath.getY(), 6))) + self.pos_y.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'y')) + transform_layout.addWidget(self.pos_y, 1, 3) - transform_layout.addWidget(self.world_pos_x, 2, 1) - transform_layout.addWidget(self.world_pos_y, 2, 2) - transform_layout.addWidget(self.world_pos_z, 2, 3) + # Z坐标 + transform_layout.addWidget(QLabel("Z:"), 1, 4) + self.pos_z = QLineEdit() + self.pos_z.setText(str(round(nodePath.getZ(), 6))) + self.pos_z.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'z')) + transform_layout.addWidget(self.pos_z, 1, 5) return transform_layout + except Exception as e: - print(f"创建变换控件失败: {e}") + print(f"创建变换控件时出错: {e}") return None + def _onPositionEditFinished(self, nodePath, axis): + """位置编辑完成时的处理""" + try: + # 检查控件是否仍然有效 + if not hasattr(self, f'pos_{axis}') or getattr(self, f'pos_{axis}') is None: + return + + line_edit = getattr(self, f'pos_{axis}') + if line_edit is None or line_edit.isHidden(): + return + + # 获取文本并转换为数值 + text = line_edit.text() + try: + new_value = float(text) + except ValueError: + print(f"无效的数值输入: {text}") + # 恢复原来的值 + if axis == 'x': + line_edit.setText(str(round(nodePath.getX(), 6))) + elif axis == 'y': + line_edit.setText(str(round(nodePath.getY(), 6))) + elif axis == 'z': + line_edit.setText(str(round(nodePath.getZ(), 6))) + return + + # 根据轴设置位置 + if axis == 'x': + nodePath.setX(new_value) + elif axis == 'y': + nodePath.setY(new_value) + elif axis == 'z': + nodePath.setZ(new_value) + + print(f"位置已更新: {nodePath.getName()} {axis.upper()} = {new_value}") + + # 如果是坐标轴节点,需要更新坐标轴位置 + if hasattr(self.world, 'selection_manager'): + selection_manager = self.world.selection_manager + if (hasattr(selection_manager, 'gizmoTarget') and + selection_manager.gizmoTarget == nodePath): + # 更新坐标轴位置 + selection_manager._updateGizmoPositionAndOrientation() + + except Exception as e: + print(f"更新位置时出错: {e}") def _createSafeSpinBox(self, min_val, max_val, read_only=False): """创建安全的数值框""" try: @@ -825,8 +892,9 @@ class PropertyPanelManager: except: pass setattr(self, name, None) - def _refreshWorldPos(self,model): - if not hasattr(self,'worldXSpin'): + + def _refreshWorldPos(self, model): + if not hasattr(self, 'worldXSpin'): return world = model.getPos(self.world.render) self._worldXSpin.setValue(world.x) @@ -904,23 +972,23 @@ class PropertyPanelManager: scale_group.setLayout(scale_layout) self._propertyLayout.addWidget(scale_group) - # 删除按钮 - delete_btn = QPushButton("删除 Tileset") - delete_btn.setStyleSheet(""" - QPushButton { - background-color: #ff4444; - color: white; - border: none; - padding: 8px; - border-radius: 4px; - margin-top: 10px; - } - QPushButton:hover { - background-color: #ff6666; - } - """) - delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) - self._propertyLayout.addWidget(delete_btn) + # # 删除按钮 + # delete_btn = QPushButton("删除 Tileset") + # delete_btn.setStyleSheet(""" + # QPushButton { + # background-color: #ff4444; + # color: white; + # border: none; + # padding: 8px; + # border-radius: 4px; + # margin-top: 10px; + # } + # QPushButton:hover { + # background-color: #ff6666; + # } + # """) + # delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) + # self._propertyLayout.addWidget(delete_btn) # 添加弹性空间 self._propertyLayout.addStretch() @@ -1018,27 +1086,29 @@ class PropertyPanelManager: ) if reply == QMessageBox.Yes: - # 从场景中移除 - nodePath.removeNode() + tree_widget = self.world.treeWidget + tree_widget.delete_item(nodePath) + # # 从场景中移除 + # nodePath.removeNode() + # + # # 从 tilesets 列表中移除 + # if hasattr(self.world, 'scene_manager'): + # tilesets_to_remove = [] + # for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + # if tileset_info['node'] == nodePath: + # tilesets_to_remove.append(i) + # + # # 从后往前删除,避免索引问题 + # for i in reversed(tilesets_to_remove): + # del self.world.scene_manager.tilesets[i] + # + # # 更新场景树 + # self.world.scene_manager.updateSceneTree() + # + # # 清空属性面板 + # self.clearPropertyPanel() - # 从 tilesets 列表中移除 - if hasattr(self.world, 'scene_manager'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - if tileset_info['node'] == nodePath: - tilesets_to_remove.append(i) - - # 从后往前删除,避免索引问题 - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - - # 更新场景树 - self.world.scene_manager.updateSceneTree() - - # 清空属性面板 - self.clearPropertyPanel() - - print(f"成功删除 Cesium tileset: {nodePath.getName()}") + # print(f"成功删除 Cesium tileset: {nodePath.getName()}") except Exception as e: print(f"删除 Cesium tileset 失败: {str(e)}") @@ -1046,7 +1116,7 @@ class PropertyPanelManager: def _updateModelPropertyPanel(self, model): """更新模型属性面板""" - if hasattr(model,'getTag') and model.getTag("is_gui_element") == "1": + if hasattr(model, 'getTag') and model.getTag("is_gui_element") == "1": self.updateGUIPropertyPanel(model) return @@ -1083,16 +1153,19 @@ class PropertyPanelManager: 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 标签居中 @@ -1206,6 +1279,9 @@ class PropertyPanelManager: self.transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(self.transform_group) + # 碰撞检测面板 + self._addCollisionPanel(model) + # 动画和太阳方位角面板 self._addAnimationPanel(model) self._addSunAzimuthPanel() @@ -1273,12 +1349,54 @@ class PropertyPanelManager: model.setScale(current_scale.getX(), current_scale.getY(), value) self.refreshModelValues(model) - - - def updateGUIPropertyPanel(self, gui_element): + def updateGUIPropertyPanel(self, gui_element, item): """更新GUI元素属性面板""" self.clearPropertyPanel() + itemText = gui_element.getTag("name") or "未命名GUI元素" + + user_visible = True + user_visible = gui_element.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + gui_element.setPythonTag("user_visible", True) + + self.name_group = QGroupBox("物体名称") + name_layout = QHBoxLayout() + self.active_check = QCheckBox() + # 根据元素的实际可见性状态设置复选框 + self.active_check.setChecked(user_visible) + self.name_input = QLineEdit(itemText) + + # 注意:对于GUI元素,我们需要特殊处理名称更新 + def updateGUIName(text): + # 更新GUI元素的标签 + gui_element.setTag("name", text) + self.world.treeWidget.update_item_name(self.name_input.text(), item) + # gui_element.setName(text) + # 如果有场景管理器,也需要更新场景树 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + + self.name_input.returnPressed.connect(lambda: updateGUIName(self.name_input.text())) + + # 如果失去焦点也更新名称 + self.name_input.editingFinished.connect(lambda: updateGUIName(self.name_input.text())) + + 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 gui_element: + try: + self.active_check.stateChanged.disconnect() + except TypeError: + pass + self.active_check.stateChanged.connect( + lambda state, elem=gui_element: self._setUserVisible(elem, state == Qt.Checked) + ) + gui_type = gui_element.getTag("gui_type") gui_text = gui_element.getTag("gui_text") @@ -1298,15 +1416,19 @@ class PropertyPanelManager: gui_info_layout.addWidget(QLabel("文本:"), 1, 0) textEdit = QLineEdit(gui_text or "") - # 创建一个更新函数来处理文本变化 - def updateText(text): + # 使用编辑完成信号而不是文本变化信号 + def updateText(): + text = textEdit.text() success = self.world.gui_manager.editGUIElement(gui_element, "text", text) - if success: - # 更新场景树显示的名称 - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - self.world.scene_manager.updateSceneTree() + # if success: + # # 更新场景树显示的名称 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + + # 只在按下回车键或失去焦点时更新 + textEdit.returnPressed.connect(updateText) + textEdit.editingFinished.connect(updateText) - textEdit.textChanged.connect(updateText) gui_info_layout.addWidget(textEdit, 1, 1) gui_info_group.setLayout(gui_info_layout) @@ -1315,7 +1437,7 @@ class PropertyPanelManager: # 变换属性组(合并位置和变换) if hasattr(gui_element, 'getPos'): # 根据GUI类型设置组名—— - if gui_type in ["button", "label", "entry","2d_image","2d_video_screen"]: + if gui_type in ["button", "label", "entry", "2d_image", "2d_video_screen"]: transform_group = QGroupBox("变换 Rect Transform") else: transform_group = QGroupBox("变换 Transform") @@ -1325,7 +1447,7 @@ class PropertyPanelManager: pos = gui_element.getPos() # 根据GUI类型决定位置编辑方式 - if gui_type in ["button", "label", "entry","2d_image","2d_video_screen"]: + if gui_type in ["button", "label", "entry", "2d_image", "2d_video_screen"]: # 2D GUI组件使用屏幕坐标 logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 logical_z = pos.getZ() / 0.1 @@ -1365,35 +1487,38 @@ class PropertyPanelManager: transform_layout.addWidget(actualXLabel, 3, 1) transform_layout.addWidget(actualZLabel, 3, 2) - if gui_type in ["2d_image","2d_video_screen"]: + if gui_type in ["2d_image", "2d_video_screen"]: scale = gui_element.getScale() - width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale,(tuple, list)) else scale - height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale,(tuple, list)) and len(scale) > 1 else scale + width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, + (tuple, list)) else scale + height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, + (tuple, list)) and len( + scale) > 1 else scale - transform_layout.addWidget(QLabel("宽度"),4,0) + transform_layout.addWidget(QLabel("宽度"), 4, 0) self.scale_x = QDoubleSpinBox() - self.scale_x.setRange(0.1,100) + self.scale_x.setRange(0.1, 100) self.scale_x.setSingleStep(0.01) self.scale_x.setValue(width) - self.scale_x.valueChanged.connect(lambda v:self.editGUIScale(gui_element,"x",v)) - transform_layout.addWidget(self.scale_x,4,1) + self.scale_x.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "x", v)) + transform_layout.addWidget(self.scale_x, 4, 1) - transform_layout.addWidget(QLabel("高度"),4,2) + transform_layout.addWidget(QLabel("高度"), 4, 2) self.scale_z = QDoubleSpinBox() - self.scale_z.setRange(0.1,10) + self.scale_z.setRange(0.1, 10) self.scale_z.setSingleStep(0.01) self.scale_z.setValue(height) - self.scale_z.valueChanged.connect(lambda v:self.editGUIScale(gui_element,"z",v)) - transform_layout.addWidget(self.scale_z,4,3) + self.scale_z.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "z", v)) + transform_layout.addWidget(self.scale_z, 4, 3) else: - transform_layout.addWidget(QLabel("缩放"),4,0) + transform_layout.addWidget(QLabel("缩放"), 4, 0) scaleSpinBox = QDoubleSpinBox() - scaleSpinBox.setRange(0.01,100) + scaleSpinBox.setRange(0.01, 100) scaleSpinBox.setSingleStep(0.01) - scaleSpinBox.setValue(gui_element.getScale().getX()*2) - scaleSpinBox.valueChanged.connect(lambda v:self._update2DImageScale(gui_element,v)) - transform_layout.addWidget(scaleSpinBox,4,1) + scaleSpinBox.setValue(gui_element.getScale().getX() * 2) + scaleSpinBox.valueChanged.connect(lambda v: self._update2DImageScale(gui_element, v)) + transform_layout.addWidget(scaleSpinBox, 4, 1) else: # 3D GUI组件使用世界坐标 @@ -1476,12 +1601,11 @@ class PropertyPanelManager: self.scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) transform_layout.addWidget(self.scale_z, 3, 3) - transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) # 为2D图像和视频屏幕添加Sort属性 - if gui_type in ["2d_image","2d_video_screen","info_panel"]: + if gui_type in ["2d_image", "2d_video_screen", "info_panel"]: sort_group = QGroupBox("显示顺序") sort_layout = QGridLayout() @@ -1548,7 +1672,8 @@ class PropertyPanelManager: # 显示当前贴图路径(简化显示) current_texture_path = gui_element.getTag("texture_path") or "未设置" if current_texture_path != "未设置": - display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[:7] + "..." + display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ + :7] + "..." else: display_path = current_texture_path texture_label = QLabel(display_path) @@ -1575,7 +1700,7 @@ class PropertyPanelManager: if success: # 保存路径到 Tag gui_element.setTag("texture_path", file_path) - gui_element.setTag("gui_image_path",file_path) + gui_element.setTag("gui_image_path", file_path) # 更新显示 texture_label.setText(file_path) # 可选:刷新场景树或其他 UI @@ -1586,7 +1711,7 @@ class PropertyPanelManager: image_group.setLayout(image_layout) self._propertyLayout.addWidget(image_group) - if gui_type in [ "2d_image"]: + if gui_type in ["2d_image"]: image_group = QGroupBox("2D图片设置") image_layout = QGridLayout() @@ -1597,7 +1722,8 @@ class PropertyPanelManager: # 显示当前贴图路径(简化显示) current_texture_path = gui_element.getTag("texture_path") or gui_element.getTag("image_path") or "未设置" if current_texture_path != "未设置": - display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[:7] + "..." + display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ + :7] + "..." else: display_path = current_texture_path texture_label = QLabel(display_path) @@ -1637,12 +1763,12 @@ class PropertyPanelManager: # 添加弹性空间 if gui_type == "video_screen": - self._addVideoScreenProperties(gui_element) + self._addVideoScreenProperties(gui_element, item) elif gui_type == "2d_video_screen": - self._add2DVideoScreenProperties(gui_element) + self._add2DVideoScreenProperties(gui_element, item) elif gui_type == "spherical_video": self._addSphericalVideoProperties(gui_element) - elif gui_type == 'info_panel': + elif gui_type in ['info_panel', 'info_panel_3d']: self._addInfoPanelProperties(gui_element) self._propertyLayout.addStretch() @@ -1655,7 +1781,7 @@ class PropertyPanelManager: propertyWidget.update() def _addInfoPanelProperties(self, info_panel): - """为信息面板添加属性控制面板""" + """为信息面板添加属性控制面板 - 同时适配2D和3D信息面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, QDoubleSpinBox, QColorDialog, QSpinBox, QTextEdit) @@ -1668,9 +1794,13 @@ class PropertyPanelManager: print("无法找到信息面板ID") return + # 检测面板类型 (2D或3D) + gui_type = info_panel.getTag("gui_type") + is_3d_panel = gui_type == "info_panel_3d" + # 获取面板当前属性 current_size = (1.0, 0.6) - current_position = (0, 0) + current_position = (0, 0) if not is_3d_panel else (0, 0, 0) current_bg_color = (0.15, 0.15, 0.15, 0.9) current_border_color = (0.3, 0.3, 0.3, 1.0) current_title_color = (1.0, 1.0, 0.0, 1.0) @@ -1701,44 +1831,55 @@ class PropertyPanelManager: current_content_color = props.get('content_color', current_content_color) current_bg_image = props.get('bg_image', current_bg_image) - # 获取文本内容 - if 'title_label' in panel_data and panel_data['title_label']: - current_title_text = panel_data['title_label']['text'] - # 获取标题位置 - title_pos = panel_data['title_label'].getPos() - current_title_pos = (title_pos.getX(), title_pos.getZ()) + # 获取文本内容和位置(根据面板类型区分处理) + if not is_3d_panel: # 2D面板 + if 'title_label' in panel_data and panel_data['title_label']: + current_title_text = panel_data['title_label']['text'] + # 获取标题位置 + title_pos = panel_data['title_label'].getPos() + current_title_pos = (title_pos.getX(), title_pos.getZ()) - if 'content_label' in panel_data and panel_data['content_label']: - current_content_text = panel_data['content_label']['text'] - # 获取内容位置 - content_pos = panel_data['content_label'].getPos() - current_content_pos = (content_pos.getX(), content_pos.getZ()) + if 'content_label' in panel_data and panel_data['content_label']: + current_content_text = panel_data['content_label']['text'] + # 获取内容位置 + content_pos = panel_data['content_label'].getPos() + current_content_pos = (content_pos.getX(), content_pos.getZ()) - # 获取字体大小(确保获取的是数值而不是元组) - if 'title_label' in panel_data and panel_data['title_label']: - # 正确获取DirectLabel的text_scale属性 - try: - title_scale = panel_data['title_label']['text_scale'] - # 如果是元组,取第一个值 - if isinstance(title_scale, (tuple, list)): - current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 - else: - current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 - except: - current_title_size = 0.06 + # 获取字体大小 + if 'title_label' in panel_data and panel_data['title_label']: + try: + title_scale = panel_data['title_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(title_scale, (tuple, list)): + current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 + else: + current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 + except: + current_title_size = 0.06 - if 'content_label' in panel_data and panel_data['content_label']: - # 正确获取DirectLabel的text_scale属性 - try: - content_scale = panel_data['content_label']['text_scale'] - # 如果是元组,取第一个值 - if isinstance(content_scale, (tuple, list)): - current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 - else: - current_content_size = content_scale if isinstance(content_scale, - (int, float)) else 0.045 - except: - current_content_size = 0.045 + if 'content_label' in panel_data and panel_data['content_label']: + try: + content_scale = panel_data['content_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(content_scale, (tuple, list)): + current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 + else: + current_content_size = content_scale if isinstance(content_scale, + (int, float)) else 0.045 + except: + current_content_size = 0.045 + else: # 3D面板 + if 'title_node' in panel_data and panel_data['title_node']: + current_title_text = panel_data['title_node'].getText() + + if 'content_node' in panel_data and panel_data['content_node']: + current_content_text = panel_data['content_node'].getText() + + if 'title_text' in panel_data and panel_data['title_text']: + current_title_size = panel_data['title_text'].getScale().getX() + + if 'content_text' in panel_data and panel_data['content_text']: + current_content_size = panel_data['content_text'].getScale().getX() # 获取sort值 if info_panel.hasTag("sort"): @@ -1859,12 +2000,16 @@ class PropertyPanelManager: panel_width = width_spin.value() panel_height = height_spin.value() - # 更新面板大小 - self.world.info_panel_manager.updatePanelProperties( - panel_id, size=(panel_width, panel_height)) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelProperties( + panel_id, size=(panel_width, panel_height)) + else: + self.world.info_panel_manager.updatePanelProperties( + panel_id, size=(panel_width, panel_height)) - # 同步更新内容的换行设置 - self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) + # 同步更新内容的换行设置 + self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) width_spin.valueChanged.connect(onSizeChanged) height_spin.valueChanged.connect(onSizeChanged) @@ -1872,6 +2017,70 @@ class PropertyPanelManager: size_group.setLayout(size_layout) self._propertyLayout.addWidget(size_group) + # 位置设置组 (根据面板类型显示不同的位置控件) + position_group = QGroupBox("位置设置") + position_layout = QGridLayout() + + if is_3d_panel: + # 3D面板位置设置 (X, Y, Z) + position_layout.addWidget(QLabel("X:"), 0, 0) + pos_x_spin = QDoubleSpinBox() + pos_x_spin.setRange(-1000, 1000) + pos_x_spin.setSingleStep(0.1) + pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) + position_layout.addWidget(pos_x_spin, 0, 1) + + position_layout.addWidget(QLabel("Y:"), 0, 2) + pos_y_spin = QDoubleSpinBox() + pos_y_spin.setRange(-1000, 1000) + pos_y_spin.setSingleStep(0.1) + pos_y_spin.setValue(current_position[1] if len(current_position) > 1 else 0) + position_layout.addWidget(pos_y_spin, 0, 3) + + position_layout.addWidget(QLabel("Z:"), 1, 0) + pos_z_spin = QDoubleSpinBox() + pos_z_spin.setRange(-1000, 1000) + pos_z_spin.setSingleStep(0.1) + pos_z_spin.setValue(current_position[2] if len(current_position) > 2 else 0) + position_layout.addWidget(pos_z_spin, 1, 1) + + # 连接位置变化信号 + def on3DPositionChanged(): + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.update3DPanelProperties( + panel_id, position=(pos_x_spin.value(), pos_y_spin.value(), pos_z_spin.value())) + + pos_x_spin.valueChanged.connect(on3DPositionChanged) + pos_y_spin.valueChanged.connect(on3DPositionChanged) + pos_z_spin.valueChanged.connect(on3DPositionChanged) + else: + # 2D面板位置设置 (X, Z) + position_layout.addWidget(QLabel("X:"), 0, 0) + pos_x_spin = QDoubleSpinBox() + pos_x_spin.setRange(-1000, 1000) + pos_x_spin.setSingleStep(0.1) + pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) + position_layout.addWidget(pos_x_spin, 0, 1) + + position_layout.addWidget(QLabel("Z:"), 0, 2) + pos_z_spin = QDoubleSpinBox() + pos_z_spin.setRange(-1000, 1000) + pos_z_spin.setSingleStep(0.1) + pos_z_spin.setValue(current_position[1] if len(current_position) > 1 else 0) + position_layout.addWidget(pos_z_spin, 0, 3) + + # 连接位置变化信号 + def on2DPositionChanged(): + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.updatePanelProperties( + panel_id, position=(pos_x_spin.value(), pos_z_spin.value())) + + pos_x_spin.valueChanged.connect(on2DPositionChanged) + pos_z_spin.valueChanged.connect(on2DPositionChanged) + + position_group.setLayout(position_layout) + self._propertyLayout.addWidget(position_group) + # 边框属性组 border_group = QGroupBox("边框属性") border_layout = QGridLayout() @@ -1919,15 +2128,19 @@ class PropertyPanelManager: # 连接边框颜色变化信号 def onBorderColorChanged(): if hasattr(self.world, 'info_panel_manager'): - self.world.info_panel_manager.updatePanelProperties( - panel_id, - border_color=( - border_r_spin.value(), - border_g_spin.value(), - border_b_spin.value(), - border_a_spin.value() - ) + border_color = ( + border_r_spin.value(), + border_g_spin.value(), + border_b_spin.value(), + border_a_spin.value() ) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelProperties( + panel_id, border_color=border_color) + else: + self.world.info_panel_manager.updatePanelProperties( + panel_id, border_color=border_color) border_r_spin.valueChanged.connect(onBorderColorChanged) border_g_spin.valueChanged.connect(onBorderColorChanged) @@ -2049,39 +2262,51 @@ class PropertyPanelManager: title_size_spin = QDoubleSpinBox() title_size_spin.setRange(0.01, 1.0) title_size_spin.setSingleStep(0.01) - title_size_spin.setValue(current_title_size) # 现在确保是浮点数 + title_size_spin.setValue(current_title_size) title_layout.addWidget(title_size_spin, 5, 1, 1, 3) - # 标题位置控制 - title_layout.addWidget(QLabel("标题位置:"), 6, 0) - title_layout.addWidget(QLabel("X:"), 7, 0) - title_x_spin = QDoubleSpinBox() - title_x_spin.setRange(-1.0, 1.0) - title_x_spin.setSingleStep(0.01) - title_x_spin.setValue(current_title_pos[0]) # 从面板获取的实际位置 - title_layout.addWidget(title_x_spin, 7, 1) + # 标题位置控制 (仅对2D面板) + if not is_3d_panel: + title_layout.addWidget(QLabel("标题位置:"), 6, 0) + title_layout.addWidget(QLabel("X:"), 7, 0) + title_x_spin = QDoubleSpinBox() + title_x_spin.setRange(-1.0, 1.0) + title_x_spin.setSingleStep(0.01) + title_x_spin.setValue(current_title_pos[0]) + title_layout.addWidget(title_x_spin, 7, 1) - title_layout.addWidget(QLabel("Y:"), 7, 2) - title_y_spin = QDoubleSpinBox() - title_y_spin.setRange(-1.0, 1.0) - title_y_spin.setSingleStep(0.01) - title_y_spin.setValue(current_title_pos[1]) # 从面板获取的实际位置 - title_layout.addWidget(title_y_spin, 7, 3) + title_layout.addWidget(QLabel("Y:"), 7, 2) + title_y_spin = QDoubleSpinBox() + title_y_spin.setRange(-1.0, 1.0) + title_y_spin.setSingleStep(0.01) + title_y_spin.setValue(current_title_pos[1]) + title_layout.addWidget(title_y_spin, 7, 3) - # 连接标题属性变化信号,自动更新标题属性 + # 连接标题属性变化信号 def onTitleTextChanged(): if hasattr(self.world, 'info_panel_manager'): - self.world.info_panel_manager.updatePanelContent( - panel_id, title=title_text_edit.toPlainText()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelContent( + panel_id, title=title_text_edit.toPlainText()) + else: + self.world.info_panel_manager.updatePanelContent( + panel_id, title=title_text_edit.toPlainText()) def onTitlePropertyChanged(): - self._applyInfoPanelTitleProperties( - info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), - title_a_spin.value(), title_size_spin.value()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self._applyInfoPanel3DTitleProperties( + info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), + title_a_spin.value(), title_size_spin.value()) + else: + self._applyInfoPanelTitleProperties( + info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), + title_a_spin.value(), title_size_spin.value()) def onTitlePositionChanged(): - # 更新标题位置 - if hasattr(self.world, 'info_panel_manager'): + # 仅对2D面板更新标题位置 + if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data and 'title_label' in panel_data: # 设置标题位置 @@ -2093,8 +2318,10 @@ class PropertyPanelManager: title_b_spin.valueChanged.connect(onTitlePropertyChanged) title_a_spin.valueChanged.connect(onTitlePropertyChanged) title_size_spin.valueChanged.connect(onTitlePropertyChanged) - title_x_spin.valueChanged.connect(onTitlePositionChanged) - title_y_spin.valueChanged.connect(onTitlePositionChanged) + + if not is_3d_panel: # 仅对2D面板连接位置变化信号 + title_x_spin.valueChanged.connect(onTitlePositionChanged) + title_y_spin.valueChanged.connect(onTitlePositionChanged) title_group.setLayout(title_layout) self._propertyLayout.addWidget(title_group) @@ -2156,61 +2383,89 @@ class PropertyPanelManager: content_size_spin = QDoubleSpinBox() content_size_spin.setRange(0.01, 1.0) content_size_spin.setSingleStep(0.01) - content_size_spin.setValue(current_content_size) # 现在确保是浮点数 + content_size_spin.setValue(current_content_size) content_layout.addWidget(content_size_spin, 5, 1, 1, 3) - # 内容位置控制 - content_layout.addWidget(QLabel("内容位置:"), 6, 0) - content_layout.addWidget(QLabel("X:"), 7, 0) - content_x_spin = QDoubleSpinBox() - content_x_spin.setRange(-1.0, 1.0) - content_x_spin.setSingleStep(0.01) - content_x_spin.setValue(current_content_pos[0]) # 从面板获取的实际位置 - content_layout.addWidget(content_x_spin, 7, 1) + # 内容位置控制 (仅对2D面板) + if not is_3d_panel: + content_layout.addWidget(QLabel("内容位置:"), 6, 0) + content_layout.addWidget(QLabel("X:"), 7, 0) + content_x_spin = QDoubleSpinBox() + content_x_spin.setRange(-1.0, 1.0) + content_x_spin.setSingleStep(0.01) + content_x_spin.setValue(current_content_pos[0]) + content_layout.addWidget(content_x_spin, 7, 1) - content_layout.addWidget(QLabel("Y:"), 7, 2) - content_y_spin = QDoubleSpinBox() - content_y_spin.setRange(-1.0, 1.0) - content_y_spin.setSingleStep(0.01) - content_y_spin.setValue(current_content_pos[1]) # 从面板获取的实际位置 - content_layout.addWidget(content_y_spin, 7, 3) + content_layout.addWidget(QLabel("Y:"), 7, 2) + content_y_spin = QDoubleSpinBox() + content_y_spin.setRange(-1.0, 1.0) + content_y_spin.setSingleStep(0.01) + content_y_spin.setValue(current_content_pos[1]) + content_layout.addWidget(content_y_spin, 7, 3) - # 连接内容属性变化信号,自动更新内容属性 + # 连接内容属性变化信号 def onContentTextChanged(): if hasattr(self.world, 'info_panel_manager'): - self.world.info_panel_manager.updatePanelContent( - panel_id, content=content_text_edit.toPlainText()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelContent( + panel_id, content=content_text_edit.toPlainText()) + else: + self.world.info_panel_manager.updatePanelContent( + panel_id, content=content_text_edit.toPlainText()) def onContentPropertyChanged(): - self._applyInfoPanelContentProperties( - info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), - content_a_spin.value(), content_size_spin.value()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self._applyInfoPanel3DContentProperties( + info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), + content_a_spin.value(), content_size_spin.value()) + else: + self._applyInfoPanelContentProperties( + info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), + content_a_spin.value(), content_size_spin.value()) def onContentPositionChanged(): - # 更新内容位置 - if hasattr(self.world, 'info_panel_manager'): + # 仅对2D面板更新内容位置 + if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data and 'content_label' in panel_data: # 设置内容位置 panel_data['content_label'].setPos(content_x_spin.value(), 0, content_y_spin.value()) - # 添加内容换行调整函数 def adjustContentWordwrap(): """根据面板宽度和字体大小调整内容换行""" - panel_width = width_spin.value() - font_size = content_size_spin.value() - self._adjustContentWordwrap(panel_id, panel_width, font_size) + # 仅对2D面板调整换行 + if not is_3d_panel: + panel_width = width_spin.value() + font_size = content_size_spin.value() + self._adjustContentWordwrap(panel_id, panel_width, font_size) # 连接相关信号 - content_text_edit.textChanged.connect(onContentTextChanged) content_r_spin.valueChanged.connect(onContentPropertyChanged) content_g_spin.valueChanged.connect(onContentPropertyChanged) content_b_spin.valueChanged.connect(onContentPropertyChanged) content_a_spin.valueChanged.connect(onContentPropertyChanged) - content_size_spin.valueChanged.connect(lambda: [onContentPropertyChanged(), adjustContentWordwrap()]) - content_x_spin.valueChanged.connect(onContentPositionChanged) - content_y_spin.valueChanged.connect(onContentPositionChanged) - width_spin.valueChanged.connect(adjustContentWordwrap) + + # 内容字体大小变化信号连接 + content_size_spin.valueChanged.connect(onContentPropertyChanged) + + # 内容文本变化信号连接 + content_text_edit.textChanged.connect(onContentTextChanged) + + # 标题相关信号连接 + title_text_edit.textChanged.connect(onTitleTextChanged) + title_r_spin.valueChanged.connect(onTitlePropertyChanged) + title_g_spin.valueChanged.connect(onTitlePropertyChanged) + title_b_spin.valueChanged.connect(onTitlePropertyChanged) + title_a_spin.valueChanged.connect(onTitlePropertyChanged) + title_size_spin.valueChanged.connect(onTitlePropertyChanged) + + if not is_3d_panel: # 仅对2D面板连接位置变化信号 + title_x_spin.valueChanged.connect(onTitlePositionChanged) + title_y_spin.valueChanged.connect(onTitlePositionChanged) + content_x_spin.valueChanged.connect(onContentPositionChanged) + content_y_spin.valueChanged.connect(onContentPositionChanged) content_group.setLayout(content_layout) self._propertyLayout.addWidget(content_group) @@ -2230,27 +2485,65 @@ class PropertyPanelManager: 'content_a_spin': content_a_spin, 'title_size_spin': title_size_spin, 'content_size_spin': content_size_spin, - 'title_x_spin': title_x_spin, - 'title_y_spin': title_y_spin, - 'content_x_spin': content_x_spin, - 'content_y_spin': content_y_spin, 'width_spin': width_spin, 'height_spin': height_spin } + # 对于2D面板,添加位置控件引用 + if not is_3d_panel: + self._info_panel_controls.update({ + 'title_x_spin': title_x_spin, + 'title_y_spin': title_y_spin, + 'content_x_spin': content_x_spin, + 'content_y_spin': content_y_spin + }) + # 启动定时器定期检查信息面板内容变化 self._startInfoPanelMonitoring(info_panel, panel_id) - # 初始调整内容换行 - adjustContentWordwrap() + # 初始调整内容换行 (仅对2D面板) + if not is_3d_panel: + adjustContentWordwrap() - print("✅ 信息面板属性面板已添加") + print(f"✅ 信息面板属性面板已添加 (类型: {'3D' if is_3d_panel else '2D'})") except Exception as e: print(f"❌ 添加信息面板属性面板失败: {e}") import traceback traceback.print_exc() + def _applyInfoPanel3DTitleProperties(self, info_panel, r, g, b, a, size): + """应用3D信息面板标题属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id or not hasattr(self.world, 'info_panel_manager'): + return + + # 更新3D面板标题属性 + self.world.info_panel_manager.update3DPanelProperties( + panel_id, + title_color=(r, g, b, a), + title_size=size + ) + except Exception as e: + print(f"应用3D信息面板标题属性失败: {e}") + + def _applyInfoPanel3DContentProperties(self, info_panel, r, g, b, a, size): + """应用3D信息面板内容属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id or not hasattr(self.world, 'info_panel_manager'): + return + + # 更新3D面板内容属性 + self.world.info_panel_manager.update3DPanelProperties( + panel_id, + content_color=(r, g, b, a), + content_size=size + ) + except Exception as e: + print(f"应用3D信息面板内容属性失败: {e}") + def _adjustContentWordwrap(self, panel_id, panel_width, font_size): """调整信息面板内容的换行设置""" try: @@ -2379,7 +2672,7 @@ class PropertyPanelManager: color = QColorDialog.getColor(current_color, None, "选择边框颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) @@ -2388,7 +2681,7 @@ class PropertyPanelManager: print(f"❌ 选择边框颜色失败: {e}") def updateInfoPanelBackgroundImage(self, info_panel, image_path): - """更新信息面板背景图片""" + """更新信息面板背景图片 - 修复版""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: @@ -2399,25 +2692,33 @@ class PropertyPanelManager: if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager - # 直接调用 InfoPanelManager 的方法设置背景图片 - if image_path: - # 设置新的背景图片 - success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) - if success: - print(f"✅ 成功设置信息面板背景图片: {image_path}") - return True - else: - print(f"❌ 设置信息面板背景图片失败: {image_path}") - return False + # 检查是否是3D信息面板 + gui_type = info_panel.getTag("gui_type") + is_3d_panel = gui_type == "info_panel_3d" + + if is_3d_panel: + # 对于3D信息面板,使用专门的方法 + return self._update3DInfoPanelBackgroundImage(panel_id, info_panel, image_path) else: - # 清除背景图片 - success = info_panel_manager.setPanelBackgroundImage(panel_id, None) - if success: - print("✅ 成功清除信息面板背景图片") - return True + # 对于2D信息面板,使用原有方法 + if image_path: + # 设置新的背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) + if success: + print(f"✅ 成功设置信息面板背景图片: {image_path}") + return True + else: + print(f"❌ 设置信息面板背景图片失败: {image_path}") + return False else: - print("❌ 清除信息面板背景图片失败") - return False + # 清除背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, None) + if success: + print("✅ 成功清除信息面板背景图片") + return True + else: + print("❌ 清除信息面板背景图片失败") + return False else: print("❌ 未找到 info_panel_manager") return False @@ -2428,13 +2729,166 @@ class PropertyPanelManager: traceback.print_exc() return False + def _applyInfoPanelBackgroundImage(self, panel_node, image_path, panel_data): + """应用信息面板背景图片 - 改进版""" + try: + from panda3d.core import CardMaker, TransparencyAttrib, Vec4 + + # 如果已有背景图片,先移除 + if 'bg_image' in panel_data and panel_data['bg_image']: + try: + panel_data['bg_image'].removeNode() + except: + pass + panel_data['bg_image'] = None + + # 加载纹理 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"❌ 无法加载背景图片: {image_path}") + return False + + # 设置纹理属性 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 获取面板尺寸 + size = panel_data.get('size', (1.0, 0.6)) + + # 创建背景卡片 + cm = CardMaker('info_panel_background') + cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + + # 生成几何体并创建节点 + bg_node = panel_node.attachNewNode(cm.generate()) + + # 应用纹理 + bg_node.setTexture(texture, 1) + + # 设置渲染属性 + bg_node.setTransparency(TransparencyAttrib.MAlpha) + bg_node.setBin("background", -10) + bg_node.setDepthWrite(False) + bg_node.setDepthTest(False) + bg_node.setLightOff() + bg_node.setTwoSided(True) + + # 确保在最底层 + bg_node.setZ(-0.1) + + # 保存引用 + panel_data['bg_image'] = bg_node + + print(f"✅ 成功应用信息面板背景图片: {image_path}") + return True + + except Exception as e: + print(f"❌ 应用信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update3DInfoPanelBackgroundImage(self, panel_id, info_panel, image_path): + """更新3D信息面板背景图片 - 完整修复版""" + try: + # 从info_panel_manager获取面板数据 + if not hasattr(self.world, 'info_panel_manager'): + print("❌ 未找到 info_panel_manager") + return False + + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if not panel_data: + print(f"❌ 无法找到面板数据: {panel_id}") + return False + + # 获取面板节点 + panel_node = panel_data.get('node') + if not panel_node: + print("❌ 无法找到面板节点") + return False + + # 如果已有背景图片,先销毁它 + if 'bg_image' in panel_data and panel_data['bg_image']: + try: + # 如果是NodePath对象,使用removeNode方法 + if hasattr(panel_data['bg_image'], 'removeNode'): + panel_data['bg_image'].removeNode() + # 如果是DirectGUI对象,使用destroy方法 + elif hasattr(panel_data['bg_image'], 'destroy'): + panel_data['bg_image'].destroy() + except Exception as e: + print(f"⚠️ 清理旧背景图片时出错: {e}") + panel_data['bg_image'] = None + + # 如果image_path为None,只清除背景图片 + if not image_path: + # 清除节点标签 + if info_panel.hasTag("bg_image_path"): + info_panel.clearTag("bg_image_path") + print("✅ 成功清除3D信息面板背景图片") + return True + + # 使用 world.loader 加载新图片 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"❌ 无法加载图片: {image_path}") + return False + + # 设置纹理过滤 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 获取面板大小 + size = panel_data['properties'].get('size', (1.0, 0.6)) + + # 使用CardMaker创建卡片几何体 + from panda3d.core import CardMaker, TransparencyAttrib + + cm = CardMaker('info_panel_bg') + # 设置卡片大小,居中放置 + cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + bg_geom = cm.generate() + + # 创建节点并附加几何体,直接附加到面板节点 + bg_node = panel_node.attachNewNode(bg_geom) + + # 应用纹理 + bg_node.setTexture(texture, 1) + + # 设置正确的渲染属性 + bg_node.setTransparency(TransparencyAttrib.MAlpha) + bg_node.setBin("background", -10) # 确保在最底层 + bg_node.setDepthWrite(False) + bg_node.setDepthTest(False) + bg_node.setLightOff() + bg_node.setTwoSided(True) # 双面渲染 + + # 调整位置确保在面板内容后面 + bg_node.setPos(0, 0.1, 0) # 稍微向后一点避免z-fighting + + # 保存引用 + panel_data['bg_image'] = bg_node + panel_data['properties']['bg_image'] = image_path + + # 更新节点标签 + info_panel.setTag("bg_image_path", image_path) + + print(f"✅ 成功设置3D信息面板背景图片: {image_path}") + return True + + except Exception as e: + print(f"❌ 更新3D信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + def _selectInfoPanelBackgroundColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板背景颜色""" try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor - # 获取当前颜色值 + # 获取当前背景颜色 current_color = QColor( int(r_spin.value() * 255), int(g_spin.value() * 255), @@ -2442,40 +2896,41 @@ class PropertyPanelManager: int(a_spin.value() * 255) ) - # 打开颜色选择对话框 + # 显示颜色选择对话框 color = QColorDialog.getColor(current_color, None, "选择背景颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 - g_spin.setValue(color.greenF()) - b_spin.setValue(color.blueF()) - a_spin.setValue(color.alphaF()) + r_spin.setValue(color.red() / 255.0) + g_spin.setValue(color.green() / 255.0) + b_spin.setValue(color.blue() / 255.0) + a_spin.setValue(color.alpha() / 255.0) except Exception as e: - print(f"❌ 选择背景颜色失败: {e}") + print(f"选择背景颜色失败: {e}") - def _applyInfoPanelBackgroundColor(self,info_panel,r,g,b,a): - """应用信息面部背景颜色""" + def _applyInfoPanelBackgroundColor(self, info_panel, r, g, b, a): + """应用信息面板背景颜色""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: - print("❌ 找不到信息面板ID") return - if hasattr(self.world,'info_panel_manager'): - info_panel_manager = self.world.info_panel_manager - success = info_panel_manager.updatePanelProperties(panel_id,bg_color=(r,g,b,a)) - if success: - print(f"✅ 成功设置信息面板背景颜色: R={r:.2f}, G={g:.2f}, B={b:.2f}, A={a:.2f}") - return True + # 更新InfoPanelManager中的背景颜色 + if hasattr(self.world, 'info_panel_manager'): + bg_color = (r, g, b, a) + + # 根据面板类型选择更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + self.world.info_panel_manager.update3DPanelProperties( + panel_id, bg_color=bg_color) else: - print("❌ 设置信息面板背景颜色失败") - else: - print("❌ 未找到 info_panel_manager") - return False + self.world.info_panel_manager.updatePanelProperties( + panel_id, bg_color=bg_color) + + print(f"已更新信息面板 {panel_id} 的背景颜色为 ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") except Exception as e: - print(f"应用背景颜色失败{e}") - return False + print(f"应用背景颜色失败: {e}") def _selectInfoPanelTitleColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板标题颜色""" @@ -2495,7 +2950,7 @@ class PropertyPanelManager: color = QColorDialog.getColor(current_color, None, "选择标题颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) @@ -2521,7 +2976,7 @@ class PropertyPanelManager: color = QColorDialog.getColor(current_color, None, "选择内容颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) @@ -2541,38 +2996,31 @@ class PropertyPanelManager: if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager - # 更新标题颜色和字体大小 - # 注意:DirectGUI中字体大小通过text_scale设置,需要直接操作title_label对象 - panel_data = info_panel_manager.panels.get(panel_id) - if panel_data and 'title_label' in panel_data: - # 更新颜色 - panel_data['title_label']['text_fg'] = (r, g, b, a) - # 更新字体大小 - panel_data['title_label']['text_scale'] = size - # 同时更新属性存储 - if 'properties' in panel_data: - panel_data['properties']['title_color'] = (r, g, b, a) - - print(f"✅ 成功设置信息面板标题属性: 颜色=({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小={size}") - return True - else: - # 回退到使用updatePanelProperties方法 - success = info_panel_manager.updatePanelProperties( + # 根据面板类型选择不同的更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + # 3D面板使用update3DPanelProperties + info_panel_manager.update3DPanelProperties( panel_id, - title_color=(r, g, b, a) + title_color=(r, g, b, a), + title_size=size + ) + else: + # 2D面板使用updatePanelProperties + info_panel_manager.updatePanelProperties( + panel_id, + title_color=(r, g, b, a), + title_size=size ) - if success: - print(f"✅ 成功设置信息面板标题颜色: ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") - return True - print("❌ 设置信息面板标题属性失败") - return False + print(f"✓ 信息面板标题属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") + return True else: - print("❌ 未找到 info_panel_manager") + print("❌ 无法找到 info_panel_manager") return False except Exception as e: - print(f"❌ 应用标题属性失败: {e}") + print(f"❌ 应用信息面板标题属性失败: {e}") return False def _applyInfoPanelContentProperties(self, info_panel, r, g, b, a, size): @@ -2587,38 +3035,31 @@ class PropertyPanelManager: if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager - # 更新内容颜色和字体大小 - # 注意:DirectGUI中字体大小通过text_scale设置,需要直接操作content_label对象 - panel_data = info_panel_manager.panels.get(panel_id) - if panel_data and 'content_label' in panel_data: - # 更新颜色 - panel_data['content_label']['text_fg'] = (r, g, b, a) - # 更新字体大小 - panel_data['content_label']['text_scale'] = size - # 同时更新属性存储 - if 'properties' in panel_data: - panel_data['properties']['content_color'] = (r, g, b, a) - - print(f"✅ 成功设置信息面板内容属性: 颜色=({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小={size}") - return True - else: - # 回退到使用updatePanelProperties方法 - success = info_panel_manager.updatePanelProperties( + # 根据面板类型选择不同的更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + # 3D面板使用update3DPanelProperties + info_panel_manager.update3DPanelProperties( panel_id, - content_color=(r, g, b, a) + content_color=(r, g, b, a), + content_size=size + ) + else: + # 2D面板使用updatePanelProperties + info_panel_manager.updatePanelProperties( + panel_id, + content_color=(r, g, b, a), + content_size=size ) - if success: - print(f"✅ 成功设置信息面板内容颜色: ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") - return True - print("❌ 设置信息面板内容属性失败") - return False + # print(f"✓ 信息面板内容属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") + return True else: - print("❌ 未找到 info_panel_manager") + print("❌ 无法找到 info_panel_manager") return False except Exception as e: - print(f"❌ 应用内容属性失败: {e}") + print(f"❌ 应用信息面板内容属性失败: {e}") return False def _addSphericalVideoProperties(self, spherical_video): @@ -2925,7 +3366,7 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _addVideoScreenProperties(self, video_screen): + def _addVideoScreenProperties(self, video_screen, item): """添加视频屏幕属性面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, @@ -2944,6 +3385,12 @@ class PropertyPanelManager: video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) path_label = QLabel(video_path) path_label.setWordWrap(True) + if len(video_path)>30: + display_path = video_path[:27]+"..." + path_label.setToolTip(video_path) + else: + display_path = video_path + path_label.setText(display_path) path_label.setStyleSheet("color: #00AAFF;") video_info_layout.addWidget(path_label, 0, 1) elif os.path.exists(video_path): @@ -2992,7 +3439,7 @@ class PropertyPanelManager: # 加载新视频按钮 load_btn = QPushButton("📁 加载新视频...") - load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen)) + load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen, item)) self._propertyLayout.addWidget(load_btn) # 添加URL输入区域 @@ -3006,7 +3453,8 @@ class PropertyPanelManager: load_url_btn = QPushButton("加载URL") self._current_load_url_btn_3d = load_url_btn - load_url_btn.clicked.connect(lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip())) + load_url_btn.clicked.connect( + lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip())) url_layout.addWidget(load_url_btn) url_group.setLayout(url_layout) @@ -3015,7 +3463,7 @@ class PropertyPanelManager: except Exception as e: print(f"添加视频屏幕属性失败: {e}") - def _add2DVideoScreenProperties(self, video_screen): + def _add2DVideoScreenProperties(self, video_screen, item): """为2D视频屏幕添加属性控制面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, @@ -3086,7 +3534,7 @@ class PropertyPanelManager: # 加载新视频按钮 load_btn = QPushButton("📁 加载新视频...") - load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen)) + load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen, item)) self._propertyLayout.addWidget(load_btn) # 添加URL输入区域 @@ -3100,7 +3548,8 @@ class PropertyPanelManager: load_url_btn = QPushButton("加载URL") self._current_load_url_btn_2d = load_url_btn - load_url_btn.clicked.connect(lambda: self._loadVideoFromURLWithOpenCV(video_screen, self.url_input.text().strip())) + load_url_btn.clicked.connect( + lambda: self._loadVideoFromURLWithOpenCV(video_screen, self.url_input.text().strip())) url_layout.addWidget(load_url_btn) url_group.setLayout(url_layout) @@ -3159,7 +3608,7 @@ class PropertyPanelManager: QMessageBox.critical(None, "视频流错误", error_msg) print(f"❌ 无法打开视频流: {url}") - #恢复按钮状态 + # 恢复按钮状态 if load_url_btn: load_url_btn.setText("加载URL") load_url_btn.setEnabled(True) @@ -3384,7 +3833,8 @@ class PropertyPanelManager: except Exception as e: print(f"❌ 停止视频失败: {e}") return False - def _loadNew2DVideo(self, video_screen): + + def _loadNew2DVideo(self, video_screen, item): """为2D视频屏幕加载新视频文件""" try: file_path, _ = QFileDialog.getOpenFileName( @@ -3399,60 +3849,12 @@ class PropertyPanelManager: print(f"成功加载新视频: {file_path}") # 刷新属性面板以显示新视频信息 self._stop2DVideo(video_screen) - self.updateGUIPropertyPanel(video_screen) + self.updateGUIPropertyPanel(video_screen, item) return True except Exception as e: print(f"加载新视频失败: {e}") return False - def load2DVideoFile(self, video_screen, video_path): - """为2D视频屏幕加载新的视频文件""" - try: - import os - - # 处理空路径情况 - 显示空白 - if not video_path or video_path == "": - print("ℹ️ 空视频路径,显示空白") - video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 - video_screen.clearPythonTag("movie_texture") - video_screen.setTag("video_path", "") - return True - - # 检查文件是否存在 - if not os.path.exists(video_path): - print(f"❌ 2D视频文件不存在: {video_path}") - # 显示空白而不是红色错误提示 - video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 - return False - - # 加载新的视频纹理 - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - # 应用纹理到2D视频屏幕 - video_screen["frameTexture"] = movie_texture - # 设置白色背景以正确显示视频 - video_screen["frameColor"] = (1, 1, 1, 1) - - # 保存视频纹理引用 - video_screen.setPythonTag("movie_texture", movie_texture) - video_screen.setTag("video_path", video_path) - - print(f"✅ 成功加载新2D视频: {video_path}") - return True - else: - print(f"❌ 无法加载2D视频文件: {video_path}") - # 显示空白而不是红色错误提示 - video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 - return False - - except Exception as e: - print(f"❌ 加载2D视频文件失败: {e}") - import traceback - traceback.print_exc() - # 显示空白而不是红色错误提示 - video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景 - return False - def _loadVideoFromURLWithOpenCV_3D(self, video_screen, url): """使用OpenCV从URL加载视频流并在3D视频屏幕上显示""" try: @@ -3465,7 +3867,7 @@ class PropertyPanelManager: is_first_load = not video_screen.hasTag("video_path") or not video_screen.getTag("video_path") needs_second_call = False - if is_first_load and not hasattr(self,'_first_load_processed'): + if is_first_load and not hasattr(self, '_first_load_processed'): needs_second_call = True self._first_load_processed = True @@ -3619,7 +4021,8 @@ class PropertyPanelManager: print("检测到首次加载,再次调用_loadVideoFromURLWithOpenCV_3D以确保正确显示") from PyQt5.QtCore import QTimer QTimer.singleShot(100, lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, url)) - QTimer.singleShot(200,lambda :setattr(self,'_first_load_processed',False)if hasattr(self,'_first_load_processed')else None) + QTimer.singleShot(200, lambda: setattr(self, '_first_load_processed', False) if hasattr(self, + '_first_load_processed') else None) # 恢复按钮状态 update_button_text("加载URL") @@ -3657,7 +4060,7 @@ class PropertyPanelManager: return False - def _verifyVideoDisplay(self,video_screen,texture): + def _verifyVideoDisplay(self, video_screen, texture): try: applied_texture = video_screen.getTexture() if applied_texture and applied_texture.getName() == texture.getName(): @@ -3670,9 +4073,9 @@ class PropertyPanelManager: print(f"验证视频显示时出错:{e}") return False - def _retryLoadVideoFromURLWithOpenCV_3D(self,video_screen,url): + def _retryLoadVideoFromURLWithOpenCV_3D(self, video_screen, url): print("重新加载3D视频流") - return self._loadVideoFromURLWithOpenCV_3D(video_screen,url) + return self._loadVideoFromURLWithOpenCV_3D(video_screen, url) def _stop3DVideo(self, video_screen): """停止3D视频(内部方法)""" @@ -3709,8 +4112,7 @@ class PropertyPanelManager: print(f"❌ 停止3D视频失败: {e}") return False - - def _loadNewVideo(self,video_screen): + def _loadNewVideo(self, video_screen, item): try: file_path, _ = QFileDialog.getOpenFileName( None, @@ -3719,10 +4121,9 @@ class PropertyPanelManager: "视频文件(*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" ) if file_path: - success = self.world.gui_manager.loadVideoFile(video_screen,file_path) + success = self.world.gui_manager.loadVideoFile(video_screen, file_path) if success: - print(f"成功加载新视频{file_path}") - self.updateGUIPropertyPanel(video_screen) + self.updateGUIPropertyPanel(video_screen, item) return True except Exception as e: print(f"加载新视频失败{e}") @@ -3769,7 +4170,7 @@ class PropertyPanelManager: if value == 0: value = 0.01 - if gui_type in ["3d_text", "3d_image","2d_image","video_screen","2d_video_screen"]: + if gui_type in ["3d_text", "3d_image", "2d_image", "video_screen", "2d_video_screen"]: # 3D元素处理 if axis == "x": new_scale = (value, current_scale.getY(), current_scale.getZ()) @@ -3805,17 +4206,17 @@ class PropertyPanelManager: traceback.print_exc() return False - def _update2DImageWidth(self,gui_element,width): + def _update2DImageWidth(self, gui_element, width): try: current_scale = gui_element.getScale() - width_scaled = width/2 + width_scaled = width / 2 height_scaled = current_scale.getZ() - gui_element.setScale(width_scaled,current_scale.getY(),height_scaled) + gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) - if hasattr(gui_element,'_height_spinbox'): + if hasattr(gui_element, '_height_spinbox'): gui_element._height_spinbox.blockSignals(True) - gui_element._height_spinbox.setValue(height_scaled*2) + gui_element._height_spinbox.setValue(height_scaled * 2) gui_element._height_spinbox.blockSignals(False) print(f"✓ 更新2D图片宽度: {width}") @@ -3843,10 +4244,10 @@ class PropertyPanelManager: except Exception as e: print(f"✗ 更新2D图片高度失败: {e}") - def _update2DImageScale(self,gui_element,scale): + def _update2DImageScale(self, gui_element, scale): try: - scaled_value = scale/2 - gui_element.setScale(scaled_value,0,scaled_value) + scaled_value = scale / 2 + gui_element.setScale(scaled_value, 0, scaled_value) print(f"✓ 更新2D图片缩放: {scale}") except Exception as e: print(f"✗ 更新2D图片缩放失败: {e}") @@ -3870,7 +4271,6 @@ class PropertyPanelManager: except Exception as e: print(f"✗ 更新3D元素轴缩放失败: {e}") - def _selectGUIColor(self, gui_element): """选择GUI元素的字体颜色""" from PyQt5.QtWidgets import QColorDialog @@ -3932,7 +4332,7 @@ class PropertyPanelManager: # gui_element.setColor(*color) # print(f"✓ 更新3D文本字体颜色: {gui_type}") - from panda3d.core import Material,Vec4 + from panda3d.core import Material, Vec4 if not gui_element.hasMaterial(): material = Material(f"text-material-{gui_element.getName()}") material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) @@ -4000,41 +4400,40 @@ class PropertyPanelManager: except Exception as e: print(f"✗ 更新GUI元素Z轴缩放失败: {e}") - def update2DImageTexture(self, gui_element, image_path): + def update2DImageTexture(self, image_element, texture_path): + """更新2D图片纹理 - 修复版""" try: - new_texture = self.world.loader.loadTexture(image_path) - if new_texture: - if hasattr(gui_element,'setTexture'): - gui_element.setTexture(new_texture,1) - else: - from direct.gui.DirectGui import DirectFrame - if isinstance(gui_element,DirectFrame): - gui_element['frameTexture']=None - gui_element['frameTexture']=new_texture - else: - print("❌ 不支持的GUI元素类型,无法更新纹理") - return False - gui_element.setTag("image_path",image_path) + from panda3d.core import TextureStage, TransparencyAttrib - if not gui_element.hasMaterial(): - from panda3d.core import Material,LColor - mat = Material() - mat.setName(f"image-material-{id(gui_element)}") - mat.setBaseColor(LColor(1,1,1,1)) - mat.setDiffuse(LColor(1,1,1,1)) - mat.setAmbient(LColor(0.5,0.5,0.5,1)) - mat.setSpecular(LColor(0.1,0.1,0.1,1.0)) - mat.setShininess(10.0) - gui_element.setMaterial(mat,1) - print(f"✅ 2D图像纹理已更新为: {image_path}") - return True - else: - print(f"❌ 无法加载2D图片纹理: {image_path}") + # 加载纹理 + texture = self.world.loader.loadTexture(texture_path) + if not texture: + print(f"❌ 无法加载纹理: {texture_path}") return False - except Exception as e: - print(f"❌ 更新2D图片纹理时出错: {e}") - return False + # 设置纹理过滤 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 应用纹理到元素 + image_element.setTexture(texture, 1) + + # 设置正确的渲染属性以避免黑色显示 + image_element.setTransparency(TransparencyAttrib.MAlpha) + image_element.setBin("fixed", 20) # 确保在正确层级渲染 + image_element.setDepthWrite(False) + image_element.setDepthTest(False) + image_element.setLightOff() + image_element.setTwoSided(True) + + print(f"✅ 2D图片纹理已更新: {texture_path}") + return True + + except Exception as e: + print(f"❌ 更新2D图片纹理失败: {e}") + import traceback + traceback.print_exc() + return False def _updateScriptPropertyPanel(self, game_object): """更新脚本属性面板""" @@ -4053,7 +4452,7 @@ class PropertyPanelManager: enabled = script_component.enabled # 脚本名称和状态 - scriptLabel = QLabel(f"脚本 {i+1}:") + scriptLabel = QLabel(f"脚本 {i + 1}:") scriptInfo = QLabel(f"{script_name}") scriptInfo.setStyleSheet("color: green; font-weight: bold;" if enabled else "color: gray;") self._propertyLayout.addRow(scriptLabel, scriptInfo) @@ -4254,11 +4653,11 @@ class PropertyPanelManager: light_group.setLayout(light_layout) self._propertyLayout.addWidget(light_group) - def _updateLightPosition(self,light_object,node_path,axis,value): + def _updateLightPosition(self, light_object, node_path, axis, value): current_pos = light_object.pos - if axis=='x': - new_pos = Vec3(value,current_pos.getY(),current_pos.getZ()) + if axis == 'x': + new_pos = Vec3(value, current_pos.getY(), current_pos.getZ()) elif axis == 'y': new_pos = Vec3(current_pos.getX(), value, current_pos.getZ()) else: # z @@ -4269,73 +4668,73 @@ class PropertyPanelManager: # 同步更新场景节点位置(用于显示) node_path.setPos(new_pos) - def _updateLightRotation(self,light_object,node_path,axis,value): + def _updateLightRotation(self, light_object, node_path, axis, value): """更新光源旋转""" from panda3d.core import Vec3 current_hpr = node_path.getHpr() - if axis=='h': - new_hpr = Vec3(value,current_hpr.getY(),current_hpr.getZ()) - elif axis=='p': - new_hpr = Vec3(current_hpr.getX(),value,current_hpr.getZ()) + if axis == 'h': + new_hpr = Vec3(value, current_hpr.getY(), current_hpr.getZ()) + elif axis == 'p': + new_hpr = Vec3(current_hpr.getX(), value, current_hpr.getZ()) else: - new_hpr = Vec3(current_hpr.getX(),current_hpr.getY(),value) + new_hpr = Vec3(current_hpr.getX(), current_hpr.getY(), value) node_path.setHpr(new_hpr) - if hasattr(light_object,'direction'): + if hasattr(light_object, 'direction'): direction_mat = node_path.getMat() - new_direction = direction_mat.xformVec(Vec3(0,1,0)) + new_direction = direction_mat.xformVec(Vec3(0, 1, 0)) light_object.direction = new_direction print(f"光源旋转已更新:{axis}={value}") - def _updateLightEnergy(self,light_object,value): + def _updateLightEnergy(self, light_object, value): """更新光源强度""" light_object.energy = value - def _updateLightRadius(self,light_object,value): + def _updateLightRadius(self, light_object, value): """更新光源半径""" light_object.radius = value - def _updateLightFOV(self,light_Object,value): + def _updateLightFOV(self, light_Object, value): """更新聚光灯视野角度""" - if hasattr(light_Object,'fov'): + if hasattr(light_Object, 'fov'): light_Object.fov = value - def _updateLightTemperature(self,light_object,value): + def _updateLightTemperature(self, light_object, value): """更新光源色温""" light_object.set_color_from_temperature(value) - #保存色温值以便下次显示 - light_object._temperature=value + # 保存色温值以便下次显示 + light_object._temperature = value - def _updateLightInnerRadius(self,light_object,value): + def _updateLightInnerRadius(self, light_object, value): """更新点光源内半径""" - if hasattr(light_object,'inner_radius'): - light_object.inner_radius=value + if hasattr(light_object, 'inner_radius'): + light_object.inner_radius = value - def _updateLightShaowResolution(self,light_object,value): + def _updateLightShaowResolution(self, light_object, value): """更新阴影分辨率""" light_object.shadow_map_resolution = value - def _updateLightNearPlane(self,light_object,value): + def _updateLightNearPlane(self, light_object, value): """更新近平面距离""" light_object.near_plane = value - def _updateLightCastsShadows(self,light_object,casts_shadows): + def _updateLightCastsShadows(self, light_object, casts_shadows): """更新光源是否投射阴影""" light_object.casts_shadows = casts_shadows - def _updateLightScale(self,node_path,axis,value): + def _updateLightScale(self, node_path, axis, value): """更新光源节点缩放""" current_scale = node_path.getScale() - if axis=='x': - new_scale = Vec3(value,current_scale.getY(),current_scale.getZ()) - elif axis=='y': - new_scale = Vec3(current_scale.getX(),value,current_scale.getZ()) + if axis == 'x': + new_scale = Vec3(value, current_scale.getY(), current_scale.getZ()) + elif axis == 'y': + new_scale = Vec3(current_scale.getX(), value, current_scale.getZ()) else: - new_scale = Vec3(current_scale.getX(),current_scale.getY(),value) + new_scale = Vec3(current_scale.getX(), current_scale.getY(), value) node_path.setScale(new_scale) @@ -4363,8 +4762,7 @@ class PropertyPanelManager: return unique_names - - def _updateModelMaterialPanel(self,model): + def _updateModelMaterialPanel(self, model): """模型材质属性""" if model.is_empty(): print("警告: 无法在空的 NodePath 上查找材质") @@ -4403,12 +4801,12 @@ class PropertyPanelManager: if geom_node: geom_node_name = geom_node.getName() unique_name = f"{geom_node_name}({model_name})" - print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") + #print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") else: material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" unique_name = f"{material_name}({model_name})" - print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") + #print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") # 处理重复名称 if unique_name in name_counter: @@ -4447,7 +4845,7 @@ class PropertyPanelManager: # 基础颜色编辑 base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: - print(f"材质基础颜色: {base_color}") + #print(f"材质基础颜色: {base_color}") # 基础颜色标题 color_row = 2 if material_status != "标准PBR材质" else 1 @@ -4630,8 +5028,6 @@ class PropertyPanelManager: # gloss_button.clicked.connect(lambda checked, mat=material: self._selectGlossTexture(mat)) # self._propertyLayout.addRow("光泽贴图:", gloss_button) - - # 在纹理按钮后添加当前贴图信息显示 current_row = self._displayCurrentTextures(material, material_layout, current_row) @@ -4663,7 +5059,7 @@ class PropertyPanelManager: # elif component == 'a': # Alpha分量处理 # self._updateMaterialTransparency(material, value) # return - #new_color = Vec4(current_color.x, current_color.y, current_color.z, value) + # new_color = Vec4(current_color.x, current_color.y, current_color.z, value) else: print(f"未知的颜色分量: {component}") return @@ -4706,18 +5102,18 @@ class PropertyPanelManager: except Exception as e: print(f"更新材质基础颜色失败: {e}") - def _updateMaterialTransparency(self,material,alpha_value): + def _updateMaterialTransparency(self, material, alpha_value): try: from panda3d.core import Vec4 - if hasattr(material,'emission'): - material.emission = Vec4(3,0,0,0) + if hasattr(material, 'emission'): + material.emission = Vec4(3, 0, 0, 0) print("设置透明着色器模型") - if hasattr(material,'shading_model_param0'): + if hasattr(material, 'shading_model_param0'): material.shading_model_param0 = alpha_value print(f"设置透明度参数{alpha_value}") - if hasattr(material,'base_color'): + if hasattr(material, 'base_color'): current_color = material.base_color - material.base_color = Vec4(current_color.x,current_color.y,current_color.z,alpha_value) + material.base_color = Vec4(current_color.x, current_color.y, current_color.z, alpha_value) print(f"更新基础颜色透明度{alpha_value}") print(f"材质透明度已更新:{alpha_value}") except Exception as e: @@ -4781,7 +5177,6 @@ class PropertyPanelManager: print(f"检查材质状态时出错: {e}") return "未知材质类型(可尝试编辑)" - def _getTextureModeString(self, mode): """获取纹理模式的字符串表示""" from panda3d.core import TextureStage @@ -4836,7 +5231,7 @@ class PropertyPanelManager: try: # 方法1: 尝试获取base_color属性 if hasattr(material, 'base_color') and material.base_color is not None: - print(f"✓ 找到base_color属性: {material.base_color}") + #print(f"✓ 找到base_color属性: {material.base_color}") return material.base_color # 方法2: 尝试调用get_base_color方法 @@ -4844,7 +5239,7 @@ class PropertyPanelManager: try: base_color = material.get_base_color() if base_color is not None: - #print(f"✓ 通过get_base_color()获取: {base_color}") + # print(f"✓ 通过get_base_color()获取: {base_color}") return base_color except: pass @@ -4854,7 +5249,7 @@ class PropertyPanelManager: try: diffuse_color = material.getDiffuse() if diffuse_color is not None: - print(f"✓ 从diffuse颜色获取: {diffuse_color}") + #print(f"✓ 从diffuse颜色获取: {diffuse_color}") # 同时设置为base_color if hasattr(material, 'set_base_color'): material.set_base_color(diffuse_color) @@ -4893,164 +5288,162 @@ class PropertyPanelManager: print(f"✗ 获取材质基础颜色失败: {e}") return None - def _selectDiffuseTexture(self,material_title): + def _selectDiffuseTexture(self, material_title): """漫反射贴图""" - from PyQt5.QtWidgets import QFileDialog + from PyQt5.QtWidgets import QFileDialog import os - file_dialog = QFileDialog(None,"选择漫反射贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择漫反射贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyDiffuseTexture(material_title,normalized_path) + self._applyDiffuseTexture(material_title, normalized_path) print(f"已选择漫反射贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectNormalTexture(self,material): + def _selectNormalTexture(self, material): """选择法线贴图""" - from PyQt5.QtWidgets import QFileDialog + from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择法线贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择法线贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyNormalTexture(material,normalized_path) + self._applyNormalTexture(material, normalized_path) print(f"已选择法线贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectRoughnessTexture(self,material): + def _selectRoughnessTexture(self, material): """选择粗糙度贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择粗糙度贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择粗糙度贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyRoughnessTexture_FINAL(material,normalized_path) + self._applyRoughnessTexture_FINAL(material, normalized_path) print(f"已选择粗糙度贴图: {filename} -> 标准化路径:{normalized_path}") - def _selectMetallicTexture(self,material): + def _selectMetallicTexture(self, material): """选择金属性贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择金属性贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择金属性贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyMetallicTexture_NEW(material,normalized_path) + self._applyMetallicTexture_NEW(material, normalized_path) print(f"已选择金属性贴图: {filename} -> 标准化路径:{normalized_path}") - #IOR贴图 - def _selectIORTexture(self,material): + # IOR贴图 + def _selectIORTexture(self, material): """选择IOR贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialong = QFileDialog(None,"选择IOR贴图","","图像(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialong = QFileDialog(None, "选择IOR贴图", "", "图像(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialong.exec_(): filename = file_dialong.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyIORTexture(material,normalized_path) + self._applyIORTexture(material, normalized_path) print(f"已选择IOR贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectParallaxTexture(self,material): + def _selectParallaxTexture(self, material): """选择视差贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择视差贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择视差贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyParallaxTexture(material,normalized_path) + self._applyParallaxTexture(material, normalized_path) print(f"已选择视差贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectEmissionTexture(self,material): + def _selectEmissionTexture(self, material): """选择自发光贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择自发光贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择自发光贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyEmissionTexture(material,normalized_path) + self._applyEmissionTexture(material, normalized_path) print(f"已选择自发光贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectAOTexture(self,material): + def _selectAOTexture(self, material): """选择环境光遮蔽贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择AO贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择AO贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyAOTexture(material,normalized_path) + self._applyAOTexture(material, normalized_path) print(f"已选择AO贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectAlphaTexture(self,material): + def _selectAlphaTexture(self, material): """选择透明度贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择透明度贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择透明度贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyAlphaTexture(material,normalized_path) + self._applyAlphaTexture(material, normalized_path) print(f"已选择透明度贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectDetailTexture(self,material): + def _selectDetailTexture(self, material): """选择细节贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择细节贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择细节贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyDetailTexture(material,normalized_path) + self._applyDetailTexture(material, normalized_path) print(f"已选择细节贴图:{filename} -> 标准化路径:{normalized_path}") - def _selectGlossTexture(self,material): + def _selectGlossTexture(self, material): """选择光泽贴图""" from PyQt5.QtWidgets import QFileDialog - file_dialog = QFileDialog(None,"选择光泽贴图","","图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + file_dialog = QFileDialog(None, "选择光泽贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") if file_dialog.exec_(): filename = file_dialog.selectedFiles()[0] if filename: # 使用跨平台路径标准化 normalized_path = util.normalize_model_path(filename) - self._applyGlossTexture(material,normalized_path) + self._applyGlossTexture(material, normalized_path) print(f"已选择光泽贴图:{filename} -> 标准化路径:{normalized_path}") - - # def _applyDiffuseTexture(self, texture_path): # from panda3d.core import TextureStage # try: @@ -5081,17 +5474,17 @@ class PropertyPanelManager: # print("贴图已直接贴到节点:", node.getName()) # except Exception as e: # print("贴图失败:", e) - def _applyDiffuseTexture(self,material_title,texture_path): + def _applyDiffuseTexture(self, material_title, texture_path): """应用漫反射贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader from panda3d.core import TextureStage - #加载纹理 + # 加载纹理 texture = RPLoader.load_texture(texture_path) if texture: - #获取材质所属的节点 - material,node = self._findMaterialAndNodeByTitle(material_title) + # 获取材质所属的节点 + material, node = self._findMaterialAndNodeByTitle(material_title) if node and material: print(f"正在为节点 {node.getName()} 应用漫反射贴图") @@ -5114,7 +5507,7 @@ class PropertyPanelManager: node, effect_file, { - "normal_mapping": True, # 启用法线映射支持 + "normal_mapping": True, # 启用法线映射支持 "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, @@ -5152,7 +5545,8 @@ class PropertyPanelManager: for i, stage in enumerate(all_stages): tex = node.getTexture(stage) mode_name = self._getTextureModeString(stage.getMode()) - print(f"阶段 {i}: {stage.getName()}, Sort: {stage.getSort()}, 模式: {mode_name}, 纹理: {tex.getName() if tex else 'None'}") + print( + f"阶段 {i}: {stage.getName()}, Sort: {stage.getSort()}, 模式: {mode_name}, 纹理: {tex.getName() if tex else 'None'}") print("==========================================") print(f"漫反射贴图已成功应用:{texture_path}") @@ -5221,7 +5615,7 @@ class PropertyPanelManager: node, effect_file, { - "normal_mapping": True, # 强制启用法线映射 + "normal_mapping": True, # 强制启用法线映射 "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, @@ -5282,7 +5676,6 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _applyRoughnessTexture_FINAL(self, material, texture_path): """应用粗糙度贴图 - 先编译后绑定策略""" try: @@ -5316,14 +5709,12 @@ class PropertyPanelManager: if not has_normal: print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") - #self._applyDefaultNormalTexture(node) - self._applyNormalTexture(material,"RenderPipelineFile/Default_NRM_2K.png") + # self._applyDefaultNormalTexture(node) + self._applyNormalTexture(material, "RenderPipelineFile/Default_NRM_2K.png") print("✅ 默认法线贴图已添加") else: print("✅ 检测到材质已有法线贴图") - - # 5. 检查是否有金属性贴图和透明漫反射贴图,选择合适的PBR效果 print("🔧 步骤2:检查金属性贴图和透明度设置...") has_metallic = self._hasMetallicTexture(node) @@ -5352,19 +5743,19 @@ class PropertyPanelManager: 100 ) print(f"✅ {effect_file} 效果已应用") - #print("✅ 着色器预编译完成") + # print("✅ 着色器预编译完成") # 5. 等待编译完成 - #time.sleep(0.2) # 200ms等待 - #print("⏱️ 等待着色器编译...") + # time.sleep(0.2) # 200ms等待 + # print("⏱️ 等待着色器编译...") # 6. 现在绑定纹理到已编译的着色器 - #print("🔧 步骤2:绑定纹理到编译完成的着色器...") + # print("🔧 步骤2:绑定纹理到编译完成的着色器...") # roughness_stage = TextureStage("roughness_map") # roughness_stage.setSort(3) # p3d_Texture3 # roughness_stage.setMode(TextureStage.MModulate) # node.setTexture(roughness_stage, texture) - #print("✅ 纹理已绑定到预编译着色器") + # print("✅ 纹理已绑定到预编译着色器") print("🧹 清理现有粗糙度贴图...") existing_stages = node.findAllTextureStages() @@ -5381,7 +5772,6 @@ class PropertyPanelManager: roughness_stage.setMode(TextureStage.MModulate) node.setTexture(roughness_stage, texture) - # 7. 验证效果 applied_texture = node.getTexture(roughness_stage) if applied_texture: @@ -5492,7 +5882,7 @@ class PropertyPanelManager: "effects/pbr_with_metallic.yaml", { "normal_mapping": False, # 关闭法线贴图避免干扰 - "render_gbuffer": True, # 必须启用gbuffer渲染 + "render_gbuffer": True, # 必须启用gbuffer渲染 "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, @@ -5576,7 +5966,7 @@ class PropertyPanelManager: print(f"⚠️ 创建白色纹理失败: {e}") return False - def _applyMetallicTexture(self,material,texture_path): + def _applyMetallicTexture(self, material, texture_path): """应用金属性贴图 - Blender风格效果""" try: from RenderPipelineFile.rpcore.loader import RPLoader @@ -5656,7 +6046,7 @@ class PropertyPanelManager: "effects/metallic_only.yaml", { "normal_mapping": False, # 关闭法线贴图避免干扰 - "render_gbuffer": True, # 必须启用gbuffer渲染 + "render_gbuffer": True, # 必须启用gbuffer渲染 "alpha_testing": False, "parallax_mapping": False, "render_shadow": True, @@ -5758,7 +6148,7 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _applyIORTexture(self,material,texture_path): + def _applyIORTexture(self, material, texture_path): """应用IOR贴图到特定材质""" try: from RenderPipelineFile.rpcore.loader import RPLoader @@ -5789,7 +6179,7 @@ class PropertyPanelManager: ior_stage.setSort(2) # 对应p3d_Texture2 ior_stage.setMode(TextureStage.MModulate) - node.setTexture(ior_stage,texture) + node.setTexture(ior_stage, texture) print("IOR贴图已应用到p3d_Texture2槽") # 不再需要手动刷新渲染状态,避免闪烁 @@ -5801,7 +6191,7 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _applyParallaxTexture(self,material,texture_path): + def _applyParallaxTexture(self, material, texture_path): """应用视差贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader @@ -5831,7 +6221,7 @@ class PropertyPanelManager: parallax_stage.setSort(4) # 对应p3d_Texture4 parallax_stage.setMode(TextureStage.MHeight) # 高度贴图模式 - node.setTexture(parallax_stage,texture) + node.setTexture(parallax_stage, texture) print("视差贴图已应用到p3d_Texture4槽") print(f"视差贴图已成功应用:{texture_path}") @@ -5842,16 +6232,16 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _ensureNormalMappingEnabled(self,model): + def _ensureNormalMappingEnabled(self, model): """确保模型启用了法线映射功能""" try: self.world.render_pipeline.set_effect( model, "effects/default.yaml", { - "normal_mapping":True, - "render_gbuffer":True, - "alpha_testing":True + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True }, 30 ) @@ -6040,7 +6430,7 @@ class PropertyPanelManager: { "normal_mapping": True, "render_gbuffer": False, # 透明物体不渲染到GBuffer - "render_forward": True, # 使用前向渲染 + "render_forward": True, # 使用前向渲染 "alpha_testing": True, "parallax_mapping": False, "render_shadow": True, @@ -6119,8 +6509,6 @@ class PropertyPanelManager: except: pass - - def _applyEmissionTexture(self, material, texture_path): """应用自发光贴图""" try: @@ -6438,17 +6826,17 @@ class PropertyPanelManager: """查找使用指定材质的具体几何节点""" from panda3d.core import MaterialAttrib, GeomNode - #print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") + # print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") # 首先尝试查找GeomNode geom_nodes = model.find_all_matches("**/+GeomNode") - #print(f"找到 {len(geom_nodes)} 个几何节点") + # print(f"找到 {len(geom_nodes)} 个几何节点") # 如果没有找到GeomNode,尝试查找所有子节点 if len(geom_nodes) == 0: - #print("未找到GeomNode,尝试查找所有子节点...") + # print("未找到GeomNode,尝试查找所有子节点...") all_nodes = model.find_all_matches("**") - #print(f"找到 {len(all_nodes)} 个子节点") + # print(f"找到 {len(all_nodes)} 个子节点") for node_np in all_nodes: node = node_np.node() @@ -6459,17 +6847,17 @@ class PropertyPanelManager: for geom_np in geom_nodes: geom_node = geom_np.node() geom_count = geom_node.get_num_geoms() - #rint(f"检查几何节点 {geom_node.get_name()}: {geom_count} 个几何体") + # rint(f"检查几何节点 {geom_node.get_name()}: {geom_count} 个几何体") for i in range(geom_count): state = geom_node.get_geom_state(i) if state.has_attrib(MaterialAttrib): material = state.get_attrib(MaterialAttrib).get_material() if material == target_material: - #print(f"找到匹配的几何节点: {geom_np.get_name()}") + # print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np - print("未找到匹配的几何节点") + #print("未找到匹配的几何节点") return None def _findSpecificGeomNodeForMaterial(self, target_material): @@ -6493,14 +6881,14 @@ class PropertyPanelManager: # 使用现有的精确查找方法 geom_node = self._findSpecificGeomNodeWithMaterial(current_model, target_material) if geom_node: - #print(f"✓ 找到特定几何节点: {geom_node.getName()}") + # print(f"✓ 找到特定几何节点: {geom_node.getName()}") # 存储映射以供后续使用 if not hasattr(self, '_material_geom_mapping'): self._material_geom_mapping = {} self._material_geom_mapping[material_id] = geom_node return geom_node else: - #print("⚠️ 未找到特定几何节点,使用模型节点(可能影响所有材质)") + # print("⚠️ 未找到特定几何节点,使用模型节点(可能影响所有材质)") return current_model print("❌ 未找到当前选中的模型") @@ -6510,23 +6898,23 @@ class PropertyPanelManager: """查找使用指定材质的具体几何节点""" from panda3d.core import MaterialAttrib - #print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") + # print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") # 遍历模型下的所有几何节点 geom_nodes = model.find_all_matches("**/+GeomNode") - #print(f"找到 {len(geom_nodes)} 个几何节点") + # print(f"找到 {len(geom_nodes)} 个几何节点") for geom_np in geom_nodes: geom_node = geom_np.node() geom_count = geom_node.get_num_geoms() - #print(f"几何节点 {geom_node.get_name()}: {geom_count} 个几何体") + # print(f"几何节点 {geom_node.get_name()}: {geom_count} 个几何体") for i in range(geom_count): state = geom_node.get_geom_state(i) if state.has_attrib(MaterialAttrib): material = state.get_attrib(MaterialAttrib).get_material() if material == target_material: - #print(f"找到匹配的几何节点: {geom_np.get_name()}") + # print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np else: print(f"几何体 {i} 没有材质属性") @@ -6593,7 +6981,7 @@ class PropertyPanelManager: shading_title = QLabel("着色模型") shading_title.setStyleSheet("font-weight:bold;") - material_layout.addWidget(shading_title,current_row, 0, 1, 4) + material_layout.addWidget(shading_title, current_row, 0, 1, 4) current_row += 1 material_layout.addWidget(QLabel("着色模型:"), current_row, 0) @@ -6669,7 +7057,7 @@ class PropertyPanelManager: self._updateMaterialAlphaForTransparency(material, default_opacity) # 应用透明渲染效果 - #self._applyTransparentRenderingEffect() + # self._applyTransparentRenderingEffect() print(f"透明着色模型设置完成") print(f" - emission.x = {model_index} (透明着色模型)") @@ -6684,13 +7072,14 @@ class PropertyPanelManager: if model_index in [1, 3]: # 自发光或透明模式 self._refreshMaterialUI() - print(f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})") + print( + f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})") def _addTransparencyPanel(self, material, material_layout, current_row): """添加透明度控制面板""" transparency_title = QLabel("透明度属性") transparency_title.setStyleSheet("color: #00BFFF; font-weight:bold;") - material_layout.addWidget(transparency_title,current_row, 0, 1, 4) + material_layout.addWidget(transparency_title, current_row, 0, 1, 4) current_row += 1 # 不透明度滑块(避免混淆,使用不透明度) @@ -6754,11 +7143,11 @@ class PropertyPanelManager: alpha = opacity_slider # 反转 color = self._getOrCreateMaterialBaseColor(material) or Vec4(1, 1, 1, 1) - material.base_color=Vec4(color.x, color.y, color.z, alpha) - material.base_color=Vec4(color.x, color.y, color.z, alpha) + material.base_color = Vec4(color.x, color.y, color.z, alpha) + material.base_color = Vec4(color.x, color.y, color.z, alpha) em = material.emission or Vec4(0, 0, 0, 0) - material.set_emission(Vec4(3.0,alpha,em.z,em.w)) + material.set_emission(Vec4(3.0, alpha, em.z, em.w)) self.world.render_pipeline.set_effect( model, @@ -6769,7 +7158,6 @@ class PropertyPanelManager: self.world.render_pipeline.prepare_scene(model) print(f"[透明] 不透明度={opacity_slider:.2f} 已同步") - def _applyTransparentRenderingEffect(self): from panda3d.core import TransparencyAttrib """为当前选中的模型应用透明渲染效果(简化版本)""" @@ -6784,7 +7172,7 @@ class PropertyPanelManager: model, "effects/default.yaml", { - "render_gbuffer":True, + "render_gbuffer": True, "alpha_testing": False, "normal_mapping": True, "render_shadow": True, @@ -6793,7 +7181,6 @@ class PropertyPanelManager: sort=100 ) - # 让RenderPipeline自动处理透明材质 # 当emission.x=3时,RenderPipeline会自动设置正确的渲染参数 self.world.render_pipeline.prepare_scene(model) @@ -6921,9 +7308,9 @@ class PropertyPanelManager: if hasattr(material, 'base_color') and material.base_color is not None: try: base_color_match = ( - abs(material.base_color.x - preset_values["base_color"][0]) < tolerance and - abs(material.base_color.y - preset_values["base_color"][1]) < tolerance and - abs(material.base_color.z - preset_values["base_color"][2]) < tolerance + abs(material.base_color.x - preset_values["base_color"][0]) < tolerance and + abs(material.base_color.y - preset_values["base_color"][1]) < tolerance and + abs(material.base_color.z - preset_values["base_color"][2]) < tolerance ) except (AttributeError, TypeError): base_color_match = False @@ -6961,7 +7348,8 @@ class PropertyPanelManager: presets = { "塑料": {"base_color": Vec4(0.8, 0.8, 0.8, 1.0), "roughness": 0.7, "metallic": 0.0, "ior": 1.4}, "金属": {"base_color": Vec4(0.7, 0.7, 0.7, 1.0), "roughness": 0.1, "metallic": 1.0, "ior": 1.5}, - "玻璃": {"base_color": Vec4(0.9, 0.9, 1.0, 0.2), "roughness": 0.0, "metallic": 0.0, "ior": 1.5,"shading_model":3,"transparency":0.2}, + "玻璃": {"base_color": Vec4(0.9, 0.9, 1.0, 0.2), "roughness": 0.0, "metallic": 0.0, "ior": 1.5, + "shading_model": 3, "transparency": 0.2}, "橡胶": {"base_color": Vec4(0.2, 0.2, 0.2, 1.0), "roughness": 0.9, "metallic": 0.0, "ior": 1.3}, "木材": {"base_color": Vec4(0.6, 0.4, 0.2, 1.0), "roughness": 0.8, "metallic": 0.0, "ior": 1.3}, "陶瓷": {"base_color": Vec4(0.9, 0.9, 0.85, 1.0), "roughness": 0.1, "metallic": 0.0, "ior": 1.6}, @@ -6990,16 +7378,16 @@ class PropertyPanelManager: material.set_refractive_index(preset["ior"]) if "shading_model" in preset: - emission = Vec4(float (preset["shading_model"]),0,0,0) + emission = Vec4(float(preset["shading_model"]), 0, 0, 0) if "transparency" in preset: emission.y = preset["transparency"] material.set_emission(emission) - #关键:为透明材质应用正确的渲染效果 - if preset["shading_model"]==3: + # 关键:为透明材质应用正确的渲染效果 + if preset["shading_model"] == 3: self._apply_transparent_effect() - #material._applied_preset = preset_name + # material._applied_preset = preset_name self._refreshMaterialUI() print(f"已应用材质预设: {preset_name}") @@ -7096,8 +7484,8 @@ class PropertyPanelManager: if not has_normal: print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") - #self._applyDefaultNormalTexture(node) - self._applyNormalTexture(material,"RenderPipelineFile/Default_NRM_2K.png") + # self._applyDefaultNormalTexture(node) + self._applyNormalTexture(material, "RenderPipelineFile/Default_NRM_2K.png") print("✅ 默认法线贴图已添加") else: print("✅ 检测到材质已有法线贴图") @@ -7146,7 +7534,7 @@ class PropertyPanelManager: node, "effects/pbr_with_metallic.yaml", { - #"normal_mapping": has_normal, + # "normal_mapping": has_normal, "normal_mapping": True, "render_gbuffer": True, "alpha_testing": False, @@ -7166,7 +7554,7 @@ class PropertyPanelManager: # 多次重新绑定,模拟手动"应用两次"的效果 for i in range(3): - print(f" 第{i+1}次绑定...") + print(f" 第{i + 1}次绑定...") # 等待更长时间确保着色器编译完成 time.sleep(0.05) # 50ms延迟 @@ -7181,9 +7569,9 @@ class PropertyPanelManager: # 验证绑定 applied_texture = node.getTexture(metallic_stage) if applied_texture: - print(f" ✅ 第{i+1}次绑定成功") + print(f" ✅ 第{i + 1}次绑定成功") else: - print(f" ❌ 第{i+1}次绑定失败") + print(f" ❌ 第{i + 1}次绑定失败") # 最终验证 final_texture = node.getTexture(metallic_stage) @@ -7329,7 +7717,6 @@ class PropertyPanelManager: sun_group.setLayout(sun_layout) self._propertyLayout.addWidget(sun_group) - def _onSunAzimuthSliderChanged(self, value): """滑块值改变时的回调""" try: @@ -7390,10 +7777,10 @@ class PropertyPanelManager: """设置太阳预设位置""" try: presets = { - "sunrise": (90, 45), # 东方,低角度 - "noon": (180, 90), # 南方,天顶 - "sunset": (270, 45), # 西方,低角度 - "midnight": (0, 0) # 北方,地平线 + "sunrise": (90, 45), # 东方,低角度 + "noon": (180, 90), # 南方,天顶 + "sunset": (270, 45), # 西方,低角度 + "midnight": (0, 0) # 北方,地平线 } if preset_name in presets: @@ -7666,8 +8053,8 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _buildSkeletalUI(self,origin_model,actor,layout): - from PyQt5.QtWidgets import QLabel,QComboBox,QHBoxLayout,QWidget,QPushButton,QDoubleSpinBox + def _buildSkeletalUI(self, origin_model, actor, layout): + from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox actor.hide() origin_model.show() @@ -7711,24 +8098,24 @@ class PropertyPanelManager: btn_box = QWidget() btn_lay = QHBoxLayout(btn_box) - for txt,slot in (("播放",self._playAnimation), - ("暂停",self._pauseAnimation), - ("停止",self._stopAnimation), - ("循环",self._loopAnimation)): + for txt, slot in (("播放", self._playAnimation), + ("暂停", self._pauseAnimation), + ("停止", self._stopAnimation), + ("循环", self._loopAnimation)): btn = QPushButton(txt) - btn.clicked.connect(lambda _,f=slot:f(origin_model)) + btn.clicked.connect(lambda _, f=slot: f(origin_model)) btn_lay.addWidget(btn) layout.addWidget(QLabel("控制:"), current_row, 0) layout.addWidget(btn_box, current_row, 1, 1, 3) current_row += 1 self.speed_spinbox = QDoubleSpinBox() - self.speed_spinbox.setRange(0.1,5.0) + self.speed_spinbox.setRange(0.1, 5.0) self.speed_spinbox.setSingleStep(0.1) saved = origin_model.getPythonTag("anim_speed") self.speed_spinbox.setValue(saved if saved is not None else 1.0) - #self.speed_spinbox.setValue(1.0) - self.speed_spinbox.valueChanged.connect(lambda v:self._setAnimationSpeed(origin_model,v)) + # self.speed_spinbox.setValue(1.0) + self.speed_spinbox.valueChanged.connect(lambda v: self._setAnimationSpeed(origin_model, v)) layout.addWidget(QLabel("播放速度:"), current_row, 0) layout.addWidget(self.speed_spinbox, current_row, 1) @@ -7755,7 +8142,7 @@ class PropertyPanelManager: format_info = self._getModelFormat(origin_model) processed = [] - print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + #print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") for name in anim_names: display_name = name @@ -7789,7 +8176,7 @@ class PropertyPanelManager: display_name = name processed.append((display_name, original_name)) - print(f"[动画分析] {original_name} → {display_name}") + #print(f"[动画分析] {original_name} → {display_name}") return processed @@ -7823,7 +8210,7 @@ class PropertyPanelManager: if frames > 1: valid_anims += 1 total_frames += frames - print(f"[动画分析] '{anim_name}': {frames} 帧") + #print(f"[动画分析] '{anim_name}': {frames} 帧") else: print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") except Exception as e: @@ -7841,12 +8228,12 @@ class PropertyPanelManager: print(f"[动画分析] 分析失败: {e}") return "分析失败" - def _getActor(self,origin_model): + def _getActor(self, origin_model): if origin_model in self._actor_cache: return self._actor_cache[origin_model] filepath = origin_model.getTag("model_path") if not filepath: - return None + return None print(f"[Actor加载] 尝试加载: {filepath}") @@ -7856,17 +8243,19 @@ class PropertyPanelManager: # 其他格式使用标准 Actor 加载 try: - test_actor=Actor(filepath) + import gltf + print(f"[GLTF加载] 尝试加载: {filepath}") + # test_actor=Actor(NodePath(gltf._loader.GltfLoader.load_file(filepath,None))) + test_actor = Actor(NodePath(gltf.load_model(filepath, None))) anims = test_actor.getAnimNames() + test_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = test_actor print(f"[Actor加载] 标准加载检测到动画: {anims}") if not anims: test_actor.cleanup() test_actor.removeNode() return None - actor = Actor(filepath) - actor.reparentTo(self.world.render) - self._actor_cache[origin_model] = actor - return actor + return test_actor except Exception as e: print(f"创建Actor失败: {e}") return None @@ -8085,7 +8474,7 @@ bpy.ops.export_scene.gltf(filepath="{egg_path.replace('.egg', '.gltf')}", export # 显示成功消息 QMessageBox.information(None, "转换成功", - f"FBX动画转换成功!\n请重新选择模型查看动画。") + f"FBX动画转换成功!\n请重新选择模型查看动画。") print(f"[手动转换] 转换完成: {converted_path}") @@ -8101,10 +8490,10 @@ bpy.ops.export_scene.gltf(filepath="{egg_path.replace('.egg', '.gltf')}", export - 打开 Blender - 导入 FBX 文件 - 导出为 glTF (.gltf) 格式,确保选择"包含动画" - + 2. 使用命令行工具: - gltf2bam your_file.gltf your_file.bam - + 3. 检查原始 FBX 文件: - 确保 FBX 文件确实包含动画数据 - 尝试在其他软件中验证动画 @@ -8136,7 +8525,7 @@ bpy.ops.export_scene.gltf(filepath="{egg_path.replace('.egg', '.gltf')}", export try: # 首先尝试看看是否有直接的 FBX 支持 result = subprocess.run(['gltf2bam', '--help'], - capture_output=True, text=True, timeout=10) + capture_output=True, text=True, timeout=10) print(f"[系统转换] gltf2bam 可用") except: print(f"[系统转换] gltf2bam 不可用") @@ -8161,7 +8550,7 @@ bpy.ops.object.delete(use_global=False) try: bpy.ops.import_scene.fbx(filepath="{fbx_path}") print("FBX导入成功") - + # 导出为 glTF bpy.ops.export_scene.gltf( filepath="{gltf_path}", @@ -8170,7 +8559,7 @@ try: export_frame_range=True ) print("glTF导出成功") - + except Exception as e: print(f"转换失败: {{e}}") sys.exit(1) @@ -8200,7 +8589,7 @@ except Exception as e: # 转换 glTF 为 BAM result2 = subprocess.run(['gltf2bam', gltf_path, bam_path], - capture_output=True, text=True, timeout=60) + capture_output=True, text=True, timeout=60) if result2.returncode == 0 and os.path.exists(bam_path): print(f"[系统转换] 成功转换为: {bam_path}") @@ -8222,8 +8611,8 @@ except Exception as e: print(f"[系统转换] 系统转换失败: {e}") return None - def _playAnimation(self,origin_model): - actor=self._getActor(origin_model) + def _playAnimation(self, origin_model): + actor = self._getActor(origin_model) if not actor: return actor.setPos(origin_model.getPos()) @@ -8232,7 +8621,7 @@ except Exception as e: origin_model.hide() actor.show() - if hasattr(self,'animation_combo'): + if hasattr(self, 'animation_combo'): # 获取原始动画名称(存储在 userData 中) current_index = self.animation_combo.currentIndex() if current_index >= 0: @@ -8246,8 +8635,7 @@ except Exception as e: actor.play(display_name) print(f"『动画播放』:{display_name}") - - def _pauseAnimation(self,origin_model): + def _pauseAnimation(self, origin_model): actor = self._getActor(origin_model) if not actor: return @@ -8260,7 +8648,7 @@ except Exception as e: actor.stop() print("『动画』暂停") - def _stopAnimation(self,origin_model): + def _stopAnimation(self, origin_model): actor = self._getActor(origin_model) if not actor: return @@ -8279,8 +8667,7 @@ except Exception as e: origin_model.show() print("『动画』停止切换至原始模型") - - def _loopAnimation(self,origin_model): + def _loopAnimation(self, origin_model): actor = self._getActor(origin_model) if not actor: return @@ -8320,21 +8707,21 @@ except Exception as e: if anim_name: actor.setPlayRate(speed, anim_name) - origin_model.setPythonTag("anim_speed",speed) + origin_model.setPythonTag("anim_speed", speed) print(f"[动画] 速度设为: {speed} ({display_name})") - def _dispatchAnimCommand(self,origin_model,cmd): + def _dispatchAnimCommand(self, origin_model, cmd): cache = self._actor_cache.get(origin_model) if not cache: return - kind,player = cache + kind, player = cache if kind == "actor": - actor=player + actor = player anim_name = self.animation_combo.currentText() actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale()) + actor.setScale(origin_model.getScale() / 100) if cmd == "play": origin_model.hide() @@ -8348,12 +8735,13 @@ except Exception as e: actor.stop() if anim_name and actor.getAnimControl(anim_name): actor.getAnimControl(anim_name).pose(0) - actor.hide();origin_model.show() + actor.hide(); + origin_model.show() elif cmd == "loop": origin_model.hide() actor.show() actor.loop(anim_name) - elif isinstance(cmd,tuple) and cmd[0] == "speed": + elif isinstance(cmd, tuple) and cmd[0] == "speed": actor.setPlayRate(cmd[1], anim_name) def removeActorForModel(self, model): @@ -8362,4 +8750,1337 @@ except Exception as e: if actor: actor.stop() actor.cleanup() - actor.removeNode() \ No newline at end of file + actor.removeNode() + + def _addCollisionPanel(self, model): + """添加碰撞检测面板""" + try: + # 创建碰撞检测组 + collision_group = QGroupBox("碰撞检测") + collision_layout = QGridLayout() + + # 检查模型是否已有碰撞 + has_collision = self._hasCollision(model) + + # 碰撞状态标签 + status_label = QLabel("状态:") + collision_layout.addWidget(status_label, 0, 0) + + # 状态文本(需要保存引用以便更新) + self.collision_status_text = QLabel("已启用" if has_collision else "未启用") + self.collision_status_text.setStyleSheet("color: green;" if has_collision else "color: red;") + collision_layout.addWidget(self.collision_status_text, 0, 1) + + # 形状选择标签(始终显示) + self.collision_shape_label = QLabel("碰撞形状:") + collision_layout.addWidget(self.collision_shape_label, 1, 0) + + # 形状选择下拉框(始终显示) + self.collision_shape_combo = QComboBox() + self.collision_shape_combo.addItems([ + "球形 (Sphere)", + "盒型 (Box)", + "胶囊体 (Capsule)", + "平面 (Plane)", + "自动选择 (Auto)" + ]) + collision_layout.addWidget(self.collision_shape_combo, 1, 1) + + # 保存布局引用,用于动态添加/移除控件 + self.collision_layout = collision_layout + self.collision_group = collision_group + + current_row = 2 # 下一行的索引 + + # 显示/隐藏切换按钮(只有有碰撞时才显示) + if has_collision: + # 检查碰撞的当前可见性 + is_collision_visible = self._isCollisionVisible(model) + + # 显示当前碰撞类型并设置为只读 + current_shape = self._getCurrentCollisionShape(model) + self._setComboToShape(current_shape) + self.collision_shape_combo.setEnabled(False) + + # 添加碰撞参数调整控件 + current_row = self._addCollisionParameterControls(model, collision_layout, current_row, current_shape) + + # 显示/隐藏切换按钮 + self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") + self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) + collision_layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 2) + current_row += 1 + + # 移除碰撞按钮 + self.collision_button = QPushButton("移除碰撞") + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 2) + else: + # 如果没有碰撞,设置默认选择并允许编辑 + self.collision_shape_combo.setCurrentText("球形 (Sphere)") + self.collision_shape_combo.setEnabled(True) + + # 清理之前的参数控件 + self._clearCollisionParameterControls() + + # 隐藏显示/隐藏按钮(如果存在) + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + + # 添加碰撞按钮 + self.collision_button = QPushButton("添加碰撞") + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 2) + collision_group.setLayout(collision_layout) + self._propertyLayout.addWidget(collision_group) + + except Exception as e: + print(f"创建碰撞面板失败: {e}") + import traceback + traceback.print_exc() + + def _addCollisionParameterControls(self, model, layout, start_row, shape_type): + """添加碰撞参数调整控件""" + try: + current_row = start_row + + # 位置调整控件(所有类型都有) + pos_label = QLabel("位置偏移:") + layout.addWidget(pos_label, current_row, 0) + current_row += 1 + + # X, Y, Z 位置调整 + self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置默认位置偏移(无偏移) + self.collision_pos_x.setValue(0.0) + self.collision_pos_y.setValue(0.0) + self.collision_pos_z.setValue(0.0) + + layout.addWidget(QLabel("X:"), current_row, 0) + layout.addWidget(self.collision_pos_x, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Y:"), current_row, 0) + layout.addWidget(self.collision_pos_y, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Z:"), current_row, 0) + layout.addWidget(self.collision_pos_z, current_row, 1) + current_row += 1 + + # 连接位置变化信号 + self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) + self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) + self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) + + # 根据形状类型添加特定参数 + if shape_type == 'sphere': + current_row = self._addSphereParameters(model, layout, current_row) + elif shape_type == 'box': + current_row = self._addBoxParameters(model, layout, current_row) + elif shape_type == 'capsule': + current_row = self._addCapsuleParameters(model, layout, current_row) + elif shape_type == 'plane': + current_row = self._addPlaneParameters(model, layout, current_row) + + # 获取并设置当前参数值 + self._loadCurrentCollisionParameters(model, shape_type) + + return current_row + + except Exception as e: + print(f"添加碰撞参数控件失败: {e}") + return start_row + + def _createCollisionSpinBox(self, min_val, max_val, decimals=2): + """创建碰撞参数调整用的SpinBox""" + spinbox = QDoubleSpinBox() + spinbox.setRange(min_val, max_val) + spinbox.setDecimals(decimals) + spinbox.setSingleStep(0.01) + return spinbox + + def _addSphereParameters(self, model, layout, start_row): + """添加球形碰撞参数""" + current_row = start_row + + # 半径调整 + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + + self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后尺寸的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + default_radius = transformed_info['radius'] + self.collision_radius.setValue(default_radius) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + default_radius = bounds.getRadius() + self.collision_radius.setValue(default_radius) + + self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) + layout.addWidget(self.collision_radius, current_row, 1) + current_row += 1 + + return current_row + + def _addBoxParameters(self, model, layout, start_row): + """添加盒型碰撞参数""" + current_row = start_row + + size_label = QLabel("尺寸:") + layout.addWidget(size_label, current_row, 0) + current_row += 1 + + # 宽度、长度、高度 + self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2) + self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2) + self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后尺寸的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + actual_size = transformed_info['size'] + self.collision_width.setValue(actual_size.x) + self.collision_length.setValue(actual_size.y) + self.collision_height.setValue(actual_size.z) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + model_size = bounds.getMax() - bounds.getMin() + self.collision_width.setValue(model_size.x) + self.collision_length.setValue(model_size.y) + self.collision_height.setValue(model_size.z) + + layout.addWidget(QLabel("宽度:"), current_row, 0) + layout.addWidget(self.collision_width, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("长度:"), current_row, 0) + layout.addWidget(self.collision_length, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("高度:"), current_row, 0) + layout.addWidget(self.collision_height, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) + self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) + self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) + + return current_row + + def _addCapsuleParameters(self, model, layout, start_row): + """添加胶囊体碰撞参数""" + current_row = start_row + + # 半径和高度 + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + + self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后尺寸的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + actual_size = transformed_info['size'] + # 更合理的默认半径:基于变换后模型宽度的平均值 + default_radius = min(actual_size.x, actual_size.y) / 2.5 + self.collision_capsule_radius.setValue(default_radius) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + model_size = bounds.getMax() - bounds.getMin() + # 更合理的默认半径:基于模型宽度的平均值 + default_radius = min(model_size.x, model_size.y) / 2.5 + self.collision_capsule_radius.setValue(default_radius) + + self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) + layout.addWidget(self.collision_capsule_radius, current_row, 1) + current_row += 1 + + height_label = QLabel("高度:") + layout.addWidget(height_label, current_row, 0) + + self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后高度的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + actual_size = transformed_info['size'] + self.collision_capsule_height.setValue(actual_size.z) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + model_size = bounds.getMax() - bounds.getMin() + self.collision_capsule_height.setValue(model_size.z) + + self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) + layout.addWidget(self.collision_capsule_height, current_row, 1) + current_row += 1 + + return current_row + + def _addPlaneParameters(self, model, layout, start_row): + """添加平面碰撞参数""" + current_row = start_row + + # 法向量 + normal_label = QLabel("法向量:") + layout.addWidget(normal_label, current_row, 0) + current_row += 1 + + self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置默认法向量(向上) + self.collision_normal_x.setValue(0.0) + self.collision_normal_y.setValue(0.0) + self.collision_normal_z.setValue(1.0) + + layout.addWidget(QLabel("Nx:"), current_row, 0) + layout.addWidget(self.collision_normal_x, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Ny:"), current_row, 0) + layout.addWidget(self.collision_normal_y, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Nz:"), current_row, 0) + layout.addWidget(self.collision_normal_z, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) + self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) + self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) + + return current_row + + def _hasCollision(self, model): + """检查模型是否已有碰撞体""" + try: + from panda3d.core import CollisionNode + + # 检查模型及其子节点是否有碰撞节点 + collision_nodes = model.findAllMatches("**/+CollisionNode") + has_collision = collision_nodes.getNumPaths() > 0 + + print( + f"碰撞检查:模型 {model.getName()} - {'有' if has_collision else '无'}碰撞 (找到{collision_nodes.getNumPaths()}个碰撞节点)") + + return has_collision + except Exception as e: + print(f"检查碰撞失败: {e}") + return False + + def _isCollisionVisible(self, model): + """检查碰撞是否可见""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + # 检查碰撞节点是否隐藏 + return not collision_np.isHidden() + return False + except Exception as e: + print(f"检查碰撞可见性失败: {e}") + return False + + def _toggleCollisionVisibility(self, model): + """切换碰撞可见性""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + is_visible = self._isCollisionVisible(model) + + for collision_np in collision_nodes: + if is_visible: + collision_np.hide() + print(f"隐藏碰撞:{model.getName()}") + else: + collision_np.show() + print(f"显示碰撞:{model.getName()}") + + # 立即更新按钮状态 + self._updateCollisionVisibilityButton(model) + + except Exception as e: + print(f"切换碰撞可见性失败: {e}") + + def _updateCollisionVisibilityButton(self, model): + """更新碰撞可见性按钮状态""" + try: + if hasattr(self, 'collision_visibility_button'): + is_visible = self._isCollisionVisible(model) + self.collision_visibility_button.setText("隐藏碰撞" if is_visible else "显示碰撞") + print(f"更新可见性按钮:{model.getName()} - {'可见' if is_visible else '隐藏'}") + except Exception as e: + print(f"更新碰撞可见性按钮失败: {e}") + + def _getCurrentCollisionShape(self, model): + """获取当前模型的碰撞形状类型""" + try: + from panda3d.core import CollisionNode, CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane + + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_node = collision_np.node() + if collision_node.getNumSolids() > 0: + solid = collision_node.getSolid(0) + solid_type = type(solid).__name__ + + if solid_type == "CollisionSphere": + return "sphere" + elif solid_type == "CollisionBox": + return "box" + elif solid_type == "CollisionCapsule": + return "capsule" + elif solid_type == "CollisionPlane": + return "plane" + + return "sphere" # 默认返回球形 + except Exception as e: + print(f"获取碰撞形状失败: {e}") + return "sphere" + + def _setComboToShape(self, shape_type): + """根据形状类型设置下拉框选择""" + shape_map = { + "sphere": "球形 (Sphere)", + "box": "盒型 (Box)", + "capsule": "胶囊体 (Capsule)", + "plane": "平面 (Plane)", + "auto": "自动选择 (Auto)" + } + + if shape_type in shape_map: + self.collision_shape_combo.setCurrentText(shape_map[shape_type]) + else: + self.collision_shape_combo.setCurrentText("球形 (Sphere)") + + def _getSelectedCollisionShape(self): + """获取选中的碰撞形状类型""" + if hasattr(self, 'collision_shape_combo'): + shape_text = self.collision_shape_combo.currentText() + # 从显示文本中提取形状类型 + if "球形" in shape_text: + return 'sphere' + elif "盒型" in shape_text: + return 'box' + elif "胶囊体" in shape_text: + return 'capsule' + elif "平面" in shape_text: + return 'plane' + elif "自动选择" in shape_text: + return 'auto' + return 'sphere' # 默认返回球形 + + def _addCollisionAndUpdate(self, model): + """添加指定形状的碰撞体并更新界面""" + try: + # 防止重复调用 + if getattr(self, '_adding_collision', False): + print("正在添加碰撞,跳过重复调用") + return + + self._adding_collision = True + + # 初始化加载参数标志位 + if not hasattr(self, '_loading_collision_params'): + self._loading_collision_params = False + + if hasattr(self.world, 'scene_manager'): + # 获取选中的碰撞形状 + shape_type = self._getSelectedCollisionShape() + + # 参考scene_manager的setupCollision方法 + from panda3d.core import CollisionNode, BitMask32 + + # 创建碰撞节点 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + + # 设置碰撞掩码 + cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 + + # 如果启用了模型间碰撞检测,添加额外的掩码 + if (hasattr(self.world, 'collision_manager') and + hasattr(self.world.collision_manager, 'model_collision_enabled') and + self.world.collision_manager.model_collision_enabled): + current_mask = cNode.getIntoCollideMask() + model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION + cNode.setIntoCollideMask(current_mask | model_collision_mask) + + # 创建指定形状的碰撞体 + if hasattr(self.world, 'collision_manager'): + collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type) + else: + # 回退方案:创建简单球体 + from panda3d.core import CollisionSphere, Point3 + bounds = model.getBounds() + if bounds.isEmpty(): + collision_shape = CollisionSphere(Point3(0, 0, 0), 1.0) + else: + center = bounds.getCenter() + radius = bounds.getRadius() + if radius <= 0: + radius = 1.0 + collision_shape = CollisionSphere(center, radius) + + cNode.addSolid(collision_shape) + + # 将碰撞节点附加到模型上 + cNodePath = model.attachNewNode(cNode) + + # 根据调试设置决定是否显示碰撞体 + if hasattr(self.world, 'debug_collision') and self.world.debug_collision: + cNodePath.show() + else: + cNodePath.hide() + + # 为模型添加碰撞相关标签 + model.setTag("has_collision", "true") + model.setTag("collision_shape", shape_type) + if 'radius' in locals(): + model.setTag("collision_radius", str(radius)) + + print(f"✅ 为模型 {model.getName()} 添加了 {shape_type} 碰撞体") + + # 重置更新标志,确保状态能够更新 + self._updating_collision_panel = False + + # 更新面板状态 - 添加参数控件和显示/隐藏按钮 + self._updateCollisionPanelState(model) + + else: + print("场景管理器未初始化") + + except Exception as e: + print(f"添加碰撞失败: {e}") + import traceback + traceback.print_exc() + finally: + # 确保标志被重置 + self._adding_collision = False + + def _removeCollisionAndUpdate(self, model): + """移除模型的碰撞体并更新界面""" + try: + # 查找并移除碰撞节点 + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_np.removeNode() + + print(f"移除了模型 {model.getName()} 的碰撞体") + + # 重置状态并更新界面 + self._previous_collision_state = True # 强制刷新 + self._updating_collision_panel = False # 确保状态能够更新 + self._updateCollisionPanelState(model) + + except Exception as e: + print(f"移除碰撞失败: {e}") + + def _updateCollisionPanelState(self, model): + """更新碰撞面板状态""" + try: + # 防止重复调用 + if getattr(self, '_updating_collision_panel', False): + return + + self._updating_collision_panel = True + + if hasattr(self, 'collision_button') and hasattr(self, 'collision_status_text') and hasattr(self, + 'collision_shape_combo'): + has_collision = self._hasCollision(model) + + # 更新状态文本和颜色 + self.collision_status_text.setText("已启用" if has_collision else "未启用") + self.collision_status_text.setStyleSheet("color: green;" if has_collision else "color: red;") + + if has_collision: + # 有碰撞:显示移除按钮,下拉框变为只读并显示当前类型 + self.collision_button.setText("移除碰撞") + + # 先断开所有连接,再重新连接 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + + # 获取并显示当前碰撞类型,设置为只读 + current_shape = self._getCurrentCollisionShape(model) + self._setComboToShape(current_shape) + self.collision_shape_combo.setEnabled(False) + + # 确保参数控件存在 - 只在没有参数控件时才添加 + if not hasattr(self, 'collision_pos_x'): + print("添加碰撞参数控件") + self._addParameterControlsToExistingPanel(model, current_shape) + + # 显示/隐藏按钮状态更新 + if not hasattr(self, 'collision_visibility_button'): + # 创建可见性按钮 + self._addVisibilityButtonToExistingPanel(model) + else: + self.collision_visibility_button.setVisible(True) + self._updateCollisionVisibilityButton(model) + + print(f"碰撞面板状态更新:有碰撞 - {current_shape}") + + else: + # 无碰撞:显示添加按钮,下拉框变为可编辑 + self.collision_button.setText("添加碰撞") + + # 先断开所有连接,再重新连接 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + + # 恢复为可编辑状态 + self.collision_shape_combo.setEnabled(True) + + # 隐藏并清理参数控件 - 只在有参数控件时才清理 + if hasattr(self, 'collision_pos_x'): + print("清理碰撞参数控件") + self._hideCollisionParameterControls() + + # 隐藏显示/隐藏按钮 + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + + print(f"碰撞面板状态更新:无碰撞 - 可编辑") + + except Exception as e: + print(f"更新碰撞面板状态失败: {e}") + import traceback + traceback.print_exc() + finally: + # 确保标志被重置 + self._updating_collision_panel = False + + def _loadCurrentCollisionParameters(self, model, shape_type): + """加载当前碰撞参数到界面""" + try: + # 设置标志位,防止在加载参数时触发更新 + self._loading_collision_params = True + + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_node = collision_np.node() + if collision_node.getNumSolids() > 0: + solid = collision_node.getSolid(0) + + # 从碰撞体形状中提取位置偏移 + if hasattr(self, 'collision_pos_x'): + # 获取模型的实际中心(考虑变换) + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + model_center = transformed_info['center'] + else: + model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) + else: + model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) + + # 获取碰撞体的中心 + collision_center = self._getCollisionShapeCenter(solid) + if collision_center: + # 计算偏移:碰撞体中心 - 模型中心 + offset_x = collision_center.x - model_center.x + offset_y = collision_center.y - model_center.y + offset_z = collision_center.z - model_center.z + self.collision_pos_x.setValue(offset_x) + self.collision_pos_y.setValue(offset_y) + self.collision_pos_z.setValue(offset_z) + print(f"加载位置偏移: ({offset_x:.2f}, {offset_y:.2f}, {offset_z:.2f})") + else: + # 如果无法计算偏移,设置为0 + self.collision_pos_x.setValue(0.0) + self.collision_pos_y.setValue(0.0) + self.collision_pos_z.setValue(0.0) + print("无法计算位置偏移,设置为0") + + if shape_type == 'sphere': + self._loadSphereParameters(solid) + elif shape_type == 'box': + self._loadBoxParameters(solid) + elif shape_type == 'capsule': + self._loadCapsuleParameters(solid) + elif shape_type == 'plane': + self._loadPlaneParameters(solid) + break + + except Exception as e: + print(f"加载碰撞参数失败: {e}") + finally: + # 重置标志位 + self._loading_collision_params = False + + def _getCollisionShapeCenter(self, solid): + """从碰撞体形状中获取中心点""" + try: + from panda3d.core import CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane, Point3 + + if isinstance(solid, CollisionSphere): + return solid.getCenter() + elif isinstance(solid, CollisionBox): + # 盒子的中心是最小点和最大点的中点 + min_pt = solid.getMin() + max_pt = solid.getMax() + return Point3((min_pt.x + max_pt.x) * 0.5, + (min_pt.y + max_pt.y) * 0.5, + (min_pt.z + max_pt.z) * 0.5) + elif isinstance(solid, CollisionCapsule): + # 胶囊体的中心是两个端点的中点 + point_a = solid.getPointA() + point_b = solid.getPointB() + return Point3((point_a.x + point_b.x) * 0.5, + (point_a.y + point_b.y) * 0.5, + (point_a.z + point_b.z) * 0.5) + elif isinstance(solid, CollisionPlane): + # 平面没有明确的中心,返回平面上的一个点 + plane = solid.getPlane() + return plane.getPoint() + else: + return None + except Exception as e: + print(f"获取碰撞体中心失败: {e}") + return None + + def _loadSphereParameters(self, solid): + """加载球形参数""" + try: + from panda3d.core import CollisionSphere + if isinstance(solid, CollisionSphere): + radius = solid.getRadius() + self.collision_radius.setValue(radius) + except Exception as e: + print(f"加载球形参数失败: {e}") + + def _loadBoxParameters(self, solid): + """加载盒型参数""" + try: + from panda3d.core import CollisionBox + if isinstance(solid, CollisionBox): + min_point = solid.getMin() + max_point = solid.getMax() + width = max_point.x - min_point.x + length = max_point.y - min_point.y + height = max_point.z - min_point.z + + self.collision_width.setValue(width) + self.collision_length.setValue(length) + self.collision_height.setValue(height) + except Exception as e: + print(f"加载盒型参数失败: {e}") + + def _loadCapsuleParameters(self, solid): + """加载胶囊体参数""" + try: + from panda3d.core import CollisionCapsule + if isinstance(solid, CollisionCapsule): + radius = solid.getRadius() + point_a = solid.getPointA() + point_b = solid.getPointB() + height = (point_b - point_a).length() + + self.collision_capsule_radius.setValue(radius) + self.collision_capsule_height.setValue(height) + except Exception as e: + print(f"加载胶囊体参数失败: {e}") + + def _loadPlaneParameters(self, solid): + """加载平面参数""" + try: + from panda3d.core import CollisionPlane + if isinstance(solid, CollisionPlane): + plane = solid.getPlane() + normal = plane.getNormal() + + self.collision_normal_x.setValue(normal.x) + self.collision_normal_y.setValue(normal.y) + self.collision_normal_z.setValue(normal.z) + except Exception as e: + print(f"加载平面参数失败: {e}") + + def _updateCollisionPosition(self, model, axis, value): + """更新碰撞位置偏移""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_collision_position', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_collision_position = True + + # 获取当前所有位置偏移值 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + + # 获取当前碰撞形状类型 + current_shape = self._getCurrentCollisionShape(model) + if not current_shape: + return + + # 重新创建碰撞体,传入位置偏移 + from panda3d.core import Vec3 + position_offset = Vec3(pos_x, pos_y, pos_z) + + # 根据形状类型收集其他参数 + if current_shape == 'sphere': + radius = self.collision_radius.value() if hasattr(self, 'collision_radius') else 1.0 + self._recreateCollisionShape(model, current_shape, radius=radius, position_offset=position_offset) + elif current_shape == 'box': + width = self.collision_width.value() if hasattr(self, 'collision_width') else 2.0 + length = self.collision_length.value() if hasattr(self, 'collision_length') else 2.0 + height = self.collision_height.value() if hasattr(self, 'collision_height') else 2.0 + self._recreateCollisionShape(model, current_shape, width=width, length=length, height=height, position_offset=position_offset) + elif current_shape == 'capsule': + cap_radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 + cap_height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 + self._recreateCollisionShape(model, current_shape, radius=cap_radius, height=cap_height, position_offset=position_offset) + elif current_shape == 'plane': + normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 + normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 + normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 + self._recreateCollisionShape(model, current_shape, normal=(normal_x, normal_y, normal_z), position_offset=position_offset) + + print(f"更新碰撞位置偏移 {axis}: {value}") + + except Exception as e: + print(f"更新碰撞位置失败: {e}") + finally: + self._updating_collision_position = False + + def _updateSphereRadius(self, model, radius): + """更新球形半径""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_sphere_radius', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_sphere_radius = True + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'sphere', radius=radius, position_offset=position_offset) + print(f"更新球形半径: {radius}") + except Exception as e: + print(f"更新球形半径失败: {e}") + finally: + self._updating_sphere_radius = False + + def _updateBoxSize(self, model, dimension, value): + """更新盒型尺寸""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_box_size', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_box_size = True + + # 获取当前所有尺寸 + width = self.collision_width.value() if hasattr(self, 'collision_width') else value + length = self.collision_length.value() if hasattr(self, 'collision_length') else value + height = self.collision_height.value() if hasattr(self, 'collision_height') else value + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'box', width=width, length=length, height=height, position_offset=position_offset) + print(f"更新盒型{dimension}: {value}") + except Exception as e: + print(f"更新盒型尺寸失败: {e}") + finally: + self._updating_box_size = False + + def _updateCapsuleRadius(self, model, radius): + """更新胶囊体半径""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_capsule_radius', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_capsule_radius = True + + height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) + print(f"更新胶囊体半径: {radius}") + except Exception as e: + print(f"更新胶囊体半径失败: {e}") + finally: + self._updating_capsule_radius = False + + def _updateCapsuleHeight(self, model, height): + """更新胶囊体高度""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_capsule_height', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_capsule_height = True + + radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) + print(f"更新胶囊体高度: {height}") + except Exception as e: + print(f"更新胶囊体高度失败: {e}") + finally: + self._updating_capsule_height = False + + def _updatePlaneNormal(self, model, axis, value): + """更新平面法向量""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_plane_normal', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_plane_normal = True + + # 获取当前法向量 + normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 + normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 + normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'plane', normal=(normal_x, normal_y, normal_z), position_offset=position_offset) + print(f"更新平面法向量 {axis}: {value}") + except Exception as e: + print(f"更新平面法向量失败: {e}") + finally: + self._updating_plane_normal = False + + def _recreateCollisionShape(self, model, shape_type, **kwargs): + """重新创建碰撞形状(保持可见性)""" + try: + # 保存当前状态 + collision_nodes = model.findAllMatches("**/+CollisionNode") + if not collision_nodes: + return + + collision_np = collision_nodes[0] + is_visible = not collision_np.isHidden() + + # 移除旧的碰撞体 + collision_np.removeNode() + + # 创建新的碰撞体 + from panda3d.core import CollisionNode, BitMask32 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + cNode.setIntoCollideMask(BitMask32.bit(2)) + + # 创建新形状(位置偏移已经烘焙在形状中) + if hasattr(self.world, 'collision_manager'): + collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type, **kwargs) + else: + # 回退方案 + from panda3d.core import CollisionSphere, Point3 + collision_shape = CollisionSphere(Point3(0, 0, 0), kwargs.get('radius', 1.0)) + + cNode.addSolid(collision_shape) + + # 重新附加(不设置额外位置,因为位置偏移已经在形状中) + new_collision_np = model.attachNewNode(cNode) + # 碰撞节点默认位置为 (0, 0, 0),位置偏移通过形状几何体处理 + new_collision_np.setPos(0, 0, 0) + + if is_visible: + new_collision_np.show() + else: + new_collision_np.hide() + + except Exception as e: + print(f"重新创建碰撞形状失败: {e}") + import traceback + traceback.print_exc() + + def _clearCollisionParameterControls(self): + """清理碰撞参数控件""" + try: + # 位置控件 + for attr in ['collision_pos_x', 'collision_pos_y', 'collision_pos_z']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 球形参数控件 + if hasattr(self, 'collision_radius'): + if self.collision_radius and self.collision_radius.parent(): + self.collision_radius.setParent(None) + self.collision_radius.deleteLater() + delattr(self, 'collision_radius') + + # 盒型参数控件 + for attr in ['collision_width', 'collision_length', 'collision_height']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 胶囊体参数控件 + for attr in ['collision_capsule_radius', 'collision_capsule_height']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 平面参数控件 + for attr in ['collision_normal_x', 'collision_normal_y', 'collision_normal_z']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + print("清理碰撞参数控件完成") + + except Exception as e: + print(f"清理碰撞参数控件失败: {e}") + + def _refreshCollisionPanel(self, model): + """刷新整个碰撞面板(简化版)""" + try: + print("使用简化的面板刷新") + # 直接调用状态更新,不删除面板 + self._updateCollisionPanelState(model) + + except Exception as e: + print(f"刷新碰撞面板失败: {e}") + import traceback + traceback.print_exc() + + def _addParameterControlsToExistingPanel(self, model, shape_type): + """向现有面板添加参数控件""" + try: + if not hasattr(self, 'collision_layout'): + return + + layout = self.collision_layout + + # 首先清理可能存在的旧控件 + self._hideCollisionParameterControls() + + # 找到插入位置(在按钮之前) + current_row = 2 + + # 位置调整控件 + pos_label = QLabel("位置偏移:") + pos_label.setVisible(True) # 确保可见 + layout.addWidget(pos_label, current_row, 0) + current_row += 1 + + # X, Y, Z 位置调整 + self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) + + x_label = QLabel("X:") + x_label.setVisible(True) + layout.addWidget(x_label, current_row, 0) + self.collision_pos_x.setVisible(True) + layout.addWidget(self.collision_pos_x, current_row, 1) + current_row += 1 + + y_label = QLabel("Y:") + y_label.setVisible(True) + layout.addWidget(y_label, current_row, 0) + self.collision_pos_y.setVisible(True) + layout.addWidget(self.collision_pos_y, current_row, 1) + current_row += 1 + + z_label = QLabel("Z:") + z_label.setVisible(True) + layout.addWidget(z_label, current_row, 0) + self.collision_pos_z.setVisible(True) + layout.addWidget(self.collision_pos_z, current_row, 1) + current_row += 1 + + # 连接位置变化信号 + self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) + self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) + self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) + + # 根据形状类型添加特定参数 + if shape_type == 'sphere': + current_row = self._addSphereParametersToExisting(model, layout, current_row) + elif shape_type == 'box': + current_row = self._addBoxParametersToExisting(model, layout, current_row) + elif shape_type == 'capsule': + current_row = self._addCapsuleParametersToExisting(model, layout, current_row) + elif shape_type == 'plane': + current_row = self._addPlaneParametersToExisting(model, layout, current_row) + + # 重新定位按钮 + self._repositionButtons(current_row) + + # 加载参数值 + self._loadCurrentCollisionParameters(model, shape_type) + + except Exception as e: + print(f"添加参数控件到现有面板失败: {e}") + import traceback + traceback.print_exc() + + def _addSphereParametersToExisting(self, model, layout, start_row): + """向现有面板添加球形参数""" + current_row = start_row + + radius_label = QLabel("半径:") + radius_label.setVisible(True) + layout.addWidget(radius_label, current_row, 0) + + self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_radius.setVisible(True) + self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) + layout.addWidget(self.collision_radius, current_row, 1) + current_row += 1 + + return current_row + + def _addBoxParametersToExisting(self, model, layout, start_row): + """向现有面板添加盒型参数""" + current_row = start_row + + size_label = QLabel("尺寸:") + size_label.setVisible(True) + layout.addWidget(size_label, current_row, 0) + current_row += 1 + + self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2) + + width_label = QLabel("宽度:") + width_label.setVisible(True) + layout.addWidget(width_label, current_row, 0) + self.collision_width.setVisible(True) + layout.addWidget(self.collision_width, current_row, 1) + current_row += 1 + + length_label = QLabel("长度:") + length_label.setVisible(True) + layout.addWidget(length_label, current_row, 0) + self.collision_length.setVisible(True) + layout.addWidget(self.collision_length, current_row, 1) + current_row += 1 + + height_label = QLabel("高度:") + height_label.setVisible(True) + layout.addWidget(height_label, current_row, 0) + self.collision_height.setVisible(True) + layout.addWidget(self.collision_height, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) + self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) + self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) + + return current_row + + def _addCapsuleParametersToExisting(self, model, layout, start_row): + """向现有面板添加胶囊体参数""" + current_row = start_row + + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + + self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) + layout.addWidget(self.collision_capsule_radius, current_row, 1) + current_row += 1 + + height_label = QLabel("高度:") + layout.addWidget(height_label, current_row, 0) + + self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) + layout.addWidget(self.collision_capsule_height, current_row, 1) + current_row += 1 + + return current_row + + def _addPlaneParametersToExisting(self, model, layout, start_row): + """向现有面板添加平面参数""" + current_row = start_row + + normal_label = QLabel("法向量:") + layout.addWidget(normal_label, current_row, 0) + current_row += 1 + + self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) + + layout.addWidget(QLabel("Nx:"), current_row, 0) + layout.addWidget(self.collision_normal_x, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Ny:"), current_row, 0) + layout.addWidget(self.collision_normal_y, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Nz:"), current_row, 0) + layout.addWidget(self.collision_normal_z, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) + self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) + self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) + + return current_row + + def _addVisibilityButtonToExistingPanel(self, model): + """向现有面板添加可见性按钮""" + try: + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + is_collision_visible = self._isCollisionVisible(model) + + # 找到合适的行位置 + current_row = layout.rowCount() + + self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") + self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) + layout.addWidget(self.collision_visibility_button, current_row - 1, 0, 1, 2) + + except Exception as e: + print(f"添加可见性按钮失败: {e}") + + def _repositionButtons(self, new_row): + """重新定位按钮位置""" + try: + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + + # 移动可见性按钮 + if hasattr(self, 'collision_visibility_button'): + layout.addWidget(self.collision_visibility_button, new_row, 0, 1, 2) + new_row += 1 + + # 移动主按钮 + if hasattr(self, 'collision_button'): + layout.addWidget(self.collision_button, new_row, 0, 1, 2) + + except Exception as e: + print(f"重新定位按钮失败: {e}") + + def _hideCollisionParameterControls(self): + """隐藏碰撞参数控件(保留按钮)""" + try: + # 清理属性引用,但保留按钮 + param_attrs = [ + 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', + 'collision_radius', + 'collision_width', 'collision_length', 'collision_height', + 'collision_capsule_radius', 'collision_capsule_height', + 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' + ] + + # 隐藏并删除参数控件 + for attr in param_attrs: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget: + widget.setVisible(False) + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 同时清理可能的标签控件 + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + + # 收集需要移除的控件(不包括基本控件和按钮) + widgets_to_remove = [] + + for i in range(layout.rowCount()): + if i >= 2: # 从第3行开始检查 + for j in range(layout.columnCount()): + item = layout.itemAtPosition(i, j) + if item: + widget = item.widget() + if widget and hasattr(widget, 'text'): + # 检查是否是参数相关的标签 + text = widget.text() + if any(keyword in text for keyword in + ['位置偏移', 'X:', 'Y:', 'Z:', '半径:', '尺寸:', '宽度:', '长度:', '高度:', + '法向量:', 'Nx:', 'Ny:', 'Nz:']): + widgets_to_remove.append(widget) + + # 移除参数标签 + for widget in widgets_to_remove: + widget.setVisible(False) + widget.setParent(None) + widget.deleteLater() + + print("隐藏碰撞参数控件完成(保留按钮)") + + except Exception as e: + print(f"隐藏碰撞参数控件失败: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/ui/widgets.py b/ui/widgets.py index c84deb42..cefd9b3f 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -17,7 +17,8 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout, from PyQt5.QtCore import Qt, QUrl, QMimeData from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush from PyQt5.sip import wrapinstance -from panda3d.core import ModelRoot +from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import ModelRoot, NodePath, CollisionNode from QPanda3D.QPanda3DWidget import QPanda3DWidget from scene import util @@ -29,7 +30,67 @@ class NewProjectDialog(QDialog): super().__init__(parent) self.setWindowTitle("新建项目") self.setMinimumWidth(500) - + + # 设置对话框样式与主窗口保持一致 + self.setStyleSheet(""" + QDialog { + background-color: #252538; + color: #e0e0ff; + } + QGroupBox { + background-color: #2d2d44; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; /* 保持这个设置 */ + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; /* 增加顶部内边距,为标题留出空间 */ + } + QGroupBox::title { + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 6px; + } + QLineEdit:disabled { + background-color: #1e1e2e; + color: #8888aa; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QLabel { + color: #e0e0ff; + } + QLabel:disabled { + color: #8888aa; + } + QDialogButtonBox QPushButton { + min-width: 80px; + } + """) + # 创建布局 layout = QVBoxLayout(self) @@ -155,7 +216,7 @@ class CustomPanda3DWidget(QPanda3DWidget): event.acceptProposedAction() else: event.ignore() - + def wheelEvent(self, event): """处理滚轮事件""" if event.angleDelta().y() > 0: @@ -606,8 +667,27 @@ class CustomAssetsTreeWidget(QTreeWidget): """获取项目根路径下的Resources文件夹,考虑跨平台""" import os - # 获取项目根路径 - project_root = os.getcwd() + # 获取当前文件所在目录,然后向上查找项目根目录 + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # 向上查找直到找到项目根目录(包含特定标识文件或文件夹) + project_root = current_dir + max_depth = 10 # 限制向上查找的深度 + depth = 0 + + while depth < max_depth: + # 检查是否是项目根目录(可以根据实际情况调整判断条件) + if (os.path.exists(os.path.join(project_root, "main.py")) or + os.path.exists(os.path.join(project_root, "setup.py")) or + os.path.exists(os.path.join(project_root, ".git"))): + break + parent_dir = os.path.dirname(project_root) + if parent_dir == project_root: # 已经到达文件系统根目录 + # 回退到使用当前工作目录 + project_root = os.getcwd() + break + project_root = parent_dir + depth += 1 # 构建Resources文件夹路径(跨平台) resources_path = os.path.join(project_root, "Resources") @@ -624,6 +704,28 @@ class CustomAssetsTreeWidget(QTreeWidget): return resources_path + # def getProjectRootPath(self): + # """获取项目根路径下的Resources文件夹,考虑跨平台""" + # import os + # + # # 获取项目根路径 + # project_root = os.getcwd() + # + # # 构建Resources文件夹路径(跨平台) + # resources_path = os.path.join(project_root, "Resources") + # + # # 如果Resources文件夹不存在,创建它 + # if not os.path.exists(resources_path): + # try: + # os.makedirs(resources_path, exist_ok=True) + # print(f"创建Resources文件夹: {resources_path}") + # except OSError as e: + # print(f"无法创建Resources文件夹: {e}") + # # 如果无法创建,回退到项目根路径 + # return project_root + # + # return resources_path + def load_file_tree(self): """加载树形视图""" self.clear() @@ -654,7 +756,7 @@ class CustomAssetsTreeWidget(QTreeWidget): if os.path.exists(directory) and directory not in self.watched_directories: if self.file_watcher.addPath(directory): self.watched_directories.add(directory) - print(f"开始监控目录:{directory}") + #print(f"开始监控目录:{directory}") try: for item in os.listdir(directory): item_path = os.path.join(directory,item) @@ -1002,7 +1104,6 @@ class CustomAssetsTreeWidget(QTreeWidget): internal_paths.append(filepath) # 检查是否是模型文件(用于向外拖拽) if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - print(f"模型路ing!!!!!!!!!!!!!!!!!{QUrl.fromLocalFile(filepath)}") urls.append(QUrl.fromLocalFile(filepath)) # 设置内部拖拽数据 @@ -1210,14 +1311,83 @@ class CustomConsoleDockWidget(QWidget): self.autoScrollBtn = QPushButton("自动滚动") self.autoScrollBtn.setCheckable(True) self.autoScrollBtn.setChecked(True) + self.autoScrollBtn.setStyleSheet(""" + QPushButton { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:checked { + background-color: #8b5cf6; + color: white; + border: 1px solid #7c3aed; + } + QPushButton:hover { + background-color: #3a3a4a; + } + QPushButton:checked:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + """) toolbar.addWidget(self.autoScrollBtn) # 时间戳开关 self.timestampBtn = QPushButton("显示时间") self.timestampBtn.setCheckable(True) self.timestampBtn.setChecked(True) + self.timestampBtn.setStyleSheet(""" + QPushButton { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:checked { + background-color: #8b5cf6; + color: white; + border: 1px solid #7c3aed; + } + QPushButton:hover { + background-color: #3a3a4a; + } + QPushButton:checked:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + """) toolbar.addWidget(self.timestampBtn) + self.fpsLabel = QLabel("FPS:0.0") + self.fpsLabel.setStyleSheet(""" + QLabel { + background-color: #2d2d44; + color: #80ff80; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + font-family: 'Consolas', 'Monaco', monospace; + } + """) + self.fpsLabel.setMinimumWidth(100) + self.fpsLabel.setAlignment(Qt.AlignCenter) + toolbar.addWidget(self.fpsLabel) + + # 帧率更新定时器 + self.fpsTimer = QTimer() + self.fpsTimer.timeout.connect(self.updateFPS) + self.fpsTimer.start(1000) # 每秒更新一次 + toolbar.addStretch() layout.addLayout(toolbar) @@ -1259,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget): # 添加欢迎信息 self.addMessage("🎮 编辑器控制台已启动", "INFO") + def updateFPS(self): + try: + if hasattr(self.world,'clock'): + fps = self.world.clock.getAverageFrameRate() + self.fpsLabel.setText(f"FPS:{fps:.1f}") + + # 根据帧率设置颜色 + if fps >= 50: + color = "#80ff80" # 绿色 - 优秀 + elif fps >= 30: + color = "#ffff80" # 黄色 - 一般 + else: + color = "#ff8080" # 红色 - 较差 + + self.fpsLabel.setStyleSheet(f""" + QLabel {{ + background-color: #2d2d44; + color: {color}; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + font-family: 'Consolas', 'Monaco', monospace; + }} + """) + except Exception as e: + pass # 静默处理错误,避免影响控制台功能 + def setupConsoleRedirect(self): """设置控制台重定向""" import sys @@ -1369,42 +1567,66 @@ class CustomTreeWidget(QTreeWidget): parent = wrapinstance(0, QWidget) super().__init__(parent) self.world = world + # self.selectedItems = None self.initData() self.setupUI() # 初始化界面 self.setupContextMenu() # 初始化右键菜单 - self.setupDragDrop() # 设置拖拽功能 - self.original_scales={} + self.setStyleSheet(""" + /* 设置折叠状态下,带子节点的箭头颜色 */ + QTreeWidget::branch:has-children:!open { + color: #8b5cf6; /* 紫色 */ + } + + /* 设置展开状态下,带子节点的箭头颜色 */ + QTreeWidget::branch:has-children:open { + color: #9ca3af; /* 灰色,提供状态变化反馈 */ + } + + /* 鼠标悬停在任意箭头上时,颜色变亮 */ + QTreeWidget::branch:hover { + color: #a78bfa; /* 亮紫色 */ + } + """) + def initData(self): """初始化变量""" # 定义2D GUI元素类型 self.gui_2d_types = { - "GUI_BUTTON", # DirectButton - "GUI_LABEL", # DirectLabel - "GUI_ENTRY", # DirectEntry - "GUI_IMAGE", + "GUI_BUTTON", # GUI 按钮 + "GUI_LABEL", # GUI 标签 + "GUI_ENTRY", # GUI 输入框 + "GUI_IMAGE", # GUI 图片 + "GUI_2D_VIDEO_SCREEN", # GUI 2D视频 + "GUI_SPHERICAL_VIDEO", # GUI 3D球形视频 "GUI_NODE" # 其他2D GUI容器 } # 定义3D GUI元素类型 self.gui_3d_types = { - "GUI_3DTEXT", # 3D TextNode - "GUI_3DIMAGE", - "GUI_VIRTUAL_SCREEN" # Virtual Screen + "GUI_3DTEXT", # 3D 文本节点 + "GUI_3DIMAGE", # 3D 图片节点 + "GUI_VIRTUAL_SCREEN", # 3D视频 + "GUI_VirtualScreen" # 3D虚拟视频 } # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) self.scene_3d_types = { "SCENE_ROOT", "SCENE_NODE", - "LIGHT_NODE", + "LIGHT_NODE", # 灯节点 "CAMERA_NODE", - "IMPORTED_MODEL_NODE", - "MODEL_NODE" + "IMPORTED_MODEL_NODE", # 导入模型节点 + "MODEL_NODE", + "TERRAIN_NODE", # 地形节点 + "CESIUM_TILESET_NODE" # 3D Tileset } + # 这是一个最佳实践,它让代码的意图变得非常清晰。 + self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types) + def setupUI(self): """初始化UI设置""" self.setHeaderHidden(True) @@ -1427,12 +1649,9 @@ class CustomTreeWidget(QTreeWidget): def showContextMenu(self, position): """显示右键菜单 - 复用主菜单的创建动作""" - if not self.selectedItems(): - print("没有选中的项目,不显示右键菜单") - return - - item = self.selectedItems()[0] - print(f"为项目 '{item.text(0)}' 显示右键菜单") + if self.selectedItems(): + item = self.selectedItems()[0] + print(f"为项目 '{item.text(0)}' 显示右键菜单") # 创建右键菜单 menu = QMenu(self) @@ -1682,7 +1901,9 @@ class CustomTreeWidget(QTreeWidget): # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下 if is_dragged_2d_gui: - if is_target_2d_gui: + if target_type == "SCENE_ROOT": + return True + elif is_target_2d_gui: print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}") return True elif is_target_3d_gui: @@ -1701,7 +1922,7 @@ class CustomTreeWidget(QTreeWidget): elif is_dragged_3d_gui: if is_target_3d_scene: print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return True + return False elif is_target_2d_gui: print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") @@ -1727,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget): elif is_target_3d_gui: print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下") print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") - return True + return False else: print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") return False @@ -1773,6 +1994,20 @@ class CustomTreeWidget(QTreeWidget): def dragMoveEvent(self, event): """处理拖动事件""" + indicator_pos = self.dropIndicatorPosition() + indicator_str = "Unknown" + + if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem: + indicator_str = "OnItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem: + indicator_str = "AboveItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem: + indicator_str = "BelowItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: + indicator_str = "OnViewport" + + #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') + if event.source() != self: event.ignore() return @@ -1811,6 +2046,8 @@ class CustomTreeWidget(QTreeWidget): selected_items = self.selectedItems() if selected_items: # 执行删除操作 + # if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE": + # self._preprocess_light_items_for_deletion(selected_items) self.delete_items(selected_items) else: # 没有选中任何项目,执行默认操作 @@ -1818,12 +2055,58 @@ class CustomTreeWidget(QTreeWidget): else: super().keyPressEvent(event) + def _preprocess_light_items_for_deletion(self, selected_items): + """预处理灯光节点删除,特别处理最后一个灯光节点的问题""" + if not selected_items: + return selected_items + + # 检查选中的项目中是否包含灯光节点 + light_items = [] + for item in selected_items: + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "LIGHT_NODE": + light_items.append(item) + + # 如果没有灯光节点,直接返回 + if not light_items: + return selected_items + + # 检查是否只有最后一个灯光节点被选中 + processed_items = list(selected_items) # 创建副本 + + for item in light_items: + panda_node = item.data(0, Qt.UserRole) + if not panda_node: + continue + + # 获取灯光类型 + if hasattr(panda_node, 'getTag'): + light_type = panda_node.getTag("light_type") + + # 检查是否是最后一个Spotlight + if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and + self.world.Spotlight and self.world.Spotlight[-1] == panda_node and + len(self.world.Spotlight) > 1): + + print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + # 检查是否是最后一个Pointlight + elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and + self.world.Pointlight and self.world.Pointlight[-1] == panda_node and + len(self.world.Pointlight) > 1): + + print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + return processed_items + def delete_items(self, selected_items): """删除选中的item - 简化版本""" if not selected_items: return - # 过滤掉不能删除的节点 + # 1. 过滤掉不能删除的节点 deletable_items = [] for item in selected_items: node_type = item.data(0, Qt.UserRole + 1) @@ -1839,7 +2122,7 @@ class CustomTreeWidget(QTreeWidget): QMessageBox.information(self, "提示", "没有可删除的节点") return - # 确认删除 + # 2. 确认删除 item_count = len(deletable_items) if item_count == 1: message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?" @@ -1854,11 +2137,19 @@ class CustomTreeWidget(QTreeWidget): if reply != QMessageBox.Yes: return - # 执行删除 + # 默认选中场景根节点,通常是第一个顶级节点 + #next_item_to_select = self.topLevelItem(0) + + # 3. 执行删除循环 deleted_count = 0 for item in deletable_items: try: + # 在删除前,记录其父节点,作为删除后的新选择 + # 选择最后一个被删除项的父节点作为新的焦点 + if item.parent(): + next_item_to_select = item.parent() panda_node = item.data(0, Qt.UserRole) + if panda_node: # 清理选择状态 if (hasattr(self.world, 'selection') and @@ -1874,38 +2165,32 @@ class CustomTreeWidget(QTreeWidget): if hasattr(panda_node, 'getPythonTag'): light_object = panda_node.getPythonTag('rp_light_object') if light_object and hasattr(self.world, 'render_pipeline'): - try: - self.world.render_pipeline.remove_light(light_object) - print(f"移除灯光{panda_node.getName()}") - except Exception as e: - print(f"移除灯光失败: {str(e)}") - panda_node.clearPythonTag('rp_light_object') - #self.world.render_pipeline.remove_light(light_object) - - if hasattr(self.world,'gui_manager') and hasattr(self.world.gui_manager,'gui_elements'): - if panda_node in self.world.gui_manager.gui_elements: - self.world.gui_manager.gui_elements.remove(panda_node) - print(f"从gui_elements列表中移除{panda_node.getName()}") + print(f'11111111111111111111111111,{light_object.casts_shadows}') + self.world.render_pipeline.remove_light(light_object) # 从world列表中移除 + if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements: + self.world.gui_elements.remove(panda_node) if hasattr(self.world, 'models') and panda_node in self.world.models: self.world.models.remove(panda_node) + if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: + self.world.Spotlight.remove(panda_node) + if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: + self.world.Pointlight.remove(panda_node) + if hasattr(self.world, 'terrains') and panda_node in self.world.terrains: + self.world.terrains.remove(panda_node) + if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets: + # self.world.tilesets.remove(panda_node) + # 从 tilesets 列表中移除 + if hasattr(self.world, 'scene_manager'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + if tileset_info['node'] == panda_node: + tilesets_to_remove.append(i) - # if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: - # self.world.Spotlight.remove(panda_node) - - if hasattr(self.world,'Spotlight'): - self.world.Spotlight = [light for light in self.world.Spotlight if light != panda_node] - if panda_node in self.world.Spotlight: - print(f"从Spotlight列表中移除{panda_node.getName()}") - - # if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: - # self.world.Pointlight.remove(panda_node) - - if hasattr(self.world,'Pointlight'): - self.world.Pointlight = [light for light in self.world.Pointlight if light != panda_node] - if panda_node in self.world.Pointlight: - print(f"从Pointlight列表中移除{panda_node.getName()}") + # 从后往前删除,避免索引问题 + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] # 从Panda3D场景中移除 try: @@ -1935,7 +2220,210 @@ class CustomTreeWidget(QTreeWidget): # if hasattr(self.world, 'property_panel'): # self.world.property_panel.clearPropertyPanel() - print(f"✅ 已删除 {deleted_count} 个节点") + # 4. 删除操作完成后,更新UI --- + if deleted_count > 0: + print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") + self.update_selection_and_properties(None, None) + + def delete_item(self, panda_node): + """删除指定节点 panda3D(node)- 优化和修复版本""" + if not panda_node or panda_node.is_empty(): + print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") + return + + # --- 关键修复:在操作前,安全地获取节点名字 --- + node_name_for_logging = panda_node.getName() + + # 1. 寻找对应的Qt Item + item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot()) + + # 场景清理(无论是否找到item,都应该执行) + self._cleanup_panda_node_resources(panda_node) + panda_node.removeNode() + + # 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出 + if not item: + print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") + return + try: + # 2. 过滤受保护节点 + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中 + print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。") + return + + # 3. 从UI树中移除 + parent_for_next_selection = item.parent() + if item.parent(): + item.parent().removeChild(item) + else: + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + print(f"✅ 成功删除节点: {node_name_for_logging}") + + # 4. 更新UI + print(f"🔄 正在更新UI...") + if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid(): + new_selection_item = parent_for_next_selection + else: + new_selection_item = self.topLevelItem(0) + + if new_selection_item: + self.setCurrentItem(new_selection_item) + new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole) + self.update_selection_and_properties(new_panda_node_to_select, new_selection_item) + else: + self.update_selection_and_properties(None, None) + + except Exception as e: + print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}") + import traceback + traceback.print_exc() + + def clear_tree(self): + """清空UI树""" + print("Clear") + self.clear() + # 创建场景根节点 + sceneRoot = QTreeWidgetItem(self, ['场景']) + sceneRoot.setData(0, Qt.UserRole, self.world.render) + sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") + # 添加相机节点 + cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) + cameraItem.setData(0, Qt.UserRole, self.world.cam) + cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") + # 添加地板节点 + if hasattr(self.world, 'ground') and self.world.ground: + groundItem = QTreeWidgetItem(sceneRoot, ['地板']) + groundItem.setData(0, Qt.UserRole, self.world.ground) + groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + + def _cleanup_panda_node_resources(self, panda_node): + """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" + if not panda_node or panda_node.is_empty(): + return + try: + # 清理选择状态 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node: + self.world.selection.updateSelection(None) + # 清理属性面板 + if hasattr(self.world, 'property_panel'): + self.world.property_panel.removeActorForModel(panda_node) + # 清理灯光 + if hasattr(panda_node, 'getPythonTag'): + light_object = panda_node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + # 从各种world管理列表中移除 + lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains'] + for list_name in lists_to_check: + if hasattr(self.world, list_name): + world_list = getattr(self.world, list_name) + if panda_node in world_list: + world_list.remove(panda_node) + # 特殊处理tilesets + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'): + tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if + info.get('node') == panda_node] + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。") + except Exception as e: + # 即便这里出错,也要打印信息,但不要让整个删除流程中断 + print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}") + + # def mousePressEvent(self, event): + # """鼠标按下事件""" + # if event.button() == Qt.LeftButton: + # if self.currentItem(): + # print(f"self.currentItem() = {self.currentItem()}") + # else: + # print(f"self.currentItem() = None") + # + # # 调用父类处理其他事件 + # super().mousePressEvent(event) + + def update_item_name(self, text, item): + """ 树节点名字 """ + if not item: + return + try: + # 正确的代码 + node = item.data(0, Qt.UserRole) + + item.setText(0, text) + node.setName(text) + except Exception as e: + print(e) + + def _findSceneRoot(self): + """查找场景根节点""" + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + return top_item + return None + + def create_model_items(self, model: NodePath): + """ + 【此函数保持不变】 + 创建模型项。 + 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, + 然后完整地展示这些分支。 + """ + if not model: + print("传入的参数model为空") + return + + # 找到场景树的根节点,我们将把模型节点添加到这里 + root_item = self._findSceneRoot() + if not root_item: + print("错误:未能找到场景根节点项") + return + + # 1. 在模型的第一层子节点中进行筛选 + for child_node in model.getChildren(): + if child_node.hasTag("is_scene_element"): + print(f"找到带标签的根节点:{child_node.getName()}") + if (child_node.hasTag("gui_type")and + child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]): + print(f"跳过3dGUI节点{child_node.getName()}") + continue + + # 为这个带标签的节点创建一个树项 + child_item = QTreeWidgetItem(root_item) + child_item.setText(0, child_node.getName() or "Unnamed Tagged Node") + child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 + + # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) + self._add_all_children_unconditionally(child_item, child_node) + + def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): + """ + 【此函数已更新】 + 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 + """ + for child_node in node_path.getChildren(): + + # 新增:检查节点是否为碰撞节点 + if isinstance(child_node.node(), CollisionNode): + # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 + continue # 如果是,则跳过此节点及其所有子节点 + + # 创建子项 + child_item = QTreeWidgetItem(parent_item) + child_item.setText(0, child_node.getName() or "Unnamed Child") + child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 + + # 继续无条件地递归 + if not child_node.is_empty(): + self._add_all_children_unconditionally(child_item, child_node) # ==================== 辅助方法 ==================== def _findSceneRoot(self): @@ -1966,52 +2454,79 @@ class CustomTreeWidget(QTreeWidget): def add_node_to_tree_widget(self, node, parent_item, node_type): """将node元素添加到树形控件""" + if hasattr(node, 'getTag'): + if node.hasTag('tree_item_type'): + print(f"node0: {node.getName()},{node.getTag('tree_item_type')}") + tree_type = node.getTag('tree_item_type') + else: + node.setTag('tree_item_type', node_type) + else: + print(f"node2: {node.getName()},{node_type}") + tree_type = node_type + + # BLACK_LIST 和依赖项导入保持不变 BLACK_LIST = {'', '**', 'temp', 'collision'} + from panda3d.core import CollisionNode, ModelRoot + from PyQt5.QtWidgets import QTreeWidgetItem + from PyQt5.QtCore import Qt - from panda3d.core import CollisionNode + # 1. 修改内部函数,让它返回创建的节点 + def addNodeToTree(node, parentItem, force=False): + """内部递归函数,现在会返回创建的顶级节点项""" + if not force and should_skip(node): + return None # 如果跳过,返回None + + nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) + nodeItem.setData(0, Qt.UserRole, node) + nodeItem.setData(0, Qt.UserRole + 1, tree_type) + + for child in node.getChildren(): + # 递归调用,但我们只关心顶级的nodeItem + addNodeToTree(child, nodeItem, force=False) + + return nodeItem # <-- 新增:返回创建的QTreeWidgetItem def should_skip(node): name = node.getName() return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance( node.node(), ModelRoot) or name == "" - def addNodeToTree(node, parentItem, force=False): - if not force and should_skip(node): - return None - nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) - nodeItem.setData(0, Qt.UserRole, node) - nodeItem.setData(0, Qt.UserRole + 1, node_type) - - for child in node.getChildren(): - addNodeToTree(child, nodeItem, force=False) - return nodeItem + # 使用一个变量来确保无论哪个分支都有返回值 + new_qt_item = None + node_name = "" try: - from PyQt5.QtWidgets import QTreeWidgetItem - from PyQt5.QtCore import Qt + if tree_type == "IMPORTED_MODEL_NODE": + # getTag('file') 可能是你自己设置的tag,这里假设它存在 + node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() - # 初始化new_qt_item变量 - new_qt_item = None - - if node_type == "IMPORTED_MODEL_NODE": - node_name = node.getTag("file") if hasattr(node, 'getTag') else "model" + # 2. 接收 addNodeToTree 的返回值 new_qt_item = addNodeToTree(node, parent_item, force=True) + else: node_name = node.getName() if hasattr(node, 'getName') else "node" new_qt_item = QTreeWidgetItem(parent_item, [node_name]) new_qt_item.setData(0, Qt.UserRole, node) - new_qt_item.setData(0, Qt.UserRole + 1, node_type) + new_qt_item.setData(0, Qt.UserRole + 1, tree_type) - # 展开父节点 - if hasattr(parent_item, 'setExpanded'): - parent_item.setExpanded(True) + # 确保 new_qt_item 成功创建后再继续操作 + if new_qt_item: + # 展开父节点 + if hasattr(parent_item, 'setExpanded'): + parent_item.setExpanded(True) - print(f"✅ Qt树节点添加成功: {node_name}") - return new_qt_item + print(f"✅ Qt树节点添加成功: {node_name}") + return new_qt_item + else: + # 如果 addNodeToTree 因为 should_skip 返回了 None + print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。") + return None except Exception as e: + import traceback print(f"❌ 添加node到树形控件失败: {str(e)}") + traceback.print_exc() # 打印更详细的错误堆栈,方便调试 return None def update_selection_and_properties(self, node, qt_item): @@ -2085,7 +2600,7 @@ class CustomTreeWidget(QTreeWidget): node_type = item.data(0, Qt.UserRole + 1) # 场景根节点和普通场景节点可以作为父节点 - if node_type in self.gui_3d_types and self.scene_3d_types: + if node_type in self.valid_3d_parent_types: return True # # 模型节点也可以作为父节点