addRender #7

Merged
Hector merged 30 commits from addRender into main 2025-09-29 03:26:04 +00:00
34 changed files with 10860 additions and 1950 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -351,7 +351,11 @@ class RenderPipeline(RPObject):
continue
material = state.get_attrib(MaterialAttrib).get_material()
shading_model = material.emission.x
if material.emission is not None:
shading_model = material.emission.x
else:
shading_model = 0.0
# SHADING_MODEL_TRANSPARENT
if shading_model == 3:

View File

@ -52,6 +52,11 @@ class MainApp(ShowBase):
# Load the scene
model = loader.loadModel("scene/scene.bam")
# model = loader.loadModel("scene2/Scene.bam")
model_0 = self.loader.loadModel("/home/tiger/下载/Benci/source/s65/s65/s65.fbx")
model_0.reparentTo(self.render)
model_0.setScale(0.01)
model_0.setPos(-8, 42, 0)
model_0.setHpr(0, 90, 0)
model.reparent_to(render)
self.render_pipeline.prepare_scene(model)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -17,13 +17,18 @@ sys.path.insert(0, render_pipeline_file_path)
icons_path = os.path.join(project_root, "icons")
sys.path.insert(0, icons_path)
# 现在可以导入并运行主程序
if __name__ == "__main__":
args = sys.argv[1:]
# args = "/home/tiger/桌面/Test1"
# args = "C:/Users/29381/Desktop/1"
print(f'Path is {args}')
# 将整个列表转换为字符串(包括方括号)
args_str = ''.join(args)
from main import run
if args:
run(args[0])
run(args_str)
# run(args)
else:
run()

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

