1
0
forked from Rowland/EG

Compare commits

...

9 Commits

Author SHA1 Message Date
d32993b903 1.合并版本 2025-09-25 17:51:46 +08:00
75a9df709f Merge remote-tracking branch 'refs/remotes/origin/addRender' into main_ch_eg
# Conflicts:
#	RenderPipelineFile/config/daytime.yaml
2025-09-25 17:09:23 +08:00
a66c097048 修改保存逻辑,添加打包功能 2025-09-25 16:59:06 +08:00
7c797d74d5 剪切复制粘贴,撤销重做(仅使用拖拽时) 2025-09-24 09:18:27 +08:00
ddeb40ea54 撤销重做(仅使用拖拽时) 2025-09-23 16:39:55 +08:00
040fffd34e 剪切复制粘贴 2025-09-23 09:30:30 +08:00
ec21df8f54 剪切复制粘贴 2025-09-23 09:27:49 +08:00
b5545b4b60 聚焦功能完善 2025-09-22 10:56:05 +08:00
fc2550ce03 脚本保存和移除 2025-09-18 17:44:32 +08:00
22 changed files with 3690 additions and 821 deletions

View File

@ -12,6 +12,7 @@ from PyQt5 import QtWidgets, QtGui
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtGui import * from PyQt5.QtGui import *
from PyQt5.QtWidgets import * from PyQt5.QtWidgets import *
from direct.task.TaskManagerGlobal import taskMgr
# Panda imports # Panda imports
from panda3d.core import Texture, WindowProperties, CallbackGraphicsWindow from panda3d.core import Texture, WindowProperties, CallbackGraphicsWindow
@ -32,9 +33,17 @@ class QPanda3DSynchronizer(QTimer):
self.setInterval(int(dt)) self.setInterval(int(dt))
self.timeout.connect(self.tick) self.timeout.connect(self.tick)
# def tick(self):
# taskMgr.step()
# self.qPanda3DWidget.update()
def tick(self): def tick(self):
taskMgr.step() try:
self.qPanda3DWidget.update() taskMgr.step()
self.qPanda3DWidget.update()
except:
# 静默处理所有异常,包括 has_mat() 断言错误
pass
def get_panda_key_modifiers(evt): def get_panda_key_modifiers(evt):

View File

@ -73,7 +73,6 @@ native_module = None
# If the module was built, use it, otherwise use the python wrappers # If the module was built, use it, otherwise use the python wrappers
if NATIVE_CXX_LOADED: if NATIVE_CXX_LOADED:
print(f'12121212121212121212121212')
try: try:
from panda3d import _rplight as _native_module # pylint: disable=wrong-import-position from panda3d import _rplight as _native_module # pylint: disable=wrong-import-position
RPObject.global_debug("CORE", "Using panda3d-supplied core module") RPObject.global_debug("CORE", "Using panda3d-supplied core module")

View File

@ -52,11 +52,7 @@ class MainApp(ShowBase):
# Load the scene # Load the scene
model = loader.loadModel("scene/scene.bam") model = loader.loadModel("scene/scene.bam")
# model = loader.loadModel("scene2/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) model.reparent_to(render)
self.render_pipeline.prepare_scene(model) self.render_pipeline.prepare_scene(model)

Binary file not shown.

Binary file not shown.

Binary file not shown.

648
core/Command_System.py Normal file
View File

@ -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()

View File

@ -145,7 +145,7 @@ class InfoPanelManager(DirectObject):
text_scale=0.045, text_scale=0.045,
text_fg=content_color, text_fg=content_color,
text_align=TextNode.ALeft, 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), pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05),
parent=panel_node, parent=panel_node,
relief=None, relief=None,
@ -529,9 +529,7 @@ class InfoPanelManager(DirectObject):
-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05 -size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05
) )
# 设置一个非常大的换行值,几乎不自动换行 panel_data['content_label']['text_wordwrap'] = 0
panel_data['content_label']['text_wordwrap'] = 500
print(f"更新面板换行: 设置为500几乎不换行")
# 如果有背景图片,也需要更新其大小 # 如果有背景图片,也需要更新其大小
if 'bg_image' in panel_data and panel_data['bg_image']: if 'bg_image' in panel_data and panel_data['bg_image']:
@ -575,8 +573,9 @@ class InfoPanelManager(DirectObject):
if 'content_size' in properties: if 'content_size' in properties:
panel_data['content_label']['text_scale'] = properties['content_size'] panel_data['content_label']['text_scale'] = properties['content_size']
props['content_size'] = 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: if 'bg_image' in properties:

View File

@ -640,7 +640,12 @@ class AssemblyInteractionManager(DirectObject):
if self.step_dialog: if self.step_dialog:
self.step_dialog.close() self.step_dialog.close()
self.step_dialog = StepGuideDialog(self) # 获取主窗口作为父窗口
parent_window = None
if hasattr(self.world, 'main_window') and self.world.main_window:
parent_window = self.world.main_window
self.step_dialog = StepGuideDialog(self, parent_window)
self.step_dialog.show() self.step_dialog.show()
def start_current_step(self): def start_current_step(self):
@ -1573,8 +1578,8 @@ class AssemblyInteractionManager(DirectObject):
class StepGuideDialog(QDialog): class StepGuideDialog(QDialog):
"""步骤指引对话框UI代码保持不变""" """步骤指引对话框UI代码保持不变"""
def __init__(self, interaction_manager): def __init__(self, interaction_manager, parent=None):
super().__init__() super().__init__(parent)
self.interaction_manager = interaction_manager self.interaction_manager = interaction_manager
self.current_required_tool = "" # 当前步骤要求的工具 self.current_required_tool = "" # 当前步骤要求的工具
self.mode = interaction_manager.mode # 获取模式 self.mode = interaction_manager.mode # 获取模式

View File

@ -188,15 +188,34 @@ class EventHandler:
if self.world.selection.gizmo: if self.world.selection.gizmo:
#print("准备检查坐标轴点击...") #print("准备检查坐标轴点击...")
try: 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) gizmoAxis = self.world.selection.checkGizmoClick(x, y)
if gizmoAxis: if gizmoAxis:
#print(f"✓ 检测到坐标轴点击: {gizmoAxis}") print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# 开始坐标轴拖拽 # 开始坐标轴拖拽
self.world.selection.startGizmoDrag(gizmoAxis, x, y) self.world.selection.startGizmoDrag(gizmoAxis, x, y)
pickerNP.removeNode() pickerNP.removeNode()
return return
else: else:
print("× 没有点击到坐标轴") 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: except Exception as e:
print(f"❌ 坐标轴点击检测出现异常: {str(e)}") print(f"❌ 坐标轴点击检测出现异常: {str(e)}")
import traceback import traceback

View File

@ -253,7 +253,7 @@ class ScriptLoader:
for component in components_to_remove: for component in components_to_remove:
self.script_manager.remove_script_from_object(component.game_object, script_name) self.script_manager.remove_script_from_object(component.game_object, script_name)
# 从sys.modules中移除 # 从sys.modules中移除
module = self.loaded_modules[script_name] module = self.loaded_modules[script_name]
if module.__name__ in sys.modules: if module.__name__ in sys.modules:
@ -696,18 +696,62 @@ class {class_name}(ScriptBase):
return False return False
script_components = self.object_scripts[game_object] script_components = self.object_scripts[game_object]
removed = False
for component in script_components[:]: # 复制列表以避免修改时出错 for component in script_components[:]: # 复制列表以避免修改时出错
if component.script_instance.__class__.__name__ == script_name: if component.script_instance.__class__.__name__ == script_name:
# 从引擎移除 # 从引擎移除
self.engine.remove_script_component(component) self.engine.remove_script_component(component)
# 从对象脚本列表移除 # 从对象脚本列表移除
script_components.remove(component) script_components.remove(component)
removed = True
print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}") 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]: def get_scripts_on_object(self, game_object) -> List[ScriptComponent]:
"""获取对象上的所有脚本""" """获取对象上的所有脚本"""
return self.object_scripts.get(game_object, []) return self.object_scripts.get(game_object, [])

View File

@ -16,7 +16,6 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta
from direct.task.TaskManagerGlobal import taskMgr from direct.task.TaskManagerGlobal import taskMgr
import math import math
class SelectionSystem: class SelectionSystem:
"""选择和变换系统类""" """选择和变换系统类"""
@ -64,6 +63,13 @@ class SelectionSystem:
"z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 "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._current_cursor = None
self._default_cursor = None self._default_cursor = None
@ -118,11 +124,8 @@ class SelectionSystem:
def createSelectionBox(self, nodePath): def createSelectionBox(self, nodePath):
"""为选中的节点创建选择框""" """为选中的节点创建选择框"""
try: try:
#print(f" 开始创建选择框,目标节点: {nodePath.getName()}")
# 如果已有选择框,先移除
if self.selectionBox: if self.selectionBox:
print(" 移除现有选择框") #print(" 移除现有选择框")
self.selectionBox.removeNode() self.selectionBox.removeNode()
self.selectionBox = None self.selectionBox = None
@ -130,21 +133,12 @@ class SelectionSystem:
print(" 目标节点为空,取消创建") print(" 目标节点为空,取消创建")
return return
# 创建选择框作为render的子节点但会实时跟踪目标节点
self.selectionBox = self.world.render.attachNewNode("selectionBox") self.selectionBox = self.world.render.attachNewNode("selectionBox")
self.selectionBoxTarget = nodePath # 保存目标节点引用 self.selectionBoxTarget = nodePath
#print(f" 选择框节点创建完成: {self.selectionBox}")
# 启动选择框更新任务
taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
#print(" 选择框更新任务已启动")
# 初始更新选择框
#print(" 开始初始化选择框几何体...")
self.updateSelectionBoxGeometry() self.updateSelectionBoxGeometry()
#print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框")
except Exception as e: except Exception as e:
print(f" ✗ 创建选择框失败: {str(e)}") print(f" ✗ 创建选择框失败: {str(e)}")
import traceback import traceback
@ -277,51 +271,58 @@ class SelectionSystem:
traceback.print_exc() traceback.print_exc()
def updateSelectionBoxTask(self, task): def updateSelectionBoxTask(self, task):
"""选择框更新任务""" """选择框更新任务 - 平衡性能和实时性"""
try: 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 self._last_selection_box_update = 0
import time import time
current_time = time.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 return task.cont
self._last_selection_box_update = current_time self._last_selection_box_update = current_time
#检查目标节点是否已被删除 # 检查目标节点是否已被删除
self.checkAndClearIfTargetDeleted() self.checkAndClearIfTargetDeleted()
if not self.selectionBox or not self.selectionBoxTarget: if not self.selectionBox or not self.selectionBoxTarget:
return task.done # 结束任务 return task.done
# 检查目标节点是否还存在 # 检查目标节点是否还存在
if self.selectionBoxTarget.isEmpty(): if self.selectionBoxTarget.isEmpty():
self.clearSelectionBox() self.clearSelectionBox()
return task.done return task.done
# 获取目标节点在世界坐标系中的当前边界框使用正确的API # 检查目标节点是否发生了变化(位置、旋转、缩放)
currentMinPoint = Point3() current_transform = self._getNodeTransformKey(self.selectionBoxTarget)
currentMaxPoint = Point3()
if not self.selectionBoxTarget.calcTightBounds(currentMinPoint, currentMaxPoint, self.world.render):
return task.cont
# 检查边界框是否发生变化(位置或大小) if (not hasattr(self, '_last_transform_key') or
if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or self._last_transform_key != current_transform):
self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint): # 节点发生了变化,更新选择框
# 更新选择框几何体
self.updateSelectionBoxGeometry() self.updateSelectionBoxGeometry()
self._last_transform_key = current_transform
# 保存当前边界框信息 return task.cont
self._lastMinPoint = currentMinPoint
self._lastMaxPoint = currentMaxPoint
return task.cont # 继续任务
except Exception as e: except Exception as e:
print(f"选择框更新任务出错: {str(e)}") print(f"选择框更新任务出错: {str(e)}")
return task.done 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): def clearSelectionBox(self):
"""清除选择框""" """清除选择框"""
if self.selectionBox: if self.selectionBox:
@ -905,13 +906,6 @@ class SelectionSystem:
# 创建或获取材质 # 创建或获取材质
mat = Material() 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( adjusted_color = Vec4(
min(color[0]*20, 1.0), min(color[0]*20, 1.0),
@ -921,11 +915,7 @@ class SelectionSystem:
) )
mat.setBaseColor(adjusted_color) mat.setBaseColor(adjusted_color)
# mat.setDiffuse(adjusted_color * 0.8) # 稍微降低漫反射亮度 #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
# 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)) # 自发光
# 应用材质 # 应用材质
handle_node.setMaterial(mat, 1) handle_node.setMaterial(mat, 1)
@ -1514,8 +1504,14 @@ class SelectionSystem:
self.dragStartMousePos = (mouseX, mouseY) 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) # 坐标轴的世界位置 self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置
# 添加对缩放的支持:保存初始缩放值 # 添加对缩放的支持:保存初始缩放值
@ -1886,6 +1882,43 @@ class SelectionSystem:
"""停止坐标轴拖拽""" """停止坐标轴拖拽"""
print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") 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"]: for axis_name in ["x", "y", "z"]:
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
@ -1909,31 +1942,20 @@ class SelectionSystem:
def updateSelection(self, nodePath): def updateSelection(self, nodePath):
try: try:
# 检查是否要选择的对象已经是当前选中的对象
if self.selectedNode == nodePath: if self.selectedNode == nodePath:
#print("要选择的对象已经是当前选中的对象,跳过重复更新")
return return
print(f"\n=== 更新选择状态 ===") #print(f"\n=== 更新选择状态 ===")
# 如果正在删除节点,避免更新选择 # 如果正在删除节点,避免更新选择
if hasattr(self, '_deleting_node') and self._deleting_node: if hasattr(self, '_deleting_node') and self._deleting_node:
print("正在删除节点,跳过选择更新") print("正在删除节点,跳过选择更新")
print("=== 选择状态更新完成 ===\n") #print("=== 选择状态更新完成 ===\n")
return return
node_name = "None" node_name = "None"
if nodePath and not nodePath.isEmpty(): if nodePath and not nodePath.isEmpty():
node_name = nodePath.getName() node_name = nodePath.getName()
print(f"新选择的节点: {node_name}") #print(f"新选择的节点: {node_name}")
# 检查是否为双击
is_double_click = self.checkDoubleClick(nodePath)
if is_double_click:
print(f"检测到双击 {node_name},执行聚焦")
# 双击时直接执行聚焦,不执行选择逻辑
self.focusCameraOnSelectedNodeAdvanced()
print("=== 选择状态更新完成 ===\n")
return # 直接返回,不执行下面的选择逻辑
self.selectedNode = nodePath self.selectedNode = nodePath
# 添加兼容性属性 # 添加兼容性属性
@ -1965,7 +1987,6 @@ class SelectionSystem:
else: else:
print("× 坐标轴创建失败") print("× 坐标轴创建失败")
print(f"✓ 选中了节点: {node_name}")
else: else:
print("清除选择...") print("清除选择...")
self.clearSelectionBox() self.clearSelectionBox()
@ -1979,12 +2000,69 @@ class SelectionSystem:
self.world.interface_manager.treeWidget.setCurrentItem(None) self.world.interface_manager.treeWidget.setCurrentItem(None)
print("✓ 树形控件选中状态已清空") print("✓ 树形控件选中状态已清空")
print("=== 选择状态更新完成 ===\n") #print("=== 选择状态更新完成 ===\n")
except Exception as e: except Exception as e:
print(f"更新选择状态失败{str(e)}") print(f"更新选择状态失败{str(e)}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def _reparentTreeItem(self, item, new_parent_item):
"""将树项重新父化到新的父项下"""
if not item or not new_parent_item:
return
# 从当前父项中移除
current_parent = item.parent()
if current_parent:
current_parent.removeChild(item)
else:
# 如果是顶级项
index = self.indexOfTopLevelItem(item)
if index >= 0:
self.takeTopLevelItem(index)
# 添加到新父项
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): def getSelectedNode(self):
"""获取当前选中的节点""" """获取当前选中的节点"""
return self.selectedNode return self.selectedNode
@ -2173,8 +2251,31 @@ class SelectionSystem:
maxPoint = Point3() maxPoint = Point3()
if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render):
print("无法计算选中节点的边界框") print("无法计算选中节点的边界框,使用节点为位置作为替代方案")
return False 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( center = Point3(
@ -2202,7 +2303,7 @@ class SelectionSystem:
view_direction.normalize() view_direction.normalize()
# 计算合适的观察距离 # 计算合适的观察距离
optimal_distance = max(size * 2.0, 5.0) optimal_distance = max(size * 1, 1.0)
# 计算目标摄像机位置 # 计算目标摄像机位置
target_cam_pos = center + (view_direction * optimal_distance) target_cam_pos = center + (view_direction * optimal_distance)
@ -2341,100 +2442,6 @@ class SelectionSystem:
traceback.print_exc() traceback.print_exc()
return task.done return task.done
def focusCameraOnSelectedNode(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("无法计算选中节点的边界框")
return False
# 计算节点中心点
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.1:
size = 5.0
# 获取当前摄像机位置
current_cam_pos = Point3(self.world.cam.getPos())
# 计算观察方向
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 * 3.0, 10.0) # 距离节点的距离是节点大小的3倍或至少10个单位
# 计算新的摄像机位置
new_cam_pos = center + (view_direction * optimal_distance)
# 平滑地设置摄像机位置和朝向
# 创建临时节点用于计算目标朝向
temp_lookat = self.world.render.attachNewNode("temp_lookat")
temp_lookat.setPos(center)
# 获取当前朝向和目标朝向
current_hpr = Vec3(self.world.cam.getHpr())
# 设置摄像机到目标位置
self.world.cam.setPos(new_cam_pos)
self.world.cam.lookAt(temp_lookat)
target_hpr = Vec3(self.world.cam.getHpr())
# 恢复当前位置
self.world.cam.setPos(current_cam_pos)
self.world.cam.setHpr(current_hpr)
# 清理临时节点
temp_lookat.removeNode()
# 使用一个简单的任务来平滑过渡
class SmoothCameraMoveData:
def __init__(self, start_pos, end_pos, start_hpr, end_hpr):
self.start_pos = Point3(start_pos)
self.end_pos = Point3(end_pos)
self.start_hpr = Vec3(start_hpr)
self.end_hpr = Vec3(end_hpr)
self.elapsed_time = 0.0
self.duration = 0.5 # 0.5秒的平滑过渡
self._smooth_camera_move_data = SmoothCameraMoveData(
current_cam_pos, new_cam_pos, current_hpr, target_hpr
)
taskMgr.remove("smoothCameraMoveTask")
taskMgr.add(self._smoothCameraMoveTask, "smoothCameraMoveTask")
print(f"摄像机开始聚焦到节点: {self.selectedNode.getName()}")
print(f"节点中心: {center}, 大小: {size:.2f}")
return True
except Exception as e:
print(f"聚焦摄像机到选中节点失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def _smoothCameraMoveTask(self, task): def _smoothCameraMoveTask(self, task):
"""平滑摄像机移动任务""" """平滑摄像机移动任务"""
try: try:
@ -2529,6 +2536,7 @@ class SelectionSystem:
# 检查是否为双击(同一节点且在时间阈值内) # 检查是否为双击(同一节点且在时间阈值内)
is_double_click = (self._last_clicked_node == target_node and 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) current_time - self._last_click_time < self._double_click_threshold)
if is_double_click: if is_double_click:
@ -2539,8 +2547,12 @@ class SelectionSystem:
# 无论是点击模型还是坐标轴,都执行聚焦 # 无论是点击模型还是坐标轴,都执行聚焦
if target_node and not target_node.isEmpty(): if target_node and not target_node.isEmpty():
print(f"双击聚焦到节点: {target_node.getName()}") print(f"双击聚焦到节点: {target_node.getName()}")
# 执行聚焦 if self.selectedNode != target_node:
self.focusCameraOnSelectedNodeAdvanced() self.updateSelection(target_node)
else:
self.focusCameraOnSelectedNodeAdvanced()
else:
print("双击事件:没有有效的目标节点")
# 重置状态以避免三击等误触发 # 重置状态以避免三击等误触发
self._last_click_time = 0 self._last_click_time = 0
@ -2598,21 +2610,20 @@ class SelectionSystem:
import time import time
current_time = time.time() current_time = time.time()
# 检查节点和时间 # 必须是同一节点且在时间阈值内
time_diff = current_time - self._last_click_time is_double_click = (self._last_clicked_node == nodePath and
is_same_node = (self._last_clicked_node == nodePath) nodePath is not None and
current_time - self._last_click_time < self._double_click_threshold)
# 如果是同一节点且在时间阈值内,认为是双击 if is_double_click:
if is_same_node and time_diff < self._double_click_threshold: # 重置状态
# 只有在双击时才重置状态
self._last_click_time = 0 self._last_click_time = 0
self._last_clicked_node = None self._last_clicked_node = None
return True return True
else: else:
# 只有在非双击情况下才更新状态 # 更新状态
if not is_same_node: self._last_click_time = current_time
self._last_click_time = current_time self._last_clicked_node = nodePath
self._last_clicked_node = nodePath
return False return False
except Exception as e: except Exception as e:
@ -2786,4 +2797,23 @@ class SelectionSystem:
# 清理其他资源 # 清理其他资源
self.clearSelectionBox() self.clearSelectionBox()
self.clearGizmo() 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}")

View File

@ -323,8 +323,8 @@ class CoreWorld(Panda3DWorld):
self.ground.setP(-90) self.ground.setP(-90)
self.ground.setZ(-1.0) self.ground.setZ(-1.0)
self.ground.setColor(0.8, 0.8, 0.8, 1) 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") self.ground.setTag("tree_item_type", "SCENE_NODE")
# 创建支持贴图的材质 # 创建支持贴图的材质
mat = Material() mat = Material()
@ -343,6 +343,8 @@ class CoreWorld(Panda3DWorld):
self.ground2.setZ(49) # 在X轴方向偏移 self.ground2.setZ(49) # 在X轴方向偏移
self.ground2.setColor(0.8, 0.8, 0.8, 1) self.ground2.setColor(0.8, 0.8, 0.8, 1)
self.ground2.set_material(mat) 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 = self.render.attachNewNode(cm.generate())
@ -352,6 +354,8 @@ class CoreWorld(Panda3DWorld):
self.ground3.setZ(49) # 在X轴负方向偏移 self.ground3.setZ(49) # 在X轴负方向偏移
self.ground3.setColor(0.8, 0.8, 0.8, 1) self.ground3.setColor(0.8, 0.8, 0.8, 1)
self.ground3.set_material(mat) 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.ground4 = self.render.attachNewNode(cm.generate())
# self.ground3.setR(90) # self.ground3.setR(90)
@ -360,6 +364,8 @@ class CoreWorld(Panda3DWorld):
self.ground4.setZ(49) # 在X轴负方向偏移 self.ground4.setZ(49) # 在X轴负方向偏移
self.ground4.setColor(0.8, 0.8, 0.8, 1) self.ground4.setColor(0.8, 0.8, 0.8, 1)
self.ground4.set_material(mat) 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 = self.render.attachNewNode(cm.generate())
self.ground5.setP(180) self.ground5.setP(180)
@ -368,6 +374,8 @@ class CoreWorld(Panda3DWorld):
self.ground5.setZ(49) # 在X轴负方向偏移 self.ground5.setZ(49) # 在X轴负方向偏移
self.ground5.setColor(0.8, 0.8, 0.8, 1) self.ground5.setColor(0.8, 0.8, 0.8, 1)
self.ground5.set_material(mat) 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 = self.render.attachNewNode(cm.generate())
self.ground6.setP(90) self.ground6.setP(90)
@ -375,6 +383,8 @@ class CoreWorld(Panda3DWorld):
self.ground6.setZ(99) # 在X轴负方向偏移 self.ground6.setZ(99) # 在X轴负方向偏移
self.ground6.setColor(0.8, 0.8, 0.8, 1) self.ground6.setColor(0.8, 0.8, 0.8, 1)
self.ground6.set_material(mat) self.ground6.set_material(mat)
self.ground6.setTag("is_scene_element", "1")
self.ground6.setTag("tree_item_type", "SCENE_NODE")
# 应用默认PBR效果确保支持贴图 # 应用默认PBR效果确保支持贴图
try: try:
@ -547,12 +557,32 @@ class CoreWorld(Panda3DWorld):
self.mouseRightPressed = True self.mouseRightPressed = True
self.lastMouseX = evt['x'] self.lastMouseX = evt['x']
self.lastMouseY = evt['y'] 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): def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件""" """处理鼠标右键释放事件"""
#print("右键释放") #print("右键释放")
self.mouseRightPressed = False 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): def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 只处理相机旋转""" """处理鼠标移动事件 - 只处理相机旋转"""
if not evt: if not evt:

