diff --git a/QPanda3D/QPanda3DWidget.py b/QPanda3D/QPanda3DWidget.py index 249fc355..5877a623 100644 --- a/QPanda3D/QPanda3DWidget.py +++ b/QPanda3D/QPanda3DWidget.py @@ -12,6 +12,7 @@ from PyQt5 import QtWidgets, QtGui from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * +from direct.task.TaskManagerGlobal import taskMgr # Panda imports from panda3d.core import Texture, WindowProperties, CallbackGraphicsWindow @@ -32,9 +33,17 @@ 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: + # 静默处理所有异常,包括 has_mat() 断言错误 + pass def get_panda_key_modifiers(evt): diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index a1862356..9c9622cd 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -5,7 +5,7 @@ control_points: clouds: - cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000]]] + cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000],[0.4932562620,0.9526462396]]] color_correction: camera_iso: [[[0.4708024067,0.2757660168]]] camera_shutter: [[[0.5134061147,0.0552217053]]] @@ -17,8 +17,8 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.5000000000]]] - sun_altitude: [[[0.5000000000,0.9666666667]]] + sun_azimuth: [[[0.5000000000,0.4833333333]]] + sun_altitude: [[[0.5000000000,0.9777777778]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: fog_ramp_size: [[[0.5510597303,0.7409470752]]] diff --git a/RenderPipelineFile/rpcore/native/__init__.py b/RenderPipelineFile/rpcore/native/__init__.py index 16fa5411..a2f219a3 100644 --- a/RenderPipelineFile/rpcore/native/__init__.py +++ b/RenderPipelineFile/rpcore/native/__init__.py @@ -73,7 +73,6 @@ native_module = None # If the module was built, use it, otherwise use the python wrappers if NATIVE_CXX_LOADED: - print(f'12121212121212121212121212') try: from panda3d import _rplight as _native_module # pylint: disable=wrong-import-position RPObject.global_debug("CORE", "Using panda3d-supplied core module") diff --git a/RenderPipelineFile/rpcore/render_pipeline.py b/RenderPipelineFile/rpcore/render_pipeline.py index a7ac29df..e4c46dbb 100644 --- a/RenderPipelineFile/rpcore/render_pipeline.py +++ b/RenderPipelineFile/rpcore/render_pipeline.py @@ -351,7 +351,11 @@ class RenderPipeline(RPObject): continue material = state.get_attrib(MaterialAttrib).get_material() - shading_model = material.emission.x + if material.emission is not None: + shading_model = material.emission.x + else: + shading_model = 0.0 + # SHADING_MODEL_TRANSPARENT if shading_model == 3: diff --git a/RenderPipelineFile/samples/06-Car/main.py b/RenderPipelineFile/samples/06-Car/main.py index 1720104b..da6715f8 100644 --- a/RenderPipelineFile/samples/06-Car/main.py +++ b/RenderPipelineFile/samples/06-Car/main.py @@ -52,6 +52,11 @@ class MainApp(ShowBase): # Load the scene model = loader.loadModel("scene/scene.bam") # model = loader.loadModel("scene2/Scene.bam") + model_0 = self.loader.loadModel("/home/tiger/下载/Benci/source/s65/s65/s65.fbx") + model_0.reparentTo(self.render) + model_0.setScale(0.01) + model_0.setPos(-8, 42, 0) + model_0.setHpr(0, 90, 0) model.reparent_to(render) self.render_pipeline.prepare_scene(model) 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/Start_Run.py b/Start_Run.py index 225cd6a8..8e6a1214 100644 --- a/Start_Run.py +++ b/Start_Run.py @@ -17,13 +17,18 @@ sys.path.insert(0, render_pipeline_file_path) 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[0]) + run(args_str) + # run(args) else: run() \ No newline at end of file diff --git a/core/Command_System.py b/core/Command_System.py new file mode 100644 index 00000000..fd8d245b --- /dev/null +++ b/core/Command_System.py @@ -0,0 +1,648 @@ +from abc import ABC, abstractmethod +from collections import deque +from typing import List +from panda3d.core import NodePath, Point3 + + +class Command(ABC): + """ + 抽象命令类,所有具体命令都需要继承此类 + """ + + @abstractmethod + def execute(self): + """ + 执行命令 + """ + pass + + @abstractmethod + def undo(self): + """ + 撤销命令 + """ + pass + + @abstractmethod + def redo(self): + """ + 重做命令 + """ + pass + + +class CommandManager: + """ + 命令管理器,负责管理命令的执行、撤销和重做 + """ + + def __init__(self, max_history: int = 100): + # 用于存储已执行的命令的历史记录 + self._undo_stack: deque = deque(maxlen=max_history) + # 用于存储已撤销的命令,支持重做 + self._redo_stack: deque = deque(maxlen=max_history) + # 最大历史记录数 + self._max_history = max_history + + def execute_command(self, command: Command): + """ + 执行命令,并将其添加到撤销栈中 + """ + try: + command.execute() + self._undo_stack.append(command) + # 清空重做栈,因为执行新命令后就无法重做之前的命令了 + self._redo_stack.clear() + except Exception as e: + print(f"执行命令时出错: {e}") + raise + + def undo(self) -> bool: + """ + 撤销上一个命令 + 返回是否成功撤销 + """ + if not self._undo_stack: + return False + + try: + command = self._undo_stack.pop() + command.undo() + self._redo_stack.append(command) + return True + except Exception as e: + print(f"撤销命令时出错: {e}") + # 如果撤销失败,将命令放回撤销栈 + self._undo_stack.append(command) + return False + + def redo(self) -> bool: + """ + 重做上一个被撤销的命令 + 返回是否成功重做 + """ + if not self._redo_stack: + return False + + try: + command = self._redo_stack.pop() + command.redo() + self._undo_stack.append(command) + return True + except Exception as e: + print(f"重做命令时出错: {e}") + # 如果重做失败,将命令放回重做栈 + self._redo_stack.append(command) + return False + + def can_undo(self) -> bool: + """ + 检查是否可以撤销 + """ + return len(self._undo_stack) > 0 + + def can_redo(self) -> bool: + """ + 检查是否可以重做 + """ + return len(self._redo_stack) > 0 + + def clear_history(self): + """ + 清空所有历史记录 + """ + self._undo_stack.clear() + self._redo_stack.clear() + + def get_undo_count(self) -> int: + """ + 获取可撤销的命令数量 + """ + return len(self._undo_stack) + + def get_redo_count(self) -> int: + """ + 获取可重做的命令数量 + """ + return len(self._redo_stack) + + +# 示例命令实现 +class MoveNodeCommand(Command): + """ + 移动节点命令示例 + """ + + def __init__(self, node: NodePath, old_pos, new_pos): + self.node = node + self.old_pos = old_pos + self.new_pos = new_pos + + def execute(self): + """ + 执行移动操作 + """ + self.node.setPos(self.new_pos) + + def undo(self): + """ + 撤销移动操作 + """ + self.node.setPos(self.old_pos) + + def redo(self): + """ + 重做移动操作 + """ + self.node.setPos(self.new_pos) + + +class DeleteNodeCommand(Command): + """ + 删除节点命令示例 + """ + + def __init__(self, node: NodePath, parent_node: NodePath,world=None): + self.node = node + self.parent_node = parent_node + self.world = world + + self.node_name = node.getName() + self.node_pos = node.getPos() + self.node_hpr = node.getHpr() + self.node_scale = node.getScale() + + # 保存节点类型信息 + self.node_type = "NODE" + if node.hasTag("tree_item_type"): + self.node_type = node.getTag("tree_item_type") + elif node.hasTag("gui_type"): + gui_type = node.getTag("gui_type") + if gui_type == "button": + self.node_type = "GUI_BUTTON" + elif gui_type == "label": + self.node_type = "GUI_LABEL" + elif gui_type == "entry": + self.node_type = "GUI_ENTRY" + elif gui_type == "2d_image": + self.node_type = "GUI_IMAGE" + elif gui_type == "3d_text": + self.node_type = "GUI_3DTEXT" + elif gui_type == "3d_image": + self.node_type = "GUI_3DIMAGE" + elif gui_type == "video_screen": + self.node_type = "GUI_VIDEO_SCREEN" + elif gui_type == "2d_video_screen": + self.node_type = "GUI_2D_VIDEO_SCREEN" + elif node.hasTag("light_type"): + self.node_type = "LIGHT_NODE" + elif node.hasTag("element_type") and node.getTag("element_type") == "cesium_tileset": + self.node_type = "CESIUM_TILESET_NODE" + elif node.hasTag("is_scene_element"): + self.node_type = "SCENE_NODE" + + self.node_tags = {} + if hasattr(node,'hasTag'): + for tag_key in node.getNetTag('tags').split(',') if node.hasTag('tags') else []: + if node.hasTag(tag_key): + self.node_tags[tag_key] = node.getTag(tag_key) + else: + try: + if hasattr(node,'getTag'): + common_tags = ['is_scene_element','tree_item_type','gui_type','light_type', + 'element_type','file','model_path','video_path','image_path', + 'gui_text','name','created_by_user'] + for tag in common_tags: + if node.hasTag(tag): + self.node_tags[tag] = node.getTag(tag) + except: + pass + + self.node_python_tags = {} + if hasattr(node,'getPythonTagKeys'): + try: + for tag_key in node.getPythonTagKeys(): + self.node_python_tags[tag_key] = node.getPythonTag(tag_key) + except Exception as e: + pass + + # 对于特定类型的节点,保存额外的数据 + self.extra_data = {} + if self.node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN", "GUI_2D_VIDEO_SCREEN"]: + if node.hasTag("gui_text"): + self.extra_data["gui_text"] = node.getTag("gui_text") + if node.hasTag("video_path"): + self.extra_data["video_path"] = node.getTag("video_path") + if node.hasTag("image_path"): + self.extra_data["image_path"] = node.getTag("image_path") + elif self.node_type == "LIGHT_NODE": + if node.hasTag("light_type"): + self.extra_data["light_type"] = node.getTag("light_type") + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + self.extra_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 70, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else 0.4, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True), + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) + } + elif self.node_type == "CESIUM_TILESET_NODE": + if node.hasTag("tileset_url"): + self.extra_data["tileset_url"] = node.getTag("tileset_url") + + def execute(self): + """ + 执行删除操作 + """ + # 从world的相应列表中移除节点引用 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + if self.node_type == "LIGHT_NODE": + if self.node.hasTag("light_type"): + light_type = self.node.getTag("light_type") + if light_type == "spot_light" and hasattr(scene_manager, + 'Spotlight') and self.node in scene_manager.Spotlight: + scene_manager.Spotlight.remove(self.node) + elif light_type == "point_light" and hasattr(scene_manager, + 'Pointlight') and self.node in scene_manager.Pointlight: + scene_manager.Pointlight.remove(self.node) + elif self.node_type == "IMPORTED_MODEL_NODE" and hasattr(scene_manager, + 'models') and self.node in scene_manager.models: + scene_manager.models.remove(self.node) + elif self.node_type.startswith("GUI_") and hasattr(self.world, + 'gui_elements') and self.node in self.world.gui_elements: + self.world.gui_elements.remove(self.node) + elif self.node_type == "CESIUM_TILESET_NODE": + # 从tilesets列表中移除 + if hasattr(scene_manager, 'tilesets'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(scene_manager.tilesets): + if tileset_info.get('node') == self.node: + tilesets_to_remove.append(i) + for i in reversed(tilesets_to_remove): + del scene_manager.tilesets[i] + + # 从场景图中移除节点 + if self.node and not self.node.isEmpty(): + self.node.removeNode() + + def undo(self): + """ + 撤销删除操作(重新创建节点) + """ + try: + # 使用场景管理器重建节点 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + + # 创建节点数据字典 + node_data = { + 'name': self.node_name, + 'node_type': self.node_type, + 'pos': (self.node_pos.x, self.node_pos.y, self.node_pos.z), + 'hpr': (self.node_hpr.x, self.node_hpr.y, self.node_hpr.z), + 'scale': (self.node_scale.x, self.node_scale.y, self.node_scale.z), + 'tags': self.node_tags + } + + # 添加额外数据 + if self.extra_data: + if self.node_type.startswith("GUI_"): + node_data['gui_data'] = self.extra_data + elif self.node_type == "LIGHT_NODE": + node_data['light_data'] = self.extra_data.get('light_data', {}) + elif self.node_type == "CESIUM_TILESET_NODE": + node_data['tileset_url'] = self.extra_data.get('tileset_url', '') + + # 重建节点 + new_node = scene_manager.recreateNodeFromData(node_data, self.parent_node) + + if new_node: + print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") + # 更新节点引用 + self.node = new_node + else: + print(f"❌ 撤销删除操作失败,无法重建节点 {self.node_name}") + else: + print("❌ 无法撤销删除操作,缺少场景管理器引用") + + except Exception as e: + print(f"❌ 撤销删除操作时出错: {e}") + import traceback + traceback.print_exc() + + def redo(self): + """ + 重做删除操作 + """ + self.execute() + + +class RotateNodeCommand(Command): + """ + 旋转节点命令 + """ + + def __init__(self, node: NodePath, old_hpr, new_hpr): + self.node = node + self.old_hpr = old_hpr + self.new_hpr = new_hpr + + def execute(self): + """ + 执行旋转操作 + """ + self.node.setHpr(self.new_hpr) + + def undo(self): + """ + 撤销旋转操作 + """ + self.node.setHpr(self.old_hpr) + + def redo(self): + """ + 重做旋转操作 + """ + self.node.setHpr(self.new_hpr) + + +class ScaleNodeCommand(Command): + """ + 缩放节点命令 + """ + + def __init__(self, node: NodePath, old_scale, new_scale): + self.node = node + self.old_scale = old_scale + self.new_scale = new_scale + + def execute(self): + """ + 执行缩放操作 + """ + self.node.setScale(self.new_scale) + + def undo(self): + """ + 撤销缩放操作 + """ + self.node.setScale(self.old_scale) + + def redo(self): + """ + 重做缩放操作 + """ + self.node.setScale(self.new_scale) + + +class CreateNodeCommand(Command): + """ + 创建节点命令 + """ + + def __init__(self, node_creator_func,parent_node, *args, **kwargs): + self.node_creator_func = node_creator_func + self.parent_node = parent_node + self.args = args + self.kwargs = kwargs + self.created_node = None + + def execute(self): + """ + 执行创建节点操作 + """ + self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs) + return self.created_node + + def undo(self): + """ + 撤销创建节点操作 + """ + if self.created_node: + self.created_node.removeNode() + + def redo(self): + """ + 重做创建节点操作 + """ + self.execute() + + +class ReparentNodeCommand(Command): + """ + 重新设置节点父子关系命令 - 增强版(同时处理Panda3D和Qt树) + """ + + def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath, + old_parent_item=None, new_parent_item=None, is_2d_gui=False, world=None): + self.node = node + self.old_parent = old_parent + self.new_parent = new_parent + self.old_parent_item = old_parent_item # Qt树中的旧父节点项 + self.new_parent_item = new_parent_item # Qt树中的新父节点项 + self.is_2d_gui = is_2d_gui + self.world = world + + # 保存节点在操作前的世界坐标和局部坐标,以便在撤销/重做时保持位置不变 + self.world_pos = node.getPos(self.world.render if self.world else node.getParent()) + self.world_hpr = node.getHpr(self.world.render if self.world else node.getParent()) + self.world_scale = node.getScale(self.world.render if self.world else node.getParent()) + # 同时保存局部坐标,因为在父节点改变后可能需要恢复 + self.local_pos = node.getPos() + self.local_hpr = node.getHpr() + self.local_scale = node.getScale() + + def _updateQtTree(self, node_item, new_parent_item): + """更新Qt树控件中的节点位置""" + if not node_item or not new_parent_item: + return + + # 从当前父节点中移除 + current_parent = node_item.parent() + if current_parent: + current_parent.removeChild(node_item) + else: + # 如果是顶级项目 + tree_widget = node_item.treeWidget() + if tree_widget: + index = tree_widget.indexOfTopLevelItem(node_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) + + # 添加到新父节点 + new_parent_item.addChild(node_item) + + def execute(self): + """ + 执行重新父化操作 + """ + # 更新Panda3D节点父子关系 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + def undo(self): + """ + 撤销重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + # 恢复Panda3D节点父子关系 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.old_parent and not self.old_parent.isEmpty(): + if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1": + # 原父节点是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.old_parent) + else: + # 原父节点是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d下") + else: + # 如果原父节点为空,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d") + else: + # 普通3D节点的处理 + if self.old_parent and not self.old_parent.isEmpty(): + self.node.wrtReparentTo(self.old_parent) + else: + # 如果原父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + def redo(self): + """ + 重做重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + # 重新执行Panda3D节点父子关系更新 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + +class CompositeCommand(Command): + """ + 组合命令类,用于同时执行多个命令 + """ + def __init__(self,commands:List[Command]): + self.commands = commands + + def execute(self): + """ + 执行所有命令 + """ + for command in self.commands: + command.execute() + + def undo(self): + """ + 撤销所有命令(逆序执行) + """ + for command in reversed(self.commands): + command.undo() + + def redo(self): + """ + 重做所有命令 + """ + for command in self.commands: + command.redo() + +class MoveLightCommand(Command): + def __init__(self, node, old_pos, new_pos, light_object=None): + self.node = node + self.old_pos = Point3(old_pos) + self.new_pos = Point3(new_pos) + self.light_object = light_object + + def execute(self): # 将原来的 do() 改为 execute() + if self.light_object: + self.light_object.pos = self.new_pos + if self.node: + self.node.setPos(self.new_pos) + + def undo(self): + if self.light_object: + self.light_object.pos = self.old_pos + if self.node: + self.node.setPos(self.old_pos) + + def redo(self): + self.execute() # 调用 execute() 而不是 do() + + + diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py index 9363d567..786a4763 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -1,6 +1,8 @@ # 修改后的 InfoPanelManager.py from xml.sax.handler import property_encoding +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox from direct.gui.DirectGui import DirectFrame, DirectLabel from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import TextNode, Vec4, NodePath @@ -144,7 +146,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 +184,62 @@ 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("image_path", bg_image) + if not visible: panel_node.hide() + # 将面板添加到场景树 + #self._addPanelToSceneTree(panel_node, panel_id) + if hasattr(self.world, 'gui_elements'): + self.world.gui_elements.append(panel_node) + 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): """ 为指定面板设置背景图片 @@ -480,9 +532,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']: @@ -526,8 +576,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: @@ -758,11 +809,23 @@ class InfoPanelManager(DirectObject): # 设置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) + if hasattr(self.world, 'gui_elements'): + self.world.gui_elements.append(panel_node) + return panel_node def update3DPanelContent(self, panel_id, title=None, content=None): @@ -1208,6 +1271,101 @@ class InfoPanelManager(DirectObject): traceback.print_exc() return None + def onCreateSampleInfoPanel(self): + """创建示例天气信息面板(模拟数据)""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 使用唯一的面板ID + import time + unique_id = f"weather_info_{int(time.time())}" + + # 创建示例面板 + weather_panel = self.createInfoPanel( + panel_id=unique_id, # 使用唯一ID + 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, + bg_image="/home/tiger/图片/内部信息框2@2x.png" + ) + + # 更新面板标题 + self.updatePanelContent(unique_id, title="北京天气") + + self._addPanelToSceneTree(weather_panel, unique_id) + # 立即显示加载中信息 + self.updatePanelContent(unique_id, content="正在获取天气数据...") + + self.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) + + print("✓ 示例天气信息面板已创建") + + except Exception as e: + print(f"✗ 创建示例天气信息面板失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") + + def getRealWeatherData(self): + """获取真实天气数据""" + try: + import requests + import json + from datetime import datetime + + # 请求天气数据 + url = "https://wttr.in/Beijing?format=j1" + response = requests.get(url, timeout=10) + response.raise_for_status() + + # 解析JSON数据 + weather_data = response.json() + + # 提取当前天气信息 + current_condition = weather_data['current_condition'][0] + weather_desc = current_condition['weatherDesc'][0]['value'] + temp_c = current_condition['temp_C'] + feels_like = current_condition['FeelsLikeC'] + humidity = current_condition['humidity'] + pressure = current_condition['pressure'] + visibility = current_condition['visibility'] + wind_speed = current_condition['windspeedKmph'] + wind_dir = current_condition['winddir16Point'] + + # 提取空气质量(如果可用) + air_quality = "N/A" + if 'air_quality' in weather_data and weather_data['air_quality']: + if 'us-epa-index' in current_condition: + air_quality_index = current_condition['air_quality_index'] + air_quality = f"指数: {air_quality_index}" + + # 获取更新时间 + update_time = datetime.now().strftime("%Y-%m-%d %H:%M") + + # 格式化显示内容 + content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}" + + return content + + except requests.exceptions.Timeout: + return "错误: 获取天气数据超时" + except requests.exceptions.ConnectionError: + return "错误: 网络连接失败" + except requests.exceptions.HTTPError as e: + return f"HTTP错误: {e}" + except json.JSONDecodeError: + return "错误: 无法解析天气数据" + except KeyError as e: + return f"错误: 天气数据格式不正确 (缺少字段: {e})" + except Exception as e: + return f"获取天气数据失败: {str(e)}" + # 示例数据源函数 def getRealtimeData(): 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..89cb2ad9 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,10 +184,19 @@ 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: + highlighted_axis = self.world.selection.gizmoHighlightAxis + if highlighted_axis: + print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") + # 直接使用高亮轴开始拖拽 + self.world.selection.startGizmoDrag(highlighted_axis, x, y) + pickerNP.removeNode() + return + + # 如果没有高亮轴,再尝试检测点击 gizmoAxis = self.world.selection.checkGizmoClick(x, y) if gizmoAxis: print(f"✓ 检测到坐标轴点击: {gizmoAxis}") @@ -197,22 +206,32 @@ class EventHandler: return else: print("× 没有点击到坐标轴") + + # gizmoAxis = self.world.selection.checkGizmoClick(x, y) + # if gizmoAxis: + # #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + # # 开始坐标轴拖拽 + # self.world.selection.startGizmoDrag(gizmoAxis, x, y) + # pickerNP.removeNode() + # return + # else: + # print("× 没有点击到坐标轴") except Exception as e: print(f"❌ 坐标轴点击检测出现异常: {str(e)}") import traceback 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 +430,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 +463,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 6f160c1e..281a36aa 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, @@ -15,7 +16,6 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta from direct.task.TaskManagerGlobal import taskMgr import math - class SelectionSystem: """选择和变换系统类""" @@ -63,9 +63,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,11 +124,8 @@ class SelectionSystem: def createSelectionBox(self, nodePath): """为选中的节点创建选择框""" try: - print(f" 开始创建选择框,目标节点: {nodePath.getName()}") - - # 如果已有选择框,先移除 if self.selectionBox: - print(" 移除现有选择框") + #print(" 移除现有选择框") self.selectionBox.removeNode() self.selectionBox = None @@ -124,21 +133,12 @@ class SelectionSystem: print(" 目标节点为空,取消创建") return - # 创建选择框作为render的子节点,但会实时跟踪目标节点 self.selectionBox = self.world.render.attachNewNode("selectionBox") - self.selectionBoxTarget = nodePath # 保存目标节点引用 - print(f" 选择框节点创建完成: {self.selectionBox}") + self.selectionBoxTarget = nodePath - # 启动选择框更新任务 taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - print(" 选择框更新任务已启动") - - # 初始更新选择框 - print(" 开始初始化选择框几何体...") self.updateSelectionBoxGeometry() - print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") - except Exception as e: print(f" ✗ 创建选择框失败: {str(e)}") import traceback @@ -271,51 +271,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 +342,7 @@ class SelectionSystem: def createGizmo(self, nodePath): """为选中的节点创建坐标轴工具 - 保留箭头版本""" try: - print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") + #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") # 如果已有坐标轴,先移除 if self.gizmo: @@ -350,6 +357,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 +408,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 +454,7 @@ class SelectionSystem: else: gizmo_model = self.world.loader.loadModel(path) if gizmo_model: - print(f"成功加载模型: {path}") + #print(f"成功加载模型: {path}") break except: continue @@ -883,13 +906,6 @@ class SelectionSystem: # 创建或获取材质 mat = Material() - # # 设置材质属性 - 使用自发光确保在RenderPipeline下可见 - # mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) - # mat.setDiffuse(Vec4(0, 0, 0, 1)) - # #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光 - # mat.setEmission(Vec4(1,1,1,1.0)) # 自发光 - # mat.set_roughness(1) - # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 adjusted_color = Vec4( min(color[0]*20, 1.0), @@ -899,11 +915,7 @@ class SelectionSystem: ) mat.setBaseColor(adjusted_color) - # mat.setDiffuse(adjusted_color * 0.8) # 稍微降低漫反射亮度 - # mat.setAmbient(adjusted_color * 0.3) # 设置环境光反射 - # mat.setSpecular(Vec4(0.3, 0.3, 0.3, 1.0)) # 适度的镜面反射 - # mat.setShininess(25.0) # 适中的高光强度 - mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 # 应用材质 handle_node.setMaterial(mat, 1) @@ -999,7 +1011,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 +1189,7 @@ class SelectionSystem: return self.checkGizmoClickFallback(mouseX, mouseY) # 计算点击阈值 - click_threshold = 30 # 增大检测范围 + click_threshold = 15 # 增大检测范围 # 检测各个轴,对于端点在屏幕外的轴提供回退方案 def getClickDetectionPoint(axis_name, original_screen_pos): @@ -1206,10 +1218,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 @@ -1492,8 +1504,14 @@ class SelectionSystem: self.dragStartMousePos = (mouseX, mouseY) + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + self.gizmoTargetStartPos = Point3(light_object.pos) + else: + self.gizmoTargetStartPos = self.gizmoTarget.getPos() + # 保存开始拖拽时目标节点的位置和坐标轴的位置 - self.gizmoTargetStartPos = self.gizmoTarget.getPos() + #self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 # 添加对缩放的支持:保存初始缩放值 @@ -1657,14 +1675,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 +1828,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: @@ -1849,6 +1882,43 @@ class SelectionSystem: """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") + if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: + current_pos = self.gizmoTarget.getPos() + + if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and + (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or + abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or + abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): + from core.Command_System import MoveNodeCommand + from core.Command_System import MoveLightCommand + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object) + else: + command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) + self.world.command_manager.execute_command(command) + # 如果是缩放操作且缩放发生了变化,则创建缩放命令 + elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and + self.gizmoTargetStartScale): + current_scale = self.gizmoTarget.getScale() + if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or + abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or + abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): + from core.Command_System import ScaleNodeCommand + command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) + self.world.command_manager.execute_command(command) + # 如果是旋转操作且旋转发生了变化,则创建旋转命令 + elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and + self.gizmoTargetStartHpr): + current_hpr = self.gizmoTarget.getHpr() + if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or + abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or + abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): + from core.Command_System import RotateNodeCommand + command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) + self.world.command_manager.execute_command(command) + # 恢复所有轴的颜色 for axis_name in ["x", "y", "z"]: self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) @@ -1871,58 +1941,127 @@ 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") + # 如果正在删除节点,避免更新选择 + 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("× 坐标轴创建失败") + + 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("✓ 树形控件选中状态已清空") + + #print("=== 选择状态更新完成 ===\n") + except Exception as e: + print(f"更新选择状态失败{str(e)}") + import traceback + traceback.print_exc() + + def _reparentTreeItem(self, item, new_parent_item): + """将树项重新父化到新的父项下""" + if not item or not new_parent_item: 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}") + # 从当前父项中移除 + current_parent = item.parent() + if current_parent: + current_parent.removeChild(item) else: - print("清除选择...") - self.clearSelectionBox() - self.clearGizmo() - print("✓ 取消选择") + # 如果是顶级项 + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) - print("=== 选择状态更新完成 ===\n") + # 添加到新父项 + new_parent_item.addChild(item) + + def _updateSelectionVisuals(self, nodePath): + """更新选择的视觉效果(选择框和坐标轴)""" + try: + 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("✓ 取消选择") + + except Exception as e: + print(f"更新选择视觉效果失败: {e}") def getSelectedNode(self): """获取当前选中的节点""" @@ -2010,7 +2149,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 +2237,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 * 1, 1.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 6c7f0564..edbec218 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -104,6 +104,9 @@ class TerrainManager: 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) @@ -235,6 +238,9 @@ class TerrainManager: 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) 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 0538877f..4f0ef3a3 100644 --- a/core/world.py +++ b/core/world.py @@ -288,7 +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("is_scene_element", "1") + self.cam.setTag("tree_item_type", "CAMERA_NODE") print("✓ 相机设置完成") def _setupLighting(self): @@ -320,19 +321,71 @@ 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("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.ground2.setTag("is_scene_element", "1") + self.ground2.setTag("tree_item_type", "SCENE_NODE") + + # 创建第三个相同的地面,位置在另一个方向 + 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.ground3.setTag("is_scene_element", "1") + self.ground3.setTag("tree_item_type", "SCENE_NODE") + + 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.ground4.setTag("is_scene_element", "1") + self.ground4.setTag("tree_item_type", "SCENE_NODE") + + 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.ground5.setTag("is_scene_element", "1") + self.ground5.setTag("tree_item_type", "SCENE_NODE") + + 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) + self.ground6.setTag("is_scene_element", "1") + self.ground6.setTag("tree_item_type", "SCENE_NODE") + # 应用默认PBR效果,确保支持贴图 try: if hasattr(self, 'render_pipeline') and self.render_pipeline: @@ -349,6 +402,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未初始化,地板将使用基础渲染") @@ -438,12 +557,32 @@ class CoreWorld(Panda3DWorld): self.mouseRightPressed = True self.lastMouseX = evt['x'] self.lastMouseY = evt['y'] + # + # # 通过 Qt 窗口隐藏光标并捕获鼠标 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.setCursor(Qt.BlankCursor) + # # 捕获鼠标,使其无法离开窗口 + # self.qtWidget.grabMouse() + # except Exception as e: + # print(f"通过 Qt 隐藏光标时出错: {e}") def mouseReleaseEventRight(self, evt): """处理鼠标右键释放事件""" #print("右键释放") self.mouseRightPressed = False + # # 恢复 Qt 窗口光标并释放鼠标捕获 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.unsetCursor() # 恢复默认光标 + # # 释放鼠标捕获 + # self.qtWidget.releaseMouse() + # except Exception as e: + # print(f"恢复 Qt 光标时出错: {e}") + def mouseMoveEvent(self, evt): """处理鼠标移动事件 - 只处理相机旋转""" if not evt: 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 7b4aea60..ba9febdd 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元素管理系统类""" @@ -203,6 +205,7 @@ class GUIManager: button.setTag("gui_text", text) button.setTag("is_gui_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") @@ -319,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父节点,建立引用关系 @@ -412,9 +417,13 @@ 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, + pos=(pos[0],pos[1],pos[2]), scale=size, command=self.onGUIEntrySubmit, extraArgs=[f"entry_{len(self.gui_elements)}"], @@ -422,7 +431,15 @@ class GUIManager: numLines=1, width=12, focus=0, - parent=parent_gui_node # 设置GUI父节点 + frameColor = (0,0,0,0), + text_fg=(1,1,1,1), + text_align=TextNode.ACenter, + text_wordwrap=None, + rolloverSound=None, + clickSound=None, + parent=parent_gui_node, # 设置GUI父节点 + text_font = font, + frameSize=(-0.1,0.1,-0.05,0.05) ) # 设置节点标签 @@ -430,9 +447,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父节点,建立引用关系 @@ -483,7 +502,7 @@ class GUIManager: traceback.print_exc() return None - 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图片""" try: from direct.gui.DirectGui import DirectButton @@ -523,9 +542,17 @@ class GUIManager: parent_gui_node = None # 使用默认的aspect2d print(f"📎 挂载到3D父节点: {parent_item.text(0)}") - # 使用CardMaker创建一个更可靠的图片框架 - cm = CardMaker('gui-2d-image') - cm.setFrame(-size, size, -size, size) + if isinstance(size, (list, tuple)) and len(size) >= 2: + # 分别处理宽度和高度的缩放 + width_scale = size[0] * 0.25 + height_scale = size[2] * 0.25 + else: + # 如果只提供了一个缩放值,则使用相同值 + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-2d-image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) # image_node = self.world.aspect2d.attachNewNode(cm.generate()) if parent_gui_node: @@ -564,8 +591,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父节点,建立引用关系 @@ -715,7 +744,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) @@ -763,6 +794,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 @@ -851,7 +883,9 @@ class GUIManager: 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) @@ -968,7 +1002,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): @@ -1149,6 +1185,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 @@ -1159,9 +1196,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 @@ -1472,8 +1507,6 @@ class GUIManager: 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, - - ) video_screen.setName(screen_name) @@ -1487,10 +1520,10 @@ class GUIManager: 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("video_path", video_path if video_path else "") - print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}") + video_screen.setTag("name",screen_name) + video_screen.setTag("video_path",video_path or "") # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") @@ -1535,7 +1568,7 @@ class GUIManager: import traceback traceback.print_exc() else: - if video_path: + if not video_path: print(f"⚠️ 2D视频文件不存在: {video_path}") # 添加到GUI元素列表 @@ -1590,7 +1623,9 @@ class GUIManager: import os video_screen.setTag("video_path",video_path) - print(f"🔧 更新2D视频屏幕标签 - video_path: {video_path if video_path else '空'}") + path = video_screen.getTag("video_path") + print({video_screen.getTag("gui_type")}) + print(f"🔧 更新2D视频屏幕标签 - video_path: {path}") if not video_path or not os.path.exists(video_path): print(f"❌ 2D视频文件不存在: {video_path}") @@ -1798,6 +1833,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)) @@ -2010,6 +2046,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元素列表 @@ -2162,9 +2199,17 @@ class GUIManager: print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") if property_name == "text": + original_frame_size = None + if hasattr(gui_element,'getFrameSize'): + try: + original_frame_size = gui_element.getFrameSize() + except: + pass if gui_type in ["button", "label"]: gui_element['text'] = value print(f"成功更新2D GUI文本: {value}") + # if gui_type == "button": + # self._resizeButtonToText(gui_element,value,original_frame_size) elif gui_type == "entry": gui_element.set(value) print(f"成功更新输入框文本: {value}") @@ -2243,6 +2288,64 @@ class GUIManager: traceback.print_exc() return False + def _resizeButtonToText(self, button, text, original_frame_size=None): + """ + 根据文本内容调整按钮大小 + + Args: + button: DirectButton 对象 + text: 新的文本内容 + original_frame_size: 原始frameSize,用于计算合适的padding + """ + try: + # 获取按钮当前的文本缩放 + text_scale = 0.03 # 默认文本缩放 + padding = 0.2 # 默认边距 + + # 尝试从按钮获取实际的文本缩放值 + if hasattr(button, 'getTextScale'): + text_scale = button.getTextScale() + elif hasattr(button, 'textScale'): + text_scale = button.textScale + + # 根据原始frameSize计算合适的padding + if original_frame_size and len(original_frame_size) >= 4: + # 基于原始按钮宽度计算合适的padding + padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1) + + # 更精确的文本尺寸估算 + # 考虑中文字符和英文字符的不同宽度 + chinese_chars = len([c for c in text if ord(c) > 127]) + english_chars = len(text) - chinese_chars + + # 中文字符通常比英文字符宽 + char_width = text_scale * 0.6 + text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width) + text_height = text_scale * 1.2 # 文本高度 + + # 计算新的frameSize,确保有足够的边距 + half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3 + half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15 + + # 正确设置frameSize - 使用字典方式设置 + new_frame = (-half_width, half_width, -half_height, half_height) + button['frameSize'] = new_frame # 使用字典方式设置 + + print(f"按钮大小已调整: {new_frame} (文本: {text})") + + except Exception as e: + print(f"调整按钮大小失败: {e}") + # 如果自动调整失败,保持原有大小或设置一个合理的默认大小 + try: + if original_frame_size: + # 保持原有大小 + button['frameSize'] = original_frame_size + else: + # 设置一个合理的默认大小 + button['frameSize'] = (-0.5, 0.5, -0.15, 0.15) + except: + pass + def duplicateGUIElement(self, gui_element): """复制GUI元素""" try: 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 abcf1f99..d03ddd78 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ from demo.video_integration import VideoManager warnings.filterwarnings("ignore", category=DeprecationWarning) import sys -import builtins # 添加这一行 +import builtins from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) @@ -14,6 +14,8 @@ from PyQt5.QtCore import Qt, QDir, QUrl from PyQt5.QtGui import QDrag, QPainter, QPixmap from PyQt5.QtWidgets import QFileSystemModel from QPanda3D.QPanda3DWidget import QPanda3DWidget +from panda3d.core import loadPrcFileData +loadPrcFileData("", "assertions 0") from core.world import CoreWorld from core.selection import SelectionSystem from core.event_handler import EventHandler @@ -52,7 +54,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) @@ -80,7 +91,7 @@ class MyWorld(CoreWorld): # 初始化界面管理系统 self.interface_manager = InterfaceManager(self) - + # 启动脚本系统 self.script_manager.start_system() @@ -93,6 +104,8 @@ class MyWorld(CoreWorld): self.info_panel_manager = InfoPanelManager(self) + self.command_manager = CommandManager() + # 初始化碰撞管理器 from core.collision_manager import CollisionManager self.collision_manager = CollisionManager(self) @@ -107,7 +120,7 @@ class MyWorld(CoreWorld): self.vr_manager = None # 调试选项 - self.debug_collision = False # 是否显示碰撞体 + self.debug_collision = True # 是否显示碰撞体 # 默认启用模型间碰撞检测(可选) self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) @@ -227,17 +240,17 @@ 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) - def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)): + def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1)): """创建3D图片""" return self.gui_manager.createGUI3DImage(pos,text,size) - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1): + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2): """创建2D GUI图片""" - return self.gui_manager.createGUI2DImage(pos, image_path, size*0.2) + return self.gui_manager.createGUI2DImage(pos, image_path, size) def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None): """创建视频屏幕""" @@ -675,6 +688,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模块的对应功能 @@ -715,7 +819,7 @@ def run(args = None): # 使用新的UI模块创建主窗口 from ui.main_window import setup_main_window - + print(f'Path is {args}') app, main_window = setup_main_window(world, args) # 启动应用程序 diff --git a/project/project_manager.py b/project/project_manager.py index 5ed451ce..649beadd 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -79,7 +79,7 @@ class ProjectManager: # 自动保存初始场景 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: @@ -217,7 +217,7 @@ class ProjectManager: config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): if parent_window: - QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!") + QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}") else: print("警告: 选择的不是有效的项目文件夹!") return False @@ -302,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): @@ -392,197 +392,645 @@ class ProjectManager: # 复制场景文件到构建目录 shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam")) - + + # 复制Resources文件夹到build目录 + source_resources = os.path.join(project_path, "scenes", "resources") + self.copy_folder(source_resources, build_dir) + + self._saveGUIElementsToJSON(build_dir, project_path) + + self._copyScriptsToBuild(build_dir, project_path) + + self._copyScriptSystemToBuild(build_dir) + + source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile") + dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile") + + if os.path.exists(source_render_pipeline): + if os.path.exists(dest_render_pipeline): + shutil.rmtree(dest_render_pipeline) + + shutil.copytree( + source_render_pipeline, + dest_render_pipeline, + ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log') + ) + print("✓ RenderPipelineFile文件夹已复制到build目录") + else: + print("⚠️ RenderPipelineFile文件夹未找到") + # 创建标准的应用程序入口文件 self._createAppFile(build_dir, project_name) # 创建标准的setup.py文件 self._createStandardSetupFile(build_dir, project_name) - - def _createAppFile(self, build_dir, project_name): - """创建应用程序主文件""" - app_code = f'''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -{project_name} - Panda3D应用程序 -使用Panda3D引擎编辑器创建 -""" + #创建requirements.txt文件 + self._createRequirementsFile(build_dir) -import sys -import os -from direct.showbase.ShowBase import ShowBase -from panda3d.core import (loadPrcFileData, WindowProperties, AmbientLight, - DirectionalLight, Point3, Vec3) - -# 配置Panda3D -loadPrcFileData("", """ - win-size 1280 720 - window-title {project_name} - show-frame-rate-meter 1 - sync-video 1 - want-directtools #f - want-tk #f - audio-library-name p3openal_audio -""") - -class {project_name.replace(' ', '').replace('-', '')}App(ShowBase): - """应用程序主类""" - - def __init__(self): - ShowBase.__init__(self) - - print(f"启动 {project_name}...") - - # 设置窗口属性 - self.setupWindow() - - # 设置光照 - self.setupLighting() - - # 加载场景 - self.loadScene() - - # 设置相机控制 - self.setupControls() - - print("✓ 应用程序初始化完成") - - def setupWindow(self): - """设置窗口""" - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.2) - - # 设置窗口属性 - props = WindowProperties() - props.setTitle("{project_name}") - self.win.requestProperties(props) - - def setupLighting(self): - """设置光照系统""" - # 环境光 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - # 定向光(模拟太阳光) - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlight.setDirection(Vec3(-1, -1, -1)) - dlnp = self.render.attachNewNode(dlight) - self.render.setLight(dlnp) - - def loadScene(self): - """加载场景""" + def _copyScriptsToBuild(self, build_dir, project_path): + """复制脚本文件到构建目录的scripts文件夹""" try: - # 查找场景文件 - scene_file = "scene.bam" - if not os.path.exists(scene_file): - print("警告: 没有找到场景文件,创建默认场景") - self.createDefaultScene() - return - - # 加载场景 - scene = self.loader.loadModel(scene_file) - if scene: - scene.reparentTo(self.render) - print("✓ 场景加载成功") - - # 自动调整相机位置 - self.adjustCamera() + # 创建目标scripts目录 + scripts_dest = os.path.join(build_dir, "scripts") + + # 正确的源scripts目录路径 + scripts_src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts") + + # 如果上面的路径不存在,尝试项目路径下的scripts目录 + if not os.path.exists(scripts_src): + scripts_src = os.path.join(project_path, "scripts") + + if os.path.exists(scripts_src): + # 直接复制整个scripts目录 + if os.path.exists(scripts_dest): + shutil.rmtree(scripts_dest) + + shutil.copytree( + scripts_src, + scripts_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print("✓ Scripts目录已复制到build/scripts") else: - print("警告: 场景加载失败,创建默认场景") - self.createDefaultScene() - + # 创建空的scripts目录 + if not os.path.exists(scripts_dest): + os.makedirs(scripts_dest) + print("⚠️ 项目中没有scripts目录") + except Exception as e: - print(f"加载场景时出错: {{str(e)}}") - self.createDefaultScene() - - def createDefaultScene(self): - """创建默认场景""" - # 加载默认的环境模型 - env = self.loader.loadModel("models/environment") - if env: - env.reparentTo(self.render) - env.setScale(0.25) - env.setPos(-8, 42, 0) - - # 创建一个简单的立方体作为示例 - from panda3d.core import CardMaker - cm = CardMaker("ground") - cm.setFrame(-10, 10, -10, 10) - ground = self.render.attachNewNode(cm.generate()) - ground.setP(-90) - ground.setColor(0.5, 0.8, 0.5, 1) - - def adjustCamera(self): - """调整相机位置以查看场景""" - # 计算场景边界 - bounds = self.render.getBounds() - if bounds and not bounds.isEmpty(): - center = bounds.getCenter() - radius = bounds.getRadius() - - # 设置相机位置 - distance = radius * 3 - self.cam.setPos(center.x, center.y - distance, center.z + radius) - self.cam.lookAt(center) - else: - # 默认相机位置 - self.cam.setPos(0, -20, 5) - self.cam.lookAt(0, 0, 0) - - def setupControls(self): - """设置相机控制""" - # 启用鼠标控制 - self.accept("wheel_up", self.zoomIn) - self.accept("wheel_down", self.zoomOut) - - # 键盘控制说明 - print("\\n=== 控制说明 ===") - print("鼠标滚轮: 缩放") - print("ESC: 退出") - print("================\\n") - - # ESC键退出 - self.accept("escape", sys.exit) - - def zoomIn(self): - """放大""" - pos = self.cam.getPos() - lookAt = Point3(0, 0, 0) # 假设看向原点 - direction = (lookAt - pos).normalized() - newPos = pos + direction * 2 - self.cam.setPos(newPos) - - def zoomOut(self): - """缩小""" - pos = self.cam.getPos() - lookAt = Point3(0, 0, 0) # 假设看向原点 - direction = (lookAt - pos).normalized() - newPos = pos - direction * 2 - self.cam.setPos(newPos) + print(f"⚠️ 复制脚本文件时出错: {str(e)}") -def main(): - """主函数""" - try: - app = {project_name.replace(' ', '').replace('-', '')}App() - app.run() - except Exception as e: - print(f"应用程序启动失败: {{str(e)}}") - import traceback - traceback.print_exc() - input("按Enter键退出...") + def _copyScriptSystemToBuild(self,build_dir): + core_files = [ + "script_system.py", + "InfoPanelManager.py", + "CustomMouseController.py" + ] -if __name__ == "__main__": - main() -''' - + source_core_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"core") + + core_dest = os.path.join(build_dir,"core") + + if not os.path.exists(core_dest): + os.makedirs(core_dest) + + for file_name in core_files: + source_file = os.path.join(source_core_dir,file_name) + + if os.path.exists(source_file): + shutil.copy2(source_file,os.path.join(core_dest,file_name)) + + def _saveGUIElementsToJSON(self, build_dir, project_path): + """保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致""" + try: + # 创建目标gui目录 + gui_dest = os.path.join(build_dir, "gui") + if not os.path.exists(gui_dest): + os.makedirs(gui_dest) + + # 收集所有GUI元素信息 + gui_data = [] + + # 获取当前场景中的GUI元素 + if hasattr(self.world, 'gui_elements'): + for gui_node in self.world.gui_elements: + if gui_node and not gui_node.isEmpty(): + # 使用_collectGUIElementInfo方法收集信息 + gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node) + if gui_info: + gui_data.append(gui_info) + print(f"收集GUI元素信息: {gui_info['name']}") + + # 保存GUI信息到JSON文件 + gui_file_path = os.path.join(gui_dest, "gui_elements.json") + with open(gui_file_path, "w", encoding="utf-8") as f: + json.dump(gui_data, f, ensure_ascii=False, indent=4) + + print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素") + return True + + except Exception as e: + print(f"⚠️ 保存GUI元素时出错: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _createRequirementsFile(self,build_dir): + requirements_content = """panda3d>=1.10.13""" + + requirements_path = os.path.join(build_dir,"requirements.txt") + with open(requirements_path,"w",encoding="utf-8") as f: + f.write(requirements_content) + + def copy_folder(self, source_folder, destination_folder): + """将一个文件夹从源路径复制到目标路径下的resources文件夹中 + + Args: + source_folder (str): 源文件夹路径 + destination_folder (str): 目标文件夹路径 + """ + try: + # 创建resources文件夹作为目标 + resources_dest = os.path.join(destination_folder, "resources") + + # 确保目标目录存在 + if not os.path.exists(destination_folder): + os.makedirs(destination_folder) + + # 如果目标resources文件夹已存在,先删除 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + # 检查源文件夹是否存在 + if os.path.exists(source_folder): + # 复制整个文件夹到resources目录下 + shutil.copytree( + source_folder, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}") + return True + else: + print(f"⚠️ 源文件夹不存在: {source_folder}") + # 即使源文件夹不存在,也创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + return False + + except Exception as e: + print(f"⚠️ 复制文件夹时出错: {str(e)}") + return False + + def _copyResourcesToBuild(self, build_dir, project_path): + """复制GUI资源到构建目录的resources文件夹""" + try: + # 创建目标resources目录 + resources_dest = os.path.join(build_dir, "resources") + + # 源Resources目录 + resources_src = os.path.join(project_path, "Resources") + + if os.path.exists(resources_src): + # 直接复制整个Resources目录 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + shutil.copytree( + resources_src, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print("✓ Resources目录已复制到build/resources") + + # 统计复制的文件数量 + file_count = 0 + for root, dirs, files in os.walk(resources_dest): + file_count += len(files) + print(f"✓ 共复制了 {file_count} 个资源文件") + else: + # 创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + print("⚠️ 项目中没有Resources目录") + + except Exception as e: + print(f"⚠️ 复制资源文件时出错: {str(e)}") + + def _collectResourceFiles(self, project_path): + """收集项目中GUI使用的资源文件""" + resource_files = set() + + try: + # 收集Resources目录中的所有文件(这是最主要的资源来源) + resources_dir = os.path.join(project_path, "Resources") + if os.path.exists(resources_dir): + for root, dirs, files in os.walk(resources_dir): + for file in files: + file_path = os.path.join(root, file) + # 收集所有文件,不仅仅是媒体文件 + resource_files.add(file_path) + + # 同时收集场景中引用的特定资源 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 从场景文件中提取资源引用 + referenced_files = self._extractResourcesFromScene(scene_file, project_path) + for file_path in referenced_files: + if os.path.isabs(file_path): + if os.path.exists(file_path): + resource_files.add(file_path) + else: + # 相对路径 + full_path = os.path.join(project_path, file_path) + if os.path.exists(full_path): + resource_files.add(full_path) + + except Exception as e: + print(f"收集资源文件时出错: {str(e)}") + + return list(resource_files) + + def _extractResourcesFromScene(self, scene_file, project_path): + """从场景文件中提取资源引用""" + referenced_files = [] + + try: + # 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑 + # 由于直接解析BAM文件比较复杂,我们采用间接方式 + + # 检查项目配置文件或其他元数据文件中可能包含的资源引用 + config_file = os.path.join(project_path, "project.json") + if os.path.exists(config_file): + try: + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 如果配置中有资源列表信息,可以在这里处理 + # 这里暂时保持简单实现 + except Exception as e: + print(f"读取项目配置时出错: {str(e)}") + + except Exception as e: + print(f"从场景提取资源引用时出错: {str(e)}") + + return referenced_files + + def _isMediaFile(self, file_path): + """判断是否为媒体文件(图片或视频)""" + media_extensions = { + # 图片格式 + '.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff', + # 视频格式 + '.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv' + } + + _, ext = os.path.splitext(file_path.lower()) + return ext in media_extensions + + def _createAppFile(self, build_dir, project_name): + """创建应用程序主文件 - 通过复制模板文件""" + # 获取模板文件路径(假设模板文件在项目根目录下的templates文件夹中) + template_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "templates", "main_template.py") + + # 目标文件路径 app_path = os.path.join(build_dir, "main.py") - with open(app_path, "w", encoding="utf-8") as f: - f.write(app_code) - + + # 检查模板文件是否存在 + if os.path.exists(template_path): + # 直接复制模板文件 + shutil.copy2(template_path, app_path) + print(f"✓ 应用程序主文件已从模板创建: {app_path}") + + # def _createAppFile(self, build_dir, project_name): +# """创建应用程序主文件""" +# app_code = f'''#!/usr/bin/env python3 +# # -*- coding: utf-8 -*- +# +# """ +# {project_name} - Panda3D应用程序 +# 使用Panda3D引擎编辑器创建 +# """ +# +# from __future__ import print_function +# +# import json +# +# from direct.actor.Actor import Actor +# from panda3d.core import TextNode, CardMaker, TextureStage, NodePath +# #获取渲染管线路径 +# import sys +# import os +# +# render_pipeline_path = 'RenderPipelineFile' +# project_root = os.path.dirname(os.path.abspath(__file__)) +# sys.path.insert(0,project_root) +# sys.path.insert(0,render_pipeline_path) +# +# import math +# from random import random,randint,seed +# from panda3d.core import Vec3,load_prc_file_data,Filename +# from direct.showbase.ShowBase import ShowBase +# +# os.chdir(os.path.dirname(os.path.realpath(__file__))) +# +# class MainApp(ShowBase): +# def __init__(self): +# load_prc_file_data("",""" +# win-size 1200 720 +# window-title Render +# """) +# +# pipeline_path = "../../" +# +# if not os.path.isfile(os.path.join(pipeline_path,"setup.py")): +# pipeline_path = "../../RenderPipeline" +# +# sys.path.insert(0,pipeline_path) +# +# from rpcore import RenderPipeline,SpotLight +# self.render_pipeline = RenderPipeline() +# self.render_pipeline.create(self) +# +# from rpcore.util.movement_controller import MovementController +# +# self.render_pipeline.daytime_mgr.time = "12:00" +# self._loadFont() +# +# self.loadFullScene() +# self.loadGUIFromJSON() +# +# self.controller = MovementController(self) +# self.controller.set_initial_position( +# Vec3(-7.5,-5.3,1.8),Vec3(-5.9,-4.0,1.6)) +# self.controller.setup() +# +# base.accept("l",self.tour) +# +# def _loadFont(self): +# """加载中文字体""" +# self.chinese_font = None +# try: +# self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') +# if not self.chinese_font: +# print("警告: 无法加载中文字体,将使用默认字体") +# else: +# print("✓ 中文字体加载成功") +# except: +# print("警告: 无法加载中文字体,将使用默认字体") +# self.chinese_font = None +# +# def getChineseFont(self): +# """获取中文字体""" +# return self.chinese_font +# +# def loadFullScene(self): +# """加载完整场景,包括所有元素""" +# try: +# scene_file = "scene.bam" +# if os.path.exists(scene_file): +# # 使用readBamFile加载完整场景 +# from panda3d.core import BamCache +# BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 +# +# scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) +# if scene: +# scene.reparentTo(self.render) +# self.render_pipeline.prepare_scene(scene) +# print("✓ 完整场景加载成功") +# +# # 处理场景中的各种元素 +# self.processSceneElements(scene) +# else: +# print("⚠️ 场景文件加载失败") +# else: +# print("⚠️ 未找到场景文件") +# except Exception as e: +# print(f"加载完整场景时出错: {{str(e)}}") +# import traceback +# traceback.print_exc() +# +# def processSceneElements(self, scene): +# """处理场景中的各种元素""" +# try: +# # 处理光源 +# self.processLights(scene) +# +# # 处理GUI元素 +# self.processGUIElements(scene) +# +# except Exception as e: +# print(f"处理场景元素时出错: {{str(e)}}") +# +# def processLights(self, scene): +# """处理场景中的光源""" +# try: +# # 查找并处理点光源 +# point_lights = scene.findAllMatches("**/=element_type=point_light") +# for light_node in point_lights: +# try: +# from RenderPipelineFile.rpcore import PointLight +# 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()) +# self.render_pipeline.add_light(light) +# print(f"✓ 点光源 {{light_node.getName()}} 恢复成功") +# except Exception as e: +# print(f"恢复点光源 {{light_node.getName()}} 失败: {{str(e)}}") +# +# # 查找并处理聚光灯 +# spot_lights = scene.findAllMatches("**/=element_type=spot_light") +# for light_node in spot_lights: +# try: +# from RenderPipelineFile.rpcore import SpotLight +# light = SpotLight() +# +# # 恢复光源属性 +# 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()) +# self.render_pipeline.add_light(light) +# print(f"✓ 聚光灯 {{light_node.getName()}} 恢复成功") +# except Exception as e: +# print(f"恢复聚光灯 {{light_node.getName()}} 失败: {{str(e)}}") +# +# except Exception as e: +# print(f"处理光源时出错: {{str(e)}}") +# +# def processGUIElements(self, scene): +# """处理场景中的GUI元素""" +# try: +# # 查找并处理2D图像 +# images_2d = scene.findAllMatches("**/=gui_type=image_2d") +# for img_node in images_2d: +# try: +# # GUI元素通常在场景加载时自动处理 +# print(f"✓ 2D图像 {{img_node.getName()}} 已加载") +# except Exception as e: +# print(f"处理2D图像 {{img_node.getName()}} 失败: {{str(e)}}") +# +# except Exception as e: +# print(f"处理GUI元素时出错: {{str(e)}}") +# +# def tour(self): +# mopath = ( +# (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), +# (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), +# (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), +# (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), +# (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), +# (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), +# ) +# self.controller.play_motion_path(mopath,3.0) +# +# def loadGUIFromJSON(self): +# gui_json_path = "gui/gui_elements.json" +# +# try: +# if os.path.exists(gui_json_path): +# with open(gui_json_path, "r", encoding="utf-8") as f: +# content = f.read().strip() +# if content: +# gui_data = json.loads(content) +# self.createGUIElement(gui_data) +# except Exception as e: +# print(f"加载GUI元素失败: {{str(e)}}") +# import traceback +# traceback.print_exc() +# +# def createGUIElement(self,element_data): +# try: +# processed_names = set() +# element_original_data={{}} +# for i, gui_info in enumerate(element_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 element_data: +# name = gui_info.get("name", f"gui_element_{{gui_info.get('index', 0)}}") +# valid_parents.add(name) +# +# for i ,gui_info in enumerate(element_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","") +# parent_name = gui_info.get("parent_name") +# +# if name in processed_names: +# continue +# +# processed_names.add(name) +# +# 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] +# +# new_element = None +# +# if gui_type =="3d_text": +# size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 +# new_element = self.createGUI3DText( +# pos = tuple(absolute_position), +# text = text, +# size = size +# ) +# elif gui_type == "button": +# # 确保传入正确的参数类型 +# new_element = self.createGUIButton( +# pos=tuple(absolute_position), +# text=text, +# size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, +# ) +# except Exception as e: +# print(f"重建GUI元素失败 {{name}}: {{e}}") +# import traceback +# traceback.print_exc() +# continue +# except Exception as e: +# print(f"重建GUI元素失败: {{str(e)}}") +# +# def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1,command=None): +# from direct.gui.DirectGui import DirectButton +# +# button = DirectButton( +# text=text, +# pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 +# scale=size, # size 应该是数值而不是元组 +# frameColor=(0.2, 0.6, 0.8, 1), +# text_font=self.getChineseFont() if self.getChineseFont() else None, +# rolloverSound=None, +# clickSound=None, +# parent=None, +# command=command +# ) +# +# def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): +# """创建3D文本GUI元素""" +# try: +# # 创建文本节点 +# text_node = TextNode("gui_3d_text") +# text_node.setText(text) +# text_node.setAlign(TextNode.ACenter) +# +# # 设置字体(如果可用) +# if self.getChineseFont(): +# text_node.setFont(self.getChineseFont()) +# +# # 创建节点路径并添加到场景 +# text_np = self.render.attachNewNode(text_node) +# +# # 设置位置和大小 +# text_np.setPos(Vec3(pos[0], pos[1], pos[2])) +# text_np.setScale(size) +# +# # 设置面向摄像机 +# #text_np.setBillboardPointEye() +# +# # 设置渲染属性 +# text_np.setBin("fixed", 40) +# text_np.setDepthWrite(False) +# +# return text_np +# except Exception as e: +# print(f"❌ 创建3D文本失败: {{str(e)}}") +# import traceback +# traceback.print_exc() +# return None +# +# MainApp().run() +# ''' +# +# app_path = os.path.join(build_dir, "main.py") +# with open(app_path, "w", encoding="utf-8") as f: +# f.write(app_code) + def _createStandardSetupFile(self, build_dir, project_name): """创建标准的setup.py文件 - 按照Panda3D官方文档""" setup_code = f'''#!/usr/bin/env python3 @@ -644,8 +1092,31 @@ setup( 'direct.task', 'direct.actor', 'direct.interval', + 'direct.stdpy.file', + 'direct.stdpy.pickle', 'panda3d.core', 'panda3d.direct', + 'rpcore', + 'rpcore.util.movement_controller', + 'rpcore.native', + 'rpcore.render_pipeline', + 'rplibs', + 'rpplugins', + 'rpplugins.scattering', + 'rpplugins.pssm', + 'rpplugins.godrays', + 'json', + 'os', + 'sys', + 'six', + 'collections', + 'collections.abs', + 'weakref', + 'copy', + 'itertools', + 'importlib', + 'importlib.util', + 'importlib.machinery', ], }}, diff --git a/scene/scene_manager.py b/scene/scene_manager.py index fa4bc075..fd9fadc0 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -7,12 +7,15 @@ """ import os +import shutil +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 @@ -22,6 +25,7 @@ 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: @@ -101,18 +105,26 @@ class SceneManager: 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: @@ -127,7 +139,7 @@ class SceneManager: # 总是重新加载模型以确保材质信息完整 # 不使用ModelPool缓存,避免材质信息丢失问题 - print("直接从文件加载模型...") + #print("直接从文件加载模型...") model = self.world.loader.loadModel(filepath) if not model: print("加载模型失败") @@ -135,10 +147,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) @@ -146,38 +166,30 @@ class SceneManager: model.setTag("converted_from", os.path.splitext(original_filepath)[1]) model.setTag("converted_to_glb", "true") - # file_extension = os.path.splitext(filepath)[1].lower() - # if file_extension == '.fbx': - # print("应用FBX特定缩放 (0.01)...") - # self._applyModelScale(model, 0.01) - # model.setTag("format_scale_applied", "fbx_0.01") - # current_hpr = model.getHpr() - # model.setHpr(current_hpr.getX(), 90, current_hpr.getZ()) - # elif file_extension == '.glb': - # print("应用GLB特定缩放 (100)...") - # self._applyModelScale(model, 100.0) - # model.setTag("format_scale_applied", "glb_100") - # else: - # print(f"应用默认缩放 (1.0) 到 {file_extension} 格式...") - # self._applyModelScale(model, 1.0) - # model.setTag("format_scale_applied", f"{file_extension}_1.0") + #特殊处理FBX模型 + if filepath.lower().endswith('.fbx'): + print("检测到FBX模型,应用特殊处理...") - # 可选的单位转换(主要针对FBX) - # if apply_unit_conversion and filepath.lower().endswith('.fbx'): - # print("应用FBX单位转换(厘米到米)...") - # self._applyUnitConversion(model, 0.01) + # 将模型缩放设置为原来的1/100 + model.setScale(0.01) + print("设置模型缩放为 0.01 (原始大小的1/100)") - # # 可选的单位转换(主要针对FBX) + # 设置模型旋转为 (0, 90, 0) + model.setHpr(0, 90, 0) + print("设置模型旋转为 (0, 90, 0)") + + # # 可选的单位转换(主要针对FBX # if apply_unit_conversion and filepath.lower().endswith('.fbx'): - # print("应用FBX单位转换(厘米到米)...") + # #print("应用FBX单位转换(厘米到米)...") # self._applyUnitConversion(model, 0.01) # # # 智能缩放标准化(处理FBX子节点的大缩放值) # if normalize_scales and filepath.lower().endswith('.fbx'): - # print("标准化FBX模型缩放层级...") + # #print("标准化FBX模型缩放层级...") # self._normalizeModelScales(model) # 调整模型位置到地面 + model.setPos(0,0,0) #self._adjustModelToGround(model) # 创建并设置基础材质 @@ -192,6 +204,7 @@ 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: @@ -217,9 +230,9 @@ class SceneManager: 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.setCurrentItem(qt_item) # 更新选择和属性面板 - tree_widget.update_selection_and_properties(model, qt_item) + #tree_widget.update_selection_and_properties(model, qt_item) print("✅ Qt树节点添加成功") else: print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") @@ -234,6 +247,118 @@ 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): """应用模型特定缩放 @@ -269,11 +394,11 @@ class SceneManager: 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() # 检查所有几何体的状态 @@ -285,40 +410,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()) @@ -327,12 +435,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 # 检查颜色属性 @@ -341,7 +449,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}") @@ -349,17 +457,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)) # 设置其他材质属性 @@ -368,15 +485,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) @@ -386,7 +508,7 @@ class SceneManager: continue # 应用材质 - print("\n开始递归应用材质...") + #print("\n开始递归应用材质...") try: apply_material(model) except Exception as e: @@ -396,7 +518,7 @@ class SceneManager: def _adjustModelToGround(self, model): """智能调整模型到地面,但保持原有缩放结构""" try: - print("调整模型位置到地面...") + #print("调整模型位置到地面...") # 获取模型的边界框 bounds = model.getBounds() @@ -416,9 +538,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)}") @@ -448,6 +570,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() @@ -653,6 +787,7 @@ class SceneManager: def setupCollision(self, model): """为模型设置碰撞检测(增强版本)""" try: + # 创建碰撞节点 cNode = CollisionNode(f'modelCollision_{model.getName()}') @@ -748,6 +883,11 @@ class SceneManager: 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()}") @@ -757,10 +897,20 @@ class SceneManager: "name": gui_node.getName(), "type": gui_type, "position": list(gui_node.getPos()), + "rotation": list(gui_node.getHpr()), "scale": list(gui_node.getScale()), - "tags": {} + "tags": {}, + "parent_name":None, + "video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None, + "panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else 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(): @@ -802,45 +952,51 @@ class SceneManager: 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 == "video_screen": - if hasattr(gui_node,'hasTag') and gui_node.hasTag("video_path"): + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): gui_info["video_path"] = gui_node.getTag("video_path") elif gui_type == "2d_video_screen": if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): gui_info["video_path"] = gui_node.getTag("video_path") - else: - print(f"无法保存2D视频屏幕: {gui_node.getName()}") - if hasattr(gui_node,'hasTag'): - video_path = gui_node.getTag("video_path") - try: - print(f"正在保存2D视频路径: {video_path}") - except Exception as e: - print(f"保存2D视频屏幕失败: {e}") - 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 == "info_panel": - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_data"): - gui_info["panel_data"] = gui_node.getTag("panel_data") + 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("image_path"): + gui_info["image_path"] = gui_node.getTag("image_path") + + + # 收集挂载的脚本信息 if hasattr(self.world, 'script_manager') and self.world.script_manager: - 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: - script_name = script_component.script_name # 使用脚本组件的名称 - # 获取脚本类的文件路径 - script_class = script_component.script_instance.__class__ - try: - script_file = inspect.getfile(script_class) - except: - script_file = "" - gui_info["scripts"].append({ - "name": script_name, - "file": script_file - }) + try: + script_manager = self.world.script_manager + scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node + if scripts: + gui_info["scripts"] = [] + for script_component in scripts: + try: + script_name = script_component.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["scripts"].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 @@ -850,7 +1006,75 @@ class SceneManager: traceback.print_exc() return None - def saveScene(self, filename): + 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} ===") @@ -863,6 +1087,10 @@ class SceneManager: if directory and not os.path.exists(directory): os.makedirs(directory) + resources_dir = os.path.join(directory,"resources") + if not os.path.exists(resources_dir): + os.makedirs(resources_dir) + # 存储需要临时隐藏的节点,以便保存后恢复 nodes_to_restore = [] @@ -910,13 +1138,79 @@ class SceneManager: # 创建用于保存GUI信息的JSON文件路径 gui_info_file = filename.replace('.bam', '_gui.json') + print(self.world.gui_elements) # 收集GUI元素信息(排除3D文本和3D图像) gui_data = [] + copied_resources = {} for gui_node in gui_elements: gui_info = self._collectGUIElementInfo(gui_node) if gui_info: + gui_type = gui_info.get("type","") + #处理2d图片 + if gui_type =="2d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + if original_path and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir,resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path,new_path) + copied_resources[original_path] = new_path + print(f"复制图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理3D图片 + elif gui_type == "3d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制3D图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制3D图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理背景图片 + if "bg_image_path" in gui_info and gui_info["bg_image_path"]: + original_path = gui_info["bg_image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制背景图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制背景图片资源失败: {original_path}, 错误: {e}") + gui_info["bg_image_path"] = new_path + + # 处理视频资源 + if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info: + original_path = gui_info["video_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制视频资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制视频资源失败: {original_path}, 错误: {e}") + gui_info["video_path"] = new_path + gui_data.append(gui_info) - print(f"添加GUI信息{gui_info['name']}") + print(f"添加GUI信息: {gui_info['name']}") # 保存GUI信息到JSON文件(确保即使没有GUI元素也创建有效的空JSON数组) try: @@ -929,16 +1223,6 @@ class SceneManager: 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(): @@ -950,6 +1234,22 @@ class SceneManager: 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 = node.getState() @@ -988,7 +1288,6 @@ class SceneManager: 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标签数据 @@ -1008,11 +1307,36 @@ class SceneManager: 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.fromOsSpecific(filename)) @@ -1040,6 +1364,51 @@ 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: @@ -1112,17 +1481,27 @@ class SceneManager: print("场景加载失败") return False - # tree_widget.create_model_items(scene) + 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()} (类型: {type(nodePath.node()).__name__})") + #存储节点以便后续处理父子关系 + loaded_nodes[nodePath.getName()] = nodePath + + if nodePath.getName().startswith('ground'): + print(f"{indent}跳过ground节点: {nodePath.getName()}") + return + # 跳过render节点的递归 if nodePath.getName() == "render" and depth > 0: print(f"{indent}跳过重复的render节点") @@ -1216,6 +1595,51 @@ class SceneManager: nodePath.setScale(scale) print(f"{indent}恢复缩放: {scale}") + 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)} 个脚本") + + script_manager = self.world.script_manager + for script_info in scripts_info: + 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_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""" @@ -1305,7 +1729,17 @@ class SceneManager: # 为模型节点设置碰撞检测 if nodePath.hasTag("is_model_root"): - self.setupCollision(nodePath) + 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) # 递归处理子节点 @@ -1315,6 +1749,10 @@ 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): @@ -1345,8 +1783,8 @@ class SceneManager: scene.removeNode() # 更新场景树 - self.updateSceneTree() - # self._get_tree_widget().create_model_items(scene) + #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: @@ -1364,19 +1802,120 @@ 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): + """判断节点是否应该在场景树中跳过显示""" + + if nodePath.getName().startswith('ground'): + return True + + # 跳过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") @@ -1386,13 +1925,21 @@ class SceneManager: tags = gui_info.get("tags", {}) text = gui_info.get("text", "") image_path = gui_info.get("image_path", "") - video_path = gui_info.get("video_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})") @@ -1400,84 +1947,135 @@ class SceneManager: print(f" 缩放: {scale}") print(f" 文本: {text}") print(f" 图像路径: {image_path}") - print(f"视频路径:{video_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(position), + pos=tuple(absolute_position), text=text, - size=scale[0] if scale and len(scale) > 0 else 1.0 + 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 = scale[0] if scale and len(scale) > 0 else 1.0 + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 new_element = gui_manager.createGUILabel( - pos=tuple(position), + 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(position), + pos=tuple(absolute_position), placeholder=text, - size=scale[0] if scale and len(scale) > 0 else 1.0 + 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 = scale[0] if scale and len(scale) > 0 else 0.2 + scale_value = absolute_scale[0] + print(f"2d_image{scale_value}") new_element = gui_manager.createGUI2DImage( - pos=tuple(position), + pos=tuple(absolute_position), image_path=image_path, - size=scale_value*0.2 + size=absolute_scale ) - elif gui_type == "3d_text" and hasattr(gui_manager,'createGUI3DText'): - size = scale[0] if scale and len(scale) > 0 else 0.5 + 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(position), + pos=tuple(absolute_position), text=text, size=size ) elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): # 处理3D图像 # 根据缩放值的数量处理尺寸 - if len(scale) >= 3: - size = (scale[0] * 2, scale[1] * 2) - elif len(scale) >= 2: - size = (scale[0] * 2, scale[1] * 2) + 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 = (scale[0] * 2, scale[0] * 2) + size = (absolute_scale[0] * 2, absolute_scale[0] * 2) else: size = (1.0, 1.0) new_element = gui_manager.createGUI3DImage( - pos=tuple(position), + pos=tuple(absolute_position), image_path=image_path, size=size ) - elif gui_type == "video_screen" and hasattr(gui_manager,'createVideoScreen'): + elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'): new_element = gui_manager.createVideoScreen( - pos=tuple(position), - size=scale, + pos=tuple(absolute_position), + size=absolute_scale, video_path=video_path ) - 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 + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + from direct.task.TaskManagerGlobal import taskMgr - taskMgr.doMethodLater(0.1, load_video_task, 'loadVideoTask') + def load_video_stream_task(task): + if hasattr(property_manager, '_loadVideoFromURLWithOpenCV_3D'): + property_manager._loadVideoFromURLWithOpenCV_3D(new_element, video_path) + return task.done - elif gui_type == "2d_video_screen" and hasattr(gui_manager,'createGUI2DVideoScreen'): + 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') + + elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): new_element = gui_manager.createGUI2DVideoScreen( - pos=tuple(position), - size=scale, + 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') + elif gui_type == "info_panel": + new_element = self.world.info_panel_manager.onCreateSampleInfoPanel() # 如果创建成功,设置属性 if new_element: # 如果返回的是列表(多选创建),取第一个 @@ -1506,21 +2104,7 @@ class SceneManager: elif hasattr(new_element, '_tags'): new_element._tags.update(tags) - # 重新挂载脚本(如果有的话) - 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"] - # 检查脚本是否已加载,如果没有则尝试加载 - if script_name not in script_manager.loader.script_classes: - script_file = script_info.get("file", "") - if script_file and os.path.exists(script_file): - script_manager.load_script_from_file(script_file) - - # 为元素添加脚本 - script_manager.add_script_to_object(new_element, script_name) - + created_elements[name] = new_element print(f"GUI元素重建成功: {name}") else: @@ -1532,276 +2116,237 @@ class SceneManager: traceback.print_exc() continue - print("GUI元素重建完成") + # 第二遍:设置父子级关系并更新Qt树 + print("开始设置父子级关系...") + try: + # 创建父子级关系映射 + parent_child_map = {} + for gui_info in gui_data: + name = gui_info.get("name") + parent_name = gui_info.get("parent_name") - except Exception as e: - print(f"重建GUI元素时发生错误: {e}") - import traceback - traceback.print_exc() + if name and parent_name and parent_name in created_elements: + parent_child_map[name] = parent_name + print(f"父子级关系映射: {parent_name} -> {name}") - def _recreate3DText(self, text_node): - """重新创建3D文本元素 - 使用gui_manager中的createGUI3DText方法""" - try: - print(f"重新创建3D文本: {text_node.getName()}") + # 按正确的顺序设置父子级关系并更新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 - # 获取保存的文本内容 - text_content = "默认文本" - if text_node.hasTag("gui_text"): - text_content = text_node.getTag("gui_text") - elif text_node.hasTag("text"): - text_content = text_node.getTag("text") + # 然后设置父子级关系 + 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] - print(f"3D文本内容: {text_content}") + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") - # 获取位置 - pos = (0, 0, 0) - if text_node.hasTag("transform_pos"): - try: - pos_str = text_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: - pos = (pos_values[0], pos_values[1], pos_values[2]) - except Exception as e: - print(f"恢复位置失败: {e}") + # 更新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] - # 获取尺寸 - size = 0.5 - if text_node.hasTag("transform_scale"): - try: - scale_str = text_node.getTag("transform_scale") - scale_str = scale_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') - scales = [float(x.strip()) for x in scale_str.split(',')] - size = scales[0] if scales else 0.5 - except Exception as e: - print(f"恢复缩放失败: {e}") + # 从当前位置移除子项 + if child_item.parent(): + child_item.parent().removeChild(child_item) + else: + # 如果是顶级项,从树中移除 + index = tree_widget.indexOfTopLevelItem(child_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) - # 使用gui_manager中的createGUI3DText方法创建新的3D文本 - # 首先需要找到gui_manager - gui_manager = None - if hasattr(self.world, 'gui_manager'): - gui_manager = self.world.gui_manager - - if gui_manager and hasattr(gui_manager, 'createGUI3DText'): - # 记录创建前的GUI元素数量 - original_count = len(gui_manager.gui_elements) - - # 调用gui_manager的createGUI3DText方法创建新的3D文本 - new_text_node = gui_manager.createGUI3DText(pos=pos, text=text_content, size=size) - - if new_text_node: - # 如果返回的是列表(多选创建),取第一个 - if isinstance(new_text_node, list): - created_node = new_text_node[0] - else: - created_node = new_text_node - - print(f"3D文本重建完成: {created_node.getName()}, 内容: '{text_content}'") - return created_node + # 将子项添加到新的父项下 + 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: - print("gui_manager.createGUI3DText返回None") + # 如果没有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"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() + continue + + print(f"GUI元素重建完成,共创建 {len(created_elements)} 个元素") except Exception as e: - print(f"重新创建3D文本失败: {str(e)}") + print(f"重建GUI元素时出错: {e}") import traceback traceback.print_exc() - return None - def _recreate3DImage(self, image_node): - """重新创建3D图像元素 - 使用gui_manager中的createGUI3DImage方法""" + def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name): + """在Qt树中查找或创建指定元素对应的项""" try: - print(f"重新创建3D图像: {image_node.getName()}") + # 首先尝试查找现有的项 + existing_item = self._findQtTreeItem(tree_widget, target_element) + if existing_item: + return existing_item - # 获取保存的图像路径 - image_path = None - if image_node.hasTag("gui_image_path"): - image_path = image_node.getTag("gui_image_path") + # 如果找不到,创建新的项 + # 找到场景根节点 + 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 - # 获取位置 - pos = (0, 0, 0) - if image_node.hasTag("transform_pos"): - try: - pos_str = image_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: - pos = (pos_values[0], pos_values[1], pos_values[2]) - except Exception as e: - print(f"恢复位置失败: {e}") - - # 获取尺寸 - 改进的尺寸处理逻辑 - size = (1.0, 1.0) # 默认尺寸为1x1 - if image_node.hasTag("transform_scale"): - try: - scale_str = image_node.getTag("transform_scale") - scale_str = scale_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') - scales = [float(x.strip()) for x in scale_str.split(',')] - - # 根据缩放值的数量处理尺寸 - if len(scales) >= 3: - # 3D缩放 (x, y, z) - size = (scales[0]*2, scales[1]*2) # 取前两个值作为宽度和高度 - elif len(scales) >= 2: - # 2D缩放 (x, y) - size = (scales[0]*2, scales[1]*2) - elif len(scales) >= 1: - # 均匀缩放 - size = (scales[0]*2, scales[0]*2) - else: - size = (1.0, 1.0) - - print(f"恢复尺寸: {size} (来自缩放: {scales})") - except Exception as e: - print(f"恢复尺寸失败: {e}") - - # 使用gui_manager中的createGUI3DImage方法创建新的3D图像 - # 首先需要找到gui_manager - gui_manager = None - if hasattr(self.world, 'gui_manager'): - gui_manager = self.world.gui_manager - - if gui_manager and hasattr(gui_manager, 'createGUI3DImage'): - # 调用gui_manager的createGUI3DImage方法创建新的3D图像 - new_image_node = gui_manager.createGUI3DImage(pos=pos, image_path=image_path, size=size) - - if new_image_node: - # 如果返回的是列表(多选创建),取第一个 - if isinstance(new_image_node, list): - created_node = new_image_node[0] - else: - created_node = new_image_node - - print(f"3D图像重建完成: {created_node.getName()}, 路径: '{image_path}', 尺寸: {size}") - return created_node - else: - print("gui_manager.createGUI3DImage返回None") - - except Exception as e: - print(f"重新创建3D图像失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - - def _recreateGUIElement(self, gui_node): - """重新创建GUI元素的通用方法""" - try: - # 获取GUI元素类型 - gui_type = "unknown" - if gui_node.hasTag("gui_type"): - gui_type = gui_node.getTag("gui_type") - - # 根据类型重新创建GUI元素 - recreated_node = None - if gui_type == "3d_text": - recreated_node = self._recreate3DText(gui_node) - elif gui_type == "3d_image": - recreated_node = self._recreate3DImage(gui_node) - else: + if not scene_root: + print("无法找到场景根节点") return None - # 如果没有特定的重建方法,至少确保基本属性 - target_node = recreated_node if recreated_node else gui_node + # 创建新的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") # 或根据元素类型设置适当的类型 - # 确保节点被正确标记 - if not target_node.hasTag("is_scene_element"): - target_node.setTag("is_scene_element", "1") + print(f"为元素 {element_name} 创建了新的Qt树项") + return new_item - # 添加到GUI元素列表(如果还没有添加) - if hasattr(self.world, 'gui_elements'): - if target_node not in self.world.gui_elements: - self.world.gui_elements.append(target_node) - print(f"将{target_node}添加至gui_elements") - - # 恢复GUI特定属性 - self._restoreGUIProperties(target_node) - - print(f"GUI元素 {target_node.getName()} 重建完成") - return True except Exception as e: - print(f"重新创建GUI元素失败: {str(e)}") - return False - - def _restoreGUIProperties(self, gui_node): - """恢复GUI元素的特定属性""" - try: - # 恢复文本内容(对于文本相关的GUI元素) - if gui_node.hasTag("gui_text"): - text_content = gui_node.getTag("gui_text") - # 如果节点有setText方法,则设置文本 - if hasattr(gui_node, 'setText'): - gui_node.setText(text_content) - # 如果是TextNode,则设置文本 - elif hasattr(gui_node, 'node') and hasattr(gui_node.node(), 'setText'): - gui_node.node().setText(text_content) - - # 恢复大小 - if gui_node.hasTag("gui_size"): - try: - size_str = gui_node.getTag("gui_size") - # 解析并设置大小 - pass # 根据具体需要实现 - except: - pass - - # 恢复颜色 - if gui_node.hasTag("gui_color"): - try: - color_str = gui_node.getTag("gui_color") - # 解析并设置颜色 - pass # 根据具体需要实现 - except: - pass - - print(f"已恢复GUI元素 {gui_node.getName()} 的属性") - except Exception as e: - print(f"恢复GUI属性时出错: {str(e)}") - - def createGUIElement(self, element_type, name=None, **kwargs): - """创建GUI元素的通用方法""" - try: - from panda3d.core import NodePath - - # 生成唯一名称 - if name is None: - name = f"{element_type}_{len(getattr(self.world, 'gui_elements', []))}" - - # 创建GUI元素节点 - element_node = NodePath(name) - - # 设置GUI元素标签 - element_node.setTag("is_gui_element", "1") - element_node.setTag("gui_type", element_type) - element_node.setTag("is_scene_element", "1") - element_node.setTag("saved_gui_type", element_type) - - # 添加到场景 - element_node.reparentTo(self.world.render) - - # 添加到GUI元素列表 - if not hasattr(self.world, 'gui_elements'): - self.world.gui_elements = [] - self.world.gui_elements.append(element_node) - print(f"1111111111111111111{self.world.gui_elements}") - - # 保存额外属性 - for key, value in kwargs.items(): - element_node.setTag(f"gui_{key}", str(value)) - - # 更新场景树 - self.updateSceneTree() - - print(f"创建GUI元素: {name} (类型: {element_type})") - return element_node - except Exception as e: - print(f"创建GUI元素失败: {str(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() @@ -1819,8 +2364,8 @@ class SceneManager: light.casts_shadows = True light.shadow_map_resolution = 256 - # 设置位置 - light.setPos(light_node.getPos()) + light_pos = light_node.getPos() + light.setPos(light_pos) # 添加到渲染管线 render_pipeline = get_render_pipeline() @@ -1839,6 +2384,8 @@ class SceneManager: print(f"重新创建聚光灯: {light_node.getName()}") except Exception as e: print(f"重新创建聚光灯失败: {str(e)}") + import traceback + traceback.print_exc() def _recreatePointLight(self, light_node): """重新创建点光源""" @@ -1881,6 +2428,8 @@ class SceneManager: print(f"重新创建点光源: {light_node.getName()}") except Exception as e: print(f"重新创建点光源失败: {str(e)}") + import traceback + traceback.print_exc() def _cleanupAuxiliaryNodes(self): """清理场景中可能存在的辅助节点""" @@ -2015,6 +2564,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) @@ -2024,7 +2575,7 @@ class SceneManager: light.radius = 1000 light.casts_shadows = True light.shadow_map_resolution = 256 - light.setPos(*pos) + light.setPos(pos) # 添加到渲染管线 render_pipeline.add_light(light) @@ -2032,6 +2583,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") @@ -2120,11 +2672,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 @@ -2138,6 +2691,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") @@ -2198,177 +2752,6 @@ class SceneManager: pass return None - # def createSpotLight(self, pos=(0, 0, 0)): - # """创建聚光灯 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建聚光灯节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建聚光灯节点 - # created_nodes = tree_widget.create_item("spot_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建聚光灯失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建聚光灯时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createSpotLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import SpotLight, RenderPipeline - # from panda3d.core import Vec3,NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("SpotlightAttachNode") - # light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # self.half_energy = 5000 - # self.lamp_fov = 70 - # self.lamp_radius = 1000 - # - # light = SpotLight() - # light.direction = Vec3(0, 0, -1) # 光照方向 - # light.fov = self.lamp_fov # 光源角度(类似手电筒) - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.energy = self.half_energy # 光照强度 - # light.radius = self.lamp_radius # 影响范围 - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # light.setPos(*pos) - # #light_np.setPos(*pos) - # - # #light_np = render_pipeline.add_light(light, parent=self.world.render) - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Spotlight_{len(self.Spotlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # #light_np.reparentTo(self.world.render) # 挂载到场景根节点 - # - # light_np.setTag("light_type", "spot_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # light_np.setPythonTag("rp_light_object", light) - # - # self.Spotlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # #print("nikan"+light_np.getHpr()) - - # def createPointLight(self, pos=(0, 0, 0)): - # """创建点光源 - 使用统一的create_item方法""" - # try: - # # 调用CustomTreeWidget的create_item方法创建点光源节点 - # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - # tree_widget = self.world.interface_manager.treeWidget - # if tree_widget and hasattr(tree_widget, 'create_item'): - # # 创建点光源节点 - # created_nodes = tree_widget.create_item("point_light") - # - # if created_nodes: - # # 获取创建的节点 - # light_np, qt_item = created_nodes[0] - # - # # 设置位置(如果指定了非默认位置) - # if pos != (0, 0, 0): - # light_np.setPos(*pos) - # # 同时更新光源对象的位置 - # light_obj = light_np.getPythonTag("rp_light_object") - # if light_obj: - # light_obj.setPos(*pos) - # - # print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}") - # return light_np - # else: - # print("❌ create_item创建点光源失败") - # return None - # else: - # print("❌ 无法访问树形控件的create_item方法") - # return None - # else: - # print("❌ 无法访问界面管理器或树形控件") - # return None - # - # except Exception as e: - # print(f"❌ 创建点光源时发生错误: {str(e)}") - # import traceback - # traceback.print_exc() - # return None - - # def createPointLight(self, pos=(0, 0, 0)): - # from RenderPipelineFile.rpcore import PointLight, RenderPipeline - # from panda3d.core import Vec3, NodePath - # - # render_pipeline = get_render_pipeline() - # - # # 创建一个挂载节点(你控制的) - # light_np = NodePath("PointlightAttachNode") - # light_np.reparentTo(self.world.render) - # - # - # light = PointLight() - # light.setPos(*pos) - # light_np.setPos(*pos) - # light.energy = 5000 - # light.radius = 1000 - # light.inner_radius = 0.4 - # light.set_color_from_temperature(5 * 1000.0) # 色温(K) - # light.casts_shadows = True # 是否投射阴影 - # light.shadow_map_resolution = 256 # 阴影分辨率 - # - # render_pipeline.add_light(light) # 添加到渲染管线 - # - # light_name = f"Pointlight{len(self.Pointlight)}" - # - # light_np.setName(light_name) # 设置唯一名称 - # - # #light_np = NodePath(f"PointLight_{len(self.Pointlight)}") - # #light_np.reparentTo(self.world.render) - # #light_np.setPos(*pos) - # - # light_np.setTag("light_type", "point_light") - # light_np.setTag("is_scene_element", "1") - # light_np.setTag("light_energy", str(light.energy)) - # - # # 保存光源对象引用(重要!用于属性面板) - # light_np.setPythonTag("rp_light_object", light) - # - # self.Pointlight.append(light_np) - # - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # return light,light_np # ==================== GLB 转换方法 ==================== @@ -2666,6 +3049,7 @@ except Exception as e: # 添加标签以便场景识别和保存 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) # 使用唯一名称作为文件标识,代替索引 @@ -2975,4 +3359,739 @@ 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_VIDEO_SCREEN","GUI_2D_VIDEO_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('gui_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.gui_manager.createGUI3DImage(pos, image_path, scale) + elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1,1)) + new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) + elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1, 1)) + new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) + + 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/templates/main_template.py b/templates/main_template.py new file mode 100644 index 00000000..51ededf4 --- /dev/null +++ b/templates/main_template.py @@ -0,0 +1,1061 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试动画模型 - Panda3D应用程序 +使用Panda3D引擎编辑器创建 +""" + +from __future__ import print_function + +import json + +from direct.actor.Actor import Actor +from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser +from core.InfoPanelManager import InfoPanelManager +# 获取渲染管线路径 +# 在文件开头添加sys导入(如果还没有的话) +import sys +import os + +# 修改工作目录设置部分 +if getattr(sys, 'frozen', False): + # 打包后的环境 + project_root = os.path.dirname(sys.executable) +else: + # 开发环境 + try: + project_root = os.path.dirname(os.path.abspath(__file__)) + except NameError: + project_root = os.getcwd() + +os.chdir(project_root) + +render_pipeline_path = 'RenderPipelineFile' +sys.path.insert(0, render_pipeline_path) + +# 改进路径设置逻辑 +pipeline_path = os.path.join(project_root, "RenderPipelineFile") +if os.path.exists(pipeline_path) and pipeline_path not in sys.path: + sys.path.insert(0, pipeline_path) + # 同时添加子目录以确保所有模块都能正确导入 + for root, dirs, files in os.walk(pipeline_path): + if root not in sys.path: + sys.path.insert(0, root) + + +import math +from random import random, randint, seed +from panda3d.core import Vec3, load_prc_file_data, Filename +from direct.showbase.ShowBase import ShowBase +from direct.task.TaskManagerGlobal import taskMgr + +# os.chdir(os.path.dirname(os.path.realpath(__file__))) +from core.script_system import ScriptManager +from core.CustomMouseController import CustomMouseController +from panda3d.core import CollisionTraverser + +class MainApp(ShowBase): + def __init__(self): + # 在调用父类构造函数前确保必要的属性存在 + if not hasattr(self, 'appRunner'): + self.appRunner = None + + load_prc_file_data("", """ + win-size 1380 750 + window-title Render + """) + + # 简化 sys.path 设置逻辑 + pipeline_path = os.path.join(project_root, "RenderPipelineFile") + if os.path.exists(pipeline_path): + if pipeline_path not in sys.path: + sys.path.insert(0, pipeline_path) + else: + print(f"错误: 找不到渲染管线目录: {pipeline_path}") + return + + try: + from rpcore import RenderPipeline + self.render_pipeline = RenderPipeline() + self.render_pipeline.create(self) + #self.render_pipeline.pre_show_base_init() + #ShowBase.__init__(self) + + + except ImportError as e: + + print(f"导入RenderPipeline模块失败: {e}") + import traceback + traceback.print_exc() + ShowBase.__init__(self) + self.render_pipeline = None + return + + self.script_manager = ScriptManager(self) + self.script_manager.start_system() + + # 加载所有脚本 + self.script_manager.load_all_scripts_from_directory() + + self.info_panel_manager = InfoPanelManager(self) + + try: + # 再导入controller模块 + from rpcore.util.movement_controller import MovementController + self.controller = MovementController(self) + self.controller.set_initial_position( + Vec3(0, -50, 20), Vec3(0, 0, 0)) + self.controller.setup() + except ImportError as e: + print(f"导入MovementController失败: {e}") + self.controller = None + + self._last_click_time = 0 + self._last_clicked_node = None + self._double_click_threshold = 0.3 + + + self._loadFont() + self.loadFullScene() + self.loadGUIFromJSON() + self.setupMouseClickHandler() + self.cTrav = CollisionTraverser() + + if hasattr(self, 'accept'): + base.accept("l", self.tour) + + def _loadFont(self): + """加载中文字体""" + self.chinese_font = None + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + else: + print("✓ 中文字体加载成功") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + def getChineseFont(self): + """获取中文字体""" + return self.chinese_font + + def getResourcePath(self,relative_path): + if getattr(sys,'frozen',False): + base_path = os.path.dirname(sys.executable) + else: + base_path = os.path.dirname(os.path.abspath(__file__)) + + return os.path.join(base_path,relative_path) + + def loadFullScene(self): + if not hasattr(self, 'loader') or not hasattr(self, 'render'): + print("错误: Panda3D核心组件未正确初始化") + return + try: + scene_file = self.getResourcePath("scene.bam") + + if os.path.exists(scene_file): + # 使用readBamFile加载完整场景 + from panda3d.core import BamCache + BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 + + scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) + if scene: + scene.reparentTo(self.render) + self.render_pipeline.prepare_scene(scene) + print("✓ 完整场景加载成功") + + # 检测并播放模型动画 + #self._processModelAnimations(scene) + + # 处理场景中的各种元素 + self.processSceneElements(scene) + else: + print("⚠️ 场景文件加载失败") + else: + print("⚠️ 未找到场景文件") + except Exception as e: + print(f"加载完整场景时出错: {str(e)}") + import traceback + traceback.print_exc() + + def _processModelAnimations(self, node_path): + """处理节点中的动画模型""" + try: + # 查找场景中所有可能的动画模型 + char_nodes = node_path.findAllMatches("**/+Character") + + for char_node in char_nodes: + try: + # 获取父节点(通常是模型根节点) + model_root = char_node.getParent() + model_name = model_root.getName() + + print(f"检测到可能的动画模型: {model_name}") + + # 尝试创建Actor + actor = Actor(model_root) + actor.reparentTo(self.render) + + actor.setPos(node_path.getPos()) + actor.setHpr(node_path.getHpr()) + actor.setScale(node_path.getScale()) + + + + # 获取动画名称 + anim_names = actor.getAnimNames() + if anim_names: + print(f"✓ 成功创建动画模型: {model_name}") + print(f" 可用动画: {anim_names}") + + # 循环播放所有动画 + for anim_name in anim_names: + print(f" 循环播放动画: {anim_name}") + actor.loop(anim_name) + break # 只播放第一个动画,避免同时播放多个动画 + + # 替换原始模型 + model_root.detachNode() + + else: + # 没有动画,使用原始模型 + print(f"模型 {model_name} 不包含动画") + actor.detachNode() # 移除创建的Actor + + except Exception as e: + print(f"处理动画模型 {char_node.getName()} 时出错: {str(e)}") + + except Exception as e: + print(f"处理模型动画时出错: {str(e)}") + import traceback + traceback.print_exc() + + def processSceneElements(self, scene): + """处理场景中的各种元素""" + try: + processed_lights = [] + loaded_nodes = {} + def processNode(nodePath,depth=0): + loaded_nodes[nodePath.getName()] = nodePath + + if nodePath.hasTag("scripts_info"): + try: + import json + scripts_info = json.loads(nodePath.getTag("scripts_info")) + self.processScripts(nodePath,scripts_info) + except Exception as e: + print(f"处理节点 {nodePath.getName()} 的脚本时出错: {str(e)}") + + if nodePath.hasTag("light_type"): + light_type = nodePath.getTag("light_type") + + if nodePath not in processed_lights: + if light_type == "spot_light": + self._recreateSpotLight(nodePath) + elif light_type == "point_light": + self._recreatePointLight(nodePath) + processed_lights.append(nodePath) + + for child in nodePath.getChildren(): + processNode(child,depth+1) + + processNode(scene) + + # 处理GUI元素 + #self.processGUIElements(scene) + + except Exception as e: + print(f"处理场景元素时出错: {str(e)}") + + def _recreateSpotLight(self,light_node): + try: + from RenderPipelineFile.rpcore import SpotLight + 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) + + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def _recreatePointLight(self,light_node): + try: + from RenderPipelineFile.rpcore import PointLight + + 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()) + + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def processGUIElements(self, scene): + """处理场景中的GUI元素""" + try: + # 查找并处理2D图像 + images_2d = scene.findAllMatches("**/=gui_type=image_2d") + for img_node in images_2d: + try: + # GUI元素通常在场景加载时自动处理 + print(f"✓ 2D图像 {img_node.getName()} 已加载") + except Exception as e: + print(f"处理2D图像 {img_node.getName()} 失败: {str(e)}") + + except Exception as e: + print(f"处理GUI元素时出错: {str(e)}") + + def tour(self): + mopath = ( + (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), + (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), + (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), + (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), + (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), + (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), + ) + self.controller.play_motion_path(mopath, 3.0) + + def loadGUIFromJSON(self): + try: + gui_json_path = self.getResourcePath("gui/gui_elements.json") + if os.path.exists(gui_json_path): + with open(gui_json_path, 'r', encoding='utf-8') as f: + content = f.read().strip() + if content: + gui_data = json.loads(content) + self.createGUIElement(gui_data) + else: + print("GUI配置文件为空 ") + except Exception as e: + print(f"加载GUI元素时出错: {str(e)}") + import traceback + traceback.print_exc() + + def createGUIElement(self, element_data): + try: + processed_names = set() + element_original_data = {} + + for i, gui_info in enumerate(element_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 element_data: + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") + valid_parents.add(name) + + for i, gui_info in enumerate(element_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", "") + parent_name = gui_info.get("parent_name") + + if name in processed_names: + continue + + processed_names.add(name) + + 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] + + new_element = None + + if gui_type == "3d_text": + size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 + new_element = self.createGUI3DText( + pos=tuple(absolute_position), + text=text, + size=size + ) + elif gui_type == "button": + new_element = self.createGUIButton( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, + #command=self.resetWomenModel + ) + elif gui_type == "label": + new_element = self.createGUILabel( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "entry": + new_element = self.createGUIEntry( + pos=tuple(absolute_position), + placeholder=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, + command=self.onGUIEntrySubmit + ) + elif gui_type == "2d_image": + + scale_value = absolute_scale[0] + print(f"2d_image{scale_value}") + new_element = self.createGUI2DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=absolute_scale + ) + elif gui_type == "2d_video_screen": + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.2 + new_element = self.createGUI2DVideoScreen( + pos=tuple(absolute_position), + video_path=video_path, + size=absolute_scale + ) + elif gui_type == "info_panel": + new_element = self.info_panel_manager.onCreateSampleInfoPanel() + + if "scripts" in gui_info and new_element: + self.processScripts(new_element,gui_info["scripts"]) + + except Exception as e: + print(f"重建GUI元素失败 {name}: {e}") + import traceback + traceback.print_exc() + continue + + except Exception as e: + print(f"创建GUI元素时出错: {str(e)}") + + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1, command=None): + from direct.gui.DirectGui import DirectButton + + button = DirectButton( + text=text, + pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 + scale=size, # size 应该是数值而不是元组 + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + rolloverSound=None, + clickSound=None, + parent=None, + command=command + ) + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D文本GUI元素""" + try: + # 创建文本节点 + text_node = TextNode("gui_3d_text") + text_node.setText(text) + text_node.setAlign(TextNode.ACenter) + + # 设置字体(如果可用) + if self.getChineseFont(): + text_node.setFont(self.getChineseFont()) + + # 创建节点路径并添加到场景 + text_np = self.render.attachNewNode(text_node) + + # 设置位置和大小 + text_np.setPos(Vec3(pos[0], pos[1], pos[2])) + text_np.setScale(size) + + # 设置面向摄像机 + # text_np.setBillboardPointEye() + + # 设置渲染属性 + text_np.setBin("fixed", 40) + text_np.setDepthWrite(False) + + return text_np + except Exception as e: + print(f"❌ 创建3D文本失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + from direct.gui.DirectGui import DirectLabel + label = DirectLabel( + text=text, + pos=(pos[0], pos[1], pos[2]), + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + text_align=TextNode.ACenter, + text_wordwrap=None, + text_mayChange=True, + parent=None + ) + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08, command=None): + from direct.gui.DirectGui import DirectEntry + + entry = DirectEntry( + text="", + pos=(pos[0], pos[1], pos[2]), + scale=size, + command=command, + initialText=placeholder, + numLines=1, + width=12, + focus=0, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + text_align=TextNode.ACenter, + text_wordwrap=None, + text_mayChange=True, + parent=None, + rolloverSound=None, + clickSound=None, + # 添加焦点管理命令 + focusInCommand=self.disableCameraControl, + focusOutCommand=self.enableCameraControl, + # 确保输入框能正确捕获所有键盘事件 + suppressKeys=True, # 这个参数很重要,它会阻止按键事件传播到其他处理器 + suppressMouse=True + ) + return entry + + def disableCameraControl(self): + """禁用相机控制""" + try: + if hasattr(self, 'controller'): + # 如果控制器有内置的禁用方法 + if hasattr(self.controller, 'disable'): + self.controller.disable() + else: + # 否则手动禁用事件监听 + self.controller.ignoreAll() # 忽略所有已注册的事件 + print("相机控制已禁用") + except Exception as e: + print(f"禁用相机控制时出错: {e}") + + def enableCameraControl(self): + """启用相机控制""" + try: + if hasattr(self, 'controller'): + # 如果控制器有内置的启用方法 + if hasattr(self.controller, 'enable'): + self.controller.enable() + else: + # 重新设置控制器 + self.controller.setup() + print("相机控制已启用") + except Exception as e: + print(f"启用相机控制时出错: {e}") + + def onGUIEntrySubmit(self, text, entry_id=None): + """GUI输入框提交事件处理""" + try: + print(f"GUI输入框提交: {entry_id} = {text}") + + # 重新启用相机控制 + self.enableCameraControl() + + # 清除输入框焦点(如果需要) + # base.win.focus() + + # 在这里添加您需要的文本处理逻辑 + # 例如保存文本、更新UI等 + + except Exception as e: + print(f"处理输入框提交时出错: {e}") + import traceback + traceback.print_exc() + + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + # 添加属性检查 + if not hasattr(self, 'aspect2d'): + print("错误: aspect2d未初始化") + return None + if image_path and not os.path.isabs(image_path): + image_path = self.getResourcePath(image_path) + # 处理非均匀缩放 + if isinstance(size, (list, tuple)) and len(size) >= 2: + # 分别处理宽度和高度的缩放 + width_scale = size[0] * 0.2 + height_scale = size[2] * 0.2 + else: + # 如果只提供了一个缩放值,则使用相同值 + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-2d-image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + #cm.setFrame(-size, size, -size, size) + + image_node = self.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(pos) + image_node.setBin("fixed", 0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setColor(1, 1, 1, 1) + + # 设置透明度支持 + image_node.setTransparency(TransparencyAttrib.MAlpha) + + if image_path: + try: + texture = self.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + image_node.setColor(1, 1, 1, 1) + else: + print(f"无法加载图片: {image_path}") + except Exception as e: + print(f"加载图片时出错: {e}") + return image_node + + def createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): + import os + if video_path and not os.path.isabs(video_path): + image_path = self.getResourcePath(video_path) + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib, Texture, TextureStage + import os + + if isinstance(size,(list,tuple)) and len(size) >= 2: + width_scale = size[0]*0.2 + height_scale = size[2]*0.2 + else: + width_scale = size*0.1 if isinstance(size,(int,float)) else 0.2 + height_scale = width_scale + + video_screen = DirectFrame( + frameSize=(-width_scale, width_scale, -height_scale, height_scale), + frameColor=(1, 1, 1, 1), + pos=pos, + parent=None, + suppressMouse=True + ) + video_screen.setTransparency(TransparencyAttrib.MAlpha) + + placeholder_texture = Texture() + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' + placeholder_texture.setRamImage(placeholder_data) + + if video_path and os.path.exists(video_path): + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + video_screen["frameTexture"] = movie_texture + + return video_screen + + def _loadMovieTexture(self, video_path): + from panda3d.core import Texture, MovieTexture + import os + movie_texture = MovieTexture(video_path) + if movie_texture.read(video_path): + self._configureVideoTexture(movie_texture) + return movie_texture + + def _configureVideoTexture(self, texture): + from panda3d.core import Texture + + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + + if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): + texture.set_loop(True) + texture.set_play_rate(1.0) + + def resetWomenModel(self): + """调整 Women_1.glb 模型大小,实现从小到大再到小的完整循环效果""" + try: + # 查找 Women_1.glb 模型 + women_models = self.render.findAllMatches("**/Women_1.glb*") + + if women_models: + for model in women_models: + # 定义完整的缩放级别序列(从0.5到3.0再回到0.5) + scale_levels = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5] + + # 获取当前缩放值 + current_scale = model.getScale() + + # 查找当前最接近的缩放级别索引 + current_index = 0 + min_diff = float('inf') + for i, scale in enumerate(scale_levels): + diff = abs(current_scale.x - scale) + if diff < min_diff: + min_diff = diff + current_index = i + + # 计算下一个缩放级别(循环) + next_index = (current_index + 1) % len(scale_levels) + next_scale = scale_levels[next_index] + + # 应用新的缩放 + model.setScale(next_scale) + print(f"✓ 调整模型 {model.getName()} 大小: {current_scale.x:.1f} -> {next_scale:.1f}") + print(f" 当前索引: {current_index}, 下一个索引: {next_index}") + else: + print("⚠️ 未找到 Women_1.glb 模型") + + except Exception as e: + print(f"调整模型大小时出错: {str(e)}") + import traceback + traceback.print_exc() + + def processScripts(self, element, script_info_list): + """处理元素上挂载的脚本 - 使用新的脚本系统""" + try: + print(f"正在为元素 {element.getName()} 挂载脚本") + print(f"可用脚本列表: {self.script_manager.get_available_scripts()}") + + if not hasattr(self,'script_manager'): + print("脚本管理器未初始化") + return + + for script_info in script_info_list: + script_name = script_info["name"] + script_file = script_info.get("file", "") + + if script_name: + script_component = self.script_manager.add_script_to_object(element,script_name) + + if script_component: + print(f"✓ 脚本 {script_name} 已挂载到元素 {element.getName()}") + else: + print(f"⚠️ 脚本 {script_name} 挂载失败") + # 列出可用脚本帮助调试 + available_scripts = self.script_manager.get_available_scripts() + print(f"当前可用脚本: {available_scripts}") + else: + print(f"⚠️ 脚本信息不完整: {script_info}") + + # # 从文件路径中提取脚本类名 + # if script_file: + # # 获取脚本文件名(不含路径和扩展名) + # script_filename = os.path.basename(script_file) + # script_class_name = os.path.splitext(script_filename)[0] + # print(f"尝试挂载脚本: {script_class_name} (来自文件: {script_file})") + # + # # 使用脚本管理器为元素添加脚本 + # script_component = self.script_manager.add_script_to_object(element, script_class_name) + # + # if script_component: + # print(f"✓ 脚本 {script_class_name} 已挂载到元素 {element.getName()}") + # else: + # print(f"⚠️ 脚本 {script_class_name} 挂载失败") + # # 列出可用脚本帮助调试 + # available_scripts = self.script_manager.get_available_scripts() + # print(f"当前可用脚本: {available_scripts}") + # else: + # print(f"⚠️ 脚本信息不完整: {script_name}") + + except Exception as e: + print(f"挂载脚本到元素 {element.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + 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_time180: + 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 + from direct.showbase.ShowBaseGlobal import globalClock + data.elapsed_time += globalClock.getDt() + + t = min(1.0,data.elapsed_time/data.duration) + + smooth_t = t*t*(3-2*t) + + current_pos = data.start_pos + (data.end_pos - data.start_pos) * smooth_t + self.cam.setPos(current_pos) + + start_h = self._normalizeAngle(data.start_hpr.x) + end_h = self._normalizeAngle(data.end_hpr.x) + start_p = self._normalizeAngle(data.start_hpr.y) + end_p = self._normalizeAngle(data.end_hpr.y) + start_r = self._normalizeAngle(data.start_hpr.z) + end_r = self._normalizeAngle(data.end_hpr.z) + + if abs(end_h - start_h)>180: + if end_h > start_h: + start_h += 360 + else: + end_h += 360 + + if abs(end_p - start_p) > 180: + if end_p > start_p: + start_p += 360 + else: + end_p += 360 + + if abs(end_r - start_r)>180: + if end_r > start_r: + start_r += 360 + else: + end_r += 360 + + current_hpr = Vec3( + start_h + (end_h - start_h)*smooth_t, + start_p + (end_p - start_p)*smooth_t, + start_r + (end_r - start_r)*smooth_t + ) + + current_hpr.x = self._normalizeAngle(current_hpr.x) + current_hpr.y = self._normalizeAngle(current_hpr.y) + current_hpr.z = self._normalizeAngle(current_hpr.z) + + self.cam.setHpr(current_hpr) + + if t>=1.0: + return task.done + + return task.cont + except Exception as e: + print(f"摄像机聚焦动画失败{e}") + return task.done + + def setupMouseClickHandler(self): + try: + self.accept("mouse1", self.onMouseClick) + + if not hasattr(self,'mouseWatcherNode'): + from panda3d.core import MouseWatcher + self.mouseWatcherNode = MouseWatcher() + except Exception as e: + print(f"设置鼠标点击处理器失败: {e}") + + def onMouseClick(self): + try: + if not hasattr(self, 'mouseWatcherNode') or not self.mouseWatcherNode.hasMouse(): + return + + mouse_pos = self.mouseWatcherNode.getMouse() + + from panda3d.core import CollisionRay, CollisionNode, CollisionHandlerQueue, BitMask32 + from panda3d.core import GeomNode + + # 创建射线 + ray = CollisionRay() + ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y) + + # 创建碰撞节点 + ray_node = CollisionNode('mouseRay') + ray_node.addSolid(ray) + ray_node.setFromCollideMask(BitMask32.bit(0)) + + # 附加到相机 + ray_np = self.camera.attachNewNode(ray_node) + + # 创建碰撞队列 + handler = CollisionHandlerQueue() + + # 修复语法错误:正确初始化碰撞遍历器 + if not hasattr(self, 'cTrav') or self.cTrav is None: + self.cTrav = CollisionTraverser() + + self.cTrav.addCollider(ray_np, handler) + self.cTrav.traverse(self.render) + + # 检查碰撞结果 + if handler.getNumEntries() > 0: + # 按距离排序 + handler.sortEntries() + + # 获取最近的碰撞节点 + entry = handler.getEntry(0) + clicked_node = entry.getIntoNodePath() + + # 向上遍历找到可选择的父节点 + current_node = clicked_node + while current_node and not current_node.isEmpty(): + # 检查节点是否应该被选择 + if (current_node.hasTag("is_scene_element") or + current_node.hasTag("element_type") or + current_node.getName() not in ["render", "camera", "ambient_light", "directional_light"]): + + # 检查是否为双击 + if self.checkDoubleClick(current_node): + print(f"双击节点: {current_node.getName()}") + self.focusCameraOnNode(current_node) + break + else: + print(f"单击节点: {current_node.getName()}") + + current_node = current_node.getParent() + # 避免无限循环 + if current_node.getName() == "render": + break + + # 清理碰撞器 + ray_np.removeNode() + + except Exception as e: + print(f"处理鼠标点击失败: {e}") + import traceback + traceback.print_exc() + + +# 在 main.py 的最后部分,修改为: +if __name__ == "__main__": + try: + app = MainApp() + if hasattr(app, 'run'): + app.run() + else: + print("应用程序初始化失败") + except Exception as e: + print(f"应用程序启动失败: {str(e)}") + import traceback + traceback.print_exc() + + 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 17042936..959e3201 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,15 +68,12 @@ 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() + self.world.property_panel.clearPropertyPanel() + print("点击了无数据项,清除选中状态") # def showTreeContextMenu(self, position): # """显示树形控件的右键菜单""" @@ -313,6 +339,23 @@ class InterfaceManager: groundItem.setData(0, Qt.UserRole, self.world.ground) groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + ground_nodes = [ + ('ground2','地板2'), + ('ground3','地板3'), + ('ground4','地板4'), + ('ground5','地板5'), + ('ground6','地板6') + ] + + for attr_name,display_name in ground_nodes: + if hasattr(self.world,attr_name): + ground_node = getattr(self.world,attr_name) + if ground_node: + extraGroundItem = QTreeWidgetItem(sceneRoot,[display_name]) + extraGroundItem.setData(0,Qt.UserRole,ground_node) + extraGroundItem.setData(0,Qt.UserRole+1,"SCENE_NODE") + + #添加灯光节点 for light in self.world.Spotlight: if light: diff --git a/ui/main_window.py b/ui/main_window.py index 541588f6..a13fb789 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -9,26 +9,151 @@ 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) @@ -88,28 +386,56 @@ class MainWindow(QMainWindow): # 创建工具栏容器 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); /* 按下时稍暗的紫色 */ } """) @@ -123,11 +449,10 @@ class MainWindow(QMainWindow): # 选择工具 self.selectTool = QToolButton() - icon_path = self.get_icon_path("select_tool.png") - if icon_path and os.path.exists(icon_path): - self.selectTool.setIcon(QIcon(icon_path)) - else: - self.selectTool.setText('选择') # 如果没有图标则显示文字 + select_icon = get_icon('select_tool', QSize(16, 16)) + if not select_icon.isNull(): + self.selectTool.setIcon(select_icon) + self.selectTool.setText('选择') # 如果没有图标则显示文字 self.selectTool.setIconSize(QSize(16, 16)) self.selectTool.setCheckable(True) self.selectTool.setToolTip("选择工具 (Q)") @@ -140,8 +465,7 @@ class MainWindow(QMainWindow): icon_path = self.get_icon_path("move_tool.png") if icon_path and os.path.exists(icon_path): self.moveTool.setIcon(QIcon(icon_path)) - else: - self.moveTool.setText('移动') + self.moveTool.setText("移动") self.moveTool.setIconSize(QSize(16, 16)) self.moveTool.setCheckable(True) self.moveTool.setToolTip("移动工具 (W)") @@ -151,11 +475,10 @@ class MainWindow(QMainWindow): # 旋转工具 self.rotateTool = QToolButton() - icon_path = self.get_icon_path("rotate_tool.png") - if icon_path and os.path.exists(icon_path): - self.rotateTool.setIcon(QIcon(icon_path)) - else: - 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)") @@ -165,11 +488,10 @@ class MainWindow(QMainWindow): # 缩放工具 self.scaleTool = QToolButton() - icon_path = self.get_icon_path("scale_tool.png") - if icon_path and os.path.exists(icon_path): - self.scaleTool.setIcon(QIcon(icon_path)) - else: - 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)") @@ -335,8 +657,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('创建') @@ -408,7 +734,7 @@ class MainWindow(QMainWindow): self.infoPanelMenu = menubar.addMenu('信息面板') # 创建示例面板动作 self.createSamplePanelAction = self.infoPanelMenu.addAction('创建示例面板') - self.createSamplePanelAction.triggered.connect(self.onCreateSampleInfoPanel) + self.createSamplePanelAction.triggered.connect(self.world.info_panel_manager.onCreateSampleInfoPanel) # 添加更多面板创建选项 # self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板') # self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel) @@ -422,6 +748,9 @@ class MainWindow(QMainWindow): 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) @@ -582,6 +911,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对象的创建方法 @@ -629,6 +1190,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) @@ -636,6 +1244,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() @@ -684,9 +1350,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) @@ -703,9 +1433,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) @@ -734,15 +1461,178 @@ class MainWindow(QMainWindow): # 创建底部停靠窗口(资源窗口) 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): """创建工具栏""" @@ -885,6 +1775,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) @@ -928,6 +1843,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) @@ -958,6 +1899,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("图形编辑")) @@ -987,8 +1931,23 @@ 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) @@ -1026,6 +1985,313 @@ class MainWindow(QMainWindow): self.vrDebugSettingsAction.triggered.connect(self.onShowVRDebugSettings) + 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): + """撤销操作""" + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_undo(): + success = self.world.command_manager.undo() + if success: + print("成功操作") + else: + print("撤销失败") + QMessageBox.information(self,"提示","撤销操作失败") + else: + print("没有可撤销的操作") + QMessageBox.information(self,"提示","没有可撤销的操作") + else: + print("命令管理器未初始化") + QMessageBox.information(self,"提示","命令系统未初始化") + + def onRedo(self): + """重做操作""" + if hasattr(self.world,'command_manager'): + if self.world.command_manager.can_redo(): + success = self.world.command_manager.redo() + if success: + print("成功重做") + else: + print("重做失败") + QMessageBox.information(self,"提示","重做操作失败") + else: + print("没有可重做的操作") + QMessageBox.information(self,"提示","没有可重做的操作") + else: + print("命令管理器未初始化") + QMessageBox.information(self,"提示","命令系统未初始化") def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: @@ -1049,13 +2315,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): """处理加入模型按钮点击事件""" @@ -1089,53 +2363,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): """获取模型坐标信息的对话框""" @@ -1208,7 +2485,7 @@ class MainWindow(QMainWindow): return None, False def onLoadCesiumTileset(self): - url,ok = QInputDialog.getText( + dialog = self.createStyledInputDialog( self, "加载 Cesium 3D Tiles", "输入 tileset.json URL:", @@ -1216,153 +2493,135 @@ 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 onCreateSampleInfoPanel(self): - """创建示例天气信息面板(模拟数据)""" + def openWebBrowser(self): + if not WEB_ENGINE_AVAILABLE: + return None try: - # 获取中文字体 - from panda3d.core import TextNode - font = self.world.getChineseFont() if self.world.getChineseFont() else None + from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWidgets import QDockWidget + from PyQt5.QtCore import QUrl + import os - # 创建面板 - info_manager = self.world.info_panel_manager - info_manager.setParent(aspect2d) + main_window = self.world.main_window - # 使用唯一的面板ID - import time - unique_id = f"weather_info_{int(time.time())}" + # 尝试获取主窗口引用 + if main_window is None: + print("🔍 尝试获取主窗口引用...") - # 创建示例面板 - weather_panel = info_manager.createInfoPanel( - panel_id=unique_id, # 使用唯一ID - position=(0, 0), - size=(1, 1), - 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 - ) + # 检查各种可能的主窗口引用 + 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}") - # 更新面板标题 - info_manager.updatePanelContent(unique_id, title="北京天气") + if main_window is None and hasattr(self.world, 'main_window'): + main_window = self.world.main_window + print(f" - world.main_window: {main_window}") - # 添加到场景树 - self.addInfoPanelToTree(weather_panel, "天气信息面板") + # 如果仍然没有主窗口,尝试从树形控件获取 + if main_window is None and self.world.treeWidget: + try: + main_window = self.world.treeWidget.window() + print(f" - 从 treeWidget 获取窗口: {main_window}") + except: + pass - # 立即显示加载中信息 - info_manager.updatePanelContent(unique_id, content="正在获取天气数据...") + if main_window is None: + print("✗ 无法获取主窗口引用") + return None + else: + print(f"✅ 使用传入的主窗口引用: {main_window}") - info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) + # 检查主窗口是否有效 + if not hasattr(main_window, 'addDockWidget'): + print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") + return None - # # 立即显示示例数据 - # sample_data = self.getSampleWeatherData() - # info_manager.updatePanelContent(unique_id, content=sample_data) - # - # # 注册数据源,定期更新示例数据 - # info_manager.registerDataSource(unique_id, self.getSampleWeatherData, update_interval=2.0) + # 检查是否已经存在浏览器视图 + for element in self.world.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "WebBrowserView": + print("⚠ 浏览器视图已经存在") + # 将其前置显示 + element.show() + element.raise_() + return element - print("✓ 示例天气信息面板已创建") + # 创建停靠窗口 + 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"✗ 创建示例天气信息面板失败: {e}") + print(f"✗ 创建浏览器视图失败: {str(e)}") import traceback traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") + return None - def getRealWeatherData(self): - """获取真实天气数据""" - try: - import requests - import json - from datetime import datetime - # 请求天气数据 - url = "https://wttr.in/Beijing?format=j1" - response = requests.get(url, timeout=10) - response.raise_for_status() - - # 解析JSON数据 - weather_data = response.json() - - # 提取当前天气信息 - current_condition = weather_data['current_condition'][0] - weather_desc = current_condition['weatherDesc'][0]['value'] - temp_c = current_condition['temp_C'] - feels_like = current_condition['FeelsLikeC'] - humidity = current_condition['humidity'] - pressure = current_condition['pressure'] - visibility = current_condition['visibility'] - wind_speed = current_condition['windspeedKmph'] - wind_dir = current_condition['winddir16Point'] - - # 提取空气质量(如果可用) - air_quality = "N/A" - if 'air_quality' in weather_data and weather_data['air_quality']: - if 'us-epa-index' in current_condition: - air_quality_index = current_condition['air_quality_index'] - air_quality = f"指数: {air_quality_index}" - - # 获取更新时间 - update_time = datetime.now().strftime("%Y-%m-%d %H:%M") - - # 格式化显示内容 - content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}" - - return content - - except requests.exceptions.Timeout: - return "错误: 获取天气数据超时" - except requests.exceptions.ConnectionError: - return "错误: 网络连接失败" - except requests.exceptions.HTTPError as e: - return f"HTTP错误: {e}" - except json.JSONDecodeError: - return "错误: 无法解析天气数据" - except KeyError as e: - return f"错误: 天气数据格式不正确 (缺少字段: {e})" - except Exception as e: - return f"获取天气数据失败: {str(e)}" def getSampleWeatherData(self): """获取示例天气数据""" @@ -1407,7 +2666,7 @@ class MainWindow(QMainWindow): weather_panel = info_manager.create3DInfoPanel( panel_id=unique_id, position=(2, 0, 2), # 调整Z坐标避免与其他对象重叠 - size=(1, 1), + 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), @@ -1735,17 +2994,6 @@ class MainWindow(QMainWindow): except Exception as e: return f"获取场景信息失败: {str(e)}" - def onCreateAllInfoPanels(self): - """创建所有信息面板""" - try: - self.onCreateSampleInfoPanel() - self.onCreateSystemStatusPanel() - self.onCreateSensorDataPanel() - self.onCreateSceneInfoPanel() - QMessageBox.information(self, "成功", - "所有信息面板已创建完成!\n快捷键:\nF1 - 示例面板\nF2 - 系统状态面板\nF3 - 传感器数据面板\nF4 - 场景信息面板") - except Exception as e: - QMessageBox.critical(self, "错误", f"创建信息面板时出错: {str(e)}") # ==================== 脚本管理事件处理 ==================== @@ -1836,17 +3084,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): """加载脚本按钮事件""" @@ -1867,19 +3119,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): """重载所有脚本事件""" @@ -1965,6 +3224,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: @@ -2006,6 +3275,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) @@ -2063,78 +3369,80 @@ 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, "错误", "高度图地形创建失败!") # ==================== VR事件处理 ==================== @@ -2680,7 +3988,7 @@ 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 diff --git a/ui/property_panel.py b/ui/property_panel.py index 3305cd7f..f7e4bb02 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -7,14 +7,15 @@ 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 direct.task.TaskManagerGlobal import taskMgr from idna import check_label from jinja2.compiler import has_safe_repr -from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath +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 +28,7 @@ class PropertyPanelManager: """初始化属性面板管理器""" self.world = world self._propertyLayout = None - self._actor_cache={} + self._actor_cache = {} self._spherical_video_controls = {} # 初始化地形编辑参数 @@ -37,6 +38,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 +99,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() @@ -154,7 +159,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) @@ -163,12 +168,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, item) + self.updateGUIPropertyPanel(model, item) elif model and hasattr(model, 'getTag') and model.getTag("light_type"): self.updateLightPropertyPanel(model) elif model: @@ -176,6 +181,9 @@ class PropertyPanelManager: self._propertyLayout.addStretch() + # 重置碰撞相关标志,确保下次选择时正常显示 + self._just_added_collision = False + # 强制更新布局 if self._propertyLayout: self._propertyLayout.update() @@ -183,23 +191,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 @@ -212,52 +220,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") @@ -266,7 +274,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") @@ -275,16 +283,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()) @@ -292,37 +300,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") @@ -331,28 +342,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") @@ -361,12 +372,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) @@ -424,14 +435,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): @@ -446,46 +457,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, "选择地形纹理", "", @@ -493,8 +504,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: @@ -558,13 +569,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): @@ -745,6 +770,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: @@ -767,42 +793,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: @@ -830,8 +893,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) @@ -909,23 +973,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() @@ -1053,7 +1117,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 @@ -1090,16 +1154,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 标签居中 @@ -1213,6 +1280,9 @@ class PropertyPanelManager: self.transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(self.transform_group) + # 碰撞检测面板 + self._addCollisionPanel(model) + # 动画和太阳方位角面板 self._addAnimationPanel(model) self._addSunAzimuthPanel() @@ -1280,9 +1350,7 @@ class PropertyPanelManager: model.setScale(current_scale.getX(), current_scale.getY(), value) self.refreshModelValues(model) - - - def updateGUIPropertyPanel(self, gui_element,item): + def updateGUIPropertyPanel(self, gui_element, item): """更新GUI元素属性面板""" self.clearPropertyPanel() @@ -1292,7 +1360,7 @@ class PropertyPanelManager: user_visible = gui_element.getPythonTag("user_visible") if user_visible is None: user_visible = True - gui_element.setPythonTag("user_visible",True) + gui_element.setPythonTag("user_visible", True) self.name_group = QGroupBox("物体名称") name_layout = QHBoxLayout() @@ -1370,7 +1438,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") @@ -1380,7 +1448,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 @@ -1420,35 +1488,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组件使用世界坐标 @@ -1531,12 +1602,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() @@ -1603,7 +1673,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) @@ -1630,7 +1701,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 @@ -1641,7 +1712,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() @@ -1652,7 +1723,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) @@ -1692,12 +1764,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 in ['info_panel','info_panel_3d']: + elif gui_type in ['info_panel', 'info_panel_3d']: self._addInfoPanelProperties(gui_element) self._propertyLayout.addStretch() @@ -2810,6 +2882,7 @@ class PropertyPanelManager: import traceback traceback.print_exc() return False + def _selectInfoPanelBackgroundColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板背景颜色""" try: @@ -2980,7 +3053,7 @@ class PropertyPanelManager: content_size=size ) - #print(f"✓ 信息面板内容属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") + # print(f"✓ 信息面板内容属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") return True else: print("❌ 无法找到 info_panel_manager") @@ -3294,7 +3367,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, @@ -3313,6 +3386,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): @@ -3361,7 +3440,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输入区域 @@ -3375,7 +3454,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) @@ -3384,7 +3464,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, @@ -3413,6 +3493,16 @@ class PropertyPanelManager: path_label.setWordWrap(True) path_label.setStyleSheet("color: #00AAFF;") video_info_layout.addWidget(path_label, 0, 1) + + # 添加文件大小信息 + try: + file_size = os.path.getsize(video_path) + file_size_mb = file_size / (1024 * 1024) + size_label = QLabel(f"大小: {file_size_mb:.2f} MB") + size_label.setStyleSheet("font-size: 10px; color: gray;") + video_info_layout.addWidget(size_label, 1, 1) + except Exception as e: + pass else: # 文件不存在 video_info_layout.addWidget(QLabel("状态:"), 0, 0) @@ -3455,7 +3545,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输入区域 @@ -3469,7 +3559,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) @@ -3528,7 +3619,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) @@ -3753,7 +3844,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( @@ -3767,8 +3859,9 @@ class PropertyPanelManager: if success: print(f"成功加载新视频: {file_path}") # 刷新属性面板以显示新视频信息 - self._stop2DVideo(video_screen) - self.updateGUIPropertyPanel(video_screen) + + self.updateGUIPropertyPanel(video_screen, item) + #self._stop2DVideo(video_screen) return True except Exception as e: print(f"加载新视频失败: {e}") @@ -3786,7 +3879,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 @@ -3940,7 +4033,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") @@ -3978,7 +4072,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(): @@ -3991,9 +4085,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视频(内部方法)""" @@ -4030,8 +4124,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, @@ -4040,10 +4133,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}") @@ -4090,7 +4182,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()) @@ -4126,17 +4218,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}") @@ -4164,10 +4256,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}") @@ -4252,7 +4344,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])) @@ -4355,7 +4447,6 @@ class PropertyPanelManager: traceback.print_exc() return False - def _updateScriptPropertyPanel(self, game_object): """更新脚本属性面板""" # 获取对象上的脚本 @@ -4373,7 +4464,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) @@ -4574,11 +4665,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 @@ -4589,73 +4680,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) @@ -4683,8 +4774,7 @@ class PropertyPanelManager: return unique_names - - def _updateModelMaterialPanel(self,model): + def _updateModelMaterialPanel(self, model): """模型材质属性""" if model.is_empty(): print("警告: 无法在空的 NodePath 上查找材质") @@ -4723,12 +4813,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: @@ -4767,7 +4857,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 @@ -4950,8 +5040,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) @@ -4983,7 +5071,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 @@ -5026,18 +5114,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: @@ -5101,7 +5189,6 @@ class PropertyPanelManager: print(f"检查材质状态时出错: {e}") return "未知材质类型(可尝试编辑)" - def _getTextureModeString(self, mode): """获取纹理模式的字符串表示""" from panda3d.core import TextureStage @@ -5156,7 +5243,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方法 @@ -5164,7 +5251,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 @@ -5174,7 +5261,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) @@ -5213,164 +5300,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: @@ -5401,17 +5486,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()} 应用漫反射贴图") @@ -5434,7 +5519,7 @@ class PropertyPanelManager: node, effect_file, { - "normal_mapping": True, # 启用法线映射支持 + "normal_mapping": True, # 启用法线映射支持 "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, @@ -5472,7 +5557,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}") @@ -5541,7 +5627,7 @@ class PropertyPanelManager: node, effect_file, { - "normal_mapping": True, # 强制启用法线映射 + "normal_mapping": True, # 强制启用法线映射 "render_gbuffer": True, "alpha_testing": needs_alpha, # 根据是否需要透明度决定 "parallax_mapping": False, @@ -5602,7 +5688,6 @@ class PropertyPanelManager: import traceback traceback.print_exc() - def _applyRoughnessTexture_FINAL(self, material, texture_path): """应用粗糙度贴图 - 先编译后绑定策略""" try: @@ -5636,14 +5721,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) @@ -5672,19 +5755,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() @@ -5701,7 +5784,6 @@ class PropertyPanelManager: roughness_stage.setMode(TextureStage.MModulate) node.setTexture(roughness_stage, texture) - # 7. 验证效果 applied_texture = node.getTexture(roughness_stage) if applied_texture: @@ -5812,7 +5894,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, @@ -5896,7 +5978,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 @@ -5976,7 +6058,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, @@ -6078,7 +6160,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 @@ -6109,7 +6191,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槽") # 不再需要手动刷新渲染状态,避免闪烁 @@ -6121,7 +6203,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 @@ -6151,7 +6233,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}") @@ -6162,16 +6244,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 ) @@ -6360,7 +6442,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, @@ -6439,8 +6521,6 @@ class PropertyPanelManager: except: pass - - def _applyEmissionTexture(self, material, texture_path): """应用自发光贴图""" try: @@ -6758,17 +6838,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() @@ -6779,17 +6859,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): @@ -6813,14 +6893,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("❌ 未找到当前选中的模型") @@ -6830,23 +6910,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} 没有材质属性") @@ -6913,7 +6993,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) @@ -6989,7 +7069,7 @@ class PropertyPanelManager: self._updateMaterialAlphaForTransparency(material, default_opacity) # 应用透明渲染效果 - #self._applyTransparentRenderingEffect() + # self._applyTransparentRenderingEffect() print(f"透明着色模型设置完成") print(f" - emission.x = {model_index} (透明着色模型)") @@ -7004,13 +7084,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 # 不透明度滑块(避免混淆,使用不透明度) @@ -7074,11 +7155,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, @@ -7089,7 +7170,6 @@ class PropertyPanelManager: self.world.render_pipeline.prepare_scene(model) print(f"[透明] 不透明度={opacity_slider:.2f} 已同步") - def _applyTransparentRenderingEffect(self): from panda3d.core import TransparencyAttrib """为当前选中的模型应用透明渲染效果(简化版本)""" @@ -7104,7 +7184,7 @@ class PropertyPanelManager: model, "effects/default.yaml", { - "render_gbuffer":True, + "render_gbuffer": True, "alpha_testing": False, "normal_mapping": True, "render_shadow": True, @@ -7113,7 +7193,6 @@ class PropertyPanelManager: sort=100 ) - # 让RenderPipeline自动处理透明材质 # 当emission.x=3时,RenderPipeline会自动设置正确的渲染参数 self.world.render_pipeline.prepare_scene(model) @@ -7241,9 +7320,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 @@ -7281,7 +7360,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}, @@ -7310,16 +7390,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}") @@ -7416,8 +7496,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("✅ 检测到材质已有法线贴图") @@ -7466,7 +7546,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, @@ -7486,7 +7566,7 @@ class PropertyPanelManager: # 多次重新绑定,模拟手动"应用两次"的效果 for i in range(3): - print(f" 第{i+1}次绑定...") + print(f" 第{i + 1}次绑定...") # 等待更长时间确保着色器编译完成 time.sleep(0.05) # 50ms延迟 @@ -7501,9 +7581,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) @@ -7649,7 +7729,6 @@ class PropertyPanelManager: sun_group.setLayout(sun_layout) self._propertyLayout.addWidget(sun_group) - def _onSunAzimuthSliderChanged(self, value): """滑块值改变时的回调""" try: @@ -7710,10 +7789,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: @@ -7986,8 +8065,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() @@ -8031,24 +8110,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) @@ -8075,7 +8154,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 @@ -8109,7 +8188,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 @@ -8143,7 +8222,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: @@ -8161,12 +8240,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}") @@ -8179,7 +8258,7 @@ class PropertyPanelManager: 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))) + 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 @@ -8407,7 +8486,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}") @@ -8423,10 +8502,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 文件确实包含动画数据 - 尝试在其他软件中验证动画 @@ -8458,7 +8537,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 不可用") @@ -8483,7 +8562,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}", @@ -8492,7 +8571,7 @@ try: export_frame_range=True ) print("glTF导出成功") - + except Exception as e: print(f"转换失败: {{e}}") sys.exit(1) @@ -8522,7 +8601,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}") @@ -8544,19 +8623,40 @@ 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 + + original_world_pos = origin_model.getPos(self.world.render) + original_world_hpr = origin_model.getHpr(self.world.render) + original_world_scale = origin_model.getScale(self.world.render) + actor.setPos(origin_model.getPos()) actor.setHpr(origin_model.getHpr()) actor.setScale(origin_model.getScale()) + origin_model.hide() actor.show() - if hasattr(self,'animation_combo'): + #创建人物来维持世界坐标不变 + def maintainWorldPosition(task): + try: + if not actor.isEmpty(): + actor.setPos(self.world.render,original_world_pos) + actor.setHpr(self.world.render,original_world_hpr) + actor.setScale(self.world.render,original_world_scale) + return task.cont + else: + return task.done + except: + return task.done + + taskMgr.add(maintainWorldPosition,f"maintain_anim_pos_{id(actor)}") + + + + if hasattr(self, 'animation_combo'): # 获取原始动画名称(存储在 userData 中) current_index = self.animation_combo.currentIndex() if current_index >= 0: @@ -8570,8 +8670,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 @@ -8584,7 +8683,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 @@ -8603,8 +8702,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 @@ -8644,21 +8742,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()/100) + actor.setScale(origin_model.getScale() / 100) if cmd == "play": origin_model.hide() @@ -8672,12 +8770,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): @@ -8686,4 +8785,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 c1507f18..d5978703 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -18,7 +18,7 @@ from PyQt5.QtCore import Qt, QUrl, QMimeData from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush from PyQt5.sip import wrapinstance from direct.showbase.ShowBaseGlobal import aspect2d -from panda3d.core import ModelRoot, NodePath +from panda3d.core import ModelRoot, NodePath, CollisionNode from QPanda3D.QPanda3DWidget import QPanda3DWidget from scene import util @@ -30,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) @@ -156,7 +216,7 @@ class CustomPanda3DWidget(QPanda3DWidget): event.acceptProposedAction() else: event.ignore() - + def wheelEvent(self, event): """处理滚轮事件""" if event.angleDelta().y() > 0: @@ -1251,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) @@ -1300,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 @@ -1414,39 +1571,57 @@ class CustomTreeWidget(QTreeWidget): 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", + "IMPORTED_MODEL_NODE", # 导入模型节点 "MODEL_NODE", - "TERRAIN_NODE", - "CESIUM_TILESET_NODE" + "TERRAIN_NODE", # 地形节点 + "CESIUM_TILESET_NODE" # 3D Tileset } # 这是一个最佳实践,它让代码的意图变得非常清晰。 @@ -1747,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元素建立父子关系") @@ -1773,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 @@ -1831,7 +2006,7 @@ class CustomTreeWidget(QTreeWidget): elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: indicator_str = "OnViewport" - print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') + #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') if event.source() != self: event.ignore() @@ -1963,7 +2138,7 @@ class CustomTreeWidget(QTreeWidget): return # 默认选中场景根节点,通常是第一个顶级节点 - next_item_to_select = self.topLevelItem(0) + #next_item_to_select = self.topLevelItem(0) # 3. 执行删除循环 deleted_count = 0 @@ -1990,7 +2165,6 @@ 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'): - print(f'11111111111111111111111111,{light_object.casts_shadows}') self.world.render_pipeline.remove_light(light_object) # 从world列表中移除 @@ -2048,25 +2222,7 @@ class CustomTreeWidget(QTreeWidget): # 4. 删除操作完成后,更新UI --- if deleted_count > 0: print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") - - # 检查预备选择的节点是否还有效 (例如,父节点可能也一同被删了) - # 如果next_item_to_select在树中找不到了,就退回到选择根节点 - if next_item_to_select and self.indexFromItem(next_item_to_select).isValid(): - new_selection_item = next_item_to_select - else: - # 如果之前的父节点也一并被删除了,就默认选择场景根节点 - new_selection_item = self.topLevelItem(0) - - if new_selection_item: - # 设置UI树的选择 - self.setCurrentItem(new_selection_item) - # 获取对应的Panda3D节点 - new_panda_node = new_selection_item.data(0, Qt.UserRole) - # 调用您提供的函数来更新选择状态和属性面板 - self.update_selection_and_properties(new_panda_node, new_selection_item) - else: - # 如果连根节点都没有了(例如清空场景),则清空选择 - self.update_selection_and_properties(None, None) + self.update_selection_and_properties(None, None) def delete_item(self, panda_node): """删除指定节点 panda3D(node)- 优化和修复版本""" @@ -2074,6 +2230,14 @@ class CustomTreeWidget(QTreeWidget): print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") return + # #如果有命令管理系统,则使用命令系统 + # if hasattr(self.world,'command_manager') and self.world.command_manager: + # from core.Command_System import DeleteNodeCommand + # parent_node = panda_node.getParent() + # command = DeleteNodeCommand(panda_node,parent_node) + # self.world.command_manager.execute_command(command) + # return + # --- 关键修复:在操作前,安全地获取节点名字 --- node_name_for_logging = panda_node.getName() @@ -2088,7 +2252,6 @@ class CustomTreeWidget(QTreeWidget): if not item: print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") return - try: # 2. 过滤受保护节点 node_type = item.data(0, Qt.UserRole + 1) @@ -2126,6 +2289,24 @@ class CustomTreeWidget(QTreeWidget): 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(): @@ -2192,53 +2373,64 @@ class CustomTreeWidget(QTreeWidget): return top_item return None - def create_model_items(self, model): + def create_model_items(self, model: NodePath): + """ + 【此函数保持不变】 + 创建模型项。 + 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, + 然后完整地展示这些分支。 + """ if not model: print("传入的参数model为空") return - # 创建根节点项 - # root_item = QTreeWidgetItem(self) - # root_item.setText(0, model.getName() or "Unnamed Node") - # root_item.setText(1, model.node().getTypeName()) - # root_item.setIcon(0, self.item_icons.get('model', self.item_icons['default'])) - # 存储NodePath引用以便后续操作 - # root_item.setData(0, Qt.UserRole, model) + # 找到场景树的根节点,我们将把模型节点添加到这里 root_item = self._findSceneRoot() + if not root_item: + print("错误:未能找到场景根节点项") + return - # 递归添加子节点 - self._add_children_recursive(root_item, model) + # 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 - return root_item + # 为这个带标签的节点创建一个树项 + 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) # 可选信息 - def _add_children_recursive(self, parent_item, node_path: NodePath): - """递归添加子节点到树项""" - print(f'开始递归添加子节点') - # 获取所有子节点 - children = node_path.getChildren() + # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) + self._add_all_children_unconditionally(child_item, child_node) - for i in range(children.getNumPaths()): - child_node: NodePath = children.getPath(i) + def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): + """ + 【此函数已更新】 + 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 + """ + for child_node in node_path.getChildren(): - # 过滤条件 - if not child_node.hasTag("is_scene_element"): - print(f"不存在------------------------{child_node.hasTag('is_scene_element')}") - continue + # 新增:检查节点是否为碰撞节点 + if isinstance(child_node.node(), CollisionNode): + # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 + continue # 如果是,则跳过此节点及其所有子节点 - print(f"存在------------------------{child_node.getName()}") # 创建子项 child_item = QTreeWidgetItem(parent_item) - child_item.setText(0, child_node.getName() or f"Child_{i}") - - # 存储NodePath引用 + 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) # 可选信息 - # 添加额外信息(可选) - # self._add_node_info(child_item, child_node) - - # 递归处理子节点的子节点 - if child_node.getNumChildren() > 0: - self._add_children_recursive(child_item, child_node) + # 继续无条件地递归 + if not child_node.is_empty(): + self._add_all_children_unconditionally(child_item, child_node) # ==================== 辅助方法 ==================== def _findSceneRoot(self): @@ -2269,6 +2461,17 @@ 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 @@ -2283,7 +2486,7 @@ class CustomTreeWidget(QTreeWidget): nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) nodeItem.setData(0, Qt.UserRole, node) - nodeItem.setData(0, Qt.UserRole + 1, node_type) + nodeItem.setData(0, Qt.UserRole + 1, tree_type) for child in node.getChildren(): # 递归调用,但我们只关心顶级的nodeItem @@ -2301,7 +2504,7 @@ class CustomTreeWidget(QTreeWidget): node_name = "" try: - if node_type == "IMPORTED_MODEL_NODE": + 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() @@ -2312,7 +2515,7 @@ class CustomTreeWidget(QTreeWidget): 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) # 确保 new_qt_item 成功创建后再继续操作 if new_qt_item: