forked from Rowland/EG
addRender #27
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -3,5 +3,5 @@
|
|||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="Python 3.12 (PythonProject)" />
|
<option name="sdkName" value="Python 3.12 (PythonProject)" />
|
||||||
</component>
|
</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>
|
</project>
|
||||||
@ -32,9 +32,24 @@ class QPanda3DSynchronizer(QTimer):
|
|||||||
self.setInterval(int(dt))
|
self.setInterval(int(dt))
|
||||||
self.timeout.connect(self.tick)
|
self.timeout.connect(self.tick)
|
||||||
|
|
||||||
|
# def tick(self):
|
||||||
|
# taskMgr.step()
|
||||||
|
# self.qPanda3DWidget.update()
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
taskMgr.step()
|
try:
|
||||||
self.qPanda3DWidget.update()
|
taskMgr.step()
|
||||||
|
self.qPanda3DWidget.update()
|
||||||
|
except 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):
|
def get_panda_key_modifiers(evt):
|
||||||
@ -160,8 +175,8 @@ class QPanda3DWidget(QWidget):
|
|||||||
def resizeEvent(self, evt):
|
def resizeEvent(self, evt):
|
||||||
width = evt.size().width()
|
width = evt.size().width()
|
||||||
height = evt.size().height()
|
height = evt.size().height()
|
||||||
print(f"width:{width}")
|
#print(f"width:{width}")
|
||||||
print(f"height:{height}")
|
#print(f"height:{height}")
|
||||||
|
|
||||||
from Panda3DWorld import resize_buffer
|
from Panda3DWorld import resize_buffer
|
||||||
#resize_buffer(width, height)
|
#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):
|
def remove_light(self, light):
|
||||||
""" Removes a 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.internal_mgr.remove_light(light)
|
||||||
self.pta_max_light_index[0] = self.internal_mgr.max_light_index
|
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")
|
RPObject.global_debug("CORE", "Using native core module")
|
||||||
from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position
|
from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position
|
||||||
else:
|
else:
|
||||||
|
print(f'343434343434343434343434343')
|
||||||
from rpcore import pynative as _native_module # pylint: disable=wrong-import-position
|
from rpcore import pynative as _native_module # pylint: disable=wrong-import-position
|
||||||
RPObject.global_debug("CORE", "Using simulated python-wrapper module")
|
RPObject.global_debug("CORE", "Using simulated python-wrapper module")
|
||||||
|
|
||||||
|
|||||||
@ -161,7 +161,12 @@ public:
|
|||||||
|
|
||||||
// Update maximum index
|
// Update maximum index
|
||||||
if (slot == _max_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)
|
source.set_slot(slot)
|
||||||
|
|
||||||
def remove_light(self, light):
|
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
|
assert light is not None
|
||||||
if not light.has_slot():
|
if not light.has_slot():
|
||||||
print("ERROR: Could not detach light, light was not attached!")
|
print("ERROR: Could not detach light, light was not attached!")
|
||||||
@ -134,6 +152,9 @@ class InternalLightManager(object):
|
|||||||
first_source = light.get_shadow_source(0)
|
first_source = light.get_shadow_source(0)
|
||||||
first_source_slot = first_source.get_slot() # 保存slot值
|
first_source_slot = first_source.get_slot() # 保存slot值
|
||||||
|
|
||||||
|
# --- 在这里加上打印语句 ---
|
||||||
|
print(f"DEBUG: Removing shadow sources. Start Slot: {first_source_slot}, Count: {num_sources}")
|
||||||
|
|
||||||
# 先发送GPU移除命令(在清理之前)
|
# 先发送GPU移除命令(在清理之前)
|
||||||
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
|
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
|
||||||
cmd_remove.push_int(first_source_slot) # 使用保存的slot值
|
cmd_remove.push_int(first_source_slot) # 使用保存的slot值
|
||||||
|
|||||||
@ -208,6 +208,7 @@ class RenderPipeline(RPObject):
|
|||||||
def remove_light(self, light):
|
def remove_light(self, light):
|
||||||
""" Removes a previously attached light, check out the LightManager
|
""" Removes a previously attached light, check out the LightManager
|
||||||
remove_light documentation for further information. """
|
remove_light documentation for further information. """
|
||||||
|
print(f'222222222222222222222222222,{light.casts_shadows}')
|
||||||
self.light_mgr.remove_light(light)
|
self.light_mgr.remove_light(light)
|
||||||
|
|
||||||
def load_ies_profile(self, filename):
|
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
|
# 修改后的 InfoPanelManager.py
|
||||||
from xml.sax.handler import property_encoding
|
from xml.sax.handler import property_encoding
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
from direct.gui.DirectGui import DirectFrame, DirectLabel
|
from direct.gui.DirectGui import DirectFrame, DirectLabel
|
||||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||||
from panda3d.core import TextNode, Vec4, NodePath
|
from panda3d.core import TextNode, Vec4, NodePath
|
||||||
@ -144,7 +145,7 @@ class InfoPanelManager(DirectObject):
|
|||||||
text_scale=0.045,
|
text_scale=0.045,
|
||||||
text_fg=content_color,
|
text_fg=content_color,
|
||||||
text_align=TextNode.ALeft,
|
text_align=TextNode.ALeft,
|
||||||
text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行
|
text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行
|
||||||
pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05),
|
pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05),
|
||||||
parent=panel_node,
|
parent=panel_node,
|
||||||
relief=None,
|
relief=None,
|
||||||
@ -182,12 +183,60 @@ class InfoPanelManager(DirectObject):
|
|||||||
panel_node.setTag("gui_type", "info_panel")
|
panel_node.setTag("gui_type", "info_panel")
|
||||||
panel_node.setTag("panel_id", panel_id)
|
panel_node.setTag("panel_id", panel_id)
|
||||||
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
|
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:
|
if not visible:
|
||||||
panel_node.hide()
|
panel_node.hide()
|
||||||
|
|
||||||
|
# 将面板添加到场景树
|
||||||
|
#self._addPanelToSceneTree(panel_node, panel_id)
|
||||||
|
|
||||||
return panel_node
|
return panel_node
|
||||||
|
|
||||||
|
def _addPanelToSceneTree(self, panel_node, panel_id):
|
||||||
|
"""
|
||||||
|
将信息面板添加到场景树中
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取树形控件
|
||||||
|
if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
|
||||||
|
tree_widget = self.world.interface_manager.treeWidget
|
||||||
|
if tree_widget:
|
||||||
|
# 查找根节点项
|
||||||
|
root_item = None
|
||||||
|
for i in range(tree_widget.topLevelItemCount()):
|
||||||
|
item = tree_widget.topLevelItem(i)
|
||||||
|
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
|
||||||
|
root_item = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if root_item:
|
||||||
|
# 使用现有的 add_node_to_tree_widget 方法添加节点
|
||||||
|
qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL")
|
||||||
|
if qt_item:
|
||||||
|
print(f"✅ 信息面板 {panel_id} 已添加到场景树")
|
||||||
|
# 选中创建的节点
|
||||||
|
tree_widget.setCurrentItem(qt_item)
|
||||||
|
# 更新选择和属性面板
|
||||||
|
tree_widget.update_selection_and_properties(panel_node, qt_item)
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败")
|
||||||
|
else:
|
||||||
|
print("⚠️ 未找到场景树根节点,无法添加信息面板")
|
||||||
|
else:
|
||||||
|
print("⚠️ 无法访问场景树控件,信息面板未添加到场景树")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 添加信息面板到场景树时出错: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def setPanelBackgroundImage(self, panel_id, image_path):
|
def setPanelBackgroundImage(self, panel_id, image_path):
|
||||||
"""
|
"""
|
||||||
为指定面板设置背景图片
|
为指定面板设置背景图片
|
||||||
@ -317,11 +366,10 @@ class InfoPanelManager(DirectObject):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# 更新 registerDataSource 方法以更好地处理不同类型面板
|
||||||
def registerDataSource(self, panel_id, data_callback, update_interval=1.0):
|
def registerDataSource(self, panel_id, data_callback, update_interval=1.0):
|
||||||
"""
|
"""
|
||||||
注册数据源,定期更新面板内容
|
注册数据源,定期更新面板内容 - 改进版
|
||||||
data_callback: 返回数据的回调函数
|
|
||||||
update_interval: 更新间隔(秒)
|
|
||||||
"""
|
"""
|
||||||
if panel_id not in self.panels:
|
if panel_id not in self.panels:
|
||||||
print(f"面板 {panel_id} 不存在")
|
print(f"面板 {panel_id} 不存在")
|
||||||
@ -335,7 +383,8 @@ class InfoPanelManager(DirectObject):
|
|||||||
data_source = {
|
data_source = {
|
||||||
'callback': data_callback,
|
'callback': data_callback,
|
||||||
'interval': update_interval,
|
'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
|
self.data_sources[panel_id] = data_source
|
||||||
@ -346,9 +395,10 @@ class InfoPanelManager(DirectObject):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# 在 InfoPanelManager 类中修复 _updateDataThread 方法
|
||||||
def _updateDataThread(self, panel_id):
|
def _updateDataThread(self, panel_id):
|
||||||
"""
|
"""
|
||||||
数据更新线程
|
数据更新线程 - 最终修复版
|
||||||
"""
|
"""
|
||||||
while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']:
|
while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']:
|
||||||
try:
|
try:
|
||||||
@ -357,7 +407,18 @@ class InfoPanelManager(DirectObject):
|
|||||||
|
|
||||||
# 更新面板内容
|
# 更新面板内容
|
||||||
if data and panel_id in self.panels:
|
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']
|
interval = self.data_sources[panel_id]['interval']
|
||||||
@ -365,8 +426,29 @@ class InfoPanelManager(DirectObject):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"更新面板 {panel_id} 数据时出错: {e}")
|
print(f"更新面板 {panel_id} 数据时出错: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
time.sleep(1.0) # 出错时等待1秒再重试
|
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):
|
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
|
-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05
|
||||||
)
|
)
|
||||||
|
|
||||||
# 设置一个非常大的换行值,几乎不自动换行
|
panel_data['content_label']['text_wordwrap'] = 0
|
||||||
panel_data['content_label']['text_wordwrap'] = 500
|
|
||||||
print(f"更新面板换行: 设置为500(几乎不换行)")
|
|
||||||
|
|
||||||
# 如果有背景图片,也需要更新其大小
|
# 如果有背景图片,也需要更新其大小
|
||||||
if 'bg_image' in panel_data and panel_data['bg_image']:
|
if 'bg_image' in panel_data and panel_data['bg_image']:
|
||||||
@ -493,8 +573,9 @@ class InfoPanelManager(DirectObject):
|
|||||||
if 'content_size' in properties:
|
if 'content_size' in properties:
|
||||||
panel_data['content_label']['text_scale'] = properties['content_size']
|
panel_data['content_label']['text_scale'] = properties['content_size']
|
||||||
props['content_size'] = properties['content_size']
|
props['content_size'] = properties['content_size']
|
||||||
|
current_size = props.get('size',(1.0,0.6))
|
||||||
# 当字体大小改变时,仍然保持较大的换行值
|
# 当字体大小改变时,仍然保持较大的换行值
|
||||||
panel_data['content_label']['text_wordwrap'] = 500
|
panel_data['content_label']['text_wordwrap'] = 0
|
||||||
|
|
||||||
# 更新背景图片
|
# 更新背景图片
|
||||||
if 'bg_image' in properties:
|
if 'bg_image' in properties:
|
||||||
@ -657,10 +738,247 @@ class InfoPanelManager(DirectObject):
|
|||||||
|
|
||||||
return True
|
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 函数中添加以下方法
|
# 在 add_methods_to_property_panel 函数中添加以下方法
|
||||||
|
|
||||||
def add_methods_to_property_panel(property_panel_instance):
|
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,
|
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):
|
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}")
|
print(f"✗ 更新HTTP信息面板失败: {e}")
|
||||||
return False
|
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.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance)
|
||||||
property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, 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():
|
def getRealtimeData():
|
||||||
@ -777,6 +1316,7 @@ def add_methods_to_property_panel(property_panel_instance):
|
|||||||
"""
|
"""
|
||||||
为 property_panel 实例添加 DirectGUI 信息面板支持
|
为 property_panel 实例添加 DirectGUI 信息面板支持
|
||||||
"""
|
"""
|
||||||
|
import types
|
||||||
|
|
||||||
# 添加信息面板管理器作为类属性
|
# 添加信息面板管理器作为类属性
|
||||||
if not hasattr(property_panel_instance, 'info_panel_manager'):
|
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.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance)
|
||||||
property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance)
|
property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance)
|
||||||
|
|
||||||
|
|
||||||
def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5):
|
def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5):
|
||||||
"""
|
"""
|
||||||
获取HTTP数据的通用函数
|
获取HTTP数据的通用函数
|
||||||
|
|||||||
@ -309,40 +309,81 @@ class CollisionManager:
|
|||||||
CollisionPlane, CollisionPolygon, Plane, Vec3
|
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)
|
return CollisionSphere(Point3(0, 0, 0), 1.0)
|
||||||
|
|
||||||
center = bounds.getCenter()
|
center = transformed_info['center']
|
||||||
radius = bounds.getRadius()
|
radius = transformed_info['radius']
|
||||||
|
actual_size = transformed_info['size']
|
||||||
|
scale_factor = transformed_info['scale_factor']
|
||||||
|
|
||||||
# 自动选择最适合的形状
|
# 自动选择最适合的形状
|
||||||
if shape_type == 'auto':
|
if shape_type == 'auto':
|
||||||
shape_type = self._determineOptimalShape(model, bounds)
|
shape_type = self._determineOptimalShape(model, transformed_info)
|
||||||
|
|
||||||
if shape_type == 'sphere':
|
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':
|
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)
|
return CollisionBox(min_point, max_point)
|
||||||
|
|
||||||
elif shape_type == 'capsule':
|
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))
|
custom_height = kwargs.get('height', actual_size.z)
|
||||||
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)
|
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':
|
elif shape_type == 'plane':
|
||||||
# 创建平面(适合地面、墙面)
|
# 优化平面碰撞 - 支持位置偏移和更灵活的法向量
|
||||||
normal = kwargs.get('normal', Vec3(0, 0, 1))
|
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)
|
return CollisionPlane(plane)
|
||||||
|
|
||||||
elif shape_type == 'polygon':
|
elif shape_type == 'polygon':
|
||||||
@ -364,13 +405,13 @@ class CollisionManager:
|
|||||||
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
|
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
|
||||||
return collision_poly
|
return collision_poly
|
||||||
else:
|
else:
|
||||||
print("⚠️ 多边形至少需要3个顶点,回退到球体")
|
#print("⚠️ 多边形至少需要3个顶点,回退到球体")
|
||||||
return CollisionSphere(center, radius)
|
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)
|
max_dim = max(size.x, size.y, size.z)
|
||||||
min_dim = min(size.x, size.y, size.z)
|
min_dim = min(size.x, size.y, size.z)
|
||||||
|
|
||||||
@ -399,6 +440,87 @@ class CollisionManager:
|
|||||||
else: # 其他用包围盒
|
else: # 其他用包围盒
|
||||||
return 'box'
|
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']):
|
def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
|
||||||
"""创建鼠标射线"""
|
"""创建鼠标射线"""
|
||||||
# 组合掩码
|
# 组合掩码
|
||||||
|
|||||||
@ -167,7 +167,7 @@ class EventHandler:
|
|||||||
picker.addCollider(pickerNP, queue)
|
picker.addCollider(pickerNP, queue)
|
||||||
picker.traverse(self.world.render)
|
picker.traverse(self.world.render)
|
||||||
|
|
||||||
print(f"碰撞检测结果数量: {queue.getNumEntries()}")
|
#print(f"碰撞检测结果数量: {queue.getNumEntries()}")
|
||||||
|
|
||||||
# 射线检测结果处理
|
# 射线检测结果处理
|
||||||
hitPos = None
|
hitPos = None
|
||||||
@ -184,13 +184,13 @@ class EventHandler:
|
|||||||
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
|
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
|
||||||
|
|
||||||
# 优先检查是否点击了坐标轴
|
# 优先检查是否点击了坐标轴
|
||||||
print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
|
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
|
||||||
if self.world.selection.gizmo:
|
if self.world.selection.gizmo:
|
||||||
print("准备检查坐标轴点击...")
|
#print("准备检查坐标轴点击...")
|
||||||
try:
|
try:
|
||||||
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
|
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
|
||||||
if gizmoAxis:
|
if gizmoAxis:
|
||||||
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
|
#print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
|
||||||
# 开始坐标轴拖拽
|
# 开始坐标轴拖拽
|
||||||
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
|
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
|
||||||
pickerNP.removeNode()
|
pickerNP.removeNode()
|
||||||
@ -203,16 +203,16 @@ class EventHandler:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print("继续处理模型选择...")
|
print("继续处理模型选择...")
|
||||||
|
|
||||||
print("继续处理碰撞结果...")
|
#print("继续处理碰撞结果...")
|
||||||
|
|
||||||
if hitPos and hitNode:
|
if hitPos and hitNode:
|
||||||
print(f"✓ 检测到碰撞,开始处理点击事件")
|
#print(f"✓ 检测到碰撞,开始处理点击事件")
|
||||||
print(f"GUI编辑模式: {self.world.guiEditMode}")
|
#print(f"GUI编辑模式: {self.world.guiEditMode}")
|
||||||
print(f"当前工具: {self.world.currentTool}")
|
#print(f"当前工具: {self.world.currentTool}")
|
||||||
|
|
||||||
# 处理GUI编辑模式
|
# 处理GUI编辑模式
|
||||||
if self.world.guiEditMode:
|
if self.world.guiEditMode:
|
||||||
print("处理GUI编辑模式点击")
|
#print("处理GUI编辑模式点击")
|
||||||
# 检查是否点击了GUI元素
|
# 检查是否点击了GUI元素
|
||||||
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
|
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
|
||||||
if clickedGUI:
|
if clickedGUI:
|
||||||
@ -411,103 +411,6 @@ class EventHandler:
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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):
|
def _handleSelectionClick(self, hitNode):
|
||||||
"""处理选择工具的点击事件"""
|
"""处理选择工具的点击事件"""
|
||||||
print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}")
|
print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}")
|
||||||
@ -548,26 +451,34 @@ class EventHandler:
|
|||||||
current = current.getParent()
|
current = current.getParent()
|
||||||
|
|
||||||
if selectedModel:
|
if selectedModel:
|
||||||
print(f"✓ 最终选中模型: {selectedModel.getName()}")
|
#print(f"✓ 最终选中模型: {selectedModel.getName()}")
|
||||||
|
self.world.selection.handleMouseClick(selectedModel)
|
||||||
|
|
||||||
# 更新选择状态并显示选择框和坐标轴
|
# 更新选择状态并显示选择框和坐标轴
|
||||||
self.world.selection.updateSelection(selectedModel)
|
self.world.selection.updateSelection(selectedModel)
|
||||||
|
|
||||||
# 在树形控件中查找并选中对应的项
|
# 在树形控件中查找并选中对应的项
|
||||||
if self.world.interface_manager.treeWidget:
|
if self.world.interface_manager.treeWidget:
|
||||||
print("查找树形控件中的对应项...")
|
#print("查找树形控件中的对应项...")
|
||||||
root = self.world.interface_manager.treeWidget.invisibleRootItem()
|
root = self.world.interface_manager.treeWidget.invisibleRootItem()
|
||||||
foundItem = None
|
foundItem = None
|
||||||
|
|
||||||
for i in range(root.childCount()):
|
for i in range(root.childCount()):
|
||||||
sceneItem = root.child(i)
|
sceneItem = root.child(i)
|
||||||
if sceneItem.text(0) == "场景":
|
if sceneItem.text(0) == "场景":
|
||||||
print(f"在场景节点下查找...")
|
#print(f"在场景节点下查找...")
|
||||||
foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem)
|
foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem)
|
||||||
if foundItem:
|
if foundItem:
|
||||||
print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}")
|
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.interface_manager.treeWidget.setCurrentItem(foundItem)
|
||||||
self.world.property_panel.updatePropertyPanel(foundItem)
|
|
||||||
|
self.world.interface_manager.treeWidget.itemClicked.connect(
|
||||||
|
self.world.interface_manager.onTreeItemClicked)
|
||||||
else:
|
else:
|
||||||
print("× 在树形控件中没有找到对应项")
|
print("× 在树形控件中没有找到对应项")
|
||||||
break
|
break
|
||||||
@ -578,6 +489,7 @@ class EventHandler:
|
|||||||
print("× 树形控件不存在")
|
print("× 树形控件不存在")
|
||||||
else:
|
else:
|
||||||
print("× 没有找到可选择的模型节点")
|
print("× 没有找到可选择的模型节点")
|
||||||
|
self.world.selection.updateSelection(None)
|
||||||
|
|
||||||
def mouseReleaseEventLeft(self, evt):
|
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("✓ 默认巡检路线已创建")
|
||||||
|
|
||||||
@ -294,6 +294,31 @@ class ScriptLoader:
|
|||||||
|
|
||||||
return len(changed_scripts) > 0
|
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:
|
class ScriptAPI:
|
||||||
"""脚本API - 提供给脚本使用的API接口"""
|
"""脚本API - 提供给脚本使用的API接口"""
|
||||||
@ -671,17 +696,61 @@ class {class_name}(ScriptBase):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
script_components = self.object_scripts[game_object]
|
script_components = self.object_scripts[game_object]
|
||||||
|
removed = False
|
||||||
|
|
||||||
for component in script_components[:]: # 复制列表以避免修改时出错
|
for component in script_components[:]: # 复制列表以避免修改时出错
|
||||||
if component.script_instance.__class__.__name__ == script_name:
|
if component.script_instance.__class__.__name__ == script_name:
|
||||||
# 从引擎移除
|
# 从引擎移除
|
||||||
self.engine.remove_script_component(component)
|
self.engine.remove_script_component(component)
|
||||||
# 从对象脚本列表移除
|
# 从对象脚本列表移除
|
||||||
script_components.remove(component)
|
script_components.remove(component)
|
||||||
|
removed = True
|
||||||
|
|
||||||
print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}")
|
print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}")
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
if not script_components:
|
||||||
|
del self.object_scripts[game_object]
|
||||||
|
|
||||||
|
# 更新节点上保存的脚本信息标签
|
||||||
|
if removed:
|
||||||
|
self._update_node_script_tags_after_removal(game_object, script_name)
|
||||||
|
|
||||||
|
return removed
|
||||||
|
|
||||||
|
def _update_node_script_tags_after_removal(self, game_object, removed_script_name):
|
||||||
|
"""在移除脚本后更新节点标签"""
|
||||||
|
try:
|
||||||
|
# 获取对象上剩余的脚本
|
||||||
|
remaining_scripts = self.get_scripts_on_object(game_object)
|
||||||
|
|
||||||
|
if not remaining_scripts:
|
||||||
|
# 如果没有其他脚本,清除所有脚本标签
|
||||||
|
if game_object.hasTag("has_scripts"):
|
||||||
|
game_object.clearTag("has_scripts")
|
||||||
|
if game_object.hasTag("scripts_info"):
|
||||||
|
game_object.clearTag("scripts_info")
|
||||||
|
print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签")
|
||||||
|
else:
|
||||||
|
# 如果还有其他脚本,更新脚本信息标签
|
||||||
|
script_info_list = []
|
||||||
|
for script_component in remaining_scripts:
|
||||||
|
script_name = script_component.script_name
|
||||||
|
script_class = script_component.script_instance.__class__
|
||||||
|
script_file = self.loader.find_script_file(script_name) or ""
|
||||||
|
|
||||||
|
script_info_list.append({
|
||||||
|
"name": script_name,
|
||||||
|
"file": script_file
|
||||||
|
})
|
||||||
|
|
||||||
|
import json
|
||||||
|
game_object.setTag("has_scripts", "true")
|
||||||
|
game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
|
||||||
|
print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"更新节点标签失败: {e}")
|
||||||
|
|
||||||
def get_scripts_on_object(self, game_object) -> List[ScriptComponent]:
|
def get_scripts_on_object(self, game_object) -> List[ScriptComponent]:
|
||||||
"""获取对象上的所有脚本"""
|
"""获取对象上的所有脚本"""
|
||||||
@ -744,6 +813,36 @@ class {class_name}(ScriptBase):
|
|||||||
|
|
||||||
print("==================\n")
|
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
|
# 添加全局便捷函数,让脚本更容易使用API
|
||||||
def get_script_api():
|
def get_script_api():
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,13 @@
|
|||||||
# core/terrain_manager.py
|
# core/terrain_manager.py
|
||||||
import time
|
import time
|
||||||
|
import urllib
|
||||||
|
|
||||||
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
|
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
|
||||||
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
|
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from scene import util
|
||||||
|
|
||||||
|
|
||||||
class TerrainManager:
|
class TerrainManager:
|
||||||
"""地形管理类"""
|
"""地形管理类"""
|
||||||
@ -31,106 +35,151 @@ class TerrainManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建GeoMipTerrain对象
|
print(f"🔆 开始创建高度图地形")
|
||||||
terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
|
||||||
terrain = GeoMipTerrain(terrain_name)
|
|
||||||
|
|
||||||
# 加载高度图
|
# 获取树形控件
|
||||||
height_image = PNMImage(Filename.fromOsSpecific(heightmap_path))
|
tree_widget = self._get_tree_widget()
|
||||||
|
if not tree_widget:
|
||||||
# 检查并调整图像尺寸为2的幂次方加1
|
print("❌ 无法访问树形控件")
|
||||||
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("错误:无法生成有效的地形节点")
|
|
||||||
return None
|
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
|
created_terrains = []
|
||||||
terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5)
|
try:
|
||||||
|
parent_item, parent_node = target_parents[0]
|
||||||
|
|
||||||
# 设置缩放
|
# 创建GeoMipTerrain对象
|
||||||
terrain_node.setScale(scale[0], scale[1], scale[2])
|
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
|
# 检查并调整图像尺寸为2的幂次方加1
|
||||||
# 设置地形节点的碰撞掩码
|
width, height = height_image.getXSize(), height_image.getYSize()
|
||||||
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
|
print(f"原始图像尺寸: {width}x{height}")
|
||||||
# 为地形的所有子节点也设置碰撞掩码
|
|
||||||
for child in terrain_node.getChildren():
|
|
||||||
child.setCollideMask(BitMask32.bit(2))
|
|
||||||
|
|
||||||
# 添加材质
|
# 找到最接近的有效尺寸
|
||||||
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 = {
|
if width != target_size or height != target_size:
|
||||||
'terrain': terrain,
|
print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}")
|
||||||
'node': terrain_node,
|
# 使用正确的图像缩放方法
|
||||||
'heightmap': heightmap_path,
|
resized_image = PNMImage(target_size, target_size)
|
||||||
'heightfield': height_image, # 保存高度图副本
|
resized_image.quickFilterFrom(height_image)
|
||||||
'scale': scale,
|
height_image = resized_image
|
||||||
'name': node_name
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
terrain.setBruteforce(True) # 使用LOD
|
||||||
'updateSceneTree'):
|
|
||||||
try:
|
|
||||||
self.world.scene_manager.updateSceneTree()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"警告: 更新场景树时出错: {e}")
|
|
||||||
|
|
||||||
print(f"✓ 成功从 {heightmap_path} 创建地形")
|
terrain.setBlockSize(32)
|
||||||
return terrain_info
|
# 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:
|
except Exception as e:
|
||||||
print(f"创建地形时出错: {e}")
|
print(f"❌ 创建高度图地形过程失败: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
@ -138,84 +187,129 @@ class TerrainManager:
|
|||||||
def createFlatTerrain(self, size=(0.3, 0.3), resolution=129):
|
def createFlatTerrain(self, size=(0.3, 0.3), resolution=129):
|
||||||
"""创建平面地形"""
|
"""创建平面地形"""
|
||||||
try:
|
try:
|
||||||
# 确保分辨率是2的幂次方加1 (如129, 257, 513等)
|
print(f"🔆 开始创建平面地形")
|
||||||
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
|
|
||||||
|
|
||||||
# 创建GeoMipTerrain对象
|
# 获取树形控件
|
||||||
terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
tree_widget = self._get_tree_widget()
|
||||||
terrain = GeoMipTerrain(terrain_name)
|
if not tree_widget:
|
||||||
|
print("❌ 无法访问树形控件")
|
||||||
# 创建一个平面高度图,尺寸必须是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("错误:无法生成有效的平面地形节点")
|
|
||||||
return None
|
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
|
created_terrains = []
|
||||||
terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0)
|
|
||||||
|
|
||||||
# 将地形添加到场景中
|
try:
|
||||||
terrain_node.reparentTo(self.world.render)
|
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
|
||||||
|
|
||||||
# 为地形添加碰撞体
|
# 创建GeoMipTerrain对象
|
||||||
from panda3d.core import BitMask32
|
terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
|
||||||
# 设置地形节点的碰撞掩码
|
terrain = GeoMipTerrain(terrain_name)
|
||||||
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
|
|
||||||
# 为地形的所有子节点也设置碰撞掩码
|
|
||||||
for child in terrain_node.getChildren():
|
|
||||||
child.setCollideMask(BitMask32.bit(2))
|
|
||||||
|
|
||||||
# 添加材质
|
# 创建一个平面高度图,尺寸必须是2^n+1
|
||||||
self._applyTerrainMaterial(terrain_node)
|
height_image = PNMImage(resolution, resolution)
|
||||||
|
height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围)
|
||||||
|
|
||||||
# 保存地形信息(包括高度图)
|
# 使用正确的方法设置高度图
|
||||||
terrain_info = {
|
terrain.setHeightfield(height_image)
|
||||||
'terrain': terrain,
|
terrain.setBruteforce(True) # 设置LOD
|
||||||
'node': terrain_node,
|
|
||||||
'heightmap': None,
|
|
||||||
'heightfield': height_image, # 保存高度图
|
|
||||||
'scale': (size[0], size[1], 50),
|
|
||||||
'name': node_name
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
terrain.generate()
|
||||||
'updateSceneTree'):
|
|
||||||
try:
|
|
||||||
self.world.scene_manager.updateSceneTree()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"警告: 更新场景树时出错: {e}")
|
|
||||||
|
|
||||||
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:
|
except Exception as e:
|
||||||
print(f"创建平面地形时出错: {e}")
|
print(f"❌ 创建平面地形过程失败: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
@ -390,7 +484,6 @@ class TerrainManager:
|
|||||||
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}")
|
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}")
|
||||||
print(f"✓ 重新设置了地形碰撞体")
|
print(f"✓ 重新设置了地形碰撞体")
|
||||||
|
|
||||||
|
|
||||||
return modified
|
return modified
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -405,14 +498,17 @@ class TerrainManager:
|
|||||||
# 从场景中移除地形节点
|
# 从场景中移除地形节点
|
||||||
terrain_node = terrain_info['node']
|
terrain_node = terrain_info['node']
|
||||||
if terrain_node and not terrain_node.isEmpty():
|
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')}")
|
print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}")
|
||||||
|
|
||||||
@ -461,6 +557,7 @@ class TerrainManager:
|
|||||||
if terrain_info in self.terrains:
|
if terrain_info in self.terrains:
|
||||||
terrain_node = terrain_info['node']
|
terrain_node = terrain_info['node']
|
||||||
if terrain_node and os.path.exists(texture_path):
|
if terrain_node and os.path.exists(texture_path):
|
||||||
|
texture_path = util.normalize_model_path(texture_path)
|
||||||
# 加载纹理
|
# 加载纹理
|
||||||
texture = self.world.loader.loadTexture(texture_path)
|
texture = self.world.loader.loadTexture(texture_path)
|
||||||
if texture:
|
if texture:
|
||||||
@ -476,3 +573,88 @@ class TerrainManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"应用地形纹理时出错: {e}")
|
print(f"应用地形纹理时出错: {e}")
|
||||||
return False
|
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.world = world
|
||||||
self.currentTool = "选择" # 默认工具为选择工具
|
self.currentTool = "选择" # 默认工具为选择工具
|
||||||
|
print(f"当前工具: {self.currentTool}")
|
||||||
|
|
||||||
def setCurrentTool(self, tool):
|
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.setPos(0, -50, 20)
|
||||||
self.cam.lookAt(0, 0, 0)
|
self.cam.lookAt(0, 0, 0)
|
||||||
self.camLens.setFov(80)
|
self.camLens.setFov(80)
|
||||||
|
self.cam.setTag("is_scene_element", "1")
|
||||||
|
self.cam.setTag("tree_item_type", "CAMERA_NODE")
|
||||||
print("✓ 相机设置完成")
|
print("✓ 相机设置完成")
|
||||||
|
|
||||||
def _setupLighting(self):
|
def _setupLighting(self):
|
||||||
@ -319,18 +321,61 @@ class CoreWorld(Panda3DWorld):
|
|||||||
# 创建地板节点
|
# 创建地板节点
|
||||||
self.ground = self.render.attachNewNode(cm.generate())
|
self.ground = self.render.attachNewNode(cm.generate())
|
||||||
self.ground.setP(-90)
|
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.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 = Material()
|
||||||
mat.setName("GroundMaterial")
|
mat.setName("GroundMaterial")
|
||||||
color = LColor(1, 1, 1, 0.8)
|
color = LColor(1, 1, 1, 0.8)
|
||||||
mat.set_base_color(color)
|
mat.set_base_color(color)
|
||||||
mat.set_roughness(0.5) # 设置合适的初始粗糙度
|
mat.set_roughness(1) # 设置合适的初始粗糙度
|
||||||
mat.set_metallic(0.5) # 设置较低的初始金属性
|
mat.set_metallic(0.5) # 设置较低的初始金属性
|
||||||
self.ground.set_material(mat)
|
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效果,确保支持贴图
|
# 应用默认PBR效果,确保支持贴图
|
||||||
try:
|
try:
|
||||||
if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
||||||
@ -347,6 +392,72 @@ class CoreWorld(Panda3DWorld):
|
|||||||
},
|
},
|
||||||
50
|
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效果已应用")
|
print("✓ 地板PBR效果已应用")
|
||||||
else:
|
else:
|
||||||
print("⚠️ RenderPipeline未初始化,地板将使用基础渲染")
|
print("⚠️ RenderPipeline未初始化,地板将使用基础渲染")
|
||||||
|
|||||||
@ -359,7 +359,7 @@ class GizmoDragTestWorld(Panda3DWorld):
|
|||||||
distance = distance_to_line(
|
distance = distance_to_line(
|
||||||
(mouseX, mouseY), center_screen, axis_screen
|
(mouseX, mouseY), center_screen, axis_screen
|
||||||
)
|
)
|
||||||
print(f"{axis_label}距离: {distance:.2f}")
|
#print(f"{axis_label}距离: {distance:.2f}")
|
||||||
|
|
||||||
if distance < click_threshold:
|
if distance < click_threshold:
|
||||||
print(f"✓ 点击了{axis_label}")
|
print(f"✓ 点击了{axis_label}")
|
||||||
|
|||||||
@ -24,78 +24,80 @@ except ImportError:
|
|||||||
WEB_ENGINE_AVAILABLE = False
|
WEB_ENGINE_AVAILABLE = False
|
||||||
print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用")
|
print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用")
|
||||||
|
|
||||||
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
|
# def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
|
||||||
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
|
# from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
|
||||||
|
#
|
||||||
# 参数类型检查和转换
|
# # 参数类型检查和转换
|
||||||
if isinstance(size, (list, tuple)):
|
# if isinstance(size, (list, tuple)):
|
||||||
if len(size) >= 2:
|
# if len(size) >= 2:
|
||||||
x_size, y_size = float(size[0]), float(size[1])
|
# x_size, y_size = float(size[0]), float(size[1])
|
||||||
else:
|
# else:
|
||||||
x_size = y_size = float(size[0]) if size else 1.0
|
# x_size = y_size = float(size[0]) if size else 1.0
|
||||||
else:
|
# else:
|
||||||
x_size = y_size = float(size)
|
# x_size = y_size = float(size)
|
||||||
|
#
|
||||||
# 创建卡片
|
# # 创建卡片
|
||||||
cm = CardMaker('gui_3d_image')
|
# cm = CardMaker('gui_3d_image')
|
||||||
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
|
# cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
|
||||||
|
#
|
||||||
# 创建3D图像节点
|
# # 创建3D图像节点
|
||||||
image_node = self.world.render.attachNewNode(cm.generate())
|
# image_node = self.world.render.attachNewNode(cm.generate())
|
||||||
image_node.setPos(*pos)
|
# image_node.setPos(*pos)
|
||||||
|
#
|
||||||
# 为3D图像创建独立的材质
|
# # 为3D图像创建独立的材质
|
||||||
material = Material(f"image-material-{len(self.gui_elements)}")
|
# material = Material(f"image-material-{len(self.gui_elements)}")
|
||||||
material.setBaseColor(LColor(1, 1, 1, 1))
|
# material.setBaseColor(LColor(1, 1, 1, 1))
|
||||||
material.setDiffuse(LColor(1, 1, 1, 1))
|
# material.setDiffuse(LColor(1, 1, 1, 1))
|
||||||
material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
|
# material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
|
||||||
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
|
# material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
|
||||||
material.setShininess(10.0)
|
# material.setShininess(10.0)
|
||||||
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
|
# material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
|
||||||
image_node.setMaterial(material, 1)
|
# image_node.setMaterial(material, 1)
|
||||||
|
#
|
||||||
image_node.setTransparency(TransparencyAttrib.MAlpha)
|
# image_node.setTransparency(TransparencyAttrib.MAlpha)
|
||||||
|
#
|
||||||
# 如果提供了图像路径,则加载纹理
|
# # 如果提供了图像路径,则加载纹理
|
||||||
if image_path:
|
# if image_path:
|
||||||
self.update3DImageTexture(image_node, image_path)
|
# self.update3DImageTexture(image_node, image_path)
|
||||||
|
#
|
||||||
# 应用PBR效果(如果可用)
|
# # 应用PBR效果(如果可用)
|
||||||
try:
|
# try:
|
||||||
if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
# if hasattr(self, 'render_pipeline') and self.render_pipeline:
|
||||||
self.render_pipeline.set_effect(
|
# self.render_pipeline.set_effect(
|
||||||
image_node,
|
# image_node,
|
||||||
"effects/default.yaml",
|
# "effects/default.yaml",
|
||||||
{
|
# {
|
||||||
"normal_mapping": True,
|
# "normal_mapping": True,
|
||||||
"render_gbuffer": True,
|
# "render_gbuffer": True,
|
||||||
"alpha_testing": False,
|
# "alpha_testing": False,
|
||||||
"parallax_mapping": False,
|
# "parallax_mapping": False,
|
||||||
"render_shadow": False,
|
# "render_shadow": False,
|
||||||
"render_envmap": True,
|
# "render_envmap": True,
|
||||||
"disable_children_effects": True
|
# "disable_children_effects": True
|
||||||
},
|
# },
|
||||||
50
|
# 50
|
||||||
)
|
# )
|
||||||
print("✓ GUI 3D图像PBR效果已应用")
|
# print("✓ GUI 3D图像PBR效果已应用")
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
|
# print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
|
||||||
|
#
|
||||||
# 为GUI元素添加标识(效仿3D文本方法)
|
# # 为GUI元素添加标识(效仿3D文本方法)
|
||||||
image_node.setTag("gui_type", "3d_image")
|
# image_node.setTag("gui_type", "3d_image")
|
||||||
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
|
# image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
|
||||||
if image_path:
|
# image_node.setTag("is_scene_element", "1")
|
||||||
image_node.setTag("gui_image_path", image_path)
|
# image_node.setTag("tree_item_type", "GUI_3DIMAGE")
|
||||||
image_node.setTag("is_gui_element", "1")
|
# if image_path:
|
||||||
|
# image_node.setTag("gui_image_path", image_path)
|
||||||
self.gui_elements.append(image_node)
|
# image_node.setTag("is_gui_element", "1")
|
||||||
|
#
|
||||||
# 更新场景树
|
# self.gui_elements.append(image_node)
|
||||||
if hasattr(self.world, 'updateSceneTree'):
|
#
|
||||||
self.world.updateSceneTree()
|
# # 更新场景树
|
||||||
|
# if hasattr(self.world, 'updateSceneTree'):
|
||||||
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
|
# self.world.updateSceneTree()
|
||||||
return image_node
|
#
|
||||||
|
# print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
|
||||||
|
# return image_node
|
||||||
|
|
||||||
class GUIManager:
|
class GUIManager:
|
||||||
"""GUI元素管理系统类"""
|
"""GUI元素管理系统类"""
|
||||||
@ -124,7 +126,7 @@ class GUIManager:
|
|||||||
|
|
||||||
# ==================== GUI元素创建方法 ====================
|
# ==================== 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父子关系,优化版本"""
|
"""创建2D GUI按钮 - 支持多选创建和GUI父子关系,优化版本"""
|
||||||
try:
|
try:
|
||||||
from direct.gui.DirectGui import DirectButton
|
from direct.gui.DirectGui import DirectButton
|
||||||
@ -180,17 +182,35 @@ class GUIManager:
|
|||||||
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
||||||
rolloverSound=None,
|
rolloverSound=None,
|
||||||
clickSound=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_type", "button")
|
||||||
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
|
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
|
||||||
button.setTag("gui_text", text)
|
button.setTag("gui_text", text)
|
||||||
button.setTag("is_gui_element", "1")
|
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("created_by_user", "1")
|
||||||
button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||||
|
button.setTag("name", button_name)
|
||||||
button.setName(button_name)
|
button.setName(button_name)
|
||||||
|
|
||||||
# 如果有GUI父节点,建立引用关系
|
# 如果有GUI父节点,建立引用关系
|
||||||
@ -200,6 +220,7 @@ class GUIManager:
|
|||||||
|
|
||||||
# 添加到GUI元素列表
|
# 添加到GUI元素列表
|
||||||
self.gui_elements.append(button)
|
self.gui_elements.append(button)
|
||||||
|
button.reparentTo(self.world.aspect2d)
|
||||||
|
|
||||||
print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}")
|
print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}")
|
||||||
|
|
||||||
@ -221,11 +242,11 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的按钮并更新场景树
|
# 选中最后创建的按钮并更新场景树
|
||||||
if created_buttons:
|
# if created_buttons:
|
||||||
last_button, last_qt_item = created_buttons[-1]
|
# last_button, last_qt_item = created_buttons[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮")
|
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_id", f"label_{len(self.gui_elements)}")
|
||||||
label.setTag("gui_text", text)
|
label.setTag("gui_text", text)
|
||||||
label.setTag("is_gui_element", "1")
|
label.setTag("is_gui_element", "1")
|
||||||
|
label.setTag("tree_item_type", "GUI_LABEL")
|
||||||
label.setTag("is_scene_element", "1")
|
label.setTag("is_scene_element", "1")
|
||||||
label.setTag("created_by_user", "1")
|
label.setTag("created_by_user", "1")
|
||||||
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||||
|
label.setTag("name",label_name)
|
||||||
label.setName(label_name)
|
label.setName(label_name)
|
||||||
|
|
||||||
# 如果有GUI父节点,建立引用关系
|
# 如果有GUI父节点,建立引用关系
|
||||||
@ -334,11 +357,11 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的标签并更新场景树
|
# 选中最后创建的标签并更新场景树
|
||||||
if created_labels:
|
# if created_labels:
|
||||||
last_label, last_qt_item = created_labels[-1]
|
# last_label, last_qt_item = created_labels[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
tree_widget.update_selection_and_properties(last_label, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_label, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签")
|
print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签")
|
||||||
|
|
||||||
@ -394,6 +417,10 @@ class GUIManager:
|
|||||||
parent_gui_node = None
|
parent_gui_node = None
|
||||||
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
||||||
|
|
||||||
|
font = None
|
||||||
|
if hasattr(self.world,'getChineseFont'):
|
||||||
|
font = self.world.getChineseFont()
|
||||||
|
|
||||||
entry = DirectEntry(
|
entry = DirectEntry(
|
||||||
text="",
|
text="",
|
||||||
pos=gui_pos,
|
pos=gui_pos,
|
||||||
@ -404,7 +431,9 @@ class GUIManager:
|
|||||||
numLines=1,
|
numLines=1,
|
||||||
width=12,
|
width=12,
|
||||||
focus=0,
|
focus=0,
|
||||||
parent=parent_gui_node # 设置GUI父节点
|
parent=parent_gui_node, # 设置GUI父节点
|
||||||
|
text_font = font,
|
||||||
|
frameSize=(-0.1,0.1,-0.05,0.05)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 设置节点标签
|
# 设置节点标签
|
||||||
@ -412,9 +441,11 @@ class GUIManager:
|
|||||||
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
|
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
|
||||||
entry.setTag("gui_placeholder", placeholder)
|
entry.setTag("gui_placeholder", placeholder)
|
||||||
entry.setTag("is_gui_element", "1")
|
entry.setTag("is_gui_element", "1")
|
||||||
|
entry.setTag("tree_item_type", "GUI_ENTRY")
|
||||||
entry.setTag("is_scene_element", "1")
|
entry.setTag("is_scene_element", "1")
|
||||||
entry.setTag("created_by_user", "1")
|
entry.setTag("created_by_user", "1")
|
||||||
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||||
|
entry.setTag("name",entry_name)
|
||||||
entry.setName(entry_name)
|
entry.setName(entry_name)
|
||||||
|
|
||||||
# 如果有GUI父节点,建立引用关系
|
# 如果有GUI父节点,建立引用关系
|
||||||
@ -445,11 +476,11 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的输入框并更新场景树
|
# 选中最后创建的输入框并更新场景树
|
||||||
if created_entries:
|
# if created_entries:
|
||||||
last_entry, last_qt_item = created_entries[-1]
|
# last_entry, last_qt_item = created_entries[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
tree_widget.update_selection_and_properties(last_entry, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_entry, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
|
print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
|
||||||
|
|
||||||
@ -509,7 +540,11 @@ class GUIManager:
|
|||||||
cm = CardMaker('gui-2d-image')
|
cm = CardMaker('gui-2d-image')
|
||||||
cm.setFrame(-size, size, -size, size)
|
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.setPos(gui_pos)
|
||||||
image_node.setBin('fixed', 0)
|
image_node.setBin('fixed', 0)
|
||||||
image_node.setDepthWrite(False)
|
image_node.setDepthWrite(False)
|
||||||
@ -522,6 +557,7 @@ class GUIManager:
|
|||||||
# 如果提供了图像路径,则加载纹理
|
# 如果提供了图像路径,则加载纹理
|
||||||
if image_path:
|
if image_path:
|
||||||
try:
|
try:
|
||||||
|
image_node.setTag("image_path", image_path)
|
||||||
texture = self.world.loader.loadTexture(image_path)
|
texture = self.world.loader.loadTexture(image_path)
|
||||||
if texture:
|
if texture:
|
||||||
image_node.setTexture(texture, 1)
|
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("gui_text", f"2D图片_{len(self.gui_elements)}")
|
||||||
image_node.setTag("is_gui_element", "1")
|
image_node.setTag("is_gui_element", "1")
|
||||||
image_node.setTag("is_scene_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("created_by_user", "1")
|
||||||
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
|
||||||
|
image_node.setTag("name",image_name)
|
||||||
image_node.setName(image_name)
|
image_node.setName(image_name)
|
||||||
|
|
||||||
# 如果有GUI父节点,建立引用关系
|
# 如果有GUI父节点,建立引用关系
|
||||||
@ -573,11 +611,11 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的按钮并更新场景树
|
# 选中最后创建的按钮并更新场景树
|
||||||
if created_2dimage:
|
# if created_2dimage:
|
||||||
last_button, last_qt_item = created_2dimage[-1]
|
# last_button, last_qt_item = created_2dimage[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮")
|
print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮")
|
||||||
|
|
||||||
@ -692,7 +730,9 @@ class GUIManager:
|
|||||||
textNodePath.setTag("gui_text", text)
|
textNodePath.setTag("gui_text", text)
|
||||||
textNodePath.setTag("is_gui_element", "1")
|
textNodePath.setTag("is_gui_element", "1")
|
||||||
textNodePath.setTag("is_scene_element", "1")
|
textNodePath.setTag("is_scene_element", "1")
|
||||||
|
textNodePath.setTag("tree_item_type", "GUI_3DTEXT")
|
||||||
textNodePath.setTag("created_by_user", "1")
|
textNodePath.setTag("created_by_user", "1")
|
||||||
|
textNodePath.setTag("name", text_name)
|
||||||
|
|
||||||
# 添加到GUI元素列表
|
# 添加到GUI元素列表
|
||||||
self.gui_elements.append(textNodePath)
|
self.gui_elements.append(textNodePath)
|
||||||
@ -717,11 +757,11 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的文本并更新场景树
|
# 选中最后创建的文本并更新场景树
|
||||||
if created_texts:
|
# if created_texts:
|
||||||
last_text, last_qt_item = created_texts[-1]
|
# last_text, last_qt_item = created_texts[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
tree_widget.update_selection_and_properties(last_text, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_text, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_texts)} 个3D文本")
|
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):
|
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
|
||||||
"""创建3D空间图片"""
|
"""创建3D空间图片"""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
from panda3d.core import TextNode
|
from panda3d.core import TextNode
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
@ -779,7 +820,8 @@ class GUIManager:
|
|||||||
cm.setFrame(-x_size / 2, x_size / 2, -y_size / 2, y_size / 2)
|
cm.setFrame(-x_size / 2, x_size / 2, -y_size / 2, y_size / 2)
|
||||||
|
|
||||||
# 创建3D图像节点
|
# 创建3D图像节点
|
||||||
image_node = self.world.render.attachNewNode(cm.generate())
|
# image_node = self.world.render.attachNewNode(cm.generate())
|
||||||
|
image_node = parent_node.attachNewNode(cm.generate())
|
||||||
image_node.setPos(*pos)
|
image_node.setPos(*pos)
|
||||||
|
|
||||||
# 为3D图像创建独立的材质
|
# 为3D图像创建独立的材质
|
||||||
@ -824,10 +866,12 @@ class GUIManager:
|
|||||||
image_node.setTag("gui_type", "3d_image")
|
image_node.setTag("gui_type", "3d_image")
|
||||||
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
|
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
|
||||||
if image_path:
|
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_gui_element", "1")
|
||||||
image_node.setTag("is_scene_element", "1")
|
image_node.setTag("is_scene_element", "1")
|
||||||
|
image_node.setTag("tree_item_type", "GUI_3DIMAGE")
|
||||||
image_node.setTag("created_by_user", "1")
|
image_node.setTag("created_by_user", "1")
|
||||||
|
image_node.setTag("name",image_name)
|
||||||
|
|
||||||
# 添加到GUI元素列表
|
# 添加到GUI元素列表
|
||||||
self.gui_elements.append(image_node)
|
self.gui_elements.append(image_node)
|
||||||
@ -852,11 +896,11 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的文本并更新场景树
|
# 选中最后创建的文本并更新场景树
|
||||||
if created_3dimage:
|
# if created_3dimage:
|
||||||
last_image, last_qt_item = created_3dimage[-1]
|
# last_image, last_qt_item = created_3dimage[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
tree_widget.update_selection_and_properties(last_image, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_image, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本")
|
print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本")
|
||||||
|
|
||||||
@ -872,7 +916,7 @@ class GUIManager:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
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视频播放屏幕 - 添加占位符纹理支持"""
|
"""创建3D视频播放屏幕 - 添加占位符纹理支持"""
|
||||||
try:
|
try:
|
||||||
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
|
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
|
||||||
@ -895,7 +939,7 @@ class GUIManager:
|
|||||||
size = float(size)
|
size = float(size)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}")
|
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}")
|
||||||
size = 0.2
|
size = 0.2*5
|
||||||
|
|
||||||
print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}")
|
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("gui_text", f"视频屏幕_{len(self.gui_elements)}")
|
||||||
video_screen.setTag("is_gui_element", "1")
|
video_screen.setTag("is_gui_element", "1")
|
||||||
video_screen.setTag("is_scene_element", "1")
|
video_screen.setTag("is_scene_element", "1")
|
||||||
|
video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN")
|
||||||
video_screen.setTag("created_by_user", "1")
|
video_screen.setTag("created_by_user", "1")
|
||||||
|
video_screen.setTag("name",screen_name)
|
||||||
|
|
||||||
# 设置视频路径标签
|
# 设置视频路径标签
|
||||||
if video_path and os.path.exists(video_path):
|
if video_path and os.path.exists(video_path):
|
||||||
@ -1042,12 +1088,12 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的视频屏幕
|
# 选中最后创建的视频屏幕
|
||||||
if created_videoscreens:
|
# if created_videoscreens:
|
||||||
last_screen_np, last_qt_item = created_videoscreens[-1]
|
# last_screen_np, last_qt_item = created_videoscreens[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
# 更新选择和属性面板
|
# # 更新选择和属性面板
|
||||||
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕")
|
print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕")
|
||||||
|
|
||||||
@ -1125,6 +1171,7 @@ class GUIManager:
|
|||||||
# 检查是否有播放方法
|
# 检查是否有播放方法
|
||||||
if hasattr(movie_texture, 'play'):
|
if hasattr(movie_texture, 'play'):
|
||||||
try:
|
try:
|
||||||
|
self.loadVideoFile(video_screen, video_screen.getTag("video_path"))
|
||||||
movie_texture.play()
|
movie_texture.play()
|
||||||
print(f"▶️ 继续播放视频: {video_screen.getName()}")
|
print(f"▶️ 继续播放视频: {video_screen.getName()}")
|
||||||
return True
|
return True
|
||||||
@ -1135,9 +1182,7 @@ class GUIManager:
|
|||||||
print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}")
|
print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}")
|
self.loadVideoFile(video_screen,video_screen.getTag("video_path"))
|
||||||
self._debugVideoScreenTextures(video_screen)
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 播放视频失败: {e}")
|
print(f"❌ 播放视频失败: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
@ -1264,7 +1309,6 @@ class GUIManager:
|
|||||||
def loadVideoFile(self, video_screen, video_path):
|
def loadVideoFile(self, video_screen, video_path):
|
||||||
"""为视频屏幕加载新的视频文件"""
|
"""为视频屏幕加载新的视频文件"""
|
||||||
try:
|
try:
|
||||||
from panda3d.core import Texture, TextureStage
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
if not os.path.exists(video_path):
|
if not os.path.exists(video_path):
|
||||||
@ -1422,8 +1466,6 @@ class GUIManager:
|
|||||||
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}")
|
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}")
|
||||||
size = 0.2
|
size = 0.2
|
||||||
|
|
||||||
print(f"📺 开始创建2D视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}")
|
|
||||||
|
|
||||||
# 获取树形控件
|
# 获取树形控件
|
||||||
tree_widget = self._get_tree_widget()
|
tree_widget = self._get_tree_widget()
|
||||||
if not tree_widget:
|
if not tree_widget:
|
||||||
@ -1450,7 +1492,7 @@ class GUIManager:
|
|||||||
frameColor=(1, 1, 1, 1), # 默认背景色
|
frameColor=(1, 1, 1, 1), # 默认背景色
|
||||||
pos=(pos[0] * 0.1, 0, pos[1] * 0.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,
|
parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d,
|
||||||
suppressMouse=True
|
suppressMouse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
video_screen.setName(screen_name)
|
video_screen.setName(screen_name)
|
||||||
@ -1458,19 +1500,18 @@ class GUIManager:
|
|||||||
# 设置透明度支持
|
# 设置透明度支持
|
||||||
video_screen.setTransparency(TransparencyAttrib.MAlpha)
|
video_screen.setTransparency(TransparencyAttrib.MAlpha)
|
||||||
|
|
||||||
# 设置2D视频屏幕特有的标签
|
#设置2D视频屏幕特有的标签
|
||||||
video_screen.setTag("gui_type", "2d_video_screen")
|
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_id", f"2d_video_screen_{len(self.gui_elements)}")
|
||||||
video_screen.setTag("gui_text", f"2D视频屏幕_{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_gui_element", "1")
|
||||||
video_screen.setTag("is_scene_element", "1")
|
video_screen.setTag("is_scene_element", "1")
|
||||||
|
video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN")
|
||||||
video_screen.setTag("created_by_user", "1")
|
video_screen.setTag("created_by_user", "1")
|
||||||
|
video_screen.setTag("name",screen_name)
|
||||||
|
|
||||||
# 设置视频路径标签
|
video_screen.setTag("video_path", video_path if video_path else "")
|
||||||
if video_path and os.path.exists(video_path):
|
print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else '空'}")
|
||||||
video_screen.setTag("video_path", video_path)
|
|
||||||
else:
|
|
||||||
video_screen.setTag("video_path", "")
|
|
||||||
|
|
||||||
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
|
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
|
||||||
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
|
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
|
||||||
@ -1543,12 +1584,12 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的视频屏幕
|
# 选中最后创建的视频屏幕
|
||||||
if created_videoscreens:
|
# if created_videoscreens:
|
||||||
last_screen_np, last_qt_item = created_videoscreens[-1]
|
# last_screen_np, last_qt_item = created_videoscreens[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
# 更新选择和属性面板
|
# # 更新选择和属性面板
|
||||||
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕")
|
print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕")
|
||||||
|
|
||||||
@ -1569,7 +1610,11 @@ class GUIManager:
|
|||||||
try:
|
try:
|
||||||
import os
|
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}")
|
print(f"❌ 2D视频文件不存在: {video_path}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1581,7 +1626,6 @@ class GUIManager:
|
|||||||
|
|
||||||
# 保存视频纹理引用
|
# 保存视频纹理引用
|
||||||
video_screen.setPythonTag("movie_texture", movie_texture)
|
video_screen.setPythonTag("movie_texture", movie_texture)
|
||||||
video_screen.setTag("video_path", video_path)
|
|
||||||
|
|
||||||
print(f"✅ 成功加载新2D视频: {video_path}")
|
print(f"✅ 成功加载新2D视频: {video_path}")
|
||||||
return True
|
return True
|
||||||
@ -1776,6 +1820,7 @@ class GUIManager:
|
|||||||
# 设置标签以便识别和管理
|
# 设置标签以便识别和管理
|
||||||
sphere_np.setTag("gui_type", "spherical_video")
|
sphere_np.setTag("gui_type", "spherical_video")
|
||||||
sphere_np.setTag("is_gui_element", "1")
|
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("video_path", video_path or "")
|
||||||
sphere_np.setTag("original_radius", str(radius))
|
sphere_np.setTag("original_radius", str(radius))
|
||||||
|
|
||||||
@ -1808,12 +1853,12 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的球形视频
|
# 选中最后创建的球形视频
|
||||||
if created_spherical_videos:
|
# if created_spherical_videos:
|
||||||
last_sphere_np, last_qt_item = created_spherical_videos[-1]
|
# last_sphere_np, last_qt_item = created_spherical_videos[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
# 更新选择和属性面板
|
# # 更新选择和属性面板
|
||||||
tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频")
|
print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频")
|
||||||
|
|
||||||
@ -1988,6 +2033,7 @@ class GUIManager:
|
|||||||
virtual_screen.setTag("gui_text", text)
|
virtual_screen.setTag("gui_text", text)
|
||||||
virtual_screen.setTag("is_gui_element", "1")
|
virtual_screen.setTag("is_gui_element", "1")
|
||||||
virtual_screen.setTag("is_scene_element", "1")
|
virtual_screen.setTag("is_scene_element", "1")
|
||||||
|
virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen")
|
||||||
virtual_screen.setTag("created_by_user", "1")
|
virtual_screen.setTag("created_by_user", "1")
|
||||||
|
|
||||||
# 添加到GUI元素列表
|
# 添加到GUI元素列表
|
||||||
@ -2013,12 +2059,12 @@ class GUIManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 选中最后创建的虚拟屏幕
|
# 选中最后创建的虚拟屏幕
|
||||||
if created_screens:
|
# if created_screens:
|
||||||
last_screen_np, last_qt_item = created_screens[-1]
|
# last_screen_np, last_qt_item = created_screens[-1]
|
||||||
if last_qt_item:
|
# if last_qt_item:
|
||||||
tree_widget.setCurrentItem(last_qt_item)
|
# tree_widget.setCurrentItem(last_qt_item)
|
||||||
# 更新选择和属性面板
|
# # 更新选择和属性面板
|
||||||
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
|
||||||
|
|
||||||
print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕")
|
print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕")
|
||||||
|
|
||||||
@ -2070,7 +2116,9 @@ class GUIManager:
|
|||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 暂无滑块功能
|
||||||
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
|
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
|
||||||
|
pass
|
||||||
"""创建2D GUI滑块"""
|
"""创建2D GUI滑块"""
|
||||||
from direct.gui.DirectGui import DirectSlider
|
from direct.gui.DirectGui import DirectSlider
|
||||||
|
|
||||||
@ -2105,19 +2153,22 @@ class GUIManager:
|
|||||||
"""删除GUI元素"""
|
"""删除GUI元素"""
|
||||||
try:
|
try:
|
||||||
if gui_element in self.gui_elements:
|
if gui_element in self.gui_elements:
|
||||||
# 移除GUI元素
|
# # 移除GUI元素
|
||||||
if hasattr(gui_element, 'removeNode'):
|
# if hasattr(gui_element, 'removeNode'):
|
||||||
gui_element.removeNode()
|
# gui_element.removeNode()
|
||||||
elif hasattr(gui_element, 'destroy'):
|
# elif hasattr(gui_element, 'destroy'):
|
||||||
gui_element.destroy()
|
# gui_element.destroy()
|
||||||
|
#
|
||||||
# 从列表中移除
|
# # 从列表中移除
|
||||||
self.gui_elements.remove(gui_element)
|
# self.gui_elements.remove(gui_element)
|
||||||
|
|
||||||
# 更新场景树
|
# 更新场景树
|
||||||
# 安全地调用updateSceneTree
|
# 安全地调用updateSceneTree
|
||||||
if hasattr(self.world, 'updateSceneTree'):
|
tree_widget = self._get_tree_widget()
|
||||||
self.world.updateSceneTree()
|
if tree_widget:
|
||||||
|
tree_widget.delete_items(tree_widget.selectedItems())
|
||||||
|
# if hasattr(self.world, 'updateSceneTree'):
|
||||||
|
# self.world.updateSceneTree()
|
||||||
|
|
||||||
print(f"删除GUI元素: {gui_element}")
|
print(f"删除GUI元素: {gui_element}")
|
||||||
return True
|
return True
|
||||||
@ -3207,7 +3258,7 @@ class GUIManager:
|
|||||||
try:
|
try:
|
||||||
gui_type = gui_element.getTag("gui_type")
|
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()
|
current_pos = gui_element.getPos()
|
||||||
|
|
||||||
if axis == "x":
|
if axis == "x":
|
||||||
@ -3220,7 +3271,7 @@ class GUIManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
gui_element.setPos(*new_pos)
|
gui_element.setPos(*new_pos)
|
||||||
print(f"✓ 更新3D GUI元素位置: {axis}={value}")
|
#print(f"✓ 更新3D GUI元素位置: {axis}={value}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}")
|
print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}")
|
||||||
@ -3241,7 +3292,7 @@ class GUIManager:
|
|||||||
if value == 0:
|
if value == 0:
|
||||||
value = 0.01
|
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元素处理
|
# 3D元素处理
|
||||||
if axis == "x":
|
if axis == "x":
|
||||||
new_scale = (value, current_scale.getY(), current_scale.getZ())
|
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 |
127
main.py
127
main.py
@ -22,6 +22,7 @@ from core.script_system import ScriptManager
|
|||||||
from core.vr_manager import VRManager
|
from core.vr_manager import VRManager
|
||||||
from core.vr_input_handler import VRInputHandler
|
from core.vr_input_handler import VRInputHandler
|
||||||
from core.alvr_streamer import ALVRStreamer
|
from core.alvr_streamer import ALVRStreamer
|
||||||
|
from core.patrol_system import PatrolSystem
|
||||||
from gui.gui_manager import GUIManager
|
from gui.gui_manager import GUIManager
|
||||||
from core.terrain_manager import TerrainManager
|
from core.terrain_manager import TerrainManager
|
||||||
from scene.scene_manager import SceneManager
|
from scene.scene_manager import SceneManager
|
||||||
@ -56,6 +57,15 @@ class MyWorld(CoreWorld):
|
|||||||
# 初始化选择和变换系统
|
# 初始化选择和变换系统
|
||||||
self.selection = SelectionSystem(self)
|
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)
|
self.event_handler = EventHandler(self)
|
||||||
|
|
||||||
@ -225,7 +235,7 @@ class MyWorld(CoreWorld):
|
|||||||
"""创建2D GUI文本输入框"""
|
"""创建2D GUI文本输入框"""
|
||||||
return self.gui_manager.createGUIEntry(pos, placeholder, size)
|
return self.gui_manager.createGUIEntry(pos, placeholder, size)
|
||||||
|
|
||||||
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
|
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1):
|
||||||
"""创建3D空间文本"""
|
"""创建3D空间文本"""
|
||||||
return self.gui_manager.createGUI3DText(pos, text, size)
|
return self.gui_manager.createGUI3DText(pos, text, size)
|
||||||
|
|
||||||
@ -233,9 +243,9 @@ class MyWorld(CoreWorld):
|
|||||||
"""创建3D图片"""
|
"""创建3D图片"""
|
||||||
return self.gui_manager.createGUI3DImage(pos,text,size)
|
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图片"""
|
"""创建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):
|
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)
|
return self.scene_manager.importModelAsync(filepath)
|
||||||
|
|
||||||
def loadAnimatedModel(self, model_path, anims=None):
|
|
||||||
"""加载带动画的模型"""
|
|
||||||
return self.scene_manager.loadAnimatedModel(model_path, anims)
|
|
||||||
|
|
||||||
# 材质和几何体处理方法 - 代理到scene_manager
|
# 材质和几何体处理方法 - 代理到scene_manager
|
||||||
def processMaterials(self, model):
|
def processMaterials(self, model):
|
||||||
"""处理模型材质"""
|
"""处理模型材质"""
|
||||||
@ -814,6 +820,97 @@ class MyWorld(CoreWorld):
|
|||||||
"""获取碰撞统计"""
|
"""获取碰撞统计"""
|
||||||
return self.collision_manager.getCollisionStatistics()
|
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模块的对应功能
|
# 以下函数代理到project_manager模块的对应功能
|
||||||
@ -830,6 +927,7 @@ def createNewProject(parent_window):
|
|||||||
|
|
||||||
def saveProject(appw):
|
def saveProject(appw):
|
||||||
"""保存项目 - 代理到project_manager"""
|
"""保存项目 - 代理到project_manager"""
|
||||||
|
|
||||||
world = appw.centralWidget().world
|
world = appw.centralWidget().world
|
||||||
return world.project_manager.saveProject(appw)
|
return world.project_manager.saveProject(appw)
|
||||||
|
|
||||||
@ -838,19 +936,26 @@ def openProject(appw):
|
|||||||
world = appw.centralWidget().world
|
world = appw.centralWidget().world
|
||||||
return world.project_manager.openProject(appw)
|
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):
|
def buildPackage(appw):
|
||||||
"""打包项目 - 代理到project_manager"""
|
"""打包项目 - 代理到project_manager"""
|
||||||
world = appw.centralWidget().world
|
world = appw.centralWidget().world
|
||||||
return world.project_manager.buildPackage(appw)
|
return world.project_manager.buildPackage(appw)
|
||||||
|
|
||||||
|
def run(args = None):
|
||||||
if __name__ == "__main__":
|
|
||||||
world = MyWorld()
|
world = MyWorld()
|
||||||
|
|
||||||
# 使用新的UI模块创建主窗口
|
# 使用新的UI模块创建主窗口
|
||||||
from ui.main_window import setup_main_window
|
from ui.main_window import setup_main_window
|
||||||
|
print(f'Path is {args}')
|
||||||
app, main_window = setup_main_window(world)
|
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 os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import datetime
|
import datetime
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
@ -47,7 +46,9 @@ class ProjectManager:
|
|||||||
|
|
||||||
project_path = dialog.projectPath
|
project_path = dialog.projectPath
|
||||||
project_name = dialog.projectName
|
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:
|
try:
|
||||||
# 创建项目文件夹结构
|
# 创建项目文件夹结构
|
||||||
@ -78,7 +79,7 @@ class ProjectManager:
|
|||||||
|
|
||||||
# 自动保存初始场景
|
# 自动保存初始场景
|
||||||
scene_file = os.path.join(scenes_path, "scene.bam")
|
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)
|
project_config["scene_file"] = os.path.relpath(scene_file, full_project_path)
|
||||||
with open(config_file, "w", encoding="utf-8") as f:
|
with open(config_file, "w", encoding="utf-8") as f:
|
||||||
@ -135,23 +136,114 @@ class ProjectManager:
|
|||||||
|
|
||||||
# 检查场景文件
|
# 检查场景文件
|
||||||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||||
if not os.path.exists(scene_file):
|
if os.path.exists(scene_file):
|
||||||
QMessageBox.warning(parent_window, "警告", "没有找到场景文件!")
|
# 加载场景
|
||||||
|
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
|
return False
|
||||||
|
|
||||||
# 加载场景
|
# 读取项目配置
|
||||||
if self.world.scene_manager.loadScene(scene_file):
|
with open(config_file, "r", encoding="utf-8") as f:
|
||||||
# 更新项目配置
|
project_config = json.load(f)
|
||||||
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)
|
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")
|
||||||
self.current_project_path = project_path
|
with open(config_file, "w", encoding="utf-8") as f:
|
||||||
self.project_config = project_config
|
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
|
parent_window.current_project_path = project_path
|
||||||
|
|
||||||
@ -164,13 +256,22 @@ class ProjectManager:
|
|||||||
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
|
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
|
||||||
|
|
||||||
QMessageBox.information(parent_window, "成功", "项目加载成功!")
|
QMessageBox.information(parent_window, "成功", "项目加载成功!")
|
||||||
|
|
||||||
|
print(f"项目 '{project_path}' 加载成功!")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
|
if parent_window:
|
||||||
|
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
|
||||||
|
else:
|
||||||
|
print("错误: 加载场景失败!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}")
|
error_msg = f"加载项目时发生错误:{str(e)}"
|
||||||
|
if parent_window:
|
||||||
|
QMessageBox.critical(parent_window, "错误", error_msg)
|
||||||
|
else:
|
||||||
|
print(error_msg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def saveProject(self, parent_window):
|
def saveProject(self, parent_window):
|
||||||
@ -201,7 +302,7 @@ class ProjectManager:
|
|||||||
return False
|
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")
|
config_file = os.path.join(project_path, "project.json")
|
||||||
if os.path.exists(config_file):
|
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):
|
def normalize_model_path(self, filepath):
|
||||||
"""标准化模型文件路径"""
|
"""标准化模型文件路径"""
|
||||||
try:
|
try:
|
||||||
print(f"\n=== 路径标准化处理 ===")
|
#print(f"\n=== 路径标准化处理 ===")
|
||||||
print(f"原始路径: {filepath}")
|
#print(f"原始路径: {filepath}")
|
||||||
print(f"当前系统: {self.system}")
|
#print(f"当前系统: {self.system}")
|
||||||
|
|
||||||
# 步骤1: 检查原始路径是否存在
|
# 步骤1: 检查原始路径是否存在
|
||||||
if self._check_file_exists(filepath):
|
if self._check_file_exists(filepath):
|
||||||
@ -54,10 +54,6 @@ class CrossPlatformPathHandler:
|
|||||||
def _check_file_exists(self, filepath):
|
def _check_file_exists(self, filepath):
|
||||||
"""检查文件是否存在"""
|
"""检查文件是否存在"""
|
||||||
exists = os.path.exists(filepath)
|
exists = os.path.exists(filepath)
|
||||||
if exists:
|
|
||||||
print(f"✓ 文件存在: {filepath}")
|
|
||||||
else:
|
|
||||||
print(f"⚠️ 文件不存在: {filepath}")
|
|
||||||
return exists
|
return exists
|
||||||
|
|
||||||
def _panda3d_normalize(self, filepath):
|
def _panda3d_normalize(self, filepath):
|
||||||
|
|||||||
@ -21,6 +21,10 @@ class RotatorScript(ScriptBase):
|
|||||||
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
|
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
|
||||||
|
|
||||||
def update(self, dt):
|
def update(self, dt):
|
||||||
|
# 检查 gameObject 是否存在且不为空
|
||||||
|
if not self.gameObject or self.gameObject.isEmpty():
|
||||||
|
print("RotatorScript: gameObject is empty or None, skipping update")
|
||||||
|
return
|
||||||
"""每帧更新"""
|
"""每帧更新"""
|
||||||
if not self.is_rotating:
|
if not self.is_rotating:
|
||||||
return
|
return
|
||||||
|
|||||||
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()
|
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):
|
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)
|
self.world.selection.updateSelection(None)
|
||||||
|
print("点击空白区域,清除选中状态")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.world.property_panel.updatePropertyPanel(item)
|
self.world.property_panel.updatePropertyPanel(item)
|
||||||
@ -39,187 +68,90 @@ class InterfaceManager:
|
|||||||
# 更新选择状态
|
# 更新选择状态
|
||||||
self.world.selected_np = nodePath
|
self.world.selected_np = nodePath
|
||||||
self.world.selection.updateSelection(nodePath)
|
self.world.selection.updateSelection(nodePath)
|
||||||
|
|
||||||
# 更新属性面板
|
|
||||||
#self.world.property_panel.updatePropertyPanel(item)
|
|
||||||
|
|
||||||
print(f"树形控件点击: {item.text(0)}")
|
print(f"树形控件点击: {item.text(0)}")
|
||||||
else:
|
else:
|
||||||
# 如果没有节点对象,清除选择
|
# 如果没有节点对象,清除选择
|
||||||
self.world.selection.updateSelection(None)
|
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.property_panel.clearPropertyPanel()
|
||||||
self.world.selection.updateSelection(None)
|
print("点击了无数据项,清除选中状态")
|
||||||
|
|
||||||
# 更新场景树
|
# def showTreeContextMenu(self, position):
|
||||||
self.updateSceneTree()
|
# """显示树形控件的右键菜单"""
|
||||||
|
# item = self.treeWidget.itemAt(position)
|
||||||
except Exception as e:
|
# if not item:
|
||||||
print(f"删除 Cesium tileset 失败: {str(e)}")
|
# 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):
|
def isModelOrChild(self, item):
|
||||||
"""检查是否是模型节点或其子节点"""
|
"""检查是否是模型节点或其子节点"""
|
||||||
@ -248,9 +180,9 @@ class InterfaceManager:
|
|||||||
if terrain_to_remove:
|
if terrain_to_remove:
|
||||||
self.world.terrain_manager.deleteTerrain(terrain_to_remove)
|
self.world.terrain_manager.deleteTerrain(terrain_to_remove)
|
||||||
print(f"成功删除地形节点:{nodePath.getName()}")
|
print(f"成功删除地形节点:{nodePath.getName()}")
|
||||||
self.updateSceneTree()
|
# self.updateSceneTree()
|
||||||
self.world.property_panel.clearPropertyPanel()
|
# self.world.property_panel.clearPropertyPanel()
|
||||||
self.world.selection.updateSelection(None)
|
# self.world.selection.updateSelection(None)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 先递归删除所有子节点中的灯光
|
# 先递归删除所有子节点中的灯光
|
||||||
@ -408,10 +340,19 @@ class InterfaceManager:
|
|||||||
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
|
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
|
||||||
|
|
||||||
#添加灯光节点
|
#添加灯光节点
|
||||||
for light in self.world.Spotlight + self.world.Pointlight:
|
for light in self.world.Spotlight:
|
||||||
if not light.isEmpty:
|
if light:
|
||||||
addNodeToTree(light, sceneRoot, force=True)
|
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
|
#添加 Cesium tilesets
|
||||||
if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'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):
|
for i , tileset_info in enumerate(self.world.scene_manager.tilesets):
|
||||||
|
|||||||
2024
ui/main_window.py
2024
ui/main_window.py
File diff suppressed because it is too large
Load Diff
3183
ui/property_panel.py
3183
ui/property_panel.py
File diff suppressed because it is too large
Load Diff
679
ui/widgets.py
679
ui/widgets.py
@ -17,7 +17,8 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
|
|||||||
from PyQt5.QtCore import Qt, QUrl, QMimeData
|
from PyQt5.QtCore import Qt, QUrl, QMimeData
|
||||||
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
|
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
|
||||||
from PyQt5.sip import wrapinstance
|
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 QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||||||
from scene import util
|
from scene import util
|
||||||
@ -30,6 +31,66 @@ class NewProjectDialog(QDialog):
|
|||||||
self.setWindowTitle("新建项目")
|
self.setWindowTitle("新建项目")
|
||||||
self.setMinimumWidth(500)
|
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)
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
@ -606,8 +667,27 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
|||||||
"""获取项目根路径下的Resources文件夹,考虑跨平台"""
|
"""获取项目根路径下的Resources文件夹,考虑跨平台"""
|
||||||
import os
|
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文件夹路径(跨平台)
|
||||||
resources_path = os.path.join(project_root, "Resources")
|
resources_path = os.path.join(project_root, "Resources")
|
||||||
@ -624,6 +704,28 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
|||||||
|
|
||||||
return resources_path
|
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):
|
def load_file_tree(self):
|
||||||
"""加载树形视图"""
|
"""加载树形视图"""
|
||||||
self.clear()
|
self.clear()
|
||||||
@ -654,7 +756,7 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
|||||||
if os.path.exists(directory) and directory not in self.watched_directories:
|
if os.path.exists(directory) and directory not in self.watched_directories:
|
||||||
if self.file_watcher.addPath(directory):
|
if self.file_watcher.addPath(directory):
|
||||||
self.watched_directories.add(directory)
|
self.watched_directories.add(directory)
|
||||||
print(f"开始监控目录:{directory}")
|
#print(f"开始监控目录:{directory}")
|
||||||
try:
|
try:
|
||||||
for item in os.listdir(directory):
|
for item in os.listdir(directory):
|
||||||
item_path = os.path.join(directory,item)
|
item_path = os.path.join(directory,item)
|
||||||
@ -1002,7 +1104,6 @@ class CustomAssetsTreeWidget(QTreeWidget):
|
|||||||
internal_paths.append(filepath)
|
internal_paths.append(filepath)
|
||||||
# 检查是否是模型文件(用于向外拖拽)
|
# 检查是否是模型文件(用于向外拖拽)
|
||||||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||||||
print(f"模型路ing!!!!!!!!!!!!!!!!!{QUrl.fromLocalFile(filepath)}")
|
|
||||||
urls.append(QUrl.fromLocalFile(filepath))
|
urls.append(QUrl.fromLocalFile(filepath))
|
||||||
|
|
||||||
# 设置内部拖拽数据
|
# 设置内部拖拽数据
|
||||||
@ -1210,14 +1311,83 @@ class CustomConsoleDockWidget(QWidget):
|
|||||||
self.autoScrollBtn = QPushButton("自动滚动")
|
self.autoScrollBtn = QPushButton("自动滚动")
|
||||||
self.autoScrollBtn.setCheckable(True)
|
self.autoScrollBtn.setCheckable(True)
|
||||||
self.autoScrollBtn.setChecked(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)
|
toolbar.addWidget(self.autoScrollBtn)
|
||||||
|
|
||||||
# 时间戳开关
|
# 时间戳开关
|
||||||
self.timestampBtn = QPushButton("显示时间")
|
self.timestampBtn = QPushButton("显示时间")
|
||||||
self.timestampBtn.setCheckable(True)
|
self.timestampBtn.setCheckable(True)
|
||||||
self.timestampBtn.setChecked(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)
|
toolbar.addWidget(self.timestampBtn)
|
||||||
|
|
||||||
|
self.fpsLabel = QLabel("FPS:0.0")
|
||||||
|
self.fpsLabel.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background-color: #2d2d44;
|
||||||
|
color: #80ff80;
|
||||||
|
border: 1px solid #3a3a4a;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.fpsLabel.setMinimumWidth(100)
|
||||||
|
self.fpsLabel.setAlignment(Qt.AlignCenter)
|
||||||
|
toolbar.addWidget(self.fpsLabel)
|
||||||
|
|
||||||
|
# 帧率更新定时器
|
||||||
|
self.fpsTimer = QTimer()
|
||||||
|
self.fpsTimer.timeout.connect(self.updateFPS)
|
||||||
|
self.fpsTimer.start(1000) # 每秒更新一次
|
||||||
|
|
||||||
toolbar.addStretch()
|
toolbar.addStretch()
|
||||||
layout.addLayout(toolbar)
|
layout.addLayout(toolbar)
|
||||||
|
|
||||||
@ -1259,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget):
|
|||||||
# 添加欢迎信息
|
# 添加欢迎信息
|
||||||
self.addMessage("🎮 编辑器控制台已启动", "INFO")
|
self.addMessage("🎮 编辑器控制台已启动", "INFO")
|
||||||
|
|
||||||
|
def updateFPS(self):
|
||||||
|
try:
|
||||||
|
if hasattr(self.world,'clock'):
|
||||||
|
fps = self.world.clock.getAverageFrameRate()
|
||||||
|
self.fpsLabel.setText(f"FPS:{fps:.1f}")
|
||||||
|
|
||||||
|
# 根据帧率设置颜色
|
||||||
|
if fps >= 50:
|
||||||
|
color = "#80ff80" # 绿色 - 优秀
|
||||||
|
elif fps >= 30:
|
||||||
|
color = "#ffff80" # 黄色 - 一般
|
||||||
|
else:
|
||||||
|
color = "#ff8080" # 红色 - 较差
|
||||||
|
|
||||||
|
self.fpsLabel.setStyleSheet(f"""
|
||||||
|
QLabel {{
|
||||||
|
background-color: #2d2d44;
|
||||||
|
color: {color};
|
||||||
|
border: 1px solid #3a3a4a;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
except Exception as e:
|
||||||
|
pass # 静默处理错误,避免影响控制台功能
|
||||||
|
|
||||||
def setupConsoleRedirect(self):
|
def setupConsoleRedirect(self):
|
||||||
"""设置控制台重定向"""
|
"""设置控制台重定向"""
|
||||||
import sys
|
import sys
|
||||||
@ -1369,42 +1567,66 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
parent = wrapinstance(0, QWidget)
|
parent = wrapinstance(0, QWidget)
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.world = world
|
self.world = world
|
||||||
|
# self.selectedItems = None
|
||||||
self.initData()
|
self.initData()
|
||||||
self.setupUI() # 初始化界面
|
self.setupUI() # 初始化界面
|
||||||
self.setupContextMenu() # 初始化右键菜单
|
self.setupContextMenu() # 初始化右键菜单
|
||||||
|
|
||||||
self.setupDragDrop() # 设置拖拽功能
|
self.setupDragDrop() # 设置拖拽功能
|
||||||
|
|
||||||
self.original_scales={}
|
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):
|
def initData(self):
|
||||||
"""初始化变量"""
|
"""初始化变量"""
|
||||||
# 定义2D GUI元素类型
|
# 定义2D GUI元素类型
|
||||||
self.gui_2d_types = {
|
self.gui_2d_types = {
|
||||||
"GUI_BUTTON", # DirectButton
|
"GUI_BUTTON", # GUI 按钮
|
||||||
"GUI_LABEL", # DirectLabel
|
"GUI_LABEL", # GUI 标签
|
||||||
"GUI_ENTRY", # DirectEntry
|
"GUI_ENTRY", # GUI 输入框
|
||||||
"GUI_IMAGE",
|
"GUI_IMAGE", # GUI 图片
|
||||||
|
"GUI_2D_VIDEO_SCREEN", # GUI 2D视频
|
||||||
|
"GUI_SPHERICAL_VIDEO", # GUI 3D球形视频
|
||||||
"GUI_NODE" # 其他2D GUI容器
|
"GUI_NODE" # 其他2D GUI容器
|
||||||
}
|
}
|
||||||
|
|
||||||
# 定义3D GUI元素类型
|
# 定义3D GUI元素类型
|
||||||
self.gui_3d_types = {
|
self.gui_3d_types = {
|
||||||
"GUI_3DTEXT", # 3D TextNode
|
"GUI_3DTEXT", # 3D 文本节点
|
||||||
"GUI_3DIMAGE",
|
"GUI_3DIMAGE", # 3D 图片节点
|
||||||
"GUI_VIRTUAL_SCREEN" # Virtual Screen
|
"GUI_VIRTUAL_SCREEN", # 3D视频
|
||||||
|
"GUI_VirtualScreen" # 3D虚拟视频
|
||||||
}
|
}
|
||||||
|
|
||||||
# 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素)
|
# 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素)
|
||||||
self.scene_3d_types = {
|
self.scene_3d_types = {
|
||||||
"SCENE_ROOT",
|
"SCENE_ROOT",
|
||||||
"SCENE_NODE",
|
"SCENE_NODE",
|
||||||
"LIGHT_NODE",
|
"LIGHT_NODE", # 灯节点
|
||||||
"CAMERA_NODE",
|
"CAMERA_NODE",
|
||||||
"IMPORTED_MODEL_NODE",
|
"IMPORTED_MODEL_NODE", # 导入模型节点
|
||||||
"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):
|
def setupUI(self):
|
||||||
"""初始化UI设置"""
|
"""初始化UI设置"""
|
||||||
self.setHeaderHidden(True)
|
self.setHeaderHidden(True)
|
||||||
@ -1427,12 +1649,9 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
|
|
||||||
def showContextMenu(self, position):
|
def showContextMenu(self, position):
|
||||||
"""显示右键菜单 - 复用主菜单的创建动作"""
|
"""显示右键菜单 - 复用主菜单的创建动作"""
|
||||||
if not self.selectedItems():
|
if self.selectedItems():
|
||||||
print("没有选中的项目,不显示右键菜单")
|
item = self.selectedItems()[0]
|
||||||
return
|
print(f"为项目 '{item.text(0)}' 显示右键菜单")
|
||||||
|
|
||||||
item = self.selectedItems()[0]
|
|
||||||
print(f"为项目 '{item.text(0)}' 显示右键菜单")
|
|
||||||
|
|
||||||
# 创建右键菜单
|
# 创建右键菜单
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
@ -1682,7 +1901,9 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
|
|
||||||
# 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下
|
# 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下
|
||||||
if is_dragged_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)}")
|
print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}")
|
||||||
return True
|
return True
|
||||||
elif is_target_3d_gui:
|
elif is_target_3d_gui:
|
||||||
@ -1701,7 +1922,7 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
elif is_dragged_3d_gui:
|
elif is_dragged_3d_gui:
|
||||||
if is_target_3d_scene:
|
if is_target_3d_scene:
|
||||||
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
|
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
|
||||||
return True
|
return False
|
||||||
elif is_target_2d_gui:
|
elif is_target_2d_gui:
|
||||||
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
|
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
|
||||||
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
|
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
|
||||||
@ -1727,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
elif is_target_3d_gui:
|
elif is_target_3d_gui:
|
||||||
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下")
|
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下")
|
||||||
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
|
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
|
||||||
return True
|
return False
|
||||||
else:
|
else:
|
||||||
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
||||||
return False
|
return False
|
||||||
@ -1773,6 +1994,20 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
|
|
||||||
def dragMoveEvent(self, event):
|
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:
|
if event.source() != self:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
return
|
return
|
||||||
@ -1811,6 +2046,8 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
selected_items = self.selectedItems()
|
selected_items = self.selectedItems()
|
||||||
if selected_items:
|
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)
|
self.delete_items(selected_items)
|
||||||
else:
|
else:
|
||||||
# 没有选中任何项目,执行默认操作
|
# 没有选中任何项目,执行默认操作
|
||||||
@ -1818,12 +2055,58 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
else:
|
else:
|
||||||
super().keyPressEvent(event)
|
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):
|
def delete_items(self, selected_items):
|
||||||
"""删除选中的item - 简化版本"""
|
"""删除选中的item - 简化版本"""
|
||||||
if not selected_items:
|
if not selected_items:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 过滤掉不能删除的节点
|
# 1. 过滤掉不能删除的节点
|
||||||
deletable_items = []
|
deletable_items = []
|
||||||
for item in selected_items:
|
for item in selected_items:
|
||||||
node_type = item.data(0, Qt.UserRole + 1)
|
node_type = item.data(0, Qt.UserRole + 1)
|
||||||
@ -1839,7 +2122,7 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
QMessageBox.information(self, "提示", "没有可删除的节点")
|
QMessageBox.information(self, "提示", "没有可删除的节点")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 确认删除
|
# 2. 确认删除
|
||||||
item_count = len(deletable_items)
|
item_count = len(deletable_items)
|
||||||
if item_count == 1:
|
if item_count == 1:
|
||||||
message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?"
|
message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?"
|
||||||
@ -1854,11 +2137,19 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 执行删除
|
# 默认选中场景根节点,通常是第一个顶级节点
|
||||||
|
#next_item_to_select = self.topLevelItem(0)
|
||||||
|
|
||||||
|
# 3. 执行删除循环
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
for item in deletable_items:
|
for item in deletable_items:
|
||||||
try:
|
try:
|
||||||
|
# 在删除前,记录其父节点,作为删除后的新选择
|
||||||
|
# 选择最后一个被删除项的父节点作为新的焦点
|
||||||
|
if item.parent():
|
||||||
|
next_item_to_select = item.parent()
|
||||||
panda_node = item.data(0, Qt.UserRole)
|
panda_node = item.data(0, Qt.UserRole)
|
||||||
|
|
||||||
if panda_node:
|
if panda_node:
|
||||||
# 清理选择状态
|
# 清理选择状态
|
||||||
if (hasattr(self.world, 'selection') and
|
if (hasattr(self.world, 'selection') and
|
||||||
@ -1874,38 +2165,32 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
if hasattr(panda_node, 'getPythonTag'):
|
if hasattr(panda_node, 'getPythonTag'):
|
||||||
light_object = panda_node.getPythonTag('rp_light_object')
|
light_object = panda_node.getPythonTag('rp_light_object')
|
||||||
if light_object and hasattr(self.world, 'render_pipeline'):
|
if light_object and hasattr(self.world, 'render_pipeline'):
|
||||||
try:
|
print(f'11111111111111111111111111,{light_object.casts_shadows}')
|
||||||
self.world.render_pipeline.remove_light(light_object)
|
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()}")
|
|
||||||
|
|
||||||
# 从world列表中移除
|
# 从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:
|
if hasattr(self.world, 'models') and panda_node in self.world.models:
|
||||||
self.world.models.remove(panda_node)
|
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)
|
for i in reversed(tilesets_to_remove):
|
||||||
|
del self.world.scene_manager.tilesets[i]
|
||||||
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()}")
|
|
||||||
|
|
||||||
# 从Panda3D场景中移除
|
# 从Panda3D场景中移除
|
||||||
try:
|
try:
|
||||||
@ -1935,7 +2220,210 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
# if hasattr(self.world, 'property_panel'):
|
# if hasattr(self.world, 'property_panel'):
|
||||||
# self.world.property_panel.clearPropertyPanel()
|
# 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):
|
def _findSceneRoot(self):
|
||||||
@ -1966,52 +2454,79 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
|
|
||||||
def add_node_to_tree_widget(self, node, parent_item, node_type):
|
def add_node_to_tree_widget(self, node, parent_item, node_type):
|
||||||
"""将node元素添加到树形控件"""
|
"""将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'}
|
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):
|
def should_skip(node):
|
||||||
name = node.getName()
|
name = node.getName()
|
||||||
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance(
|
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance(
|
||||||
node.node(), ModelRoot) or name == ""
|
node.node(), ModelRoot) or name == ""
|
||||||
|
|
||||||
def addNodeToTree(node, parentItem, force=False):
|
# 使用一个变量来确保无论哪个分支都有返回值
|
||||||
if not force and should_skip(node):
|
new_qt_item = None
|
||||||
return None
|
node_name = ""
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtWidgets import QTreeWidgetItem
|
if tree_type == "IMPORTED_MODEL_NODE":
|
||||||
from PyQt5.QtCore import Qt
|
# getTag('file') 可能是你自己设置的tag,这里假设它存在
|
||||||
|
node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName()
|
||||||
|
|
||||||
# 初始化new_qt_item变量
|
# 2. 接收 addNodeToTree 的返回值
|
||||||
new_qt_item = None
|
|
||||||
|
|
||||||
if node_type == "IMPORTED_MODEL_NODE":
|
|
||||||
node_name = node.getTag("file") if hasattr(node, 'getTag') else "model"
|
|
||||||
new_qt_item = addNodeToTree(node, parent_item, force=True)
|
new_qt_item = addNodeToTree(node, parent_item, force=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
node_name = node.getName() if hasattr(node, 'getName') else "node"
|
node_name = node.getName() if hasattr(node, 'getName') else "node"
|
||||||
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
|
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
|
||||||
new_qt_item.setData(0, Qt.UserRole, node)
|
new_qt_item.setData(0, Qt.UserRole, node)
|
||||||
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
|
new_qt_item.setData(0, Qt.UserRole + 1, tree_type)
|
||||||
|
|
||||||
# 展开父节点
|
# 确保 new_qt_item 成功创建后再继续操作
|
||||||
if hasattr(parent_item, 'setExpanded'):
|
if new_qt_item:
|
||||||
parent_item.setExpanded(True)
|
# 展开父节点
|
||||||
|
if hasattr(parent_item, 'setExpanded'):
|
||||||
|
parent_item.setExpanded(True)
|
||||||
|
|
||||||
print(f"✅ Qt树节点添加成功: {node_name}")
|
print(f"✅ Qt树节点添加成功: {node_name}")
|
||||||
return new_qt_item
|
return new_qt_item
|
||||||
|
else:
|
||||||
|
# 如果 addNodeToTree 因为 should_skip 返回了 None
|
||||||
|
print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。")
|
||||||
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
print(f"❌ 添加node到树形控件失败: {str(e)}")
|
print(f"❌ 添加node到树形控件失败: {str(e)}")
|
||||||
|
traceback.print_exc() # 打印更详细的错误堆栈,方便调试
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_selection_and_properties(self, node, qt_item):
|
def update_selection_and_properties(self, node, qt_item):
|
||||||
@ -2085,7 +2600,7 @@ class CustomTreeWidget(QTreeWidget):
|
|||||||
node_type = item.data(0, Qt.UserRole + 1)
|
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
|
return True
|
||||||
|
|
||||||
# # 模型节点也可以作为父节点
|
# # 模型节点也可以作为父节点
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user