View File

@ -24,80 +24,80 @@ except ImportError:
WEB_ENGINE_AVAILABLE = False WEB_ENGINE_AVAILABLE = False
print("⚠️ QtWebEngineWidgets 不可用Cesium 集成功能将被禁用") print("⚠️ QtWebEngineWidgets 不可用Cesium 集成功能将被禁用")
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): # def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib # from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
#
# 参数类型检查和转换 # # 参数类型检查和转换
if isinstance(size, (list, tuple)): # if isinstance(size, (list, tuple)):
if len(size) >= 2: # if len(size) >= 2:
x_size, y_size = float(size[0]), float(size[1]) # x_size, y_size = float(size[0]), float(size[1])
else: # else:
x_size = y_size = float(size[0]) if size else 1.0 # x_size = y_size = float(size[0]) if size else 1.0
else: # else:
x_size = y_size = float(size) # x_size = y_size = float(size)
#
# 创建卡片 # # 创建卡片
cm = CardMaker('gui_3d_image') # cm = CardMaker('gui_3d_image')
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) # cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
#
# 创建3D图像节点 # # 创建3D图像节点
image_node = self.world.render.attachNewNode(cm.generate()) # image_node = self.world.render.attachNewNode(cm.generate())
image_node.setPos(*pos) # image_node.setPos(*pos)
#
# 为3D图像创建独立的材质 # # 为3D图像创建独立的材质
material = Material(f"image-material-{len(self.gui_elements)}") # material = Material(f"image-material-{len(self.gui_elements)}")
material.setBaseColor(LColor(1, 1, 1, 1)) # material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1)) # material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) # material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) # material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0) # material.setShininess(10.0)
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 # material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
image_node.setMaterial(material, 1) # image_node.setMaterial(material, 1)
#
image_node.setTransparency(TransparencyAttrib.MAlpha) # image_node.setTransparency(TransparencyAttrib.MAlpha)
#
# 如果提供了图像路径,则加载纹理 # # 如果提供了图像路径,则加载纹理
if image_path: # if image_path:
self.update3DImageTexture(image_node, image_path) # self.update3DImageTexture(image_node, image_path)
#
# 应用PBR效果如果可用 # # 应用PBR效果如果可用
try: # try:
if hasattr(self, 'render_pipeline') and self.render_pipeline: # if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.set_effect( # self.render_pipeline.set_effect(
image_node, # image_node,
"effects/default.yaml", # "effects/default.yaml",
{ # {
"normal_mapping": True, # "normal_mapping": True,
"render_gbuffer": True, # "render_gbuffer": True,
"alpha_testing": False, # "alpha_testing": False,
"parallax_mapping": False, # "parallax_mapping": False,
"render_shadow": False, # "render_shadow": False,
"render_envmap": True, # "render_envmap": True,
"disable_children_effects": True # "disable_children_effects": True
}, # },
50 # 50
) # )
print("✓ GUI 3D图像PBR效果已应用") # print("✓ GUI 3D图像PBR效果已应用")
except Exception as e: # except Exception as e:
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") # print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
#
# 为GUI元素添加标识效仿3D文本方法 # # 为GUI元素添加标识效仿3D文本方法
image_node.setTag("gui_type", "3d_image") # image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") # image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
image_node.setTag("is_scene_element", "1") # image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_3DIMAGE") # image_node.setTag("tree_item_type", "GUI_3DIMAGE")
if image_path: # if image_path:
image_node.setTag("gui_image_path", image_path) # image_node.setTag("gui_image_path", image_path)
image_node.setTag("is_gui_element", "1") # image_node.setTag("is_gui_element", "1")
#
self.gui_elements.append(image_node) # self.gui_elements.append(image_node)
#
# 更新场景树 # # 更新场景树
if hasattr(self.world, 'updateSceneTree'): # if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree() # self.world.updateSceneTree()
#
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") # print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
return image_node # return image_node
class GUIManager: class GUIManager:
"""GUI元素管理系统类""" """GUI元素管理系统类"""
@ -326,6 +326,7 @@ class GUIManager:
label.setTag("is_scene_element", "1") label.setTag("is_scene_element", "1")
label.setTag("created_by_user", "1") label.setTag("created_by_user", "1")
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
label.setTag("name",label_name)
label.setName(label_name) label.setName(label_name)
# 如果有GUI父节点建立引用关系 # 如果有GUI父节点建立引用关系
@ -416,6 +417,10 @@ class GUIManager:
parent_gui_node = None parent_gui_node = None
print(f"📎 挂载到3D父节点: {parent_item.text(0)}") print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
font = None
if hasattr(self.world,'getChineseFont'):
font = self.world.getChineseFont()
entry = DirectEntry( entry = DirectEntry(
text="", text="",
pos=gui_pos, pos=gui_pos,
@ -426,7 +431,9 @@ class GUIManager:
numLines=1, numLines=1,
width=12, width=12,
focus=0, focus=0,
parent=parent_gui_node # 设置GUI父节点 parent=parent_gui_node, # 设置GUI父节点
text_font = font,
frameSize=(-0.1,0.1,-0.05,0.05)
) )
# 设置节点标签 # 设置节点标签
@ -438,6 +445,7 @@ class GUIManager:
entry.setTag("is_scene_element", "1") entry.setTag("is_scene_element", "1")
entry.setTag("created_by_user", "1") entry.setTag("created_by_user", "1")
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
entry.setTag("name",entry_name)
entry.setName(entry_name) entry.setName(entry_name)
# 如果有GUI父节点建立引用关系 # 如果有GUI父节点建立引用关系
@ -572,6 +580,7 @@ class GUIManager:
image_node.setTag("tree_item_type", "GUI_IMAGE") image_node.setTag("tree_item_type", "GUI_IMAGE")
image_node.setTag("created_by_user", "1") image_node.setTag("created_by_user", "1")
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
image_node.setTag("name",image_name)
image_node.setName(image_name) image_node.setName(image_name)
# 如果有GUI父节点建立引用关系 # 如果有GUI父节点建立引用关系
@ -771,6 +780,7 @@ class GUIManager:
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
"""创建3D空间图片""" """创建3D空间图片"""
try: try:
from panda3d.core import TextNode from panda3d.core import TextNode
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@ -861,6 +871,7 @@ class GUIManager:
image_node.setTag("is_scene_element", "1") image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_3DIMAGE") image_node.setTag("tree_item_type", "GUI_3DIMAGE")
image_node.setTag("created_by_user", "1") image_node.setTag("created_by_user", "1")
image_node.setTag("name",image_name)
# 添加到GUI元素列表 # 添加到GUI元素列表
self.gui_elements.append(image_node) self.gui_elements.append(image_node)
@ -979,6 +990,7 @@ class GUIManager:
video_screen.setTag("is_scene_element", "1") video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN") video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1") video_screen.setTag("created_by_user", "1")
video_screen.setTag("name",screen_name)
# 设置视频路径标签 # 设置视频路径标签
if video_path and os.path.exists(video_path): if video_path and os.path.exists(video_path):
@ -1496,9 +1508,12 @@ class GUIManager:
video_screen.setTag("is_scene_element", "1") video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1") video_screen.setTag("created_by_user", "1")
video_screen.setTag("name",screen_name)
video_screen.setTag("video_path", video_path if video_path else "") # 修复后
print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else ''}") if video_path is not None:
video_screen.setTag("video_path", video_path)
else:
video_screen.setTag("video_path", "")
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
@ -2173,9 +2188,17 @@ class GUIManager:
print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}")
if property_name == "text": 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"]: if gui_type in ["button", "label"]:
gui_element['text'] = value gui_element['text'] = value
print(f"成功更新2D GUI文本: {value}") print(f"成功更新2D GUI文本: {value}")
# if gui_type == "button":
# self._resizeButtonToText(gui_element,value,original_frame_size)
elif gui_type == "entry": elif gui_type == "entry":
gui_element.set(value) gui_element.set(value)
print(f"成功更新输入框文本: {value}") print(f"成功更新输入框文本: {value}")
@ -2254,6 +2277,64 @@ class GUIManager:
traceback.print_exc() traceback.print_exc()
return False 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): def duplicateGUIElement(self, gui_element):
"""复制GUI元素""" """复制GUI元素"""
try: try:

