forked from Rowland/EG
Merge pull request 'addRender' (#27) from addRender into main
Reviewed-on: #27
This commit is contained in:
commit
88b027100b
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (PythonProject)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (EG)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@ -32,9 +32,24 @@ 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 AssertionError as e:
|
||||
if "has_mat()" in str(e):
|
||||
print("⚠️ 检测到变换矩阵错误,跳过此帧")
|
||||
# 继续运行而不是崩溃
|
||||
else:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"❌ 渲染循环错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def get_panda_key_modifiers(evt):
|
||||
@ -160,8 +175,8 @@ class QPanda3DWidget(QWidget):
|
||||
def resizeEvent(self, evt):
|
||||
width = evt.size().width()
|
||||
height = evt.size().height()
|
||||
print(f"width:{width}")
|
||||
print(f"height:{height}")
|
||||
#print(f"width:{width}")
|
||||
#print(f"height:{height}")
|
||||
|
||||
from Panda3DWorld import resize_buffer
|
||||
#resize_buffer(width, height)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -90,6 +90,10 @@ class LightManager(RPObject):
|
||||
|
||||
def remove_light(self, light):
|
||||
""" Removes a light """
|
||||
print(f'333333333333333333333333333333,{light.casts_shadows}')
|
||||
# from RenderPipelineFile.rpcore.pynative.internal_light_manager import InternalLightManager
|
||||
# inter = InternalLightManager()
|
||||
# inter.remove_light(light)
|
||||
self.internal_mgr.remove_light(light)
|
||||
self.pta_max_light_index[0] = self.internal_mgr.max_light_index
|
||||
|
||||
|
||||
@ -80,6 +80,7 @@ if NATIVE_CXX_LOADED:
|
||||
RPObject.global_debug("CORE", "Using native core module")
|
||||
from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position
|
||||
else:
|
||||
print(f'343434343434343434343434343')
|
||||
from rpcore import pynative as _native_module # pylint: disable=wrong-import-position
|
||||
RPObject.global_debug("CORE", "Using simulated python-wrapper module")
|
||||
|
||||
|
||||
@ -161,7 +161,12 @@ public:
|
||||
|
||||
// Update maximum index
|
||||
if (slot == _max_index) {
|
||||
while (_max_index >= 0 && !_data[_max_index--]);
|
||||
while (_max_index >= 0 && !_data[_max_index--]);
|
||||
// 正确的修复代码
|
||||
// while (_max_index >= 0 && _data[_max_index] == NULL) {
|
||||
// _max_index--;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -116,7 +116,25 @@ class InternalLightManager(object):
|
||||
source.set_slot(slot)
|
||||
|
||||
def remove_light(self, light):
|
||||
print("111111111111111111111111111111111111111111111111")
|
||||
print(f'44444444444444444444444444')
|
||||
print("\n" + "=" * 50)
|
||||
print(f"DEBUG: Entering remove_light for light object: {light.casts_shadows}")
|
||||
if light:
|
||||
print(f" - Light's Slot: {light.get_slot() if light.has_slot() else 'No Slot'}")
|
||||
print(f" - Does it cast shadows? light.get_casts_shadows() -> {light.get_casts_shadows()}")
|
||||
else:
|
||||
print(" - Light object is None!")
|
||||
|
||||
print("\n --- State of Light System BEFORE removal ---")
|
||||
if hasattr(self, '_lights') and hasattr(self._lights, '_data'):
|
||||
# 打印出当前所有灯光对象,看看有没有异常
|
||||
print(f" - Light Data Array (_data):")
|
||||
for i, l_obj in enumerate(self._lights._data):
|
||||
print(f" [{i}]: {l_obj}")
|
||||
|
||||
print(f"\n - Max Index (_max_index): {self._lights._max_index}")
|
||||
print(f" - Num Entries (_num_entries): {self._lights._num_entries}")
|
||||
print("=" * 50 + "\n")
|
||||
assert light is not None
|
||||
if not light.has_slot():
|
||||
print("ERROR: Could not detach light, light was not attached!")
|
||||
@ -133,7 +151,10 @@ class InternalLightManager(object):
|
||||
# 关键修复:先保存第一个source的slot,再清理
|
||||
first_source = light.get_shadow_source(0)
|
||||
first_source_slot = first_source.get_slot() # 保存slot值
|
||||
|
||||
|
||||
# --- 在这里加上打印语句 ---
|
||||
print(f"DEBUG: Removing shadow sources. Start Slot: {first_source_slot}, Count: {num_sources}")
|
||||
|
||||
# 先发送GPU移除命令(在清理之前)
|
||||
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
|
||||
cmd_remove.push_int(first_source_slot) # 使用保存的slot值
|
||||
|
||||
@ -208,6 +208,7 @@ class RenderPipeline(RPObject):
|
||||
def remove_light(self, light):
|
||||
""" Removes a previously attached light, check out the LightManager
|
||||
remove_light documentation for further information. """
|
||||
print(f'222222222222222222222222222,{light.casts_shadows}')
|
||||
self.light_mgr.remove_light(light)
|
||||
|
||||
def load_ies_profile(self, filename):
|
||||
|
||||
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.
BIN
Resources/models/Women_1.glb
Normal file
BIN
Resources/models/Women_1.glb
Normal file
Binary file not shown.
BIN
Resources/models/Women_2.glb
Normal file
BIN
Resources/models/Women_2.glb
Normal file
Binary file not shown.
BIN
Resources/models/women_1.glb
Normal file
BIN
Resources/models/women_1.glb
Normal file
Binary file not shown.
34
Start_Run.py
Normal file
34
Start_Run.py
Normal file
@ -0,0 +1,34 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 添加 RenderPipeline 到路径(注意路径名称应与实际目录一致)
|
||||
render_pipeline_path = os.path.join(project_root, "RenderPipeline")
|
||||
sys.path.insert(0, render_pipeline_path)
|
||||
|
||||
# 添加 RenderPipelineFile 路径
|
||||
render_pipeline_file_path = os.path.join(project_root, "RenderPipelineFile")
|
||||
sys.path.insert(0, render_pipeline_file_path)
|
||||
|
||||
# 添加 icons 目录到路径
|
||||
icons_path = os.path.join(project_root, "icons")
|
||||
sys.path.insert(0, icons_path)
|
||||
|
||||
# 现在可以导入并运行主程序
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
# args = "/home/tiger/桌面/Test1"
|
||||
# args = "C:/Users/29381/Desktop/1"
|
||||
print(f'Path is {args}')
|
||||
# 将整个列表转换为字符串(包括方括号)
|
||||
args_str = ''.join(args)
|
||||
|
||||
from main import run
|
||||
if args:
|
||||
run(args_str)
|
||||
# run(args)
|
||||
else:
|
||||
run()
|
||||
@ -1,6 +1,7 @@
|
||||
# 修改后的 InfoPanelManager.py
|
||||
from xml.sax.handler import property_encoding
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from direct.gui.DirectGui import DirectFrame, DirectLabel
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import TextNode, Vec4, NodePath
|
||||
@ -144,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,
|
||||
@ -182,12 +183,60 @@ class InfoPanelManager(DirectObject):
|
||||
panel_node.setTag("gui_type", "info_panel")
|
||||
panel_node.setTag("panel_id", panel_id)
|
||||
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
|
||||
panel_node.setTag("is_gui_element",'1')
|
||||
panel_node.setTag("tree_item_type","INFO_PANEL")
|
||||
panel_node.setTag("supports_3d_position_editing","1")
|
||||
|
||||
# 如果有背景图片,保存背景图片路径
|
||||
if bg_image:
|
||||
panel_node.setTag("bg_image_path", bg_image)
|
||||
|
||||
|
||||
if not visible:
|
||||
panel_node.hide()
|
||||
|
||||
# 将面板添加到场景树
|
||||
#self._addPanelToSceneTree(panel_node, panel_id)
|
||||
|
||||
return panel_node
|
||||
|
||||
def _addPanelToSceneTree(self, panel_node, panel_id):
|
||||
"""
|
||||
将信息面板添加到场景树中
|
||||
"""
|
||||
try:
|
||||
# 获取树形控件
|
||||
if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
|
||||
tree_widget = self.world.interface_manager.treeWidget
|
||||
if tree_widget:
|
||||
# 查找根节点项
|
||||
root_item = None
|
||||
for i in range(tree_widget.topLevelItemCount()):
|
||||
item = tree_widget.topLevelItem(i)
|
||||
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
|
||||
root_item = item
|
||||
break
|
||||
|
||||
if root_item:
|
||||
# 使用现有的 add_node_to_tree_widget 方法添加节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL")
|
||||
if qt_item:
|
||||
print(f"✅ 信息面板 {panel_id} 已添加到场景树")
|
||||
# 选中创建的节点
|
||||
tree_widget.setCurrentItem(qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(panel_node, qt_item)
|
||||
else:
|
||||
print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败")
|
||||
else:
|
||||
print("⚠️ 未找到场景树根节点,无法添加信息面板")
|
||||
else:
|
||||
print("⚠️ 无法访问场景树控件,信息面板未添加到场景树")
|
||||
except Exception as e:
|
||||
print(f"❌ 添加信息面板到场景树时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def setPanelBackgroundImage(self, panel_id, image_path):
|
||||
"""
|
||||
为指定面板设置背景图片
|
||||
@ -317,11 +366,10 @@ class InfoPanelManager(DirectObject):
|
||||
|
||||
return True
|
||||
|
||||
# 更新 registerDataSource 方法以更好地处理不同类型面板
|
||||
def registerDataSource(self, panel_id, data_callback, update_interval=1.0):
|
||||
"""
|
||||
注册数据源,定期更新面板内容
|
||||
data_callback: 返回数据的回调函数
|
||||
update_interval: 更新间隔(秒)
|
||||
注册数据源,定期更新面板内容 - 改进版
|
||||
"""
|
||||
if panel_id not in self.panels:
|
||||
print(f"面板 {panel_id} 不存在")
|
||||
@ -335,7 +383,8 @@ class InfoPanelManager(DirectObject):
|
||||
data_source = {
|
||||
'callback': data_callback,
|
||||
'interval': update_interval,
|
||||
'stop': False
|
||||
'stop': False,
|
||||
'panel_type': '3d' if self._is3DPanel(panel_id) else '2d' if self._is2DPanel(panel_id) else 'unknown'
|
||||
}
|
||||
|
||||
self.data_sources[panel_id] = data_source
|
||||
@ -346,9 +395,10 @@ class InfoPanelManager(DirectObject):
|
||||
|
||||
return True
|
||||
|
||||
# 在 InfoPanelManager 类中修复 _updateDataThread 方法
|
||||
def _updateDataThread(self, panel_id):
|
||||
"""
|
||||
数据更新线程
|
||||
数据更新线程 - 最终修复版
|
||||
"""
|
||||
while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']:
|
||||
try:
|
||||
@ -357,7 +407,18 @@ class InfoPanelManager(DirectObject):
|
||||
|
||||
# 更新面板内容
|
||||
if data and panel_id in self.panels:
|
||||
self.updatePanelContent(panel_id, content=data)
|
||||
panel_type = self.data_sources[panel_id].get('panel_type', 'unknown')
|
||||
|
||||
if panel_type == '2d':
|
||||
self.updatePanelContent(panel_id, content=data)
|
||||
elif panel_type == '3d':
|
||||
self.update3DPanelContent(panel_id, content=data)
|
||||
else:
|
||||
# 尝试自动检测
|
||||
if self._is2DPanel(panel_id):
|
||||
self.updatePanelContent(panel_id, content=data)
|
||||
elif self._is3DPanel(panel_id):
|
||||
self.update3DPanelContent(panel_id, content=data)
|
||||
|
||||
# 等待下次更新
|
||||
interval = self.data_sources[panel_id]['interval']
|
||||
@ -365,8 +426,29 @@ class InfoPanelManager(DirectObject):
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新面板 {panel_id} 数据时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
time.sleep(1.0) # 出错时等待1秒再重试
|
||||
|
||||
# 在 InfoPanelManager 类中添加以下方法
|
||||
def _is3DPanel(self, panel_id):
|
||||
"""
|
||||
判断面板是否为3D面板
|
||||
"""
|
||||
if panel_id not in self.panels:
|
||||
return False
|
||||
panel_data = self.panels[panel_id]
|
||||
return 'content_node' in panel_data and 'content_label' not in panel_data
|
||||
|
||||
def _is2DPanel(self, panel_id):
|
||||
"""
|
||||
判断面板是否为2D面板
|
||||
"""
|
||||
if panel_id not in self.panels:
|
||||
return False
|
||||
panel_data = self.panels[panel_id]
|
||||
return 'content_label' in panel_data and 'content_node' not in panel_data
|
||||
|
||||
def unregisterDataSource(self, panel_id):
|
||||
"""
|
||||
注销数据源
|
||||
@ -447,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']:
|
||||
@ -493,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:
|
||||
@ -657,10 +738,247 @@ class InfoPanelManager(DirectObject):
|
||||
|
||||
return True
|
||||
|
||||
def create3DInfoPanel(self, panel_id, position=(0, 0, 0), size=(1.0, 0.6),
|
||||
bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0),
|
||||
title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0),
|
||||
visible=True, font=None, bg_image=None):
|
||||
"""
|
||||
创建简化版3D信息面板 - 只显示文字,无面板背景,避免闪烁
|
||||
"""
|
||||
# 如果面板已存在,先移除它
|
||||
if panel_id in self.panels:
|
||||
self.removePanel(panel_id)
|
||||
|
||||
# 确保父节点存在
|
||||
parent_node = self.parent if self.parent else self.world.render
|
||||
|
||||
# 根据面板ID确定标题和内容
|
||||
title, content = self._getPanelContent(panel_id)
|
||||
|
||||
# 创建主节点,便于统一管理
|
||||
panel_node = parent_node.attachNewNode(f"info_panel_3d_{panel_id}")
|
||||
panel_node.setPos(position[0], position[1], position[2])
|
||||
|
||||
# 直接创建文字节点,不创建面板背景和边框
|
||||
from panda3d.core import TextNode
|
||||
|
||||
# 创建标题文本
|
||||
title_text_node = TextNode(f'title_{panel_id}')
|
||||
title_text_node.setText(title)
|
||||
title_text_node.setTextColor(*title_color)
|
||||
title_text_node.setAlign(TextNode.ACenter)
|
||||
if font:
|
||||
title_text_node.setFont(font)
|
||||
|
||||
title_text = panel_node.attachNewNode(title_text_node)
|
||||
title_text.setScale(0.06)
|
||||
title_text.setPos(0, 0, size[1] / 4) # 将标题放在上方
|
||||
|
||||
# 创建内容文本
|
||||
content_text_node = TextNode(f'content_{panel_id}')
|
||||
content_text_node.setText(content)
|
||||
content_text_node.setTextColor(*content_color)
|
||||
content_text_node.setAlign(TextNode.ALeft)
|
||||
content_text_node.setWordwrap(size[0] * 2) # 根据面板宽度设置换行
|
||||
if font:
|
||||
content_text_node.setFont(font)
|
||||
|
||||
content_text = panel_node.attachNewNode(content_text_node)
|
||||
content_text.setScale(0.045)
|
||||
content_text.setPos(-size[0] / 2, 0, size[1] / 4 - 0.1) # 将内容放在标题下方
|
||||
|
||||
# 保存引用
|
||||
self.panels[panel_id] = {
|
||||
'node': panel_node,
|
||||
'title_text': title_text,
|
||||
'content_text': content_text,
|
||||
'title_node': title_text_node,
|
||||
'content_node': content_text_node,
|
||||
'properties': {
|
||||
'size': size,
|
||||
'position': position,
|
||||
'title_color': title_color,
|
||||
'content_color': content_color,
|
||||
'font': font
|
||||
}
|
||||
}
|
||||
|
||||
# 设置GUI类型标记和支持3D编辑的标记
|
||||
panel_node.setTag("gui_type", "info_panel_3d")
|
||||
panel_node.setTag("panel_id", panel_id)
|
||||
panel_node.setTag("is_gui_element", "1") # 添加此标记确保节点被识别为GUI元素
|
||||
panel_node.setTag("is_scene_element", "1") # 添加此标记确保节点被识别为场景元素
|
||||
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
|
||||
panel_node.setTag("tree_item_type", "INFO_PANEL_3D") # 添加树节点类型标记
|
||||
|
||||
# 如果有背景图片,保存背景图片路径
|
||||
if bg_image:
|
||||
panel_node.setTag("bg_image_path", bg_image)
|
||||
|
||||
if not visible:
|
||||
panel_node.hide()
|
||||
|
||||
# 将面板添加到场景树
|
||||
#self._addPanelToSceneTree(panel_node, panel_id)
|
||||
|
||||
return panel_node
|
||||
|
||||
def update3DPanelContent(self, panel_id, title=None, content=None):
|
||||
"""
|
||||
更新3D面板内容
|
||||
"""
|
||||
if panel_id not in self.panels:
|
||||
print(f"面板 {panel_id} 不存在")
|
||||
return False
|
||||
|
||||
panel_data = self.panels[panel_id]
|
||||
|
||||
if title is not None and 'title_node' in panel_data:
|
||||
panel_data['title_node'].setText(title)
|
||||
|
||||
if content is not None and 'content_node' in panel_data:
|
||||
panel_data['content_node'].setText(content)
|
||||
|
||||
return True
|
||||
|
||||
def update3DPanelProperties(self, panel_id, **properties):
|
||||
"""
|
||||
更新3D面板属性
|
||||
"""
|
||||
if panel_id not in self.panels:
|
||||
print(f"面板 {panel_id} 不存在")
|
||||
return False
|
||||
|
||||
panel_data = self.panels[panel_id]
|
||||
props = panel_data['properties']
|
||||
|
||||
# 更新位置
|
||||
if 'position' in properties:
|
||||
pos = properties['position']
|
||||
panel_data['node'].setPos(pos[0], pos[1], pos[2])
|
||||
props['position'] = pos
|
||||
|
||||
# 更新大小
|
||||
if 'size' in properties:
|
||||
size = properties['size']
|
||||
props['size'] = size
|
||||
|
||||
# 由于3D面板使用CardMaker创建,需要重新创建几何体
|
||||
print("注意:3D面板大小调整需要重新创建面板几何体")
|
||||
|
||||
# 更新背景颜色
|
||||
if 'bg_color' in properties:
|
||||
bg_color = properties['bg_color']
|
||||
if 'panel_bg' in panel_data:
|
||||
from panda3d.core import Material
|
||||
material = Material()
|
||||
material.setDiffuse(Vec4(*bg_color))
|
||||
material.setAmbient(Vec4(*bg_color[:3], 1.0))
|
||||
panel_data['panel_bg'].setMaterial(material, 1)
|
||||
props['bg_color'] = bg_color
|
||||
|
||||
# 更新边框颜色
|
||||
if 'border_color' in properties:
|
||||
border_color = properties['border_color']
|
||||
from panda3d.core import Material
|
||||
border_mat = Material()
|
||||
border_mat.setDiffuse(Vec4(*border_color))
|
||||
for border in panel_data['borders'].values():
|
||||
border.setMaterial(border_mat, 1)
|
||||
props['border_color'] = border_color
|
||||
|
||||
# 更新标题颜色
|
||||
if 'title_color' in properties:
|
||||
title_color = properties['title_color']
|
||||
if 'title_node' in panel_data:
|
||||
panel_data['title_node'].setTextColor(*title_color)
|
||||
props['title_color'] = title_color
|
||||
|
||||
# 更新内容颜色
|
||||
if 'content_color' in properties:
|
||||
content_color = properties['content_color']
|
||||
if 'content_node' in panel_data:
|
||||
panel_data['content_node'].setTextColor(*content_color)
|
||||
props['content_color'] = content_color
|
||||
|
||||
# 更新标题
|
||||
if 'title' in properties:
|
||||
if 'title_node' in panel_data:
|
||||
panel_data['title_node'].setText(properties['title'])
|
||||
|
||||
# 更新内容
|
||||
if 'content' in properties:
|
||||
if 'content_node' in panel_data:
|
||||
panel_data['content_node'].setText(properties['content'])
|
||||
|
||||
# 更新字体大小
|
||||
if 'title_size' in properties:
|
||||
if 'title_text' in panel_data:
|
||||
panel_data['title_text'].setScale(properties['title_size'])
|
||||
props['title_size'] = properties['title_size']
|
||||
|
||||
if 'content_size' in properties:
|
||||
if 'content_text' in panel_data:
|
||||
panel_data['content_text'].setScale(properties['content_size'])
|
||||
props['content_size'] = properties['content_size']
|
||||
|
||||
return True
|
||||
|
||||
def create3DHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None,
|
||||
position=(0, 0, 0), size=(1.0, 0.6),
|
||||
bg_color=(0.15, 0.15, 0.15, 0.9),
|
||||
border_color=(0.3, 0.3, 0.3, 1.0),
|
||||
title_color=(1.0, 1.0, 1.0, 1.0),
|
||||
content_color=(0.9, 0.9, 0.9, 1.0),
|
||||
update_interval=30.0, font=None):
|
||||
"""
|
||||
创建3D HTTP信息面板
|
||||
"""
|
||||
# 创建面板
|
||||
domain = urlparse(url).netloc or url[:30]
|
||||
title = f"HTTP数据: {domain}"
|
||||
|
||||
panel_node = self.create3DInfoPanel(
|
||||
panel_id=panel_id,
|
||||
position=position,
|
||||
size=size,
|
||||
bg_color=bg_color,
|
||||
border_color=border_color,
|
||||
title_color=title_color,
|
||||
content_color=content_color,
|
||||
font=font
|
||||
)
|
||||
|
||||
# 更新标题
|
||||
self.update3DPanelContent(panel_id, title=title)
|
||||
|
||||
# 立即获取并显示数据
|
||||
content = fetchHTTPData(url, method, headers, data)
|
||||
self.update3DPanelContent(panel_id, content=content)
|
||||
|
||||
# 注册数据源,定期更新
|
||||
def http_data_callback():
|
||||
return fetchHTTPData(url, method, headers, data)
|
||||
|
||||
self.registerDataSource(panel_id, http_data_callback, update_interval)
|
||||
|
||||
# 保存HTTP请求信息,便于后续更新
|
||||
if panel_id not in self.data_sources:
|
||||
self.data_sources[panel_id] = {}
|
||||
self.data_sources[panel_id]['http_info'] = {
|
||||
'url': url,
|
||||
'method': method,
|
||||
'headers': headers,
|
||||
'data': data
|
||||
}
|
||||
|
||||
return panel_node
|
||||
|
||||
# 在 add_methods_to_property_panel 函数中添加以下方法
|
||||
|
||||
def add_methods_to_property_panel(property_panel_instance):
|
||||
# ... (原有代码保持不变)
|
||||
import types
|
||||
|
||||
def createHTTPInfoPanel(self, url, panel_id="http_info", method="GET", headers=None, data=None,
|
||||
position=(0.8, 0.0), size=(0.4, 0.3), update_interval=30.0):
|
||||
@ -721,12 +1039,233 @@ class InfoPanelManager(DirectObject):
|
||||
print(f"✗ 更新HTTP信息面板失败: {e}")
|
||||
return False
|
||||
|
||||
def create3DRealtimeDataPanel(self, data_callback=None, update_interval=1.0):
|
||||
"""创建3D实时数据面板"""
|
||||
try:
|
||||
# 确保父节点已设置
|
||||
if self.info_panel_manager.parent is None and hasattr(self, 'world'):
|
||||
self.info_panel_manager.setParent(self.world.render)
|
||||
|
||||
# 创建3D实时数据面板
|
||||
panel_node = self.info_panel_manager.create3DInfoPanel(
|
||||
panel_id="realtime_data_3d",
|
||||
position=(0, 0, 0),
|
||||
size=(0.35, 0.3),
|
||||
bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景
|
||||
border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框
|
||||
title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题
|
||||
content_color=(0.95, 0.95, 0.95, 1.0)
|
||||
)
|
||||
|
||||
# 设置标签
|
||||
panel_node.setTag("element_type", "info_panel_3d")
|
||||
panel_node.setTag("is_scene_element", "1")
|
||||
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
|
||||
|
||||
# 如果提供了数据回调函数,则注册数据源
|
||||
if data_callback:
|
||||
# 立即显示初始数据
|
||||
initial_data = data_callback()
|
||||
self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=initial_data)
|
||||
|
||||
# 注册数据源
|
||||
self.info_panel_manager.registerDataSource("realtime_data_3d", data_callback, update_interval)
|
||||
else:
|
||||
# 使用默认数据
|
||||
default_data = "等待数据..."
|
||||
self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=default_data)
|
||||
|
||||
print("✓ 已创建3D实时数据面板")
|
||||
|
||||
return panel_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 创建3D实时数据面板失败: {e}")
|
||||
return None
|
||||
|
||||
def create3DModelInfoPanel(self, model):
|
||||
"""为模型创建3D信息面板"""
|
||||
try:
|
||||
# 确保父节点已设置
|
||||
if self.info_panel_manager.parent is None and hasattr(self, 'world'):
|
||||
self.info_panel_manager.setParent(self.world.render)
|
||||
|
||||
# 获取模型信息
|
||||
model_name = model.getName() if hasattr(model, 'getName') else 'Unknown'
|
||||
num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0
|
||||
|
||||
# 创建面板内容
|
||||
content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}"
|
||||
|
||||
# 创建或更新面板
|
||||
panel_node = self.info_panel_manager.create3DInfoPanel(
|
||||
panel_id="model_info_3d",
|
||||
position=(2, 0, 0), # 默认放在模型旁边
|
||||
size=(0.35, 0.25),
|
||||
bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景
|
||||
border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框
|
||||
title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题
|
||||
content_color=(0.95, 0.95, 0.95, 1.0)
|
||||
)
|
||||
|
||||
# 更新面板内容为模型特定信息
|
||||
self.info_panel_manager.update3DPanelContent("model_info_3d",
|
||||
title="模型信息",
|
||||
content=content)
|
||||
|
||||
# 设置标签
|
||||
panel_node.setTag("element_type", "info_panel_3d")
|
||||
panel_node.setTag("is_scene_element", "1")
|
||||
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
|
||||
|
||||
print(f"✓ 已创建3D模型信息面板: {model_name}")
|
||||
|
||||
return panel_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 创建3D模型信息面板失败: {e}")
|
||||
return None
|
||||
|
||||
# 在绑定方法的部分添加3D面板方法
|
||||
property_panel_instance.create3DRealtimeDataPanel = types.MethodType(create3DRealtimeDataPanel,
|
||||
property_panel_instance)
|
||||
property_panel_instance.create3DModelInfoPanel = types.MethodType(create3DModelInfoPanel,
|
||||
property_panel_instance)
|
||||
|
||||
# 将新方法绑定到实例
|
||||
import types
|
||||
# ... (原有绑定保持不变)
|
||||
property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance)
|
||||
property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance)
|
||||
|
||||
def serializePanelData(self, panel_id):
|
||||
"""序列化面板数据用于保存"""
|
||||
if panel_id not in self.panels:
|
||||
return None
|
||||
|
||||
panel_data = self.panels[panel_id]
|
||||
props = panel_data['properties']
|
||||
|
||||
# 获取面板类型
|
||||
panel_type = "2d"
|
||||
if panel_data['node'].hasTag("gui_type"):
|
||||
gui_type = panel_data['node'].getTag("gui_type")
|
||||
if "3d" in gui_type.lower():
|
||||
panel_type = "3d"
|
||||
|
||||
# 构建序列化数据
|
||||
serialized_data = {
|
||||
'panel_id': panel_id,
|
||||
'panel_type': panel_type,
|
||||
'position': props.get('position', (0, 0) if panel_type == "2d" else (0, 0, 0)),
|
||||
'size': props.get('size', (1.0, 0.6)),
|
||||
'bg_color': props.get('bg_color', (0.15, 0.15, 0.15, 0.9)),
|
||||
'border_color': props.get('border_color', (0.3, 0.3, 0.3, 1.0)),
|
||||
'title_color': props.get('title_color', (1.0, 1.0, 1.0, 1.0)),
|
||||
'content_color': props.get('content_color', (0.9, 0.9, 0.9, 1.0)),
|
||||
'title': panel_data['title_label'].getText() if 'title_label' in panel_data else "信息面板",
|
||||
'content': panel_data[
|
||||
'content_label'].getText() if 'content_label' in panel_data else "" if 'content_node' not in panel_data else
|
||||
panel_data['content_node'].getText(),
|
||||
'font_path': props.get('font', None),
|
||||
'bg_image': props.get('bg_image', None),
|
||||
'visible': not panel_data['node'].isHidden()
|
||||
}
|
||||
|
||||
# 添加HTTP面板特有数据
|
||||
if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]:
|
||||
serialized_data['http_info'] = self.data_sources[panel_id]['http_info']
|
||||
|
||||
return serialized_data
|
||||
|
||||
def getAllPanelData(self):
|
||||
"""获取所有面板的序列化数据"""
|
||||
panel_data_list = []
|
||||
for panel_id in self.panels:
|
||||
data = self.serializePanelData(panel_id)
|
||||
if data:
|
||||
panel_data_list.append(data)
|
||||
return panel_data_list
|
||||
|
||||
def recreatePanelFromData(self, panel_data):
|
||||
"""从序列化数据重新创建面板"""
|
||||
try:
|
||||
panel_id = panel_data['panel_id']
|
||||
panel_type = panel_data['panel_type']
|
||||
position = panel_data['position']
|
||||
size = panel_data['size']
|
||||
|
||||
# 重建面板
|
||||
if panel_type == "3d":
|
||||
panel_node = self.create3DInfoPanel(
|
||||
panel_id=panel_id,
|
||||
position=position,
|
||||
size=size,
|
||||
bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)),
|
||||
border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)),
|
||||
title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)),
|
||||
content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)),
|
||||
visible=panel_data.get('visible', True),
|
||||
font=panel_data.get('font_path', None),
|
||||
bg_image=panel_data.get('bg_image', None)
|
||||
)
|
||||
|
||||
# 更新内容
|
||||
self.update3DPanelContent(
|
||||
panel_id,
|
||||
title=panel_data.get('title', '信息面板'),
|
||||
content=panel_data.get('content', '')
|
||||
)
|
||||
else:
|
||||
panel_node = self.createInfoPanel(
|
||||
panel_id=panel_id,
|
||||
position=(position[0], position[1]) if len(position) >= 2 else (0, 0),
|
||||
size=size,
|
||||
bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)),
|
||||
border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)),
|
||||
title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)),
|
||||
content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)),
|
||||
visible=panel_data.get('visible', True),
|
||||
font=panel_data.get('font_path', None),
|
||||
bg_image=panel_data.get('bg_image', None)
|
||||
)
|
||||
|
||||
# 更新内容
|
||||
self.updatePanelContent(
|
||||
panel_id,
|
||||
title=panel_data.get('title', '信息面板'),
|
||||
content=panel_data.get('content', '')
|
||||
)
|
||||
|
||||
# 设置标签
|
||||
if panel_node:
|
||||
panel_node.setTag("element_type", "info_panel")
|
||||
panel_node.setTag("is_scene_element", "1")
|
||||
panel_node.setTag("supports_3d_position_editing", "1")
|
||||
|
||||
# 如果是HTTP面板,重新注册数据源
|
||||
if 'http_info' in panel_data:
|
||||
http_info = panel_data['http_info']
|
||||
self.createHTTPInfoPanel(
|
||||
panel_id=panel_id,
|
||||
url=http_info['url'],
|
||||
method=http_info.get('method', 'GET'),
|
||||
headers=http_info.get('headers', None),
|
||||
data=http_info.get('data', None),
|
||||
position=position,
|
||||
size=size,
|
||||
update_interval=self.data_sources.get(panel_id, {}).get('interval',
|
||||
30.0) if panel_id in self.data_sources else 30.0
|
||||
)
|
||||
|
||||
print(f"✓ 信息面板 {panel_id} 已重建")
|
||||
return panel_node
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 重建信息面板 {panel_data.get('panel_id', 'unknown')} 失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
# 示例数据源函数
|
||||
def getRealtimeData():
|
||||
@ -777,6 +1316,7 @@ def add_methods_to_property_panel(property_panel_instance):
|
||||
"""
|
||||
为 property_panel 实例添加 DirectGUI 信息面板支持
|
||||
"""
|
||||
import types
|
||||
|
||||
# 添加信息面板管理器作为类属性
|
||||
if not hasattr(property_panel_instance, 'info_panel_manager'):
|
||||
@ -982,7 +1522,6 @@ def add_methods_to_property_panel(property_panel_instance):
|
||||
property_panel_instance.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance)
|
||||
property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance)
|
||||
|
||||
|
||||
def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5):
|
||||
"""
|
||||
获取HTTP数据的通用函数
|
||||
|
||||
@ -309,40 +309,81 @@ class CollisionManager:
|
||||
CollisionPlane, CollisionPolygon, Plane, Vec3
|
||||
)
|
||||
|
||||
bounds = model.getBounds()
|
||||
if bounds.isEmpty():
|
||||
# 获取考虑变换后的实际尺寸和中心
|
||||
transformed_info = self._getTransformedModelInfo(model)
|
||||
if not transformed_info:
|
||||
# 默认小球体
|
||||
return CollisionSphere(Point3(0, 0, 0), 1.0)
|
||||
|
||||
center = bounds.getCenter()
|
||||
radius = bounds.getRadius()
|
||||
center = transformed_info['center']
|
||||
radius = transformed_info['radius']
|
||||
actual_size = transformed_info['size']
|
||||
scale_factor = transformed_info['scale_factor']
|
||||
|
||||
# 自动选择最适合的形状
|
||||
if shape_type == 'auto':
|
||||
shape_type = self._determineOptimalShape(model, bounds)
|
||||
shape_type = self._determineOptimalShape(model, transformed_info)
|
||||
|
||||
if shape_type == 'sphere':
|
||||
return CollisionSphere(center, kwargs.get('radius', radius))
|
||||
# 优化球形碰撞体
|
||||
sphere_radius = kwargs.get('radius', radius)
|
||||
|
||||
# 支持位置偏移
|
||||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||||
sphere_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
|
||||
|
||||
return CollisionSphere(sphere_center, sphere_radius)
|
||||
|
||||
elif shape_type == 'box':
|
||||
# 创建包围盒
|
||||
min_point = bounds.getMin()
|
||||
max_point = bounds.getMax()
|
||||
# 优化盒型碰撞体 - 更精确的尺寸和位置控制(考虑缩放)
|
||||
# 获取自定义尺寸,如果没有提供则使用变换后的实际尺寸
|
||||
width = kwargs.get('width', actual_size.x)
|
||||
length = kwargs.get('length', actual_size.y)
|
||||
height = kwargs.get('height', actual_size.z)
|
||||
|
||||
# 支持位置偏移
|
||||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||||
box_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
|
||||
|
||||
# 计算盒子的最小和最大点(基于偏移后的中心)
|
||||
half_width = width / 2
|
||||
half_length = length / 2
|
||||
half_height = height / 2
|
||||
|
||||
min_point = Point3(box_center.x - half_width, box_center.y - half_length, box_center.z - half_height)
|
||||
max_point = Point3(box_center.x + half_width, box_center.y + half_length, box_center.z + half_height)
|
||||
return CollisionBox(min_point, max_point)
|
||||
|
||||
elif shape_type == 'capsule':
|
||||
# 创建胶囊体(适合角色)
|
||||
height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z))
|
||||
radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3))
|
||||
point_a = Point3(center.x, center.y, bounds.getMin().z + radius)
|
||||
point_b = Point3(center.x, center.y, bounds.getMax().z - radius)
|
||||
return CollisionCapsule(point_a, point_b, radius)
|
||||
# 优化胶囊体碰撞 - 更合理的比例和位置控制(考虑缩放)
|
||||
# 使用变换后的实际高度,或自定义高度
|
||||
custom_height = kwargs.get('height', actual_size.z)
|
||||
|
||||
# 更合理的半径计算:基于变换后模型宽度的平均值
|
||||
default_radius = min(actual_size.x, actual_size.y) / 2.5 # 稍微小于模型的宽度
|
||||
custom_radius = kwargs.get('radius', min(default_radius, custom_height * 0.4))
|
||||
|
||||
# 支持位置偏移
|
||||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||||
capsule_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
|
||||
|
||||
# 计算胶囊体的两个端点(确保半径不会超出高度)
|
||||
effective_height = max(custom_height, custom_radius * 2.1) # 确保高度至少是半径的2倍多一点
|
||||
point_a = Point3(capsule_center.x, capsule_center.y, capsule_center.z - effective_height/2 + custom_radius)
|
||||
point_b = Point3(capsule_center.x, capsule_center.y, capsule_center.z + effective_height/2 - custom_radius)
|
||||
return CollisionCapsule(point_a, point_b, custom_radius)
|
||||
|
||||
elif shape_type == 'plane':
|
||||
# 创建平面(适合地面、墙面)
|
||||
# 优化平面碰撞 - 支持位置偏移和更灵活的法向量
|
||||
normal = kwargs.get('normal', Vec3(0, 0, 1))
|
||||
point = kwargs.get('point', center)
|
||||
plane = Plane(normal, point)
|
||||
|
||||
# 支持位置偏移
|
||||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||||
plane_point = kwargs.get('point', Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z))
|
||||
|
||||
# 标准化法向量
|
||||
normal.normalize()
|
||||
plane = Plane(normal, plane_point)
|
||||
return CollisionPlane(plane)
|
||||
|
||||
elif shape_type == 'polygon':
|
||||
@ -364,13 +405,13 @@ class CollisionManager:
|
||||
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
|
||||
return collision_poly
|
||||
else:
|
||||
print("⚠️ 多边形至少需要3个顶点,回退到球体")
|
||||
#print("⚠️ 多边形至少需要3个顶点,回退到球体")
|
||||
return CollisionSphere(center, radius)
|
||||
|
||||
def _determineOptimalShape(self, model, bounds):
|
||||
def _determineOptimalShape(self, model, transformed_info):
|
||||
"""根据模型特征自动确定最适合的碰撞体形状"""
|
||||
# 获取模型尺寸比例
|
||||
size = bounds.getMax() - bounds.getMin()
|
||||
# 获取变换后的模型尺寸比例
|
||||
size = transformed_info['size']
|
||||
max_dim = max(size.x, size.y, size.z)
|
||||
min_dim = min(size.x, size.y, size.z)
|
||||
|
||||
@ -399,6 +440,87 @@ class CollisionManager:
|
||||
else: # 其他用包围盒
|
||||
return 'box'
|
||||
|
||||
def _getTransformedModelInfo(self, model):
|
||||
"""获取考虑变换后的模型信息
|
||||
|
||||
Args:
|
||||
model: 模型节点
|
||||
|
||||
Returns:
|
||||
dict: 包含变换后的尺寸、中心、半径等信息
|
||||
"""
|
||||
try:
|
||||
# 获取原始包围盒
|
||||
bounds = model.getBounds()
|
||||
if bounds.isEmpty():
|
||||
return None
|
||||
|
||||
# 获取模型的变换矩阵
|
||||
transform = model.getTransform()
|
||||
scale = model.getScale()
|
||||
|
||||
# 计算缩放因子(取三轴缩放的平均值)
|
||||
scale_factor = (abs(scale.x) + abs(scale.y) + abs(scale.z)) / 3.0
|
||||
|
||||
# 获取原始尺寸
|
||||
original_size = bounds.getMax() - bounds.getMin()
|
||||
|
||||
# 应用缩放到尺寸
|
||||
actual_size = Vec3(
|
||||
original_size.x * abs(scale.x),
|
||||
original_size.y * abs(scale.y),
|
||||
original_size.z * abs(scale.z)
|
||||
)
|
||||
|
||||
# 获取变换后的中心点(在世界坐标系中)
|
||||
original_center = bounds.getCenter()
|
||||
if hasattr(model, 'getPos'):
|
||||
# 模型在世界坐标系中的位置
|
||||
world_center = model.getPos(model.getParent() if model.getParent() else model)
|
||||
center = Point3(world_center.x, world_center.y, world_center.z)
|
||||
else:
|
||||
# 如果无法获取世界位置,使用原始中心
|
||||
center = original_center
|
||||
|
||||
# 计算变换后的半径(考虑缩放)
|
||||
original_radius = bounds.getRadius()
|
||||
transformed_radius = original_radius * scale_factor
|
||||
|
||||
# 调试信息
|
||||
print(f"🔍 模型 {model.getName()} 变换信息:")
|
||||
print(f" 原始尺寸: {original_size}")
|
||||
print(f" 缩放因子: {scale}")
|
||||
print(f" 变换后尺寸: {actual_size}")
|
||||
print(f" 变换后半径: {transformed_radius:.2f}")
|
||||
|
||||
return {
|
||||
'center': center,
|
||||
'radius': transformed_radius,
|
||||
'size': actual_size,
|
||||
'scale_factor': scale_factor,
|
||||
'original_size': original_size,
|
||||
'scale': scale,
|
||||
'transform': transform
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 获取模型变换信息失败: {e}")
|
||||
# 回退到原始包围盒
|
||||
bounds = model.getBounds()
|
||||
if bounds.isEmpty():
|
||||
return None
|
||||
|
||||
original_size = bounds.getMax() - bounds.getMin()
|
||||
return {
|
||||
'center': bounds.getCenter(),
|
||||
'radius': bounds.getRadius(),
|
||||
'size': original_size,
|
||||
'scale_factor': 1.0,
|
||||
'original_size': original_size,
|
||||
'scale': Vec3(1, 1, 1),
|
||||
'transform': None
|
||||
}
|
||||
|
||||
def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
|
||||
"""创建鼠标射线"""
|
||||
# 组合掩码
|
||||
|
||||
@ -167,7 +167,7 @@ class EventHandler:
|
||||
picker.addCollider(pickerNP, queue)
|
||||
picker.traverse(self.world.render)
|
||||
|
||||
print(f"碰撞检测结果数量: {queue.getNumEntries()}")
|
||||
#print(f"碰撞检测结果数量: {queue.getNumEntries()}")
|
||||
|
||||
# 射线检测结果处理
|
||||
hitPos = None
|
||||
@ -184,13 +184,13 @@ class EventHandler:
|
||||
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
|
||||
|
||||
# 优先检查是否点击了坐标轴
|
||||
print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
|
||||
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
|
||||
if self.world.selection.gizmo:
|
||||
print("准备检查坐标轴点击...")
|
||||
#print("准备检查坐标轴点击...")
|
||||
try:
|
||||
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
|
||||
if gizmoAxis:
|
||||
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
|
||||
#print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
|
||||
# 开始坐标轴拖拽
|
||||
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
|
||||
pickerNP.removeNode()
|
||||
@ -203,16 +203,16 @@ class EventHandler:
|
||||
traceback.print_exc()
|
||||
print("继续处理模型选择...")
|
||||
|
||||
print("继续处理碰撞结果...")
|
||||
#print("继续处理碰撞结果...")
|
||||
|
||||
if hitPos and hitNode:
|
||||
print(f"✓ 检测到碰撞,开始处理点击事件")
|
||||
print(f"GUI编辑模式: {self.world.guiEditMode}")
|
||||
print(f"当前工具: {self.world.currentTool}")
|
||||
#print(f"✓ 检测到碰撞,开始处理点击事件")
|
||||
#print(f"GUI编辑模式: {self.world.guiEditMode}")
|
||||
#print(f"当前工具: {self.world.currentTool}")
|
||||
|
||||
# 处理GUI编辑模式
|
||||
if self.world.guiEditMode:
|
||||
print("处理GUI编辑模式点击")
|
||||
#print("处理GUI编辑模式点击")
|
||||
# 检查是否点击了GUI元素
|
||||
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
|
||||
if clickedGUI:
|
||||
@ -411,103 +411,6 @@ class EventHandler:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def mousePressEventRight(self,evt):
|
||||
"""处理鼠标右键按下事件"""
|
||||
print(f"当前工具: {self.world.currentTool}")
|
||||
|
||||
# 检查是否是地形编辑模式
|
||||
if self.world.currentTool == "地形编辑":
|
||||
self._handleTerrainEdit(evt, "subtract") # 降低地形
|
||||
return
|
||||
|
||||
# 其他右键处理逻辑可以在这里添加
|
||||
print("鼠标右键事件处理")
|
||||
|
||||
def _handleTerrainEdit(self,evt,operation):
|
||||
try:
|
||||
x = evt.get('x',0)
|
||||
y = evt.get('y',0)
|
||||
|
||||
winWidth,winHeight = self.world.getWindowSize()
|
||||
|
||||
mx = 2.0 * x/float(winWidth) - 1.0
|
||||
my = 1.0 -2.0*y/float(winHeight)
|
||||
|
||||
nearPoint = Point3()
|
||||
farPoint = Point3()
|
||||
self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint)
|
||||
|
||||
worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint)
|
||||
worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint)
|
||||
|
||||
picker = CollisionTraverser()
|
||||
queue = CollisionHandlerQueue()
|
||||
|
||||
pickerNode = CollisionNode('terrain_edit_ray')
|
||||
pickerNP = self.world.cam.attachNewNode(pickerNode)
|
||||
|
||||
from panda3d.core import BitMask32
|
||||
pickerNode.setFromCollideMask(BitMask32.allOn()) # 检查所有碰撞
|
||||
|
||||
# 使用相机坐标系的点创建射线
|
||||
direction = farPoint - nearPoint
|
||||
direction.normalize()
|
||||
pickerNode.addSolid(CollisionRay(nearPoint, direction))
|
||||
|
||||
picker.addCollider(pickerNP, queue)
|
||||
picker.traverse(self.world.render)
|
||||
print(f"地形碰撞检测结果数量: {queue.getNumEntries()}")
|
||||
|
||||
# 射线检测结果处理
|
||||
hitPos = None
|
||||
hitNode = None
|
||||
|
||||
if queue.getNumEntries() > 0:
|
||||
# 遍历所有碰撞结果,找到地形节点
|
||||
for i in range(queue.getNumEntries()):
|
||||
entry = queue.getEntry(i)
|
||||
collided_node = entry.getIntoNodePath()
|
||||
print(f"碰撞到节点: {collided_node.getName()}")
|
||||
|
||||
# 检查是否是地形节点
|
||||
for terrain_info in self.world.terrain_manager.terrains:
|
||||
terrain_node = terrain_info['node']
|
||||
if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node):
|
||||
hitPos = entry.getSurfacePoint(self.world.render)
|
||||
hitNode = collided_node
|
||||
print(f"找到地形节点: {terrain_node.getName()}")
|
||||
|
||||
# 修改地形高度
|
||||
x_pos, y_pos = hitPos.getX(), hitPos.getY()
|
||||
success = self.world.modifyTerrainHeight(
|
||||
terrain_info, x_pos, y_pos, radius=3.0, strength=0.3, operation=operation)
|
||||
|
||||
if success:
|
||||
print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})")
|
||||
# 显示射线
|
||||
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
|
||||
else:
|
||||
print("✗ 地形编辑失败")
|
||||
break
|
||||
|
||||
if hitPos:
|
||||
break
|
||||
|
||||
if not hitPos:
|
||||
print("没有检测到地形碰撞")
|
||||
# 显示射线(无碰撞)
|
||||
self.showClickRay(worldNearPoint, worldFarPoint)
|
||||
|
||||
# 清理碰撞检测节点
|
||||
pickerNP.removeNode()
|
||||
print("地形编辑处理完成")
|
||||
|
||||
except Exception as e:
|
||||
print(f"地形编辑处理出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _handleSelectionClick(self, hitNode):
|
||||
"""处理选择工具的点击事件"""
|
||||
print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}")
|
||||
@ -541,43 +444,52 @@ class EventHandler:
|
||||
selectedModel = model
|
||||
print(f"找到父模型: {selectedModel.getName()}")
|
||||
break
|
||||
|
||||
|
||||
if selectedModel:
|
||||
break
|
||||
|
||||
current = current.getParent()
|
||||
|
||||
if selectedModel:
|
||||
print(f"✓ 最终选中模型: {selectedModel.getName()}")
|
||||
#print(f"✓ 最终选中模型: {selectedModel.getName()}")
|
||||
self.world.selection.handleMouseClick(selectedModel)
|
||||
|
||||
# 更新选择状态并显示选择框和坐标轴
|
||||
self.world.selection.updateSelection(selectedModel)
|
||||
|
||||
# 在树形控件中查找并选中对应的项
|
||||
if self.world.interface_manager.treeWidget:
|
||||
print("查找树形控件中的对应项...")
|
||||
#print("查找树形控件中的对应项...")
|
||||
root = self.world.interface_manager.treeWidget.invisibleRootItem()
|
||||
foundItem = None
|
||||
|
||||
|
||||
for i in range(root.childCount()):
|
||||
sceneItem = root.child(i)
|
||||
if sceneItem.text(0) == "场景":
|
||||
print(f"在场景节点下查找...")
|
||||
#print(f"在场景节点下查找...")
|
||||
foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem)
|
||||
if foundItem:
|
||||
print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}")
|
||||
try:
|
||||
self.world.interface_manager.treeWidget.itemClicked.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
self.world.interface_manager.treeWidget.setCurrentItem(foundItem)
|
||||
self.world.property_panel.updatePropertyPanel(foundItem)
|
||||
|
||||
self.world.interface_manager.treeWidget.itemClicked.connect(
|
||||
self.world.interface_manager.onTreeItemClicked)
|
||||
else:
|
||||
print("× 在树形控件中没有找到对应项")
|
||||
break
|
||||
|
||||
|
||||
if not foundItem:
|
||||
print("× 没有找到场景节点或对应的树形项")
|
||||
else:
|
||||
print("× 树形控件不存在")
|
||||
else:
|
||||
print("× 没有找到可选择的模型节点")
|
||||
self.world.selection.updateSelection(None)
|
||||
|
||||
def mouseReleaseEventLeft(self, evt):
|
||||
"""处理鼠标左键释放事件"""
|
||||
|
||||
495
core/patrol_system.py
Normal file
495
core/patrol_system.py
Normal file
@ -0,0 +1,495 @@
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
from panda3d.core import Point3, Vec3
|
||||
import math
|
||||
|
||||
|
||||
class PatrolSystem:
|
||||
"""巡检系统类"""
|
||||
|
||||
def __init__(self, world):
|
||||
"""初始化巡检系统
|
||||
|
||||
Args:
|
||||
world: 核心世界对象引用
|
||||
"""
|
||||
self.world = world
|
||||
|
||||
# 巡检状态
|
||||
self.is_patrolling = False
|
||||
self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...]
|
||||
self.current_patrol_index = 0
|
||||
self.patrol_task = None
|
||||
|
||||
# 巡检参数
|
||||
self.patrol_speed = 5.0 # 巡检移动速度(单位/秒)
|
||||
self.patrol_turn_speed = 90.0 # 转向速度(度/秒)
|
||||
self.patrol_wait_timer = 0.0
|
||||
self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back"
|
||||
|
||||
# 相机状态保存
|
||||
self.original_cam_pos = None
|
||||
self.original_cam_hpr = None
|
||||
|
||||
print("✓ 巡检系统初始化完成")
|
||||
|
||||
def add_patrol_point(self, position, heading=None, wait_time=3.0):
|
||||
if heading is None:
|
||||
if self.patrol_points:
|
||||
last_pos = self.patrol_points[-1][0]
|
||||
direction_x = position[0] - last_pos.x
|
||||
direction_y = position[1] - last_pos.y
|
||||
direction_z = position[2] - last_pos.z
|
||||
|
||||
import math
|
||||
h=math.degrees(math.atan2(-direction_x,-direction_y))
|
||||
|
||||
distance_xy = math.sqrt(direction_x**2+direction_y**2)
|
||||
p = math.degrees(math.atan2(direction_z,distance_xy))
|
||||
p = max(-89,min(89,p))
|
||||
|
||||
r=0
|
||||
|
||||
heading = (h,p,r)
|
||||
|
||||
else:
|
||||
# 使用当前相机朝向
|
||||
current_hpr = self.world.cam.getHpr()
|
||||
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
|
||||
|
||||
pos = Point3(position[0], position[1], position[2])
|
||||
hpr = Vec3(heading[0], heading[1], heading[2])
|
||||
|
||||
self.patrol_points.append((pos, hpr, wait_time))
|
||||
print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}秒")
|
||||
|
||||
# 在 PatrolSystem 类中添加以下方法
|
||||
|
||||
def add_auto_heading_patrol_point(self, position, wait_time=3.0):
|
||||
"""添加自动计算朝向的巡检点(朝向路径前进方向)
|
||||
|
||||
Args:
|
||||
position: 相机位置 (x, y, z)
|
||||
wait_time: 在该点停留时间(秒)
|
||||
"""
|
||||
heading = None # 将自动计算朝向
|
||||
|
||||
# 复用原有的 add_patrol_point 方法
|
||||
self.add_patrol_point(position, heading, wait_time)
|
||||
|
||||
def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0):
|
||||
"""添加朝向指定位置的巡检点
|
||||
|
||||
Args:
|
||||
position: 相机位置 (x, y, z)
|
||||
look_at_position: 相机朝向的目标位置 (x, y, z)
|
||||
wait_time: 在该点停留时间(秒)
|
||||
"""
|
||||
import math
|
||||
|
||||
# 计算从当前位置到目标位置的方向向量
|
||||
direction_x = look_at_position[0] - position[0]
|
||||
direction_y = look_at_position[1] - position[1]
|
||||
direction_z = look_at_position[2] - position[2]
|
||||
|
||||
# 计算HPR朝向
|
||||
h = math.degrees(math.atan2(-direction_x, -direction_y))
|
||||
|
||||
distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2)
|
||||
p = math.degrees(math.atan2(direction_z, distance_xy))
|
||||
p = max(-89, min(89, p)) # 限制pitch角度在合理范围内
|
||||
|
||||
r = 0 # roll通常为0
|
||||
|
||||
heading = (h, p, r)
|
||||
self.add_patrol_point(position, heading, wait_time)
|
||||
|
||||
def clear_patrol_points(self):
|
||||
"""清空所有巡检点"""
|
||||
self.patrol_points = []
|
||||
print("✓ 巡检点已清空")
|
||||
|
||||
def set_patrol_speed(self, move_speed, turn_speed=None):
|
||||
"""设置巡检速度
|
||||
|
||||
Args:
|
||||
move_speed: 移动速度(单位/秒)
|
||||
turn_speed: 转向速度(度/秒),如果为None则保持当前值
|
||||
"""
|
||||
self.patrol_speed = move_speed
|
||||
if turn_speed is not None:
|
||||
self.patrol_turn_speed = turn_speed
|
||||
print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}")
|
||||
|
||||
def start_patrol(self):
|
||||
"""开始巡检"""
|
||||
if not self.patrol_points:
|
||||
print("✗ 没有设置巡检点,无法开始巡检")
|
||||
return False
|
||||
|
||||
if self.is_patrolling:
|
||||
print("⚠ 巡检已在进行中")
|
||||
return True
|
||||
|
||||
# 保存当前相机状态
|
||||
self.original_cam_pos = Point3(self.world.cam.getPos())
|
||||
self.original_cam_hpr = Vec3(self.world.cam.getHpr())
|
||||
|
||||
# 重置巡检状态
|
||||
self.current_patrol_index = 0
|
||||
self.patrol_state = "moving"
|
||||
self.patrol_wait_timer = 0.0
|
||||
self.is_patrolling = True
|
||||
|
||||
# 启动巡检任务
|
||||
if self.patrol_task:
|
||||
taskMgr.remove(self.patrol_task)
|
||||
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
|
||||
|
||||
print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点")
|
||||
return True
|
||||
|
||||
def stop_patrol(self):
|
||||
"""停止巡检"""
|
||||
if not self.is_patrolling:
|
||||
print("⚠ 巡检未在进行中")
|
||||
return False
|
||||
|
||||
# 停止巡检任务
|
||||
if self.patrol_task:
|
||||
taskMgr.remove(self.patrol_task)
|
||||
self.patrol_task = None
|
||||
|
||||
self.is_patrolling = False
|
||||
self.patrol_state = "moving"
|
||||
self.patrol_wait_timer = 0.0
|
||||
|
||||
print("✓ 巡检已停止")
|
||||
return True
|
||||
|
||||
def pause_patrol(self):
|
||||
"""暂停巡检"""
|
||||
if not self.is_patrolling:
|
||||
print("⚠ 巡检未在进行中")
|
||||
return False
|
||||
|
||||
if self.patrol_task:
|
||||
taskMgr.remove(self.patrol_task)
|
||||
self.patrol_task = None
|
||||
|
||||
print("✓ 巡检已暂停")
|
||||
return True
|
||||
|
||||
def resume_patrol(self):
|
||||
"""恢复巡检"""
|
||||
if self.is_patrolling:
|
||||
print("⚠ 巡检已在进行中")
|
||||
return False
|
||||
|
||||
if not self.patrol_points:
|
||||
print("✗ 没有设置巡检点")
|
||||
return False
|
||||
|
||||
self.is_patrolling = True
|
||||
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
|
||||
|
||||
print("✓ 巡检已恢复")
|
||||
return True
|
||||
|
||||
def reset_to_original_position(self):
|
||||
"""重置相机到原始位置"""
|
||||
if self.original_cam_pos and self.original_cam_hpr:
|
||||
self.world.cam.setPos(self.original_cam_pos)
|
||||
self.world.cam.setHpr(self.original_cam_hpr)
|
||||
print("✓ 相机已重置到原始位置")
|
||||
return True
|
||||
else:
|
||||
print("✗ 没有保存的原始位置")
|
||||
return False
|
||||
|
||||
def _patrol_task(self, task):
|
||||
"""巡检主任务"""
|
||||
try:
|
||||
if not self.is_patrolling or not self.patrol_points:
|
||||
return task.done
|
||||
|
||||
# 获取当前巡检点
|
||||
current_point = self.patrol_points[self.current_patrol_index]
|
||||
target_pos, target_hpr, wait_time = current_point
|
||||
|
||||
# 根据当前状态执行不同操作
|
||||
if self.patrol_state == "moving":
|
||||
self._handle_moving_state(target_pos)
|
||||
elif self.patrol_state == "turning_to_target":
|
||||
self._handle_turning_to_target_state(target_hpr)
|
||||
elif self.patrol_state == "waiting":
|
||||
self._handle_waiting_state(wait_time)
|
||||
elif self.patrol_state == "turning_back":
|
||||
self._handle_turning_back_state()
|
||||
|
||||
return task.cont
|
||||
|
||||
except Exception as e:
|
||||
print(f"巡检任务出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return task.done
|
||||
|
||||
def _handle_moving_state(self, target_pos):
|
||||
"""处理移动状态"""
|
||||
current_pos = self.world.cam.getPos()
|
||||
distance = (target_pos - current_pos).length()
|
||||
|
||||
if distance < 0.1: # 到达目标点
|
||||
print(f"✓ 到达巡检点 {self.current_patrol_index + 1}")
|
||||
self.patrol_state = "turning_to_target"
|
||||
return
|
||||
|
||||
# 计算移动方向和距离
|
||||
direction = target_pos - current_pos
|
||||
direction.normalize()
|
||||
|
||||
# 计算目标朝向(看向目标点)
|
||||
target_hpr = self._look_at_to_hpr(direction)
|
||||
current_hpr = self.world.cam.getHpr()
|
||||
|
||||
# 平滑转向到目标朝向
|
||||
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
|
||||
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
|
||||
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
|
||||
|
||||
# 计算本帧应转动的角度
|
||||
dt = globalClock.getDt()
|
||||
turn_amount = self.patrol_turn_speed * dt
|
||||
|
||||
# 逐步转向目标角度
|
||||
new_hpr = Vec3(current_hpr)
|
||||
|
||||
if abs(h_diff) > turn_amount:
|
||||
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
|
||||
else:
|
||||
new_hpr.x = target_hpr.x
|
||||
|
||||
if abs(p_diff) > turn_amount:
|
||||
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
|
||||
else:
|
||||
new_hpr.y = target_hpr.y
|
||||
|
||||
if abs(r_diff) > turn_amount:
|
||||
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
|
||||
else:
|
||||
new_hpr.z = target_hpr.z
|
||||
|
||||
self.world.cam.setHpr(new_hpr)
|
||||
|
||||
# 计算本帧应移动的距离
|
||||
move_distance = self.patrol_speed * dt
|
||||
|
||||
# 如果移动距离大于剩余距离,则直接移动到目标点
|
||||
if move_distance >= distance:
|
||||
self.world.cam.setPos(target_pos)
|
||||
else:
|
||||
# 否则按方向移动
|
||||
new_pos = current_pos + direction * move_distance
|
||||
self.world.cam.setPos(new_pos)
|
||||
|
||||
def _handle_turning_to_target_state(self, target_hpr):
|
||||
"""处理转向目标状态"""
|
||||
# 检查是否需要朝向下一个点
|
||||
if target_hpr == "look_next":
|
||||
# 计算朝向下一个点的方向
|
||||
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
|
||||
next_point_pos = self.patrol_points[next_index][0]
|
||||
|
||||
current_pos = self.world.cam.getPos()
|
||||
direction = next_point_pos - current_pos
|
||||
direction.normalize()
|
||||
|
||||
# 计算目标朝向
|
||||
target_hpr = self._look_at_to_hpr(direction)
|
||||
|
||||
current_hpr = self.world.cam.getHpr()
|
||||
|
||||
# 计算角度差
|
||||
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
|
||||
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
|
||||
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
|
||||
|
||||
# 检查是否已完成转向
|
||||
if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0:
|
||||
print(f"✓ 完成转向,开始停留")
|
||||
self.patrol_state = "waiting"
|
||||
self.patrol_wait_timer = 0.0
|
||||
return
|
||||
|
||||
# 计算本帧应转动的角度
|
||||
dt = globalClock.getDt()
|
||||
turn_amount = self.patrol_turn_speed * dt
|
||||
|
||||
# 逐步转向目标角度
|
||||
new_hpr = Vec3(current_hpr)
|
||||
|
||||
if abs(h_diff) > turn_amount:
|
||||
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
|
||||
else:
|
||||
new_hpr.x = target_hpr.x
|
||||
|
||||
if abs(p_diff) > turn_amount:
|
||||
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
|
||||
else:
|
||||
new_hpr.y = target_hpr.y
|
||||
|
||||
if abs(r_diff) > turn_amount:
|
||||
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
|
||||
else:
|
||||
new_hpr.z = target_hpr.z
|
||||
|
||||
self.world.cam.setHpr(new_hpr)
|
||||
|
||||
def _handle_waiting_state(self, wait_time):
|
||||
"""处理等待状态"""
|
||||
self.patrol_wait_timer += globalClock.getDt()
|
||||
|
||||
if self.patrol_wait_timer >= wait_time:
|
||||
print(f"✓ 停留结束,准备转回原朝向")
|
||||
self.patrol_state = "turning_back"
|
||||
|
||||
# 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法
|
||||
|
||||
def _handle_turning_back_state(self):
|
||||
"""处理转回原朝向状态"""
|
||||
# 直接完成转向状态,进入移动状态
|
||||
print(f"✓ 停留结束,开始移动到下一个点")
|
||||
# 移动到下一个巡检点
|
||||
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
|
||||
self.current_patrol_index = next_index
|
||||
self.patrol_state = "moving"
|
||||
return
|
||||
|
||||
def _normalize_angle(self, angle):
|
||||
"""规范化角度到-180到180度之间"""
|
||||
while angle > 180:
|
||||
angle -= 360
|
||||
while angle < -180:
|
||||
angle += 360
|
||||
return angle
|
||||
|
||||
def _look_at_to_hpr(self, direction):
|
||||
"""将方向向量转换为HPR角度"""
|
||||
# 简化的转换,实际应用中可能需要更精确的计算
|
||||
h = math.degrees(math.atan2(-direction.x, -direction.y))
|
||||
p = math.degrees(math.asin(direction.z))
|
||||
return Vec3(h, p, 0)
|
||||
|
||||
def get_patrol_status(self):
|
||||
"""获取巡检状态信息"""
|
||||
return {
|
||||
"is_patrolling": self.is_patrolling,
|
||||
"current_point": self.current_patrol_index,
|
||||
"total_points": len(self.patrol_points),
|
||||
"state": self.patrol_state,
|
||||
"wait_timer": self.patrol_wait_timer
|
||||
}
|
||||
|
||||
def list_patrol_points(self):
|
||||
"""列出所有巡检点"""
|
||||
if not self.patrol_points:
|
||||
print("没有设置巡检点")
|
||||
return
|
||||
|
||||
print(f"巡检点列表 (共{len(self.patrol_points)}个):")
|
||||
for i, (pos, hpr, wait_time) in enumerate(self.patrol_points):
|
||||
current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else ""
|
||||
print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) "
|
||||
f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) "
|
||||
f"停留:{wait_time}秒{current_marker}")
|
||||
|
||||
def remove_patrol_point(self, index):
|
||||
"""移除指定索引的巡检点"""
|
||||
if 0 <= index < len(self.patrol_points):
|
||||
removed_point = self.patrol_points.pop(index)
|
||||
print(
|
||||
f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})")
|
||||
|
||||
# 调整当前索引
|
||||
if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points:
|
||||
self.current_patrol_index = len(self.patrol_points) - 1
|
||||
elif self.current_patrol_index >= len(self.patrol_points):
|
||||
self.current_patrol_index = 0
|
||||
else:
|
||||
print(f"✗ 无效的巡检点索引: {index}")
|
||||
|
||||
def insert_patrol_point(self, index, position, heading=None, wait_time=3.0):
|
||||
"""在指定位置插入巡检点"""
|
||||
if index < 0 or index > len(self.patrol_points):
|
||||
print(f"✗ 无效的插入位置: {index}")
|
||||
return
|
||||
|
||||
if heading is None:
|
||||
# 使用当前相机朝向
|
||||
current_hpr = self.world.cam.getHpr()
|
||||
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
|
||||
|
||||
pos = Point3(position[0], position[1], position[2])
|
||||
hpr = Vec3(heading[0], heading[1], heading[2])
|
||||
|
||||
self.patrol_points.insert(index, (pos, hpr, wait_time))
|
||||
print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}秒")
|
||||
|
||||
def update_patrol_point(self, index, position=None, heading=None, wait_time=None):
|
||||
"""更新指定巡检点的信息"""
|
||||
if 0 <= index < len(self.patrol_points):
|
||||
pos, hpr, wt = self.patrol_points[index]
|
||||
|
||||
if position is not None:
|
||||
pos = Point3(position[0], position[1], position[2])
|
||||
if heading is not None:
|
||||
hpr = Vec3(heading[0], heading[1], heading[2])
|
||||
if wait_time is not None:
|
||||
wt = wait_time
|
||||
|
||||
self.patrol_points[index] = (pos, hpr, wt)
|
||||
print(f"✓ 更新巡检点 {index + 1}")
|
||||
else:
|
||||
print(f"✗ 无效的巡检点索引: {index}")
|
||||
|
||||
def goto_patrol_point(self, index):
|
||||
"""直接跳转到指定巡检点"""
|
||||
if not self.patrol_points:
|
||||
print("✗ 没有设置巡检点")
|
||||
return False
|
||||
|
||||
if 0 <= index < len(self.patrol_points):
|
||||
pos, hpr, _ = self.patrol_points[index]
|
||||
self.world.cam.setPos(pos)
|
||||
self.world.cam.setHpr(hpr)
|
||||
self.current_patrol_index = index
|
||||
print(f"✓ 跳转到巡检点 {index + 1}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 无效的巡检点索引: {index}")
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""清理巡检系统资源"""
|
||||
self.stop_patrol()
|
||||
self.clear_patrol_points()
|
||||
self.original_cam_pos = None
|
||||
self.original_cam_hpr = None
|
||||
print("✓ 巡检系统资源已清理")
|
||||
|
||||
|
||||
# 使用示例和便捷函数
|
||||
def create_default_patrol_route(patrol_system):
|
||||
"""创建默认的巡检路线(示例)"""
|
||||
# 清空现有巡检点
|
||||
patrol_system.clear_patrol_points()
|
||||
|
||||
# 添加一些示例巡检点
|
||||
patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1:前方低位置
|
||||
patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2:中央高位置
|
||||
patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3:右侧位置
|
||||
patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4:左侧位置
|
||||
|
||||
print("✓ 默认巡检路线已创建")
|
||||
|
||||
@ -253,7 +253,7 @@ class ScriptLoader:
|
||||
|
||||
for component in components_to_remove:
|
||||
self.script_manager.remove_script_from_object(component.game_object, script_name)
|
||||
|
||||
|
||||
# 从sys.modules中移除
|
||||
module = self.loaded_modules[script_name]
|
||||
if module.__name__ in sys.modules:
|
||||
@ -294,6 +294,31 @@ class ScriptLoader:
|
||||
|
||||
return len(changed_scripts) > 0
|
||||
|
||||
def find_script_file(self, script_name: str) -> Optional[str]:
|
||||
"""根据脚本名称查找脚本文件路径"""
|
||||
# 首先检查已加载的脚本
|
||||
if script_name in self.loaded_modules:
|
||||
module = self.loaded_modules[script_name]
|
||||
if hasattr(module, '__file__') and module.__file__:
|
||||
return module.__file__
|
||||
|
||||
# 在已知的文件路径中查找
|
||||
for file_path in self.file_mtimes.keys():
|
||||
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
if file_name == script_name:
|
||||
return file_path
|
||||
|
||||
# 在脚本目录中查找
|
||||
scripts_dir = self.script_manager.scripts_directory
|
||||
if os.path.exists(scripts_dir):
|
||||
for file_name in os.listdir(scripts_dir):
|
||||
if file_name.endswith('.py'):
|
||||
base_name = os.path.splitext(file_name)[0]
|
||||
if base_name == script_name:
|
||||
return os.path.join(scripts_dir, file_name)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ScriptAPI:
|
||||
"""脚本API - 提供给脚本使用的API接口"""
|
||||
@ -671,18 +696,62 @@ class {class_name}(ScriptBase):
|
||||
return False
|
||||
|
||||
script_components = self.object_scripts[game_object]
|
||||
removed = False
|
||||
|
||||
for component in script_components[:]: # 复制列表以避免修改时出错
|
||||
if component.script_instance.__class__.__name__ == script_name:
|
||||
# 从引擎移除
|
||||
self.engine.remove_script_component(component)
|
||||
# 从对象脚本列表移除
|
||||
script_components.remove(component)
|
||||
removed = True
|
||||
|
||||
print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}")
|
||||
return True
|
||||
|
||||
|
||||
if not script_components:
|
||||
del self.object_scripts[game_object]
|
||||
|
||||
# 更新节点上保存的脚本信息标签
|
||||
if removed:
|
||||
self._update_node_script_tags_after_removal(game_object, script_name)
|
||||
|
||||
return False
|
||||
|
||||
return removed
|
||||
|
||||
def _update_node_script_tags_after_removal(self, game_object, removed_script_name):
|
||||
"""在移除脚本后更新节点标签"""
|
||||
try:
|
||||
# 获取对象上剩余的脚本
|
||||
remaining_scripts = self.get_scripts_on_object(game_object)
|
||||
|
||||
if not remaining_scripts:
|
||||
# 如果没有其他脚本,清除所有脚本标签
|
||||
if game_object.hasTag("has_scripts"):
|
||||
game_object.clearTag("has_scripts")
|
||||
if game_object.hasTag("scripts_info"):
|
||||
game_object.clearTag("scripts_info")
|
||||
print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签")
|
||||
else:
|
||||
# 如果还有其他脚本,更新脚本信息标签
|
||||
script_info_list = []
|
||||
for script_component in remaining_scripts:
|
||||
script_name = script_component.script_name
|
||||
script_class = script_component.script_instance.__class__
|
||||
script_file = self.loader.find_script_file(script_name) or ""
|
||||
|
||||
script_info_list.append({
|
||||
"name": script_name,
|
||||
"file": script_file
|
||||
})
|
||||
|
||||
import json
|
||||
game_object.setTag("has_scripts", "true")
|
||||
game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
|
||||
print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本")
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新节点标签失败: {e}")
|
||||
|
||||
def get_scripts_on_object(self, game_object) -> List[ScriptComponent]:
|
||||
"""获取对象上的所有脚本"""
|
||||
return self.object_scripts.get(game_object, [])
|
||||
@ -744,6 +813,36 @@ class {class_name}(ScriptBase):
|
||||
|
||||
print("==================\n")
|
||||
|
||||
def save_object_scripts(self,game_object,node_data:dict):
|
||||
try:
|
||||
if game_object in self.object_scripts:
|
||||
scripts_data = []
|
||||
for script_commponent in self.object_scripts[game_object]:
|
||||
script_info = {
|
||||
'script_name':script_commponent.script_name,
|
||||
'enabled':script_commponent.enabled,
|
||||
'script_class':script_commponent.script_instance.__class__.__name__
|
||||
}
|
||||
scripts_data.append(script_info)
|
||||
if scripts_data:
|
||||
node_data['scripts'] = scripts_data
|
||||
print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}")
|
||||
except Exception as e:
|
||||
print(f"保存对象脚本信息失败: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
# def restore_object_scripts(self,game_object,node_data:dict):
|
||||
# try:
|
||||
# if 'scripts' in node_data:
|
||||
# scripts_data = node_data['scripts']
|
||||
# restored_count = 0
|
||||
# for script_info in scripts_data:
|
||||
# script_name = script_info.get('script_name')
|
||||
# enabled = script_info.get('enabled',True)
|
||||
#
|
||||
# #检查脚本是否可用
|
||||
# if script_name in self.loader.script_classes:
|
||||
#为
|
||||
|
||||
# 添加全局便捷函数,让脚本更容易使用API
|
||||
def get_script_api():
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,13 @@
|
||||
# core/terrain_manager.py
|
||||
import time
|
||||
import urllib
|
||||
|
||||
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
|
||||
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
|
||||
import os
|
||||
|
||||
from scene import util
|
||||
|
||||
|
||||
class TerrainManager:
|
||||
"""地形管理类"""
|
||||
@ -31,106 +35,151 @@ class TerrainManager:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 创建GeoMipTerrain对象
|
||||
terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
||||
terrain = GeoMipTerrain(terrain_name)
|
||||
print(f"🔆 开始创建高度图地形")
|
||||
|
||||
# 加载高度图
|
||||
height_image = PNMImage(Filename.fromOsSpecific(heightmap_path))
|
||||
|
||||
# 检查并调整图像尺寸为2的幂次方加1
|
||||
width, height = height_image.getXSize(), height_image.getYSize()
|
||||
print(f"原始图像尺寸: {width}x{height}")
|
||||
|
||||
# 找到最接近的有效尺寸
|
||||
valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049]
|
||||
target_size = 129 # 默认尺寸
|
||||
|
||||
# 选择最接近的尺寸
|
||||
max_dim = max(width, height)
|
||||
for size in valid_sizes:
|
||||
if size >= max_dim:
|
||||
target_size = size
|
||||
break
|
||||
else:
|
||||
target_size = valid_sizes[-1] # 使用最大尺寸
|
||||
|
||||
# 如果需要,调整图像尺寸
|
||||
if width != target_size or height != target_size:
|
||||
print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}")
|
||||
# 使用正确的图像缩放方法
|
||||
resized_image = PNMImage(target_size, target_size)
|
||||
resized_image.quickFilterFrom(height_image)
|
||||
height_image = resized_image
|
||||
|
||||
# 使用正确的方法设置高度图
|
||||
terrain.setHeightfield(height_image)
|
||||
|
||||
# 设置地形参数
|
||||
terrain.setBruteforce(True) # 使用LOD
|
||||
|
||||
terrain.setBlockSize(32)
|
||||
# terrain.setNearFarThreshold(50.0,200.0)
|
||||
|
||||
# 生成地形
|
||||
terrain.generate()
|
||||
|
||||
# 获取地形节点
|
||||
terrain_node = terrain.getRoot()
|
||||
|
||||
if terrain_node.isEmpty():
|
||||
print("错误:无法生成有效的地形节点")
|
||||
# 获取树形控件
|
||||
tree_widget = self._get_tree_widget()
|
||||
if not tree_widget:
|
||||
print("❌ 无法访问树形控件")
|
||||
return None
|
||||
|
||||
node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}"
|
||||
terrain_node.setName(node_name)
|
||||
# 获取目标父节点列表
|
||||
target_parents = tree_widget.get_target_parents_for_creation()
|
||||
if not target_parents:
|
||||
print("❌ 没有找到有效的父节点")
|
||||
return None
|
||||
|
||||
center_offset = (target_size - 1) / 2
|
||||
terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5)
|
||||
created_terrains = []
|
||||
try:
|
||||
parent_item, parent_node = target_parents[0]
|
||||
|
||||
# 设置缩放
|
||||
terrain_node.setScale(scale[0], scale[1], scale[2])
|
||||
# 创建GeoMipTerrain对象
|
||||
terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
||||
terrain = GeoMipTerrain(terrain_name)
|
||||
|
||||
# 将地形添加到场景中
|
||||
terrain_node.reparentTo(self.world.render)
|
||||
# 加载高度图
|
||||
height_image = PNMImage(Filename.fromOsSpecific(heightmap_path))
|
||||
|
||||
from panda3d.core import BitMask32
|
||||
# 设置地形节点的碰撞掩码
|
||||
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
|
||||
# 为地形的所有子节点也设置碰撞掩码
|
||||
for child in terrain_node.getChildren():
|
||||
child.setCollideMask(BitMask32.bit(2))
|
||||
# 检查并调整图像尺寸为2的幂次方加1
|
||||
width, height = height_image.getXSize(), height_image.getYSize()
|
||||
print(f"原始图像尺寸: {width}x{height}")
|
||||
|
||||
# 添加材质
|
||||
self._applyTerrainMaterial(terrain_node)
|
||||
# 找到最接近的有效尺寸
|
||||
valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049]
|
||||
target_size = 129 # 默认尺寸
|
||||
|
||||
terrain_node.setPythonTag("selectable", True)
|
||||
# 选择最接近的尺寸
|
||||
max_dim = max(width, height)
|
||||
for size in valid_sizes:
|
||||
if size >= max_dim:
|
||||
target_size = size
|
||||
break
|
||||
else:
|
||||
target_size = valid_sizes[-1] # 使用最大尺寸
|
||||
|
||||
# 保存地形信息(包括高度图的副本)
|
||||
terrain_info = {
|
||||
'terrain': terrain,
|
||||
'node': terrain_node,
|
||||
'heightmap': heightmap_path,
|
||||
'heightfield': height_image, # 保存高度图副本
|
||||
'scale': scale,
|
||||
'name': node_name
|
||||
}
|
||||
# 如果需要,调整图像尺寸
|
||||
if width != target_size or height != target_size:
|
||||
print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}")
|
||||
# 使用正确的图像缩放方法
|
||||
resized_image = PNMImage(target_size, target_size)
|
||||
resized_image.quickFilterFrom(height_image)
|
||||
height_image = resized_image
|
||||
|
||||
self.terrains.append(terrain_info)
|
||||
# 使用正确的方法设置高度图
|
||||
terrain.setHeightfield(height_image)
|
||||
|
||||
# 更新场景树(再次检查节点是否有效)
|
||||
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
|
||||
'updateSceneTree'):
|
||||
try:
|
||||
self.world.scene_manager.updateSceneTree()
|
||||
except Exception as e:
|
||||
print(f"警告: 更新场景树时出错: {e}")
|
||||
# 设置地形参数
|
||||
terrain.setBruteforce(True) # 使用LOD
|
||||
|
||||
print(f"✓ 成功从 {heightmap_path} 创建地形")
|
||||
return terrain_info
|
||||
terrain.setBlockSize(32)
|
||||
# terrain.setNearFarThreshold(50.0,200.0)
|
||||
|
||||
# 生成地形
|
||||
terrain.generate()
|
||||
|
||||
# 获取地形节点
|
||||
terrain_node = terrain.getRoot()
|
||||
|
||||
if terrain_node.isEmpty():
|
||||
print("错误:无法生成有效的地形节点")
|
||||
return None
|
||||
|
||||
terrain_node.setTag("is_scene_element", "1")
|
||||
terrain_node.setTag("tree_item_type", "TERRAIN_NODE")
|
||||
|
||||
node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}"
|
||||
terrain_node.setName(node_name)
|
||||
|
||||
center_offset = (target_size - 1) / 2
|
||||
terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5)
|
||||
|
||||
# 设置缩放
|
||||
terrain_node.setScale(scale[0], scale[1], scale[2])
|
||||
|
||||
# 将地形添加到场景中
|
||||
terrain_node.reparentTo(parent_node)
|
||||
|
||||
from panda3d.core import BitMask32
|
||||
# 设置地形节点的碰撞掩码
|
||||
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
|
||||
# 为地形的所有子节点也设置碰撞掩码
|
||||
for child in terrain_node.getChildren():
|
||||
child.setCollideMask(BitMask32.bit(2))
|
||||
|
||||
# 添加材质
|
||||
self._applyTerrainMaterial(terrain_node)
|
||||
|
||||
terrain_node.setPythonTag("selectable", True)
|
||||
|
||||
# 保存地形信息(包括高度图的副本)
|
||||
terrain_info = {
|
||||
'terrain': terrain,
|
||||
'node': terrain_node,
|
||||
'heightmap': heightmap_path,
|
||||
'heightfield': height_image, # 保存高度图副本
|
||||
'scale': scale,
|
||||
'name': node_name
|
||||
}
|
||||
|
||||
self.terrains.append(terrain_info)
|
||||
|
||||
print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}")
|
||||
|
||||
# 在Qt树形控件中添加对应节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
|
||||
if qt_item:
|
||||
created_terrains.append((terrain_node, qt_item))
|
||||
else:
|
||||
created_terrains.append((terrain_node, None))
|
||||
print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}")
|
||||
return None
|
||||
|
||||
# 处理创建结果
|
||||
if not created_terrains:
|
||||
print("❌ 没有成功创建任何高度图地形")
|
||||
return None
|
||||
|
||||
# 选中最后创建的光源
|
||||
if created_terrains:
|
||||
last_light_np, last_qt_item = created_terrains[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形")
|
||||
|
||||
# 返回值处理
|
||||
if len(created_terrains) == 1:
|
||||
return created_terrains[0][0] # 单个光源返回NodePath
|
||||
else:
|
||||
return [light_np for light_np, _ in created_terrains] # 多个光源返回列表
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建地形时出错: {e}")
|
||||
print(f"❌ 创建高度图地形过程失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
@ -138,84 +187,129 @@ class TerrainManager:
|
||||
def createFlatTerrain(self, size=(0.3, 0.3), resolution=129):
|
||||
"""创建平面地形"""
|
||||
try:
|
||||
# 确保分辨率是2的幂次方加1 (如129, 257, 513等)
|
||||
valid_resolutions = [17, 33, 65, 129, 257, 513, 1025]
|
||||
if resolution not in valid_resolutions:
|
||||
# 找到最接近的有效分辨率
|
||||
closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution))
|
||||
print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}")
|
||||
resolution = closest_res
|
||||
print(f"🔆 开始创建平面地形")
|
||||
|
||||
# 创建GeoMipTerrain对象
|
||||
terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
||||
terrain = GeoMipTerrain(terrain_name)
|
||||
|
||||
# 创建一个平面高度图,尺寸必须是2^n+1
|
||||
height_image = PNMImage(resolution, resolution)
|
||||
height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围)
|
||||
|
||||
# 使用正确的方法设置高度图
|
||||
terrain.setHeightfield(height_image)
|
||||
terrain.setBruteforce(True) # 设置LOD
|
||||
|
||||
# 设置地形参数
|
||||
terrain.setBlockSize(32) # 设置块大小
|
||||
|
||||
# 生成地形
|
||||
terrain.generate()
|
||||
|
||||
# 获取地形节点
|
||||
terrain_node = terrain.getRoot()
|
||||
|
||||
if terrain_node.isEmpty():
|
||||
print("错误:无法生成有效的平面地形节点")
|
||||
# 获取树形控件
|
||||
tree_widget = self._get_tree_widget()
|
||||
if not tree_widget:
|
||||
print("❌ 无法访问树形控件")
|
||||
return None
|
||||
|
||||
node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
||||
terrain_node.setName(node_name)
|
||||
# 获取目标父节点列表
|
||||
target_parents = tree_widget.get_target_parents_for_creation()
|
||||
if not target_parents:
|
||||
print("❌ 没有找到有效的父节点")
|
||||
return None
|
||||
|
||||
center_offset = (resolution - 1) / 2
|
||||
terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0)
|
||||
created_terrains = []
|
||||
|
||||
# 将地形添加到场景中
|
||||
terrain_node.reparentTo(self.world.render)
|
||||
try:
|
||||
parent_item, parent_node = target_parents[0]
|
||||
# 确保分辨率是2的幂次方加1 (如129, 257, 513等)
|
||||
valid_resolutions = [17, 33, 65, 129, 257, 513, 1025]
|
||||
if resolution not in valid_resolutions:
|
||||
# 找到最接近的有效分辨率
|
||||
closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution))
|
||||
print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}")
|
||||
resolution = closest_res
|
||||
|
||||
# 为地形添加碰撞体
|
||||
from panda3d.core import BitMask32
|
||||
# 设置地形节点的碰撞掩码
|
||||
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
|
||||
# 为地形的所有子节点也设置碰撞掩码
|
||||
for child in terrain_node.getChildren():
|
||||
child.setCollideMask(BitMask32.bit(2))
|
||||
# 创建GeoMipTerrain对象
|
||||
terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
||||
terrain = GeoMipTerrain(terrain_name)
|
||||
|
||||
# 添加材质
|
||||
self._applyTerrainMaterial(terrain_node)
|
||||
# 创建一个平面高度图,尺寸必须是2^n+1
|
||||
height_image = PNMImage(resolution, resolution)
|
||||
height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围)
|
||||
|
||||
# 保存地形信息(包括高度图)
|
||||
terrain_info = {
|
||||
'terrain': terrain,
|
||||
'node': terrain_node,
|
||||
'heightmap': None,
|
||||
'heightfield': height_image, # 保存高度图
|
||||
'scale': (size[0], size[1], 50),
|
||||
'name': node_name
|
||||
}
|
||||
# 使用正确的方法设置高度图
|
||||
terrain.setHeightfield(height_image)
|
||||
terrain.setBruteforce(True) # 设置LOD
|
||||
|
||||
self.terrains.append(terrain_info)
|
||||
# 设置地形参数
|
||||
terrain.setBlockSize(32) # 设置块大小
|
||||
|
||||
# 更新场景树(再次检查节点是否有效)
|
||||
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
|
||||
'updateSceneTree'):
|
||||
try:
|
||||
self.world.scene_manager.updateSceneTree()
|
||||
except Exception as e:
|
||||
print(f"警告: 更新场景树时出错: {e}")
|
||||
# 生成地形
|
||||
terrain.generate()
|
||||
|
||||
print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}")
|
||||
return terrain_info
|
||||
# 获取地形节点
|
||||
terrain_node = terrain.getRoot()
|
||||
|
||||
if terrain_node.isEmpty():
|
||||
print("错误:无法生成有效的平面地形节点")
|
||||
return None
|
||||
|
||||
terrain_node.setTag("is_scene_element", "1")
|
||||
terrain_node.setTag("tree_item_type", "TERRAIN_NODE")
|
||||
|
||||
node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
||||
terrain_node.setName(node_name)
|
||||
|
||||
center_offset = (resolution - 1) / 2
|
||||
terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0)
|
||||
|
||||
# 将地形添加到场景中
|
||||
terrain_node.reparentTo(parent_node)
|
||||
|
||||
# 为地形添加碰撞体
|
||||
from panda3d.core import BitMask32
|
||||
# 设置地形节点的碰撞掩码
|
||||
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
|
||||
# 为地形的所有子节点也设置碰撞掩码
|
||||
for child in terrain_node.getChildren():
|
||||
child.setCollideMask(BitMask32.bit(2))
|
||||
|
||||
# 添加材质
|
||||
self._applyTerrainMaterial(terrain_node)
|
||||
|
||||
# 保存地形信息(包括高度图)
|
||||
terrain_info = {
|
||||
'terrain': terrain,
|
||||
'node': terrain_node,
|
||||
'heightmap': None,
|
||||
'heightfield': height_image, # 保存高度图
|
||||
'scale': (size[0], size[1], 50),
|
||||
'name': node_name
|
||||
}
|
||||
|
||||
self.terrains.append(terrain_info)
|
||||
|
||||
print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}")
|
||||
|
||||
# 在Qt树形控件中添加对应节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
|
||||
if qt_item:
|
||||
created_terrains.append((terrain_node, qt_item))
|
||||
else:
|
||||
created_terrains.append((terrain_node, None))
|
||||
print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}")
|
||||
return None
|
||||
|
||||
# 处理创建结果
|
||||
if not created_terrains:
|
||||
print("❌ 没有成功创建任何平面地形")
|
||||
return None
|
||||
|
||||
# 选中最后创建的光源
|
||||
if created_terrains:
|
||||
last_light_np, last_qt_item = created_terrains[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形")
|
||||
|
||||
# 返回值处理
|
||||
if len(created_terrains) == 1:
|
||||
return created_terrains[0][0] # 单个光源返回NodePath
|
||||
else:
|
||||
return [light_np for light_np, _ in created_terrains] # 多个光源返回列表
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建平面地形时出错: {e}")
|
||||
print(f"❌ 创建平面地形过程失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
@ -390,7 +484,6 @@ class TerrainManager:
|
||||
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}")
|
||||
print(f"✓ 重新设置了地形碰撞体")
|
||||
|
||||
|
||||
return modified
|
||||
|
||||
except Exception as e:
|
||||
@ -405,14 +498,17 @@ class TerrainManager:
|
||||
# 从场景中移除地形节点
|
||||
terrain_node = terrain_info['node']
|
||||
if terrain_node and not terrain_node.isEmpty():
|
||||
terrain_node.removeNode()
|
||||
tree_widget = self._get_tree_widget()
|
||||
if tree_widget:
|
||||
tree_widget.delete_items(tree_widget.selectedItems())
|
||||
# terrain_node.removeNode()
|
||||
#
|
||||
# # 从列表中移除
|
||||
# self.terrains.remove(terrain_info)
|
||||
|
||||
# 从列表中移除
|
||||
self.terrains.remove(terrain_info)
|
||||
|
||||
# 更新场景树
|
||||
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
|
||||
self.world.scene_manager.updateSceneTree()
|
||||
# # 更新场景树
|
||||
# if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
|
||||
# self.world.scene_manager.updateSceneTree()
|
||||
|
||||
print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}")
|
||||
|
||||
@ -461,6 +557,7 @@ class TerrainManager:
|
||||
if terrain_info in self.terrains:
|
||||
terrain_node = terrain_info['node']
|
||||
if terrain_node and os.path.exists(texture_path):
|
||||
texture_path = util.normalize_model_path(texture_path)
|
||||
# 加载纹理
|
||||
texture = self.world.loader.loadTexture(texture_path)
|
||||
if texture:
|
||||
@ -476,3 +573,88 @@ class TerrainManager:
|
||||
except Exception as e:
|
||||
print(f"应用地形纹理时出错: {e}")
|
||||
return False
|
||||
|
||||
def saveTerrainData(self, terrain_info, filename):
|
||||
"""保存地形数据到文件"""
|
||||
try:
|
||||
terrain_node = terrain_info['node']
|
||||
heightfield = terrain_info['heightfield']
|
||||
|
||||
# 保存高度图到文件
|
||||
if heightfield:
|
||||
# 创建保存路径
|
||||
terrain_dir = os.path.join(os.path.dirname(filename), "terrains")
|
||||
if not os.path.exists(terrain_dir):
|
||||
os.makedirs(terrain_dir)
|
||||
|
||||
# 生成唯一的地形文件名
|
||||
terrain_filename = f"terrain_{terrain_info.get('name', 'unnamed')}_{int(time.time())}.png"
|
||||
terrain_path = os.path.join(terrain_dir, terrain_filename)
|
||||
|
||||
# 保存高度图
|
||||
heightfield.write(Filename.fromOsSpecific(terrain_path))
|
||||
|
||||
# 保存地形信息到标签
|
||||
terrain_node.setTag("terrain_heightmap_path", terrain_path)
|
||||
terrain_node.setTag("terrain_scale", str(terrain_info['scale']))
|
||||
|
||||
print(f"✓ 地形数据已保存: {terrain_path}")
|
||||
return terrain_path
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"保存地形数据时出错: {e}")
|
||||
return None
|
||||
|
||||
def _recreateTerrain(self, terrain_node):
|
||||
"""重新创建地形"""
|
||||
try:
|
||||
print(f"重新创建地形: {terrain_node.getName()}")
|
||||
|
||||
# 获取保存的地形信息
|
||||
heightmap_path = terrain_node.getTag("terrain_heightmap_path") if terrain_node.hasTag(
|
||||
"terrain_heightmap_path") else None
|
||||
scale_str = terrain_node.getTag("terrain_scale") if terrain_node.hasTag("terrain_scale") else "(1, 1, 1)"
|
||||
|
||||
# 解析缩放信息
|
||||
try:
|
||||
scale_str = scale_str.strip("()")
|
||||
scale_values = [float(x.strip()) for x in scale_str.split(",")]
|
||||
scale = (scale_values[0], scale_values[1], scale_values[2]) if len(scale_values) >= 3 else (1, 1, 1)
|
||||
except:
|
||||
scale = (1, 1, 1)
|
||||
|
||||
# 恢复位置信息
|
||||
if terrain_node.hasTag("transform_pos"):
|
||||
try:
|
||||
pos_str = terrain_node.getTag("transform_pos")
|
||||
pos_str = pos_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()')
|
||||
pos_values = [float(x.strip()) for x in pos_str.split(',')]
|
||||
if len(pos_values) >= 3:
|
||||
terrain_node.setPos(pos_values[0], pos_values[1], pos_values[2])
|
||||
except Exception as e:
|
||||
print(f"恢复地形位置失败: {e}")
|
||||
|
||||
# 如果有高度图路径,重新创建地形
|
||||
if heightmap_path and os.path.exists(heightmap_path):
|
||||
# 使用现有方法重新创建地形
|
||||
new_terrain = self.createTerrainFromHeightMap(heightmap_path, scale)
|
||||
if new_terrain:
|
||||
# 删除旧的地形节点
|
||||
if not terrain_node.isEmpty():
|
||||
terrain_node.removeNode()
|
||||
return new_terrain
|
||||
|
||||
# 如果没有高度图或创建失败,创建一个平面地形作为替代
|
||||
print("使用平面地形作为替代")
|
||||
new_terrain = self.createFlatTerrain(size=(scale[0], scale[1]), resolution=129)
|
||||
if new_terrain:
|
||||
# 删除旧的地形节点
|
||||
if not terrain_node.isEmpty():
|
||||
terrain_node.removeNode()
|
||||
return new_terrain
|
||||
|
||||
return terrain_node
|
||||
except Exception as e:
|
||||
print(f"重新创建地形失败: {e}")
|
||||
return terrain_node
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ class ToolManager:
|
||||
"""初始化工具管理器"""
|
||||
self.world = world
|
||||
self.currentTool = "选择" # 默认工具为选择工具
|
||||
print(f"当前工具: {self.currentTool}")
|
||||
|
||||
def setCurrentTool(self, tool):
|
||||
"""设置当前工具"""
|
||||
|
||||
115
core/world.py
115
core/world.py
@ -288,6 +288,8 @@ class CoreWorld(Panda3DWorld):
|
||||
self.cam.setPos(0, -50, 20)
|
||||
self.cam.lookAt(0, 0, 0)
|
||||
self.camLens.setFov(80)
|
||||
self.cam.setTag("is_scene_element", "1")
|
||||
self.cam.setTag("tree_item_type", "CAMERA_NODE")
|
||||
print("✓ 相机设置完成")
|
||||
|
||||
def _setupLighting(self):
|
||||
@ -319,18 +321,61 @@ class CoreWorld(Panda3DWorld):
|
||||
# 创建地板节点
|
||||
self.ground = self.render.attachNewNode(cm.generate())
|
||||
self.ground.setP(-90)
|
||||
self.ground.setZ(-0.1)
|
||||
self.ground.setZ(-1.0)
|
||||
self.ground.setColor(0.8, 0.8, 0.8, 1)
|
||||
# self.ground.setTag("is_scene_element", "1")
|
||||
# self.ground.setTag("tree_item_type", "SCENE_NODE")
|
||||
|
||||
# 创建支持贴图的材质
|
||||
mat = Material()
|
||||
mat.setName("GroundMaterial")
|
||||
color = LColor(1, 1, 1, 0.8)
|
||||
mat.set_base_color(color)
|
||||
mat.set_roughness(0.5) # 设置合适的初始粗糙度
|
||||
mat.set_roughness(1) # 设置合适的初始粗糙度
|
||||
mat.set_metallic(0.5) # 设置较低的初始金属性
|
||||
self.ground.set_material(mat)
|
||||
|
||||
#创建第二个相同的地面,位置稍有偏移
|
||||
self.ground2 = self.render.attachNewNode(cm.generate())
|
||||
self.ground2.setH(-90)
|
||||
self.ground2.setZ(-1.0)
|
||||
self.ground2.setX(50) # 在X轴方向偏移
|
||||
self.ground2.setZ(49) # 在X轴方向偏移
|
||||
self.ground2.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground2.set_material(mat)
|
||||
|
||||
# 创建第三个相同的地面,位置在另一个方向
|
||||
self.ground3 = self.render.attachNewNode(cm.generate())
|
||||
self.ground3.setH(90)
|
||||
self.ground3.setZ(-1.0)
|
||||
self.ground3.setX(-50) # 在X轴负方向偏移
|
||||
self.ground3.setZ(49) # 在X轴负方向偏移
|
||||
self.ground3.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground3.set_material(mat)
|
||||
|
||||
self.ground4 = self.render.attachNewNode(cm.generate())
|
||||
# self.ground3.setR(90)
|
||||
self.ground4.setZ(-1.0)
|
||||
self.ground4.setY(50) # 在X轴负方向偏移
|
||||
self.ground4.setZ(49) # 在X轴负方向偏移
|
||||
self.ground4.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground4.set_material(mat)
|
||||
|
||||
self.ground5 = self.render.attachNewNode(cm.generate())
|
||||
self.ground5.setP(180)
|
||||
self.ground5.setZ(-1)
|
||||
self.ground5.setY(-50) # 在X轴负方向偏移
|
||||
self.ground5.setZ(49) # 在X轴负方向偏移
|
||||
self.ground5.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground5.set_material(mat)
|
||||
|
||||
self.ground6 = self.render.attachNewNode(cm.generate())
|
||||
self.ground6.setP(90)
|
||||
self.ground6.setZ(-1)
|
||||
self.ground6.setZ(99) # 在X轴负方向偏移
|
||||
self.ground6.setColor(0.8, 0.8, 0.8, 1)
|
||||
self.ground6.set_material(mat)
|
||||
|
||||
# 应用默认PBR效果,确保支持贴图
|
||||
try:
|
||||
if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
||||
@ -347,6 +392,72 @@ class CoreWorld(Panda3DWorld):
|
||||
},
|
||||
50
|
||||
)
|
||||
# 为其他两个地面也应用相同的效果
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground2,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground3,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground4,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground5,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
self.render_pipeline.set_effect(
|
||||
self.ground6,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
50
|
||||
)
|
||||
print("✓ 地板PBR效果已应用")
|
||||
else:
|
||||
print("⚠️ RenderPipeline未初始化,地板将使用基础渲染")
|
||||
|
||||
@ -359,7 +359,7 @@ class GizmoDragTestWorld(Panda3DWorld):
|
||||
distance = distance_to_line(
|
||||
(mouseX, mouseY), center_screen, axis_screen
|
||||
)
|
||||
print(f"{axis_label}距离: {distance:.2f}")
|
||||
#print(f"{axis_label}距离: {distance:.2f}")
|
||||
|
||||
if distance < click_threshold:
|
||||
print(f"✓ 点击了{axis_label}")
|
||||
|
||||
@ -24,78 +24,80 @@ except ImportError:
|
||||
WEB_ENGINE_AVAILABLE = False
|
||||
print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用")
|
||||
|
||||
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
|
||||
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
|
||||
|
||||
# 参数类型检查和转换
|
||||
if isinstance(size, (list, tuple)):
|
||||
if len(size) >= 2:
|
||||
x_size, y_size = float(size[0]), float(size[1])
|
||||
else:
|
||||
x_size = y_size = float(size[0]) if size else 1.0
|
||||
else:
|
||||
x_size = y_size = float(size)
|
||||
|
||||
# 创建卡片
|
||||
cm = CardMaker('gui_3d_image')
|
||||
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
|
||||
|
||||
# 创建3D图像节点
|
||||
image_node = self.world.render.attachNewNode(cm.generate())
|
||||
image_node.setPos(*pos)
|
||||
|
||||
# 为3D图像创建独立的材质
|
||||
material = Material(f"image-material-{len(self.gui_elements)}")
|
||||
material.setBaseColor(LColor(1, 1, 1, 1))
|
||||
material.setDiffuse(LColor(1, 1, 1, 1))
|
||||
material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
|
||||
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
|
||||
material.setShininess(10.0)
|
||||
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
|
||||
image_node.setMaterial(material, 1)
|
||||
|
||||
image_node.setTransparency(TransparencyAttrib.MAlpha)
|
||||
|
||||
# 如果提供了图像路径,则加载纹理
|
||||
if image_path:
|
||||
self.update3DImageTexture(image_node, image_path)
|
||||
|
||||
# 应用PBR效果(如果可用)
|
||||
try:
|
||||
if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
||||
self.render_pipeline.set_effect(
|
||||
image_node,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": False,
|
||||
"render_envmap": True,
|
||||
"disable_children_effects": True
|
||||
},
|
||||
50
|
||||
)
|
||||
print("✓ GUI 3D图像PBR效果已应用")
|
||||
except Exception as e:
|
||||
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
|
||||
|
||||
# 为GUI元素添加标识(效仿3D文本方法)
|
||||
image_node.setTag("gui_type", "3d_image")
|
||||
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
|
||||
if image_path:
|
||||
image_node.setTag("gui_image_path", image_path)
|
||||
image_node.setTag("is_gui_element", "1")
|
||||
|
||||
self.gui_elements.append(image_node)
|
||||
|
||||
# 更新场景树
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
|
||||
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
|
||||
return image_node
|
||||
# def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
|
||||
# from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
|
||||
#
|
||||
# # 参数类型检查和转换
|
||||
# if isinstance(size, (list, tuple)):
|
||||
# if len(size) >= 2:
|
||||
# x_size, y_size = float(size[0]), float(size[1])
|
||||
# else:
|
||||
# x_size = y_size = float(size[0]) if size else 1.0
|
||||
# else:
|
||||
# x_size = y_size = float(size)
|
||||
#
|
||||
# # 创建卡片
|
||||
# cm = CardMaker('gui_3d_image')
|
||||
# cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
|
||||
#
|
||||
# # 创建3D图像节点
|
||||
# image_node = self.world.render.attachNewNode(cm.generate())
|
||||
# image_node.setPos(*pos)
|
||||
#
|
||||
# # 为3D图像创建独立的材质
|
||||
# material = Material(f"image-material-{len(self.gui_elements)}")
|
||||
# material.setBaseColor(LColor(1, 1, 1, 1))
|
||||
# material.setDiffuse(LColor(1, 1, 1, 1))
|
||||
# material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
|
||||
# material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
|
||||
# material.setShininess(10.0)
|
||||
# material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
|
||||
# image_node.setMaterial(material, 1)
|
||||
#
|
||||
# image_node.setTransparency(TransparencyAttrib.MAlpha)
|
||||
#
|
||||
# # 如果提供了图像路径,则加载纹理
|
||||
# if image_path:
|
||||
# self.update3DImageTexture(image_node, image_path)
|
||||
#
|
||||
# # 应用PBR效果(如果可用)
|
||||
# try:
|
||||
# if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
||||
# self.render_pipeline.set_effect(
|
||||
# image_node,
|
||||
# "effects/default.yaml",
|
||||
# {
|
||||
# "normal_mapping": True,
|
||||
# "render_gbuffer": True,
|
||||
# "alpha_testing": False,
|
||||
# "parallax_mapping": False,
|
||||
# "render_shadow": False,
|
||||
# "render_envmap": True,
|
||||
# "disable_children_effects": True
|
||||
# },
|
||||
# 50
|
||||
# )
|
||||
# print("✓ GUI 3D图像PBR效果已应用")
|
||||
# except Exception as e:
|
||||
# print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
|
||||
#
|
||||
# # 为GUI元素添加标识(效仿3D文本方法)
|
||||
# image_node.setTag("gui_type", "3d_image")
|
||||
# image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
|
||||
# image_node.setTag("is_scene_element", "1")
|
||||
# image_node.setTag("tree_item_type", "GUI_3DIMAGE")
|
||||
# if image_path:
|
||||
# image_node.setTag("gui_image_path", image_path)
|
||||
# image_node.setTag("is_gui_element", "1")
|
||||
#
|
||||
# self.gui_elements.append(image_node)
|
||||
#
|
||||
# # 更新场景树
|
||||
# if hasattr(self.world, 'updateSceneTree'):
|
||||
# self.world.updateSceneTree()
|
||||
#
|
||||
# print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
|
||||
# return image_node
|
||||
|
||||
class GUIManager:
|
||||
"""GUI元素管理系统类"""
|
||||
@ -124,7 +126,7 @@ class GUIManager:
|
||||
|
||||
# ==================== GUI元素创建方法 ====================
|
||||
|
||||
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
|
||||
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=(0.1,0.1,0.1)):
|
||||
"""创建2D GUI按钮 - 支持多选创建和GUI父子关系,优化版本"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectButton
|
||||
@ -180,17 +182,35 @@ class GUIManager:
|
||||
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
||||
rolloverSound=None,
|
||||
clickSound=None,
|
||||
parent=parent_gui_node # 设置GUI父节点
|
||||
parent=parent_gui_node
|
||||
)
|
||||
|
||||
if not hasattr(button,'_tags'):
|
||||
button._tags = {}
|
||||
|
||||
# button._tags["gui_type"] = "button"
|
||||
# button._tags["gui_id"] = f"button_{len(self.gui_elements)}"
|
||||
# button._tags["gui_text"] = text
|
||||
# button._tags["is_gui_element"] = "1"
|
||||
# button._tags["is_scene_element"] = "1"
|
||||
# button._tags["saved_gui_type"] = "button"
|
||||
# button._tags["gui_element_type"] = "button"
|
||||
# button._tags["created_by_user"] = "1"
|
||||
# button._tags["name"] = button_name
|
||||
# button.setName(button_name)
|
||||
|
||||
# 设置节点标签
|
||||
button.setTag("gui_type", "button")
|
||||
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
|
||||
button.setTag("gui_text", text)
|
||||
button.setTag("is_gui_element", "1")
|
||||
button.setTag("is_scene_element", "1")
|
||||
button.setTag("is_scene_element", "1") # 确保这个标签被设置
|
||||
button.setTag("tree_item_type", "GUI_BUTTON")
|
||||
button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性
|
||||
button.setTag("gui_element_type", "button")
|
||||
button.setTag("created_by_user", "1")
|
||||
button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||
button.setTag("name", button_name)
|
||||
button.setName(button_name)
|
||||
|
||||
# 如果有GUI父节点,建立引用关系
|
||||
@ -200,6 +220,7 @@ class GUIManager:
|
||||
|
||||
# 添加到GUI元素列表
|
||||
self.gui_elements.append(button)
|
||||
button.reparentTo(self.world.aspect2d)
|
||||
|
||||
print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}")
|
||||
|
||||
@ -221,11 +242,11 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的按钮并更新场景树
|
||||
if created_buttons:
|
||||
last_button, last_qt_item = created_buttons[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
||||
# if created_buttons:
|
||||
# last_button, last_qt_item = created_buttons[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮")
|
||||
|
||||
@ -301,9 +322,11 @@ class GUIManager:
|
||||
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
|
||||
label.setTag("gui_text", text)
|
||||
label.setTag("is_gui_element", "1")
|
||||
label.setTag("tree_item_type", "GUI_LABEL")
|
||||
label.setTag("is_scene_element", "1")
|
||||
label.setTag("created_by_user", "1")
|
||||
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||
label.setTag("name",label_name)
|
||||
label.setName(label_name)
|
||||
|
||||
# 如果有GUI父节点,建立引用关系
|
||||
@ -334,11 +357,11 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的标签并更新场景树
|
||||
if created_labels:
|
||||
last_label, last_qt_item = created_labels[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
tree_widget.update_selection_and_properties(last_label, last_qt_item)
|
||||
# if created_labels:
|
||||
# last_label, last_qt_item = created_labels[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# tree_widget.update_selection_and_properties(last_label, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签")
|
||||
|
||||
@ -394,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,
|
||||
@ -404,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)
|
||||
)
|
||||
|
||||
# 设置节点标签
|
||||
@ -412,9 +441,11 @@ class GUIManager:
|
||||
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
|
||||
entry.setTag("gui_placeholder", placeholder)
|
||||
entry.setTag("is_gui_element", "1")
|
||||
entry.setTag("tree_item_type", "GUI_ENTRY")
|
||||
entry.setTag("is_scene_element", "1")
|
||||
entry.setTag("created_by_user", "1")
|
||||
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||
entry.setTag("name",entry_name)
|
||||
entry.setName(entry_name)
|
||||
|
||||
# 如果有GUI父节点,建立引用关系
|
||||
@ -445,11 +476,11 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的输入框并更新场景树
|
||||
if created_entries:
|
||||
last_entry, last_qt_item = created_entries[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
tree_widget.update_selection_and_properties(last_entry, last_qt_item)
|
||||
# if created_entries:
|
||||
# last_entry, last_qt_item = created_entries[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# tree_widget.update_selection_and_properties(last_entry, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
|
||||
|
||||
@ -509,7 +540,11 @@ class GUIManager:
|
||||
cm = CardMaker('gui-2d-image')
|
||||
cm.setFrame(-size, size, -size, size)
|
||||
|
||||
image_node = self.world.aspect2d.attachNewNode(cm.generate())
|
||||
# image_node = self.world.aspect2d.attachNewNode(cm.generate())
|
||||
if parent_gui_node:
|
||||
image_node = parent_gui_node.attachNewNode(cm.generate())
|
||||
else:
|
||||
image_node = self.world.aspect2d.attachNewNode(cm.generate())
|
||||
image_node.setPos(gui_pos)
|
||||
image_node.setBin('fixed', 0)
|
||||
image_node.setDepthWrite(False)
|
||||
@ -522,6 +557,7 @@ class GUIManager:
|
||||
# 如果提供了图像路径,则加载纹理
|
||||
if image_path:
|
||||
try:
|
||||
image_node.setTag("image_path", image_path)
|
||||
texture = self.world.loader.loadTexture(image_path)
|
||||
if texture:
|
||||
image_node.setTexture(texture, 1)
|
||||
@ -541,8 +577,10 @@ class GUIManager:
|
||||
image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}")
|
||||
image_node.setTag("is_gui_element", "1")
|
||||
image_node.setTag("is_scene_element", "1")
|
||||
image_node.setTag("tree_item_type", "GUI_IMAGE")
|
||||
image_node.setTag("created_by_user", "1")
|
||||
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||
image_node.setTag("name",image_name)
|
||||
image_node.setName(image_name)
|
||||
|
||||
# 如果有GUI父节点,建立引用关系
|
||||
@ -573,11 +611,11 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的按钮并更新场景树
|
||||
if created_2dimage:
|
||||
last_button, last_qt_item = created_2dimage[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
||||
# if created_2dimage:
|
||||
# last_button, last_qt_item = created_2dimage[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮")
|
||||
|
||||
@ -692,7 +730,9 @@ class GUIManager:
|
||||
textNodePath.setTag("gui_text", text)
|
||||
textNodePath.setTag("is_gui_element", "1")
|
||||
textNodePath.setTag("is_scene_element", "1")
|
||||
textNodePath.setTag("tree_item_type", "GUI_3DTEXT")
|
||||
textNodePath.setTag("created_by_user", "1")
|
||||
textNodePath.setTag("name", text_name)
|
||||
|
||||
# 添加到GUI元素列表
|
||||
self.gui_elements.append(textNodePath)
|
||||
@ -717,11 +757,11 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的文本并更新场景树
|
||||
if created_texts:
|
||||
last_text, last_qt_item = created_texts[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
tree_widget.update_selection_and_properties(last_text, last_qt_item)
|
||||
# if created_texts:
|
||||
# last_text, last_qt_item = created_texts[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# tree_widget.update_selection_and_properties(last_text, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_texts)} 个3D文本")
|
||||
|
||||
@ -740,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
|
||||
|
||||
@ -779,7 +820,8 @@ class GUIManager:
|
||||
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 = self.world.render.attachNewNode(cm.generate())
|
||||
image_node = parent_node.attachNewNode(cm.generate())
|
||||
image_node.setPos(*pos)
|
||||
|
||||
# 为3D图像创建独立的材质
|
||||
@ -824,10 +866,12 @@ class GUIManager:
|
||||
image_node.setTag("gui_type", "3d_image")
|
||||
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
|
||||
if image_path:
|
||||
image_node.setTag("gui_image_path", image_path)
|
||||
image_node.setTag("image_path", image_path)
|
||||
image_node.setTag("is_gui_element", "1")
|
||||
image_node.setTag("is_scene_element", "1")
|
||||
image_node.setTag("tree_item_type", "GUI_3DIMAGE")
|
||||
image_node.setTag("created_by_user", "1")
|
||||
image_node.setTag("name",image_name)
|
||||
|
||||
# 添加到GUI元素列表
|
||||
self.gui_elements.append(image_node)
|
||||
@ -852,11 +896,11 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的文本并更新场景树
|
||||
if created_3dimage:
|
||||
last_image, last_qt_item = created_3dimage[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
tree_widget.update_selection_and_properties(last_image, last_qt_item)
|
||||
# if created_3dimage:
|
||||
# last_image, last_qt_item = created_3dimage[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# tree_widget.update_selection_and_properties(last_image, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本")
|
||||
|
||||
@ -872,7 +916,7 @@ class GUIManager:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
|
||||
def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None):
|
||||
"""创建3D视频播放屏幕 - 添加占位符纹理支持"""
|
||||
try:
|
||||
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
|
||||
@ -895,7 +939,7 @@ class GUIManager:
|
||||
size = float(size)
|
||||
except (ValueError, TypeError):
|
||||
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}")
|
||||
size = 0.2
|
||||
size = 0.2*5
|
||||
|
||||
print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}")
|
||||
|
||||
@ -944,7 +988,9 @@ class GUIManager:
|
||||
video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}")
|
||||
video_screen.setTag("is_gui_element", "1")
|
||||
video_screen.setTag("is_scene_element", "1")
|
||||
video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN")
|
||||
video_screen.setTag("created_by_user", "1")
|
||||
video_screen.setTag("name",screen_name)
|
||||
|
||||
# 设置视频路径标签
|
||||
if video_path and os.path.exists(video_path):
|
||||
@ -1042,12 +1088,12 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的视频屏幕
|
||||
if created_videoscreens:
|
||||
last_screen_np, last_qt_item = created_videoscreens[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||
# if created_videoscreens:
|
||||
# last_screen_np, last_qt_item = created_videoscreens[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# # 更新选择和属性面板
|
||||
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕")
|
||||
|
||||
@ -1125,6 +1171,7 @@ class GUIManager:
|
||||
# 检查是否有播放方法
|
||||
if hasattr(movie_texture, 'play'):
|
||||
try:
|
||||
self.loadVideoFile(video_screen, video_screen.getTag("video_path"))
|
||||
movie_texture.play()
|
||||
print(f"▶️ 继续播放视频: {video_screen.getName()}")
|
||||
return True
|
||||
@ -1135,9 +1182,7 @@ class GUIManager:
|
||||
print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}")
|
||||
self._debugVideoScreenTextures(video_screen)
|
||||
return False
|
||||
self.loadVideoFile(video_screen,video_screen.getTag("video_path"))
|
||||
except Exception as e:
|
||||
print(f"❌ 播放视频失败: {e}")
|
||||
import traceback
|
||||
@ -1264,7 +1309,6 @@ class GUIManager:
|
||||
def loadVideoFile(self, video_screen, video_path):
|
||||
"""为视频屏幕加载新的视频文件"""
|
||||
try:
|
||||
from panda3d.core import Texture, TextureStage
|
||||
import os
|
||||
|
||||
if not os.path.exists(video_path):
|
||||
@ -1422,8 +1466,6 @@ class GUIManager:
|
||||
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}")
|
||||
size = 0.2
|
||||
|
||||
print(f"📺 开始创建2D视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}")
|
||||
|
||||
# 获取树形控件
|
||||
tree_widget = self._get_tree_widget()
|
||||
if not tree_widget:
|
||||
@ -1450,7 +1492,7 @@ class GUIManager:
|
||||
frameColor=(1, 1, 1, 1), # 默认背景色
|
||||
pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标
|
||||
parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d,
|
||||
suppressMouse=True
|
||||
suppressMouse=True,
|
||||
)
|
||||
|
||||
video_screen.setName(screen_name)
|
||||
@ -1458,19 +1500,18 @@ class GUIManager:
|
||||
# 设置透明度支持
|
||||
video_screen.setTransparency(TransparencyAttrib.MAlpha)
|
||||
|
||||
# 设置2D视频屏幕特有的标签
|
||||
#设置2D视频屏幕特有的标签
|
||||
video_screen.setTag("gui_type", "2d_video_screen")
|
||||
video_screen.setTag("gui_id", f"2d_video_screen_{len(self.gui_elements)}")
|
||||
video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}")
|
||||
video_screen.setTag("is_gui_element", "1")
|
||||
video_screen.setTag("is_scene_element", "1")
|
||||
video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN")
|
||||
video_screen.setTag("created_by_user", "1")
|
||||
video_screen.setTag("name",screen_name)
|
||||
|
||||
# 设置视频路径标签
|
||||
if video_path and os.path.exists(video_path):
|
||||
video_screen.setTag("video_path", video_path)
|
||||
else:
|
||||
video_screen.setTag("video_path", "")
|
||||
video_screen.setTag("video_path", video_path if video_path else "")
|
||||
print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}")
|
||||
|
||||
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
|
||||
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
|
||||
@ -1543,12 +1584,12 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的视频屏幕
|
||||
if created_videoscreens:
|
||||
last_screen_np, last_qt_item = created_videoscreens[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||
# if created_videoscreens:
|
||||
# last_screen_np, last_qt_item = created_videoscreens[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# # 更新选择和属性面板
|
||||
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕")
|
||||
|
||||
@ -1569,7 +1610,11 @@ class GUIManager:
|
||||
try:
|
||||
import os
|
||||
|
||||
if not os.path.exists(video_path):
|
||||
video_screen.setTag("video_path",video_path)
|
||||
path = video_screen.getTag("video_path")
|
||||
print(f"🔧 更新2D视频屏幕标签 - video_path: {path}")
|
||||
|
||||
if not video_path or not os.path.exists(video_path):
|
||||
print(f"❌ 2D视频文件不存在: {video_path}")
|
||||
return False
|
||||
|
||||
@ -1581,7 +1626,6 @@ class GUIManager:
|
||||
|
||||
# 保存视频纹理引用
|
||||
video_screen.setPythonTag("movie_texture", movie_texture)
|
||||
video_screen.setTag("video_path", video_path)
|
||||
|
||||
print(f"✅ 成功加载新2D视频: {video_path}")
|
||||
return True
|
||||
@ -1776,6 +1820,7 @@ class GUIManager:
|
||||
# 设置标签以便识别和管理
|
||||
sphere_np.setTag("gui_type", "spherical_video")
|
||||
sphere_np.setTag("is_gui_element", "1")
|
||||
sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO")
|
||||
sphere_np.setTag("video_path", video_path or "")
|
||||
sphere_np.setTag("original_radius", str(radius))
|
||||
|
||||
@ -1808,12 +1853,12 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的球形视频
|
||||
if created_spherical_videos:
|
||||
last_sphere_np, last_qt_item = created_spherical_videos[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
|
||||
# if created_spherical_videos:
|
||||
# last_sphere_np, last_qt_item = created_spherical_videos[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# # 更新选择和属性面板
|
||||
# tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频")
|
||||
|
||||
@ -1988,6 +2033,7 @@ class GUIManager:
|
||||
virtual_screen.setTag("gui_text", text)
|
||||
virtual_screen.setTag("is_gui_element", "1")
|
||||
virtual_screen.setTag("is_scene_element", "1")
|
||||
virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen")
|
||||
virtual_screen.setTag("created_by_user", "1")
|
||||
|
||||
# 添加到GUI元素列表
|
||||
@ -2013,12 +2059,12 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 选中最后创建的虚拟屏幕
|
||||
if created_screens:
|
||||
last_screen_np, last_qt_item = created_screens[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||
# if created_screens:
|
||||
# last_screen_np, last_qt_item = created_screens[-1]
|
||||
# if last_qt_item:
|
||||
# tree_widget.setCurrentItem(last_qt_item)
|
||||
# # 更新选择和属性面板
|
||||
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕")
|
||||
|
||||
@ -2070,7 +2116,9 @@ class GUIManager:
|
||||
pass
|
||||
return None
|
||||
|
||||
# 暂无滑块功能
|
||||
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
|
||||
pass
|
||||
"""创建2D GUI滑块"""
|
||||
from direct.gui.DirectGui import DirectSlider
|
||||
|
||||
@ -2105,19 +2153,22 @@ class GUIManager:
|
||||
"""删除GUI元素"""
|
||||
try:
|
||||
if gui_element in self.gui_elements:
|
||||
# 移除GUI元素
|
||||
if hasattr(gui_element, 'removeNode'):
|
||||
gui_element.removeNode()
|
||||
elif hasattr(gui_element, 'destroy'):
|
||||
gui_element.destroy()
|
||||
|
||||
# 从列表中移除
|
||||
self.gui_elements.remove(gui_element)
|
||||
# # 移除GUI元素
|
||||
# if hasattr(gui_element, 'removeNode'):
|
||||
# gui_element.removeNode()
|
||||
# elif hasattr(gui_element, 'destroy'):
|
||||
# gui_element.destroy()
|
||||
#
|
||||
# # 从列表中移除
|
||||
# self.gui_elements.remove(gui_element)
|
||||
|
||||
# 更新场景树
|
||||
# 安全地调用updateSceneTree
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
tree_widget = self._get_tree_widget()
|
||||
if tree_widget:
|
||||
tree_widget.delete_items(tree_widget.selectedItems())
|
||||
# if hasattr(self.world, 'updateSceneTree'):
|
||||
# self.world.updateSceneTree()
|
||||
|
||||
print(f"删除GUI元素: {gui_element}")
|
||||
return True
|
||||
@ -2910,7 +2961,7 @@ class GUIManager:
|
||||
heightSpinBox.valueChanged.connect(
|
||||
lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v))
|
||||
transform_layout.addWidget(heightSpinBox, 4, 3)
|
||||
|
||||
|
||||
else:
|
||||
# 3D GUI组件使用世界坐标
|
||||
transform_layout.addWidget(QLabel("位置"), 0, 0)
|
||||
@ -2990,11 +3041,11 @@ class GUIManager:
|
||||
transform_layout.addWidget(scale_z, 3, 3)
|
||||
transform_group.setLayout(transform_layout)
|
||||
self._propertyLayout.addWidget(transform_group)
|
||||
|
||||
|
||||
# 缩放属性
|
||||
if hasattr(gui_element, 'getScale'):
|
||||
scale = gui_element.getScale()
|
||||
|
||||
|
||||
scaleSpinBox = QDoubleSpinBox()
|
||||
scaleSpinBox.setRange(0.01, 100)
|
||||
scaleSpinBox.setSingleStep(0.1)
|
||||
@ -3207,7 +3258,7 @@ class GUIManager:
|
||||
try:
|
||||
gui_type = gui_element.getTag("gui_type")
|
||||
|
||||
if gui_type in ["3d_text", "3d_image", "video_screen","info_panel"]:
|
||||
if gui_type in ["3d_text", "3d_image", "video_screen","info_panel","info_panel_3d"]:
|
||||
current_pos = gui_element.getPos()
|
||||
|
||||
if axis == "x":
|
||||
@ -3220,7 +3271,7 @@ class GUIManager:
|
||||
return False
|
||||
|
||||
gui_element.setPos(*new_pos)
|
||||
print(f"✓ 更新3D GUI元素位置: {axis}={value}")
|
||||
#print(f"✓ 更新3D GUI元素位置: {axis}={value}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}")
|
||||
@ -3241,7 +3292,7 @@ class GUIManager:
|
||||
if value == 0:
|
||||
value = 0.01
|
||||
|
||||
if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel"]:
|
||||
if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel","info_panel_3d"]:
|
||||
# 3D元素处理
|
||||
if axis == "x":
|
||||
new_scale = (value, current_scale.getY(), current_scale.getZ())
|
||||
|
||||
BIN
icons/logo.png
Normal file
BIN
icons/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
133
main.py
133
main.py
@ -22,6 +22,7 @@ from core.script_system import ScriptManager
|
||||
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 gui.gui_manager import GUIManager
|
||||
from core.terrain_manager import TerrainManager
|
||||
from scene.scene_manager import SceneManager
|
||||
@ -55,7 +56,16 @@ class MyWorld(CoreWorld):
|
||||
|
||||
# 初始化选择和变换系统
|
||||
self.selection = SelectionSystem(self)
|
||||
|
||||
|
||||
# 绑定F键用于聚焦选中节点
|
||||
self.accept("f", self.onFocusKeyPressed)
|
||||
self.accept("F", self.onFocusKeyPressed) # 大写F
|
||||
|
||||
#初始化巡检系统
|
||||
self.patrol_system = PatrolSystem(self)
|
||||
self.accept("p",self.onPatrolKeyPressed)
|
||||
self.accept("P",self.onPatrolKeyPressed)
|
||||
|
||||
# 初始化事件处理系统
|
||||
self.event_handler = EventHandler(self)
|
||||
|
||||
@ -225,7 +235,7 @@ 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)
|
||||
|
||||
@ -233,9 +243,9 @@ class MyWorld(CoreWorld):
|
||||
"""创建3D图片"""
|
||||
return self.gui_manager.createGUI3DImage(pos,text,size)
|
||||
|
||||
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2):
|
||||
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1):
|
||||
"""创建2D GUI图片"""
|
||||
return self.gui_manager.createGUI2DImage(pos, image_path, size)
|
||||
return self.gui_manager.createGUI2DImage(pos, image_path, size*0.2)
|
||||
|
||||
def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
|
||||
"""创建视频屏幕"""
|
||||
@ -492,10 +502,6 @@ class MyWorld(CoreWorld):
|
||||
"""异步导入模型"""
|
||||
return self.scene_manager.importModelAsync(filepath)
|
||||
|
||||
def loadAnimatedModel(self, model_path, anims=None):
|
||||
"""加载带动画的模型"""
|
||||
return self.scene_manager.loadAnimatedModel(model_path, anims)
|
||||
|
||||
# 材质和几何体处理方法 - 代理到scene_manager
|
||||
def processMaterials(self, model):
|
||||
"""处理模型材质"""
|
||||
@ -814,6 +820,97 @@ class MyWorld(CoreWorld):
|
||||
"""获取碰撞统计"""
|
||||
return self.collision_manager.getCollisionStatistics()
|
||||
|
||||
def setupKeyboardEvents(self):
|
||||
"""设置键盘事件"""
|
||||
try:
|
||||
# 绑定 F 键用于聚焦选中节点
|
||||
self.accept("f", self.onFocusKeyPressed)
|
||||
self.accept("F", self.onFocusKeyPressed) # 大写F
|
||||
|
||||
print("✓ 键盘事件绑定完成")
|
||||
|
||||
except Exception as e:
|
||||
print(f"设置键盘事件失败: {e}")
|
||||
|
||||
def onFocusKeyPressed(self):
|
||||
"""处理 F 键按下事件"""
|
||||
try:
|
||||
#print("检测到 F 键按下")
|
||||
|
||||
# 检查是否有选中的节点
|
||||
if hasattr(self, 'selection') and self.selection.selectedNode:
|
||||
#print(f"当前选中节点: {self.selection.selectedNode.getName()}")
|
||||
# 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本)
|
||||
# self.selection.focusCameraOnSelectedNode() # 无动画版本
|
||||
self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本
|
||||
else:
|
||||
print("当前没有选中任何节点")
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理 F 键事件失败: {e}")
|
||||
|
||||
def onPatrolKeyPressed(self):
|
||||
"""处理 P 键按下事件 - 控制巡检系统"""
|
||||
try:
|
||||
print("检测到 P 键按下")
|
||||
|
||||
if not self.patrol_system.is_patrolling:
|
||||
# 如果巡检系统没有点,创建默认巡检路线
|
||||
if not self.patrol_system.patrol_points:
|
||||
self.createDefaultPatrolRoute()
|
||||
|
||||
# 开始巡检
|
||||
if self.patrol_system.start_patrol():
|
||||
print("✓ 巡检已开始")
|
||||
else:
|
||||
print("✗ 巡检启动失败")
|
||||
else:
|
||||
# 停止巡检
|
||||
if self.patrol_system.stop_patrol():
|
||||
print("✓ 巡检已停止")
|
||||
else:
|
||||
print("✗ 巡检停止失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理 P 键事件失败: {e}")
|
||||
|
||||
def createDefaultPatrolRoute(self):
|
||||
"""创建默认巡检路线(使用自动朝向)"""
|
||||
try:
|
||||
# 清空现有巡检点
|
||||
self.patrol_system.clear_patrol_points()
|
||||
|
||||
# 添加巡检点,使用None表示朝向下一个点
|
||||
self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5)
|
||||
self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5)
|
||||
self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5)
|
||||
self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5)
|
||||
|
||||
# 最后一个点可以指定特定的朝向,或者也设为None继续循环
|
||||
self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5)
|
||||
|
||||
print("✓ 默认自动朝向巡检路线已创建")
|
||||
self.patrol_system.list_patrol_points()
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建默认自动朝向巡检路线失败: {e}")
|
||||
|
||||
def _serializeNode(self, node):
|
||||
"""序列化节点数据"""
|
||||
try:
|
||||
return self.world.scene_manager.serializeNode(node)
|
||||
except Exception as e:
|
||||
print(f"序列化节点失败: {e}")
|
||||
return None
|
||||
|
||||
def _deserializeNode(self, node_data, parent_node):
|
||||
"""反序列化节点数据"""
|
||||
try:
|
||||
return self.world.scene_manager.deserializeNode(node_data, parent_node)
|
||||
except Exception as e:
|
||||
print(f"反序列化节点失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== 项目管理功能代理 ====================
|
||||
# 以下函数代理到project_manager模块的对应功能
|
||||
@ -830,6 +927,7 @@ def createNewProject(parent_window):
|
||||
|
||||
def saveProject(appw):
|
||||
"""保存项目 - 代理到project_manager"""
|
||||
|
||||
world = appw.centralWidget().world
|
||||
return world.project_manager.saveProject(appw)
|
||||
|
||||
@ -838,19 +936,26 @@ def openProject(appw):
|
||||
world = appw.centralWidget().world
|
||||
return world.project_manager.openProject(appw)
|
||||
|
||||
def openProjectForPath(project_path, appw):
|
||||
"""打开项目 - 代理到project_manager"""
|
||||
world = appw.centralWidget().world
|
||||
return world.project_manager.openProjectForPath(project_path, appw)
|
||||
|
||||
def buildPackage(appw):
|
||||
"""打包项目 - 代理到project_manager"""
|
||||
world = appw.centralWidget().world
|
||||
return world.project_manager.buildPackage(appw)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def run(args = None):
|
||||
world = MyWorld()
|
||||
|
||||
# 使用新的UI模块创建主窗口
|
||||
from ui.main_window import setup_main_window
|
||||
|
||||
app, main_window = setup_main_window(world)
|
||||
|
||||
print(f'Path is {args}')
|
||||
app, main_window = setup_main_window(world, args)
|
||||
|
||||
# 启动应用程序
|
||||
sys.exit(app.exec_())
|
||||
sys.exit(app.exec_())
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@ -9,7 +9,6 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
import subprocess
|
||||
import shutil
|
||||
@ -47,7 +46,9 @@ class ProjectManager:
|
||||
|
||||
project_path = dialog.projectPath
|
||||
project_name = dialog.projectName
|
||||
full_project_path = os.path.join(project_path, project_name)
|
||||
# full_project_path = os.path.join(project_path, project_name)
|
||||
full_project_path = os.path.normpath(os.path.join(project_path, project_name))
|
||||
print(f"full_project_path: {full_project_path}")
|
||||
|
||||
try:
|
||||
# 创建项目文件夹结构
|
||||
@ -72,13 +73,13 @@ class ProjectManager:
|
||||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||||
|
||||
print(f"项目配置文件已创建: {config_file}")
|
||||
|
||||
|
||||
# 清空当前场景
|
||||
self._clearCurrentScene()
|
||||
|
||||
|
||||
# 自动保存初始场景
|
||||
scene_file = os.path.join(scenes_path, "scene.bam")
|
||||
if self.world.scene_manager.saveScene(scene_file):
|
||||
if self.world.scene_manager.saveScene(scene_file, project_path):
|
||||
# 更新配置文件中的场景路径
|
||||
project_config["scene_file"] = os.path.relpath(scene_file, full_project_path)
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
@ -122,57 +123,157 @@ class ProjectManager:
|
||||
|
||||
if not project_path:
|
||||
return False
|
||||
|
||||
|
||||
# 检查是否是有效的项目文件夹
|
||||
config_file = os.path.join(project_path, "project.json")
|
||||
if not os.path.exists(config_file):
|
||||
QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!")
|
||||
return False
|
||||
|
||||
|
||||
# 读取项目配置
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
project_config = json.load(f)
|
||||
|
||||
|
||||
# 检查场景文件
|
||||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||
if not os.path.exists(scene_file):
|
||||
QMessageBox.warning(parent_window, "警告", "没有找到场景文件!")
|
||||
return False
|
||||
|
||||
# 加载场景
|
||||
if self.world.scene_manager.loadScene(scene_file):
|
||||
# 更新项目配置
|
||||
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||||
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||||
|
||||
# 更新项目状态
|
||||
self.current_project_path = project_path
|
||||
self.project_config = project_config
|
||||
|
||||
# 保存当前项目路径到主窗口
|
||||
parent_window.current_project_path = project_path
|
||||
|
||||
# 更新窗口标题
|
||||
project_name = os.path.basename(project_path)
|
||||
self.updateWindowTitle(parent_window, project_name)
|
||||
|
||||
# 更新文件浏览器
|
||||
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
|
||||
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
|
||||
|
||||
QMessageBox.information(parent_window, "成功", "项目加载成功!")
|
||||
return True
|
||||
else:
|
||||
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
|
||||
return False
|
||||
|
||||
if os.path.exists(scene_file):
|
||||
# 加载场景
|
||||
if self.world.scene_manager.loadScene(scene_file):
|
||||
# 更新项目配置
|
||||
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||||
|
||||
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||||
|
||||
# 更新项目状态
|
||||
self.current_project_path = project_path
|
||||
self.project_config = project_config
|
||||
|
||||
# 保存当前项目路径到主窗口
|
||||
parent_window.current_project_path = project_path
|
||||
|
||||
# 更新窗口标题
|
||||
project_name = os.path.basename(project_path)
|
||||
self.updateWindowTitle(parent_window, project_name)
|
||||
|
||||
# 更新文件浏览器
|
||||
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
|
||||
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
|
||||
|
||||
QMessageBox.information(parent_window, "成功", "项目加载成功!")
|
||||
return True
|
||||
# 检查场景文件
|
||||
# scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||
# if not os.path.exists(scene_file):
|
||||
# QMessageBox.warning(parent_window, "警告", "没有找到场景文件!")
|
||||
# return False
|
||||
#
|
||||
# # 加载场景
|
||||
# if self.world.scene_manager.loadScene(scene_file):
|
||||
# # 更新项目配置
|
||||
# project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||||
#
|
||||
# with open(config_file, "w", encoding="utf-8") as f:
|
||||
# json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||||
#
|
||||
# # 更新项目状态
|
||||
# self.current_project_path = project_path
|
||||
# self.project_config = project_config
|
||||
#
|
||||
# # 保存当前项目路径到主窗口
|
||||
# parent_window.current_project_path = project_path
|
||||
#
|
||||
# # 更新窗口标题
|
||||
# project_name = os.path.basename(project_path)
|
||||
# self.updateWindowTitle(parent_window, project_name)
|
||||
#
|
||||
# # 更新文件浏览器
|
||||
# if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
|
||||
# parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
|
||||
#
|
||||
# QMessageBox.information(parent_window, "成功", "项目加载成功!")
|
||||
# return True
|
||||
# else:
|
||||
# QMessageBox.warning(parent_window, "错误", "加载场景失败!")
|
||||
# return False
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def openProjectForPath(self, project_path, parent_window=None):
|
||||
"""通过路径打开项目
|
||||
|
||||
Args:
|
||||
project_path: 项目路径
|
||||
parent_window: 父窗口对象(可选)
|
||||
"""
|
||||
try:
|
||||
if not project_path:
|
||||
return False
|
||||
# 检查是否是有效的项目文件夹
|
||||
config_file = os.path.join(project_path, "project.json")
|
||||
if not os.path.exists(config_file):
|
||||
if parent_window:
|
||||
QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}")
|
||||
else:
|
||||
print("警告: 选择的不是有效的项目文件夹!")
|
||||
return False
|
||||
|
||||
# 读取项目配置
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
project_config = json.load(f)
|
||||
|
||||
# 检查场景文件
|
||||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||
if os.path.exists(scene_file):
|
||||
# 加载场景
|
||||
if self.world.scene_manager.loadScene(scene_file):
|
||||
# 更新项目配置
|
||||
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||||
|
||||
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||||
|
||||
# 更新项目状态
|
||||
self.current_project_path = project_path
|
||||
self.project_config = project_config
|
||||
|
||||
# 如果有父窗口,更新相关UI元素
|
||||
if parent_window:
|
||||
# 保存当前项目路径到主窗口
|
||||
parent_window.current_project_path = project_path
|
||||
|
||||
# 更新窗口标题
|
||||
project_name = os.path.basename(project_path)
|
||||
self.updateWindowTitle(parent_window, project_name)
|
||||
|
||||
# 更新文件浏览器
|
||||
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
|
||||
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
|
||||
|
||||
QMessageBox.information(parent_window, "成功", "项目加载成功!")
|
||||
|
||||
print(f"项目 '{project_path}' 加载成功!")
|
||||
return True
|
||||
else:
|
||||
if parent_window:
|
||||
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
|
||||
else:
|
||||
print("错误: 加载场景失败!")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"加载项目时发生错误:{str(e)}"
|
||||
if parent_window:
|
||||
QMessageBox.critical(parent_window, "错误", error_msg)
|
||||
else:
|
||||
print(error_msg)
|
||||
return False
|
||||
|
||||
def saveProject(self, parent_window):
|
||||
"""保存项目"""
|
||||
try:
|
||||
@ -201,7 +302,7 @@ class ProjectManager:
|
||||
return False
|
||||
|
||||
# 保存场景
|
||||
if self.world.scene_manager.saveScene(scene_file):
|
||||
if self.world.scene_manager.saveScene(scene_file, project_path):
|
||||
# 更新项目配置文件
|
||||
config_file = os.path.join(project_path, "project.json")
|
||||
if os.path.exists(config_file):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -25,9 +25,9 @@ class CrossPlatformPathHandler:
|
||||
def normalize_model_path(self, filepath):
|
||||
"""标准化模型文件路径"""
|
||||
try:
|
||||
print(f"\n=== 路径标准化处理 ===")
|
||||
print(f"原始路径: {filepath}")
|
||||
print(f"当前系统: {self.system}")
|
||||
#print(f"\n=== 路径标准化处理 ===")
|
||||
#print(f"原始路径: {filepath}")
|
||||
#print(f"当前系统: {self.system}")
|
||||
|
||||
# 步骤1: 检查原始路径是否存在
|
||||
if self._check_file_exists(filepath):
|
||||
@ -54,10 +54,6 @@ class CrossPlatformPathHandler:
|
||||
def _check_file_exists(self, filepath):
|
||||
"""检查文件是否存在"""
|
||||
exists = os.path.exists(filepath)
|
||||
if exists:
|
||||
print(f"✓ 文件存在: {filepath}")
|
||||
else:
|
||||
print(f"⚠️ 文件不存在: {filepath}")
|
||||
return exists
|
||||
|
||||
def _panda3d_normalize(self, filepath):
|
||||
|
||||
@ -21,6 +21,10 @@ class RotatorScript(ScriptBase):
|
||||
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
|
||||
|
||||
def update(self, dt):
|
||||
# 检查 gameObject 是否存在且不为空
|
||||
if not self.gameObject or self.gameObject.isEmpty():
|
||||
print("RotatorScript: gameObject is empty or None, skipping update")
|
||||
return
|
||||
"""每帧更新"""
|
||||
if not self.is_rotating:
|
||||
return
|
||||
|
||||
106
threejs_panel.html
Normal file
106
threejs_panel.html
Normal file
@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Three.js Panel</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
#info {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 100;
|
||||
}
|
||||
#canvas-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="info">
|
||||
<h3>场景信息面板</h3>
|
||||
<p>FPS: <span id="fps">0</span></p>
|
||||
<p>对象数: <span id="object-count">0</span></p>
|
||||
</div>
|
||||
<div id="canvas-container"></div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script>
|
||||
// 初始化Three.js场景
|
||||
let scene, camera, renderer;
|
||||
let cube;
|
||||
|
||||
function init() {
|
||||
// 创建场景
|
||||
scene = new THREE.Scene();
|
||||
|
||||
// 创建相机
|
||||
camera = new THREE.PerspectiveCamera(75,
|
||||
document.getElementById('canvas-container').offsetWidth /
|
||||
document.getElementById('canvas-container').offsetHeight,
|
||||
0.1, 1000);
|
||||
|
||||
// 创建渲染器
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(
|
||||
document.getElementById('canvas-container').offsetWidth,
|
||||
document.getElementById('canvas-container').offsetHeight
|
||||
);
|
||||
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
||||
|
||||
// 添加一个立方体
|
||||
const geometry = new THREE.BoxGeometry();
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0x00ff00,
|
||||
wireframe: true
|
||||
});
|
||||
cube = new THREE.Mesh(geometry, material);
|
||||
scene.add(cube);
|
||||
|
||||
camera.position.z = 5;
|
||||
|
||||
// 开始动画循环
|
||||
animate();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
camera.aspect = document.getElementById('canvas-container').offsetWidth /
|
||||
document.getElementById('canvas-container').offsetHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(
|
||||
document.getElementById('canvas-container').offsetWidth,
|
||||
document.getElementById('canvas-container').offsetHeight
|
||||
);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
// 旋转立方体
|
||||
cube.rotation.x += 0.01;
|
||||
cube.rotation.y += 0.01;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// 接收来自Python的消息
|
||||
function updateInfo(data) {
|
||||
document.getElementById('fps').textContent = data.fps || 0;
|
||||
document.getElementById('object-count').textContent = data.objectCount || 0;
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
window.onload = init;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
322
ui/icon_manager.py
Normal file
322
ui/icon_manager.py
Normal file
@ -0,0 +1,322 @@
|
||||
"""
|
||||
图标管理工具
|
||||
|
||||
负责统一管理应用程序中的所有图标:
|
||||
- 图标路径解析
|
||||
- 图标缓存
|
||||
- 图标预加载
|
||||
- 图标错误处理
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Optional
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtCore import QSize
|
||||
|
||||
|
||||
class IconManager:
|
||||
"""图标管理器类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化图标管理器"""
|
||||
self.icon_cache: Dict[str, QIcon] = {}
|
||||
self.icon_directory = self._get_icon_directory()
|
||||
self.default_icon = None
|
||||
|
||||
# 预定义的图标映射
|
||||
self.icon_map = {
|
||||
# 主窗口图标
|
||||
'app_logo': 'logo.png',
|
||||
|
||||
# 工具栏图标
|
||||
'select_tool': 'select_tool.png',
|
||||
'move_tool': 'move_tool.png',
|
||||
'rotate_tool': 'rotate_tool.png',
|
||||
'scale_tool': 'scale_tool.png',
|
||||
|
||||
# 菜单图标(如果有的话)
|
||||
'new_file': 'new_file.png',
|
||||
'open_file': 'open_file.png',
|
||||
'save_file': 'save_file.png',
|
||||
'exit': 'exit.png',
|
||||
|
||||
# 对象类型图标
|
||||
'object_3d': 'object_3d.png',
|
||||
'light': 'light.png',
|
||||
'camera': 'camera.png',
|
||||
'terrain': 'terrain.png',
|
||||
'script': 'script.png',
|
||||
|
||||
# 状态图标
|
||||
'success': 'success.png',
|
||||
'warning': 'warning.png',
|
||||
'error': 'error.png',
|
||||
'info': 'info.png',
|
||||
}
|
||||
|
||||
# 初始化默认图标
|
||||
self._create_default_icon()
|
||||
|
||||
# 预加载常用图标
|
||||
self._preload_icons()
|
||||
|
||||
def _get_icon_directory(self) -> str:
|
||||
"""获取图标目录的绝对路径"""
|
||||
# 获取当前文件的目录(ui目录)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 获取项目根目录(ui的父目录)
|
||||
project_root = os.path.dirname(current_dir)
|
||||
# 拼接icons目录路径
|
||||
icon_dir = os.path.join(project_root, "icons")
|
||||
|
||||
print(f"🔍 图标目录路径: {icon_dir}")
|
||||
|
||||
# 检查目录是否存在
|
||||
if not os.path.exists(icon_dir):
|
||||
print(f"⚠️ 图标目录不存在: {icon_dir}")
|
||||
# 尝试创建目录
|
||||
try:
|
||||
os.makedirs(icon_dir, exist_ok=True)
|
||||
print(f"✅ 已创建图标目录: {icon_dir}")
|
||||
except Exception as e:
|
||||
print(f"❌ 创建图标目录失败: {e}")
|
||||
|
||||
return icon_dir
|
||||
|
||||
def _create_default_icon(self):
|
||||
"""创建默认图标"""
|
||||
# 创建一个简单的默认图标
|
||||
pixmap = QPixmap(16, 16)
|
||||
pixmap.fill() # 填充为白色
|
||||
self.default_icon = QIcon(pixmap)
|
||||
|
||||
def _preload_icons(self):
|
||||
"""预加载常用图标"""
|
||||
print("🔄 开始预加载图标...")
|
||||
|
||||
for icon_name, file_name in self.icon_map.items():
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
icon = QIcon(icon_path)
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 已加载图标: {icon_name} -> {file_name}")
|
||||
except Exception as e:
|
||||
print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}")
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
|
||||
print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标")
|
||||
|
||||
def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon:
|
||||
"""
|
||||
获取图标
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称(可以是预定义名称或文件名)
|
||||
size: 图标尺寸
|
||||
|
||||
Returns:
|
||||
QIcon对象
|
||||
"""
|
||||
# 首先检查缓存
|
||||
if icon_name in self.icon_cache:
|
||||
icon = self.icon_cache[icon_name]
|
||||
if size:
|
||||
# 如果指定了尺寸,返回指定尺寸的图标
|
||||
pixmap = icon.pixmap(size)
|
||||
return QIcon(pixmap)
|
||||
return icon
|
||||
|
||||
# 如果不在缓存中,尝试从映射中获取
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
else:
|
||||
# 直接使用文件名
|
||||
icon_path = os.path.join(self.icon_directory, icon_name)
|
||||
if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
icon_path += '.png' # 默认添加.png扩展名
|
||||
|
||||
# 尝试加载图标
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
icon = QIcon(icon_path)
|
||||
# 缓存图标
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}")
|
||||
|
||||
if size:
|
||||
pixmap = icon.pixmap(size)
|
||||
return QIcon(pixmap)
|
||||
return icon
|
||||
except Exception as e:
|
||||
print(f"❌ 加载图标失败: {icon_path}, 错误: {e}")
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
|
||||
# 返回默认图标
|
||||
return self.default_icon
|
||||
|
||||
def get_icon_path(self, icon_name: str) -> str:
|
||||
"""
|
||||
获取图标文件的完整路径
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
|
||||
Returns:
|
||||
图标文件的完整路径
|
||||
"""
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
else:
|
||||
file_name = icon_name
|
||||
if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
file_name += '.png'
|
||||
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
|
||||
if os.path.exists(icon_path):
|
||||
return icon_path
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
return ""
|
||||
|
||||
def has_icon(self, icon_name: str) -> bool:
|
||||
"""
|
||||
检查图标是否存在
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
|
||||
Returns:
|
||||
是否存在
|
||||
"""
|
||||
return bool(self.get_icon_path(icon_name))
|
||||
|
||||
def add_icon(self, icon_name: str, icon_path: str) -> bool:
|
||||
"""
|
||||
添加新图标到缓存
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(icon_path):
|
||||
icon = QIcon(icon_path)
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 图标文件不存在: {icon_path}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
def refresh_cache(self):
|
||||
"""刷新图标缓存"""
|
||||
print("🔄 刷新图标缓存...")
|
||||
self.icon_cache.clear()
|
||||
self._preload_icons()
|
||||
|
||||
def get_available_icons(self) -> list:
|
||||
"""获取所有可用的图标列表"""
|
||||
available_icons = []
|
||||
|
||||
# 添加预定义的图标
|
||||
available_icons.extend(self.icon_map.keys())
|
||||
|
||||
# 扫描图标目录中的所有图标文件
|
||||
if os.path.exists(self.icon_directory):
|
||||
for file_name in os.listdir(self.icon_directory):
|
||||
if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
icon_name = os.path.splitext(file_name)[0]
|
||||
if icon_name not in available_icons:
|
||||
available_icons.append(icon_name)
|
||||
|
||||
return sorted(available_icons)
|
||||
|
||||
def get_cache_info(self) -> dict:
|
||||
"""获取缓存信息"""
|
||||
return {
|
||||
'cached_icons': len(self.icon_cache),
|
||||
'icon_directory': self.icon_directory,
|
||||
'available_icons': len(self.get_available_icons()),
|
||||
'cache_keys': list(self.icon_cache.keys())
|
||||
}
|
||||
|
||||
def debug_info(self):
|
||||
"""打印调试信息"""
|
||||
print("=" * 50)
|
||||
print("📋 图标管理器调试信息")
|
||||
print("=" * 50)
|
||||
|
||||
info = self.get_cache_info()
|
||||
print(f"图标目录: {info['icon_directory']}")
|
||||
print(f"目录存在: {os.path.exists(info['icon_directory'])}")
|
||||
print(f"缓存图标数: {info['cached_icons']}")
|
||||
print(f"可用图标数: {info['available_icons']}")
|
||||
|
||||
if info['cache_keys']:
|
||||
print("\n已缓存的图标:")
|
||||
for key in info['cache_keys']:
|
||||
print(f" - {key}")
|
||||
|
||||
print("\n图标目录内容:")
|
||||
if os.path.exists(self.icon_directory):
|
||||
for file_name in os.listdir(self.icon_directory):
|
||||
file_path = os.path.join(self.icon_directory, file_name)
|
||||
size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0
|
||||
print(f" - {file_name} ({size} bytes)")
|
||||
else:
|
||||
print(" 目录不存在")
|
||||
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# 全局图标管理器实例
|
||||
_icon_manager = None
|
||||
|
||||
|
||||
def get_icon_manager() -> IconManager:
|
||||
"""获取全局图标管理器实例"""
|
||||
global _icon_manager
|
||||
if _icon_manager is None:
|
||||
_icon_manager = IconManager()
|
||||
return _icon_manager
|
||||
|
||||
|
||||
def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon:
|
||||
"""便捷函数:获取图标"""
|
||||
return get_icon_manager().get_icon(icon_name, size)
|
||||
|
||||
|
||||
def get_icon_path(icon_name: str) -> str:
|
||||
"""便捷函数:获取图标路径"""
|
||||
return get_icon_manager().get_icon_path(icon_name)
|
||||
|
||||
|
||||
def has_icon(icon_name: str) -> bool:
|
||||
"""便捷函数:检查图标是否存在"""
|
||||
return get_icon_manager().has_icon(icon_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("🧪 测试图标管理器...")
|
||||
|
||||
manager = IconManager()
|
||||
manager.debug_info()
|
||||
|
||||
# 测试获取图标
|
||||
logo_icon = manager.get_icon('app_logo')
|
||||
print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}")
|
||||
|
||||
move_tool_icon = manager.get_icon('move_tool')
|
||||
print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}")
|
||||
405
ui/icon_manager_gui.py
Normal file
405
ui/icon_manager_gui.py
Normal file
@ -0,0 +1,405 @@
|
||||
"""
|
||||
图标管理器GUI工具
|
||||
|
||||
提供图形界面来管理和查看图标:
|
||||
- 显示所有可用图标
|
||||
- 图标预览
|
||||
- 图标信息
|
||||
- 图标刷新
|
||||
"""
|
||||
import os
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QListWidget, QListWidgetItem, QLabel, QGroupBox,
|
||||
QTextEdit, QSplitter, QDialog, QDialogButtonBox,
|
||||
QFileDialog, QMessageBox, QScrollArea, QGridLayout)
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QFont
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
|
||||
from ui.icon_manager import get_icon_manager
|
||||
|
||||
|
||||
class IconPreviewWidget(QWidget):
|
||||
"""图标预览控件"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setupUI()
|
||||
|
||||
def setupUI(self):
|
||||
"""设置UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 图标显示区域
|
||||
self.icon_label = QLabel()
|
||||
self.icon_label.setAlignment(Qt.AlignCenter)
|
||||
self.icon_label.setStyleSheet("""
|
||||
QLabel {
|
||||
border: 2px dashed #8b5cf6;
|
||||
border-radius: 8px;
|
||||
background-color: #2d2d44;
|
||||
color: #e0e0ff;
|
||||
min-height: 100px;
|
||||
margin: 10px;
|
||||
}
|
||||
""")
|
||||
self.icon_label.setText("选择图标查看预览")
|
||||
layout.addWidget(self.icon_label)
|
||||
|
||||
# 图标信息
|
||||
self.info_label = QLabel()
|
||||
self.info_label.setStyleSheet("""
|
||||
QLabel {
|
||||
background-color: #252538;
|
||||
color: #e0e0ff;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
""")
|
||||
self.info_label.setText("图标信息将在此显示")
|
||||
layout.addWidget(self.info_label)
|
||||
|
||||
def showIcon(self, icon_name: str, icon: QIcon):
|
||||
"""显示图标"""
|
||||
if not icon.isNull():
|
||||
# 显示不同尺寸的图标
|
||||
sizes = [16, 24, 32, 48, 64]
|
||||
pixmaps = []
|
||||
|
||||
# 创建组合图标显示
|
||||
for size in sizes:
|
||||
pixmap = icon.pixmap(QSize(size, size))
|
||||
if not pixmap.isNull():
|
||||
pixmaps.append((size, pixmap))
|
||||
|
||||
if pixmaps:
|
||||
# 创建合成图片显示多个尺寸
|
||||
total_width = sum(size for size, _ in pixmaps) + 20 * (len(pixmaps) - 1)
|
||||
max_height = max(size for size, _ in pixmaps)
|
||||
|
||||
combined_pixmap = QPixmap(total_width, max_height + 40)
|
||||
combined_pixmap.fill(Qt.transparent)
|
||||
|
||||
from PyQt5.QtGui import QPainter, QPen
|
||||
painter = QPainter(combined_pixmap)
|
||||
|
||||
x = 0
|
||||
for size, pixmap in pixmaps:
|
||||
# 绘制图标
|
||||
y = (max_height - size) // 2
|
||||
painter.drawPixmap(x, y, pixmap)
|
||||
|
||||
# 绘制尺寸标签
|
||||
painter.setPen(QPen(Qt.white))
|
||||
painter.drawText(x, max_height + 15, f"{size}x{size}")
|
||||
|
||||
x += size + 20
|
||||
|
||||
painter.end()
|
||||
|
||||
self.icon_label.setPixmap(combined_pixmap)
|
||||
else:
|
||||
self.icon_label.setText("无法加载图标")
|
||||
else:
|
||||
self.icon_label.setText("图标无效")
|
||||
|
||||
# 更新信息
|
||||
info_text = f"图标名称: {icon_name}\n"
|
||||
info_text += f"图标有效: {'是' if not icon.isNull() else '否'}\n"
|
||||
|
||||
# 获取图标管理器信息
|
||||
icon_manager = get_icon_manager()
|
||||
icon_path = icon_manager.get_icon_path(icon_name)
|
||||
if icon_path:
|
||||
info_text += f"文件路径: {icon_path}\n"
|
||||
if os.path.exists(icon_path):
|
||||
size = os.path.getsize(icon_path)
|
||||
info_text += f"文件大小: {size} bytes\n"
|
||||
|
||||
# 获取可用尺寸
|
||||
if not icon.isNull():
|
||||
available_sizes = icon.availableSizes()
|
||||
if available_sizes:
|
||||
sizes_text = ", ".join(f"{s.width()}x{s.height()}" for s in available_sizes)
|
||||
info_text += f"可用尺寸: {sizes_text}\n"
|
||||
|
||||
self.info_label.setText(info_text)
|
||||
|
||||
|
||||
class IconManagerDialog(QDialog):
|
||||
"""图标管理器对话框"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.icon_manager = get_icon_manager()
|
||||
self.setupUI()
|
||||
self.loadIcons()
|
||||
|
||||
def setupUI(self):
|
||||
"""设置UI"""
|
||||
self.setWindowTitle("图标管理器")
|
||||
self.setModal(False)
|
||||
self.resize(800, 600)
|
||||
|
||||
# 设置样式
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #1e1e2e;
|
||||
color: #e0e0ff;
|
||||
}
|
||||
QListWidget {
|
||||
background-color: #252538;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
border-radius: 4px;
|
||||
alternate-background-color: #2d2d44;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #3a3a4a;
|
||||
}
|
||||
QListWidget::item:hover {
|
||||
background-color: #3a3a4a;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: rgba(139, 92, 246, 100);
|
||||
color: white;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6d28d9;
|
||||
}
|
||||
QGroupBox {
|
||||
background-color: #252538;
|
||||
border: 1px solid #3a3a4a;
|
||||
border-radius: 6px;
|
||||
margin-top: 1ex;
|
||||
color: #e0e0ff;
|
||||
font-weight: 500;
|
||||
padding-top: 10px;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subline-offset: -2px;
|
||||
padding: 0 8px;
|
||||
color: #c0c0e0;
|
||||
font-weight: 500;
|
||||
}
|
||||
QTextEdit {
|
||||
background-color: #252538;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 顶部按钮栏
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.refresh_btn = QPushButton("刷新图标")
|
||||
self.refresh_btn.clicked.connect(self.refreshIcons)
|
||||
button_layout.addWidget(self.refresh_btn)
|
||||
|
||||
self.add_icon_btn = QPushButton("添加图标")
|
||||
self.add_icon_btn.clicked.connect(self.addIcon)
|
||||
button_layout.addWidget(self.add_icon_btn)
|
||||
|
||||
self.debug_btn = QPushButton("调试信息")
|
||||
self.debug_btn.clicked.connect(self.showDebugInfo)
|
||||
button_layout.addWidget(self.debug_btn)
|
||||
|
||||
button_layout.addStretch()
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# 主分割器
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# 左侧:图标列表
|
||||
left_widget = QWidget()
|
||||
left_layout = QVBoxLayout(left_widget)
|
||||
|
||||
list_group = QGroupBox("可用图标")
|
||||
list_layout = QVBoxLayout(list_group)
|
||||
|
||||
self.icon_list = QListWidget()
|
||||
self.icon_list.itemSelectionChanged.connect(self.onIconSelected)
|
||||
list_layout.addWidget(self.icon_list)
|
||||
|
||||
left_layout.addWidget(list_group)
|
||||
splitter.addWidget(left_widget)
|
||||
|
||||
# 右侧:图标预览
|
||||
right_widget = QWidget()
|
||||
right_layout = QVBoxLayout(right_widget)
|
||||
|
||||
preview_group = QGroupBox("图标预览")
|
||||
preview_layout = QVBoxLayout(preview_group)
|
||||
|
||||
self.preview_widget = IconPreviewWidget()
|
||||
preview_layout.addWidget(self.preview_widget)
|
||||
|
||||
right_layout.addWidget(preview_group)
|
||||
splitter.addWidget(right_widget)
|
||||
|
||||
# 设置分割器比例
|
||||
splitter.setSizes([300, 500])
|
||||
layout.addWidget(splitter)
|
||||
|
||||
# 底部按钮
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
button_box.rejected.connect(self.close)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
def loadIcons(self):
|
||||
"""加载图标列表"""
|
||||
self.icon_list.clear()
|
||||
|
||||
available_icons = self.icon_manager.get_available_icons()
|
||||
|
||||
for icon_name in available_icons:
|
||||
item = QListWidgetItem()
|
||||
|
||||
# 获取图标
|
||||
icon = self.icon_manager.get_icon(icon_name, QSize(24, 24))
|
||||
|
||||
# 设置图标和文本
|
||||
if not icon.isNull():
|
||||
item.setIcon(icon)
|
||||
item.setText(f"🎨 {icon_name}")
|
||||
else:
|
||||
item.setText(f"❌ {icon_name}")
|
||||
|
||||
item.setData(Qt.UserRole, icon_name)
|
||||
self.icon_list.addItem(item)
|
||||
|
||||
print(f"📊 加载了 {len(available_icons)} 个图标")
|
||||
|
||||
def onIconSelected(self):
|
||||
"""当选择图标时"""
|
||||
current_item = self.icon_list.currentItem()
|
||||
if current_item:
|
||||
icon_name = current_item.data(Qt.UserRole)
|
||||
icon = self.icon_manager.get_icon(icon_name)
|
||||
self.preview_widget.showIcon(icon_name, icon)
|
||||
|
||||
def refreshIcons(self):
|
||||
"""刷新图标"""
|
||||
print("🔄 刷新图标缓存...")
|
||||
self.icon_manager.refresh_cache()
|
||||
self.loadIcons()
|
||||
QMessageBox.information(self, "完成", "图标缓存已刷新")
|
||||
|
||||
def addIcon(self):
|
||||
"""添加新图标"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择图标文件",
|
||||
"",
|
||||
"图像文件 (*.png *.jpg *.jpeg *.svg *.ico);;所有文件 (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
# 获取文件名作为图标名称
|
||||
file_name = os.path.basename(file_path)
|
||||
icon_name = os.path.splitext(file_name)[0]
|
||||
|
||||
# 复制文件到图标目录
|
||||
import shutil
|
||||
target_path = os.path.join(self.icon_manager.icon_directory, file_name)
|
||||
|
||||
try:
|
||||
shutil.copy2(file_path, target_path)
|
||||
|
||||
# 添加到缓存
|
||||
success = self.icon_manager.add_icon(icon_name, target_path)
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, "成功", f"图标 '{icon_name}' 已添加")
|
||||
self.loadIcons()
|
||||
else:
|
||||
QMessageBox.warning(self, "失败", "添加图标失败")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"复制文件失败:\n{str(e)}")
|
||||
|
||||
def showDebugInfo(self):
|
||||
"""显示调试信息"""
|
||||
debug_dialog = QDialog(self)
|
||||
debug_dialog.setWindowTitle("图标管理器调试信息")
|
||||
debug_dialog.resize(600, 400)
|
||||
|
||||
layout = QVBoxLayout(debug_dialog)
|
||||
|
||||
text_edit = QTextEdit()
|
||||
text_edit.setFont(QFont("Consolas", 10))
|
||||
|
||||
# 获取调试信息
|
||||
info = self.icon_manager.get_cache_info()
|
||||
debug_text = "图标管理器调试信息\n"
|
||||
debug_text += "=" * 50 + "\n\n"
|
||||
debug_text += f"图标目录: {info['icon_directory']}\n"
|
||||
debug_text += f"目录存在: {os.path.exists(info['icon_directory'])}\n"
|
||||
debug_text += f"缓存图标数: {info['cached_icons']}\n"
|
||||
debug_text += f"可用图标数: {info['available_icons']}\n\n"
|
||||
|
||||
debug_text += "已缓存的图标:\n"
|
||||
for key in info['cache_keys']:
|
||||
debug_text += f" - {key}\n"
|
||||
|
||||
debug_text += "\n图标目录内容:\n"
|
||||
if os.path.exists(info['icon_directory']):
|
||||
for file_name in os.listdir(info['icon_directory']):
|
||||
file_path = os.path.join(info['icon_directory'], file_name)
|
||||
if os.path.isfile(file_path):
|
||||
size = os.path.getsize(file_path)
|
||||
debug_text += f" - {file_name} ({size} bytes)\n"
|
||||
else:
|
||||
debug_text += " 目录不存在\n"
|
||||
|
||||
text_edit.setPlainText(debug_text)
|
||||
layout.addWidget(text_edit)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
button_box.rejected.connect(debug_dialog.close)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
debug_dialog.exec_()
|
||||
|
||||
|
||||
def show_icon_manager(parent=None):
|
||||
"""显示图标管理器对话框"""
|
||||
dialog = IconManagerDialog(parent)
|
||||
dialog.show()
|
||||
return dialog
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 设置全局样式
|
||||
app.setStyleSheet("""
|
||||
QApplication {
|
||||
background-color: #1e1e2e;
|
||||
color: #e0e0ff;
|
||||
}
|
||||
""")
|
||||
|
||||
dialog = IconManagerDialog()
|
||||
dialog.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@ -25,10 +25,39 @@ class InterfaceManager:
|
||||
# 更新场景树
|
||||
self.world.scene_manager.updateSceneTree()
|
||||
|
||||
def onTreeWidgetClicked(self, index):
|
||||
"""处理树形控件点击事件(包括空白区域)"""
|
||||
# 检查点击的是否是空白区域
|
||||
if not self.treeWidget.itemFromIndex(index): # 点击的是空白区域
|
||||
self.world.selection.updateSelection(None)
|
||||
self.world.property_panel.clearPropertyPanel()
|
||||
print("点击树形控件空白区域,清除选中状态")
|
||||
|
||||
def onTreeCurrentItemChanged(self, current, previous):
|
||||
"""处理树形控件当前选中项改变事件"""
|
||||
# 当 current 为 None 时,表示点击了空白区域
|
||||
if current is None:
|
||||
self.world.selection.updateSelection(None)
|
||||
print("点击空白区域,清除选中状态")
|
||||
# 当 current 不为 None 时,表示选中了某个项目
|
||||
else:
|
||||
# 更新选择状态
|
||||
nodePath = current.data(0, Qt.UserRole)
|
||||
if nodePath:
|
||||
self.world.selected_np = nodePath
|
||||
self.world.selection.updateSelection(nodePath)
|
||||
self.world.property_panel.updatePropertyPanel(current)
|
||||
print(f"树形控件选中项改变: {current.text(0)}")
|
||||
|
||||
def onTreeItemClicked(self, item, column):
|
||||
"""处理树形控件项目点击事件"""
|
||||
if not item:
|
||||
print(f"树形控件点击事件触发,item: {item}, column: {column}")
|
||||
|
||||
# 检查是否点击了空白区域
|
||||
# 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象
|
||||
if not item or (item.text(0) == "" and item.data(0, Qt.UserRole) is None):
|
||||
self.world.selection.updateSelection(None)
|
||||
print("点击空白区域,清除选中状态")
|
||||
return
|
||||
|
||||
self.world.property_panel.updatePropertyPanel(item)
|
||||
@ -39,187 +68,90 @@ class InterfaceManager:
|
||||
# 更新选择状态
|
||||
self.world.selected_np = nodePath
|
||||
self.world.selection.updateSelection(nodePath)
|
||||
|
||||
# 更新属性面板
|
||||
#self.world.property_panel.updatePropertyPanel(item)
|
||||
|
||||
print(f"树形控件点击: {item.text(0)}")
|
||||
else:
|
||||
# 如果没有节点对象,清除选择
|
||||
self.world.selection.updateSelection(None)
|
||||
#self.world.property_panel.clearPropertyPanel()
|
||||
|
||||
def showTreeContextMenu(self, position):
|
||||
"""显示树形控件的右键菜单"""
|
||||
item = self.treeWidget.itemAt(position)
|
||||
if not item:
|
||||
return
|
||||
|
||||
# 获取节点对象
|
||||
nodePath = item.data(0, Qt.UserRole)
|
||||
if not nodePath:
|
||||
return
|
||||
|
||||
# 创建菜单
|
||||
menu = QMenu()
|
||||
|
||||
# 检查是否是GUI元素
|
||||
if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"):
|
||||
# GUI元素菜单
|
||||
editAction = menu.addAction("编辑")
|
||||
editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath))
|
||||
|
||||
deleteAction = menu.addAction("删除GUI元素")
|
||||
deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath))
|
||||
|
||||
duplicateAction = menu.addAction("复制")
|
||||
duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath))
|
||||
|
||||
elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset":
|
||||
deleteAction = menu.addAction("删除 Cesium Tileset")
|
||||
deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item))
|
||||
|
||||
else:
|
||||
#灯光节点添加特殊处理
|
||||
if self.isLightNode(nodePath):
|
||||
deleteAction = menu.addAction("删除灯光")
|
||||
deleteAction.triggered.connect(lambda:self.deleteLightNode(nodePath,item))
|
||||
else:
|
||||
deleteAction = menu.addAction("删除")
|
||||
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
|
||||
|
||||
# 显示菜单
|
||||
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
|
||||
|
||||
def isLightNode(self, nodePath):
|
||||
try:
|
||||
if not nodePath or nodePath.isEmpty():
|
||||
return False
|
||||
|
||||
# 修复:统一使用 rp_light_object
|
||||
if hasattr(nodePath, 'getPythonTag'):
|
||||
light_object = nodePath.getPythonTag('rp_light_object')
|
||||
if light_object is not None:
|
||||
return True
|
||||
|
||||
if hasattr(nodePath, 'getTag'):
|
||||
light_type = nodePath.getTag('light_type')
|
||||
if light_type in ["spot_light", "point_light"]:
|
||||
return True
|
||||
|
||||
if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight:
|
||||
return True
|
||||
if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight:
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"判断节点是否是灯光节点失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def deleteLightNode(self, nodePath, item):
|
||||
"""专门处理灯光节点的删除"""
|
||||
try:
|
||||
print(f"开始删除灯光节点: {nodePath.getName()}")
|
||||
|
||||
# 从RenderPipeline中移除灯光(如果存在)
|
||||
if hasattr(nodePath, 'getPythonTag'):
|
||||
light_object = nodePath.getPythonTag('rp_light_object')
|
||||
if light_object and hasattr(self.world, 'render_pipeline'):
|
||||
print("从RenderPipeline移除灯光")
|
||||
self.world.render_pipeline.remove_light(light_object)
|
||||
nodePath.clearPythonTag('rp_light_object')
|
||||
|
||||
if hasattr(self.world,'Spotlight') and nodePath in self.world.Spotlight:
|
||||
self.world.Spotlight.remove(nodePath)
|
||||
print("从Spotlight列表中删除")
|
||||
if hasattr(self.world,'Pointlight') and nodePath in self.world.Pointlight:
|
||||
self.world.Pointlight.remove(nodePath)
|
||||
print("从Pointlight列表中移除")
|
||||
|
||||
if hasattr(self.world,'selection'):
|
||||
if self.world.selection.selectedNode == nodePath:
|
||||
self.world.selection.clearSelectionBox()
|
||||
self.world.selection.clearGizmo()
|
||||
self.world.selection.selectedNode = None
|
||||
self.world.selection.selectedObject = None
|
||||
|
||||
print(f"移除节点{nodePath.getName()}")
|
||||
nodePath.removeNode()
|
||||
|
||||
parentItem = item.parent()
|
||||
if parentItem:
|
||||
parentItem.removeChild(item)
|
||||
|
||||
print(f"成功删除灯光节点{nodePath.getName()}")
|
||||
|
||||
if hasattr(self.world,'property_panel'):
|
||||
self.world.property_panel.clearPropertyPanel()
|
||||
if hasattr(self.world,'selection'):
|
||||
self.world.selection.updateSelection(None)
|
||||
|
||||
except Exception as e:
|
||||
print(f"删除灯光节点失败: {str(e)}")
|
||||
|
||||
def _recursiveRemoveLights(self, nodePath):
|
||||
"""递归删除节点及其子节点中的所有灯光"""
|
||||
if nodePath.isEmpty():
|
||||
return
|
||||
|
||||
# 先递归处理所有子节点
|
||||
for child in nodePath.getChildren():
|
||||
self._recursiveRemoveLights(child)
|
||||
|
||||
# 然后处理当前节点
|
||||
if self.isLightNode(nodePath):
|
||||
print(f"删除子灯光节点: {nodePath.getName()}")
|
||||
|
||||
# 从RenderPipeline中移除灯光
|
||||
if hasattr(nodePath, 'getPythonTag'):
|
||||
light_object = nodePath.getPythonTag('rp_light_object')
|
||||
if light_object and hasattr(self.world, 'render_pipeline'):
|
||||
self.world.render_pipeline.remove_light(light_object)
|
||||
nodePath.clearPythonTag('rp_light_object')
|
||||
|
||||
# 从灯光列表中移除
|
||||
if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight:
|
||||
self.world.Spotlight.remove(nodePath)
|
||||
if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight:
|
||||
self.world.Pointlight.remove(nodePath)
|
||||
|
||||
def deleteCesiumTileset(self, nodePath, item):
|
||||
"""删除 Cesium tileset"""
|
||||
try:
|
||||
# 从场景中移除
|
||||
nodePath.removeNode()
|
||||
|
||||
# 从 tilesets 列表中移除
|
||||
if hasattr(self.world, 'scene_manager'):
|
||||
tilesets_to_remove = []
|
||||
for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
|
||||
if tileset_info['node'] == nodePath:
|
||||
tilesets_to_remove.append(i)
|
||||
|
||||
# 从后往前删除,避免索引问题
|
||||
for i in reversed(tilesets_to_remove):
|
||||
del self.world.scene_manager.tilesets[i]
|
||||
|
||||
# 从树形控件中移除
|
||||
parentItem = item.parent()
|
||||
if parentItem:
|
||||
parentItem.removeChild(item)
|
||||
|
||||
print(f"成功删除 Cesium tileset: {nodePath.getName()}")
|
||||
|
||||
# 清空属性面板和选择框
|
||||
self.world.property_panel.clearPropertyPanel()
|
||||
self.world.selection.updateSelection(None)
|
||||
print("点击了无数据项,清除选中状态")
|
||||
|
||||
# 更新场景树
|
||||
self.updateSceneTree()
|
||||
|
||||
except Exception as e:
|
||||
print(f"删除 Cesium tileset 失败: {str(e)}")
|
||||
# def showTreeContextMenu(self, position):
|
||||
# """显示树形控件的右键菜单"""
|
||||
# item = self.treeWidget.itemAt(position)
|
||||
# if not item:
|
||||
# return
|
||||
#
|
||||
# # 获取节点对象
|
||||
# nodePath = item.data(0, Qt.UserRole)
|
||||
# if not nodePath:
|
||||
# return
|
||||
#
|
||||
# # 创建菜单
|
||||
# menu = QMenu()
|
||||
#
|
||||
# # 检查是否是GUI元素
|
||||
# if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"):
|
||||
# # GUI元素菜单
|
||||
# editAction = menu.addAction("编辑")
|
||||
# editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath))
|
||||
#
|
||||
# deleteAction = menu.addAction("删除GUI元素")
|
||||
# deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath))
|
||||
#
|
||||
# duplicateAction = menu.addAction("复制")
|
||||
# duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath))
|
||||
#
|
||||
# elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset":
|
||||
# deleteAction = menu.addAction("删除 Cesium Tileset")
|
||||
# deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item))
|
||||
#
|
||||
# else:
|
||||
# # 为模型节点或其子节点添加删除选项
|
||||
# parentItem = item.parent()
|
||||
# if parentItem:
|
||||
# if self.isModelOrChild(item):
|
||||
# deleteAction = menu.addAction("删除")
|
||||
# deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
|
||||
# else:
|
||||
# deleteAction = menu.addAction("删除")
|
||||
# deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
|
||||
#
|
||||
# # 显示菜单
|
||||
# menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
|
||||
#
|
||||
# def deleteCesiumTileset(self, nodePath, item):
|
||||
# """删除 Cesium tileset"""
|
||||
# try:
|
||||
# # 从场景中移除
|
||||
# nodePath.removeNode()
|
||||
#
|
||||
# # 从 tilesets 列表中移除
|
||||
# if hasattr(self.world, 'scene_manager'):
|
||||
# tilesets_to_remove = []
|
||||
# for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
|
||||
# if tileset_info['node'] == nodePath:
|
||||
# tilesets_to_remove.append(i)
|
||||
#
|
||||
# # 从后往前删除,避免索引问题
|
||||
# for i in reversed(tilesets_to_remove):
|
||||
# del self.world.scene_manager.tilesets[i]
|
||||
#
|
||||
# # 从树形控件中移除
|
||||
# parentItem = item.parent()
|
||||
# if parentItem:
|
||||
# parentItem.removeChild(item)
|
||||
#
|
||||
# print(f"成功删除 Cesium tileset: {nodePath.getName()}")
|
||||
#
|
||||
# # 清空属性面板和选择框
|
||||
# self.world.property_panel.clearPropertyPanel()
|
||||
# self.world.selection.updateSelection(None)
|
||||
#
|
||||
# # 更新场景树
|
||||
# self.updateSceneTree()
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f"删除 Cesium tileset 失败: {str(e)}")
|
||||
|
||||
def isModelOrChild(self, item):
|
||||
"""检查是否是模型节点或其子节点"""
|
||||
@ -248,9 +180,9 @@ class InterfaceManager:
|
||||
if terrain_to_remove:
|
||||
self.world.terrain_manager.deleteTerrain(terrain_to_remove)
|
||||
print(f"成功删除地形节点:{nodePath.getName()}")
|
||||
self.updateSceneTree()
|
||||
self.world.property_panel.clearPropertyPanel()
|
||||
self.world.selection.updateSelection(None)
|
||||
# self.updateSceneTree()
|
||||
# self.world.property_panel.clearPropertyPanel()
|
||||
# self.world.selection.updateSelection(None)
|
||||
return
|
||||
|
||||
# 先递归删除所有子节点中的灯光
|
||||
@ -408,10 +340,19 @@ class InterfaceManager:
|
||||
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
|
||||
|
||||
#添加灯光节点
|
||||
for light in self.world.Spotlight + self.world.Pointlight:
|
||||
if not light.isEmpty:
|
||||
for light in self.world.Spotlight:
|
||||
if light:
|
||||
addNodeToTree(light, sceneRoot, force=True)
|
||||
|
||||
for light in self.world.Pointlight:
|
||||
if light:
|
||||
addNodeToTree(light, sceneRoot, force=True)
|
||||
|
||||
# for light in self.world.Spotlight + self.world.Pointlight:
|
||||
# if not light.isEmpty:
|
||||
# print(f"33333333333333333333333333333{light}")
|
||||
# addNodeToTree(light, sceneRoot, force=True)
|
||||
|
||||
#添加 Cesium tilesets
|
||||
if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'):
|
||||
for i , tileset_info in enumerate(self.world.scene_manager.tilesets):
|
||||
|
||||
2030
ui/main_window.py
2030
ui/main_window.py
File diff suppressed because it is too large
Load Diff
3193
ui/property_panel.py
3193
ui/property_panel.py
File diff suppressed because it is too large
Load Diff
683
ui/widgets.py
683
ui/widgets.py
@ -17,7 +17,8 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
|
||||
from PyQt5.QtCore import Qt, QUrl, QMimeData
|
||||
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
|
||||
from PyQt5.sip import wrapinstance
|
||||
from panda3d.core import ModelRoot
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import ModelRoot, NodePath, CollisionNode
|
||||
|
||||
from QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||||
from scene import util
|
||||
@ -29,7 +30,67 @@ class NewProjectDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("新建项目")
|
||||
self.setMinimumWidth(500)
|
||||
|
||||
|
||||
# 设置对话框样式与主窗口保持一致
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #252538;
|
||||
color: #e0e0ff;
|
||||
}
|
||||
QGroupBox {
|
||||
background-color: #2d2d44;
|
||||
border: 1px solid #3a3a4a;
|
||||
border-radius: 6px;
|
||||
margin-top: 1ex; /* 保持这个设置 */
|
||||
color: #e0e0ff;
|
||||
font-weight: 500;
|
||||
padding-top: 10px; /* 增加顶部内边距,为标题留出空间 */
|
||||
}
|
||||
QGroupBox::title {
|
||||
padding: 0 8px;
|
||||
color: #c0c0e0;
|
||||
font-weight: 500;
|
||||
}
|
||||
QLineEdit {
|
||||
background-color: #2d2d44;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
QLineEdit:disabled {
|
||||
background-color: #1e1e2e;
|
||||
color: #8888aa;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6d28d9;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #4c4c6e;
|
||||
color: #8888aa;
|
||||
}
|
||||
QLabel {
|
||||
color: #e0e0ff;
|
||||
}
|
||||
QLabel:disabled {
|
||||
color: #8888aa;
|
||||
}
|
||||
QDialogButtonBox QPushButton {
|
||||
min-width: 80px;
|
||||
}
|
||||
""")
|
||||
|
||||
# 创建布局
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
@ -155,7 +216,7 @@ class CustomPanda3DWidget(QPanda3DWidget):
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
|
||||
def wheelEvent(self, event):
|
||||
"""处理滚轮事件"""
|
||||
if event.angleDelta().y() > 0:
|
||||
@ -606,8 +667,27 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
||||
"""获取项目根路径下的Resources文件夹,考虑跨平台"""
|
||||
import os
|
||||
|
||||
# 获取项目根路径
|
||||
project_root = os.getcwd()
|
||||
# 获取当前文件所在目录,然后向上查找项目根目录
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 向上查找直到找到项目根目录(包含特定标识文件或文件夹)
|
||||
project_root = current_dir
|
||||
max_depth = 10 # 限制向上查找的深度
|
||||
depth = 0
|
||||
|
||||
while depth < max_depth:
|
||||
# 检查是否是项目根目录(可以根据实际情况调整判断条件)
|
||||
if (os.path.exists(os.path.join(project_root, "main.py")) or
|
||||
os.path.exists(os.path.join(project_root, "setup.py")) or
|
||||
os.path.exists(os.path.join(project_root, ".git"))):
|
||||
break
|
||||
parent_dir = os.path.dirname(project_root)
|
||||
if parent_dir == project_root: # 已经到达文件系统根目录
|
||||
# 回退到使用当前工作目录
|
||||
project_root = os.getcwd()
|
||||
break
|
||||
project_root = parent_dir
|
||||
depth += 1
|
||||
|
||||
# 构建Resources文件夹路径(跨平台)
|
||||
resources_path = os.path.join(project_root, "Resources")
|
||||
@ -624,6 +704,28 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
||||
|
||||
return resources_path
|
||||
|
||||
# def getProjectRootPath(self):
|
||||
# """获取项目根路径下的Resources文件夹,考虑跨平台"""
|
||||
# import os
|
||||
#
|
||||
# # 获取项目根路径
|
||||
# project_root = os.getcwd()
|
||||
#
|
||||
# # 构建Resources文件夹路径(跨平台)
|
||||
# resources_path = os.path.join(project_root, "Resources")
|
||||
#
|
||||
# # 如果Resources文件夹不存在,创建它
|
||||
# if not os.path.exists(resources_path):
|
||||
# try:
|
||||
# os.makedirs(resources_path, exist_ok=True)
|
||||
# print(f"创建Resources文件夹: {resources_path}")
|
||||
# except OSError as e:
|
||||
# print(f"无法创建Resources文件夹: {e}")
|
||||
# # 如果无法创建,回退到项目根路径
|
||||
# return project_root
|
||||
#
|
||||
# return resources_path
|
||||
|
||||
def load_file_tree(self):
|
||||
"""加载树形视图"""
|
||||
self.clear()
|
||||
@ -654,7 +756,7 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
||||
if os.path.exists(directory) and directory not in self.watched_directories:
|
||||
if self.file_watcher.addPath(directory):
|
||||
self.watched_directories.add(directory)
|
||||
print(f"开始监控目录:{directory}")
|
||||
#print(f"开始监控目录:{directory}")
|
||||
try:
|
||||
for item in os.listdir(directory):
|
||||
item_path = os.path.join(directory,item)
|
||||
@ -1002,7 +1104,6 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
||||
internal_paths.append(filepath)
|
||||
# 检查是否是模型文件(用于向外拖拽)
|
||||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||||
print(f"模型路ing!!!!!!!!!!!!!!!!!{QUrl.fromLocalFile(filepath)}")
|
||||
urls.append(QUrl.fromLocalFile(filepath))
|
||||
|
||||
# 设置内部拖拽数据
|
||||
@ -1210,14 +1311,83 @@ class CustomConsoleDockWidget(QWidget):
|
||||
self.autoScrollBtn = QPushButton("自动滚动")
|
||||
self.autoScrollBtn.setCheckable(True)
|
||||
self.autoScrollBtn.setChecked(True)
|
||||
self.autoScrollBtn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2d2d44;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
border: 1px solid #7c3aed;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #3a3a4a;
|
||||
}
|
||||
QPushButton:checked:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6d28d9;
|
||||
}
|
||||
""")
|
||||
toolbar.addWidget(self.autoScrollBtn)
|
||||
|
||||
# 时间戳开关
|
||||
self.timestampBtn = QPushButton("显示时间")
|
||||
self.timestampBtn.setCheckable(True)
|
||||
self.timestampBtn.setChecked(True)
|
||||
self.timestampBtn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2d2d44;
|
||||
color: #e0e0ff;
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
border: 1px solid #7c3aed;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #3a3a4a;
|
||||
}
|
||||
QPushButton:checked:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #6d28d9;
|
||||
}
|
||||
""")
|
||||
toolbar.addWidget(self.timestampBtn)
|
||||
|
||||
self.fpsLabel = QLabel("FPS:0.0")
|
||||
self.fpsLabel.setStyleSheet("""
|
||||
QLabel {
|
||||
background-color: #2d2d44;
|
||||
color: #80ff80;
|
||||
border: 1px solid #3a3a4a;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
""")
|
||||
self.fpsLabel.setMinimumWidth(100)
|
||||
self.fpsLabel.setAlignment(Qt.AlignCenter)
|
||||
toolbar.addWidget(self.fpsLabel)
|
||||
|
||||
# 帧率更新定时器
|
||||
self.fpsTimer = QTimer()
|
||||
self.fpsTimer.timeout.connect(self.updateFPS)
|
||||
self.fpsTimer.start(1000) # 每秒更新一次
|
||||
|
||||
toolbar.addStretch()
|
||||
layout.addLayout(toolbar)
|
||||
|
||||
@ -1259,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
|
||||
@ -1369,42 +1567,66 @@ class CustomTreeWidget(QTreeWidget):
|
||||
parent = wrapinstance(0, QWidget)
|
||||
super().__init__(parent)
|
||||
self.world = world
|
||||
# self.selectedItems = None
|
||||
self.initData()
|
||||
self.setupUI() # 初始化界面
|
||||
self.setupContextMenu() # 初始化右键菜单
|
||||
|
||||
self.setupDragDrop() # 设置拖拽功能
|
||||
|
||||
self.original_scales={}
|
||||
|
||||
self.setStyleSheet("""
|
||||
/* 设置折叠状态下,带子节点的箭头颜色 */
|
||||
QTreeWidget::branch:has-children:!open {
|
||||
color: #8b5cf6; /* 紫色 */
|
||||
}
|
||||
|
||||
/* 设置展开状态下,带子节点的箭头颜色 */
|
||||
QTreeWidget::branch:has-children:open {
|
||||
color: #9ca3af; /* 灰色,提供状态变化反馈 */
|
||||
}
|
||||
|
||||
/* 鼠标悬停在任意箭头上时,颜色变亮 */
|
||||
QTreeWidget::branch:hover {
|
||||
color: #a78bfa; /* 亮紫色 */
|
||||
}
|
||||
""")
|
||||
|
||||
def initData(self):
|
||||
"""初始化变量"""
|
||||
# 定义2D GUI元素类型
|
||||
self.gui_2d_types = {
|
||||
"GUI_BUTTON", # DirectButton
|
||||
"GUI_LABEL", # DirectLabel
|
||||
"GUI_ENTRY", # DirectEntry
|
||||
"GUI_IMAGE",
|
||||
"GUI_BUTTON", # GUI 按钮
|
||||
"GUI_LABEL", # GUI 标签
|
||||
"GUI_ENTRY", # GUI 输入框
|
||||
"GUI_IMAGE", # GUI 图片
|
||||
"GUI_2D_VIDEO_SCREEN", # GUI 2D视频
|
||||
"GUI_SPHERICAL_VIDEO", # GUI 3D球形视频
|
||||
"GUI_NODE" # 其他2D GUI容器
|
||||
}
|
||||
|
||||
# 定义3D GUI元素类型
|
||||
self.gui_3d_types = {
|
||||
"GUI_3DTEXT", # 3D TextNode
|
||||
"GUI_3DIMAGE",
|
||||
"GUI_VIRTUAL_SCREEN" # Virtual Screen
|
||||
"GUI_3DTEXT", # 3D 文本节点
|
||||
"GUI_3DIMAGE", # 3D 图片节点
|
||||
"GUI_VIRTUAL_SCREEN", # 3D视频
|
||||
"GUI_VirtualScreen" # 3D虚拟视频
|
||||
}
|
||||
|
||||
# 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素)
|
||||
self.scene_3d_types = {
|
||||
"SCENE_ROOT",
|
||||
"SCENE_NODE",
|
||||
"LIGHT_NODE",
|
||||
"LIGHT_NODE", # 灯节点
|
||||
"CAMERA_NODE",
|
||||
"IMPORTED_MODEL_NODE",
|
||||
"MODEL_NODE"
|
||||
"IMPORTED_MODEL_NODE", # 导入模型节点
|
||||
"MODEL_NODE",
|
||||
"TERRAIN_NODE", # 地形节点
|
||||
"CESIUM_TILESET_NODE" # 3D Tileset
|
||||
}
|
||||
|
||||
# 这是一个最佳实践,它让代码的意图变得非常清晰。
|
||||
self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types)
|
||||
|
||||
def setupUI(self):
|
||||
"""初始化UI设置"""
|
||||
self.setHeaderHidden(True)
|
||||
@ -1427,12 +1649,9 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
def showContextMenu(self, position):
|
||||
"""显示右键菜单 - 复用主菜单的创建动作"""
|
||||
if not self.selectedItems():
|
||||
print("没有选中的项目,不显示右键菜单")
|
||||
return
|
||||
|
||||
item = self.selectedItems()[0]
|
||||
print(f"为项目 '{item.text(0)}' 显示右键菜单")
|
||||
if self.selectedItems():
|
||||
item = self.selectedItems()[0]
|
||||
print(f"为项目 '{item.text(0)}' 显示右键菜单")
|
||||
|
||||
# 创建右键菜单
|
||||
menu = QMenu(self)
|
||||
@ -1682,7 +1901,9 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
# 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下
|
||||
if is_dragged_2d_gui:
|
||||
if is_target_2d_gui:
|
||||
if target_type == "SCENE_ROOT":
|
||||
return True
|
||||
elif is_target_2d_gui:
|
||||
print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}")
|
||||
return True
|
||||
elif is_target_3d_gui:
|
||||
@ -1701,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元素建立父子关系")
|
||||
@ -1727,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
|
||||
@ -1773,6 +1994,20 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
"""处理拖动事件"""
|
||||
indicator_pos = self.dropIndicatorPosition()
|
||||
indicator_str = "Unknown"
|
||||
|
||||
if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem:
|
||||
indicator_str = "OnItem"
|
||||
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem:
|
||||
indicator_str = "AboveItem"
|
||||
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem:
|
||||
indicator_str = "BelowItem"
|
||||
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport:
|
||||
indicator_str = "OnViewport"
|
||||
|
||||
#print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
|
||||
|
||||
if event.source() != self:
|
||||
event.ignore()
|
||||
return
|
||||
@ -1811,6 +2046,8 @@ class CustomTreeWidget(QTreeWidget):
|
||||
selected_items = self.selectedItems()
|
||||
if selected_items:
|
||||
# 执行删除操作
|
||||
# if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE":
|
||||
# self._preprocess_light_items_for_deletion(selected_items)
|
||||
self.delete_items(selected_items)
|
||||
else:
|
||||
# 没有选中任何项目,执行默认操作
|
||||
@ -1818,12 +2055,58 @@ class CustomTreeWidget(QTreeWidget):
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def _preprocess_light_items_for_deletion(self, selected_items):
|
||||
"""预处理灯光节点删除,特别处理最后一个灯光节点的问题"""
|
||||
if not selected_items:
|
||||
return selected_items
|
||||
|
||||
# 检查选中的项目中是否包含灯光节点
|
||||
light_items = []
|
||||
for item in selected_items:
|
||||
node_type = item.data(0, Qt.UserRole + 1)
|
||||
if node_type == "LIGHT_NODE":
|
||||
light_items.append(item)
|
||||
|
||||
# 如果没有灯光节点,直接返回
|
||||
if not light_items:
|
||||
return selected_items
|
||||
|
||||
# 检查是否只有最后一个灯光节点被选中
|
||||
processed_items = list(selected_items) # 创建副本
|
||||
|
||||
for item in light_items:
|
||||
panda_node = item.data(0, Qt.UserRole)
|
||||
if not panda_node:
|
||||
continue
|
||||
|
||||
# 获取灯光类型
|
||||
if hasattr(panda_node, 'getTag'):
|
||||
light_type = panda_node.getTag("light_type")
|
||||
|
||||
# 检查是否是最后一个Spotlight
|
||||
if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and
|
||||
self.world.Spotlight and self.world.Spotlight[-1] == panda_node and
|
||||
len(self.world.Spotlight) > 1):
|
||||
|
||||
print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}")
|
||||
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
|
||||
|
||||
# 检查是否是最后一个Pointlight
|
||||
elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and
|
||||
self.world.Pointlight and self.world.Pointlight[-1] == panda_node and
|
||||
len(self.world.Pointlight) > 1):
|
||||
|
||||
print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}")
|
||||
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
|
||||
|
||||
return processed_items
|
||||
|
||||
def delete_items(self, selected_items):
|
||||
"""删除选中的item - 简化版本"""
|
||||
if not selected_items:
|
||||
return
|
||||
|
||||
# 过滤掉不能删除的节点
|
||||
# 1. 过滤掉不能删除的节点
|
||||
deletable_items = []
|
||||
for item in selected_items:
|
||||
node_type = item.data(0, Qt.UserRole + 1)
|
||||
@ -1839,7 +2122,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
QMessageBox.information(self, "提示", "没有可删除的节点")
|
||||
return
|
||||
|
||||
# 确认删除
|
||||
# 2. 确认删除
|
||||
item_count = len(deletable_items)
|
||||
if item_count == 1:
|
||||
message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?"
|
||||
@ -1854,11 +2137,19 @@ class CustomTreeWidget(QTreeWidget):
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
|
||||
# 执行删除
|
||||
# 默认选中场景根节点,通常是第一个顶级节点
|
||||
#next_item_to_select = self.topLevelItem(0)
|
||||
|
||||
# 3. 执行删除循环
|
||||
deleted_count = 0
|
||||
for item in deletable_items:
|
||||
try:
|
||||
# 在删除前,记录其父节点,作为删除后的新选择
|
||||
# 选择最后一个被删除项的父节点作为新的焦点
|
||||
if item.parent():
|
||||
next_item_to_select = item.parent()
|
||||
panda_node = item.data(0, Qt.UserRole)
|
||||
|
||||
if panda_node:
|
||||
# 清理选择状态
|
||||
if (hasattr(self.world, 'selection') and
|
||||
@ -1874,38 +2165,32 @@ 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'):
|
||||
try:
|
||||
self.world.render_pipeline.remove_light(light_object)
|
||||
print(f"移除灯光{panda_node.getName()}")
|
||||
except Exception as e:
|
||||
print(f"移除灯光失败: {str(e)}")
|
||||
panda_node.clearPythonTag('rp_light_object')
|
||||
#self.world.render_pipeline.remove_light(light_object)
|
||||
|
||||
if hasattr(self.world,'gui_manager') and hasattr(self.world.gui_manager,'gui_elements'):
|
||||
if panda_node in self.world.gui_manager.gui_elements:
|
||||
self.world.gui_manager.gui_elements.remove(panda_node)
|
||||
print(f"从gui_elements列表中移除{panda_node.getName()}")
|
||||
print(f'11111111111111111111111111,{light_object.casts_shadows}')
|
||||
self.world.render_pipeline.remove_light(light_object)
|
||||
|
||||
# 从world列表中移除
|
||||
if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements:
|
||||
self.world.gui_elements.remove(panda_node)
|
||||
if hasattr(self.world, 'models') and panda_node in self.world.models:
|
||||
self.world.models.remove(panda_node)
|
||||
if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
|
||||
self.world.Spotlight.remove(panda_node)
|
||||
if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
|
||||
self.world.Pointlight.remove(panda_node)
|
||||
if hasattr(self.world, 'terrains') and panda_node in self.world.terrains:
|
||||
self.world.terrains.remove(panda_node)
|
||||
if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets:
|
||||
# self.world.tilesets.remove(panda_node)
|
||||
# 从 tilesets 列表中移除
|
||||
if hasattr(self.world, 'scene_manager'):
|
||||
tilesets_to_remove = []
|
||||
for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
|
||||
if tileset_info['node'] == panda_node:
|
||||
tilesets_to_remove.append(i)
|
||||
|
||||
# if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
|
||||
# self.world.Spotlight.remove(panda_node)
|
||||
|
||||
if hasattr(self.world,'Spotlight'):
|
||||
self.world.Spotlight = [light for light in self.world.Spotlight if light != panda_node]
|
||||
if panda_node in self.world.Spotlight:
|
||||
print(f"从Spotlight列表中移除{panda_node.getName()}")
|
||||
|
||||
# if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
|
||||
# self.world.Pointlight.remove(panda_node)
|
||||
|
||||
if hasattr(self.world,'Pointlight'):
|
||||
self.world.Pointlight = [light for light in self.world.Pointlight if light != panda_node]
|
||||
if panda_node in self.world.Pointlight:
|
||||
print(f"从Pointlight列表中移除{panda_node.getName()}")
|
||||
# 从后往前删除,避免索引问题
|
||||
for i in reversed(tilesets_to_remove):
|
||||
del self.world.scene_manager.tilesets[i]
|
||||
|
||||
# 从Panda3D场景中移除
|
||||
try:
|
||||
@ -1935,7 +2220,210 @@ class CustomTreeWidget(QTreeWidget):
|
||||
# if hasattr(self.world, 'property_panel'):
|
||||
# self.world.property_panel.clearPropertyPanel()
|
||||
|
||||
print(f"✅ 已删除 {deleted_count} 个节点")
|
||||
# 4. 删除操作完成后,更新UI ---
|
||||
if deleted_count > 0:
|
||||
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
|
||||
self.update_selection_and_properties(None, None)
|
||||
|
||||
def delete_item(self, panda_node):
|
||||
"""删除指定节点 panda3D(node)- 优化和修复版本"""
|
||||
if not panda_node or panda_node.is_empty():
|
||||
print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。")
|
||||
return
|
||||
|
||||
# --- 关键修复:在操作前,安全地获取节点名字 ---
|
||||
node_name_for_logging = panda_node.getName()
|
||||
|
||||
# 1. 寻找对应的Qt Item
|
||||
item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot())
|
||||
|
||||
# 场景清理(无论是否找到item,都应该执行)
|
||||
self._cleanup_panda_node_resources(panda_node)
|
||||
panda_node.removeNode()
|
||||
|
||||
# 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出
|
||||
if not item:
|
||||
print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。")
|
||||
return
|
||||
try:
|
||||
# 2. 过滤受保护节点
|
||||
node_type = item.data(0, Qt.UserRole + 1)
|
||||
if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中
|
||||
print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。")
|
||||
return
|
||||
|
||||
# 3. 从UI树中移除
|
||||
parent_for_next_selection = item.parent()
|
||||
if item.parent():
|
||||
item.parent().removeChild(item)
|
||||
else:
|
||||
index = self.indexOfTopLevelItem(item)
|
||||
if index >= 0:
|
||||
self.takeTopLevelItem(index)
|
||||
|
||||
print(f"✅ 成功删除节点: {node_name_for_logging}")
|
||||
|
||||
# 4. 更新UI
|
||||
print(f"🔄 正在更新UI...")
|
||||
if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid():
|
||||
new_selection_item = parent_for_next_selection
|
||||
else:
|
||||
new_selection_item = self.topLevelItem(0)
|
||||
|
||||
if new_selection_item:
|
||||
self.setCurrentItem(new_selection_item)
|
||||
new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole)
|
||||
self.update_selection_and_properties(new_panda_node_to_select, new_selection_item)
|
||||
else:
|
||||
self.update_selection_and_properties(None, None)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def clear_tree(self):
|
||||
"""清空UI树"""
|
||||
print("Clear")
|
||||
self.clear()
|
||||
# 创建场景根节点
|
||||
sceneRoot = QTreeWidgetItem(self, ['场景'])
|
||||
sceneRoot.setData(0, Qt.UserRole, self.world.render)
|
||||
sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
|
||||
# 添加相机节点
|
||||
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
|
||||
cameraItem.setData(0, Qt.UserRole, self.world.cam)
|
||||
cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
|
||||
# 添加地板节点
|
||||
if hasattr(self.world, 'ground') and self.world.ground:
|
||||
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
|
||||
groundItem.setData(0, Qt.UserRole, self.world.ground)
|
||||
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
|
||||
|
||||
def _cleanup_panda_node_resources(self, panda_node):
|
||||
"""一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。"""
|
||||
if not panda_node or panda_node.is_empty():
|
||||
return
|
||||
try:
|
||||
# 清理选择状态
|
||||
if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node:
|
||||
self.world.selection.updateSelection(None)
|
||||
# 清理属性面板
|
||||
if hasattr(self.world, 'property_panel'):
|
||||
self.world.property_panel.removeActorForModel(panda_node)
|
||||
# 清理灯光
|
||||
if hasattr(panda_node, 'getPythonTag'):
|
||||
light_object = panda_node.getPythonTag('rp_light_object')
|
||||
if light_object and hasattr(self.world, 'render_pipeline'):
|
||||
self.world.render_pipeline.remove_light(light_object)
|
||||
# 从各种world管理列表中移除
|
||||
lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains']
|
||||
for list_name in lists_to_check:
|
||||
if hasattr(self.world, list_name):
|
||||
world_list = getattr(self.world, list_name)
|
||||
if panda_node in world_list:
|
||||
world_list.remove(panda_node)
|
||||
# 特殊处理tilesets
|
||||
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'):
|
||||
tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if
|
||||
info.get('node') == panda_node]
|
||||
for i in reversed(tilesets_to_remove):
|
||||
del self.world.scene_manager.tilesets[i]
|
||||
print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。")
|
||||
except Exception as e:
|
||||
# 即便这里出错,也要打印信息,但不要让整个删除流程中断
|
||||
print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}")
|
||||
|
||||
# def mousePressEvent(self, event):
|
||||
# """鼠标按下事件"""
|
||||
# if event.button() == Qt.LeftButton:
|
||||
# if self.currentItem():
|
||||
# print(f"self.currentItem() = {self.currentItem()}")
|
||||
# else:
|
||||
# print(f"self.currentItem() = None")
|
||||
#
|
||||
# # 调用父类处理其他事件
|
||||
# super().mousePressEvent(event)
|
||||
|
||||
def update_item_name(self, text, item):
|
||||
""" 树节点名字 """
|
||||
if not item:
|
||||
return
|
||||
try:
|
||||
# 正确的代码
|
||||
node = item.data(0, Qt.UserRole)
|
||||
|
||||
item.setText(0, text)
|
||||
node.setName(text)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def _findSceneRoot(self):
|
||||
"""查找场景根节点"""
|
||||
for i in range(self.topLevelItemCount()):
|
||||
top_item = self.topLevelItem(i)
|
||||
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
|
||||
return top_item
|
||||
return None
|
||||
|
||||
def create_model_items(self, model: NodePath):
|
||||
"""
|
||||
【此函数保持不变】
|
||||
创建模型项。
|
||||
只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根,
|
||||
然后完整地展示这些分支。
|
||||
"""
|
||||
if not model:
|
||||
print("传入的参数model为空")
|
||||
return
|
||||
|
||||
# 找到场景树的根节点,我们将把模型节点添加到这里
|
||||
root_item = self._findSceneRoot()
|
||||
if not root_item:
|
||||
print("错误:未能找到场景根节点项")
|
||||
return
|
||||
|
||||
# 1. 在模型的第一层子节点中进行筛选
|
||||
for child_node in model.getChildren():
|
||||
if child_node.hasTag("is_scene_element"):
|
||||
print(f"找到带标签的根节点:{child_node.getName()}")
|
||||
if (child_node.hasTag("gui_type")and
|
||||
child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]):
|
||||
print(f"跳过3dGUI节点{child_node.getName()}")
|
||||
continue
|
||||
|
||||
# 为这个带标签的节点创建一个树项
|
||||
child_item = QTreeWidgetItem(root_item)
|
||||
child_item.setText(0, child_node.getName() or "Unnamed Tagged Node")
|
||||
child_item.setData(0, Qt.UserRole, child_node)
|
||||
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
|
||||
# self._add_node_info(child_item, child_node) # 可选信息
|
||||
|
||||
# 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体)
|
||||
self._add_all_children_unconditionally(child_item, child_node)
|
||||
|
||||
def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath):
|
||||
"""
|
||||
【此函数已更新】
|
||||
无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。
|
||||
"""
|
||||
for child_node in node_path.getChildren():
|
||||
|
||||
# 新增:检查节点是否为碰撞节点
|
||||
if isinstance(child_node.node(), CollisionNode):
|
||||
# print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试
|
||||
continue # 如果是,则跳过此节点及其所有子节点
|
||||
|
||||
# 创建子项
|
||||
child_item = QTreeWidgetItem(parent_item)
|
||||
child_item.setText(0, child_node.getName() or "Unnamed Child")
|
||||
child_item.setData(0, Qt.UserRole, child_node)
|
||||
child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type"))
|
||||
# self._add_node_info(child_item, child_node) # 可选信息
|
||||
|
||||
# 继续无条件地递归
|
||||
if not child_node.is_empty():
|
||||
self._add_all_children_unconditionally(child_item, child_node)
|
||||
|
||||
# ==================== 辅助方法 ====================
|
||||
def _findSceneRoot(self):
|
||||
@ -1966,52 +2454,79 @@ class CustomTreeWidget(QTreeWidget):
|
||||
|
||||
def add_node_to_tree_widget(self, node, parent_item, node_type):
|
||||
"""将node元素添加到树形控件"""
|
||||
if hasattr(node, 'getTag'):
|
||||
if node.hasTag('tree_item_type'):
|
||||
print(f"node0: {node.getName()},{node.getTag('tree_item_type')}")
|
||||
tree_type = node.getTag('tree_item_type')
|
||||
else:
|
||||
node.setTag('tree_item_type', node_type)
|
||||
else:
|
||||
print(f"node2: {node.getName()},{node_type}")
|
||||
tree_type = node_type
|
||||
|
||||
|
||||
# BLACK_LIST 和依赖项导入保持不变
|
||||
BLACK_LIST = {'', '**', 'temp', 'collision'}
|
||||
from panda3d.core import CollisionNode, ModelRoot
|
||||
from PyQt5.QtWidgets import QTreeWidgetItem
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from panda3d.core import CollisionNode
|
||||
# 1. 修改内部函数,让它返回创建的节点
|
||||
def addNodeToTree(node, parentItem, force=False):
|
||||
"""内部递归函数,现在会返回创建的顶级节点项"""
|
||||
if not force and should_skip(node):
|
||||
return None # 如果跳过,返回None
|
||||
|
||||
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
|
||||
nodeItem.setData(0, Qt.UserRole, node)
|
||||
nodeItem.setData(0, Qt.UserRole + 1, tree_type)
|
||||
|
||||
for child in node.getChildren():
|
||||
# 递归调用,但我们只关心顶级的nodeItem
|
||||
addNodeToTree(child, nodeItem, force=False)
|
||||
|
||||
return nodeItem # <-- 新增:返回创建的QTreeWidgetItem
|
||||
|
||||
def should_skip(node):
|
||||
name = node.getName()
|
||||
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance(
|
||||
node.node(), ModelRoot) or name == ""
|
||||
|
||||
def addNodeToTree(node, parentItem, force=False):
|
||||
if not force and should_skip(node):
|
||||
return None
|
||||
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
|
||||
nodeItem.setData(0, Qt.UserRole, node)
|
||||
nodeItem.setData(0, Qt.UserRole + 1, node_type)
|
||||
|
||||
for child in node.getChildren():
|
||||
addNodeToTree(child, nodeItem, force=False)
|
||||
return nodeItem
|
||||
# 使用一个变量来确保无论哪个分支都有返回值
|
||||
new_qt_item = None
|
||||
node_name = ""
|
||||
|
||||
try:
|
||||
from PyQt5.QtWidgets import QTreeWidgetItem
|
||||
from PyQt5.QtCore import Qt
|
||||
if tree_type == "IMPORTED_MODEL_NODE":
|
||||
# getTag('file') 可能是你自己设置的tag,这里假设它存在
|
||||
node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName()
|
||||
|
||||
# 初始化new_qt_item变量
|
||||
new_qt_item = None
|
||||
|
||||
if node_type == "IMPORTED_MODEL_NODE":
|
||||
node_name = node.getTag("file") if hasattr(node, 'getTag') else "model"
|
||||
# 2. 接收 addNodeToTree 的返回值
|
||||
new_qt_item = addNodeToTree(node, parent_item, force=True)
|
||||
|
||||
else:
|
||||
node_name = node.getName() if hasattr(node, 'getName') else "node"
|
||||
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
|
||||
new_qt_item.setData(0, Qt.UserRole, node)
|
||||
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
|
||||
new_qt_item.setData(0, Qt.UserRole + 1, tree_type)
|
||||
|
||||
# 展开父节点
|
||||
if hasattr(parent_item, 'setExpanded'):
|
||||
parent_item.setExpanded(True)
|
||||
# 确保 new_qt_item 成功创建后再继续操作
|
||||
if new_qt_item:
|
||||
# 展开父节点
|
||||
if hasattr(parent_item, 'setExpanded'):
|
||||
parent_item.setExpanded(True)
|
||||
|
||||
print(f"✅ Qt树节点添加成功: {node_name}")
|
||||
return new_qt_item
|
||||
print(f"✅ Qt树节点添加成功: {node_name}")
|
||||
return new_qt_item
|
||||
else:
|
||||
# 如果 addNodeToTree 因为 should_skip 返回了 None
|
||||
print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"❌ 添加node到树形控件失败: {str(e)}")
|
||||
traceback.print_exc() # 打印更详细的错误堆栈,方便调试
|
||||
return None
|
||||
|
||||
def update_selection_and_properties(self, node, qt_item):
|
||||
@ -2085,7 +2600,7 @@ class CustomTreeWidget(QTreeWidget):
|
||||
node_type = item.data(0, Qt.UserRole + 1)
|
||||
|
||||
# 场景根节点和普通场景节点可以作为父节点
|
||||
if node_type in self.gui_3d_types and self.scene_3d_types:
|
||||
if node_type in self.valid_3d_parent_types:
|
||||
return True
|
||||
|
||||
# # 模型节点也可以作为父节点
|
||||
|
||||
Loading…
Reference in New Issue
Block a user