addRender #7
@ -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
@ -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")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
BIN
Resources/models/DancingTwerk.glb
Normal file
BIN
Resources/models/DancingTwerk.glb
Normal file
Binary file not shown.
BIN
Resources/models/Haqijingzhu.glb
Normal file
BIN
Resources/models/Haqijingzhu.glb
Normal file
Binary file not shown.
BIN
Resources/models/JQB_auto_converted.glb
Normal file
BIN
Resources/models/JQB_auto_converted.glb
Normal file
Binary file not shown.
@ -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
648
core/Command_System.py
Normal 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()
|
||||
|
||||
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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']):
|
||||
"""创建鼠标射线"""
|
||||
# 组合掩码
|
||||
|
||||
@ -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
495
core/patrol_system.py
Normal 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("✓ 默认巡检路线已创建")
|
||||
|
||||
@ -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
@ -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)
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ class ToolManager:
|
||||
"""初始化工具管理器"""
|
||||
self.world = world
|
||||
self.currentTool = "选择" # 默认工具为选择工具
|
||||
print(f"当前工具: {self.currentTool}")
|
||||
|
||||
def setCurrentTool(self, tool):
|
||||
"""设置当前工具"""
|
||||
|
||||
147
core/world.py
147
core/world.py
@ -288,7 +288,8 @@ class CoreWorld(Panda3DWorld):
|
||||
self.cam.setPos(0, -50, 20)
|
||||
self.cam.lookAt(0, 0, 0)
|
||||
self.camLens.setFov(80)
|
||||
# self.cam.setTag("is_scene_element", "1")
|
||||
self.cam.setTag("is_scene_element", "1")
|
||||
self.cam.setTag("tree_item_type", "CAMERA_NODE")
|
||||
print("✓ 相机设置完成")
|
||||
|
||||
def _setupLighting(self):
|
||||
@ -320,19 +321,71 @@ class CoreWorld(Panda3DWorld):
|
||||
# 创建地板节点
|
||||
self.ground = self.render.attachNewNode(cm.generate())
|
||||
self.ground.setP(-90)
|
||||
self.ground.setZ(-0.1)
|
||||
self.ground.setZ(-1.0)
|
||||
self.ground.setColor(0.8, 0.8, 0.8, 1)
|
||||
# self.ground.setTag("is_scene_element", "1")
|
||||
self.ground.setTag("is_scene_element", "1")
|
||||
self.ground.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
# 创建支持贴图的材质
|
||||
mat = Material()
|
||||
mat.setName("GroundMaterial")
|
||||
color = LColor(1, 1, 1, 0.8)
|
||||
mat.set_base_color(color)
|
||||
mat.set_roughness(0.5) # 设置合适的初始粗糙度
|
||||
mat.set_roughness(1) # 设置合适的初始粗糙度
|
||||
mat.set_metallic(0.5) # 设置较低的初始金属性
|
||||
self.ground.set_material(mat)
|
||||
|
||||
#创建第二个相同的地面,位置稍有偏移
|
||||
self.ground2 = self.render.attachNewNode(cm.generate())
|
||||
self.ground2.setH(-90)
|
||||
self.ground2.setZ(-1.0)
|
||||
self.ground2.setX(50) # 在X轴方向偏移
|
||||
self.ground2.setZ(49) # 在X轴方向偏移
|
||||
self.ground2.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground2.set_material(mat)
|
||||
self.ground2.setTag("is_scene_element", "1")
|
||||
self.ground2.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
# 创建第三个相同的地面,位置在另一个方向
|
||||
self.ground3 = self.render.attachNewNode(cm.generate())
|
||||
self.ground3.setH(90)
|
||||
self.ground3.setZ(-1.0)
|
||||
self.ground3.setX(-50) # 在X轴负方向偏移
|
||||
self.ground3.setZ(49) # 在X轴负方向偏移
|
||||
self.ground3.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground3.set_material(mat)
|
||||
self.ground3.setTag("is_scene_element", "1")
|
||||
self.ground3.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
self.ground4 = self.render.attachNewNode(cm.generate())
|
||||
# self.ground3.setR(90)
|
||||
self.ground4.setZ(-1.0)
|
||||
self.ground4.setY(50) # 在X轴负方向偏移
|
||||
self.ground4.setZ(49) # 在X轴负方向偏移
|
||||
self.ground4.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground4.set_material(mat)
|
||||
self.ground4.setTag("is_scene_element", "1")
|
||||
self.ground4.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
self.ground5 = self.render.attachNewNode(cm.generate())
|
||||
self.ground5.setP(180)
|
||||
self.ground5.setZ(-1)
|
||||
self.ground5.setY(-50) # 在X轴负方向偏移
|
||||
self.ground5.setZ(49) # 在X轴负方向偏移
|
||||
self.ground5.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground5.set_material(mat)
|
||||
self.ground5.setTag("is_scene_element", "1")
|
||||
self.ground5.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
self.ground6 = self.render.attachNewNode(cm.generate())
|
||||
self.ground6.setP(90)
|
||||
self.ground6.setZ(-1)
|
||||
self.ground6.setZ(99) # 在X轴负方向偏移
|
||||
self.ground6.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground6.set_material(mat)
|
||||
self.ground6.setTag("is_scene_element", "1")
|
||||
self.ground6.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
# 应用默认PBR效果,确保支持贴图
|
||||
try:
|
||||
if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
||||
@ -349,6 +402,72 @@ class CoreWorld(Panda3DWorld):
|
||||
},
|
||||
50
|
||||
)
|
||||
# 为其他两个地面也应用相同的效果
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground2,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground3,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground4,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground5,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground6,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
print("✓ 地板PBR效果已应用")
|
||||
else:
|
||||
print("⚠️ RenderPipeline未初始化,地板将使用基础渲染")
|
||||
@ -438,12 +557,32 @@ class CoreWorld(Panda3DWorld):
|
||||
self.mouseRightPressed = True
|
||||
self.lastMouseX = evt['x']
|
||||
self.lastMouseY = evt['y']
|
||||
#
|
||||
# # 通过 Qt 窗口隐藏光标并捕获鼠标
|
||||
# try:
|
||||
# if hasattr(self, 'qtWidget') and self.qtWidget:
|
||||
# from PyQt5.QtCore import Qt
|
||||
# self.qtWidget.setCursor(Qt.BlankCursor)
|
||||
# # 捕获鼠标,使其无法离开窗口
|
||||
# self.qtWidget.grabMouse()
|
||||
# except Exception as e:
|
||||
# print(f"通过 Qt 隐藏光标时出错: {e}")
|
||||
|
||||
def mouseReleaseEventRight(self, evt):
|
||||
"""处理鼠标右键释放事件"""
|
||||
#print("右键释放")
|
||||
self.mouseRightPressed = False
|
||||
|
||||
# # 恢复 Qt 窗口光标并释放鼠标捕获
|
||||
# try:
|
||||
# if hasattr(self, 'qtWidget') and self.qtWidget:
|
||||
# from PyQt5.QtCore import Qt
|
||||
# self.qtWidget.unsetCursor() # 恢复默认光标
|
||||
# # 释放鼠标捕获
|
||||
# self.qtWidget.releaseMouse()
|
||||
# except Exception as e:
|
||||
# print(f"恢复 Qt 光标时出错: {e}")
|
||||
|
||||
def mouseMoveEvent(self, evt):
|
||||
"""处理鼠标移动事件 - 只处理相机旋转"""
|
||||
if not evt:
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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
BIN
icons/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
122
main.py
122
main.py
@ -6,7 +6,7 @@ from demo.video_integration import VideoManager
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
import sys
|
||||
import builtins # 添加这一行
|
||||
import builtins
|
||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
|
||||
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
|
||||
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox)
|
||||
@ -14,6 +14,8 @@ from PyQt5.QtCore import Qt, QDir, QUrl
|
||||
from PyQt5.QtGui import QDrag, QPainter, QPixmap
|
||||
from PyQt5.QtWidgets import QFileSystemModel
|
||||
from QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||||
from panda3d.core import loadPrcFileData
|
||||
loadPrcFileData("", "assertions 0")
|
||||
from core.world import CoreWorld
|
||||
from core.selection import SelectionSystem
|
||||
from core.event_handler import EventHandler
|
||||
@ -52,7 +54,16 @@ class MyWorld(CoreWorld):
|
||||
|
||||
# 初始化选择和变换系统
|
||||
self.selection = SelectionSystem(self)
|
||||
|
||||
|
||||
# 绑定F键用于聚焦选中节点
|
||||
self.accept("f", self.onFocusKeyPressed)
|
||||
self.accept("F", self.onFocusKeyPressed) # 大写F
|
||||
|
||||
#初始化巡检系统
|
||||
self.patrol_system = PatrolSystem(self)
|
||||
self.accept("p",self.onPatrolKeyPressed)
|
||||
self.accept("P",self.onPatrolKeyPressed)
|
||||
|
||||
# 初始化事件处理系统
|
||||
self.event_handler = EventHandler(self)
|
||||
|
||||
@ -80,7 +91,7 @@ class MyWorld(CoreWorld):
|
||||
# 初始化界面管理系统
|
||||
self.interface_manager = InterfaceManager(self)
|
||||
|
||||
|
||||
|
||||
# 启动脚本系统
|
||||
self.script_manager.start_system()
|
||||
|
||||
@ -93,6 +104,8 @@ class MyWorld(CoreWorld):
|
||||
|
||||
self.info_panel_manager = InfoPanelManager(self)
|
||||
|
||||
self.command_manager = CommandManager()
|
||||
|
||||
# 初始化碰撞管理器
|
||||
from core.collision_manager import CollisionManager
|
||||
self.collision_manager = CollisionManager(self)
|
||||
@ -107,7 +120,7 @@ class MyWorld(CoreWorld):
|
||||
self.vr_manager = None
|
||||
|
||||
# 调试选项
|
||||
self.debug_collision = False # 是否显示碰撞体
|
||||
self.debug_collision = True # 是否显示碰撞体
|
||||
|
||||
# 默认启用模型间碰撞检测(可选)
|
||||
self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
|
||||
@ -227,17 +240,17 @@ class MyWorld(CoreWorld):
|
||||
"""创建2D GUI文本输入框"""
|
||||
return self.gui_manager.createGUIEntry(pos, placeholder, size)
|
||||
|
||||
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
|
||||
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1):
|
||||
"""创建3D空间文本"""
|
||||
return self.gui_manager.createGUI3DText(pos, text, size)
|
||||
|
||||
def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)):
|
||||
def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1)):
|
||||
"""创建3D图片"""
|
||||
return self.gui_manager.createGUI3DImage(pos,text,size)
|
||||
|
||||
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1):
|
||||
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2):
|
||||
"""创建2D GUI图片"""
|
||||
return self.gui_manager.createGUI2DImage(pos, image_path, size*0.2)
|
||||
return self.gui_manager.createGUI2DImage(pos, image_path, size)
|
||||
|
||||
def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
|
||||
"""创建视频屏幕"""
|
||||
@ -675,6 +688,97 @@ class MyWorld(CoreWorld):
|
||||
"""获取碰撞统计"""
|
||||
return self.collision_manager.getCollisionStatistics()
|
||||
|
||||
def setupKeyboardEvents(self):
|
||||
"""设置键盘事件"""
|
||||
try:
|
||||
# 绑定 F 键用于聚焦选中节点
|
||||
self.accept("f", self.onFocusKeyPressed)
|
||||
self.accept("F", self.onFocusKeyPressed) # 大写F
|
||||
|
||||
print("✓ 键盘事件绑定完成")
|
||||
|
||||
except Exception as e:
|
||||
print(f"设置键盘事件失败: {e}")
|
||||
|
||||
def onFocusKeyPressed(self):
|
||||
"""处理 F 键按下事件"""
|
||||
try:
|
||||
#print("检测到 F 键按下")
|
||||
|
||||
# 检查是否有选中的节点
|
||||
if hasattr(self, 'selection') and self.selection.selectedNode:
|
||||
#print(f"当前选中节点: {self.selection.selectedNode.getName()}")
|
||||
# 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本)
|
||||
# self.selection.focusCameraOnSelectedNode() # 无动画版本
|
||||
self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本
|
||||
else:
|
||||
print("当前没有选中任何节点")
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理 F 键事件失败: {e}")
|
||||
|
||||
def onPatrolKeyPressed(self):
|
||||
"""处理 P 键按下事件 - 控制巡检系统"""
|
||||
try:
|
||||
print("检测到 P 键按下")
|
||||
|
||||
if not self.patrol_system.is_patrolling:
|
||||
# 如果巡检系统没有点,创建默认巡检路线
|
||||
if not self.patrol_system.patrol_points:
|
||||
self.createDefaultPatrolRoute()
|
||||
|
||||
# 开始巡检
|
||||
if self.patrol_system.start_patrol():
|
||||
print("✓ 巡检已开始")
|
||||
else:
|
||||
print("✗ 巡检启动失败")
|
||||
else:
|
||||
# 停止巡检
|
||||
if self.patrol_system.stop_patrol():
|
||||
print("✓ 巡检已停止")
|
||||
else:
|
||||
print("✗ 巡检停止失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理 P 键事件失败: {e}")
|
||||
|
||||
def createDefaultPatrolRoute(self):
|
||||
"""创建默认巡检路线(使用自动朝向)"""
|
||||
try:
|
||||
# 清空现有巡检点
|
||||
self.patrol_system.clear_patrol_points()
|
||||
|
||||
# 添加巡检点,使用None表示朝向下一个点
|
||||
self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5)
|
||||
self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5)
|
||||
self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5)
|
||||
self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5)
|
||||
|
||||
# 最后一个点可以指定特定的朝向,或者也设为None继续循环
|
||||
self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5)
|
||||
|
||||
print("✓ 默认自动朝向巡检路线已创建")
|
||||
self.patrol_system.list_patrol_points()
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建默认自动朝向巡检路线失败: {e}")
|
||||
|
||||
def _serializeNode(self, node):
|
||||
"""序列化节点数据"""
|
||||
try:
|
||||
return self.world.scene_manager.serializeNode(node)
|
||||
except Exception as e:
|
||||
print(f"序列化节点失败: {e}")
|
||||
return None
|
||||
|
||||
def _deserializeNode(self, node_data, parent_node):
|
||||
"""反序列化节点数据"""
|
||||
try:
|
||||
return self.world.scene_manager.deserializeNode(node_data, parent_node)
|
||||
except Exception as e:
|
||||
print(f"反序列化节点失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== 项目管理功能代理 ====================
|
||||
# 以下函数代理到project_manager模块的对应功能
|
||||
@ -715,7 +819,7 @@ def run(args = None):
|
||||
|
||||
# 使用新的UI模块创建主窗口
|
||||
from ui.main_window import setup_main_window
|
||||
|
||||
print(f'Path is {args}')
|
||||
app, main_window = setup_main_window(world, args)
|
||||
|
||||
# 启动应用程序
|
||||
|
||||
@ -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
@ -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):
|
||||
|
||||
@ -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
1061
templates/main_template.py
Normal file
File diff suppressed because it is too large
Load Diff
322
ui/icon_manager.py
Normal file
322
ui/icon_manager.py
Normal 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
405
ui/icon_manager_gui.py
Normal 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_())
|
||||
@ -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:
|
||||
|
||||
1912
ui/main_window.py
1912
ui/main_window.py
File diff suppressed because it is too large
Load Diff
2268
ui/property_panel.py
2268
ui/property_panel.py
File diff suppressed because it is too large
Load Diff
355
ui/widgets.py
355
ui/widgets.py
@ -18,7 +18,7 @@ from PyQt5.QtCore import Qt, QUrl, QMimeData
|
||||
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
|
||||
from PyQt5.sip import wrapinstance
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import ModelRoot, NodePath
|
||||
from panda3d.core import ModelRoot, NodePath, CollisionNode
|
||||
|
||||
from QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||||
from scene import util
|
||||
@ -30,7 +30,67 @@ class NewProjectDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("新建项目")
|
||||
self.setMinimumWidth(500)
|
||||
|
||||
|
||||
# 设置对话框样式与主窗口保持一致
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #252538;
|
||||
color: #e0e0ff;
|
||||
}
|
||||
QGroupBox {
|
||||
background-color: #2d2d44;
|
||||
border: 1px solid #3a3a4a;
|
||||
border-radius: 6px;
|
||||
margin-top: 1ex; /* 保持这个设置 */
|
||||
color: #e0e0ff;
|
||||
font-weight: 500;
|
||||
padding-top: 10px; /* 增加顶部内边距,为标题留出空间 */
|
||||
}
|
||||
QGroupBox::title {
|
||||
padding: 0 8px;
|
||||
color: #c0c0e0;
|
||||
font-weight: 500;
|
||||
}
|
||||
QLineEdit {
|
||||
background-color: #2d2d44;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
QLineEdit:disabled {
|
||||
background-color: #1e1e2e;
|
||||
color: #8888aa;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6d28d9;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #4c4c6e;
|
||||
color: #8888aa;
|
||||
}
|
||||
QLabel {
|
||||
color: #e0e0ff;
|
||||
}
|
||||
QLabel:disabled {
|
||||
color: #8888aa;
|
||||
}
|
||||
QDialogButtonBox QPushButton {
|
||||
min-width: 80px;
|
||||
}
|
||||
""")
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
@ -156,7 +216,7 @@ class CustomPanda3DWidget(QPanda3DWidget):
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
|
||||
def wheelEvent(self, event):
|
||||
"""处理滚轮事件"""
|
||||
if event.angleDelta().y() > 0:
|
||||
@ -1251,14 +1311,83 @@ class CustomConsoleDockWidget(QWidget):
|
||||
self.autoScrollBtn = QPushButton("自动滚动")
|
||||
self.autoScrollBtn.setCheckable(True)
|
||||
self.autoScrollBtn.setChecked(True)
|
||||
self.autoScrollBtn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2d2d44;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
border: 1px solid #7c3aed;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #3a3a4a;
|
||||
}
|
||||
QPushButton:checked:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6d28d9;
|
||||
}
|
||||
""")
|
||||
toolbar.addWidget(self.autoScrollBtn)
|
||||
|
||||
# 时间戳开关
|
||||
self.timestampBtn = QPushButton("显示时间")
|
||||
self.timestampBtn.setCheckable(True)
|
||||
self.timestampBtn.setChecked(True)
|
||||
self.timestampBtn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2d2d44;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
border: 1px solid #7c3aed;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #3a3a4a;
|
||||
}
|
||||
QPushButton:checked:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6d28d9;
|
||||
}
|
||||
""")
|
||||
toolbar.addWidget(self.timestampBtn)
|
||||
|
||||
self.fpsLabel = QLabel("FPS:0.0")
|
||||
self.fpsLabel.setStyleSheet("""
|
||||
QLabel {
|
||||
background-color: #2d2d44;
|
||||
color: #80ff80;
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
""")
|
||||
self.fpsLabel.setMinimumWidth(100)
|
||||
self.fpsLabel.setAlignment(Qt.AlignCenter)
|
||||
toolbar.addWidget(self.fpsLabel)
|
||||
|
||||
# 帧率更新定时器
|
||||
self.fpsTimer = QTimer()
|
||||
self.fpsTimer.timeout.connect(self.updateFPS)
|
||||
self.fpsTimer.start(1000) # 每秒更新一次
|
||||
|
||||
toolbar.addStretch()
|
||||
layout.addLayout(toolbar)
|
||||
|
||||
@ -1300,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget):
|
||||
# 添加欢迎信息
|
||||
self.addMessage("🎮 编辑器控制台已启动", "INFO")
|
||||
|
||||
def updateFPS(self):
|
||||
try:
|
||||
if hasattr(self.world,'clock'):
|
||||
fps = self.world.clock.getAverageFrameRate()
|
||||
self.fpsLabel.setText(f"FPS:{fps:.1f}")
|
||||
|
||||
# 根据帧率设置颜色
|
||||
if fps >= 50:
|
||||
color = "#80ff80" # 绿色 - 优秀
|
||||
elif fps >= 30:
|
||||
color = "#ffff80" # 黄色 - 一般
|
||||
else:
|
||||
color = "#ff8080" # 红色 - 较差
|
||||
|
||||
self.fpsLabel.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
background-color: #2d2d44;
|
||||
color: {color};
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}}
|
||||
""")
|
||||
except Exception as e:
|
||||
pass # 静默处理错误,避免影响控制台功能
|
||||
|
||||
def setupConsoleRedirect(self):
|
||||
"""设置控制台重定向"""
|
||||
import sys
|
||||
@ -1414,39 +1571,57 @@ class CustomTreeWidget(QTreeWidget):
|
||||
self.initData()
|
||||
self.setupUI() # 初始化界面
|
||||
self.setupContextMenu() # 初始化右键菜单
|
||||
|
||||
self.setupDragDrop() # 设置拖拽功能
|
||||
|
||||
self.original_scales={}
|
||||
|
||||
self.setStyleSheet("""
|
||||
/* 设置折叠状态下,带子节点的箭头颜色 */
|
||||
QTreeWidget::branch:has-children:!open {
|
||||
color: #8b5cf6; /* 紫色 */
|
||||
}
|
||||
|
||||
/* 设置展开状态下,带子节点的箭头颜色 */
|
||||
QTreeWidget::branch:has-children:open {
|
||||
color: #9ca3af; /* 灰色,提供状态变化反馈 */
|
||||
}
|
||||
|
||||
/* 鼠标悬停在任意箭头上时,颜色变亮 */
|
||||
QTreeWidget::branch:hover {
|
||||
color: #a78bfa; /* 亮紫色 */
|
||||
}
|
||||
""")
|
||||
|
||||
def initData(self):
|
||||
"""初始化变量"""
|
||||
# 定义2D GUI元素类型
|
||||
self.gui_2d_types = {
|
||||
"GUI_BUTTON", # DirectButton
|
||||
"GUI_LABEL", # DirectLabel
|
||||
"GUI_ENTRY", # DirectEntry
|
||||
"GUI_IMAGE",
|
||||
"GUI_BUTTON", # GUI 按钮
|
||||
"GUI_LABEL", # GUI 标签
|
||||
"GUI_ENTRY", # GUI 输入框
|
||||
"GUI_IMAGE", # GUI 图片
|
||||
"GUI_2D_VIDEO_SCREEN", # GUI 2D视频
|
||||
"GUI_SPHERICAL_VIDEO", # GUI 3D球形视频
|
||||
"GUI_NODE" # 其他2D GUI容器
|
||||
}
|
||||
|
||||
# 定义3D GUI元素类型
|
||||
self.gui_3d_types = {
|
||||
"GUI_3DTEXT", # 3D TextNode
|
||||
"GUI_3DIMAGE",
|
||||
"GUI_VIRTUAL_SCREEN" # Virtual Screen
|
||||
"GUI_3DTEXT", # 3D 文本节点
|
||||
"GUI_3DIMAGE", # 3D 图片节点
|
||||
"GUI_VIRTUAL_SCREEN", # 3D视频
|
||||
"GUI_VirtualScreen" # 3D虚拟视频
|
||||
}
|
||||
|
||||
# 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素)
|
||||
self.scene_3d_types = {
|
||||
"SCENE_ROOT",
|
||||
"SCENE_NODE",
|
||||
"LIGHT_NODE",
|
||||
"LIGHT_NODE", # 灯节点
|
||||
"CAMERA_NODE",
|
||||
"IMPORTED_MODEL_NODE",
|
||||
"IMPORTED_MODEL_NODE", # 导入模型节点
|
||||
"MODEL_NODE",
|
||||
"TERRAIN_NODE",
|
||||
"CESIUM_TILESET_NODE"
|
||||
"TERRAIN_NODE", # 地形节点
|
||||
"CESIUM_TILESET_NODE" # 3D Tileset
|
||||
}
|
||||
|
||||
# 这是一个最佳实践,它让代码的意图变得非常清晰。
|
||||
@ -1747,7 +1922,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
elif is_dragged_3d_gui:
|
||||
if is_target_3d_scene:
|
||||
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
|
||||
return True
|
||||
return False
|
||||
elif is_target_2d_gui:
|
||||
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
|
||||
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
|
||||
@ -1773,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
elif is_target_3d_gui:
|
||||
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下")
|
||||
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
||||
return False
|
||||
@ -1831,7 +2006,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport:
|
||||
indicator_str = "OnViewport"
|
||||
|
||||
print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
|
||||
#print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
|
||||
|
||||
if event.source() != self:
|
||||
event.ignore()
|
||||
@ -1963,7 +2138,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
return
|
||||
|
||||
# 默认选中场景根节点,通常是第一个顶级节点
|
||||
next_item_to_select = self.topLevelItem(0)
|
||||
#next_item_to_select = self.topLevelItem(0)
|
||||
|
||||
# 3. 执行删除循环
|
||||
deleted_count = 0
|
||||
@ -1990,7 +2165,6 @@ class CustomTreeWidget(QTreeWidget):
|
||||
if hasattr(panda_node, 'getPythonTag'):
|
||||
light_object = panda_node.getPythonTag('rp_light_object')
|
||||
if light_object and hasattr(self.world, 'render_pipeline'):
|
||||
print(f'11111111111111111111111111,{light_object.casts_shadows}')
|
||||
self.world.render_pipeline.remove_light(light_object)
|
||||
|
||||
# 从world列表中移除
|
||||
@ -2048,25 +2222,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
# 4. 删除操作完成后,更新UI ---
|
||||
if deleted_count > 0:
|
||||
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
|
||||
|
||||
# 检查预备选择的节点是否还有效 (例如,父节点可能也一同被删了)
|
||||
# 如果next_item_to_select在树中找不到了,就退回到选择根节点
|
||||
if next_item_to_select and self.indexFromItem(next_item_to_select).isValid():
|
||||
new_selection_item = next_item_to_select
|
||||
else:
|
||||
# 如果之前的父节点也一并被删除了,就默认选择场景根节点
|
||||
new_selection_item = self.topLevelItem(0)
|
||||
|
||||
if new_selection_item:
|
||||
# 设置UI树的选择
|
||||
self.setCurrentItem(new_selection_item)
|
||||
# 获取对应的Panda3D节点
|
||||
new_panda_node = new_selection_item.data(0, Qt.UserRole)
|
||||
# 调用您提供的函数来更新选择状态和属性面板
|
||||
self.update_selection_and_properties(new_panda_node, new_selection_item)
|
||||
else:
|
||||
# 如果连根节点都没有了(例如清空场景),则清空选择
|
||||
self.update_selection_and_properties(None, None)
|
||||
self.update_selection_and_properties(None, None)
|
||||
|
||||
def delete_item(self, panda_node):
|
||||
"""删除指定节点 panda3D(node)- 优化和修复版本"""
|
||||
@ -2074,6 +2230,14 @@ class CustomTreeWidget(QTreeWidget):
|
||||
print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。")
|
||||
return
|
||||
|
||||
# #如果有命令管理系统,则使用命令系统
|
||||
# if hasattr(self.world,'command_manager') and self.world.command_manager:
|
||||
# from core.Command_System import DeleteNodeCommand
|
||||
# parent_node = panda_node.getParent()
|
||||
# command = DeleteNodeCommand(panda_node,parent_node)
|
||||
# self.world.command_manager.execute_command(command)
|
||||
# return
|
||||
|
||||
# --- 关键修复:在操作前,安全地获取节点名字 ---
|
||||
node_name_for_logging = panda_node.getName()
|
||||
|
||||
@ -2088,7 +2252,6 @@ class CustomTreeWidget(QTreeWidget):
|
||||
if not item:
|
||||
print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。")
|
||||
return
|
||||
|
||||
try:
|
||||
# 2. 过滤受保护节点
|
||||
node_type = item.data(0, Qt.UserRole + 1)
|
||||
@ -2126,6 +2289,24 @@ class CustomTreeWidget(QTreeWidget):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def clear_tree(self):
|
||||
"""清空UI树"""
|
||||
print("Clear")
|
||||
self.clear()
|
||||
# 创建场景根节点
|
||||
sceneRoot = QTreeWidgetItem(self, ['场景'])
|
||||
sceneRoot.setData(0, Qt.UserRole, self.world.render)
|
||||
sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
|
||||
# 添加相机节点
|
||||
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
|
||||
cameraItem.setData(0, Qt.UserRole, self.world.cam)
|
||||
cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
|
||||
# 添加地板节点
|
||||
if hasattr(self.world, 'ground') and self.world.ground:
|
||||
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
|
||||
groundItem.setData(0, Qt.UserRole, self.world.ground)
|
||||
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
|
||||
|
||||
def _cleanup_panda_node_resources(self, panda_node):
|
||||
"""一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。"""
|
||||
if not panda_node or panda_node.is_empty():
|
||||
@ -2192,53 +2373,64 @@ class CustomTreeWidget(QTreeWidget):
|
||||
return top_item
|
||||
return None
|
||||
|
||||
def create_model_items(self, model):
|
||||
def create_model_items(self, model: NodePath):
|
||||
"""
|
||||
【此函数保持不变】
|
||||
创建模型项。
|
||||
只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根,
|
||||
然后完整地展示这些分支。
|
||||
"""
|
||||
if not model:
|
||||
print("传入的参数model为空")
|
||||
return
|
||||
# 创建根节点项
|
||||
# root_item = QTreeWidgetItem(self)
|
||||
# root_item.setText(0, model.getName() or "Unnamed Node")
|
||||
# root_item.setText(1, model.node().getTypeName())
|
||||
# root_item.setIcon(0, self.item_icons.get('model', self.item_icons['default']))
|
||||
|
||||
# 存储NodePath引用以便后续操作
|
||||
# root_item.setData(0, Qt.UserRole, model)
|
||||
# 找到场景树的根节点,我们将把模型节点添加到这里
|
||||
root_item = self._findSceneRoot()
|
||||
if not root_item:
|
||||
print("错误:未能找到场景根节点项")
|
||||
return
|
||||
|
||||
# 递归添加子节点
|
||||
self._add_children_recursive(root_item, model)
|
||||
# 1. 在模型的第一层子节点中进行筛选
|
||||
for child_node in model.getChildren():
|
||||
if child_node.hasTag("is_scene_element"):
|
||||
print(f"找到带标签的根节点:{child_node.getName()}")
|
||||
if (child_node.hasTag("gui_type")and
|
||||
child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]):
|
||||
print(f"跳过3dGUI节点{child_node.getName()}")
|
||||
continue
|
||||
|
||||
return root_item
|
||||
# 为这个带标签的节点创建一个树项
|
||||
child_item = QTreeWidgetItem(root_item)
|
||||
child_item.setText(0, child_node.getName() or "Unnamed Tagged Node")
|
||||
child_item.setData(0, Qt.UserRole, child_node)
|
||||
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
|
||||
# self._add_node_info(child_item, child_node) # 可选信息
|
||||
|
||||
def _add_children_recursive(self, parent_item, node_path: NodePath):
|
||||
"""递归添加子节点到树项"""
|
||||
print(f'开始递归添加子节点')
|
||||
# 获取所有子节点
|
||||
children = node_path.getChildren()
|
||||
# 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体)
|
||||
self._add_all_children_unconditionally(child_item, child_node)
|
||||
|
||||
for i in range(children.getNumPaths()):
|
||||
child_node: NodePath = children.getPath(i)
|
||||
def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath):
|
||||
"""
|
||||
【此函数已更新】
|
||||
无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。
|
||||
"""
|
||||
for child_node in node_path.getChildren():
|
||||
|
||||
# 过滤条件
|
||||
if not child_node.hasTag("is_scene_element"):
|
||||
print(f"不存在------------------------{child_node.hasTag('is_scene_element')}")
|
||||
continue
|
||||
# 新增:检查节点是否为碰撞节点
|
||||
if isinstance(child_node.node(), CollisionNode):
|
||||
# print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试
|
||||
continue # 如果是,则跳过此节点及其所有子节点
|
||||
|
||||
print(f"存在------------------------{child_node.getName()}")
|
||||
# 创建子项
|
||||
child_item = QTreeWidgetItem(parent_item)
|
||||
child_item.setText(0, child_node.getName() or f"Child_{i}")
|
||||
|
||||
# 存储NodePath引用
|
||||
child_item.setText(0, child_node.getName() or "Unnamed Child")
|
||||
child_item.setData(0, Qt.UserRole, child_node)
|
||||
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
|
||||
# self._add_node_info(child_item, child_node) # 可选信息
|
||||
|
||||
# 添加额外信息(可选)
|
||||
# self._add_node_info(child_item, child_node)
|
||||
|
||||
# 递归处理子节点的子节点
|
||||
if child_node.getNumChildren() > 0:
|
||||
self._add_children_recursive(child_item, child_node)
|
||||
# 继续无条件地递归
|
||||
if not child_node.is_empty():
|
||||
self._add_all_children_unconditionally(child_item, child_node)
|
||||
|
||||
# ==================== 辅助方法 ====================
|
||||
def _findSceneRoot(self):
|
||||
@ -2269,6 +2461,17 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
def add_node_to_tree_widget(self, node, parent_item, node_type):
|
||||
"""将node元素添加到树形控件"""
|
||||
if hasattr(node, 'getTag'):
|
||||
if node.hasTag('tree_item_type'):
|
||||
print(f"node0: {node.getName()},{node.getTag('tree_item_type')}")
|
||||
tree_type = node.getTag('tree_item_type')
|
||||
else:
|
||||
node.setTag('tree_item_type', node_type)
|
||||
else:
|
||||
print(f"node2: {node.getName()},{node_type}")
|
||||
tree_type = node_type
|
||||
|
||||
|
||||
# BLACK_LIST 和依赖项导入保持不变
|
||||
BLACK_LIST = {'', '**', 'temp', 'collision'}
|
||||
from panda3d.core import CollisionNode, ModelRoot
|
||||
@ -2283,7 +2486,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
|
||||
nodeItem.setData(0, Qt.UserRole, node)
|
||||
nodeItem.setData(0, Qt.UserRole + 1, node_type)
|
||||
nodeItem.setData(0, Qt.UserRole + 1, tree_type)
|
||||
|
||||
for child in node.getChildren():
|
||||
# 递归调用,但我们只关心顶级的nodeItem
|
||||
@ -2301,7 +2504,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
node_name = ""
|
||||
|
||||
try:
|
||||
if node_type == "IMPORTED_MODEL_NODE":
|
||||
if tree_type == "IMPORTED_MODEL_NODE":
|
||||
# getTag('file') 可能是你自己设置的tag,这里假设它存在
|
||||
node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName()
|
||||
|
||||
@ -2312,7 +2515,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
node_name = node.getName() if hasattr(node, 'getName') else "node"
|
||||
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
|
||||
new_qt_item.setData(0, Qt.UserRole, node)
|
||||
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
|
||||
new_qt_item.setData(0, Qt.UserRole + 1, tree_type)
|
||||
|
||||
# 确保 new_qt_item 成功创建后再继续操作
|
||||
if new_qt_item:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user