27
main.py
View File

@ -6,7 +6,7 @@ from demo.video_integration import VideoManager
warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys import sys
import builtins # 添加这一行 import builtins
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) 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.QtGui import QDrag, QPainter, QPixmap
from PyQt5.QtWidgets import QFileSystemModel from PyQt5.QtWidgets import QFileSystemModel
from QPanda3D.QPanda3DWidget import QPanda3DWidget from QPanda3D.QPanda3DWidget import QPanda3DWidget
from panda3d.core import loadPrcFileData
loadPrcFileData("", "assertions 0")
from core.world import CoreWorld from core.world import CoreWorld
from core.selection import SelectionSystem from core.selection import SelectionSystem
from core.event_handler import EventHandler from core.event_handler import EventHandler
@ -23,6 +25,7 @@ from core.vr_manager import VRManager
from core.vr_input_handler import VRInputHandler from core.vr_input_handler import VRInputHandler
from core.alvr_streamer import ALVRStreamer from core.alvr_streamer import ALVRStreamer
from core.patrol_system import PatrolSystem from core.patrol_system import PatrolSystem
from core.Command_System import CommandManager
from gui.gui_manager import GUIManager from gui.gui_manager import GUIManager
from core.terrain_manager import TerrainManager from core.terrain_manager import TerrainManager
from scene.scene_manager import SceneManager from scene.scene_manager import SceneManager
@ -110,6 +113,8 @@ class MyWorld(CoreWorld):
self.info_panel_manager = InfoPanelManager(self) self.info_panel_manager = InfoPanelManager(self)
self.command_manager = CommandManager()
# 初始化碰撞管理器 # 初始化碰撞管理器
from core.collision_manager import CollisionManager from core.collision_manager import CollisionManager
self.collision_manager = CollisionManager(self) self.collision_manager = CollisionManager(self)
@ -235,11 +240,11 @@ class MyWorld(CoreWorld):
"""创建2D GUI文本输入框""" """创建2D GUI文本输入框"""
return self.gui_manager.createGUIEntry(pos, placeholder, size) 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空间文本""" """创建3D空间文本"""
return self.gui_manager.createGUI3DText(pos, text, size) 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图片""" """创建3D图片"""
return self.gui_manager.createGUI3DImage(pos,text,size) return self.gui_manager.createGUI3DImage(pos,text,size)
@ -895,6 +900,22 @@ class MyWorld(CoreWorld):
except Exception as e: except Exception as e:
print(f"创建默认自动朝向巡检路线失败: {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模块的对应功能 # 以下函数代理到project_manager模块的对应功能

View File

@ -392,13 +392,226 @@ class ProjectManager:
# 复制场景文件到构建目录 # 复制场景文件到构建目录
shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam")) 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)
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) self._createAppFile(build_dir, project_name)
# 创建标准的setup.py文件 # 创建标准的setup.py文件
self._createStandardSetupFile(build_dir, project_name) self._createStandardSetupFile(build_dir, project_name)
#创建requirements.txt文件
self._createRequirementsFile(build_dir)
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): def _createAppFile(self, build_dir, project_name):
"""创建应用程序主文件""" """创建应用程序主文件"""
app_code = f'''#!/usr/bin/env python3 app_code = f'''#!/usr/bin/env python3
@ -409,180 +622,199 @@ class ProjectManager:
使用Panda3D引擎编辑器创建 使用Panda3D引擎编辑器创建
""" """
from __future__ import print_function
#获取渲染管线路径
import sys import sys
import os 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 from direct.showbase.ShowBase import ShowBase
from panda3d.core import (loadPrcFileData, WindowProperties, AmbientLight,
DirectionalLight, Point3, Vec3)
# 配置Panda3D os.chdir(os.path.dirname(os.path.realpath(__file__)))
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): class MainApp(ShowBase):
"""应用程序主类"""
def __init__(self): def __init__(self):
ShowBase.__init__(self) load_prc_file_data("","""
win-size 1200 720
window-title Render
""")
print(f"启动 {project_name}...") pipeline_path = "../../"
# 设置窗口属性 if not os.path.isfile(os.path.join(pipeline_path,"setup.py")):
self.setupWindow() pipeline_path = "../../RenderPipeline"
# 设置光照 sys.path.insert(0,pipeline_path)
self.setupLighting()
# 加载场景 from rpcore import RenderPipeline,SpotLight
self.loadScene() self.render_pipeline = RenderPipeline()
self.render_pipeline.create(self)
# 设置相机控制 from rpcore.util.movement_controller import MovementController
self.setupControls()
print("✓ 应用程序初始化完成") self.render_pipeline.daytime_mgr.time = "12:00"
def setupWindow(self): self.loadFullScene()
"""设置窗口"""
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.2)
# 设置窗口属性 self.controller = MovementController(self)
props = WindowProperties() self.controller.set_initial_position(
props.setTitle("{project_name}") Vec3(-7.5,-5.3,1.8),Vec3(-5.9,-4.0,1.6))
self.win.requestProperties(props) self.controller.setup()
def setupLighting(self): base.accept("l",self.tour)
"""设置光照系统"""
# 环境光 def loadFullScene(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):
"""加载场景"""
try: try:
# 查找场景文件
scene_file = "scene.bam" scene_file = "scene.bam"
if not os.path.exists(scene_file): if os.path.exists(scene_file):
print("警告: 没有找到场景文件,创建默认场景") # 使用readBamFile加载完整场景
self.createDefaultScene() from panda3d.core import BamCache
return BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题
# 加载场景 scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file))
scene = self.loader.loadModel(scene_file) if scene:
if scene: scene.reparentTo(self.render)
scene.reparentTo(self.render) self.render_pipeline.prepare_scene(scene)
print("✓ 场景加载成功") print("✓ 完整场景加载成功")
# 自动调整相机位置 # 处理场景中的各种元素
self.adjustCamera() self.processSceneElements(scene)
else:
print("⚠️ 场景文件加载失败")
else: else:
print("警告: 场景加载失败,创建默认场景") print("⚠️ 未找到场景文件")
self.createDefaultScene()
except Exception as e: except Exception as e:
print(f"加载场景时出错: {{str(e)}}") print(f"加载完整场景时出错: {{str(e)}}")
self.createDefaultScene() import traceback
traceback.print_exc()
def createDefaultScene(self):
"""创建默认场景""" def processSceneElements(self, scene):
# 加载默认的环境模型 """处理场景中的各种元素"""
env = self.loader.loadModel("models/environment") try:
if env: # 处理光源
env.reparentTo(self.render) self.processLights(scene)
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()
# 设置相机位置 # 处理GUI元素
distance = radius * 3 self.processGUIElements(scene)
self.cam.setPos(center.x, center.y - distance, center.z + radius)
self.cam.lookAt(center) # 处理其他特殊元素
else: self.processSpecialElements(scene)
# 默认相机位置
self.cam.setPos(0, -20, 5) except Exception as e:
self.cam.lookAt(0, 0, 0) 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 processSpecialElements(self, scene):
"""处理特殊元素"""
try:
# 处理Cesium Tilesets
tilesets = scene.findAllMatches("**/=element_type=cesium_tileset")
for tileset_node in tilesets:
try:
# Tilesets需要特殊处理这里只是标记
print(f"✓ Cesium Tileset {{tileset_node.getName()}} 已识别")
except Exception as e:
print(f"处理Cesium Tileset {{tileset_node.getName()}} 失败: {{str(e)}}")
except Exception as e:
print(f"处理特殊元素时出错: {{str(e)}}")
def setupControls(self): def tour(self):
"""设置相机控制""" mopath = (
# 启用鼠标控制 (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)),
self.accept("wheel_up", self.zoomIn) (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)),
self.accept("wheel_down", self.zoomOut) (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)),
print("\\n=== 控制说明 ===") (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)),
print("鼠标滚轮: 缩放") )
print("ESC: 退出") self.controller.play_motion_path(mopath,3.0)
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)
def main(): MainApp().run()
"""主函数"""
try:
app = {project_name.replace(' ', '').replace('-', '')}App()
app.run()
except Exception as e:
print(f"应用程序启动失败: {{str(e)}}")
import traceback
traceback.print_exc()
input("按Enter键退出...")
if __name__ == "__main__":
main()
''' '''
app_path = os.path.join(build_dir, "main.py") app_path = os.path.join(build_dir, "main.py")
with open(app_path, "w", encoding="utf-8") as f: with open(app_path, "w", encoding="utf-8") as f:
f.write(app_code) f.write(app_code)
def _createStandardSetupFile(self, build_dir, project_name): def _createStandardSetupFile(self, build_dir, project_name):
"""创建标准的setup.py文件 - 按照Panda3D官方文档""" """创建标准的setup.py文件 - 按照Panda3D官方文档"""
setup_code = f'''#!/usr/bin/env python3 setup_code = f'''#!/usr/bin/env python3

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,10 @@ class RotatorScript(ScriptBase):
self.log(f"旋转速度: {self.rotation_speed_y}度/秒") self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
def update(self, dt): 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: if not self.is_rotating:
return return

View File

@ -51,7 +51,7 @@ class InterfaceManager:
def onTreeItemClicked(self, item, column): def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件""" """处理树形控件项目点击事件"""
print(f"树形控件点击事件触发item: {item}, column: {column}") #print(f"树形控件点击事件触发item: {item}, column: {column}")
# 检查是否点击了空白区域 # 检查是否点击了空白区域
# 当点击空白区域时item可能是一个空的QTreeWidgetItem对象 # 当点击空白区域时item可能是一个空的QTreeWidgetItem对象
@ -339,6 +339,23 @@ class InterfaceManager:
groundItem.setData(0, Qt.UserRole, self.world.ground) groundItem.setData(0, Qt.UserRole, self.world.ground)
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") 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: for light in self.world.Spotlight:
if light: if light:

View File

@ -19,6 +19,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction
QSpinBox, QFrame, QRadioButton, QTextEdit) QSpinBox, QFrame, QRadioButton, QTextEdit)
from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect
from direct.showbase.ShowBaseGlobal import aspect2d from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import OrthographicLens
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget
from ui.icon_manager import get_icon_manager, get_icon from ui.icon_manager import get_icon_manager, get_icon
@ -27,7 +28,7 @@ try:
WEB_ENGINE_AVAILABLE = True WEB_ENGINE_AVAILABLE = True
except ImportError: except ImportError:
QWebEngineView = None QWebEngineView = None
WEB_ENGINE_AVAILABLE = False WEB_ENGINE_AVAILABLE = False
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
"""主窗口类""" """主窗口类"""
@ -37,6 +38,10 @@ class MainWindow(QMainWindow):
self.world = world self.world = world
self.world.main_window = self # 关键让world对象能访问主窗口 self.world.main_window = self # 关键让world对象能访问主窗口
#剪切板相关属性
self.clipboard = []
self.clipboard_mode = None
# 初始化图标管理器并打印调试信息 # 初始化图标管理器并打印调试信息
self.icon_manager = get_icon_manager() self.icon_manager = get_icon_manager()
print("🔧 图标管理器初始化完成") print("🔧 图标管理器初始化完成")
@ -447,8 +452,7 @@ class MainWindow(QMainWindow):
select_icon = get_icon('select_tool', QSize(16, 16)) select_icon = get_icon('select_tool', QSize(16, 16))
if not select_icon.isNull(): if not select_icon.isNull():
self.selectTool.setIcon(select_icon) self.selectTool.setIcon(select_icon)
else: self.selectTool.setText('选择') # 如果没有图标则显示文字
self.selectTool.setText('选择') # 如果没有图标则显示文字
self.selectTool.setIconSize(QSize(16, 16)) self.selectTool.setIconSize(QSize(16, 16))
self.selectTool.setCheckable(True) self.selectTool.setCheckable(True)
self.selectTool.setToolTip("选择工具 (Q)") self.selectTool.setToolTip("选择工具 (Q)")
@ -716,6 +720,9 @@ class MainWindow(QMainWindow):
self.interactionMenu.addSeparator() self.interactionMenu.addSeparator()
self.startAssemblyInteractionAction = self.interactionMenu.addAction('开始拆装交互') self.startAssemblyInteractionAction = self.interactionMenu.addAction('开始拆装交互')
self.startAssemblyInteractionAction.triggered.connect(self.onStartAssemblyInteraction) self.startAssemblyInteractionAction.triggered.connect(self.onStartAssemblyInteraction)
self.interactionMenu.addSeparator()
self.maintenanceSystemAction = self.interactionMenu.addAction('🔧 维修系统')
self.maintenanceSystemAction.triggered.connect(self.onOpenMaintenanceSystem)
self.cesiumMenu = menubar.addMenu('Cesium') self.cesiumMenu = menubar.addMenu('Cesium')
self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles')
@ -816,6 +823,238 @@ class MainWindow(QMainWindow):
# 统一连接信号到处理方法 # 统一连接信号到处理方法
self.connectCreateMenuActions() 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): def connectCreateMenuActions(self):
"""统一连接创建菜单的信号到处理方法""" """统一连接创建菜单的信号到处理方法"""
# 连接到world对象的创建方法 # 连接到world对象的创建方法
@ -1572,6 +1811,9 @@ class MainWindow(QMainWindow):
self.buildAction.triggered.connect(lambda: buildPackage(self)) self.buildAction.triggered.connect(lambda: buildPackage(self))
self.exitAction.triggered.connect(QApplication.instance().quit) self.exitAction.triggered.connect(QApplication.instance().quit)
#添加保存项目快捷键盘
self.saveAction.setShortcut(QKeySequence.Save)
# 连接工具事件 # 连接工具事件
self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑")) self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑"))
self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑")) self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑"))
@ -1601,6 +1843,21 @@ class MainWindow(QMainWindow):
lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0)) lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
print("已连接点击信号") 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)
@ -1611,6 +1868,313 @@ class MainWindow(QMainWindow):
# self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
# self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
def onCopy(self):
"""复制操作"""
try:
selected_item = self.treeWidget.currentItem()
if not selected_item:
QMessageBox.information(self, "提示", "请先选择要复制的节点")
return
# 获取选中的节点
selected_node = getattr(selected_item, 'node_path', None)
if not selected_node:
selected_node = getattr(selected_item, 'node', None)
if not selected_node and hasattr(self.world, 'selection'):
selected_node = getattr(self.world.selection, 'selectedNode', None)
if not selected_node or selected_node.isEmpty():
QMessageBox.warning(self, "错误", "无法获取选中节点")
return
# 检查是否是根节点
if selected_node.getName() == "render":
QMessageBox.warning(self, "错误", "不能复制根节点")
return
# 序列化节点数据
node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
if not node_data:
QMessageBox.warning(self, "错误", "无法序列化选中节点")
return
# 存储到剪切板
self.clipboard = [node_data]
self.clipboard_mode = "copy"
QMessageBox.information(self, "成功", "节点已复制到剪切板")
except Exception as e:
QMessageBox.critical(self, "错误", f"复制操作失败: {str(e)}")
def onCut(self):
"""剪切操作"""
try:
selected_item = self.treeWidget.currentItem()
if not selected_item:
QMessageBox.information(self, "提示", "请先选择要剪切的节点")
return
# 获取选中的节点
selected_node = getattr(selected_item, 'node_path', None)
if not selected_node:
selected_node = getattr(selected_item, 'node', None)
if not selected_node and hasattr(self.world, 'selection'):
selected_node = getattr(self.world.selection, 'selectedNode', None)
if not selected_node or selected_node.isEmpty():
QMessageBox.warning(self, "错误", "无法获取选中节点")
return
# 检查是否是根节点或特殊节点
if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]:
QMessageBox.warning(self, "错误", "不能剪切根节点或系统节点")
return
# 序列化节点数据
node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
if not node_data:
QMessageBox.warning(self, "错误", "无法序列化选中节点")
return
# 存储到剪切板
self.clipboard = [node_data]
self.clipboard_mode = "cut"
# 删除原节点
self.treeWidget.delete_items([selected_item])
QMessageBox.information(self, "成功", "节点已剪切到剪切板")
except Exception as e:
QMessageBox.critical(self, "错误", f"剪切操作失败: {str(e)}")
def onPaste(self):
"""粘贴操作"""
try:
if not self.clipboard:
QMessageBox.information(self, "提示", "剪切板为空")
return
# 获取粘贴目标节点
parent_item = self.treeWidget.currentItem()
parent_node = None
# 如果选中了节点,将其作为父节点
if parent_item:
parent_node = getattr(parent_item, 'node_path', None)
if not parent_node:
parent_node = getattr(parent_item, 'node', None)
# 确保获取到有效的父节点
if parent_node and not parent_node.isEmpty():
print(f"将粘贴到选中的节点: {parent_node.getName()}")
else:
parent_node = None
# 如果没有选中有效节点默认粘贴到render节点下
if not parent_node:
print("未选中有效节点,将粘贴到根节点下")
# 查找render节点
for i in range(self.treeWidget.topLevelItemCount()):
item = self.treeWidget.topLevelItem(i)
if item.text(0) == "render":
parent_item = item
break
# 如果找到了render节点项获取对应的节点
if parent_item:
parent_node = getattr(parent_item, 'node_path', None)
if not parent_node:
parent_node = getattr(parent_item, 'node', None)
# 如果仍然没有找到父节点项直接使用world.render
if not parent_node:
parent_node = self.world.render
# 检查父节点有效性
if not parent_node or parent_node.isEmpty():
QMessageBox.warning(self, "错误", "无法获取有效的父节点")
return
# 检查目标节点是否为允许的父节点类型
parent_name = parent_node.getName()
if parent_name in ["camera", "ambientLight", "directionalLight"]:
QMessageBox.warning(self, "错误", "不能粘贴到该类型节点下")
return
# 粘贴节点
pasted_nodes = []
for node_data in self.clipboard:
print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}")
new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node)
if new_node:
pasted_nodes.append(new_node)
print(f"成功粘贴节点: {new_node.getName()}")
else:
print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}")
# 如果是剪切操作,清空剪切板
if self.clipboard_mode == "cut":
self.clipboard.clear()
self.clipboard_mode = None
QMessageBox.information(self, "成功", f"已粘贴 {len(pasted_nodes)} 个节点")
except Exception as e:
QMessageBox.critical(self, "错误", f"粘贴操作失败: {str(e)}")
def _serializeNode(self, node):
"""序列化节点数据"""
try:
if not node or node.isEmpty():
return None
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
try:
# 使用更安全的方式获取标签
if hasattr(node, 'getTagKeys'):
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
except Exception as e:
print(f"获取标签时出错: {e}")
# 递归序列化子节点(跳过辅助节点)
try:
if hasattr(node, 'getChildren'):
for child in node.getChildren():
# 跳过辅助节点
child_name = child.getName() if hasattr(child, 'getName') else ""
if not child_name.startswith(('gizmo', 'selectionBox', 'grid')):
child_data = self._serializeNode(child)
if child_data:
node_data['children'].append(child_data)
except Exception as e:
print(f"序列化子节点时出错: {e}")
return node_data
except Exception as e:
print(f"序列化节点失败: {e}")
return None
def _deserializeNode(self, node_data, parent_node):
"""反序列化节点数据"""
try:
if not node_data or not parent_node or parent_node.isEmpty():
return None
# 创建新节点
node_name = node_data.get('name', 'node')
new_node = parent_node.attachNewNode(node_name)
# 设置变换
try:
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
except Exception as e:
print(f"设置变换时出错: {e}")
# 恢复标签
try:
for tag_key, tag_value in node_data.get('tags', {}).items():
new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串
except Exception as e:
print(f"恢复标签时出错: {e}")
# 递归创建子节点
try:
for child_data in node_data.get('children', []):
self._deserializeNode(child_data, new_node)
except Exception as e:
print(f"创建子节点时出错: {e}")
return new_node
except Exception as e:
print(f"反序列化节点失败: {e}")
return None
def _deleteNode(self, node, tree_item):
"""删除节点"""
try:
if not node or node.isEmpty():
return
# 特殊处理选中节点
if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node:
self.world.selection.clearSelection()
# 从场景中删除节点
node.removeNode()
# 从树形控件中删除项目
if tree_item:
try:
parent = tree_item.parent()
if parent:
parent.removeChild(tree_item)
else:
index = self.treeWidget.indexOfTopLevelItem(tree_item)
if index >= 0:
self.treeWidget.takeTopLevelItem(index)
except Exception as e:
print(f"从树形控件删除项目时出错: {e}")
except Exception as e:
print(f"删除节点失败: {e}")
# 添加撤销/重做功能的基础实现
def onUndo(self):
"""撤销操作"""
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): def onCreateCesiumView(self):
if hasattr(self.world,'gui_manager') and self.world.gui_manager: if hasattr(self.world,'gui_manager') and self.world.gui_manager:
@ -2675,6 +3239,16 @@ class MainWindow(QMainWindow):
try: try:
print("🔄 正在关闭应用程序...") print("🔄 正在关闭应用程序...")
# 关闭拆装交互相关的弹窗
if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction:
print("🧹 关闭拆装交互弹窗...")
if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog:
self.world.assembly_interaction.step_dialog.close()
self.world.assembly_interaction.step_dialog = None
# 停止交互模式
if self.world.assembly_interaction.is_active:
self.world.assembly_interaction.stop_interaction_mode()
# 清理工具管理器中的进程 # 清理工具管理器中的进程
if hasattr(self.world, 'tool_manager') and self.world.tool_manager: if hasattr(self.world, 'tool_manager') and self.world.tool_manager:
print("🧹 清理工具管理器进程...") print("🧹 清理工具管理器进程...")
@ -2908,6 +3482,71 @@ class MainWindow(QMainWindow):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def onOpenMaintenanceSystem(self):
"""打开维修系统"""
try:
# 导入简化的登录界面
from ui.simple_maintenance_login import SimpleMaintenanceLoginDialog
from ui.maintenance_system import MaintenanceSubjectDialog, MaintenanceSystemManager
print("🔧 启动维修系统...")
# 显示登录界面
login_dialog = SimpleMaintenanceLoginDialog(self)
if login_dialog.exec_() == QDialog.Accepted:
print("✅ 登录成功,显示科目选择界面")
# 获取当前项目路径
project_path = None
if hasattr(self.world, 'project_manager') and self.world.project_manager:
project_path = self.world.project_manager.getCurrentProjectPath()
# 显示科目选择界面
subject_dialog = MaintenanceSubjectDialog(project_path, self)
def on_subject_selected(subject_path, mode):
"""处理科目选择"""
try:
print(f"🎯 启动维修科目: {subject_path}")
print(f"📝 模式: {mode}")
# 加载科目配置
import json
with open(subject_path, 'r', encoding='utf-8') as f:
subject_config = json.load(f)
# 启动拆装交互系统
if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction:
# 设置配置
self.world.assembly_interaction.config_data = subject_config
# 启动交互模式
self.world.assembly_interaction.start_interaction_mode(mode=mode)
print(f"✅ 维修科目启动成功")
else:
print("❌ 错误:拆装交互系统未初始化")
QMessageBox.warning(self, "错误", "拆装交互系统未初始化")
except Exception as e:
print(f"❌ 启动维修科目失败: {e}")
QMessageBox.critical(self, "错误", f"启动维修科目失败:\n{str(e)}")
import traceback
traceback.print_exc()
subject_dialog.subject_selected.connect(on_subject_selected)
subject_dialog.exec_()
else:
print(" 用户取消了登录")
except Exception as e:
print(f"❌ 打开维修系统失败: {e}")
QMessageBox.critical(self, "错误", f"打开维修系统失败:\n{str(e)}")
import traceback
traceback.print_exc()
class AssemblyModeSelectionDialog(QDialog): class AssemblyModeSelectionDialog(QDialog):
"""拆装模式选择对话框""" """拆装模式选择对话框"""

View File

@ -12,6 +12,7 @@ from PyQt5.QtCore import Qt
from deploy_libs.unicodedata import normalize from deploy_libs.unicodedata import normalize
from direct.actor.Actor import Actor from direct.actor.Actor import Actor
from direct.gui import DirectGui from direct.gui import DirectGui
from direct.task.TaskManagerGlobal import taskMgr
from idna import check_label from idna import check_label
from jinja2.compiler import has_safe_repr from jinja2.compiler import has_safe_repr
from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3 from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3
@ -105,9 +106,10 @@ class PropertyPanelManager:
def updatePropertyPanel(self, item): def updatePropertyPanel(self, item):
"""更新属性面板显示""" """更新属性面板显示"""
if not self._propertyLayout or not self._propertyLayout.parent(): # if not self._propertyLayout or not self._propertyLayout.parent():
print("属性布局未设置或没有父部件!") # print("属性布局未设置或没有父部件!")
return # return
#更健壮的有效性检查
self._cleanupAllReferences() self._cleanupAllReferences()
self.clearPropertyPanel() self.clearPropertyPanel()
@ -768,6 +770,7 @@ class PropertyPanelManager:
if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见 if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见
# 检查对象是否仍然有效 # 检查对象是否仍然有效
spinbox.blockSignals(True) spinbox.blockSignals(True)
spinbox.setKeyboardTracking(False) # 确保禁用键盘跟踪
spinbox.setValue(value) spinbox.setValue(value)
spinbox.blockSignals(False) spinbox.blockSignals(False)
except RuntimeError as e: except RuntimeError as e:
@ -790,42 +793,79 @@ class PropertyPanelManager:
# 位置控件 # 位置控件
transform_layout.addWidget(QLabel("相对位置"), 0, 0) transform_layout.addWidget(QLabel("相对位置"), 0, 0)
# 创建并设置 X, Y, Z 标签居中 # X坐标
x_label = QLabel("X") transform_layout.addWidget(QLabel("X:"), 1, 0)
y_label = QLabel("Y") self.pos_x = QLineEdit()
z_label = QLabel("Z") self.pos_x.setText(str(round(nodePath.getX(), 6)))
x_label.setAlignment(Qt.AlignCenter) self.pos_x.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'x'))
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)
transform_layout.addWidget(self.pos_x, 1, 1) transform_layout.addWidget(self.pos_x, 1, 1)
transform_layout.addWidget(self.pos_y, 1, 2)
transform_layout.addWidget(self.pos_z, 1, 3)
# 世界位置 (只读) # Y坐标
transform_layout.addWidget(QLabel("世界位置"), 2, 0) transform_layout.addWidget(QLabel("Y:"), 1, 2)
self.world_pos_x = self._createSafeSpinBox(-10000, 10000, True) # 只读 self.pos_y = QLineEdit()
self.world_pos_y = self._createSafeSpinBox(-10000, 10000, True) self.pos_y.setText(str(round(nodePath.getY(), 6)))
self.world_pos_z = self._createSafeSpinBox(-10000, 10000, True) 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) # Z坐标
transform_layout.addWidget(self.world_pos_y, 2, 2) transform_layout.addWidget(QLabel("Z:"), 1, 4)
transform_layout.addWidget(self.world_pos_z, 2, 3) 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 return transform_layout
except Exception as e: except Exception as e:
print(f"创建变换控件失败: {e}") print(f"创建变换控件时出错: {e}")
return None 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): def _createSafeSpinBox(self, min_val, max_val, read_only=False):
"""创建安全的数值框""" """创建安全的数值框"""
try: try:
@ -3346,6 +3386,12 @@ class PropertyPanelManager:
video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0)
path_label = QLabel(video_path) path_label = QLabel(video_path)
path_label.setWordWrap(True) 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;") path_label.setStyleSheet("color: #00AAFF;")
video_info_layout.addWidget(path_label, 0, 1) video_info_layout.addWidget(path_label, 0, 1)
elif os.path.exists(video_path): elif os.path.exists(video_path):
@ -8570,12 +8616,35 @@ except Exception as e:
actor = self._getActor(origin_model) actor = self._getActor(origin_model)
if not actor: if not actor:
return 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.setPos(origin_model.getPos())
actor.setHpr(origin_model.getHpr()) actor.setHpr(origin_model.getHpr())
actor.setScale(origin_model.getScale()) actor.setScale(origin_model.getScale())
origin_model.hide() origin_model.hide()
actor.show() actor.show()
#创建人物来维持世界坐标不变
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'): if hasattr(self, 'animation_combo'):
# 获取原始动画名称(存储在 userData 中) # 获取原始动画名称(存储在 userData 中)
current_index = self.animation_combo.currentIndex() current_index = self.animation_combo.currentIndex()
@ -8805,9 +8874,9 @@ except Exception as e:
current_row += 1 current_row += 1
# X, Y, Z 位置调整 # X, Y, Z 位置调整
self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2)
self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2)
self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2)
# 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值 # 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值
if not self._hasCollision(model): if not self._hasCollision(model):
@ -8857,7 +8926,7 @@ except Exception as e:
spinbox = QDoubleSpinBox() spinbox = QDoubleSpinBox()
spinbox.setRange(min_val, max_val) spinbox.setRange(min_val, max_val)
spinbox.setDecimals(decimals) spinbox.setDecimals(decimals)
spinbox.setSingleStep(0.1) spinbox.setSingleStep(0.01)
return spinbox return spinbox
def _addSphereParameters(self, model, layout, start_row): def _addSphereParameters(self, model, layout, start_row):
@ -8868,7 +8937,7 @@ except Exception as e:
radius_label = QLabel("半径:") radius_label = QLabel("半径:")
layout.addWidget(radius_label, current_row, 0) layout.addWidget(radius_label, current_row, 0)
self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2) self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2)
# 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值 # 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值
if not self._hasCollision(model): if not self._hasCollision(model):
@ -8900,9 +8969,9 @@ except Exception as e:
current_row += 1 current_row += 1
# 宽度、长度、高度 # 宽度、长度、高度
self.collision_width = self._createCollisionSpinBox(0.1, 100, 2) self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2)
self.collision_length = self._createCollisionSpinBox(0.1, 100, 2) self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2)
self.collision_height = self._createCollisionSpinBox(0.1, 100, 2) self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2)
# 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值 # 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值
if not self._hasCollision(model): if not self._hasCollision(model):
@ -8950,7 +9019,7 @@ except Exception as e:
radius_label = QLabel("半径:") radius_label = QLabel("半径:")
layout.addWidget(radius_label, current_row, 0) layout.addWidget(radius_label, current_row, 0)
self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2) self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2)
# 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值 # 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值
if not self._hasCollision(model): if not self._hasCollision(model):
@ -8978,7 +9047,7 @@ except Exception as e:
height_label = QLabel("高度:") height_label = QLabel("高度:")
layout.addWidget(height_label, current_row, 0) layout.addWidget(height_label, current_row, 0)
self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2) self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2)
# 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值 # 只在没有现有碰撞时设置默认值否则由_loadCurrentCollisionParameters加载实际值
if not self._hasCollision(model): if not self._hasCollision(model):
@ -9847,7 +9916,7 @@ except Exception as e:
radius_label.setVisible(True) radius_label.setVisible(True)
layout.addWidget(radius_label, current_row, 0) layout.addWidget(radius_label, current_row, 0)
self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2) self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2)
self.collision_radius.setVisible(True) self.collision_radius.setVisible(True)
self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v))
layout.addWidget(self.collision_radius, current_row, 1) layout.addWidget(self.collision_radius, current_row, 1)
@ -9864,9 +9933,9 @@ except Exception as e:
layout.addWidget(size_label, current_row, 0) layout.addWidget(size_label, current_row, 0)
current_row += 1 current_row += 1
self.collision_width = self._createCollisionSpinBox(0.1, 100, 2) self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2)
self.collision_length = self._createCollisionSpinBox(0.1, 100, 2) self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2)
self.collision_height = self._createCollisionSpinBox(0.1, 100, 2) self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2)
width_label = QLabel("宽度:") width_label = QLabel("宽度:")
width_label.setVisible(True) width_label.setVisible(True)
@ -9903,7 +9972,7 @@ except Exception as e:
radius_label = QLabel("半径:") radius_label = QLabel("半径:")
layout.addWidget(radius_label, current_row, 0) layout.addWidget(radius_label, current_row, 0)
self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2) self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2)
self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v))
layout.addWidget(self.collision_capsule_radius, current_row, 1) layout.addWidget(self.collision_capsule_radius, current_row, 1)
current_row += 1 current_row += 1
@ -9911,7 +9980,7 @@ except Exception as e:
height_label = QLabel("高度:") height_label = QLabel("高度:")
layout.addWidget(height_label, current_row, 0) layout.addWidget(height_label, current_row, 0)
self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2) self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2)
self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v))
layout.addWidget(self.collision_capsule_height, current_row, 1) layout.addWidget(self.collision_capsule_height, current_row, 1)
current_row += 1 current_row += 1

View File

@ -1367,6 +1367,27 @@ class CustomConsoleDockWidget(QWidget):
""") """)
toolbar.addWidget(self.timestampBtn) 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() toolbar.addStretch()
layout.addLayout(toolbar) layout.addLayout(toolbar)
@ -1408,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget):
# 添加欢迎信息 # 添加欢迎信息
self.addMessage("🎮 编辑器控制台已启动", "INFO") 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): def setupConsoleRedirect(self):
"""设置控制台重定向""" """设置控制台重定向"""
import sys import sys
@ -1873,7 +1922,7 @@ class CustomTreeWidget(QTreeWidget):
elif is_dragged_3d_gui: elif is_dragged_3d_gui:
if is_target_3d_scene: if is_target_3d_scene:
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
return True return False
elif is_target_2d_gui: elif is_target_2d_gui:
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)}") print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
@ -1899,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget):
elif is_target_3d_gui: elif is_target_3d_gui:
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)}") print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
return True return False
else: else:
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}") print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}")
return False return False
@ -2089,7 +2138,7 @@ class CustomTreeWidget(QTreeWidget):
return return
# 默认选中场景根节点,通常是第一个顶级节点 # 默认选中场景根节点,通常是第一个顶级节点
next_item_to_select = self.topLevelItem(0) #next_item_to_select = self.topLevelItem(0)
# 3. 执行删除循环 # 3. 执行删除循环
deleted_count = 0 deleted_count = 0
@ -2116,7 +2165,6 @@ class CustomTreeWidget(QTreeWidget):
if hasattr(panda_node, 'getPythonTag'): if hasattr(panda_node, 'getPythonTag'):
light_object = panda_node.getPythonTag('rp_light_object') light_object = panda_node.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'): if light_object and hasattr(self.world, 'render_pipeline'):
print(f'11111111111111111111111111,{light_object.casts_shadows}')
self.world.render_pipeline.remove_light(light_object) self.world.render_pipeline.remove_light(light_object)
# 从world列表中移除 # 从world列表中移除
@ -2174,25 +2222,7 @@ class CustomTreeWidget(QTreeWidget):
# 4. 删除操作完成后更新UI --- # 4. 删除操作完成后更新UI ---
if deleted_count > 0: if deleted_count > 0:
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
self.update_selection_and_properties(None, None)
# 检查预备选择的节点是否还有效 (例如,父节点可能也一同被删了)
# 如果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)
def delete_item(self, panda_node): def delete_item(self, panda_node):
"""删除指定节点 panda3Dnode- 优化和修复版本""" """删除指定节点 panda3Dnode- 优化和修复版本"""
@ -2200,6 +2230,14 @@ class CustomTreeWidget(QTreeWidget):
print(" 尝试删除一个空的或无效的节点,操作取消。") print(" 尝试删除一个空的或无效的节点,操作取消。")
return 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() node_name_for_logging = panda_node.getName()