forked from Rowland/EG
Compare commits
9 Commits
a76277b2fc
...
d32993b903
| Author | SHA1 | Date | |
|---|---|---|---|
| d32993b903 | |||
| 75a9df709f | |||
| a66c097048 | |||
| 7c797d74d5 | |||
| ddeb40ea54 | |||
| 040fffd34e | |||
| ec21df8f54 | |||
| b5545b4b60 | |||
| fc2550ce03 |
@ -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):
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -52,11 +52,7 @@ 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.
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()
|
||||
|
||||
|
||||
|
||||
@ -145,7 +145,7 @@ class InfoPanelManager(DirectObject):
|
||||
text_scale=0.045,
|
||||
text_fg=content_color,
|
||||
text_align=TextNode.ALeft,
|
||||
text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行
|
||||
text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行
|
||||
pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05),
|
||||
parent=panel_node,
|
||||
relief=None,
|
||||
@ -529,9 +529,7 @@ class InfoPanelManager(DirectObject):
|
||||
-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05
|
||||
)
|
||||
|
||||
# 设置一个非常大的换行值,几乎不自动换行
|
||||
panel_data['content_label']['text_wordwrap'] = 500
|
||||
print(f"更新面板换行: 设置为500(几乎不换行)")
|
||||
panel_data['content_label']['text_wordwrap'] = 0
|
||||
|
||||
# 如果有背景图片,也需要更新其大小
|
||||
if 'bg_image' in panel_data and panel_data['bg_image']:
|
||||
@ -575,8 +573,9 @@ class InfoPanelManager(DirectObject):
|
||||
if 'content_size' in properties:
|
||||
panel_data['content_label']['text_scale'] = properties['content_size']
|
||||
props['content_size'] = properties['content_size']
|
||||
current_size = props.get('size',(1.0,0.6))
|
||||
# 当字体大小改变时,仍然保持较大的换行值
|
||||
panel_data['content_label']['text_wordwrap'] = 500
|
||||
panel_data['content_label']['text_wordwrap'] = 0
|
||||
|
||||
# 更新背景图片
|
||||
if 'bg_image' in properties:
|
||||
|
||||
@ -640,7 +640,12 @@ class AssemblyInteractionManager(DirectObject):
|
||||
if self.step_dialog:
|
||||
self.step_dialog.close()
|
||||
|
||||
self.step_dialog = StepGuideDialog(self)
|
||||
# 获取主窗口作为父窗口
|
||||
parent_window = None
|
||||
if hasattr(self.world, 'main_window') and self.world.main_window:
|
||||
parent_window = self.world.main_window
|
||||
|
||||
self.step_dialog = StepGuideDialog(self, parent_window)
|
||||
self.step_dialog.show()
|
||||
|
||||
def start_current_step(self):
|
||||
@ -1573,8 +1578,8 @@ class AssemblyInteractionManager(DirectObject):
|
||||
class StepGuideDialog(QDialog):
|
||||
"""步骤指引对话框(UI代码保持不变)"""
|
||||
|
||||
def __init__(self, interaction_manager):
|
||||
super().__init__()
|
||||
def __init__(self, interaction_manager, parent=None):
|
||||
super().__init__(parent)
|
||||
self.interaction_manager = interaction_manager
|
||||
self.current_required_tool = "无" # 当前步骤要求的工具
|
||||
self.mode = interaction_manager.mode # 获取模式
|
||||
|
||||
@ -188,15 +188,34 @@ class EventHandler:
|
||||
if self.world.selection.gizmo:
|
||||
#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}")
|
||||
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
|
||||
# 开始坐标轴拖拽
|
||||
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
|
||||
pickerNP.removeNode()
|
||||
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
|
||||
|
||||
@ -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:
|
||||
@ -696,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, [])
|
||||
|
||||
@ -16,7 +16,6 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
import math
|
||||
|
||||
|
||||
class SelectionSystem:
|
||||
"""选择和变换系统类"""
|
||||
|
||||
@ -64,6 +63,13 @@ class SelectionSystem:
|
||||
"z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮
|
||||
}
|
||||
|
||||
#性能优化相关
|
||||
self._optimized_node = False
|
||||
self._last_update_time = 0
|
||||
self._cached_bounds = {}
|
||||
self._gizmo_update_interval = 0.1
|
||||
self._selection_box_update_interval = 0.2
|
||||
|
||||
self._current_cursor = None
|
||||
self._default_cursor = None
|
||||
|
||||
@ -118,11 +124,8 @@ class SelectionSystem:
|
||||
def createSelectionBox(self, nodePath):
|
||||
"""为选中的节点创建选择框"""
|
||||
try:
|
||||
#print(f" 开始创建选择框,目标节点: {nodePath.getName()}")
|
||||
|
||||
# 如果已有选择框,先移除
|
||||
if self.selectionBox:
|
||||
print(" 移除现有选择框")
|
||||
#print(" 移除现有选择框")
|
||||
self.selectionBox.removeNode()
|
||||
self.selectionBox = None
|
||||
|
||||
@ -130,21 +133,12 @@ class SelectionSystem:
|
||||
print(" 目标节点为空,取消创建")
|
||||
return
|
||||
|
||||
# 创建选择框作为render的子节点,但会实时跟踪目标节点
|
||||
self.selectionBox = self.world.render.attachNewNode("selectionBox")
|
||||
self.selectionBoxTarget = nodePath # 保存目标节点引用
|
||||
#print(f" 选择框节点创建完成: {self.selectionBox}")
|
||||
self.selectionBoxTarget = nodePath
|
||||
|
||||
# 启动选择框更新任务
|
||||
taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
|
||||
#print(" 选择框更新任务已启动")
|
||||
|
||||
# 初始更新选择框
|
||||
#print(" 开始初始化选择框几何体...")
|
||||
self.updateSelectionBoxGeometry()
|
||||
|
||||
#print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ 创建选择框失败: {str(e)}")
|
||||
import traceback
|
||||
@ -277,51 +271,58 @@ class SelectionSystem:
|
||||
traceback.print_exc()
|
||||
|
||||
def updateSelectionBoxTask(self, task):
|
||||
"""选择框更新任务"""
|
||||
"""选择框更新任务 - 平衡性能和实时性"""
|
||||
try:
|
||||
if not hasattr(self,'_last_selection_box_update'):
|
||||
update_interval = 0.1
|
||||
|
||||
if not hasattr(self, '_last_selection_box_update'):
|
||||
self._last_selection_box_update = 0
|
||||
|
||||
import time
|
||||
current_time = time.time()
|
||||
if current_time - self._last_selection_box_update < 0.1:
|
||||
if current_time - self._last_selection_box_update < update_interval:
|
||||
return task.cont
|
||||
self._last_selection_box_update = current_time
|
||||
|
||||
#检查目标节点是否已被删除
|
||||
# 检查目标节点是否已被删除
|
||||
self.checkAndClearIfTargetDeleted()
|
||||
|
||||
if not self.selectionBox or not self.selectionBoxTarget:
|
||||
return task.done # 结束任务
|
||||
return task.done
|
||||
|
||||
# 检查目标节点是否还存在
|
||||
if self.selectionBoxTarget.isEmpty():
|
||||
self.clearSelectionBox()
|
||||
return task.done
|
||||
|
||||
# 获取目标节点在世界坐标系中的当前边界框(使用正确的API)
|
||||
currentMinPoint = Point3()
|
||||
currentMaxPoint = Point3()
|
||||
if not self.selectionBoxTarget.calcTightBounds(currentMinPoint, currentMaxPoint, self.world.render):
|
||||
return task.cont
|
||||
# 检查目标节点是否发生了变化(位置、旋转、缩放)
|
||||
current_transform = self._getNodeTransformKey(self.selectionBoxTarget)
|
||||
|
||||
# 检查边界框是否发生变化(位置或大小)
|
||||
if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or
|
||||
self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint):
|
||||
|
||||
# 更新选择框几何体
|
||||
if (not hasattr(self, '_last_transform_key') or
|
||||
self._last_transform_key != current_transform):
|
||||
# 节点发生了变化,更新选择框
|
||||
self.updateSelectionBoxGeometry()
|
||||
self._last_transform_key = current_transform
|
||||
|
||||
# 保存当前边界框信息
|
||||
self._lastMinPoint = currentMinPoint
|
||||
self._lastMaxPoint = currentMaxPoint
|
||||
|
||||
return task.cont # 继续任务
|
||||
return task.cont
|
||||
|
||||
except Exception as e:
|
||||
print(f"选择框更新任务出错: {str(e)}")
|
||||
return task.done
|
||||
|
||||
def _getNodeTransformKey(self, node):
|
||||
"""获取节点变换的关键信息,用于快速比较"""
|
||||
try:
|
||||
# 获取节点的关键变换信息
|
||||
pos = node.getPos(self.world.render)
|
||||
hpr = node.getHpr(self.world.render)
|
||||
scale = node.getScale(self.world.render)
|
||||
|
||||
# 返回一个可以比较的元组
|
||||
return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z)
|
||||
except:
|
||||
return None
|
||||
|
||||
def clearSelectionBox(self):
|
||||
"""清除选择框"""
|
||||
if self.selectionBox:
|
||||
@ -905,13 +906,6 @@ class SelectionSystem:
|
||||
# 创建或获取材质
|
||||
mat = Material()
|
||||
|
||||
# # 设置材质属性 - 使用自发光确保在RenderPipeline下可见
|
||||
# mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
|
||||
# mat.setDiffuse(Vec4(0, 0, 0, 1))
|
||||
# #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光
|
||||
# mat.setEmission(Vec4(1,1,1,1.0)) # 自发光
|
||||
# mat.set_roughness(1)
|
||||
|
||||
# 设置材质属性 - 使用更自然的颜色,避免过亮的自发光
|
||||
adjusted_color = Vec4(
|
||||
min(color[0]*20, 1.0),
|
||||
@ -921,11 +915,7 @@ class SelectionSystem:
|
||||
)
|
||||
|
||||
mat.setBaseColor(adjusted_color)
|
||||
# mat.setDiffuse(adjusted_color * 0.8) # 稍微降低漫反射亮度
|
||||
# mat.setAmbient(adjusted_color * 0.3) # 设置环境光反射
|
||||
# mat.setSpecular(Vec4(0.3, 0.3, 0.3, 1.0)) # 适度的镜面反射
|
||||
# mat.setShininess(25.0) # 适中的高光强度
|
||||
mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
|
||||
#mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
|
||||
|
||||
# 应用材质
|
||||
handle_node.setMaterial(mat, 1)
|
||||
@ -1514,8 +1504,14 @@ class SelectionSystem:
|
||||
|
||||
self.dragStartMousePos = (mouseX, mouseY)
|
||||
|
||||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||||
if light_object:
|
||||
self.gizmoTargetStartPos = Point3(light_object.pos)
|
||||
else:
|
||||
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
|
||||
|
||||
# 保存开始拖拽时目标节点的位置和坐标轴的位置
|
||||
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
|
||||
#self.gizmoTargetStartPos = self.gizmoTarget.getPos()
|
||||
self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置
|
||||
|
||||
# 添加对缩放的支持:保存初始缩放值
|
||||
@ -1886,6 +1882,43 @@ class SelectionSystem:
|
||||
"""停止坐标轴拖拽"""
|
||||
print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}")
|
||||
|
||||
if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget:
|
||||
current_pos = self.gizmoTarget.getPos()
|
||||
|
||||
if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and
|
||||
(abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or
|
||||
abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or
|
||||
abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)):
|
||||
from core.Command_System import MoveNodeCommand
|
||||
from core.Command_System import MoveLightCommand
|
||||
|
||||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||||
if light_object:
|
||||
command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object)
|
||||
else:
|
||||
command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos)
|
||||
self.world.command_manager.execute_command(command)
|
||||
# 如果是缩放操作且缩放发生了变化,则创建缩放命令
|
||||
elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and
|
||||
self.gizmoTargetStartScale):
|
||||
current_scale = self.gizmoTarget.getScale()
|
||||
if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or
|
||||
abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or
|
||||
abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001):
|
||||
from core.Command_System import ScaleNodeCommand
|
||||
command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale)
|
||||
self.world.command_manager.execute_command(command)
|
||||
# 如果是旋转操作且旋转发生了变化,则创建旋转命令
|
||||
elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and
|
||||
self.gizmoTargetStartHpr):
|
||||
current_hpr = self.gizmoTarget.getHpr()
|
||||
if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or
|
||||
abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or
|
||||
abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001):
|
||||
from core.Command_System import RotateNodeCommand
|
||||
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
|
||||
self.world.command_manager.execute_command(command)
|
||||
|
||||
# 恢复所有轴的颜色
|
||||
for axis_name in ["x", "y", "z"]:
|
||||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||||
@ -1909,31 +1942,20 @@ class SelectionSystem:
|
||||
|
||||
def updateSelection(self, nodePath):
|
||||
try:
|
||||
# 检查是否要选择的对象已经是当前选中的对象
|
||||
if self.selectedNode == nodePath:
|
||||
#print("要选择的对象已经是当前选中的对象,跳过重复更新")
|
||||
return
|
||||
print(f"\n=== 更新选择状态 ===")
|
||||
#print(f"\n=== 更新选择状态 ===")
|
||||
|
||||
# 如果正在删除节点,避免更新选择
|
||||
if hasattr(self, '_deleting_node') and self._deleting_node:
|
||||
print("正在删除节点,跳过选择更新")
|
||||
print("=== 选择状态更新完成 ===\n")
|
||||
#print("=== 选择状态更新完成 ===\n")
|
||||
return
|
||||
|
||||
node_name = "None"
|
||||
if nodePath and not nodePath.isEmpty():
|
||||
node_name = nodePath.getName()
|
||||
print(f"新选择的节点: {node_name}")
|
||||
|
||||
# 检查是否为双击
|
||||
is_double_click = self.checkDoubleClick(nodePath)
|
||||
if is_double_click:
|
||||
print(f"检测到双击 {node_name},执行聚焦")
|
||||
# 双击时直接执行聚焦,不执行选择逻辑
|
||||
self.focusCameraOnSelectedNodeAdvanced()
|
||||
print("=== 选择状态更新完成 ===\n")
|
||||
return # 直接返回,不执行下面的选择逻辑
|
||||
#print(f"新选择的节点: {node_name}")
|
||||
|
||||
self.selectedNode = nodePath
|
||||
# 添加兼容性属性
|
||||
@ -1965,7 +1987,6 @@ class SelectionSystem:
|
||||
else:
|
||||
print("× 坐标轴创建失败")
|
||||
|
||||
print(f"✓ 选中了节点: {node_name}")
|
||||
else:
|
||||
print("清除选择...")
|
||||
self.clearSelectionBox()
|
||||
@ -1979,12 +2000,69 @@ class SelectionSystem:
|
||||
self.world.interface_manager.treeWidget.setCurrentItem(None)
|
||||
print("✓ 树形控件选中状态已清空")
|
||||
|
||||
print("=== 选择状态更新完成 ===\n")
|
||||
#print("=== 选择状态更新完成 ===\n")
|
||||
except Exception as e:
|
||||
print(f"更新选择状态失败{str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _reparentTreeItem(self, item, new_parent_item):
|
||||
"""将树项重新父化到新的父项下"""
|
||||
if not item or not new_parent_item:
|
||||
return
|
||||
|
||||
# 从当前父项中移除
|
||||
current_parent = item.parent()
|
||||
if current_parent:
|
||||
current_parent.removeChild(item)
|
||||
else:
|
||||
# 如果是顶级项
|
||||
index = self.indexOfTopLevelItem(item)
|
||||
if index >= 0:
|
||||
self.takeTopLevelItem(index)
|
||||
|
||||
# 添加到新父项
|
||||
new_parent_item.addChild(item)
|
||||
|
||||
def _updateSelectionVisuals(self, nodePath):
|
||||
"""更新选择的视觉效果(选择框和坐标轴)"""
|
||||
try:
|
||||
if nodePath and not nodePath.isEmpty():
|
||||
node_name = nodePath.getName()
|
||||
print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
|
||||
|
||||
# 创建选择框
|
||||
print("创建选择框...")
|
||||
self.createSelectionBox(nodePath)
|
||||
if self.selectionBox:
|
||||
box_name = "Unknown"
|
||||
if self.selectionBox and not self.selectionBox.isEmpty():
|
||||
box_name = self.selectionBox.getName()
|
||||
print(f"✓ 选择框创建成功: {box_name}")
|
||||
else:
|
||||
print("× 选择框创建失败")
|
||||
|
||||
# 创建坐标轴
|
||||
print("创建坐标轴...")
|
||||
self.createGizmo(nodePath)
|
||||
if self.gizmo:
|
||||
gizmo_name = "Unknown"
|
||||
if self.gizmo and not self.gizmo.isEmpty():
|
||||
gizmo_name = self.gizmo.getName()
|
||||
print(f"✓ 坐标轴创建成功: {gizmo_name}")
|
||||
else:
|
||||
print("× 坐标轴创建失败")
|
||||
|
||||
print(f"✓ 选中了节点: {node_name}")
|
||||
else:
|
||||
print("清除选择...")
|
||||
self.clearSelectionBox()
|
||||
self.clearGizmo()
|
||||
print("✓ 取消选择")
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新选择视觉效果失败: {e}")
|
||||
|
||||
def getSelectedNode(self):
|
||||
"""获取当前选中的节点"""
|
||||
return self.selectedNode
|
||||
@ -2173,8 +2251,31 @@ class SelectionSystem:
|
||||
maxPoint = Point3()
|
||||
|
||||
if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
print("无法计算选中节点的边界框")
|
||||
return False
|
||||
print("无法计算选中节点的边界框,使用节点为位置作为替代方案")
|
||||
node_pos = self.selectedNode.getPos(self.world.render)
|
||||
optimal_distance = 10.0
|
||||
current_cam_pos = self.world.cam.getPos()
|
||||
view_direction = node_pos - current_cam_pos
|
||||
if view_direction.length()<0.001:
|
||||
view_direction = Vec3(5,-5,2)
|
||||
view_direction.normalize()
|
||||
target_cam_pos = node_pos - (view_direction * optimal_distance)
|
||||
|
||||
temp_node =self.world.render.attachNewNode("temp_lookat_target")
|
||||
temp_node.setPos(node_pos)
|
||||
dummy_cam = self.world.render.attachNewNode("dummy_camera")
|
||||
dummy_cam.setPos(target_cam_pos)
|
||||
dummy_cam.lookAt(temp_node)
|
||||
target_cam_hpr = Vec3(dummy_cam.getHpr())
|
||||
|
||||
temp_node.removeNode()
|
||||
dummy_cam.removeNode()
|
||||
|
||||
currrent_cam_pos = Point3(self.world.cam.getPos())
|
||||
current_cam_hpr = Vec3(self.world.cam.getHpr())
|
||||
self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr)
|
||||
print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}")
|
||||
return True
|
||||
|
||||
# 计算节点中心点和大小
|
||||
center = Point3(
|
||||
@ -2202,7 +2303,7 @@ class SelectionSystem:
|
||||
view_direction.normalize()
|
||||
|
||||
# 计算合适的观察距离
|
||||
optimal_distance = max(size * 2.0, 5.0)
|
||||
optimal_distance = max(size * 1, 1.0)
|
||||
|
||||
# 计算目标摄像机位置
|
||||
target_cam_pos = center + (view_direction * optimal_distance)
|
||||
@ -2341,100 +2442,6 @@ class SelectionSystem:
|
||||
traceback.print_exc()
|
||||
return task.done
|
||||
|
||||
def focusCameraOnSelectedNode(self):
|
||||
"""将摄像机聚焦到选中的节点(无动画版本,但仍保持平滑转向)"""
|
||||
try:
|
||||
if not self.selectedNode or self.selectedNode.isEmpty():
|
||||
print("没有选中的节点,无法聚焦")
|
||||
return False
|
||||
|
||||
# 获取选中节点的边界框
|
||||
minPoint = Point3()
|
||||
maxPoint = Point3()
|
||||
|
||||
if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
print("无法计算选中节点的边界框")
|
||||
return False
|
||||
|
||||
# 计算节点中心点
|
||||
center = Point3(
|
||||
(minPoint.x + maxPoint.x) * 0.5,
|
||||
(minPoint.y + maxPoint.y) * 0.5,
|
||||
(minPoint.z + maxPoint.z) * 0.5
|
||||
)
|
||||
|
||||
# 计算节点的大小(直径)
|
||||
size = (maxPoint - minPoint).length()
|
||||
|
||||
# 如果节点太小,使用默认大小
|
||||
if size < 0.1:
|
||||
size = 5.0
|
||||
|
||||
# 获取当前摄像机位置
|
||||
current_cam_pos = Point3(self.world.cam.getPos())
|
||||
|
||||
# 计算观察方向
|
||||
view_direction = current_cam_pos - center
|
||||
if view_direction.length() < 0.001:
|
||||
# 如果摄像机正好在中心点,使用默认方向
|
||||
view_direction = Vec3(5, -5, 2) # 默认观察方向
|
||||
|
||||
# 标准化方向向量
|
||||
view_direction.normalize()
|
||||
|
||||
# 计算合适的距离(基于节点大小)
|
||||
optimal_distance = max(size * 3.0, 10.0) # 距离节点的距离是节点大小的3倍或至少10个单位
|
||||
|
||||
# 计算新的摄像机位置
|
||||
new_cam_pos = center + (view_direction * optimal_distance)
|
||||
|
||||
# 平滑地设置摄像机位置和朝向
|
||||
# 创建临时节点用于计算目标朝向
|
||||
temp_lookat = self.world.render.attachNewNode("temp_lookat")
|
||||
temp_lookat.setPos(center)
|
||||
|
||||
# 获取当前朝向和目标朝向
|
||||
current_hpr = Vec3(self.world.cam.getHpr())
|
||||
|
||||
# 设置摄像机到目标位置
|
||||
self.world.cam.setPos(new_cam_pos)
|
||||
self.world.cam.lookAt(temp_lookat)
|
||||
target_hpr = Vec3(self.world.cam.getHpr())
|
||||
|
||||
# 恢复当前位置
|
||||
self.world.cam.setPos(current_cam_pos)
|
||||
self.world.cam.setHpr(current_hpr)
|
||||
|
||||
# 清理临时节点
|
||||
temp_lookat.removeNode()
|
||||
|
||||
# 使用一个简单的任务来平滑过渡
|
||||
class SmoothCameraMoveData:
|
||||
def __init__(self, start_pos, end_pos, start_hpr, end_hpr):
|
||||
self.start_pos = Point3(start_pos)
|
||||
self.end_pos = Point3(end_pos)
|
||||
self.start_hpr = Vec3(start_hpr)
|
||||
self.end_hpr = Vec3(end_hpr)
|
||||
self.elapsed_time = 0.0
|
||||
self.duration = 0.5 # 0.5秒的平滑过渡
|
||||
|
||||
self._smooth_camera_move_data = SmoothCameraMoveData(
|
||||
current_cam_pos, new_cam_pos, current_hpr, target_hpr
|
||||
)
|
||||
|
||||
taskMgr.remove("smoothCameraMoveTask")
|
||||
taskMgr.add(self._smoothCameraMoveTask, "smoothCameraMoveTask")
|
||||
|
||||
print(f"摄像机开始聚焦到节点: {self.selectedNode.getName()}")
|
||||
print(f"节点中心: {center}, 大小: {size:.2f}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"聚焦摄像机到选中节点失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _smoothCameraMoveTask(self, task):
|
||||
"""平滑摄像机移动任务"""
|
||||
try:
|
||||
@ -2529,6 +2536,7 @@ class SelectionSystem:
|
||||
|
||||
# 检查是否为双击(同一节点且在时间阈值内)
|
||||
is_double_click = (self._last_clicked_node == target_node and
|
||||
target_node is not None and
|
||||
current_time - self._last_click_time < self._double_click_threshold)
|
||||
|
||||
if is_double_click:
|
||||
@ -2539,8 +2547,12 @@ class SelectionSystem:
|
||||
# 无论是点击模型还是坐标轴,都执行聚焦
|
||||
if target_node and not target_node.isEmpty():
|
||||
print(f"双击聚焦到节点: {target_node.getName()}")
|
||||
# 执行聚焦
|
||||
self.focusCameraOnSelectedNodeAdvanced()
|
||||
if self.selectedNode != target_node:
|
||||
self.updateSelection(target_node)
|
||||
else:
|
||||
self.focusCameraOnSelectedNodeAdvanced()
|
||||
else:
|
||||
print("双击事件:没有有效的目标节点")
|
||||
|
||||
# 重置状态以避免三击等误触发
|
||||
self._last_click_time = 0
|
||||
@ -2598,21 +2610,20 @@ class SelectionSystem:
|
||||
import time
|
||||
current_time = time.time()
|
||||
|
||||
# 检查节点和时间
|
||||
time_diff = current_time - self._last_click_time
|
||||
is_same_node = (self._last_clicked_node == nodePath)
|
||||
# 必须是同一节点且在时间阈值内
|
||||
is_double_click = (self._last_clicked_node == nodePath and
|
||||
nodePath is not None and
|
||||
current_time - self._last_click_time < self._double_click_threshold)
|
||||
|
||||
# 如果是同一节点且在时间阈值内,认为是双击
|
||||
if is_same_node and time_diff < self._double_click_threshold:
|
||||
# 只有在双击时才重置状态
|
||||
if is_double_click:
|
||||
# 重置状态
|
||||
self._last_click_time = 0
|
||||
self._last_clicked_node = None
|
||||
return True
|
||||
else:
|
||||
# 只有在非双击情况下才更新状态
|
||||
if not is_same_node:
|
||||
self._last_click_time = current_time
|
||||
self._last_clicked_node = nodePath
|
||||
# 更新状态
|
||||
self._last_click_time = current_time
|
||||
self._last_clicked_node = nodePath
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
@ -2786,4 +2797,23 @@ class SelectionSystem:
|
||||
|
||||
# 清理其他资源
|
||||
self.clearSelectionBox()
|
||||
self.clearGizmo()
|
||||
self.clearGizmo()
|
||||
def clearSelection(self):
|
||||
"""清除当前选择"""
|
||||
try:
|
||||
self.selectedNode = None
|
||||
self.selectedObject = None
|
||||
self.clearSelectionBox()
|
||||
self.clearGizmo()
|
||||
|
||||
# 清除树形控件中的选择
|
||||
if (hasattr(self.world, 'interface_manager') and
|
||||
self.world.interface_manager and
|
||||
hasattr(self.world.interface_manager, 'treeWidget') and
|
||||
self.world.interface_manager.treeWidget):
|
||||
self.world.interface_manager.treeWidget.setCurrentItem(None)
|
||||
|
||||
print("已清除选择")
|
||||
except Exception as e:
|
||||
print(f"清除选择失败: {e}")
|
||||
|
||||
|
||||
@ -323,8 +323,8 @@ class CoreWorld(Panda3DWorld):
|
||||
self.ground.setP(-90)
|
||||
self.ground.setZ(-1.0)
|
||||
self.ground.setColor(0.8, 0.8, 0.8, 1)
|
||||
# self.ground.setTag("is_scene_element", "1")
|
||||
# self.ground.setTag("tree_item_type", "SCENE_NODE")
|
||||
self.ground.setTag("is_scene_element", "1")
|
||||
self.ground.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
# 创建支持贴图的材质
|
||||
mat = Material()
|
||||
@ -343,6 +343,8 @@ class CoreWorld(Panda3DWorld):
|
||||
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())
|
||||
@ -352,6 +354,8 @@ class CoreWorld(Panda3DWorld):
|
||||
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)
|
||||
@ -360,6 +364,8 @@ class CoreWorld(Panda3DWorld):
|
||||
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)
|
||||
@ -368,6 +374,8 @@ class CoreWorld(Panda3DWorld):
|
||||
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)
|
||||
@ -375,6 +383,8 @@ class CoreWorld(Panda3DWorld):
|
||||
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:
|
||||
@ -547,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:
|
||||
|
||||
@ -24,80 +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)}")
|
||||
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
|
||||
# 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元素管理系统类"""
|
||||
@ -326,6 +326,7 @@ class GUIManager:
|
||||
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父节点,建立引用关系
|
||||
@ -416,6 +417,10 @@ class GUIManager:
|
||||
parent_gui_node = None
|
||||
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
||||
|
||||
font = None
|
||||
if hasattr(self.world,'getChineseFont'):
|
||||
font = self.world.getChineseFont()
|
||||
|
||||
entry = DirectEntry(
|
||||
text="",
|
||||
pos=gui_pos,
|
||||
@ -426,7 +431,9 @@ class GUIManager:
|
||||
numLines=1,
|
||||
width=12,
|
||||
focus=0,
|
||||
parent=parent_gui_node # 设置GUI父节点
|
||||
parent=parent_gui_node, # 设置GUI父节点
|
||||
text_font = font,
|
||||
frameSize=(-0.1,0.1,-0.05,0.05)
|
||||
)
|
||||
|
||||
# 设置节点标签
|
||||
@ -438,6 +445,7 @@ class GUIManager:
|
||||
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父节点,建立引用关系
|
||||
@ -572,6 +580,7 @@ class GUIManager:
|
||||
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父节点,建立引用关系
|
||||
@ -771,6 +780,7 @@ class GUIManager:
|
||||
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
|
||||
"""创建3D空间图片"""
|
||||
try:
|
||||
|
||||
from panda3d.core import TextNode
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
@ -861,6 +871,7 @@ class GUIManager:
|
||||
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)
|
||||
@ -979,6 +990,7 @@ class GUIManager:
|
||||
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):
|
||||
@ -1496,9 +1508,12 @@ class GUIManager:
|
||||
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)
|
||||
# 修复后
|
||||
if video_path is not None:
|
||||
video_screen.setTag("video_path", video_path)
|
||||
else:
|
||||
video_screen.setTag("video_path", "")
|
||||
|
||||
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
|
||||
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
|
||||
@ -2173,9 +2188,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}")
|
||||
@ -2254,6 +2277,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:
|
||||
|
||||
27
main.py
27
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
|
||||
@ -23,6 +25,7 @@ from core.vr_manager import VRManager
|
||||
from core.vr_input_handler import VRInputHandler
|
||||
from core.alvr_streamer import ALVRStreamer
|
||||
from core.patrol_system import PatrolSystem
|
||||
from core.Command_System import CommandManager
|
||||
from gui.gui_manager import GUIManager
|
||||
from core.terrain_manager import TerrainManager
|
||||
from scene.scene_manager import SceneManager
|
||||
@ -110,6 +113,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)
|
||||
@ -235,11 +240,11 @@ 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)
|
||||
|
||||
@ -895,6 +900,22 @@ class MyWorld(CoreWorld):
|
||||
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模块的对应功能
|
||||
|
||||
@ -392,13 +392,226 @@ 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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
#创建requirements.txt文件
|
||||
self._createRequirementsFile(build_dir)
|
||||
|
||||
def _saveGUIElementsToJSON(self, build_dir, project_path):
|
||||
"""保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致"""
|
||||
try:
|
||||
# 创建目标gui目录
|
||||
gui_dest = os.path.join(build_dir, "gui")
|
||||
if not os.path.exists(gui_dest):
|
||||
os.makedirs(gui_dest)
|
||||
|
||||
# 收集所有GUI元素信息
|
||||
gui_data = []
|
||||
|
||||
# 获取当前场景中的GUI元素
|
||||
if hasattr(self.world, 'gui_elements'):
|
||||
for gui_node in self.world.gui_elements:
|
||||
if gui_node and not gui_node.isEmpty():
|
||||
# 使用_collectGUIElementInfo方法收集信息
|
||||
gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node)
|
||||
if gui_info:
|
||||
gui_data.append(gui_info)
|
||||
print(f"收集GUI元素信息: {gui_info['name']}")
|
||||
|
||||
# 保存GUI信息到JSON文件
|
||||
gui_file_path = os.path.join(gui_dest, "gui_elements.json")
|
||||
with open(gui_file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(gui_data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 保存GUI元素时出错: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _createRequirementsFile(self,build_dir):
|
||||
requirements_content = """panda3d>=1.10.13"""
|
||||
|
||||
requirements_path = os.path.join(build_dir,"requirements.txt")
|
||||
with open(requirements_path,"w",encoding="utf-8") as f:
|
||||
f.write(requirements_content)
|
||||
|
||||
def copy_folder(self, source_folder, destination_folder):
|
||||
"""将一个文件夹从源路径复制到目标路径下的resources文件夹中
|
||||
|
||||
Args:
|
||||
source_folder (str): 源文件夹路径
|
||||
destination_folder (str): 目标文件夹路径
|
||||
"""
|
||||
try:
|
||||
# 创建resources文件夹作为目标
|
||||
resources_dest = os.path.join(destination_folder, "resources")
|
||||
|
||||
# 确保目标目录存在
|
||||
if not os.path.exists(destination_folder):
|
||||
os.makedirs(destination_folder)
|
||||
|
||||
# 如果目标resources文件夹已存在,先删除
|
||||
if os.path.exists(resources_dest):
|
||||
shutil.rmtree(resources_dest)
|
||||
|
||||
# 检查源文件夹是否存在
|
||||
if os.path.exists(source_folder):
|
||||
# 复制整个文件夹到resources目录下
|
||||
shutil.copytree(
|
||||
source_folder,
|
||||
resources_dest,
|
||||
ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log')
|
||||
)
|
||||
print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ 源文件夹不存在: {source_folder}")
|
||||
# 即使源文件夹不存在,也创建空的resources目录
|
||||
if not os.path.exists(resources_dest):
|
||||
os.makedirs(resources_dest)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 复制文件夹时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def _copyResourcesToBuild(self, build_dir, project_path):
|
||||
"""复制GUI资源到构建目录的resources文件夹"""
|
||||
try:
|
||||
# 创建目标resources目录
|
||||
resources_dest = os.path.join(build_dir, "resources")
|
||||
|
||||
# 源Resources目录
|
||||
resources_src = os.path.join(project_path, "Resources")
|
||||
|
||||
if os.path.exists(resources_src):
|
||||
# 直接复制整个Resources目录
|
||||
if os.path.exists(resources_dest):
|
||||
shutil.rmtree(resources_dest)
|
||||
|
||||
shutil.copytree(
|
||||
resources_src,
|
||||
resources_dest,
|
||||
ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log')
|
||||
)
|
||||
print("✓ Resources目录已复制到build/resources")
|
||||
|
||||
# 统计复制的文件数量
|
||||
file_count = 0
|
||||
for root, dirs, files in os.walk(resources_dest):
|
||||
file_count += len(files)
|
||||
print(f"✓ 共复制了 {file_count} 个资源文件")
|
||||
else:
|
||||
# 创建空的resources目录
|
||||
if not os.path.exists(resources_dest):
|
||||
os.makedirs(resources_dest)
|
||||
print("⚠️ 项目中没有Resources目录")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 复制资源文件时出错: {str(e)}")
|
||||
|
||||
def _collectResourceFiles(self, project_path):
|
||||
"""收集项目中GUI使用的资源文件"""
|
||||
resource_files = set()
|
||||
|
||||
try:
|
||||
# 收集Resources目录中的所有文件(这是最主要的资源来源)
|
||||
resources_dir = os.path.join(project_path, "Resources")
|
||||
if os.path.exists(resources_dir):
|
||||
for root, dirs, files in os.walk(resources_dir):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
# 收集所有文件,不仅仅是媒体文件
|
||||
resource_files.add(file_path)
|
||||
|
||||
# 同时收集场景中引用的特定资源
|
||||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||
if os.path.exists(scene_file):
|
||||
# 从场景文件中提取资源引用
|
||||
referenced_files = self._extractResourcesFromScene(scene_file, project_path)
|
||||
for file_path in referenced_files:
|
||||
if os.path.isabs(file_path):
|
||||
if os.path.exists(file_path):
|
||||
resource_files.add(file_path)
|
||||
else:
|
||||
# 相对路径
|
||||
full_path = os.path.join(project_path, file_path)
|
||||
if os.path.exists(full_path):
|
||||
resource_files.add(full_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"收集资源文件时出错: {str(e)}")
|
||||
|
||||
return list(resource_files)
|
||||
|
||||
def _extractResourcesFromScene(self, scene_file, project_path):
|
||||
"""从场景文件中提取资源引用"""
|
||||
referenced_files = []
|
||||
|
||||
try:
|
||||
# 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑
|
||||
# 由于直接解析BAM文件比较复杂,我们采用间接方式
|
||||
|
||||
# 检查项目配置文件或其他元数据文件中可能包含的资源引用
|
||||
config_file = os.path.join(project_path, "project.json")
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
project_config = json.load(f)
|
||||
|
||||
# 如果配置中有资源列表信息,可以在这里处理
|
||||
# 这里暂时保持简单实现
|
||||
except Exception as e:
|
||||
print(f"读取项目配置时出错: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"从场景提取资源引用时出错: {str(e)}")
|
||||
|
||||
return referenced_files
|
||||
|
||||
def _isMediaFile(self, file_path):
|
||||
"""判断是否为媒体文件(图片或视频)"""
|
||||
media_extensions = {
|
||||
# 图片格式
|
||||
'.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff',
|
||||
# 视频格式
|
||||
'.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv'
|
||||
}
|
||||
|
||||
_, ext = os.path.splitext(file_path.lower())
|
||||
return ext in media_extensions
|
||||
|
||||
def _createAppFile(self, build_dir, project_name):
|
||||
"""创建应用程序主文件"""
|
||||
app_code = f'''#!/usr/bin/env python3
|
||||
@ -409,180 +622,199 @@ class ProjectManager:
|
||||
使用Panda3D引擎编辑器创建
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
#获取渲染管线路径
|
||||
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
|
||||
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
|
||||
""")
|
||||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
class {project_name.replace(' ', '').replace('-', '')}App(ShowBase):
|
||||
"""应用程序主类"""
|
||||
|
||||
class MainApp(ShowBase):
|
||||
def __init__(self):
|
||||
ShowBase.__init__(self)
|
||||
load_prc_file_data("","""
|
||||
win-size 1200 720
|
||||
window-title Render
|
||||
""")
|
||||
|
||||
print(f"启动 {project_name}...")
|
||||
pipeline_path = "../../"
|
||||
|
||||
# 设置窗口属性
|
||||
self.setupWindow()
|
||||
if not os.path.isfile(os.path.join(pipeline_path,"setup.py")):
|
||||
pipeline_path = "../../RenderPipeline"
|
||||
|
||||
# 设置光照
|
||||
self.setupLighting()
|
||||
sys.path.insert(0,pipeline_path)
|
||||
|
||||
# 加载场景
|
||||
self.loadScene()
|
||||
from rpcore import RenderPipeline,SpotLight
|
||||
self.render_pipeline = RenderPipeline()
|
||||
self.render_pipeline.create(self)
|
||||
|
||||
# 设置相机控制
|
||||
self.setupControls()
|
||||
from rpcore.util.movement_controller import MovementController
|
||||
|
||||
print("✓ 应用程序初始化完成")
|
||||
self.render_pipeline.daytime_mgr.time = "12:00"
|
||||
|
||||
def setupWindow(self):
|
||||
"""设置窗口"""
|
||||
# 设置背景色
|
||||
self.setBackgroundColor(0.2, 0.2, 0.2)
|
||||
self.loadFullScene()
|
||||
|
||||
# 设置窗口属性
|
||||
props = WindowProperties()
|
||||
props.setTitle("{project_name}")
|
||||
self.win.requestProperties(props)
|
||||
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()
|
||||
|
||||
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):
|
||||
"""加载场景"""
|
||||
base.accept("l",self.tour)
|
||||
|
||||
def loadFullScene(self):
|
||||
"""加载完整场景,包括所有元素"""
|
||||
try:
|
||||
# 查找场景文件
|
||||
scene_file = "scene.bam"
|
||||
if not os.path.exists(scene_file):
|
||||
print("警告: 没有找到场景文件,创建默认场景")
|
||||
self.createDefaultScene()
|
||||
return
|
||||
if os.path.exists(scene_file):
|
||||
# 使用readBamFile加载完整场景
|
||||
from panda3d.core import BamCache
|
||||
BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题
|
||||
|
||||
# 加载场景
|
||||
scene = self.loader.loadModel(scene_file)
|
||||
if scene:
|
||||
scene.reparentTo(self.render)
|
||||
print("✓ 场景加载成功")
|
||||
|
||||
# 自动调整相机位置
|
||||
self.adjustCamera()
|
||||
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("警告: 场景加载失败,创建默认场景")
|
||||
self.createDefaultScene()
|
||||
|
||||
print("⚠️ 未找到场景文件")
|
||||
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()
|
||||
print(f"加载完整场景时出错: {{str(e)}}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def processSceneElements(self, scene):
|
||||
"""处理场景中的各种元素"""
|
||||
try:
|
||||
# 处理光源
|
||||
self.processLights(scene)
|
||||
|
||||
# 设置相机位置
|
||||
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)
|
||||
# 处理GUI元素
|
||||
self.processGUIElements(scene)
|
||||
|
||||
# 处理其他特殊元素
|
||||
self.processSpecialElements(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 processSpecialElements(self, scene):
|
||||
"""处理特殊元素"""
|
||||
try:
|
||||
# 处理Cesium Tilesets
|
||||
tilesets = scene.findAllMatches("**/=element_type=cesium_tileset")
|
||||
for tileset_node in tilesets:
|
||||
try:
|
||||
# Tilesets需要特殊处理,这里只是标记
|
||||
print(f"✓ Cesium Tileset {{tileset_node.getName()}} 已识别")
|
||||
except Exception as e:
|
||||
print(f"处理Cesium Tileset {{tileset_node.getName()}} 失败: {{str(e)}}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理特殊元素时出错: {{str(e)}}")
|
||||
|
||||
def setupControls(self):
|
||||
"""设置相机控制"""
|
||||
# 启用鼠标控制
|
||||
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)
|
||||
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 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键退出...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -51,7 +51,7 @@ class InterfaceManager:
|
||||
|
||||
def onTreeItemClicked(self, item, column):
|
||||
"""处理树形控件项目点击事件"""
|
||||
print(f"树形控件点击事件触发,item: {item}, column: {column}")
|
||||
#print(f"树形控件点击事件触发,item: {item}, column: {column}")
|
||||
|
||||
# 检查是否点击了空白区域
|
||||
# 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象
|
||||
@ -339,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:
|
||||
|
||||
@ -19,6 +19,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction
|
||||
QSpinBox, QFrame, QRadioButton, QTextEdit)
|
||||
from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import OrthographicLens
|
||||
|
||||
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget
|
||||
from ui.icon_manager import get_icon_manager, get_icon
|
||||
@ -27,7 +28,7 @@ try:
|
||||
WEB_ENGINE_AVAILABLE = True
|
||||
except ImportError:
|
||||
QWebEngineView = None
|
||||
WEB_ENGINE_AVAILABLE = False
|
||||
WEB_ENGINE_AVAILABLE = False
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""主窗口类"""
|
||||
@ -37,6 +38,10 @@ class MainWindow(QMainWindow):
|
||||
self.world = world
|
||||
self.world.main_window = self # 关键:让world对象能访问主窗口
|
||||
|
||||
#剪切板相关属性
|
||||
self.clipboard = []
|
||||
self.clipboard_mode = None
|
||||
|
||||
# 初始化图标管理器并打印调试信息
|
||||
self.icon_manager = get_icon_manager()
|
||||
print("🔧 图标管理器初始化完成")
|
||||
@ -447,8 +452,7 @@ class MainWindow(QMainWindow):
|
||||
select_icon = get_icon('select_tool', QSize(16, 16))
|
||||
if not select_icon.isNull():
|
||||
self.selectTool.setIcon(select_icon)
|
||||
else:
|
||||
self.selectTool.setText('选择') # 如果没有图标则显示文字
|
||||
self.selectTool.setText('选择') # 如果没有图标则显示文字
|
||||
self.selectTool.setIconSize(QSize(16, 16))
|
||||
self.selectTool.setCheckable(True)
|
||||
self.selectTool.setToolTip("选择工具 (Q)")
|
||||
@ -716,6 +720,9 @@ class MainWindow(QMainWindow):
|
||||
self.interactionMenu.addSeparator()
|
||||
self.startAssemblyInteractionAction = self.interactionMenu.addAction('开始拆装交互')
|
||||
self.startAssemblyInteractionAction.triggered.connect(self.onStartAssemblyInteraction)
|
||||
self.interactionMenu.addSeparator()
|
||||
self.maintenanceSystemAction = self.interactionMenu.addAction('🔧 维修系统')
|
||||
self.maintenanceSystemAction.triggered.connect(self.onOpenMaintenanceSystem)
|
||||
|
||||
self.cesiumMenu = menubar.addMenu('Cesium')
|
||||
self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles')
|
||||
@ -816,6 +823,238 @@ class MainWindow(QMainWindow):
|
||||
# 统一连接信号到处理方法
|
||||
self.connectCreateMenuActions()
|
||||
|
||||
def setupViewMenuActions(self):
|
||||
"""设置视图菜单动作"""
|
||||
# 连接视图菜单事件
|
||||
self.viewPerspectiveAction.triggered.connect(self.onViewPerspective)
|
||||
#self.viewTopAction.triggered.connect(self.onViewTop)
|
||||
#self.viewFrontAction.triggered.connect(self.onViewFront)
|
||||
self.viewOrthographicAction = self.viewMenu.addAction('正交视图') # 添加正交视图动作
|
||||
#self.viewOrthographicAction.triggered.connect(self.onViewOrthographic)
|
||||
#self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接
|
||||
|
||||
# 保存原始相机设置
|
||||
self._original_camera_fov = 80
|
||||
self._original_camera_pos = (0, -50, 20)
|
||||
self._original_camera_lookat = (0, 0, 0)
|
||||
|
||||
self._grid_visible = False
|
||||
def onViewPerspective(self):
|
||||
"""切换到透视视图"""
|
||||
try:
|
||||
lens = self.world.cam.node().getLens()
|
||||
lens.setFov(self._original_camera_fov)
|
||||
except Exception as e:
|
||||
print(f"切换到透视视图失败{e}")
|
||||
|
||||
def onViewOrthographic(self):
|
||||
"""切换到正交视图"""
|
||||
try:
|
||||
# 保存当前相机设置(如果是透视模式)
|
||||
lens = self.world.cam.node().getLens()
|
||||
if not hasattr(self, '_saved_perspective_settings'):
|
||||
self._saved_perspective_settings = {
|
||||
'fov': lens.getFov()[0],
|
||||
'pos': self.world.cam.getPos(),
|
||||
'hpr': self.world.cam.getHpr()
|
||||
}
|
||||
|
||||
# 获取窗口尺寸
|
||||
win_width, win_height = self.world.getWindowSize()
|
||||
aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9
|
||||
|
||||
# 修改现有镜头为正交投影
|
||||
if not isinstance(lens, OrthographicLens):
|
||||
# 保存当前镜头类型
|
||||
self._original_lens = lens
|
||||
|
||||
# 创建正交镜头并替换现有镜头
|
||||
ortho_lens = OrthographicLens()
|
||||
ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小
|
||||
ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面
|
||||
|
||||
# 应用正交镜头
|
||||
self.world.cam.node().setLens(ortho_lens)
|
||||
else:
|
||||
# 如果已经是正交镜头,则调整其参数
|
||||
film_height = 20
|
||||
film_width = film_height * aspect_ratio
|
||||
lens.setFilmSize(film_width, film_height)
|
||||
|
||||
print("切换到正交视图")
|
||||
except Exception as e:
|
||||
print(f"切换正交视图失败: {e}")
|
||||
|
||||
def onViewTop(self):
|
||||
"""切换到俯视图(正交)"""
|
||||
try:
|
||||
# 保存当前设置
|
||||
self._saveCurrentCameraSettings()
|
||||
|
||||
# 设置正交投影
|
||||
self._setupOrthographicLens()
|
||||
|
||||
# 设置摄像机位置(从上方俯视)
|
||||
self.world.cam.setPos(0, 0, 30)
|
||||
self.world.cam.lookAt(0, 0, 0)
|
||||
self.world.cam.setHpr(0, -90, 0) # 朝下看
|
||||
|
||||
# 更新菜单项文本
|
||||
self._updateViewMenuText()
|
||||
|
||||
print("切换到俯视图")
|
||||
except Exception as e:
|
||||
print(f"切换俯视图失败: {e}")
|
||||
|
||||
def onViewFront(self):
|
||||
"""切换到前视图(正交)"""
|
||||
try:
|
||||
# 保存当前设置
|
||||
self._saveCurrentCameraSettings()
|
||||
|
||||
# 设置正交投影
|
||||
self._setupOrthographicLens()
|
||||
|
||||
# 设置摄像机位置(从前方向看)
|
||||
self.world.cam.setPos(0, -30, 0)
|
||||
self.world.cam.lookAt(0, 0, 0)
|
||||
self.world.cam.setHpr(0, 0, 0) # 正面朝向
|
||||
|
||||
# 更新菜单项文本
|
||||
self._updateViewMenuText()
|
||||
|
||||
print("切换到前视图")
|
||||
except Exception as e:
|
||||
print(f"切换前视图失败: {e}")
|
||||
|
||||
def onViewGrid(self):
|
||||
"""切换网格显示/隐藏"""
|
||||
try:
|
||||
# 切换网格显示状态
|
||||
self._grid_visible = not self._grid_visible
|
||||
|
||||
# 查找网格节点
|
||||
grid_node = self.world.render.find("**/grid")
|
||||
if grid_node.isEmpty():
|
||||
# 如果网格不存在则创建
|
||||
self._createGridView()
|
||||
grid_node = self.world.render.find("**/grid")
|
||||
|
||||
# 设置网格可见性
|
||||
if not grid_node.isEmpty():
|
||||
if self._grid_visible:
|
||||
grid_node.show()
|
||||
self.viewGridAction.setText("隐藏网格")
|
||||
print("网格已显示")
|
||||
else:
|
||||
grid_node.hide()
|
||||
self.viewGridAction.setText("显示网格")
|
||||
print("网格已隐藏")
|
||||
else:
|
||||
print("网格节点未找到")
|
||||
|
||||
except Exception as e:
|
||||
print(f"切换网格显示失败: {e}")
|
||||
def _createGridView(self):
|
||||
"""创建网格视图"""
|
||||
try:
|
||||
from panda3d.core import LineSegs,Vec3
|
||||
grid_node = self.world.render.attachNewNode("grid")
|
||||
lines = LineSegs()
|
||||
lines.setThickness(1.0)
|
||||
lines.setColor(0.3,0.3,0.3,1.0)
|
||||
grid_size = 20
|
||||
grid_step = 1
|
||||
|
||||
for i in range(-grid_size,grid_size+1,grid_step):
|
||||
lines.moveTo(Vec3(-grid_size,i,0))
|
||||
lines.drawTo(Vec3(grid_size,i,0))
|
||||
|
||||
grid_node.attachNewNode(lines.create())
|
||||
# 添加中心轴线(红色X轴,绿色Y轴)
|
||||
axis_lines = LineSegs()
|
||||
axis_lines.setThickness(2.0)
|
||||
# X轴(红色)
|
||||
axis_lines.setColor(1.0,0.0,0.0,1.0)
|
||||
axis_lines.moveTo(Vec3(0,0,0))
|
||||
axis_lines.drawTo(Vec3(grid_size,0,0))
|
||||
# Y轴(绿色)
|
||||
axis_lines.setColor(0.0, 1.0, 0.0, 1.0)
|
||||
axis_lines.moveTo(Vec3(0, 0, 0))
|
||||
axis_lines.drawTo(Vec3(0, grid_size, 0))
|
||||
|
||||
grid_node.attachNewNode(axis_lines.create())
|
||||
|
||||
print("网格已创建")
|
||||
except Exception as e:
|
||||
print(f"创建网格失败{e}")
|
||||
|
||||
def _saveCurrentCameraSettings(self):
|
||||
"""保存当前相机设置"""
|
||||
try:
|
||||
lens = self.world.cam.node().getLens()
|
||||
self._saved_camera_settings = {
|
||||
'lens_type': 'perspective' if not isinstance(lens, OrthographicLens) else 'orthographic',
|
||||
'fov': lens.getFov()[0] if hasattr(lens, 'getFov') else None,
|
||||
'film_size': (lens.getFilmSize()[0], lens.getFilmSize()[1]) if hasattr(lens, 'getFilmSize') else None,
|
||||
'pos': self.world.cam.getPos(),
|
||||
'hpr': self.world.cam.getHpr()
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"保存相机设置失败: {e}")
|
||||
|
||||
def _setupOrthographicLens(self):
|
||||
"""设置正交镜头"""
|
||||
try:
|
||||
win_width, win_height = self.world.getWindowSize()
|
||||
aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9
|
||||
|
||||
from panda3d.core import OrthographicLens
|
||||
ortho_lens = OrthographicLens()
|
||||
ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小
|
||||
ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面
|
||||
|
||||
self.world.cam.node().setLens(ortho_lens)
|
||||
except Exception as e:
|
||||
print(f"设置正交镜头失败: {e}")
|
||||
|
||||
def _updateViewMenuText(self):
|
||||
"""更新视图菜单文本"""
|
||||
try:
|
||||
lens = self.world.cam.node().getLens()
|
||||
from panda3d.core import OrthographicLens
|
||||
|
||||
# 更新正交/透视视图动作文本
|
||||
if isinstance(lens, OrthographicLens):
|
||||
self.viewOrthographicAction.setText("切换到透视视图")
|
||||
self.viewOrthographicAction.triggered.disconnect()
|
||||
self.viewOrthographicAction.triggered.connect(self.onViewPerspective)
|
||||
else:
|
||||
self.viewOrthographicAction.setText("切换到正交视图")
|
||||
self.viewOrthographicAction.triggered.disconnect()
|
||||
self.viewOrthographicAction.triggered.connect(self.onViewOrthographic)
|
||||
except Exception as e:
|
||||
print(f"更新视图菜单文本失败: {e}")
|
||||
|
||||
# 如果需要在窗口大小改变时调整正交镜头,可以添加以下方法
|
||||
def _onWindowResized(self):
|
||||
"""窗口大小改变时的处理"""
|
||||
try:
|
||||
lens = self.world.cam.node().getLens()
|
||||
from panda3d.core import OrthographicLens
|
||||
|
||||
# 如果当前是正交镜头,需要根据新窗口大小调整
|
||||
if isinstance(lens, OrthographicLens):
|
||||
win_width, win_height = self.world.getWindowSize()
|
||||
if win_height != 0:
|
||||
aspect_ratio = win_width / win_height
|
||||
film_height = 20
|
||||
film_width = film_height * aspect_ratio
|
||||
lens.setFilmSize(film_width, film_height)
|
||||
except Exception as e:
|
||||
print(f"窗口大小调整失败: {e}")
|
||||
|
||||
|
||||
def connectCreateMenuActions(self):
|
||||
"""统一连接创建菜单的信号到处理方法"""
|
||||
# 连接到world对象的创建方法
|
||||
@ -1572,6 +1811,9 @@ class MainWindow(QMainWindow):
|
||||
self.buildAction.triggered.connect(lambda: buildPackage(self))
|
||||
self.exitAction.triggered.connect(QApplication.instance().quit)
|
||||
|
||||
#添加保存项目快捷键盘
|
||||
self.saveAction.setShortcut(QKeySequence.Save)
|
||||
|
||||
# 连接工具事件
|
||||
self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑"))
|
||||
self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑"))
|
||||
@ -1601,6 +1843,21 @@ class MainWindow(QMainWindow):
|
||||
lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
|
||||
print("已连接点击信号")
|
||||
|
||||
self.undoAction.triggered.connect(self.onUndo)
|
||||
self.redoAction.triggered.connect(self.onRedo)
|
||||
self.cutAction.triggered.connect(self.onCut)
|
||||
self.copyAction.triggered.connect(self.onCopy)
|
||||
self.pasteAction.triggered.connect(self.onPaste)
|
||||
|
||||
self.undoAction.setShortcut(QKeySequence.Undo)
|
||||
self.redoAction.setShortcut(QKeySequence.Redo)
|
||||
self.cutAction.setShortcut(QKeySequence.Cut)
|
||||
self.copyAction.setShortcut(QKeySequence.Copy)
|
||||
self.pasteAction.setShortcut(QKeySequence.Paste)
|
||||
|
||||
#连接视图菜单事件
|
||||
self.setupViewMenuActions()
|
||||
|
||||
# 连接工具切换信号
|
||||
#self.toolGroup.buttonClicked.connect(self.onToolChanged)
|
||||
|
||||
@ -1611,6 +1868,313 @@ class MainWindow(QMainWindow):
|
||||
# self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
|
||||
# self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
|
||||
|
||||
def onCopy(self):
|
||||
"""复制操作"""
|
||||
try:
|
||||
selected_item = self.treeWidget.currentItem()
|
||||
if not selected_item:
|
||||
QMessageBox.information(self, "提示", "请先选择要复制的节点")
|
||||
return
|
||||
|
||||
# 获取选中的节点
|
||||
selected_node = getattr(selected_item, 'node_path', None)
|
||||
if not selected_node:
|
||||
selected_node = getattr(selected_item, 'node', None)
|
||||
|
||||
if not selected_node and hasattr(self.world, 'selection'):
|
||||
selected_node = getattr(self.world.selection, 'selectedNode', None)
|
||||
|
||||
if not selected_node or selected_node.isEmpty():
|
||||
QMessageBox.warning(self, "错误", "无法获取选中节点")
|
||||
return
|
||||
|
||||
# 检查是否是根节点
|
||||
if selected_node.getName() == "render":
|
||||
QMessageBox.warning(self, "错误", "不能复制根节点")
|
||||
return
|
||||
|
||||
# 序列化节点数据
|
||||
node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
|
||||
if not node_data:
|
||||
QMessageBox.warning(self, "错误", "无法序列化选中节点")
|
||||
return
|
||||
|
||||
# 存储到剪切板
|
||||
self.clipboard = [node_data]
|
||||
self.clipboard_mode = "copy"
|
||||
|
||||
QMessageBox.information(self, "成功", "节点已复制到剪切板")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"复制操作失败: {str(e)}")
|
||||
|
||||
def onCut(self):
|
||||
"""剪切操作"""
|
||||
try:
|
||||
selected_item = self.treeWidget.currentItem()
|
||||
if not selected_item:
|
||||
QMessageBox.information(self, "提示", "请先选择要剪切的节点")
|
||||
return
|
||||
|
||||
# 获取选中的节点
|
||||
selected_node = getattr(selected_item, 'node_path', None)
|
||||
if not selected_node:
|
||||
selected_node = getattr(selected_item, 'node', None)
|
||||
|
||||
if not selected_node and hasattr(self.world, 'selection'):
|
||||
selected_node = getattr(self.world.selection, 'selectedNode', None)
|
||||
|
||||
if not selected_node or selected_node.isEmpty():
|
||||
QMessageBox.warning(self, "错误", "无法获取选中节点")
|
||||
return
|
||||
|
||||
# 检查是否是根节点或特殊节点
|
||||
if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]:
|
||||
QMessageBox.warning(self, "错误", "不能剪切根节点或系统节点")
|
||||
return
|
||||
|
||||
# 序列化节点数据
|
||||
node_data = self.world.scene_manager.serializeNodeForCopy(selected_node)
|
||||
if not node_data:
|
||||
QMessageBox.warning(self, "错误", "无法序列化选中节点")
|
||||
return
|
||||
|
||||
# 存储到剪切板
|
||||
self.clipboard = [node_data]
|
||||
self.clipboard_mode = "cut"
|
||||
|
||||
# 删除原节点
|
||||
self.treeWidget.delete_items([selected_item])
|
||||
|
||||
QMessageBox.information(self, "成功", "节点已剪切到剪切板")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"剪切操作失败: {str(e)}")
|
||||
|
||||
def onPaste(self):
|
||||
"""粘贴操作"""
|
||||
try:
|
||||
if not self.clipboard:
|
||||
QMessageBox.information(self, "提示", "剪切板为空")
|
||||
return
|
||||
|
||||
# 获取粘贴目标节点
|
||||
parent_item = self.treeWidget.currentItem()
|
||||
parent_node = None
|
||||
|
||||
# 如果选中了节点,将其作为父节点
|
||||
if parent_item:
|
||||
parent_node = getattr(parent_item, 'node_path', None)
|
||||
if not parent_node:
|
||||
parent_node = getattr(parent_item, 'node', None)
|
||||
|
||||
# 确保获取到有效的父节点
|
||||
if parent_node and not parent_node.isEmpty():
|
||||
print(f"将粘贴到选中的节点: {parent_node.getName()}")
|
||||
else:
|
||||
parent_node = None
|
||||
|
||||
# 如果没有选中有效节点,默认粘贴到render节点下
|
||||
if not parent_node:
|
||||
print("未选中有效节点,将粘贴到根节点下")
|
||||
# 查找render节点
|
||||
for i in range(self.treeWidget.topLevelItemCount()):
|
||||
item = self.treeWidget.topLevelItem(i)
|
||||
if item.text(0) == "render":
|
||||
parent_item = item
|
||||
break
|
||||
|
||||
# 如果找到了render节点项,获取对应的节点
|
||||
if parent_item:
|
||||
parent_node = getattr(parent_item, 'node_path', None)
|
||||
if not parent_node:
|
||||
parent_node = getattr(parent_item, 'node', None)
|
||||
|
||||
# 如果仍然没有找到父节点项,直接使用world.render
|
||||
if not parent_node:
|
||||
parent_node = self.world.render
|
||||
|
||||
# 检查父节点有效性
|
||||
if not parent_node or parent_node.isEmpty():
|
||||
QMessageBox.warning(self, "错误", "无法获取有效的父节点")
|
||||
return
|
||||
|
||||
# 检查目标节点是否为允许的父节点类型
|
||||
parent_name = parent_node.getName()
|
||||
if parent_name in ["camera", "ambientLight", "directionalLight"]:
|
||||
QMessageBox.warning(self, "错误", "不能粘贴到该类型节点下")
|
||||
return
|
||||
|
||||
# 粘贴节点
|
||||
pasted_nodes = []
|
||||
for node_data in self.clipboard:
|
||||
print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}")
|
||||
new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node)
|
||||
if new_node:
|
||||
pasted_nodes.append(new_node)
|
||||
print(f"成功粘贴节点: {new_node.getName()}")
|
||||
else:
|
||||
print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}")
|
||||
|
||||
# 如果是剪切操作,清空剪切板
|
||||
if self.clipboard_mode == "cut":
|
||||
self.clipboard.clear()
|
||||
self.clipboard_mode = None
|
||||
|
||||
QMessageBox.information(self, "成功", f"已粘贴 {len(pasted_nodes)} 个节点")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"粘贴操作失败: {str(e)}")
|
||||
|
||||
def _serializeNode(self, node):
|
||||
"""序列化节点数据"""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return None
|
||||
|
||||
node_data = {
|
||||
'name': node.getName(),
|
||||
'type': type(node.node()).__name__,
|
||||
'pos': (node.getX(), node.getY(), node.getZ()),
|
||||
'hpr': (node.getH(), node.getP(), node.getR()),
|
||||
'scale': (node.getSx(), node.getSy(), node.getSz()),
|
||||
'tags': {},
|
||||
'children': []
|
||||
}
|
||||
|
||||
# 保存所有标签
|
||||
try:
|
||||
# 使用更安全的方式获取标签
|
||||
if hasattr(node, 'getTagKeys'):
|
||||
for tag_key in node.getTagKeys():
|
||||
node_data['tags'][tag_key] = node.getTag(tag_key)
|
||||
except Exception as e:
|
||||
print(f"获取标签时出错: {e}")
|
||||
|
||||
# 递归序列化子节点(跳过辅助节点)
|
||||
try:
|
||||
if hasattr(node, 'getChildren'):
|
||||
for child in node.getChildren():
|
||||
# 跳过辅助节点
|
||||
child_name = child.getName() if hasattr(child, 'getName') else ""
|
||||
if not child_name.startswith(('gizmo', 'selectionBox', 'grid')):
|
||||
child_data = self._serializeNode(child)
|
||||
if child_data:
|
||||
node_data['children'].append(child_data)
|
||||
except Exception as e:
|
||||
print(f"序列化子节点时出错: {e}")
|
||||
|
||||
return node_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"序列化节点失败: {e}")
|
||||
return None
|
||||
|
||||
def _deserializeNode(self, node_data, parent_node):
|
||||
"""反序列化节点数据"""
|
||||
try:
|
||||
if not node_data or not parent_node or parent_node.isEmpty():
|
||||
return None
|
||||
|
||||
# 创建新节点
|
||||
node_name = node_data.get('name', 'node')
|
||||
new_node = parent_node.attachNewNode(node_name)
|
||||
|
||||
# 设置变换
|
||||
try:
|
||||
pos = node_data.get('pos', (0, 0, 0))
|
||||
hpr = node_data.get('hpr', (0, 0, 0))
|
||||
scale = node_data.get('scale', (1, 1, 1))
|
||||
|
||||
new_node.setPos(*pos)
|
||||
new_node.setHpr(*hpr)
|
||||
new_node.setScale(*scale)
|
||||
except Exception as e:
|
||||
print(f"设置变换时出错: {e}")
|
||||
|
||||
# 恢复标签
|
||||
try:
|
||||
for tag_key, tag_value in node_data.get('tags', {}).items():
|
||||
new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串
|
||||
except Exception as e:
|
||||
print(f"恢复标签时出错: {e}")
|
||||
|
||||
# 递归创建子节点
|
||||
try:
|
||||
for child_data in node_data.get('children', []):
|
||||
self._deserializeNode(child_data, new_node)
|
||||
except Exception as e:
|
||||
print(f"创建子节点时出错: {e}")
|
||||
|
||||
return new_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"反序列化节点失败: {e}")
|
||||
return None
|
||||
|
||||
def _deleteNode(self, node, tree_item):
|
||||
"""删除节点"""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
# 特殊处理选中节点
|
||||
if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node:
|
||||
self.world.selection.clearSelection()
|
||||
|
||||
# 从场景中删除节点
|
||||
node.removeNode()
|
||||
|
||||
# 从树形控件中删除项目
|
||||
if tree_item:
|
||||
try:
|
||||
parent = tree_item.parent()
|
||||
if parent:
|
||||
parent.removeChild(tree_item)
|
||||
else:
|
||||
index = self.treeWidget.indexOfTopLevelItem(tree_item)
|
||||
if index >= 0:
|
||||
self.treeWidget.takeTopLevelItem(index)
|
||||
except Exception as e:
|
||||
print(f"从树形控件删除项目时出错: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"删除节点失败: {e}")
|
||||
|
||||
# 添加撤销/重做功能的基础实现
|
||||
def onUndo(self):
|
||||
"""撤销操作"""
|
||||
if hasattr(self.world,'command_manager'):
|
||||
if self.world.command_manager.can_undo():
|
||||
success = self.world.command_manager.undo()
|
||||
if success:
|
||||
print("成功操作")
|
||||
else:
|
||||
print("撤销失败")
|
||||
QMessageBox.information(self,"提示","撤销操作失败")
|
||||
else:
|
||||
print("没有可撤销的操作")
|
||||
QMessageBox.information(self,"提示","没有可撤销的操作")
|
||||
else:
|
||||
print("命令管理器未初始化")
|
||||
QMessageBox.information(self,"提示","命令系统未初始化")
|
||||
|
||||
def onRedo(self):
|
||||
"""重做操作"""
|
||||
if hasattr(self.world,'command_manager'):
|
||||
if self.world.command_manager.can_redo():
|
||||
success = self.world.command_manager.redo()
|
||||
if success:
|
||||
print("成功重做")
|
||||
else:
|
||||
print("重做失败")
|
||||
QMessageBox.information(self,"提示","重做操作失败")
|
||||
else:
|
||||
print("没有可重做的操作")
|
||||
QMessageBox.information(self,"提示","没有可重做的操作")
|
||||
else:
|
||||
print("命令管理器未初始化")
|
||||
QMessageBox.information(self,"提示","命令系统未初始化")
|
||||
|
||||
def onCreateCesiumView(self):
|
||||
if hasattr(self.world,'gui_manager') and self.world.gui_manager:
|
||||
@ -2675,6 +3239,16 @@ class MainWindow(QMainWindow):
|
||||
try:
|
||||
print("🔄 正在关闭应用程序...")
|
||||
|
||||
# 关闭拆装交互相关的弹窗
|
||||
if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction:
|
||||
print("🧹 关闭拆装交互弹窗...")
|
||||
if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog:
|
||||
self.world.assembly_interaction.step_dialog.close()
|
||||
self.world.assembly_interaction.step_dialog = None
|
||||
# 停止交互模式
|
||||
if self.world.assembly_interaction.is_active:
|
||||
self.world.assembly_interaction.stop_interaction_mode()
|
||||
|
||||
# 清理工具管理器中的进程
|
||||
if hasattr(self.world, 'tool_manager') and self.world.tool_manager:
|
||||
print("🧹 清理工具管理器进程...")
|
||||
@ -2908,6 +3482,71 @@ class MainWindow(QMainWindow):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def onOpenMaintenanceSystem(self):
|
||||
"""打开维修系统"""
|
||||
try:
|
||||
# 导入简化的登录界面
|
||||
from ui.simple_maintenance_login import SimpleMaintenanceLoginDialog
|
||||
from ui.maintenance_system import MaintenanceSubjectDialog, MaintenanceSystemManager
|
||||
|
||||
print("🔧 启动维修系统...")
|
||||
|
||||
# 显示登录界面
|
||||
login_dialog = SimpleMaintenanceLoginDialog(self)
|
||||
|
||||
if login_dialog.exec_() == QDialog.Accepted:
|
||||
print("✅ 登录成功,显示科目选择界面")
|
||||
|
||||
# 获取当前项目路径
|
||||
project_path = None
|
||||
if hasattr(self.world, 'project_manager') and self.world.project_manager:
|
||||
project_path = self.world.project_manager.getCurrentProjectPath()
|
||||
|
||||
# 显示科目选择界面
|
||||
subject_dialog = MaintenanceSubjectDialog(project_path, self)
|
||||
|
||||
def on_subject_selected(subject_path, mode):
|
||||
"""处理科目选择"""
|
||||
try:
|
||||
print(f"🎯 启动维修科目: {subject_path}")
|
||||
print(f"📝 模式: {mode}")
|
||||
|
||||
# 加载科目配置
|
||||
import json
|
||||
with open(subject_path, 'r', encoding='utf-8') as f:
|
||||
subject_config = json.load(f)
|
||||
|
||||
# 启动拆装交互系统
|
||||
if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction:
|
||||
# 设置配置
|
||||
self.world.assembly_interaction.config_data = subject_config
|
||||
|
||||
# 启动交互模式
|
||||
self.world.assembly_interaction.start_interaction_mode(mode=mode)
|
||||
|
||||
print(f"✅ 维修科目启动成功")
|
||||
else:
|
||||
print("❌ 错误:拆装交互系统未初始化")
|
||||
QMessageBox.warning(self, "错误", "拆装交互系统未初始化")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 启动维修科目失败: {e}")
|
||||
QMessageBox.critical(self, "错误", f"启动维修科目失败:\n{str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
subject_dialog.subject_selected.connect(on_subject_selected)
|
||||
subject_dialog.exec_()
|
||||
|
||||
else:
|
||||
print("ℹ️ 用户取消了登录")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 打开维修系统失败: {e}")
|
||||
QMessageBox.critical(self, "错误", f"打开维修系统失败:\n{str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
class AssemblyModeSelectionDialog(QDialog):
|
||||
"""拆装模式选择对话框"""
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ from PyQt5.QtCore import Qt
|
||||
from deploy_libs.unicodedata import normalize
|
||||
from direct.actor.Actor import Actor
|
||||
from direct.gui import DirectGui
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
from idna import check_label
|
||||
from jinja2.compiler import has_safe_repr
|
||||
from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3
|
||||
@ -105,9 +106,10 @@ class PropertyPanelManager:
|
||||
|
||||
def updatePropertyPanel(self, item):
|
||||
"""更新属性面板显示"""
|
||||
if not self._propertyLayout or not self._propertyLayout.parent():
|
||||
print("属性布局未设置或没有父部件!")
|
||||
return
|
||||
# if not self._propertyLayout or not self._propertyLayout.parent():
|
||||
# print("属性布局未设置或没有父部件!")
|
||||
# return
|
||||
#更健壮的有效性检查
|
||||
|
||||
self._cleanupAllReferences()
|
||||
self.clearPropertyPanel()
|
||||
@ -768,6 +770,7 @@ class PropertyPanelManager:
|
||||
if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见
|
||||
# 检查对象是否仍然有效
|
||||
spinbox.blockSignals(True)
|
||||
spinbox.setKeyboardTracking(False) # 确保禁用键盘跟踪
|
||||
spinbox.setValue(value)
|
||||
spinbox.blockSignals(False)
|
||||
except RuntimeError as e:
|
||||
@ -790,42 +793,79 @@ class PropertyPanelManager:
|
||||
# 位置控件
|
||||
transform_layout.addWidget(QLabel("相对位置"), 0, 0)
|
||||
|
||||
# 创建并设置 X, Y, Z 标签居中
|
||||
x_label = QLabel("X")
|
||||
y_label = QLabel("Y")
|
||||
z_label = QLabel("Z")
|
||||
x_label.setAlignment(Qt.AlignCenter)
|
||||
y_label.setAlignment(Qt.AlignCenter)
|
||||
z_label.setAlignment(Qt.AlignCenter)
|
||||
|
||||
transform_layout.addWidget(x_label, 0, 1)
|
||||
transform_layout.addWidget(y_label, 0, 2)
|
||||
transform_layout.addWidget(z_label, 0, 3)
|
||||
|
||||
# 位置数值输入框
|
||||
self.pos_x = self._createSafeSpinBox(-1000, 1000)
|
||||
self.pos_y = self._createSafeSpinBox(-1000, 1000)
|
||||
self.pos_z = self._createSafeSpinBox(-1000, 1000)
|
||||
|
||||
# X坐标
|
||||
transform_layout.addWidget(QLabel("X:"), 1, 0)
|
||||
self.pos_x = QLineEdit()
|
||||
self.pos_x.setText(str(round(nodePath.getX(), 6)))
|
||||
self.pos_x.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'x'))
|
||||
transform_layout.addWidget(self.pos_x, 1, 1)
|
||||
transform_layout.addWidget(self.pos_y, 1, 2)
|
||||
transform_layout.addWidget(self.pos_z, 1, 3)
|
||||
|
||||
# 世界位置 (只读)
|
||||
transform_layout.addWidget(QLabel("世界位置"), 2, 0)
|
||||
self.world_pos_x = self._createSafeSpinBox(-10000, 10000, True) # 只读
|
||||
self.world_pos_y = self._createSafeSpinBox(-10000, 10000, True)
|
||||
self.world_pos_z = self._createSafeSpinBox(-10000, 10000, True)
|
||||
# Y坐标
|
||||
transform_layout.addWidget(QLabel("Y:"), 1, 2)
|
||||
self.pos_y = QLineEdit()
|
||||
self.pos_y.setText(str(round(nodePath.getY(), 6)))
|
||||
self.pos_y.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'y'))
|
||||
transform_layout.addWidget(self.pos_y, 1, 3)
|
||||
|
||||
transform_layout.addWidget(self.world_pos_x, 2, 1)
|
||||
transform_layout.addWidget(self.world_pos_y, 2, 2)
|
||||
transform_layout.addWidget(self.world_pos_z, 2, 3)
|
||||
# Z坐标
|
||||
transform_layout.addWidget(QLabel("Z:"), 1, 4)
|
||||
self.pos_z = QLineEdit()
|
||||
self.pos_z.setText(str(round(nodePath.getZ(), 6)))
|
||||
self.pos_z.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'z'))
|
||||
transform_layout.addWidget(self.pos_z, 1, 5)
|
||||
|
||||
return transform_layout
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建变换控件失败: {e}")
|
||||
print(f"创建变换控件时出错: {e}")
|
||||
return None
|
||||
|
||||
def _onPositionEditFinished(self, nodePath, axis):
|
||||
"""位置编辑完成时的处理"""
|
||||
try:
|
||||
# 检查控件是否仍然有效
|
||||
if not hasattr(self, f'pos_{axis}') or getattr(self, f'pos_{axis}') is None:
|
||||
return
|
||||
|
||||
line_edit = getattr(self, f'pos_{axis}')
|
||||
if line_edit is None or line_edit.isHidden():
|
||||
return
|
||||
|
||||
# 获取文本并转换为数值
|
||||
text = line_edit.text()
|
||||
try:
|
||||
new_value = float(text)
|
||||
except ValueError:
|
||||
print(f"无效的数值输入: {text}")
|
||||
# 恢复原来的值
|
||||
if axis == 'x':
|
||||
line_edit.setText(str(round(nodePath.getX(), 6)))
|
||||
elif axis == 'y':
|
||||
line_edit.setText(str(round(nodePath.getY(), 6)))
|
||||
elif axis == 'z':
|
||||
line_edit.setText(str(round(nodePath.getZ(), 6)))
|
||||
return
|
||||
|
||||
# 根据轴设置位置
|
||||
if axis == 'x':
|
||||
nodePath.setX(new_value)
|
||||
elif axis == 'y':
|
||||
nodePath.setY(new_value)
|
||||
elif axis == 'z':
|
||||
nodePath.setZ(new_value)
|
||||
|
||||
print(f"位置已更新: {nodePath.getName()} {axis.upper()} = {new_value}")
|
||||
|
||||
# 如果是坐标轴节点,需要更新坐标轴位置
|
||||
if hasattr(self.world, 'selection_manager'):
|
||||
selection_manager = self.world.selection_manager
|
||||
if (hasattr(selection_manager, 'gizmoTarget') and
|
||||
selection_manager.gizmoTarget == nodePath):
|
||||
# 更新坐标轴位置
|
||||
selection_manager._updateGizmoPositionAndOrientation()
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新位置时出错: {e}")
|
||||
def _createSafeSpinBox(self, min_val, max_val, read_only=False):
|
||||
"""创建安全的数值框"""
|
||||
try:
|
||||
@ -3346,6 +3386,12 @@ class PropertyPanelManager:
|
||||
video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0)
|
||||
path_label = QLabel(video_path)
|
||||
path_label.setWordWrap(True)
|
||||
if len(video_path)>30:
|
||||
display_path = video_path[:27]+"..."
|
||||
path_label.setToolTip(video_path)
|
||||
else:
|
||||
display_path = video_path
|
||||
path_label.setText(display_path)
|
||||
path_label.setStyleSheet("color: #00AAFF;")
|
||||
video_info_layout.addWidget(path_label, 0, 1)
|
||||
elif os.path.exists(video_path):
|
||||
@ -8570,12 +8616,35 @@ except Exception as e:
|
||||
actor = self._getActor(origin_model)
|
||||
if not actor:
|
||||
return
|
||||
|
||||
original_world_pos = origin_model.getPos(self.world.render)
|
||||
original_world_hpr = origin_model.getHpr(self.world.render)
|
||||
original_world_scale = origin_model.getScale(self.world.render)
|
||||
|
||||
actor.setPos(origin_model.getPos())
|
||||
actor.setHpr(origin_model.getHpr())
|
||||
actor.setScale(origin_model.getScale())
|
||||
|
||||
origin_model.hide()
|
||||
actor.show()
|
||||
|
||||
#创建人物来维持世界坐标不变
|
||||
def maintainWorldPosition(task):
|
||||
try:
|
||||
if not actor.isEmpty():
|
||||
actor.setPos(self.world.render,original_world_pos)
|
||||
actor.setHpr(self.world.render,original_world_hpr)
|
||||
actor.setScale(self.world.render,original_world_scale)
|
||||
return task.cont
|
||||
else:
|
||||
return task.done
|
||||
except:
|
||||
return task.done
|
||||
|
||||
taskMgr.add(maintainWorldPosition,f"maintain_anim_pos_{id(actor)}")
|
||||
|
||||
|
||||
|
||||
if hasattr(self, 'animation_combo'):
|
||||
# 获取原始动画名称(存储在 userData 中)
|
||||
current_index = self.animation_combo.currentIndex()
|
||||
@ -8805,9 +8874,9 @@ except Exception as e:
|
||||
current_row += 1
|
||||
|
||||
# X, Y, Z 位置调整
|
||||
self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2)
|
||||
self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2)
|
||||
self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2)
|
||||
self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2)
|
||||
self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2)
|
||||
self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -8857,7 +8926,7 @@ except Exception as e:
|
||||
spinbox = QDoubleSpinBox()
|
||||
spinbox.setRange(min_val, max_val)
|
||||
spinbox.setDecimals(decimals)
|
||||
spinbox.setSingleStep(0.1)
|
||||
spinbox.setSingleStep(0.01)
|
||||
return spinbox
|
||||
|
||||
def _addSphereParameters(self, model, layout, start_row):
|
||||
@ -8868,7 +8937,7 @@ except Exception as e:
|
||||
radius_label = QLabel("半径:")
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -8900,9 +8969,9 @@ except Exception as e:
|
||||
current_row += 1
|
||||
|
||||
# 宽度、长度、高度
|
||||
self.collision_width = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -8950,7 +9019,7 @@ except Exception as e:
|
||||
radius_label = QLabel("半径:")
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -8978,7 +9047,7 @@ except Exception as e:
|
||||
height_label = QLabel("高度:")
|
||||
layout.addWidget(height_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
|
||||
# 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值
|
||||
if not self._hasCollision(model):
|
||||
@ -9847,7 +9916,7 @@ except Exception as e:
|
||||
radius_label.setVisible(True)
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_radius.setVisible(True)
|
||||
self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v))
|
||||
layout.addWidget(self.collision_radius, current_row, 1)
|
||||
@ -9864,9 +9933,9 @@ except Exception as e:
|
||||
layout.addWidget(size_label, current_row, 0)
|
||||
current_row += 1
|
||||
|
||||
self.collision_width = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
|
||||
width_label = QLabel("宽度:")
|
||||
width_label.setVisible(True)
|
||||
@ -9903,7 +9972,7 @@ except Exception as e:
|
||||
radius_label = QLabel("半径:")
|
||||
layout.addWidget(radius_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v))
|
||||
layout.addWidget(self.collision_capsule_radius, current_row, 1)
|
||||
current_row += 1
|
||||
@ -9911,7 +9980,7 @@ except Exception as e:
|
||||
height_label = QLabel("高度:")
|
||||
layout.addWidget(height_label, current_row, 0)
|
||||
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2)
|
||||
self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2)
|
||||
self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v))
|
||||
layout.addWidget(self.collision_capsule_height, current_row, 1)
|
||||
current_row += 1
|
||||
|
||||
@ -1367,6 +1367,27 @@ class CustomConsoleDockWidget(QWidget):
|
||||
""")
|
||||
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)
|
||||
|
||||
@ -1408,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
|
||||
@ -1873,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元素建立父子关系")
|
||||
@ -1899,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
|
||||
@ -2089,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
|
||||
@ -2116,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列表中移除
|
||||
@ -2174,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)- 优化和修复版本"""
|
||||
@ -2200,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()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user