@ -1,6 +1,8 @@
# 修改后的 InfoPanelManager.py
from xml.sax.handler import property_encoding
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
from direct.gui.DirectGui import DirectFrame, DirectLabel
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import TextNode, Vec4, NodePath
@ -144,7 +146,7 @@ class InfoPanelManager(DirectObject):
text_scale=0.045,
text_fg=content_color,
text_align=TextNode.ALeft,
text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行
text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行
pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05),
parent=panel_node,
relief=None,
@ -182,12 +184,62 @@ class InfoPanelManager(DirectObject):
panel_node.setTag("gui_type", "info_panel")
panel_node.setTag("panel_id", panel_id)
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
panel_node.setTag("is_gui_element",'1')
panel_node.setTag("tree_item_type","INFO_PANEL")
panel_node.setTag("supports_3d_position_editing","1")
# 如果有背景图片,保存背景图片路径
if bg_image:
panel_node.setTag("image_path", bg_image)
if not visible:
panel_node.hide()
# 将面板添加到场景树
#self._addPanelToSceneTree(panel_node, panel_id)
if hasattr(self.world, 'gui_elements'):
self.world.gui_elements.append(panel_node)
return panel_node
def _addPanelToSceneTree(self, panel_node, panel_id):
"""
将信息面板添加到场景树中
"""
try:
# 获取树形控件
if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
# 查找根节点项
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
root_item = item
break
if root_item:
# 使用现有的 add_node_to_tree_widget 方法添加节点
qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL")
if qt_item:
print(f"✅ 信息面板 {panel_id} 已添加到场景树")
# 选中创建的节点
tree_widget.setCurrentItem(qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(panel_node, qt_item)
else:
print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败")
else:
print("⚠️ 未找到场景树根节点,无法添加信息面板")
else:
print("⚠️ 无法访问场景树控件,信息面板未添加到场景树")
except Exception as e:
print(f"❌ 添加信息面板到场景树时出错: {e}")
import traceback
traceback.print_exc()
def setPanelBackgroundImage(self, panel_id, image_path):
"""
为指定面板设置背景图片
@ -480,9 +532,7 @@ class InfoPanelManager(DirectObject):
-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05
)
# 设置一个非常大的换行值,几乎不自动换行
panel_data['content_label']['text_wordwrap'] = 500
print(f"更新面板换行: 设置为500几乎不换行")
panel_data['content_label']['text_wordwrap'] = 0
# 如果有背景图片,也需要更新其大小
if 'bg_image' in panel_data and panel_data['bg_image']:
@ -526,8 +576,9 @@ class InfoPanelManager(DirectObject):
if 'content_size' in properties:
panel_data['content_label']['text_scale'] = properties['content_size']
props['content_size'] = properties['content_size']
current_size = props.get('size',(1.0,0.6))
# 当字体大小改变时,仍然保持较大的换行值
panel_data['content_label']['text_wordwrap'] = 500
panel_data['content_label']['text_wordwrap'] = 0
# 更新背景图片
if 'bg_image' in properties:
@ -758,11 +809,23 @@ class InfoPanelManager(DirectObject):
# 设置GUI类型标记和支持3D编辑的标记
panel_node.setTag("gui_type", "info_panel_3d")
panel_node.setTag("panel_id", panel_id)
panel_node.setTag("is_gui_element", "1") # 添加此标记确保节点被识别为GUI元素
panel_node.setTag("is_scene_element", "1") # 添加此标记确保节点被识别为场景元素
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
panel_node.setTag("tree_item_type", "INFO_PANEL_3D") # 添加树节点类型标记
# 如果有背景图片,保存背景图片路径
if bg_image:
panel_node.setTag("bg_image_path", bg_image)
if not visible:
panel_node.hide()
# 将面板添加到场景树
#self._addPanelToSceneTree(panel_node, panel_id)
if hasattr(self.world, 'gui_elements'):
self.world.gui_elements.append(panel_node)
return panel_node
def update3DPanelContent(self, panel_id, title=None, content=None):
@ -1208,6 +1271,101 @@ class InfoPanelManager(DirectObject):
traceback.print_exc()
return None
def onCreateSampleInfoPanel(self):
"""创建示例天气信息面板(模拟数据)"""
try:
# 获取中文字体
from panda3d.core import TextNode
font = self.world.getChineseFont() if self.world.getChineseFont() else None
# 使用唯一的面板ID
import time
unique_id = f"weather_info_{int(time.time())}"
# 创建示例面板
weather_panel = self.createInfoPanel(
panel_id=unique_id, # 使用唯一ID
position=(1.32, 0.68),
size=(1, 0.6),
bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景
border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框
title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题
content_color=(0.95, 0.95, 0.95, 1.0),
font=font,
bg_image="/home/tiger/图片/内部信息框2@2x.png"
)
# 更新面板标题
self.updatePanelContent(unique_id, title="北京天气")
self._addPanelToSceneTree(weather_panel, unique_id)
# 立即显示加载中信息
self.updatePanelContent(unique_id, content="正在获取天气数据...")
self.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0)
print("✓ 示例天气信息面板已创建")
except Exception as e:
print(f"✗ 创建示例天气信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}")
def getRealWeatherData(self):
"""获取真实天气数据"""
try:
import requests
import json
from datetime import datetime
# 请求天气数据
url = "https://wttr.in/Beijing?format=j1"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON数据
weather_data = response.json()
# 提取当前天气信息
current_condition = weather_data['current_condition'][0]
weather_desc = current_condition['weatherDesc'][0]['value']
temp_c = current_condition['temp_C']
feels_like = current_condition['FeelsLikeC']
humidity = current_condition['humidity']
pressure = current_condition['pressure']
visibility = current_condition['visibility']
wind_speed = current_condition['windspeedKmph']
wind_dir = current_condition['winddir16Point']
# 提取空气质量(如果可用)
air_quality = "N/A"
if 'air_quality' in weather_data and weather_data['air_quality']:
if 'us-epa-index' in current_condition:
air_quality_index = current_condition['air_quality_index']
air_quality = f"指数: {air_quality_index}"
# 获取更新时间
update_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 格式化显示内容
content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}"
return content
except requests.exceptions.Timeout:
return "错误: 获取天气数据超时"
except requests.exceptions.ConnectionError:
return "错误: 网络连接失败"
except requests.exceptions.HTTPError as e:
return f"HTTP错误: {e}"
except json.JSONDecodeError:
return "错误: 无法解析天气数据"
except KeyError as e:
return f"错误: 天气数据格式不正确 (缺少字段: {e})"
except Exception as e:
return f"获取天气数据失败: {str(e)}"
# 示例数据源函数
def getRealtimeData():

View File

@ -309,40 +309,81 @@ class CollisionManager:
CollisionPlane, CollisionPolygon, Plane, Vec3
)
bounds = model.getBounds()
if bounds.isEmpty():
# 获取考虑变换后的实际尺寸和中心
transformed_info = self._getTransformedModelInfo(model)
if not transformed_info:
# 默认小球体
return CollisionSphere(Point3(0, 0, 0), 1.0)
center = bounds.getCenter()
radius = bounds.getRadius()
center = transformed_info['center']
radius = transformed_info['radius']
actual_size = transformed_info['size']
scale_factor = transformed_info['scale_factor']
# 自动选择最适合的形状
if shape_type == 'auto':
shape_type = self._determineOptimalShape(model, bounds)
shape_type = self._determineOptimalShape(model, transformed_info)
if shape_type == 'sphere':
return CollisionSphere(center, kwargs.get('radius', radius))
# 优化球形碰撞体
sphere_radius = kwargs.get('radius', radius)
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
sphere_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
return CollisionSphere(sphere_center, sphere_radius)
elif shape_type == 'box':
# 创建包围盒
min_point = bounds.getMin()
max_point = bounds.getMax()
# 优化盒型碰撞体 - 更精确的尺寸和位置控制(考虑缩放)
# 获取自定义尺寸,如果没有提供则使用变换后的实际尺寸
width = kwargs.get('width', actual_size.x)
length = kwargs.get('length', actual_size.y)
height = kwargs.get('height', actual_size.z)
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
box_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
# 计算盒子的最小和最大点(基于偏移后的中心)
half_width = width / 2
half_length = length / 2
half_height = height / 2
min_point = Point3(box_center.x - half_width, box_center.y - half_length, box_center.z - half_height)
max_point = Point3(box_center.x + half_width, box_center.y + half_length, box_center.z + half_height)
return CollisionBox(min_point, max_point)
elif shape_type == 'capsule':
# 创建胶囊体(适合角色)
height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z))
radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3))
point_a = Point3(center.x, center.y, bounds.getMin().z + radius)
point_b = Point3(center.x, center.y, bounds.getMax().z - radius)
return CollisionCapsule(point_a, point_b, radius)
# 优化胶囊体碰撞 - 更合理的比例和位置控制(考虑缩放)
# 使用变换后的实际高度,或自定义高度
custom_height = kwargs.get('height', actual_size.z)
# 更合理的半径计算:基于变换后模型宽度的平均值
default_radius = min(actual_size.x, actual_size.y) / 2.5 # 稍微小于模型的宽度
custom_radius = kwargs.get('radius', min(default_radius, custom_height * 0.4))
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
capsule_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
# 计算胶囊体的两个端点(确保半径不会超出高度)
effective_height = max(custom_height, custom_radius * 2.1) # 确保高度至少是半径的2倍多一点
point_a = Point3(capsule_center.x, capsule_center.y, capsule_center.z - effective_height/2 + custom_radius)
point_b = Point3(capsule_center.x, capsule_center.y, capsule_center.z + effective_height/2 - custom_radius)
return CollisionCapsule(point_a, point_b, custom_radius)
elif shape_type == 'plane':
# 创建平面(适合地面、墙面)
# 优化平面碰撞 - 支持位置偏移和更灵活的法向量
normal = kwargs.get('normal', Vec3(0, 0, 1))
point = kwargs.get('point', center)
plane = Plane(normal, point)
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
plane_point = kwargs.get('point', Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z))
# 标准化法向量
normal.normalize()
plane = Plane(normal, plane_point)
return CollisionPlane(plane)
elif shape_type == 'polygon':
@ -364,13 +405,13 @@ class CollisionManager:
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
return collision_poly
else:
print("⚠️ 多边形至少需要3个顶点回退到球体")
#print("⚠️ 多边形至少需要3个顶点回退到球体")
return CollisionSphere(center, radius)
def _determineOptimalShape(self, model, bounds):
def _determineOptimalShape(self, model, transformed_info):
"""根据模型特征自动确定最适合的碰撞体形状"""
# 获取模型尺寸比例
size = bounds.getMax() - bounds.getMin()
# 获取变换后的模型尺寸比例
size = transformed_info['size']
max_dim = max(size.x, size.y, size.z)
min_dim = min(size.x, size.y, size.z)
@ -399,6 +440,87 @@ class CollisionManager:
else: # 其他用包围盒
return 'box'
def _getTransformedModelInfo(self, model):
"""获取考虑变换后的模型信息
Args:
model: 模型节点
Returns:
dict: 包含变换后的尺寸中心半径等信息
"""
try:
# 获取原始包围盒
bounds = model.getBounds()
if bounds.isEmpty():
return None
# 获取模型的变换矩阵
transform = model.getTransform()
scale = model.getScale()
# 计算缩放因子(取三轴缩放的平均值)
scale_factor = (abs(scale.x) + abs(scale.y) + abs(scale.z)) / 3.0
# 获取原始尺寸
original_size = bounds.getMax() - bounds.getMin()
# 应用缩放到尺寸
actual_size = Vec3(
original_size.x * abs(scale.x),
original_size.y * abs(scale.y),
original_size.z * abs(scale.z)
)
# 获取变换后的中心点(在世界坐标系中)
original_center = bounds.getCenter()
if hasattr(model, 'getPos'):
# 模型在世界坐标系中的位置
world_center = model.getPos(model.getParent() if model.getParent() else model)
center = Point3(world_center.x, world_center.y, world_center.z)
else:
# 如果无法获取世界位置,使用原始中心
center = original_center
# 计算变换后的半径(考虑缩放)
original_radius = bounds.getRadius()
transformed_radius = original_radius * scale_factor
# 调试信息
print(f"🔍 模型 {model.getName()} 变换信息:")
print(f" 原始尺寸: {original_size}")
print(f" 缩放因子: {scale}")
print(f" 变换后尺寸: {actual_size}")
print(f" 变换后半径: {transformed_radius:.2f}")
return {
'center': center,
'radius': transformed_radius,
'size': actual_size,
'scale_factor': scale_factor,
'original_size': original_size,
'scale': scale,
'transform': transform
}
except Exception as e:
print(f"⚠️ 获取模型变换信息失败: {e}")
# 回退到原始包围盒
bounds = model.getBounds()
if bounds.isEmpty():
return None
original_size = bounds.getMax() - bounds.getMin()
return {
'center': bounds.getCenter(),
'radius': bounds.getRadius(),
'size': original_size,
'scale_factor': 1.0,
'original_size': original_size,
'scale': Vec3(1, 1, 1),
'transform': None
}
def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
"""创建鼠标射线"""
# 组合掩码

View File

@ -167,7 +167,7 @@ class EventHandler:
picker.addCollider(pickerNP, queue)
picker.traverse(self.world.render)
print(f"碰撞检测结果数量: {queue.getNumEntries()}")
#print(f"碰撞检测结果数量: {queue.getNumEntries()}")
# 射线检测结果处理
hitPos = None
@ -184,10 +184,19 @@ class EventHandler:
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
# 优先检查是否点击了坐标轴
print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
if self.world.selection.gizmo:
print("准备检查坐标轴点击...")
#print("准备检查坐标轴点击...")
try:
highlighted_axis = self.world.selection.gizmoHighlightAxis
if highlighted_axis:
print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽")
# 直接使用高亮轴开始拖拽
self.world.selection.startGizmoDrag(highlighted_axis, x, y)
pickerNP.removeNode()
return
# 如果没有高亮轴,再尝试检测点击
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
if gizmoAxis:
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
@ -197,22 +206,32 @@ class EventHandler:
return
else:
print("× 没有点击到坐标轴")
# gizmoAxis = self.world.selection.checkGizmoClick(x, y)
# if gizmoAxis:
# #print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# # 开始坐标轴拖拽
# self.world.selection.startGizmoDrag(gizmoAxis, x, y)
# pickerNP.removeNode()
# return
# else:
# print("× 没有点击到坐标轴")
except Exception as e:
print(f"❌ 坐标轴点击检测出现异常: {str(e)}")
import traceback
traceback.print_exc()
print("继续处理模型选择...")
print("继续处理碰撞结果...")
#print("继续处理碰撞结果...")
if hitPos and hitNode:
print(f"✓ 检测到碰撞,开始处理点击事件")
print(f"GUI编辑模式: {self.world.guiEditMode}")
print(f"当前工具: {self.world.currentTool}")
#print(f"✓ 检测到碰撞,开始处理点击事件")
#print(f"GUI编辑模式: {self.world.guiEditMode}")
#print(f"当前工具: {self.world.currentTool}")
# 处理GUI编辑模式
if self.world.guiEditMode:
print("处理GUI编辑模式点击")
#print("处理GUI编辑模式点击")
# 检查是否点击了GUI元素
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
if clickedGUI:
@ -411,103 +430,6 @@ class EventHandler:
import traceback
traceback.print_exc()
def mousePressEventRight(self,evt):
"""处理鼠标右键按下事件"""
print(f"当前工具: {self.world.currentTool}")
# 检查是否是地形编辑模式
if self.world.currentTool == "地形编辑":
self._handleTerrainEdit(evt, "subtract") # 降低地形
return
# 其他右键处理逻辑可以在这里添加
print("鼠标右键事件处理")
def _handleTerrainEdit(self,evt,operation):
try:
x = evt.get('x',0)
y = evt.get('y',0)
winWidth,winHeight = self.world.getWindowSize()
mx = 2.0 * x/float(winWidth) - 1.0
my = 1.0 -2.0*y/float(winHeight)
nearPoint = Point3()
farPoint = Point3()
self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint)
worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint)
worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint)
picker = CollisionTraverser()
queue = CollisionHandlerQueue()
pickerNode = CollisionNode('terrain_edit_ray')
pickerNP = self.world.cam.attachNewNode(pickerNode)
from panda3d.core import BitMask32
pickerNode.setFromCollideMask(BitMask32.allOn()) # 检查所有碰撞
# 使用相机坐标系的点创建射线
direction = farPoint - nearPoint
direction.normalize()
pickerNode.addSolid(CollisionRay(nearPoint, direction))
picker.addCollider(pickerNP, queue)
picker.traverse(self.world.render)
print(f"地形碰撞检测结果数量: {queue.getNumEntries()}")
# 射线检测结果处理
hitPos = None
hitNode = None
if queue.getNumEntries() > 0:
# 遍历所有碰撞结果,找到地形节点
for i in range(queue.getNumEntries()):
entry = queue.getEntry(i)
collided_node = entry.getIntoNodePath()
print(f"碰撞到节点: {collided_node.getName()}")
# 检查是否是地形节点
for terrain_info in self.world.terrain_manager.terrains:
terrain_node = terrain_info['node']
if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node):
hitPos = entry.getSurfacePoint(self.world.render)
hitNode = collided_node
print(f"找到地形节点: {terrain_node.getName()}")
# 修改地形高度
x_pos, y_pos = hitPos.getX(), hitPos.getY()
success = self.world.modifyTerrainHeight(
terrain_info, x_pos, y_pos, radius=3.0, strength=0.3, operation=operation)
if success:
print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})")
# 显示射线
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
else:
print("✗ 地形编辑失败")
break
if hitPos:
break
if not hitPos:
print("没有检测到地形碰撞")
# 显示射线(无碰撞)
self.showClickRay(worldNearPoint, worldFarPoint)
# 清理碰撞检测节点
pickerNP.removeNode()
print("地形编辑处理完成")
except Exception as e:
print(f"地形编辑处理出错: {e}")
import traceback
traceback.print_exc()
def _handleSelectionClick(self, hitNode):
"""处理选择工具的点击事件"""
print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}")
@ -541,43 +463,52 @@ class EventHandler:
selectedModel = model
print(f"找到父模型: {selectedModel.getName()}")
break
if selectedModel:
break
current = current.getParent()
if selectedModel:
print(f"✓ 最终选中模型: {selectedModel.getName()}")
#print(f"✓ 最终选中模型: {selectedModel.getName()}")
self.world.selection.handleMouseClick(selectedModel)
# 更新选择状态并显示选择框和坐标轴
self.world.selection.updateSelection(selectedModel)
# 在树形控件中查找并选中对应的项
if self.world.interface_manager.treeWidget:
print("查找树形控件中的对应项...")
#print("查找树形控件中的对应项...")
root = self.world.interface_manager.treeWidget.invisibleRootItem()
foundItem = None
for i in range(root.childCount()):
sceneItem = root.child(i)
if sceneItem.text(0) == "场景":
print(f"在场景节点下查找...")
#print(f"在场景节点下查找...")
foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem)
if foundItem:
print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}")
try:
self.world.interface_manager.treeWidget.itemClicked.disconnect()
except TypeError:
pass
self.world.interface_manager.treeWidget.setCurrentItem(foundItem)
self.world.property_panel.updatePropertyPanel(foundItem)
self.world.interface_manager.treeWidget.itemClicked.connect(
self.world.interface_manager.onTreeItemClicked)
else:
print("× 在树形控件中没有找到对应项")
break
if not foundItem:
print("× 没有找到场景节点或对应的树形项")
else:
print("× 树形控件不存在")
else:
print("× 没有找到可选择的模型节点")
self.world.selection.updateSelection(None)
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""

495
core/patrol_system.py Normal file
View File

@ -0,0 +1,495 @@
from direct.showbase.ShowBaseGlobal import globalClock
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import Point3, Vec3
import math
class PatrolSystem:
"""巡检系统类"""
def __init__(self, world):
"""初始化巡检系统
Args:
world: 核心世界对象引用
"""
self.world = world
# 巡检状态
self.is_patrolling = False
self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...]
self.current_patrol_index = 0
self.patrol_task = None
# 巡检参数
self.patrol_speed = 5.0 # 巡检移动速度(单位/秒)
self.patrol_turn_speed = 90.0 # 转向速度(度/秒)
self.patrol_wait_timer = 0.0
self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back"
# 相机状态保存
self.original_cam_pos = None
self.original_cam_hpr = None
print("✓ 巡检系统初始化完成")
def add_patrol_point(self, position, heading=None, wait_time=3.0):
if heading is None:
if self.patrol_points:
last_pos = self.patrol_points[-1][0]
direction_x = position[0] - last_pos.x
direction_y = position[1] - last_pos.y
direction_z = position[2] - last_pos.z
import math
h=math.degrees(math.atan2(-direction_x,-direction_y))
distance_xy = math.sqrt(direction_x**2+direction_y**2)
p = math.degrees(math.atan2(direction_z,distance_xy))
p = max(-89,min(89,p))
r=0
heading = (h,p,r)
else:
# 使用当前相机朝向
current_hpr = self.world.cam.getHpr()
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
pos = Point3(position[0], position[1], position[2])
hpr = Vec3(heading[0], heading[1], heading[2])
self.patrol_points.append((pos, hpr, wait_time))
print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}")
# 在 PatrolSystem 类中添加以下方法
def add_auto_heading_patrol_point(self, position, wait_time=3.0):
"""添加自动计算朝向的巡检点(朝向路径前进方向)
Args:
position: 相机位置 (x, y, z)
wait_time: 在该点停留时间
"""
heading = None # 将自动计算朝向
# 复用原有的 add_patrol_point 方法
self.add_patrol_point(position, heading, wait_time)
def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0):
"""添加朝向指定位置的巡检点
Args:
position: 相机位置 (x, y, z)
look_at_position: 相机朝向的目标位置 (x, y, z)
wait_time: 在该点停留时间
"""
import math
# 计算从当前位置到目标位置的方向向量
direction_x = look_at_position[0] - position[0]
direction_y = look_at_position[1] - position[1]
direction_z = look_at_position[2] - position[2]
# 计算HPR朝向
h = math.degrees(math.atan2(-direction_x, -direction_y))
distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2)
p = math.degrees(math.atan2(direction_z, distance_xy))
p = max(-89, min(89, p)) # 限制pitch角度在合理范围内
r = 0 # roll通常为0
heading = (h, p, r)
self.add_patrol_point(position, heading, wait_time)
def clear_patrol_points(self):
"""清空所有巡检点"""
self.patrol_points = []
print("✓ 巡检点已清空")
def set_patrol_speed(self, move_speed, turn_speed=None):
"""设置巡检速度
Args:
move_speed: 移动速度单位/
turn_speed: 转向速度/如果为None则保持当前值
"""
self.patrol_speed = move_speed
if turn_speed is not None:
self.patrol_turn_speed = turn_speed
print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}")
def start_patrol(self):
"""开始巡检"""
if not self.patrol_points:
print("✗ 没有设置巡检点,无法开始巡检")
return False
if self.is_patrolling:
print("⚠ 巡检已在进行中")
return True
# 保存当前相机状态
self.original_cam_pos = Point3(self.world.cam.getPos())
self.original_cam_hpr = Vec3(self.world.cam.getHpr())
# 重置巡检状态
self.current_patrol_index = 0
self.patrol_state = "moving"
self.patrol_wait_timer = 0.0
self.is_patrolling = True
# 启动巡检任务
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点")
return True
def stop_patrol(self):
"""停止巡检"""
if not self.is_patrolling:
print("⚠ 巡检未在进行中")
return False
# 停止巡检任务
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = None
self.is_patrolling = False
self.patrol_state = "moving"
self.patrol_wait_timer = 0.0
print("✓ 巡检已停止")
return True
def pause_patrol(self):
"""暂停巡检"""
if not self.is_patrolling:
print("⚠ 巡检未在进行中")
return False
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = None
print("✓ 巡检已暂停")
return True
def resume_patrol(self):
"""恢复巡检"""
if self.is_patrolling:
print("⚠ 巡检已在进行中")
return False
if not self.patrol_points:
print("✗ 没有设置巡检点")
return False
self.is_patrolling = True
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
print("✓ 巡检已恢复")
return True
def reset_to_original_position(self):
"""重置相机到原始位置"""
if self.original_cam_pos and self.original_cam_hpr:
self.world.cam.setPos(self.original_cam_pos)
self.world.cam.setHpr(self.original_cam_hpr)
print("✓ 相机已重置到原始位置")
return True
else:
print("✗ 没有保存的原始位置")
return False
def _patrol_task(self, task):
"""巡检主任务"""
try:
if not self.is_patrolling or not self.patrol_points:
return task.done
# 获取当前巡检点
current_point = self.patrol_points[self.current_patrol_index]
target_pos, target_hpr, wait_time = current_point
# 根据当前状态执行不同操作
if self.patrol_state == "moving":
self._handle_moving_state(target_pos)
elif self.patrol_state == "turning_to_target":
self._handle_turning_to_target_state(target_hpr)
elif self.patrol_state == "waiting":
self._handle_waiting_state(wait_time)
elif self.patrol_state == "turning_back":
self._handle_turning_back_state()
return task.cont
except Exception as e:
print(f"巡检任务出错: {e}")
import traceback
traceback.print_exc()
return task.done
def _handle_moving_state(self, target_pos):
"""处理移动状态"""
current_pos = self.world.cam.getPos()
distance = (target_pos - current_pos).length()
if distance < 0.1: # 到达目标点
print(f"✓ 到达巡检点 {self.current_patrol_index + 1}")
self.patrol_state = "turning_to_target"
return
# 计算移动方向和距离
direction = target_pos - current_pos
direction.normalize()
# 计算目标朝向(看向目标点)
target_hpr = self._look_at_to_hpr(direction)
current_hpr = self.world.cam.getHpr()
# 平滑转向到目标朝向
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
# 计算本帧应转动的角度
dt = globalClock.getDt()
turn_amount = self.patrol_turn_speed * dt
# 逐步转向目标角度
new_hpr = Vec3(current_hpr)
if abs(h_diff) > turn_amount:
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
else:
new_hpr.x = target_hpr.x
if abs(p_diff) > turn_amount:
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
else:
new_hpr.y = target_hpr.y
if abs(r_diff) > turn_amount:
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
else:
new_hpr.z = target_hpr.z
self.world.cam.setHpr(new_hpr)
# 计算本帧应移动的距离
move_distance = self.patrol_speed * dt
# 如果移动距离大于剩余距离,则直接移动到目标点
if move_distance >= distance:
self.world.cam.setPos(target_pos)
else:
# 否则按方向移动
new_pos = current_pos + direction * move_distance
self.world.cam.setPos(new_pos)
def _handle_turning_to_target_state(self, target_hpr):
"""处理转向目标状态"""
# 检查是否需要朝向下一个点
if target_hpr == "look_next":
# 计算朝向下一个点的方向
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
next_point_pos = self.patrol_points[next_index][0]
current_pos = self.world.cam.getPos()
direction = next_point_pos - current_pos
direction.normalize()
# 计算目标朝向
target_hpr = self._look_at_to_hpr(direction)
current_hpr = self.world.cam.getHpr()
# 计算角度差
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
# 检查是否已完成转向
if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0:
print(f"✓ 完成转向,开始停留")
self.patrol_state = "waiting"
self.patrol_wait_timer = 0.0
return
# 计算本帧应转动的角度
dt = globalClock.getDt()
turn_amount = self.patrol_turn_speed * dt
# 逐步转向目标角度
new_hpr = Vec3(current_hpr)
if abs(h_diff) > turn_amount:
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
else:
new_hpr.x = target_hpr.x
if abs(p_diff) > turn_amount:
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
else:
new_hpr.y = target_hpr.y
if abs(r_diff) > turn_amount:
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
else:
new_hpr.z = target_hpr.z
self.world.cam.setHpr(new_hpr)
def _handle_waiting_state(self, wait_time):
"""处理等待状态"""
self.patrol_wait_timer += globalClock.getDt()
if self.patrol_wait_timer >= wait_time:
print(f"✓ 停留结束,准备转回原朝向")
self.patrol_state = "turning_back"
# 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法
def _handle_turning_back_state(self):
"""处理转回原朝向状态"""
# 直接完成转向状态,进入移动状态
print(f"✓ 停留结束,开始移动到下一个点")
# 移动到下一个巡检点
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
self.current_patrol_index = next_index
self.patrol_state = "moving"
return
def _normalize_angle(self, angle):
"""规范化角度到-180到180度之间"""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def _look_at_to_hpr(self, direction):
"""将方向向量转换为HPR角度"""
# 简化的转换,实际应用中可能需要更精确的计算
h = math.degrees(math.atan2(-direction.x, -direction.y))
p = math.degrees(math.asin(direction.z))
return Vec3(h, p, 0)
def get_patrol_status(self):
"""获取巡检状态信息"""
return {
"is_patrolling": self.is_patrolling,
"current_point": self.current_patrol_index,
"total_points": len(self.patrol_points),
"state": self.patrol_state,
"wait_timer": self.patrol_wait_timer
}
def list_patrol_points(self):
"""列出所有巡检点"""
if not self.patrol_points:
print("没有设置巡检点")
return
print(f"巡检点列表 (共{len(self.patrol_points)}个):")
for i, (pos, hpr, wait_time) in enumerate(self.patrol_points):
current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else ""
print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) "
f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) "
f"停留:{wait_time}{current_marker}")
def remove_patrol_point(self, index):
"""移除指定索引的巡检点"""
if 0 <= index < len(self.patrol_points):
removed_point = self.patrol_points.pop(index)
print(
f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})")
# 调整当前索引
if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points:
self.current_patrol_index = len(self.patrol_points) - 1
elif self.current_patrol_index >= len(self.patrol_points):
self.current_patrol_index = 0
else:
print(f"✗ 无效的巡检点索引: {index}")
def insert_patrol_point(self, index, position, heading=None, wait_time=3.0):
"""在指定位置插入巡检点"""
if index < 0 or index > len(self.patrol_points):
print(f"✗ 无效的插入位置: {index}")
return
if heading is None:
# 使用当前相机朝向
current_hpr = self.world.cam.getHpr()
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
pos = Point3(position[0], position[1], position[2])
hpr = Vec3(heading[0], heading[1], heading[2])
self.patrol_points.insert(index, (pos, hpr, wait_time))
print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}")
def update_patrol_point(self, index, position=None, heading=None, wait_time=None):
"""更新指定巡检点的信息"""
if 0 <= index < len(self.patrol_points):
pos, hpr, wt = self.patrol_points[index]
if position is not None:
pos = Point3(position[0], position[1], position[2])
if heading is not None:
hpr = Vec3(heading[0], heading[1], heading[2])
if wait_time is not None:
wt = wait_time
self.patrol_points[index] = (pos, hpr, wt)
print(f"✓ 更新巡检点 {index + 1}")
else:
print(f"✗ 无效的巡检点索引: {index}")
def goto_patrol_point(self, index):
"""直接跳转到指定巡检点"""
if not self.patrol_points:
print("✗ 没有设置巡检点")
return False
if 0 <= index < len(self.patrol_points):
pos, hpr, _ = self.patrol_points[index]
self.world.cam.setPos(pos)
self.world.cam.setHpr(hpr)
self.current_patrol_index = index
print(f"✓ 跳转到巡检点 {index + 1}")
return True
else:
print(f"✗ 无效的巡检点索引: {index}")
return False
def cleanup(self):
"""清理巡检系统资源"""
self.stop_patrol()
self.clear_patrol_points()
self.original_cam_pos = None
self.original_cam_hpr = None
print("✓ 巡检系统资源已清理")
# 使用示例和便捷函数
def create_default_patrol_route(patrol_system):
"""创建默认的巡检路线(示例)"""
# 清空现有巡检点
patrol_system.clear_patrol_points()
# 添加一些示例巡检点
patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1前方低位置
patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2中央高位置
patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3右侧位置
patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4左侧位置
print("✓ 默认巡检路线已创建")

View File

@ -253,7 +253,7 @@ class ScriptLoader:
for component in components_to_remove:
self.script_manager.remove_script_from_object(component.game_object, script_name)
# 从sys.modules中移除
module = self.loaded_modules[script_name]
if module.__name__ in sys.modules:
@ -294,6 +294,31 @@ class ScriptLoader:
return len(changed_scripts) > 0
def find_script_file(self, script_name: str) -> Optional[str]:
"""根据脚本名称查找脚本文件路径"""
# 首先检查已加载的脚本
if script_name in self.loaded_modules:
module = self.loaded_modules[script_name]
if hasattr(module, '__file__') and module.__file__:
return module.__file__
# 在已知的文件路径中查找
for file_path in self.file_mtimes.keys():
file_name = os.path.splitext(os.path.basename(file_path))[0]
if file_name == script_name:
return file_path
# 在脚本目录中查找
scripts_dir = self.script_manager.scripts_directory
if os.path.exists(scripts_dir):
for file_name in os.listdir(scripts_dir):
if file_name.endswith('.py'):
base_name = os.path.splitext(file_name)[0]
if base_name == script_name:
return os.path.join(scripts_dir, file_name)
return None
class ScriptAPI:
"""脚本API - 提供给脚本使用的API接口"""
@ -671,18 +696,62 @@ class {class_name}(ScriptBase):
return False
script_components = self.object_scripts[game_object]
removed = False
for component in script_components[:]: # 复制列表以避免修改时出错
if component.script_instance.__class__.__name__ == script_name:
# 从引擎移除
self.engine.remove_script_component(component)
# 从对象脚本列表移除
script_components.remove(component)
removed = True
print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}")
return True
if not script_components:
del self.object_scripts[game_object]
# 更新节点上保存的脚本信息标签
if removed:
self._update_node_script_tags_after_removal(game_object, script_name)
return False
return removed
def _update_node_script_tags_after_removal(self, game_object, removed_script_name):
"""在移除脚本后更新节点标签"""
try:
# 获取对象上剩余的脚本
remaining_scripts = self.get_scripts_on_object(game_object)
if not remaining_scripts:
# 如果没有其他脚本,清除所有脚本标签
if game_object.hasTag("has_scripts"):
game_object.clearTag("has_scripts")
if game_object.hasTag("scripts_info"):
game_object.clearTag("scripts_info")
print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签")
else:
# 如果还有其他脚本,更新脚本信息标签
script_info_list = []
for script_component in remaining_scripts:
script_name = script_component.script_name
script_class = script_component.script_instance.__class__
script_file = self.loader.find_script_file(script_name) or ""
script_info_list.append({
"name": script_name,
"file": script_file
})
import json
game_object.setTag("has_scripts", "true")
game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本")
except Exception as e:
print(f"更新节点标签失败: {e}")
def get_scripts_on_object(self, game_object) -> List[ScriptComponent]:
"""获取对象上的所有脚本"""
return self.object_scripts.get(game_object, [])
@ -744,6 +813,36 @@ class {class_name}(ScriptBase):
print("==================\n")
def save_object_scripts(self,game_object,node_data:dict):
try:
if game_object in self.object_scripts:
scripts_data = []
for script_commponent in self.object_scripts[game_object]:
script_info = {
'script_name':script_commponent.script_name,
'enabled':script_commponent.enabled,
'script_class':script_commponent.script_instance.__class__.__name__
}
scripts_data.append(script_info)
if scripts_data:
node_data['scripts'] = scripts_data
print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}")
except Exception as e:
print(f"保存对象脚本信息失败: {e}")
traceback.print_exc()
# def restore_object_scripts(self,game_object,node_data:dict):
# try:
# if 'scripts' in node_data:
# scripts_data = node_data['scripts']
# restored_count = 0
# for script_info in scripts_data:
# script_name = script_info.get('script_name')
# enabled = script_info.get('enabled',True)
#
# #检查脚本是否可用
# if script_name in self.loader.script_classes:
#为
# 添加全局便捷函数让脚本更容易使用API
def get_script_api():

File diff suppressed because it is too large Load Diff

View File

@ -104,6 +104,9 @@ class TerrainManager:
print("错误:无法生成有效的地形节点")
return None
terrain_node.setTag("is_scene_element", "1")
terrain_node.setTag("tree_item_type", "TERRAIN_NODE")
node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}"
terrain_node.setName(node_name)
@ -235,6 +238,9 @@ class TerrainManager:
print("错误:无法生成有效的平面地形节点")
return None
terrain_node.setTag("is_scene_element", "1")
terrain_node.setTag("tree_item_type", "TERRAIN_NODE")
node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain_node.setName(node_name)

View File

@ -5,6 +5,7 @@ class ToolManager:
"""初始化工具管理器"""
self.world = world
self.currentTool = "选择" # 默认工具为选择工具
print(f"当前工具: {self.currentTool}")
def setCurrentTool(self, tool):
"""设置当前工具"""

View File

@ -288,7 +288,8 @@ class CoreWorld(Panda3DWorld):
self.cam.setPos(0, -50, 20)
self.cam.lookAt(0, 0, 0)
self.camLens.setFov(80)
# self.cam.setTag("is_scene_element", "1")
self.cam.setTag("is_scene_element", "1")
self.cam.setTag("tree_item_type", "CAMERA_NODE")
print("✓ 相机设置完成")
def _setupLighting(self):
@ -320,19 +321,71 @@ class CoreWorld(Panda3DWorld):
# 创建地板节点
self.ground = self.render.attachNewNode(cm.generate())
self.ground.setP(-90)
self.ground.setZ(-0.1)
self.ground.setZ(-1.0)
self.ground.setColor(0.8, 0.8, 0.8, 1)
# self.ground.setTag("is_scene_element", "1")
self.ground.setTag("is_scene_element", "1")
self.ground.setTag("tree_item_type", "SCENE_NODE")
# 创建支持贴图的材质
mat = Material()
mat.setName("GroundMaterial")
color = LColor(1, 1, 1, 0.8)
mat.set_base_color(color)
mat.set_roughness(0.5) # 设置合适的初始粗糙度
mat.set_roughness(1) # 设置合适的初始粗糙度
mat.set_metallic(0.5) # 设置较低的初始金属性
self.ground.set_material(mat)
#创建第二个相同的地面,位置稍有偏移
self.ground2 = self.render.attachNewNode(cm.generate())
self.ground2.setH(-90)
self.ground2.setZ(-1.0)
self.ground2.setX(50) # 在X轴方向偏移
self.ground2.setZ(49) # 在X轴方向偏移
self.ground2.setColor(0.8, 0.8, 0.8, 1)
self.ground2.set_material(mat)
self.ground2.setTag("is_scene_element", "1")
self.ground2.setTag("tree_item_type", "SCENE_NODE")
# 创建第三个相同的地面,位置在另一个方向
self.ground3 = self.render.attachNewNode(cm.generate())
self.ground3.setH(90)
self.ground3.setZ(-1.0)
self.ground3.setX(-50) # 在X轴负方向偏移
self.ground3.setZ(49) # 在X轴负方向偏移
self.ground3.setColor(0.8, 0.8, 0.8, 1)
self.ground3.set_material(mat)
self.ground3.setTag("is_scene_element", "1")
self.ground3.setTag("tree_item_type", "SCENE_NODE")
self.ground4 = self.render.attachNewNode(cm.generate())
# self.ground3.setR(90)
self.ground4.setZ(-1.0)
self.ground4.setY(50) # 在X轴负方向偏移
self.ground4.setZ(49) # 在X轴负方向偏移
self.ground4.setColor(0.8, 0.8, 0.8, 1)
self.ground4.set_material(mat)
self.ground4.setTag("is_scene_element", "1")
self.ground4.setTag("tree_item_type", "SCENE_NODE")
self.ground5 = self.render.attachNewNode(cm.generate())
self.ground5.setP(180)
self.ground5.setZ(-1)
self.ground5.setY(-50) # 在X轴负方向偏移
self.ground5.setZ(49) # 在X轴负方向偏移
self.ground5.setColor(0.8, 0.8, 0.8, 1)
self.ground5.set_material(mat)
self.ground5.setTag("is_scene_element", "1")
self.ground5.setTag("tree_item_type", "SCENE_NODE")
self.ground6 = self.render.attachNewNode(cm.generate())
self.ground6.setP(90)
self.ground6.setZ(-1)
self.ground6.setZ(99) # 在X轴负方向偏移
self.ground6.setColor(0.8, 0.8, 0.8, 1)
self.ground6.set_material(mat)
self.ground6.setTag("is_scene_element", "1")
self.ground6.setTag("tree_item_type", "SCENE_NODE")
# 应用默认PBR效果确保支持贴图
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
@ -349,6 +402,72 @@ class CoreWorld(Panda3DWorld):
},
50
)
# 为其他两个地面也应用相同的效果
self.render_pipeline.set_effect(
self.ground2,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground3,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground4,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground5,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground6,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
print("✓ 地板PBR效果已应用")
else:
print("⚠️ RenderPipeline未初始化地板将使用基础渲染")
@ -438,12 +557,32 @@ class CoreWorld(Panda3DWorld):
self.mouseRightPressed = True
self.lastMouseX = evt['x']
self.lastMouseY = evt['y']
#
# # 通过 Qt 窗口隐藏光标并捕获鼠标
# try:
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.setCursor(Qt.BlankCursor)
# # 捕获鼠标,使其无法离开窗口
# self.qtWidget.grabMouse()
# except Exception as e:
# print(f"通过 Qt 隐藏光标时出错: {e}")
def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件"""
#print("右键释放")
self.mouseRightPressed = False
# # 恢复 Qt 窗口光标并释放鼠标捕获
# try:
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.unsetCursor() # 恢复默认光标
# # 释放鼠标捕获
# self.qtWidget.releaseMouse()
# except Exception as e:
# print(f"恢复 Qt 光标时出错: {e}")
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 只处理相机旋转"""
if not evt:

View File

@ -359,7 +359,7 @@ class GizmoDragTestWorld(Panda3DWorld):
distance = distance_to_line(
(mouseX, mouseY), center_screen, axis_screen
)
print(f"{axis_label}距离: {distance:.2f}")
#print(f"{axis_label}距离: {distance:.2f}")
if distance < click_threshold:
print(f"✓ 点击了{axis_label}")

View File

@ -24,78 +24,80 @@ except ImportError:
WEB_ENGINE_AVAILABLE = False
print("⚠️ QtWebEngineWidgets 不可用Cesium 集成功能将被禁用")
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
# 参数类型检查和转换
if isinstance(size, (list, tuple)):
if len(size) >= 2:
x_size, y_size = float(size[0]), float(size[1])
else:
x_size = y_size = float(size[0]) if size else 1.0
else:
x_size = y_size = float(size)
# 创建卡片
cm = CardMaker('gui_3d_image')
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
# 创建3D图像节点
image_node = self.world.render.attachNewNode(cm.generate())
image_node.setPos(*pos)
# 为3D图像创建独立的材质
material = Material(f"image-material-{len(self.gui_elements)}")
material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
image_node.setMaterial(material, 1)
image_node.setTransparency(TransparencyAttrib.MAlpha)
# 如果提供了图像路径,则加载纹理
if image_path:
self.update3DImageTexture(image_node, image_path)
# 应用PBR效果如果可用
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.set_effect(
image_node,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": False,
"render_envmap": True,
"disable_children_effects": True
},
50
)
print("✓ GUI 3D图像PBR效果已应用")
except Exception as e:
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
# 为GUI元素添加标识效仿3D文本方法
image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
if image_path:
image_node.setTag("gui_image_path", image_path)
image_node.setTag("is_gui_element", "1")
self.gui_elements.append(image_node)
# 更新场景树
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
return image_node
# def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
# from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
#
# # 参数类型检查和转换
# if isinstance(size, (list, tuple)):
# if len(size) >= 2:
# x_size, y_size = float(size[0]), float(size[1])
# else:
# x_size = y_size = float(size[0]) if size else 1.0
# else:
# x_size = y_size = float(size)
#
# # 创建卡片
# cm = CardMaker('gui_3d_image')
# cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
#
# # 创建3D图像节点
# image_node = self.world.render.attachNewNode(cm.generate())
# image_node.setPos(*pos)
#
# # 为3D图像创建独立的材质
# material = Material(f"image-material-{len(self.gui_elements)}")
# material.setBaseColor(LColor(1, 1, 1, 1))
# material.setDiffuse(LColor(1, 1, 1, 1))
# material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
# material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
# material.setShininess(10.0)
# material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
# image_node.setMaterial(material, 1)
#
# image_node.setTransparency(TransparencyAttrib.MAlpha)
#
# # 如果提供了图像路径,则加载纹理
# if image_path:
# self.update3DImageTexture(image_node, image_path)
#
# # 应用PBR效果如果可用
# try:
# if hasattr(self, 'render_pipeline') and self.render_pipeline:
# self.render_pipeline.set_effect(
# image_node,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": False,
# "render_envmap": True,
# "disable_children_effects": True
# },
# 50
# )
# print("✓ GUI 3D图像PBR效果已应用")
# except Exception as e:
# print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
#
# # 为GUI元素添加标识效仿3D文本方法
# image_node.setTag("gui_type", "3d_image")
# image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
# image_node.setTag("is_scene_element", "1")
# image_node.setTag("tree_item_type", "GUI_3DIMAGE")
# if image_path:
# image_node.setTag("gui_image_path", image_path)
# image_node.setTag("is_gui_element", "1")
#
# self.gui_elements.append(image_node)
#
# # 更新场景树
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
#
# print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
# return image_node
class GUIManager:
"""GUI元素管理系统类"""
@ -203,6 +205,7 @@ class GUIManager:
button.setTag("gui_text", text)
button.setTag("is_gui_element", "1")
button.setTag("is_scene_element", "1") # 确保这个标签被设置
button.setTag("tree_item_type", "GUI_BUTTON")
button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性
button.setTag("gui_element_type", "button")
button.setTag("created_by_user", "1")
@ -319,9 +322,11 @@ class GUIManager:
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
label.setTag("is_gui_element", "1")
label.setTag("tree_item_type", "GUI_LABEL")
label.setTag("is_scene_element", "1")
label.setTag("created_by_user", "1")
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
label.setTag("name",label_name)
label.setName(label_name)
# 如果有GUI父节点建立引用关系
@ -412,9 +417,13 @@ class GUIManager:
parent_gui_node = None
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
font = None
if hasattr(self.world,'getChineseFont'):
font = self.world.getChineseFont()
entry = DirectEntry(
text="",
pos=gui_pos,
pos=(pos[0],pos[1],pos[2]),
scale=size,
command=self.onGUIEntrySubmit,
extraArgs=[f"entry_{len(self.gui_elements)}"],
@ -422,7 +431,15 @@ class GUIManager:
numLines=1,
width=12,
focus=0,
parent=parent_gui_node # 设置GUI父节点
frameColor = (0,0,0,0),
text_fg=(1,1,1,1),
text_align=TextNode.ACenter,
text_wordwrap=None,
rolloverSound=None,
clickSound=None,
parent=parent_gui_node, # 设置GUI父节点
text_font = font,
frameSize=(-0.1,0.1,-0.05,0.05)
)
# 设置节点标签
@ -430,9 +447,11 @@ class GUIManager:
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
entry.setTag("is_gui_element", "1")
entry.setTag("tree_item_type", "GUI_ENTRY")
entry.setTag("is_scene_element", "1")
entry.setTag("created_by_user", "1")
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
entry.setTag("name",entry_name)
entry.setName(entry_name)
# 如果有GUI父节点建立引用关系
@ -483,7 +502,7 @@ class GUIManager:
traceback.print_exc()
return None
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2):
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1):
"""创建2D GUI图片"""
try:
from direct.gui.DirectGui import DirectButton
@ -523,9 +542,17 @@ class GUIManager:
parent_gui_node = None # 使用默认的aspect2d
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
# 使用CardMaker创建一个更可靠的图片框架
cm = CardMaker('gui-2d-image')
cm.setFrame(-size, size, -size, size)
if isinstance(size, (list, tuple)) and len(size) >= 2:
# 分别处理宽度和高度的缩放
width_scale = size[0] * 0.25
height_scale = size[2] * 0.25
else:
# 如果只提供了一个缩放值,则使用相同值
width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2
height_scale = width_scale
cm = CardMaker("gui-2d-image")
cm.setFrame(-width_scale, width_scale, -height_scale, height_scale)
# image_node = self.world.aspect2d.attachNewNode(cm.generate())
if parent_gui_node:
@ -564,8 +591,10 @@ class GUIManager:
image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}")
image_node.setTag("is_gui_element", "1")
image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_IMAGE")
image_node.setTag("created_by_user", "1")
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
image_node.setTag("name",image_name)
image_node.setName(image_name)
# 如果有GUI父节点建立引用关系
@ -715,7 +744,9 @@ class GUIManager:
textNodePath.setTag("gui_text", text)
textNodePath.setTag("is_gui_element", "1")
textNodePath.setTag("is_scene_element", "1")
textNodePath.setTag("tree_item_type", "GUI_3DTEXT")
textNodePath.setTag("created_by_user", "1")
textNodePath.setTag("name", text_name)
# 添加到GUI元素列表
self.gui_elements.append(textNodePath)
@ -763,6 +794,7 @@ class GUIManager:
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
"""创建3D空间图片"""
try:
from panda3d.core import TextNode
from PyQt5.QtCore import Qt
@ -851,7 +883,9 @@ class GUIManager:
image_node.setTag("image_path", image_path)
image_node.setTag("is_gui_element", "1")
image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_3DIMAGE")
image_node.setTag("created_by_user", "1")
image_node.setTag("name",image_name)
# 添加到GUI元素列表
self.gui_elements.append(image_node)
@ -968,7 +1002,9 @@ class GUIManager:
video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}")
video_screen.setTag("is_gui_element", "1")
video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1")
video_screen.setTag("name",screen_name)
# 设置视频路径标签
if video_path and os.path.exists(video_path):
@ -1149,6 +1185,7 @@ class GUIManager:
# 检查是否有播放方法
if hasattr(movie_texture, 'play'):
try:
self.loadVideoFile(video_screen, video_screen.getTag("video_path"))
movie_texture.play()
print(f"▶️ 继续播放视频: {video_screen.getName()}")
return True
@ -1159,9 +1196,7 @@ class GUIManager:
print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}")
return False
else:
print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}")
self._debugVideoScreenTextures(video_screen)
return False
self.loadVideoFile(video_screen,video_screen.getTag("video_path"))
except Exception as e:
print(f"❌ 播放视频失败: {e}")
import traceback
@ -1472,8 +1507,6 @@ class GUIManager:
pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标
parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d,
suppressMouse=True,
)
video_screen.setName(screen_name)
@ -1487,10 +1520,10 @@ class GUIManager:
video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}")
video_screen.setTag("is_gui_element", "1")
video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1")
video_screen.setTag("video_path", video_path if video_path else "")
print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else ''}")
video_screen.setTag("name",screen_name)
video_screen.setTag("video_path",video_path or "")
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
@ -1535,7 +1568,7 @@ class GUIManager:
import traceback
traceback.print_exc()
else:
if video_path:
if not video_path:
print(f"⚠️ 2D视频文件不存在: {video_path}")
# 添加到GUI元素列表
@ -1590,7 +1623,9 @@ class GUIManager:
import os
video_screen.setTag("video_path",video_path)
print(f"🔧 更新2D视频屏幕标签 - video_path: {video_path if video_path else ''}")
path = video_screen.getTag("video_path")
print({video_screen.getTag("gui_type")})
print(f"🔧 更新2D视频屏幕标签 - video_path: {path}")
if not video_path or not os.path.exists(video_path):
print(f"❌ 2D视频文件不存在: {video_path}")
@ -1798,6 +1833,7 @@ class GUIManager:
# 设置标签以便识别和管理
sphere_np.setTag("gui_type", "spherical_video")
sphere_np.setTag("is_gui_element", "1")
sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO")
sphere_np.setTag("video_path", video_path or "")
sphere_np.setTag("original_radius", str(radius))
@ -2010,6 +2046,7 @@ class GUIManager:
virtual_screen.setTag("gui_text", text)
virtual_screen.setTag("is_gui_element", "1")
virtual_screen.setTag("is_scene_element", "1")
virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen")
virtual_screen.setTag("created_by_user", "1")
# 添加到GUI元素列表
@ -2162,9 +2199,17 @@ class GUIManager:
print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}")
if property_name == "text":
original_frame_size = None
if hasattr(gui_element,'getFrameSize'):
try:
original_frame_size = gui_element.getFrameSize()
except:
pass
if gui_type in ["button", "label"]:
gui_element['text'] = value
print(f"成功更新2D GUI文本: {value}")
# if gui_type == "button":
# self._resizeButtonToText(gui_element,value,original_frame_size)
elif gui_type == "entry":
gui_element.set(value)
print(f"成功更新输入框文本: {value}")
@ -2243,6 +2288,64 @@ class GUIManager:
traceback.print_exc()
return False
def _resizeButtonToText(self, button, text, original_frame_size=None):
"""
根据文本内容调整按钮大小
Args:
button: DirectButton 对象
text: 新的文本内容
original_frame_size: 原始frameSize用于计算合适的padding
"""
try:
# 获取按钮当前的文本缩放
text_scale = 0.03 # 默认文本缩放
padding = 0.2 # 默认边距
# 尝试从按钮获取实际的文本缩放值
if hasattr(button, 'getTextScale'):
text_scale = button.getTextScale()
elif hasattr(button, 'textScale'):
text_scale = button.textScale
# 根据原始frameSize计算合适的padding
if original_frame_size and len(original_frame_size) >= 4:
# 基于原始按钮宽度计算合适的padding
padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1)
# 更精确的文本尺寸估算
# 考虑中文字符和英文字符的不同宽度
chinese_chars = len([c for c in text if ord(c) > 127])
english_chars = len(text) - chinese_chars
# 中文字符通常比英文字符宽
char_width = text_scale * 0.6
text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width)
text_height = text_scale * 1.2 # 文本高度
# 计算新的frameSize确保有足够的边距
half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3
half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15
# 正确设置frameSize - 使用字典方式设置
new_frame = (-half_width, half_width, -half_height, half_height)
button['frameSize'] = new_frame # 使用字典方式设置
print(f"按钮大小已调整: {new_frame} (文本: {text})")
except Exception as e:
print(f"调整按钮大小失败: {e}")
# 如果自动调整失败,保持原有大小或设置一个合理的默认大小
try:
if original_frame_size:
# 保持原有大小
button['frameSize'] = original_frame_size
else:
# 设置一个合理的默认大小
button['frameSize'] = (-0.5, 0.5, -0.15, 0.15)
except:
pass
def duplicateGUIElement(self, gui_element):
"""复制GUI元素"""
try:

BIN
icons/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

122
main.py
View File

@ -6,7 +6,7 @@ from demo.video_integration import VideoManager
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
import builtins # 添加这一行
import builtins
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox)
@ -14,6 +14,8 @@ from PyQt5.QtCore import Qt, QDir, QUrl
from PyQt5.QtGui import QDrag, QPainter, QPixmap
from PyQt5.QtWidgets import QFileSystemModel
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from panda3d.core import loadPrcFileData
loadPrcFileData("", "assertions 0")
from core.world import CoreWorld
from core.selection import SelectionSystem
from core.event_handler import EventHandler
@ -52,7 +54,16 @@ class MyWorld(CoreWorld):
# 初始化选择和变换系统
self.selection = SelectionSystem(self)
# 绑定F键用于聚焦选中节点
self.accept("f", self.onFocusKeyPressed)
self.accept("F", self.onFocusKeyPressed) # 大写F
#初始化巡检系统
self.patrol_system = PatrolSystem(self)
self.accept("p",self.onPatrolKeyPressed)
self.accept("P",self.onPatrolKeyPressed)
# 初始化事件处理系统
self.event_handler = EventHandler(self)
@ -80,7 +91,7 @@ class MyWorld(CoreWorld):
# 初始化界面管理系统
self.interface_manager = InterfaceManager(self)
# 启动脚本系统
self.script_manager.start_system()
@ -93,6 +104,8 @@ class MyWorld(CoreWorld):
self.info_panel_manager = InfoPanelManager(self)
self.command_manager = CommandManager()
# 初始化碰撞管理器
from core.collision_manager import CollisionManager
self.collision_manager = CollisionManager(self)
@ -107,7 +120,7 @@ class MyWorld(CoreWorld):
self.vr_manager = None
# 调试选项
self.debug_collision = False # 是否显示碰撞体
self.debug_collision = True # 是否显示碰撞体
# 默认启用模型间碰撞检测(可选)
self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
@ -227,17 +240,17 @@ class MyWorld(CoreWorld):
"""创建2D GUI文本输入框"""
return self.gui_manager.createGUIEntry(pos, placeholder, size)
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1):
"""创建3D空间文本"""
return self.gui_manager.createGUI3DText(pos, text, size)
def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)):
def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1)):
"""创建3D图片"""
return self.gui_manager.createGUI3DImage(pos,text,size)
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1):
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2):
"""创建2D GUI图片"""
return self.gui_manager.createGUI2DImage(pos, image_path, size*0.2)
return self.gui_manager.createGUI2DImage(pos, image_path, size)
def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
"""创建视频屏幕"""
@ -675,6 +688,97 @@ class MyWorld(CoreWorld):
"""获取碰撞统计"""
return self.collision_manager.getCollisionStatistics()
def setupKeyboardEvents(self):
"""设置键盘事件"""
try:
# 绑定 F 键用于聚焦选中节点
self.accept("f", self.onFocusKeyPressed)
self.accept("F", self.onFocusKeyPressed) # 大写F
print("✓ 键盘事件绑定完成")
except Exception as e:
print(f"设置键盘事件失败: {e}")
def onFocusKeyPressed(self):
"""处理 F 键按下事件"""
try:
#print("检测到 F 键按下")
# 检查是否有选中的节点
if hasattr(self, 'selection') and self.selection.selectedNode:
#print(f"当前选中节点: {self.selection.selectedNode.getName()}")
# 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本)
# self.selection.focusCameraOnSelectedNode() # 无动画版本
self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本
else:
print("当前没有选中任何节点")
except Exception as e:
print(f"处理 F 键事件失败: {e}")
def onPatrolKeyPressed(self):
"""处理 P 键按下事件 - 控制巡检系统"""
try:
print("检测到 P 键按下")
if not self.patrol_system.is_patrolling:
# 如果巡检系统没有点,创建默认巡检路线
if not self.patrol_system.patrol_points:
self.createDefaultPatrolRoute()
# 开始巡检
if self.patrol_system.start_patrol():
print("✓ 巡检已开始")
else:
print("✗ 巡检启动失败")
else:
# 停止巡检
if self.patrol_system.stop_patrol():
print("✓ 巡检已停止")
else:
print("✗ 巡检停止失败")
except Exception as e:
print(f"处理 P 键事件失败: {e}")
def createDefaultPatrolRoute(self):
"""创建默认巡检路线(使用自动朝向)"""
try:
# 清空现有巡检点
self.patrol_system.clear_patrol_points()
# 添加巡检点使用None表示朝向下一个点
self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5)
self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5)
self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5)
self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5)
# 最后一个点可以指定特定的朝向或者也设为None继续循环
self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5)
print("✓ 默认自动朝向巡检路线已创建")
self.patrol_system.list_patrol_points()
except Exception as e:
print(f"创建默认自动朝向巡检路线失败: {e}")
def _serializeNode(self, node):
"""序列化节点数据"""
try:
return self.world.scene_manager.serializeNode(node)
except Exception as e:
print(f"序列化节点失败: {e}")
return None
def _deserializeNode(self, node_data, parent_node):
"""反序列化节点数据"""
try:
return self.world.scene_manager.deserializeNode(node_data, parent_node)
except Exception as e:
print(f"反序列化节点失败: {e}")
return None
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能
@ -715,7 +819,7 @@ def run(args = None):
# 使用新的UI模块创建主窗口
from ui.main_window import setup_main_window
print(f'Path is {args}')
app, main_window = setup_main_window(world, args)
# 启动应用程序

View File

@ -79,7 +79,7 @@ class ProjectManager:
# 自动保存初始场景
scene_file = os.path.join(scenes_path, "scene.bam")
if self.world.scene_manager.saveScene(scene_file):
if self.world.scene_manager.saveScene(scene_file, project_path):
# 更新配置文件中的场景路径
project_config["scene_file"] = os.path.relpath(scene_file, full_project_path)
with open(config_file, "w", encoding="utf-8") as f:
@ -217,7 +217,7 @@ class ProjectManager:
config_file = os.path.join(project_path, "project.json")
if not os.path.exists(config_file):
if parent_window:
QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!")
QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}")
else:
print("警告: 选择的不是有效的项目文件夹!")
return False
@ -302,7 +302,7 @@ class ProjectManager:
return False
# 保存场景
if self.world.scene_manager.saveScene(scene_file):
if self.world.scene_manager.saveScene(scene_file, project_path):
# 更新项目配置文件
config_file = os.path.join(project_path, "project.json")
if os.path.exists(config_file):
@ -392,197 +392,645 @@ class ProjectManager:
# 复制场景文件到构建目录
shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam"))
# 复制Resources文件夹到build目录
source_resources = os.path.join(project_path, "scenes", "resources")
self.copy_folder(source_resources, build_dir)
self._saveGUIElementsToJSON(build_dir, project_path)
self._copyScriptsToBuild(build_dir, project_path)
self._copyScriptSystemToBuild(build_dir)
source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile")
dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile")
if os.path.exists(source_render_pipeline):
if os.path.exists(dest_render_pipeline):
shutil.rmtree(dest_render_pipeline)
shutil.copytree(
source_render_pipeline,
dest_render_pipeline,
ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log')
)
print("✓ RenderPipelineFile文件夹已复制到build目录")
else:
print("⚠️ RenderPipelineFile文件夹未找到")
# 创建标准的应用程序入口文件
self._createAppFile(build_dir, project_name)
# 创建标准的setup.py文件
self._createStandardSetupFile(build_dir, project_name)
def _createAppFile(self, build_dir, project_name):
"""创建应用程序主文件"""
app_code = f'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
{project_name} - Panda3D应用程序
使用Panda3D引擎编辑器创建
"""
#创建requirements.txt文件
self._createRequirementsFile(build_dir)
import sys
import os
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (loadPrcFileData, WindowProperties, AmbientLight,
DirectionalLight, Point3, Vec3)
# 配置Panda3D
loadPrcFileData("", """
win-size 1280 720
window-title {project_name}
show-frame-rate-meter 1
sync-video 1
want-directtools #f
want-tk #f
audio-library-name p3openal_audio
""")
class {project_name.replace(' ', '').replace('-', '')}App(ShowBase):
"""应用程序主类"""
def __init__(self):
ShowBase.__init__(self)
print(f"启动 {project_name}...")
# 设置窗口属性
self.setupWindow()
# 设置光照
self.setupLighting()
# 加载场景
self.loadScene()
# 设置相机控制
self.setupControls()
print("✓ 应用程序初始化完成")
def setupWindow(self):
"""设置窗口"""
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.2)
# 设置窗口属性
props = WindowProperties()
props.setTitle("{project_name}")
self.win.requestProperties(props)
def setupLighting(self):
"""设置光照系统"""
# 环境光
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
# 定向光(模拟太阳光)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlight.setDirection(Vec3(-1, -1, -1))
dlnp = self.render.attachNewNode(dlight)
self.render.setLight(dlnp)
def loadScene(self):
"""加载场景"""
def _copyScriptsToBuild(self, build_dir, project_path):
"""复制脚本文件到构建目录的scripts文件夹"""
try:
# 查找场景文件
scene_file = "scene.bam"
if not os.path.exists(scene_file):
print("警告: 没有找到场景文件,创建默认场景")
self.createDefaultScene()
return
# 加载场景
scene = self.loader.loadModel(scene_file)
if scene:
scene.reparentTo(self.render)
print("✓ 场景加载成功")
# 自动调整相机位置
self.adjustCamera()
# 创建目标scripts目录
scripts_dest = os.path.join(build_dir, "scripts")
# 正确的源scripts目录路径
scripts_src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts")
# 如果上面的路径不存在尝试项目路径下的scripts目录
if not os.path.exists(scripts_src):
scripts_src = os.path.join(project_path, "scripts")
if os.path.exists(scripts_src):
# 直接复制整个scripts目录
if os.path.exists(scripts_dest):
shutil.rmtree(scripts_dest)
shutil.copytree(
scripts_src,
scripts_dest,
ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log')
)
print("✓ Scripts目录已复制到build/scripts")
else:
print("警告: 场景加载失败,创建默认场景")
self.createDefaultScene()
# 创建空的scripts目录
if not os.path.exists(scripts_dest):
os.makedirs(scripts_dest)
print("⚠️ 项目中没有scripts目录")
except Exception as e:
print(f"加载场景时出错: {{str(e)}}")
self.createDefaultScene()
def createDefaultScene(self):
"""创建默认场景"""
# 加载默认的环境模型
env = self.loader.loadModel("models/environment")
if env:
env.reparentTo(self.render)
env.setScale(0.25)
env.setPos(-8, 42, 0)
# 创建一个简单的立方体作为示例
from panda3d.core import CardMaker
cm = CardMaker("ground")
cm.setFrame(-10, 10, -10, 10)
ground = self.render.attachNewNode(cm.generate())
ground.setP(-90)
ground.setColor(0.5, 0.8, 0.5, 1)
def adjustCamera(self):
"""调整相机位置以查看场景"""
# 计算场景边界
bounds = self.render.getBounds()
if bounds and not bounds.isEmpty():
center = bounds.getCenter()
radius = bounds.getRadius()
# 设置相机位置
distance = radius * 3
self.cam.setPos(center.x, center.y - distance, center.z + radius)
self.cam.lookAt(center)
else:
# 默认相机位置
self.cam.setPos(0, -20, 5)
self.cam.lookAt(0, 0, 0)
def setupControls(self):
"""设置相机控制"""
# 启用鼠标控制
self.accept("wheel_up", self.zoomIn)
self.accept("wheel_down", self.zoomOut)
# 键盘控制说明
print("\\n=== 控制说明 ===")
print("鼠标滚轮: 缩放")
print("ESC: 退出")
print("================\\n")
# ESC键退出
self.accept("escape", sys.exit)
def zoomIn(self):
"""放大"""
pos = self.cam.getPos()
lookAt = Point3(0, 0, 0) # 假设看向原点
direction = (lookAt - pos).normalized()
newPos = pos + direction * 2
self.cam.setPos(newPos)
def zoomOut(self):
"""缩小"""
pos = self.cam.getPos()
lookAt = Point3(0, 0, 0) # 假设看向原点
direction = (lookAt - pos).normalized()
newPos = pos - direction * 2
self.cam.setPos(newPos)
print(f"⚠️ 复制脚本文件时出错: {str(e)}")
def main():
"""主函数"""
try:
app = {project_name.replace(' ', '').replace('-', '')}App()
app.run()
except Exception as e:
print(f"应用程序启动失败: {{str(e)}}")
import traceback
traceback.print_exc()
input("按Enter键退出...")
def _copyScriptSystemToBuild(self,build_dir):
core_files = [
"script_system.py",
"InfoPanelManager.py",
"CustomMouseController.py"
]
if __name__ == "__main__":
main()
'''
source_core_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"core")
core_dest = os.path.join(build_dir,"core")
if not os.path.exists(core_dest):
os.makedirs(core_dest)
for file_name in core_files:
source_file = os.path.join(source_core_dir,file_name)
if os.path.exists(source_file):
shutil.copy2(source_file,os.path.join(core_dest,file_name))
def _saveGUIElementsToJSON(self, build_dir, project_path):
"""保存GUI元素到JSON文件内容与_collectGUIElementInfo保持一致"""
try:
# 创建目标gui目录
gui_dest = os.path.join(build_dir, "gui")
if not os.path.exists(gui_dest):
os.makedirs(gui_dest)
# 收集所有GUI元素信息
gui_data = []
# 获取当前场景中的GUI元素
if hasattr(self.world, 'gui_elements'):
for gui_node in self.world.gui_elements:
if gui_node and not gui_node.isEmpty():
# 使用_collectGUIElementInfo方法收集信息
gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node)
if gui_info:
gui_data.append(gui_info)
print(f"收集GUI元素信息: {gui_info['name']}")
# 保存GUI信息到JSON文件
gui_file_path = os.path.join(gui_dest, "gui_elements.json")
with open(gui_file_path, "w", encoding="utf-8") as f:
json.dump(gui_data, f, ensure_ascii=False, indent=4)
print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素")
return True
except Exception as e:
print(f"⚠️ 保存GUI元素时出错: {str(e)}")
import traceback
traceback.print_exc()
return False
def _createRequirementsFile(self,build_dir):
requirements_content = """panda3d>=1.10.13"""
requirements_path = os.path.join(build_dir,"requirements.txt")
with open(requirements_path,"w",encoding="utf-8") as f:
f.write(requirements_content)
def copy_folder(self, source_folder, destination_folder):
"""将一个文件夹从源路径复制到目标路径下的resources文件夹中
Args:
source_folder (str): 源文件夹路径
destination_folder (str): 目标文件夹路径
"""
try:
# 创建resources文件夹作为目标
resources_dest = os.path.join(destination_folder, "resources")
# 确保目标目录存在
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
# 如果目标resources文件夹已存在先删除
if os.path.exists(resources_dest):
shutil.rmtree(resources_dest)
# 检查源文件夹是否存在
if os.path.exists(source_folder):
# 复制整个文件夹到resources目录下
shutil.copytree(
source_folder,
resources_dest,
ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log')
)
print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}")
return True
else:
print(f"⚠️ 源文件夹不存在: {source_folder}")
# 即使源文件夹不存在也创建空的resources目录
if not os.path.exists(resources_dest):
os.makedirs(resources_dest)
return False
except Exception as e:
print(f"⚠️ 复制文件夹时出错: {str(e)}")
return False
def _copyResourcesToBuild(self, build_dir, project_path):
"""复制GUI资源到构建目录的resources文件夹"""
try:
# 创建目标resources目录
resources_dest = os.path.join(build_dir, "resources")
# 源Resources目录
resources_src = os.path.join(project_path, "Resources")
if os.path.exists(resources_src):
# 直接复制整个Resources目录
if os.path.exists(resources_dest):
shutil.rmtree(resources_dest)
shutil.copytree(
resources_src,
resources_dest,
ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log')
)
print("✓ Resources目录已复制到build/resources")
# 统计复制的文件数量
file_count = 0
for root, dirs, files in os.walk(resources_dest):
file_count += len(files)
print(f"✓ 共复制了 {file_count} 个资源文件")
else:
# 创建空的resources目录
if not os.path.exists(resources_dest):
os.makedirs(resources_dest)
print("⚠️ 项目中没有Resources目录")
except Exception as e:
print(f"⚠️ 复制资源文件时出错: {str(e)}")
def _collectResourceFiles(self, project_path):
"""收集项目中GUI使用的资源文件"""
resource_files = set()
try:
# 收集Resources目录中的所有文件这是最主要的资源来源
resources_dir = os.path.join(project_path, "Resources")
if os.path.exists(resources_dir):
for root, dirs, files in os.walk(resources_dir):
for file in files:
file_path = os.path.join(root, file)
# 收集所有文件,不仅仅是媒体文件
resource_files.add(file_path)
# 同时收集场景中引用的特定资源
scene_file = os.path.join(project_path, "scenes", "scene.bam")
if os.path.exists(scene_file):
# 从场景文件中提取资源引用
referenced_files = self._extractResourcesFromScene(scene_file, project_path)
for file_path in referenced_files:
if os.path.isabs(file_path):
if os.path.exists(file_path):
resource_files.add(file_path)
else:
# 相对路径
full_path = os.path.join(project_path, file_path)
if os.path.exists(full_path):
resource_files.add(full_path)
except Exception as e:
print(f"收集资源文件时出错: {str(e)}")
return list(resource_files)
def _extractResourcesFromScene(self, scene_file, project_path):
"""从场景文件中提取资源引用"""
referenced_files = []
try:
# 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑
# 由于直接解析BAM文件比较复杂我们采用间接方式
# 检查项目配置文件或其他元数据文件中可能包含的资源引用
config_file = os.path.join(project_path, "project.json")
if os.path.exists(config_file):
try:
with open(config_file, "r", encoding="utf-8") as f:
project_config = json.load(f)
# 如果配置中有资源列表信息,可以在这里处理
# 这里暂时保持简单实现
except Exception as e:
print(f"读取项目配置时出错: {str(e)}")
except Exception as e:
print(f"从场景提取资源引用时出错: {str(e)}")
return referenced_files
def _isMediaFile(self, file_path):
"""判断是否为媒体文件(图片或视频)"""
media_extensions = {
# 图片格式
'.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff',
# 视频格式
'.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv'
}
_, ext = os.path.splitext(file_path.lower())
return ext in media_extensions
def _createAppFile(self, build_dir, project_name):
"""创建应用程序主文件 - 通过复制模板文件"""
# 获取模板文件路径假设模板文件在项目根目录下的templates文件夹中
template_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"templates", "main_template.py")
# 目标文件路径
app_path = os.path.join(build_dir, "main.py")
with open(app_path, "w", encoding="utf-8") as f:
f.write(app_code)
# 检查模板文件是否存在
if os.path.exists(template_path):
# 直接复制模板文件
shutil.copy2(template_path, app_path)
print(f"✓ 应用程序主文件已从模板创建: {app_path}")
# def _createAppFile(self, build_dir, project_name):
# """创建应用程序主文件"""
# app_code = f'''#!/usr/bin/env python3
# # -*- coding: utf-8 -*-
#
# """
# {project_name} - Panda3D应用程序
# 使用Panda3D引擎编辑器创建
# """
#
# from __future__ import print_function
#
# import json
#
# from direct.actor.Actor import Actor
# from panda3d.core import TextNode, CardMaker, TextureStage, NodePath
# #获取渲染管线路径
# import sys
# import os
#
# render_pipeline_path = 'RenderPipelineFile'
# project_root = os.path.dirname(os.path.abspath(__file__))
# sys.path.insert(0,project_root)
# sys.path.insert(0,render_pipeline_path)
#
# import math
# from random import random,randint,seed
# from panda3d.core import Vec3,load_prc_file_data,Filename
# from direct.showbase.ShowBase import ShowBase
#
# os.chdir(os.path.dirname(os.path.realpath(__file__)))
#
# class MainApp(ShowBase):
# def __init__(self):
# load_prc_file_data("","""
# win-size 1200 720
# window-title Render
# """)
#
# pipeline_path = "../../"
#
# if not os.path.isfile(os.path.join(pipeline_path,"setup.py")):
# pipeline_path = "../../RenderPipeline"
#
# sys.path.insert(0,pipeline_path)
#
# from rpcore import RenderPipeline,SpotLight
# self.render_pipeline = RenderPipeline()
# self.render_pipeline.create(self)
#
# from rpcore.util.movement_controller import MovementController
#
# self.render_pipeline.daytime_mgr.time = "12:00"
# self._loadFont()
#
# self.loadFullScene()
# self.loadGUIFromJSON()
#
# self.controller = MovementController(self)
# self.controller.set_initial_position(
# Vec3(-7.5,-5.3,1.8),Vec3(-5.9,-4.0,1.6))
# self.controller.setup()
#
# base.accept("l",self.tour)
#
# def _loadFont(self):
# """加载中文字体"""
# self.chinese_font = None
# try:
# self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
# if not self.chinese_font:
# print("警告: 无法加载中文字体,将使用默认字体")
# else:
# print("✓ 中文字体加载成功")
# except:
# print("警告: 无法加载中文字体,将使用默认字体")
# self.chinese_font = None
#
# def getChineseFont(self):
# """获取中文字体"""
# return self.chinese_font
#
# def loadFullScene(self):
# """加载完整场景,包括所有元素"""
# try:
# scene_file = "scene.bam"
# if os.path.exists(scene_file):
# # 使用readBamFile加载完整场景
# from panda3d.core import BamCache
# BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题
#
# scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file))
# if scene:
# scene.reparentTo(self.render)
# self.render_pipeline.prepare_scene(scene)
# print("✓ 完整场景加载成功")
#
# # 处理场景中的各种元素
# self.processSceneElements(scene)
# else:
# print("⚠️ 场景文件加载失败")
# else:
# print("⚠️ 未找到场景文件")
# except Exception as e:
# print(f"加载完整场景时出错: {{str(e)}}")
# import traceback
# traceback.print_exc()
#
# def processSceneElements(self, scene):
# """处理场景中的各种元素"""
# try:
# # 处理光源
# self.processLights(scene)
#
# # 处理GUI元素
# self.processGUIElements(scene)
#
# except Exception as e:
# print(f"处理场景元素时出错: {{str(e)}}")
#
# def processLights(self, scene):
# """处理场景中的光源"""
# try:
# # 查找并处理点光源
# point_lights = scene.findAllMatches("**/=element_type=point_light")
# for light_node in point_lights:
# try:
# from RenderPipelineFile.rpcore import PointLight
# light = PointLight()
#
# # 恢复光源属性
# if light_node.hasTag("light_energy"):
# light.energy = float(light_node.getTag("light_energy"))
# else:
# light.energy = 5000
#
# light.radius = 1000
# light.inner_radius = 0.4
# light.set_color_from_temperature(5 * 1000.0)
# light.casts_shadows = True
# light.shadow_map_resolution = 256
#
# light.setPos(light_node.getPos())
# self.render_pipeline.add_light(light)
# print(f"✓ 点光源 {{light_node.getName()}} 恢复成功")
# except Exception as e:
# print(f"恢复点光源 {{light_node.getName()}} 失败: {{str(e)}}")
#
# # 查找并处理聚光灯
# spot_lights = scene.findAllMatches("**/=element_type=spot_light")
# for light_node in spot_lights:
# try:
# from RenderPipelineFile.rpcore import SpotLight
# light = SpotLight()
#
# # 恢复光源属性
# if light_node.hasTag("light_energy"):
# light.energy = float(light_node.getTag("light_energy"))
# else:
# light.energy = 5000
#
# light.radius = 1000
# light.inner_radius = 0.4
# light.set_color_from_temperature(5 * 1000.0)
# light.casts_shadows = True
# light.shadow_map_resolution = 256
#
# light.setPos(light_node.getPos())
# self.render_pipeline.add_light(light)
# print(f"✓ 聚光灯 {{light_node.getName()}} 恢复成功")
# except Exception as e:
# print(f"恢复聚光灯 {{light_node.getName()}} 失败: {{str(e)}}")
#
# except Exception as e:
# print(f"处理光源时出错: {{str(e)}}")
#
# def processGUIElements(self, scene):
# """处理场景中的GUI元素"""
# try:
# # 查找并处理2D图像
# images_2d = scene.findAllMatches("**/=gui_type=image_2d")
# for img_node in images_2d:
# try:
# # GUI元素通常在场景加载时自动处理
# print(f"✓ 2D图像 {{img_node.getName()}} 已加载")
# except Exception as e:
# print(f"处理2D图像 {{img_node.getName()}} 失败: {{str(e)}}")
#
# except Exception as e:
# print(f"处理GUI元素时出错: {{str(e)}}")
#
# def tour(self):
# mopath = (
# (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)),
# (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)),
# (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)),
# (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)),
# (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)),
# (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)),
# )
# self.controller.play_motion_path(mopath,3.0)
#
# def loadGUIFromJSON(self):
# gui_json_path = "gui/gui_elements.json"
#
# try:
# if os.path.exists(gui_json_path):
# with open(gui_json_path, "r", encoding="utf-8") as f:
# content = f.read().strip()
# if content:
# gui_data = json.loads(content)
# self.createGUIElement(gui_data)
# except Exception as e:
# print(f"加载GUI元素失败: {{str(e)}}")
# import traceback
# traceback.print_exc()
#
# def createGUIElement(self,element_data):
# try:
# processed_names = set()
# element_original_data={{}}
# for i, gui_info in enumerate(element_data):
# name = gui_info.get("name", f"gui_element_{{i}}")
# element_original_data[name] = {
# "scale": gui_info.get("scale", [1, 1, 1]),
# "position": gui_info.get("position", [0, 0, 0]),
# "parent_name": gui_info.get("parent_name")
# }
# valid_parents = set()
# for gui_info in element_data:
# name = gui_info.get("name", f"gui_element_{{gui_info.get('index', 0)}}")
# valid_parents.add(name)
#
# for i ,gui_info in enumerate(element_data):
# try:
# gui_type = gui_info.get("type","unknown")
# name = gui_info.get("name",f"gui_element_{{i}}")
# position = gui_info.get("position",[0,0,0])
# scale = gui_info.get("scale",[1,1,1])
# tags = gui_info.get("tags",{{}})
# text = gui_info.get("text","")
# image_path = gui_info.get("image_path","")
# video_path = gui_info.get("video_path","")
# bg_image_path = gui_info.get("bg_image_path","")
# parent_name = gui_info.get("parent_name")
#
# if name in processed_names:
# continue
#
# processed_names.add(name)
#
# absolute_position = list(position)
# absolute_scale = list(scale)
#
# if parent_name and parent_name in element_original_data:
# parent_data = element_original_data[parent_name]
# parent_scale = parent_data["scale"]
#
# if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image",
# "2d_video_screen"]:
# # 位置需要乘以父级缩放来得到绝对位置
# for j in range(min(len(absolute_position), len(parent_scale))):
# absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0]
#
# # 缩放需要乘以父级缩放来得到绝对缩放
# for j in range(min(len(absolute_scale), len(parent_scale))):
# absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0]
#
# new_element = None
#
# if gui_type =="3d_text":
# size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5
# new_element = self.createGUI3DText(
# pos = tuple(absolute_position),
# text = text,
# size = size
# )
# elif gui_type == "button":
# # 确保传入正确的参数类型
# new_element = self.createGUIButton(
# pos=tuple(absolute_position),
# text=text,
# size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0,
# )
# except Exception as e:
# print(f"重建GUI元素失败 {{name}}: {{e}}")
# import traceback
# traceback.print_exc()
# continue
# except Exception as e:
# print(f"重建GUI元素失败: {{str(e)}}")
#
# def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1,command=None):
# from direct.gui.DirectGui import DirectButton
#
# button = DirectButton(
# text=text,
# pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式
# scale=size, # size 应该是数值而不是元组
# frameColor=(0.2, 0.6, 0.8, 1),
# text_font=self.getChineseFont() if self.getChineseFont() else None,
# rolloverSound=None,
# clickSound=None,
# parent=None,
# command=command
# )
#
# def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
# """创建3D文本GUI元素"""
# try:
# # 创建文本节点
# text_node = TextNode("gui_3d_text")
# text_node.setText(text)
# text_node.setAlign(TextNode.ACenter)
#
# # 设置字体(如果可用)
# if self.getChineseFont():
# text_node.setFont(self.getChineseFont())
#
# # 创建节点路径并添加到场景
# text_np = self.render.attachNewNode(text_node)
#
# # 设置位置和大小
# text_np.setPos(Vec3(pos[0], pos[1], pos[2]))
# text_np.setScale(size)
#
# # 设置面向摄像机
# #text_np.setBillboardPointEye()
#
# # 设置渲染属性
# text_np.setBin("fixed", 40)
# text_np.setDepthWrite(False)
#
# return text_np
# except Exception as e:
# print(f"❌ 创建3D文本失败: {{str(e)}}")
# import traceback
# traceback.print_exc()
# return None
#
# MainApp().run()
# '''
#
# app_path = os.path.join(build_dir, "main.py")
# with open(app_path, "w", encoding="utf-8") as f:
# f.write(app_code)
def _createStandardSetupFile(self, build_dir, project_name):
"""创建标准的setup.py文件 - 按照Panda3D官方文档"""
setup_code = f'''#!/usr/bin/env python3
@ -644,8 +1092,31 @@ setup(
'direct.task',
'direct.actor',
'direct.interval',
'direct.stdpy.file',
'direct.stdpy.pickle',
'panda3d.core',
'panda3d.direct',
'rpcore',
'rpcore.util.movement_controller',
'rpcore.native',
'rpcore.render_pipeline',
'rplibs',
'rpplugins',
'rpplugins.scattering',
'rpplugins.pssm',
'rpplugins.godrays',
'json',
'os',
'sys',
'six',
'collections',
'collections.abs',
'weakref',
'copy',
'itertools',
'importlib',
'importlib.util',
'importlib.machinery',
],
}},

File diff suppressed because it is too large Load Diff

View File

@ -25,9 +25,9 @@ class CrossPlatformPathHandler:
def normalize_model_path(self, filepath):
"""标准化模型文件路径"""
try:
print(f"\n=== 路径标准化处理 ===")
print(f"原始路径: {filepath}")
print(f"当前系统: {self.system}")
#print(f"\n=== 路径标准化处理 ===")
#print(f"原始路径: {filepath}")
#print(f"当前系统: {self.system}")
# 步骤1: 检查原始路径是否存在
if self._check_file_exists(filepath):
@ -54,10 +54,6 @@ class CrossPlatformPathHandler:
def _check_file_exists(self, filepath):
"""检查文件是否存在"""
exists = os.path.exists(filepath)
if exists:
print(f"✓ 文件存在: {filepath}")
else:
print(f"⚠️ 文件不存在: {filepath}")
return exists
def _panda3d_normalize(self, filepath):

View File

@ -21,6 +21,10 @@ class RotatorScript(ScriptBase):
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
def update(self, dt):
# 检查 gameObject 是否存在且不为空
if not self.gameObject or self.gameObject.isEmpty():
print("RotatorScript: gameObject is empty or None, skipping update")
return
"""每帧更新"""
if not self.is_rotating:
return

1061
templates/main_template.py Normal file

File diff suppressed because it is too large Load Diff

322
ui/icon_manager.py Normal file
View File

@ -0,0 +1,322 @@
"""
图标管理工具
负责统一管理应用程序中的所有图标
- 图标路径解析
- 图标缓存
- 图标预加载
- 图标错误处理
"""
import os
import sys
from typing import Dict, Optional
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtCore import QSize
class IconManager:
"""图标管理器类"""
def __init__(self):
"""初始化图标管理器"""
self.icon_cache: Dict[str, QIcon] = {}
self.icon_directory = self._get_icon_directory()
self.default_icon = None
# 预定义的图标映射
self.icon_map = {
# 主窗口图标
'app_logo': 'logo.png',
# 工具栏图标
'select_tool': 'select_tool.png',
'move_tool': 'move_tool.png',
'rotate_tool': 'rotate_tool.png',
'scale_tool': 'scale_tool.png',
# 菜单图标(如果有的话)
'new_file': 'new_file.png',
'open_file': 'open_file.png',
'save_file': 'save_file.png',
'exit': 'exit.png',
# 对象类型图标
'object_3d': 'object_3d.png',
'light': 'light.png',
'camera': 'camera.png',
'terrain': 'terrain.png',
'script': 'script.png',
# 状态图标
'success': 'success.png',
'warning': 'warning.png',
'error': 'error.png',
'info': 'info.png',
}
# 初始化默认图标
self._create_default_icon()
# 预加载常用图标
self._preload_icons()
def _get_icon_directory(self) -> str:
"""获取图标目录的绝对路径"""
# 获取当前文件的目录ui目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 获取项目根目录ui的父目录
project_root = os.path.dirname(current_dir)
# 拼接icons目录路径
icon_dir = os.path.join(project_root, "icons")
print(f"🔍 图标目录路径: {icon_dir}")
# 检查目录是否存在
if not os.path.exists(icon_dir):
print(f"⚠️ 图标目录不存在: {icon_dir}")
# 尝试创建目录
try:
os.makedirs(icon_dir, exist_ok=True)
print(f"✅ 已创建图标目录: {icon_dir}")
except Exception as e:
print(f"❌ 创建图标目录失败: {e}")
return icon_dir
def _create_default_icon(self):
"""创建默认图标"""
# 创建一个简单的默认图标
pixmap = QPixmap(16, 16)
pixmap.fill() # 填充为白色
self.default_icon = QIcon(pixmap)
def _preload_icons(self):
"""预加载常用图标"""
print("🔄 开始预加载图标...")
for icon_name, file_name in self.icon_map.items():
icon_path = os.path.join(self.icon_directory, file_name)
if os.path.exists(icon_path):
try:
icon = QIcon(icon_path)
self.icon_cache[icon_name] = icon
print(f"✅ 已加载图标: {icon_name} -> {file_name}")
except Exception as e:
print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}")
else:
print(f"⚠️ 图标文件不存在: {icon_path}")
print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标")
def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon:
"""
获取图标
Args:
icon_name: 图标名称可以是预定义名称或文件名
size: 图标尺寸
Returns:
QIcon对象
"""
# 首先检查缓存
if icon_name in self.icon_cache:
icon = self.icon_cache[icon_name]
if size:
# 如果指定了尺寸,返回指定尺寸的图标
pixmap = icon.pixmap(size)
return QIcon(pixmap)
return icon
# 如果不在缓存中,尝试从映射中获取
if icon_name in self.icon_map:
file_name = self.icon_map[icon_name]
icon_path = os.path.join(self.icon_directory, file_name)
else:
# 直接使用文件名
icon_path = os.path.join(self.icon_directory, icon_name)
if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
icon_path += '.png' # 默认添加.png扩展名
# 尝试加载图标
if os.path.exists(icon_path):
try:
icon = QIcon(icon_path)
# 缓存图标
self.icon_cache[icon_name] = icon
print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}")
if size:
pixmap = icon.pixmap(size)
return QIcon(pixmap)
return icon
except Exception as e:
print(f"❌ 加载图标失败: {icon_path}, 错误: {e}")
else:
print(f"⚠️ 图标文件不存在: {icon_path}")
# 返回默认图标
return self.default_icon
def get_icon_path(self, icon_name: str) -> str:
"""
获取图标文件的完整路径
Args:
icon_name: 图标名称
Returns:
图标文件的完整路径
"""
if icon_name in self.icon_map:
file_name = self.icon_map[icon_name]
else:
file_name = icon_name
if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
file_name += '.png'
icon_path = os.path.join(self.icon_directory, file_name)
if os.path.exists(icon_path):
return icon_path
else:
print(f"⚠️ 图标文件不存在: {icon_path}")
return ""
def has_icon(self, icon_name: str) -> bool:
"""
检查图标是否存在
Args:
icon_name: 图标名称
Returns:
是否存在
"""
return bool(self.get_icon_path(icon_name))
def add_icon(self, icon_name: str, icon_path: str) -> bool:
"""
添加新图标到缓存
Args:
icon_name: 图标名称
icon_path: 图标文件路径
Returns:
是否添加成功
"""
try:
if os.path.exists(icon_path):
icon = QIcon(icon_path)
self.icon_cache[icon_name] = icon
print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}")
return True
else:
print(f"❌ 图标文件不存在: {icon_path}")
return False
except Exception as e:
print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}")
return False
def refresh_cache(self):
"""刷新图标缓存"""
print("🔄 刷新图标缓存...")
self.icon_cache.clear()
self._preload_icons()
def get_available_icons(self) -> list:
"""获取所有可用的图标列表"""
available_icons = []
# 添加预定义的图标
available_icons.extend(self.icon_map.keys())
# 扫描图标目录中的所有图标文件
if os.path.exists(self.icon_directory):
for file_name in os.listdir(self.icon_directory):
if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
icon_name = os.path.splitext(file_name)[0]
if icon_name not in available_icons:
available_icons.append(icon_name)
return sorted(available_icons)
def get_cache_info(self) -> dict:
"""获取缓存信息"""
return {
'cached_icons': len(self.icon_cache),
'icon_directory': self.icon_directory,
'available_icons': len(self.get_available_icons()),
'cache_keys': list(self.icon_cache.keys())
}
def debug_info(self):
"""打印调试信息"""
print("=" * 50)
print("📋 图标管理器调试信息")
print("=" * 50)
info = self.get_cache_info()
print(f"图标目录: {info['icon_directory']}")
print(f"目录存在: {os.path.exists(info['icon_directory'])}")
print(f"缓存图标数: {info['cached_icons']}")
print(f"可用图标数: {info['available_icons']}")
if info['cache_keys']:
print("\n已缓存的图标:")
for key in info['cache_keys']:
print(f" - {key}")
print("\n图标目录内容:")
if os.path.exists(self.icon_directory):
for file_name in os.listdir(self.icon_directory):
file_path = os.path.join(self.icon_directory, file_name)
size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0
print(f" - {file_name} ({size} bytes)")
else:
print(" 目录不存在")
print("=" * 50)
# 全局图标管理器实例
_icon_manager = None
def get_icon_manager() -> IconManager:
"""获取全局图标管理器实例"""
global _icon_manager
if _icon_manager is None:
_icon_manager = IconManager()
return _icon_manager
def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon:
"""便捷函数:获取图标"""
return get_icon_manager().get_icon(icon_name, size)
def get_icon_path(icon_name: str) -> str:
"""便捷函数:获取图标路径"""
return get_icon_manager().get_icon_path(icon_name)
def has_icon(icon_name: str) -> bool:
"""便捷函数:检查图标是否存在"""
return get_icon_manager().has_icon(icon_name)
if __name__ == "__main__":
# 测试代码
print("🧪 测试图标管理器...")
manager = IconManager()
manager.debug_info()
# 测试获取图标
logo_icon = manager.get_icon('app_logo')
print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}")
move_tool_icon = manager.get_icon('move_tool')
print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}")

405
ui/icon_manager_gui.py Normal file
View File

@ -0,0 +1,405 @@
"""
图标管理器GUI工具
提供图形界面来管理和查看图标
- 显示所有可用图标
- 图标预览
- 图标信息
- 图标刷新
"""
import os
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QListWidget, QListWidgetItem, QLabel, QGroupBox,
QTextEdit, QSplitter, QDialog, QDialogButtonBox,
QFileDialog, QMessageBox, QScrollArea, QGridLayout)
from PyQt5.QtGui import QIcon, QPixmap, QFont
from PyQt5.QtCore import Qt, QSize
from ui.icon_manager import get_icon_manager
class IconPreviewWidget(QWidget):
"""图标预览控件"""
def __init__(self):
super().__init__()
self.setupUI()
def setupUI(self):
"""设置UI"""
layout = QVBoxLayout(self)
# 图标显示区域
self.icon_label = QLabel()
self.icon_label.setAlignment(Qt.AlignCenter)
self.icon_label.setStyleSheet("""
QLabel {
border: 2px dashed #8b5cf6;
border-radius: 8px;
background-color: #2d2d44;
color: #e0e0ff;
min-height: 100px;
margin: 10px;
}
""")
self.icon_label.setText("选择图标查看预览")
layout.addWidget(self.icon_label)
# 图标信息
self.info_label = QLabel()
self.info_label.setStyleSheet("""
QLabel {
background-color: #252538;
color: #e0e0ff;
padding: 10px;
border-radius: 4px;
font-family: monospace;
}
""")
self.info_label.setText("图标信息将在此显示")
layout.addWidget(self.info_label)
def showIcon(self, icon_name: str, icon: QIcon):
"""显示图标"""
if not icon.isNull():
# 显示不同尺寸的图标
sizes = [16, 24, 32, 48, 64]
pixmaps = []
# 创建组合图标显示
for size in sizes:
pixmap = icon.pixmap(QSize(size, size))
if not pixmap.isNull():
pixmaps.append((size, pixmap))
if pixmaps:
# 创建合成图片显示多个尺寸
total_width = sum(size for size, _ in pixmaps) + 20 * (len(pixmaps) - 1)
max_height = max(size for size, _ in pixmaps)
combined_pixmap = QPixmap(total_width, max_height + 40)
combined_pixmap.fill(Qt.transparent)
from PyQt5.QtGui import QPainter, QPen
painter = QPainter(combined_pixmap)
x = 0
for size, pixmap in pixmaps:
# 绘制图标
y = (max_height - size) // 2
painter.drawPixmap(x, y, pixmap)
# 绘制尺寸标签
painter.setPen(QPen(Qt.white))
painter.drawText(x, max_height + 15, f"{size}x{size}")
x += size + 20
painter.end()
self.icon_label.setPixmap(combined_pixmap)
else:
self.icon_label.setText("无法加载图标")
else:
self.icon_label.setText("图标无效")
# 更新信息
info_text = f"图标名称: {icon_name}\n"
info_text += f"图标有效: {'' if not icon.isNull() else ''}\n"
# 获取图标管理器信息
icon_manager = get_icon_manager()
icon_path = icon_manager.get_icon_path(icon_name)
if icon_path:
info_text += f"文件路径: {icon_path}\n"
if os.path.exists(icon_path):
size = os.path.getsize(icon_path)
info_text += f"文件大小: {size} bytes\n"
# 获取可用尺寸
if not icon.isNull():
available_sizes = icon.availableSizes()
if available_sizes:
sizes_text = ", ".join(f"{s.width()}x{s.height()}" for s in available_sizes)
info_text += f"可用尺寸: {sizes_text}\n"
self.info_label.setText(info_text)
class IconManagerDialog(QDialog):
"""图标管理器对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.icon_manager = get_icon_manager()
self.setupUI()
self.loadIcons()
def setupUI(self):
"""设置UI"""
self.setWindowTitle("图标管理器")
self.setModal(False)
self.resize(800, 600)
# 设置样式
self.setStyleSheet("""
QDialog {
background-color: #1e1e2e;
color: #e0e0ff;
}
QListWidget {
background-color: #252538;
color: #e0e0ff;
border: 1px solid #3a3a4a;
border-radius: 4px;
alternate-background-color: #2d2d44;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #3a3a4a;
}
QListWidget::item:hover {
background-color: #3a3a4a;
}
QListWidget::item:selected {
background-color: rgba(139, 92, 246, 100);
color: white;
}
QPushButton {
background-color: #8b5cf6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
}
QPushButton:hover {
background-color: #7c3aed;
}
QPushButton:pressed {
background-color: #6d28d9;
}
QGroupBox {
background-color: #252538;
border: 1px solid #3a3a4a;
border-radius: 6px;
margin-top: 1ex;
color: #e0e0ff;
font-weight: 500;
padding-top: 10px;
}
QGroupBox::title {
subline-offset: -2px;
padding: 0 8px;
color: #c0c0e0;
font-weight: 500;
}
QTextEdit {
background-color: #252538;
color: #e0e0ff;
border: 1px solid #3a3a4a;
border-radius: 4px;
font-family: monospace;
}
""")
layout = QVBoxLayout(self)
# 顶部按钮栏
button_layout = QHBoxLayout()
self.refresh_btn = QPushButton("刷新图标")
self.refresh_btn.clicked.connect(self.refreshIcons)
button_layout.addWidget(self.refresh_btn)
self.add_icon_btn = QPushButton("添加图标")
self.add_icon_btn.clicked.connect(self.addIcon)
button_layout.addWidget(self.add_icon_btn)
self.debug_btn = QPushButton("调试信息")
self.debug_btn.clicked.connect(self.showDebugInfo)
button_layout.addWidget(self.debug_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
# 主分割器
splitter = QSplitter(Qt.Horizontal)
# 左侧:图标列表
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
list_group = QGroupBox("可用图标")
list_layout = QVBoxLayout(list_group)
self.icon_list = QListWidget()
self.icon_list.itemSelectionChanged.connect(self.onIconSelected)
list_layout.addWidget(self.icon_list)
left_layout.addWidget(list_group)
splitter.addWidget(left_widget)
# 右侧:图标预览
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
preview_group = QGroupBox("图标预览")
preview_layout = QVBoxLayout(preview_group)
self.preview_widget = IconPreviewWidget()
preview_layout.addWidget(self.preview_widget)
right_layout.addWidget(preview_group)
splitter.addWidget(right_widget)
# 设置分割器比例
splitter.setSizes([300, 500])
layout.addWidget(splitter)
# 底部按钮
button_box = QDialogButtonBox(QDialogButtonBox.Close)
button_box.rejected.connect(self.close)
layout.addWidget(button_box)
def loadIcons(self):
"""加载图标列表"""
self.icon_list.clear()
available_icons = self.icon_manager.get_available_icons()
for icon_name in available_icons:
item = QListWidgetItem()
# 获取图标
icon = self.icon_manager.get_icon(icon_name, QSize(24, 24))
# 设置图标和文本
if not icon.isNull():
item.setIcon(icon)
item.setText(f"🎨 {icon_name}")
else:
item.setText(f"{icon_name}")
item.setData(Qt.UserRole, icon_name)
self.icon_list.addItem(item)
print(f"📊 加载了 {len(available_icons)} 个图标")
def onIconSelected(self):
"""当选择图标时"""
current_item = self.icon_list.currentItem()
if current_item:
icon_name = current_item.data(Qt.UserRole)
icon = self.icon_manager.get_icon(icon_name)
self.preview_widget.showIcon(icon_name, icon)
def refreshIcons(self):
"""刷新图标"""
print("🔄 刷新图标缓存...")
self.icon_manager.refresh_cache()
self.loadIcons()
QMessageBox.information(self, "完成", "图标缓存已刷新")
def addIcon(self):
"""添加新图标"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择图标文件",
"",
"图像文件 (*.png *.jpg *.jpeg *.svg *.ico);;所有文件 (*)"
)
if file_path:
# 获取文件名作为图标名称
file_name = os.path.basename(file_path)
icon_name = os.path.splitext(file_name)[0]
# 复制文件到图标目录
import shutil
target_path = os.path.join(self.icon_manager.icon_directory, file_name)
try:
shutil.copy2(file_path, target_path)
# 添加到缓存
success = self.icon_manager.add_icon(icon_name, target_path)
if success:
QMessageBox.information(self, "成功", f"图标 '{icon_name}' 已添加")
self.loadIcons()
else:
QMessageBox.warning(self, "失败", "添加图标失败")
except Exception as e:
QMessageBox.critical(self, "错误", f"复制文件失败:\n{str(e)}")
def showDebugInfo(self):
"""显示调试信息"""
debug_dialog = QDialog(self)
debug_dialog.setWindowTitle("图标管理器调试信息")
debug_dialog.resize(600, 400)
layout = QVBoxLayout(debug_dialog)
text_edit = QTextEdit()
text_edit.setFont(QFont("Consolas", 10))
# 获取调试信息
info = self.icon_manager.get_cache_info()
debug_text = "图标管理器调试信息\n"
debug_text += "=" * 50 + "\n\n"
debug_text += f"图标目录: {info['icon_directory']}\n"
debug_text += f"目录存在: {os.path.exists(info['icon_directory'])}\n"
debug_text += f"缓存图标数: {info['cached_icons']}\n"
debug_text += f"可用图标数: {info['available_icons']}\n\n"
debug_text += "已缓存的图标:\n"
for key in info['cache_keys']:
debug_text += f" - {key}\n"
debug_text += "\n图标目录内容:\n"
if os.path.exists(info['icon_directory']):
for file_name in os.listdir(info['icon_directory']):
file_path = os.path.join(info['icon_directory'], file_name)
if os.path.isfile(file_path):
size = os.path.getsize(file_path)
debug_text += f" - {file_name} ({size} bytes)\n"
else:
debug_text += " 目录不存在\n"
text_edit.setPlainText(debug_text)
layout.addWidget(text_edit)
button_box = QDialogButtonBox(QDialogButtonBox.Close)
button_box.rejected.connect(debug_dialog.close)
layout.addWidget(button_box)
debug_dialog.exec_()
def show_icon_manager(parent=None):
"""显示图标管理器对话框"""
dialog = IconManagerDialog(parent)
dialog.show()
return dialog
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
# 设置全局样式
app.setStyleSheet("""
QApplication {
background-color: #1e1e2e;
color: #e0e0ff;
}
""")
dialog = IconManagerDialog()
dialog.show()
sys.exit(app.exec_())

View File

@ -25,10 +25,39 @@ class InterfaceManager:
# 更新场景树
self.world.scene_manager.updateSceneTree()
def onTreeWidgetClicked(self, index):
"""处理树形控件点击事件(包括空白区域)"""
# 检查点击的是否是空白区域
if not self.treeWidget.itemFromIndex(index): # 点击的是空白区域
self.world.selection.updateSelection(None)
self.world.property_panel.clearPropertyPanel()
print("点击树形控件空白区域,清除选中状态")
def onTreeCurrentItemChanged(self, current, previous):
"""处理树形控件当前选中项改变事件"""
# 当 current 为 None 时,表示点击了空白区域
if current is None:
self.world.selection.updateSelection(None)
print("点击空白区域,清除选中状态")
# 当 current 不为 None 时,表示选中了某个项目
else:
# 更新选择状态
nodePath = current.data(0, Qt.UserRole)
if nodePath:
self.world.selected_np = nodePath
self.world.selection.updateSelection(nodePath)
self.world.property_panel.updatePropertyPanel(current)
print(f"树形控件选中项改变: {current.text(0)}")
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件"""
if not item:
#print(f"树形控件点击事件触发item: {item}, column: {column}")
# 检查是否点击了空白区域
# 当点击空白区域时item可能是一个空的QTreeWidgetItem对象
if not item or (item.text(0) == "" and item.data(0, Qt.UserRole) is None):
self.world.selection.updateSelection(None)
print("点击空白区域,清除选中状态")
return
self.world.property_panel.updatePropertyPanel(item)
@ -39,15 +68,12 @@ class InterfaceManager:
# 更新选择状态
self.world.selected_np = nodePath
self.world.selection.updateSelection(nodePath)
# 更新属性面板
#self.world.property_panel.updatePropertyPanel(item)
print(f"树形控件点击: {item.text(0)}")
else:
# 如果没有节点对象,清除选择
self.world.selection.updateSelection(None)
#self.world.property_panel.clearPropertyPanel()
self.world.property_panel.clearPropertyPanel()
print("点击了无数据项,清除选中状态")
# def showTreeContextMenu(self, position):
# """显示树形控件的右键菜单"""
@ -313,6 +339,23 @@ class InterfaceManager:
groundItem.setData(0, Qt.UserRole, self.world.ground)
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
ground_nodes = [
('ground2','地板2'),
('ground3','地板3'),
('ground4','地板4'),
('ground5','地板5'),
('ground6','地板6')
]
for attr_name,display_name in ground_nodes:
if hasattr(self.world,attr_name):
ground_node = getattr(self.world,attr_name)
if ground_node:
extraGroundItem = QTreeWidgetItem(sceneRoot,[display_name])
extraGroundItem.setData(0,Qt.UserRole,ground_node)
extraGroundItem.setData(0,Qt.UserRole+1,"SCENE_NODE")
#添加灯光节点
for light in self.world.Spotlight:
if light:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ from PyQt5.QtCore import Qt, QUrl, QMimeData
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
from PyQt5.sip import wrapinstance
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import ModelRoot, NodePath
from panda3d.core import ModelRoot, NodePath, CollisionNode
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from scene import util
@ -30,7 +30,67 @@ class NewProjectDialog(QDialog):
super().__init__(parent)
self.setWindowTitle("新建项目")
self.setMinimumWidth(500)
# 设置对话框样式与主窗口保持一致
self.setStyleSheet("""
QDialog {
background-color: #252538;
color: #e0e0ff;
}
QGroupBox {
background-color: #2d2d44;
border: 1px solid #3a3a4a;
border-radius: 6px;
margin-top: 1ex; /* 保持这个设置 */
color: #e0e0ff;
font-weight: 500;
padding-top: 10px; /* 增加顶部内边距为标题留出空间 */
}
QGroupBox::title {
padding: 0 8px;
color: #c0c0e0;
font-weight: 500;
}
QLineEdit {
background-color: #2d2d44;
color: #e0e0ff;
border: 1px solid #3a3a4a;
border-radius: 4px;
padding: 6px;
}
QLineEdit:disabled {
background-color: #1e1e2e;
color: #8888aa;
}
QPushButton {
background-color: #8b5cf6;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
}
QPushButton:hover {
background-color: #7c3aed;
}
QPushButton:pressed {
background-color: #6d28d9;
}
QPushButton:disabled {
background-color: #4c4c6e;
color: #8888aa;
}
QLabel {
color: #e0e0ff;
}
QLabel:disabled {
color: #8888aa;
}
QDialogButtonBox QPushButton {
min-width: 80px;
}
""")
# 创建布局
layout = QVBoxLayout(self)
@ -156,7 +216,7 @@ class CustomPanda3DWidget(QPanda3DWidget):
event.acceptProposedAction()
else:
event.ignore()
def wheelEvent(self, event):
"""处理滚轮事件"""
if event.angleDelta().y() > 0:
@ -1251,14 +1311,83 @@ class CustomConsoleDockWidget(QWidget):
self.autoScrollBtn = QPushButton("自动滚动")
self.autoScrollBtn.setCheckable(True)
self.autoScrollBtn.setChecked(True)
self.autoScrollBtn.setStyleSheet("""
QPushButton {
background-color: #2d2d44;
color: #e0e0ff;
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
}
QPushButton:checked {
background-color: #8b5cf6;
color: white;
border: 1px solid #7c3aed;
}
QPushButton:hover {
background-color: #3a3a4a;
}
QPushButton:checked:hover {
background-color: #7c3aed;
}
QPushButton:pressed {
background-color: #6d28d9;
}
""")
toolbar.addWidget(self.autoScrollBtn)
# 时间戳开关
self.timestampBtn = QPushButton("显示时间")
self.timestampBtn.setCheckable(True)
self.timestampBtn.setChecked(True)
self.timestampBtn.setStyleSheet("""
QPushButton {
background-color: #2d2d44;
color: #e0e0ff;
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
}
QPushButton:checked {
background-color: #8b5cf6;
color: white;
border: 1px solid #7c3aed;
}
QPushButton:hover {
background-color: #3a3a4a;
}
QPushButton:checked:hover {
background-color: #7c3aed;
}
QPushButton:pressed {
background-color: #6d28d9;
}
""")
toolbar.addWidget(self.timestampBtn)
self.fpsLabel = QLabel("FPS:0.0")
self.fpsLabel.setStyleSheet("""
QLabel {
background-color: #2d2d44;
color: #80ff80;
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
font-family: 'Consolas', 'Monaco', monospace;
}
""")
self.fpsLabel.setMinimumWidth(100)
self.fpsLabel.setAlignment(Qt.AlignCenter)
toolbar.addWidget(self.fpsLabel)
# 帧率更新定时器
self.fpsTimer = QTimer()
self.fpsTimer.timeout.connect(self.updateFPS)
self.fpsTimer.start(1000) # 每秒更新一次
toolbar.addStretch()
layout.addLayout(toolbar)
@ -1300,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget):
# 添加欢迎信息
self.addMessage("🎮 编辑器控制台已启动", "INFO")
def updateFPS(self):
try:
if hasattr(self.world,'clock'):
fps = self.world.clock.getAverageFrameRate()
self.fpsLabel.setText(f"FPS:{fps:.1f}")
# 根据帧率设置颜色
if fps >= 50:
color = "#80ff80" # 绿色 - 优秀
elif fps >= 30:
color = "#ffff80" # 黄色 - 一般
else:
color = "#ff8080" # 红色 - 较差
self.fpsLabel.setStyleSheet(f"""
QLabel {{
background-color: #2d2d44;
color: {color};
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
font-family: 'Consolas', 'Monaco', monospace;
}}
""")
except Exception as e:
pass # 静默处理错误,避免影响控制台功能
def setupConsoleRedirect(self):
"""设置控制台重定向"""
import sys
@ -1414,39 +1571,57 @@ class CustomTreeWidget(QTreeWidget):
self.initData()
self.setupUI() # 初始化界面
self.setupContextMenu() # 初始化右键菜单
self.setupDragDrop() # 设置拖拽功能
self.original_scales={}
self.setStyleSheet("""
/* 设置折叠状态下带子节点的箭头颜色 */
QTreeWidget::branch:has-children:!open {
color: #8b5cf6; /* 紫色 */
}
/* 设置展开状态下带子节点的箭头颜色 */
QTreeWidget::branch:has-children:open {
color: #9ca3af; /* 灰色,提供状态变化反馈 */
}
/* 鼠标悬停在任意箭头上时颜色变亮 */
QTreeWidget::branch:hover {
color: #a78bfa; /* 亮紫色 */
}
""")
def initData(self):
"""初始化变量"""
# 定义2D GUI元素类型
self.gui_2d_types = {
"GUI_BUTTON", # DirectButton
"GUI_LABEL", # DirectLabel
"GUI_ENTRY", # DirectEntry
"GUI_IMAGE",
"GUI_BUTTON", # GUI 按钮
"GUI_LABEL", # GUI 标签
"GUI_ENTRY", # GUI 输入框
"GUI_IMAGE", # GUI 图片
"GUI_2D_VIDEO_SCREEN", # GUI 2D视频
"GUI_SPHERICAL_VIDEO", # GUI 3D球形视频
"GUI_NODE" # 其他2D GUI容器
}
# 定义3D GUI元素类型
self.gui_3d_types = {
"GUI_3DTEXT", # 3D TextNode
"GUI_3DIMAGE",
"GUI_VIRTUAL_SCREEN" # Virtual Screen
"GUI_3DTEXT", # 3D 文本节点
"GUI_3DIMAGE", # 3D 图片节点
"GUI_VIRTUAL_SCREEN", # 3D视频
"GUI_VirtualScreen" # 3D虚拟视频
}
# 定义3D场景节点类型可以接受3D GUI元素和其他3D场景元素
self.scene_3d_types = {
"SCENE_ROOT",
"SCENE_NODE",
"LIGHT_NODE",
"LIGHT_NODE", # 灯节点
"CAMERA_NODE",
"IMPORTED_MODEL_NODE",
"IMPORTED_MODEL_NODE", # 导入模型节点
"MODEL_NODE",
"TERRAIN_NODE",
"CESIUM_TILESET_NODE"
"TERRAIN_NODE", # 地形节点
"CESIUM_TILESET_NODE" # 3D Tileset
}
# 这是一个最佳实践,它让代码的意图变得非常清晰。
@ -1747,7 +1922,7 @@ class CustomTreeWidget(QTreeWidget):
elif is_dragged_3d_gui:
if is_target_3d_scene:
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
return True
return False
elif is_target_2d_gui:
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
@ -1773,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget):
elif is_target_3d_gui:
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
return True
return False
else:
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}")
return False
@ -1831,7 +2006,7 @@ class CustomTreeWidget(QTreeWidget):
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport:
indicator_str = "OnViewport"
print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
#print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
if event.source() != self:
event.ignore()
@ -1963,7 +2138,7 @@ class CustomTreeWidget(QTreeWidget):
return
# 默认选中场景根节点,通常是第一个顶级节点
next_item_to_select = self.topLevelItem(0)
#next_item_to_select = self.topLevelItem(0)
# 3. 执行删除循环
deleted_count = 0
@ -1990,7 +2165,6 @@ class CustomTreeWidget(QTreeWidget):
if hasattr(panda_node, 'getPythonTag'):
light_object = panda_node.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
print(f'11111111111111111111111111,{light_object.casts_shadows}')
self.world.render_pipeline.remove_light(light_object)
# 从world列表中移除
@ -2048,25 +2222,7 @@ class CustomTreeWidget(QTreeWidget):
# 4. 删除操作完成后更新UI ---
if deleted_count > 0:
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
# 检查预备选择的节点是否还有效 (例如,父节点可能也一同被删了)
# 如果next_item_to_select在树中找不到了就退回到选择根节点
if next_item_to_select and self.indexFromItem(next_item_to_select).isValid():
new_selection_item = next_item_to_select
else:
# 如果之前的父节点也一并被删除了,就默认选择场景根节点
new_selection_item = self.topLevelItem(0)
if new_selection_item:
# 设置UI树的选择
self.setCurrentItem(new_selection_item)
# 获取对应的Panda3D节点
new_panda_node = new_selection_item.data(0, Qt.UserRole)
# 调用您提供的函数来更新选择状态和属性面板
self.update_selection_and_properties(new_panda_node, new_selection_item)
else:
# 如果连根节点都没有了(例如清空场景),则清空选择
self.update_selection_and_properties(None, None)
self.update_selection_and_properties(None, None)
def delete_item(self, panda_node):
"""删除指定节点 panda3Dnode- 优化和修复版本"""
@ -2074,6 +2230,14 @@ class CustomTreeWidget(QTreeWidget):
print(" 尝试删除一个空的或无效的节点,操作取消。")
return
# #如果有命令管理系统,则使用命令系统
# if hasattr(self.world,'command_manager') and self.world.command_manager:
# from core.Command_System import DeleteNodeCommand
# parent_node = panda_node.getParent()
# command = DeleteNodeCommand(panda_node,parent_node)
# self.world.command_manager.execute_command(command)
# return
# --- 关键修复:在操作前,安全地获取节点名字 ---
node_name_for_logging = panda_node.getName()
@ -2088,7 +2252,6 @@ class CustomTreeWidget(QTreeWidget):
if not item:
print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。")
return
try:
# 2. 过滤受保护节点
node_type = item.data(0, Qt.UserRole + 1)
@ -2126,6 +2289,24 @@ class CustomTreeWidget(QTreeWidget):
import traceback
traceback.print_exc()
def clear_tree(self):
"""清空UI树"""
print("Clear")
self.clear()
# 创建场景根节点
sceneRoot = QTreeWidgetItem(self, ['场景'])
sceneRoot.setData(0, Qt.UserRole, self.world.render)
sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
# 添加相机节点
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
cameraItem.setData(0, Qt.UserRole, self.world.cam)
cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
# 添加地板节点
if hasattr(self.world, 'ground') and self.world.ground:
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
def _cleanup_panda_node_resources(self, panda_node):
"""一个集中的辅助函数用于清理与Panda3D节点相关的所有资源。"""
if not panda_node or panda_node.is_empty():
@ -2192,53 +2373,64 @@ class CustomTreeWidget(QTreeWidget):
return top_item
return None
def create_model_items(self, model):
def create_model_items(self, model: NodePath):
"""
此函数保持不变
创建模型项
只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根
然后完整地展示这些分支
"""
if not model:
print("传入的参数model为空")
return
# 创建根节点项
# root_item = QTreeWidgetItem(self)
# root_item.setText(0, model.getName() or "Unnamed Node")
# root_item.setText(1, model.node().getTypeName())
# root_item.setIcon(0, self.item_icons.get('model', self.item_icons['default']))
# 存储NodePath引用以便后续操作
# root_item.setData(0, Qt.UserRole, model)
# 找到场景树的根节点,我们将把模型节点添加到这里
root_item = self._findSceneRoot()
if not root_item:
print("错误:未能找到场景根节点项")
return
# 递归添加子节点
self._add_children_recursive(root_item, model)
# 1. 在模型的第一层子节点中进行筛选
for child_node in model.getChildren():
if child_node.hasTag("is_scene_element"):
print(f"找到带标签的根节点:{child_node.getName()}")
if (child_node.hasTag("gui_type")and
child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]):
print(f"跳过3dGUI节点{child_node.getName()}")
continue
return root_item
# 为这个带标签的节点创建一个树项
child_item = QTreeWidgetItem(root_item)
child_item.setText(0, child_node.getName() or "Unnamed Tagged Node")
child_item.setData(0, Qt.UserRole, child_node)
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
# self._add_node_info(child_item, child_node) # 可选信息
def _add_children_recursive(self, parent_item, node_path: NodePath):
"""递归添加子节点到树项"""
print(f'开始递归添加子节点')
# 获取所有子节点
children = node_path.getChildren()
# 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体)
self._add_all_children_unconditionally(child_item, child_node)
for i in range(children.getNumPaths()):
child_node: NodePath = children.getPath(i)
def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath):
"""
此函数已更新
无条件地递归地添加一个节点下的所有子节点但会跳过碰撞节点
"""
for child_node in node_path.getChildren():
# 过滤条件
if not child_node.hasTag("is_scene_element"):
print(f"不存在------------------------{child_node.hasTag('is_scene_element')}")
continue
# 新增:检查节点是否为碰撞节点
if isinstance(child_node.node(), CollisionNode):
# print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试
continue # 如果是,则跳过此节点及其所有子节点
print(f"存在------------------------{child_node.getName()}")
# 创建子项
child_item = QTreeWidgetItem(parent_item)
child_item.setText(0, child_node.getName() or f"Child_{i}")
# 存储NodePath引用
child_item.setText(0, child_node.getName() or "Unnamed Child")
child_item.setData(0, Qt.UserRole, child_node)
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
# self._add_node_info(child_item, child_node) # 可选信息
# 添加额外信息(可选)
# self._add_node_info(child_item, child_node)
# 递归处理子节点的子节点
if child_node.getNumChildren() > 0:
self._add_children_recursive(child_item, child_node)
# 继续无条件地递归
if not child_node.is_empty():
self._add_all_children_unconditionally(child_item, child_node)
# ==================== 辅助方法 ====================
def _findSceneRoot(self):
@ -2269,6 +2461,17 @@ class CustomTreeWidget(QTreeWidget):
def add_node_to_tree_widget(self, node, parent_item, node_type):
"""将node元素添加到树形控件"""
if hasattr(node, 'getTag'):
if node.hasTag('tree_item_type'):
print(f"node0: {node.getName()},{node.getTag('tree_item_type')}")
tree_type = node.getTag('tree_item_type')
else:
node.setTag('tree_item_type', node_type)
else:
print(f"node2: {node.getName()},{node_type}")
tree_type = node_type
# BLACK_LIST 和依赖项导入保持不变
BLACK_LIST = {'', '**', 'temp', 'collision'}
from panda3d.core import CollisionNode, ModelRoot
@ -2283,7 +2486,7 @@ class CustomTreeWidget(QTreeWidget):
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
nodeItem.setData(0, Qt.UserRole, node)
nodeItem.setData(0, Qt.UserRole + 1, node_type)
nodeItem.setData(0, Qt.UserRole + 1, tree_type)
for child in node.getChildren():
# 递归调用但我们只关心顶级的nodeItem
@ -2301,7 +2504,7 @@ class CustomTreeWidget(QTreeWidget):
node_name = ""
try:
if node_type == "IMPORTED_MODEL_NODE":
if tree_type == "IMPORTED_MODEL_NODE":
# getTag('file') 可能是你自己设置的tag这里假设它存在
node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName()
@ -2312,7 +2515,7 @@ class CustomTreeWidget(QTreeWidget):
node_name = node.getName() if hasattr(node, 'getName') else "node"
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
new_qt_item.setData(0, Qt.UserRole, node)
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
new_qt_item.setData(0, Qt.UserRole + 1, tree_type)
# 确保 new_qt_item 成功创建后再继续操作
if new_qt_item: