1
0
forked from Rowland/EG

addRender #27

Merged
Hector merged 36 commits from addRender into main 2025-09-23 01:32:01 +00:00
39 changed files with 11369 additions and 2212 deletions

2
.idea/misc.xml generated
View File

@ -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>

View File

@ -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):
try:
taskMgr.step() taskMgr.step()
self.qPanda3DWidget.update() 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

View File

@ -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

View File

@ -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")

View File

@ -162,6 +162,11 @@ 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--;
// }
}
} }
} }

View File

@ -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值

View File

@ -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):

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

34
Start_Run.py Normal file
View 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()

View File

@ -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:
panel_type = self.data_sources[panel_id].get('panel_type', 'unknown')
if panel_type == '2d':
self.updatePanelContent(panel_id, content=data) 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数据的通用函数

View File

@ -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']):
"""创建鼠标射线""" """创建鼠标射线"""
# 组合掩码 # 组合掩码

View File

@ -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
View File

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

View File

@ -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():

View File

@ -8,6 +8,7 @@
- 射线检测和碰撞检测 - 射线检测和碰撞检测
""" """
from PIL.ImageChops import lighter from PIL.ImageChops import lighter
from direct.showbase.ShowBaseGlobal import globalClock
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
@ -63,9 +64,21 @@ class SelectionSystem:
"z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮
} }
#性能优化相关
self._optimized_node = False
self._last_update_time = 0
self._cached_bounds = {}
self._gizmo_update_interval = 0.1
self._selection_box_update_interval = 0.2
self._current_cursor = None self._current_cursor = None
self._default_cursor = None self._default_cursor = None
self._last_click_time = 0
self._double_click_threshold = 0.3
self._last_clicked_node = None
self._double_click_task = None
print("✓ 选择和变换系统初始化完成") print("✓ 选择和变换系统初始化完成")
# ==================== 光标设置 ==================== # ==================== 光标设置 ====================
def _setCursor(self,cursor_type): def _setCursor(self,cursor_type):
@ -112,7 +125,7 @@ class SelectionSystem:
def createSelectionBox(self, nodePath): def createSelectionBox(self, nodePath):
"""为选中的节点创建选择框""" """为选中的节点创建选择框"""
try: try:
print(f" 开始创建选择框,目标节点: {nodePath.getName()}") #print(f" 开始创建选择框,目标节点: {nodePath.getName()}")
# 如果已有选择框,先移除 # 如果已有选择框,先移除
if self.selectionBox: if self.selectionBox:
@ -127,17 +140,17 @@ class SelectionSystem:
# 创建选择框作为render的子节点但会实时跟踪目标节点 # 创建选择框作为render的子节点但会实时跟踪目标节点
self.selectionBox = self.world.render.attachNewNode("selectionBox") self.selectionBox = self.world.render.attachNewNode("selectionBox")
self.selectionBoxTarget = nodePath # 保存目标节点引用 self.selectionBoxTarget = nodePath # 保存目标节点引用
print(f" 选择框节点创建完成: {self.selectionBox}") #print(f" 选择框节点创建完成: {self.selectionBox}")
# 启动选择框更新任务 # 启动选择框更新任务
taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
print(" 选择框更新任务已启动") #print(" 选择框更新任务已启动")
# 初始更新选择框 # 初始更新选择框
print(" 开始初始化选择框几何体...") #print(" 开始初始化选择框几何体...")
self.updateSelectionBoxGeometry() #self.updateSelectionBoxGeometry()
print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") #print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框")
except Exception as e: except Exception as e:
print(f" ✗ 创建选择框失败: {str(e)}") print(f" ✗ 创建选择框失败: {str(e)}")
@ -212,7 +225,7 @@ class SelectionSystem:
return return
# 获取边界框的最小和最大点(世界坐标) # 获取边界框的最小和最大点(世界坐标)
print(f"世界边界框: min={minPoint}, max={maxPoint}") #print(f"世界边界框: min={minPoint}, max={maxPoint}")
# 创建线段对象 # 创建线段对象
lines = LineSegs() lines = LineSegs()
@ -271,14 +284,16 @@ class SelectionSystem:
traceback.print_exc() traceback.print_exc()
def updateSelectionBoxTask(self, task): def updateSelectionBoxTask(self, task):
"""选择框更新任务""" """选择框更新任务 - 平衡性能和实时性"""
try: try:
update_interval = 0.1
if not hasattr(self, '_last_selection_box_update'): if not hasattr(self, '_last_selection_box_update'):
self._last_selection_box_update = 0 self._last_selection_box_update = 0
import time import time
current_time = time.time() current_time = time.time()
if current_time - self._last_selection_box_update < 0.1: if current_time - self._last_selection_box_update < update_interval:
return task.cont return task.cont
self._last_selection_box_update = current_time self._last_selection_box_update = current_time
@ -286,36 +301,41 @@ class SelectionSystem:
self.checkAndClearIfTargetDeleted() self.checkAndClearIfTargetDeleted()
if not self.selectionBox or not self.selectionBoxTarget: if not self.selectionBox or not self.selectionBoxTarget:
return task.done # 结束任务 return task.done
# 检查目标节点是否还存在 # 检查目标节点是否还存在
if self.selectionBoxTarget.isEmpty(): if self.selectionBoxTarget.isEmpty():
self.clearSelectionBox() self.clearSelectionBox()
return task.done return task.done
# 获取目标节点在世界坐标系中的当前边界框使用正确的API # 检查目标节点是否发生了变化(位置、旋转、缩放)
currentMinPoint = Point3() current_transform = self._getNodeTransformKey(self.selectionBoxTarget)
currentMaxPoint = Point3()
if not self.selectionBoxTarget.calcTightBounds(currentMinPoint, currentMaxPoint, self.world.render):
return task.cont
# 检查边界框是否发生变化(位置或大小) if (not hasattr(self, '_last_transform_key') or
if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or self._last_transform_key != current_transform):
self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint): # 节点发生了变化,更新选择框
# 更新选择框几何体
self.updateSelectionBoxGeometry() self.updateSelectionBoxGeometry()
self._last_transform_key = current_transform
# 保存当前边界框信息 return task.cont
self._lastMinPoint = currentMinPoint
self._lastMaxPoint = currentMaxPoint
return task.cont # 继续任务
except Exception as e: except Exception as e:
print(f"选择框更新任务出错: {str(e)}") print(f"选择框更新任务出错: {str(e)}")
return task.done return task.done
def _getNodeTransformKey(self, node):
"""获取节点变换的关键信息,用于快速比较"""
try:
# 获取节点的关键变换信息
pos = node.getPos(self.world.render)
hpr = node.getHpr(self.world.render)
scale = node.getScale(self.world.render)
# 返回一个可以比较的元组
return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z)
except:
return None
def clearSelectionBox(self): def clearSelectionBox(self):
"""清除选择框""" """清除选择框"""
if self.selectionBox: if self.selectionBox:
@ -335,7 +355,7 @@ class SelectionSystem:
def createGizmo(self, nodePath): def createGizmo(self, nodePath):
"""为选中的节点创建坐标轴工具 - 保留箭头版本""" """为选中的节点创建坐标轴工具 - 保留箭头版本"""
try: try:
print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}")
# 如果已有坐标轴,先移除 # 如果已有坐标轴,先移除
if self.gizmo: if self.gizmo:
@ -350,6 +370,22 @@ class SelectionSystem:
self.gizmo = self.world.render.attachNewNode("gizmo") self.gizmo = self.world.render.attachNewNode("gizmo")
self.gizmoTarget = nodePath self.gizmoTarget = nodePath
# 添加标识标签,便于识别
self.gizmo.setTag("is_gizmo", "1")
if hasattr(nodePath, 'getName'):
self.gizmo.setTag("gizmo_target", nodePath.getName())
# 为各轴添加标签
if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis:
self.gizmoXAxis.setTag("is_gizmo", "1")
self.gizmoXAxis.setTag("gizmo_axis", "x")
if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis:
self.gizmoYAxis.setTag("is_gizmo", "1")
self.gizmoYAxis.setTag("gizmo_axis", "y")
if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis:
self.gizmoZAxis.setTag("is_gizmo", "1")
self.gizmoZAxis.setTag("gizmo_axis", "z")
# 设置位置和朝向 # 设置位置和朝向
minPoint = Point3() minPoint = Point3()
maxPoint = Point3() maxPoint = Point3()
@ -385,7 +421,7 @@ class SelectionSystem:
# 只启动一次更新任务 # 只启动一次更新任务
taskMgr.add(self.updateGizmoTask, "updateGizmo") taskMgr.add(self.updateGizmoTask, "updateGizmo")
print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") #print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴")
except Exception as e: except Exception as e:
print(f"创建坐标轴失败: {str(e)}") print(f"创建坐标轴失败: {str(e)}")
@ -431,7 +467,7 @@ class SelectionSystem:
else: else:
gizmo_model = self.world.loader.loadModel(path) gizmo_model = self.world.loader.loadModel(path)
if gizmo_model: if gizmo_model:
print(f"成功加载模型: {path}") #print(f"成功加载模型: {path}")
break break
except: except:
continue continue
@ -877,7 +913,7 @@ class SelectionSystem:
handle_node = children[0] handle_node = children[0]
if not handle_node: if not handle_node:
print(f"未找到{axis}轴的处理模型") #print(f"未找到{axis}轴的处理模型")
return return
# 创建或获取材质 # 创建或获取材质
@ -985,7 +1021,7 @@ class SelectionSystem:
handle_node = children[0] handle_node = children[0]
if not handle_node: if not handle_node:
print(f"未找到{axis}轴的处理模型") # print(f"未找到{axis}轴的处理模型")
return return
# 创建或获取材质 # 创建或获取材质
@ -999,7 +1035,7 @@ class SelectionSystem:
) )
mat.setBaseColor(adjusted_color) mat.setBaseColor(adjusted_color)
mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
# 应用材质 # 应用材质
handle_node.setMaterial(mat, 1) handle_node.setMaterial(mat, 1)
@ -1177,7 +1213,7 @@ class SelectionSystem:
return self.checkGizmoClickFallback(mouseX, mouseY) return self.checkGizmoClickFallback(mouseX, mouseY)
# 计算点击阈值 # 计算点击阈值
click_threshold = 30 # 增大检测范围 click_threshold = 15 # 增大检测范围
# 检测各个轴,对于端点在屏幕外的轴提供回退方案 # 检测各个轴,对于端点在屏幕外的轴提供回退方案
def getClickDetectionPoint(axis_name, original_screen_pos): def getClickDetectionPoint(axis_name, original_screen_pos):
@ -1206,10 +1242,10 @@ class SelectionSystem:
distance = self.distanceToLine( distance = self.distanceToLine(
(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}")
return axis_name return axis_name
return None return None
@ -1657,14 +1693,12 @@ class SelectionSystem:
if parent_node and parent_node != self.world.render: if parent_node and parent_node != self.world.render:
try: try:
if parent_node.getTransform().hasMat(): #获取变换矩阵
transform_mat = parent_node.getMat(self.world.render) transfrom_mat = parent_node.getMat(self.world.render)
if not transform_mat.isSingular(): if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat):
world_axis_vector = transform_mat.xformVec(local_axis_vector) world_axis_vector = transfrom_mat.xformVec(local_axis_vector)
else: else:
print("警告: 检测到奇异变换矩阵,使用默认轴向量") print("警告: 检测到无效变换矩阵,使用默认轴向量")
else:
print("警告: 父节点没有有效的变换矩阵,使用默认轴向量")
except Exception as e: except Exception as e:
print(f"变换计算出错: {e},使用默认轴向量") print(f"变换计算出错: {e},使用默认轴向量")
else: else:
@ -1812,6 +1846,23 @@ class SelectionSystem:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def _isMatrixValid(self, matrix):
"""检查矩阵是否有效,替代 isSingular 方法"""
try:
# 检查矩阵元素是否为有效数字
for i in range(4):
for j in range(4):
element = matrix.getCell(i, j)
# 检查是否为 NaN 或无穷大
if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf':
return False
# 检查是否过大
if abs(element) > 1e10:
return False
return True
except:
return False
def _safeUpdatePropertyPanel(self): def _safeUpdatePropertyPanel(self):
"""安全地更新属性面板""" """安全地更新属性面板"""
try: try:
@ -1871,7 +1922,9 @@ class SelectionSystem:
# ==================== 选择管理 ==================== # ==================== 选择管理 ====================
def updateSelection(self, nodePath): def updateSelection(self, nodePath):
"""更新选择状态""" try:
if self.selectedNode == nodePath:
return
print(f"\n=== 更新选择状态 ===") print(f"\n=== 更新选择状态 ===")
# 如果正在删除节点,避免更新选择 # 如果正在删除节点,避免更新选择
@ -1889,6 +1942,55 @@ class SelectionSystem:
# 添加兼容性属性 # 添加兼容性属性
self.selectedObject = nodePath self.selectedObject = nodePath
if nodePath and not nodePath.isEmpty():
node_name = nodePath.getName()
#print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
# 创建选择框
#print("创建选择框...")
self.createSelectionBox(nodePath)
if self.selectionBox:
box_name = "Unknown"
if self.selectionBox and not self.selectionBox.isEmpty():
box_name = self.selectionBox.getName()
#print(f"✓ 选择框创建成功: {box_name}")
else:
print("× 选择框创建失败")
# 创建坐标轴
#print("创建坐标轴...")
self.createGizmo(nodePath)
if self.gizmo:
gizmo_name = "Unknown"
if self.gizmo and not self.gizmo.isEmpty():
gizmo_name = self.gizmo.getName()
#print(f"✓ 坐标轴创建成功: {gizmo_name}")
else:
print("× 坐标轴创建失败")
print(f"✓ 选中了节点: {node_name}")
else:
print("清除选择...")
self.clearSelectionBox()
self.clearGizmo()
print("✓ 取消选择")
#当取消选择时,同步清空树形控件的选中状态
if (hasattr(self.world,'interface_manager')and
self.world.interface_manager and
self.world.interface_manager.treeWidget):
self.world.interface_manager.treeWidget.setCurrentItem(None)
print("✓ 树形控件选中状态已清空")
print("=== 选择状态更新完成 ===\n")
except Exception as e:
print(f"更新选择状态失败{str(e)}")
import traceback
traceback.print_exc()
def _updateSelectionVisuals(self, nodePath):
"""更新选择的视觉效果(选择框和坐标轴)"""
try:
if nodePath and not nodePath.isEmpty(): if nodePath and not nodePath.isEmpty():
node_name = nodePath.getName() node_name = nodePath.getName()
print(f"开始为节点 {node_name} 创建选择框和坐标轴...") print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
@ -1922,7 +2024,8 @@ class SelectionSystem:
self.clearGizmo() self.clearGizmo()
print("✓ 取消选择") print("✓ 取消选择")
print("=== 选择状态更新完成 ===\n") except Exception as e:
print(f"更新选择视觉效果失败: {e}")
def getSelectedNode(self): def getSelectedNode(self):
"""获取当前选中的节点""" """获取当前选中的节点"""
@ -2010,7 +2113,7 @@ class SelectionSystem:
collision_np.hide() # 隐藏碰撞体,只用于检测 collision_np.hide() # 隐藏碰撞体,只用于检测
print(f"✓ 成功创建 {axis_name} 轴碰撞体") #print(f"✓ 成功创建 {axis_name} 轴碰撞体")
except Exception as e: except Exception as e:
print(f"创建{axis_name}轴碰撞体失败: {e}") print(f"创建{axis_name}轴碰撞体失败: {e}")
@ -2099,3 +2202,582 @@ class SelectionSystem:
print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}")
else: else:
print(f"{axis_name.upper()}轴节点不存在") print(f"{axis_name.upper()}轴节点不存在")
def focusCameraOnSelectedNodeAdvanced(self):
"""高级版的摄像机聚焦功能,包含平滑动画效果"""
try:
if not self.selectedNode or self.selectedNode.isEmpty():
print("没有选中的节点,无法聚焦")
return False
# 获取选中节点的边界框
minPoint = Point3()
maxPoint = Point3()
if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render):
print("无法计算选中节点的边界框,使用节点为位置作为替代方案")
node_pos = self.selectedNode.getPos(self.world.render)
optimal_distance = 10.0
current_cam_pos = self.world.cam.getPos()
view_direction = node_pos - current_cam_pos
if view_direction.length()<0.001:
view_direction = Vec3(5,-5,2)
view_direction.normalize()
target_cam_pos = node_pos - (view_direction * optimal_distance)
temp_node =self.world.render.attachNewNode("temp_lookat_target")
temp_node.setPos(node_pos)
dummy_cam = self.world.render.attachNewNode("dummy_camera")
dummy_cam.setPos(target_cam_pos)
dummy_cam.lookAt(temp_node)
target_cam_hpr = Vec3(dummy_cam.getHpr())
temp_node.removeNode()
dummy_cam.removeNode()
currrent_cam_pos = Point3(self.world.cam.getPos())
current_cam_hpr = Vec3(self.world.cam.getHpr())
self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr)
print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}")
return True
# 计算节点中心点和大小
center = Point3(
(minPoint.x + maxPoint.x) * 0.5,
(minPoint.y + maxPoint.y) * 0.5,
(minPoint.z + maxPoint.z) * 0.5
)
# 计算节点的对角线长度
size = (maxPoint - minPoint).length()
# 如果节点太小,使用默认大小
if size < 0.01:
size = 1.0
# 获取当前摄像机位置和朝向
current_cam_pos = Point3(self.world.cam.getPos())
current_cam_hpr = Vec3(self.world.cam.getHpr())
# 计算观察方向
view_direction = current_cam_pos - center
if view_direction.length() < 0.001:
view_direction = Vec3(5, -5, 2)
view_direction.normalize()
# 计算合适的观察距离
optimal_distance = max(size * 2.0, 5.0)
# 计算目标摄像机位置
target_cam_pos = center + (view_direction * optimal_distance)
# 计算目标朝向不直接使用lookAt而是计算目标HPR
# 创建临时节点用于计算目标朝向
temp_node = self.world.render.attachNewNode("temp_lookat_target")
temp_node.setPos(center)
# 创建另一个临时节点用于计算朝向
dummy_cam = self.world.render.attachNewNode("dummy_camera")
dummy_cam.setPos(target_cam_pos)
dummy_cam.lookAt(temp_node)
target_cam_hpr = Vec3(dummy_cam.getHpr())
# 清理临时节点
temp_node.removeNode()
dummy_cam.removeNode()
# 使用任务来实现平滑移动动画
self._startCameraFocusAnimation(current_cam_pos, target_cam_pos,
current_cam_hpr, target_cam_hpr)
print(f"开始聚焦到节点: {self.selectedNode.getName()}")
return True
except Exception as e:
print(f"高级聚焦功能失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr):
"""启动摄像机聚焦动画"""
try:
# 创建动画任务
class CameraFocusData:
def __init__(self, start_pos, end_pos, start_hpr, end_hpr):
self.start_pos = Point3(start_pos) # 确保是Point3类型
self.end_pos = Point3(end_pos) # 确保是Point3类型
self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型
self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型
self.elapsed_time = 0.0
self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑
self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr)
# 移除之前的任务(如果存在)
taskMgr.remove("cameraFocusTask")
# 添加新任务
taskMgr.add(self._cameraFocusTask, "cameraFocusTask")
except Exception as e:
print(f"启动摄像机聚焦动画失败: {e}")
def _normalizeAngle(self, angle):
"""规范化角度到-180到180度之间"""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def _cameraFocusTask(self, task):
"""摄像机聚焦动画任务"""
try:
if not hasattr(self, '_camera_focus_data'):
return task.done
data = self._camera_focus_data
data.elapsed_time += globalClock.getDt()
# 计算插值因子
t = min(1.0, data.elapsed_time / data.duration)
# 使用更平滑的插值函数
smooth_t = t * t * (3 - 2 * t) # 平滑步进插值
# 手动实现lerp功能
def lerp_point3(start, end, factor):
return Point3(
start.x + (end.x - start.x) * factor,
start.y + (end.y - start.y) * factor,
start.z + (end.z - start.z) * factor
)
# 角度插值需要特殊处理,确保选择最短路径
def lerp_hpr(start, end, factor):
# 规范化角度
start_x = self._normalizeAngle(start.x)
start_y = self._normalizeAngle(start.y)
start_z = self._normalizeAngle(start.z)
end_x = self._normalizeAngle(end.x)
end_y = self._normalizeAngle(end.y)
end_z = self._normalizeAngle(end.z)
# 计算最短旋转路径
diff_x = self._normalizeAngle(end_x - start_x)
diff_y = self._normalizeAngle(end_y - start_y)
diff_z = self._normalizeAngle(end_z - start_z)
# 插值
result_x = start_x + diff_x * factor
result_y = start_y + diff_y * factor
result_z = start_z + diff_z * factor
# 再次规范化
result_x = self._normalizeAngle(result_x)
result_y = self._normalizeAngle(result_y)
result_z = self._normalizeAngle(result_z)
return Vec3(result_x, result_y, result_z)
# 计算当前位置和朝向
current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t)
current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t)
# 应用到摄像机
self.world.cam.setPos(current_pos)
self.world.cam.setHpr(current_hpr)
# 检查是否完成
if t >= 1.0:
if hasattr(self, '_camera_focus_data'):
delattr(self, '_camera_focus_data')
print("摄像机聚焦动画完成")
return task.done
return task.cont
except Exception as e:
print(f"摄像机聚焦任务出错: {e}")
import traceback
traceback.print_exc()
return task.done
def _smoothCameraMoveTask(self, task):
"""平滑摄像机移动任务"""
try:
if not hasattr(self, '_smooth_camera_move_data'):
return task.done
data = self._smooth_camera_move_data
data.elapsed_time += globalClock.getDt()
# 计算插值因子
t = min(1.0, data.elapsed_time / data.duration)
# 使用平滑插值
smooth_t = t * t * (3 - 2 * t)
# 角度插值需要特殊处理
def lerp_hpr(start, end, factor):
# 规范化角度
start_x = self._normalizeAngle(start.x)
start_y = self._normalizeAngle(start.y)
start_z = self._normalizeAngle(start.z)
end_x = self._normalizeAngle(end.x)
end_y = self._normalizeAngle(end.y)
end_z = self._normalizeAngle(end.z)
# 计算最短旋转路径
diff_x = self._normalizeAngle(end_x - start_x)
diff_y = self._normalizeAngle(end_y - start_y)
diff_z = self._normalizeAngle(end_z - start_z)
# 插值
result_x = start_x + diff_x * factor
result_y = start_y + diff_y * factor
result_z = start_z + diff_z * factor
return Vec3(result_x, result_y, result_z)
# 计算当前位置和朝向
current_pos = Point3(
data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t,
data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t,
data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t
)
current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t)
# 应用到摄像机
self.world.cam.setPos(current_pos)
self.world.cam.setHpr(current_hpr)
# 检查是否完成
if t >= 1.0:
if hasattr(self, '_smooth_camera_move_data'):
delattr(self, '_smooth_camera_move_data')
print("摄像机平滑移动完成")
return task.done
return task.cont
except Exception as e:
print(f"平滑摄像机移动任务出错: {e}")
import traceback
traceback.print_exc()
return task.done
def handleMouseClick(self, nodePath, mouseX=None, mouseY=None):
"""处理鼠标点击事件 - 支持坐标轴双击聚焦"""
try:
# 如果正在删除节点,忽略鼠标点击
if hasattr(self, '_deleting_node') and self._deleting_node:
print("正在删除节点,忽略鼠标点击")
return
import time
current_time = time.time()
# 检查是否点击了坐标轴
is_gizmo_click = False
target_node = nodePath
# 判断是否点击了坐标轴
if (nodePath and hasattr(nodePath, 'getName') and
(nodePath.getName().startswith("gizmo") or
"gizmo" in nodePath.getName().lower() or
(hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))):
is_gizmo_click = True
# 如果有选中的模型,使用选中的模型作为聚焦目标
if self.selectedNode and not self.selectedNode.isEmpty():
target_node = self.selectedNode
print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}")
# 检查是否为双击(同一节点且在时间阈值内)
is_double_click = (self._last_clicked_node == target_node and
target_node is not None and
current_time - self._last_click_time < self._double_click_threshold)
if is_double_click:
# 双击 detected
node_name = target_node.getName() if target_node else "None"
print(f"检测到双击节点: {node_name}")
# 无论是点击模型还是坐标轴,都执行聚焦
if target_node and not target_node.isEmpty():
print(f"双击聚焦到节点: {target_node.getName()}")
if self.selectedNode != target_node:
self.updateSelection(target_node)
else:
self.focusCameraOnSelectedNodeAdvanced()
else:
print("双击事件:没有有效的目标节点")
# 重置状态以避免三击等误触发
self._last_click_time = 0
self._last_clicked_node = None
else:
# 单击,更新状态
self._last_click_time = current_time
self._last_clicked_node = target_node
# 如果点击的是坐标轴,保持当前选择不变
if is_gizmo_click:
print("坐标轴单击,保持当前选择")
else:
# 正常的单击选择
self.updateSelection(nodePath)
except Exception as e:
print(f"处理鼠标点击事件失败: {e}")
def _onDoubleClick(self, nodePath):
"""双击事件处理"""
try:
# 获取实际要聚焦的目标节点
target_node = nodePath
# 如果是坐标轴,确保使用关联的模型作为目标
if (nodePath and hasattr(nodePath, 'hasTag') and
nodePath.hasTag("is_gizmo")):
if self.selectedNode and not self.selectedNode.isEmpty():
target_node = self.selectedNode
print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}")
else:
print("坐标轴双击,但没有关联的选中模型")
return
if target_node and not target_node.isEmpty():
print(f"双击聚焦到节点: {target_node.getName()}")
# 更新选择(如果需要)
if self.selectedNode != target_node:
self.updateSelection(target_node)
# 执行聚焦
self.focusCameraOnSelectedNodeAdvanced()
else:
print("双击事件:没有有效的目标节点")
except Exception as e:
print(f"双击事件处理失败: {e}")
# 添加一个更精确的双击检测方法
def checkDoubleClick(self, nodePath):
"""检查是否为双击,返回布尔值"""
try:
import time
current_time = time.time()
# 必须是同一节点且在时间阈值内
is_double_click = (self._last_clicked_node == nodePath and
nodePath is not None and
current_time - self._last_click_time < self._double_click_threshold)
if is_double_click:
# 重置状态
self._last_click_time = 0
self._last_clicked_node = None
return True
else:
# 更新状态
self._last_click_time = current_time
self._last_clicked_node = nodePath
return False
except Exception as e:
print(f"双击检测失败: {e}")
return False
# 添加一个定时重置方法,用于清除长时间未完成的双击状态
def _resetDoubleClickState(self):
"""重置双击状态"""
self._last_click_time = 0
self._last_clicked_node = None
# 添加一个任务来自动重置双击状态
def _startDoubleClickResetTask(self):
"""启动双击状态重置任务"""
if self._double_click_task:
taskMgr.remove(self._double_click_task)
self._double_click_task = taskMgr.doMethodLater(
self._double_click_threshold * 2, # 等待2倍阈值时间
self._resetDoubleClickStateTask,
"resetDoubleClickState"
)
def _resetDoubleClickStateTask(self, task):
"""任务:重置双击状态"""
self._resetDoubleClickState()
self._double_click_task = None
return task.done
# 修改 updateSelection 方法以集成双击检测
# def updateSelection(self, nodePath):
# """更新选择状态"""
# print(f"\n=== 更新选择状态 ===")
#
# # 如果正在删除节点,避免更新选择
# if hasattr(self, '_deleting_node') and self._deleting_node:
# print("正在删除节点,跳过选择更新")
# print("=== 选择状态更新完成 ===\n")
# return
#
# node_name = "None"
# if nodePath and not nodePath.isEmpty():
# node_name = nodePath.getName()
# print(f"新选择的节点: {node_name}")
#
# # 检查是否为双击
# is_double_click = self.checkDoubleClick(nodePath)
# if is_double_click:
# print(f"检测到双击 {node_name},执行聚焦")
# # 启动聚焦(在下一帧执行,确保选择状态已更新)
# taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus")
#
# self.selectedNode = nodePath
# # 添加兼容性属性
# self.selectedObject = nodePath
#
# if nodePath and not nodePath.isEmpty():
# node_name = nodePath.getName()
# print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
#
# # 创建选择框
# print("创建选择框...")
# self.createSelectionBox(nodePath)
# if self.selectionBox:
# box_name = "Unknown"
# if self.selectionBox and not self.selectionBox.isEmpty():
# box_name = self.selectionBox.getName()
# print(f"✓ 选择框创建成功: {box_name}")
# else:
# print("× 选择框创建失败")
#
# # 创建坐标轴
# print("创建坐标轴...")
# self.createGizmo(nodePath)
# if self.gizmo:
# gizmo_name = "Unknown"
# if self.gizmo and not self.gizmo.isEmpty():
# gizmo_name = self.gizmo.getName()
# print(f"✓ 坐标轴创建成功: {gizmo_name}")
# else:
# print("× 坐标轴创建失败")
#
# print(f"✓ 选中了节点: {node_name}")
# else:
# print("清除选择...")
# self.clearSelectionBox()
# self.clearGizmo()
# print("✓ 取消选择")
#
# print("=== 选择状态更新完成 ===\n")
def _delayedFocusTask(self, task):
"""延迟执行聚焦任务"""
try:
self.focusCameraOnSelectedNodeAdvanced()
except Exception as e:
print(f"延迟聚焦任务失败: {e}")
return task.done
# 添加一个更智能的双击检测方法,考虑鼠标位置
def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None):
"""检查是否为双击,同时考虑鼠标位置"""
try:
import time
current_time = time.time()
# 如果没有提供鼠标位置,直接使用基本双击检测
if mouse_x is None or mouse_y is None:
return self.checkDoubleClick(nodePath)
# 检查节点和时间
time_diff = current_time - self._last_click_time
is_same_node = (self._last_clicked_node == nodePath)
# 如果是同一节点且在时间阈值内,认为是双击
if is_same_node and time_diff < self._double_click_threshold:
# 重置状态
self._last_click_time = 0
self._last_clicked_node = None
return True
else:
# 更新状态
self._last_click_time = current_time
self._last_clicked_node = nodePath
return False
except Exception as e:
print(f"带位置的双击检测失败: {e}")
return False
# 添加一个公共方法来设置双击阈值
def setDoubleClickThreshold(self, threshold_seconds):
"""设置双击时间阈值"""
if threshold_seconds > 0:
self._double_click_threshold = threshold_seconds
print(f"双击阈值已设置为: {threshold_seconds}")
else:
print("无效的双击阈值")
# 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式)
def triggerDoubleClickFocus(self, nodePath=None):
"""手动触发双击聚焦"""
try:
target_node = nodePath if nodePath else self.selectedNode
if target_node and not target_node.isEmpty():
print(f"手动触发聚焦到节点: {target_node.getName()}")
if self.selectedNode != target_node:
self.updateSelection(target_node)
self.focusCameraOnSelectedNodeAdvanced()
return True
else:
print("没有有效的目标节点进行聚焦")
return False
except Exception as e:
print(f"手动触发聚焦失败: {e}")
return False
def cleanup(self):
"""清理选择系统资源"""
# 清理双击任务
if self._double_click_task:
taskMgr.remove(self._double_click_task)
self._double_click_task = None
# 清理其他资源
self.clearSelectionBox()
self.clearGizmo()
def clearSelection(self):
"""清除当前选择"""
try:
self.selectedNode = None
self.selectedObject = None
self.clearSelectionBox()
self.clearGizmo()
# 清除树形控件中的选择
if (hasattr(self.world, 'interface_manager') and
self.world.interface_manager and
hasattr(self.world.interface_manager, 'treeWidget') and
self.world.interface_manager.treeWidget):
self.world.interface_manager.treeWidget.setCurrentItem(None)
print("已清除选择")
except Exception as e:
print(f"清除选择失败: {e}")

View File

@ -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,6 +35,24 @@ class TerrainManager:
return None return None
try: try:
print(f"🔆 开始创建高度图地形")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_terrains = []
try:
parent_item, parent_node = target_parents[0]
# 创建GeoMipTerrain对象 # 创建GeoMipTerrain对象
terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain = GeoMipTerrain(terrain_name) terrain = GeoMipTerrain(terrain_name)
@ -82,6 +104,9 @@ class TerrainManager:
print("错误:无法生成有效的地形节点") print("错误:无法生成有效的地形节点")
return None 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)}" node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}"
terrain_node.setName(node_name) terrain_node.setName(node_name)
@ -92,7 +117,7 @@ class TerrainManager:
terrain_node.setScale(scale[0], scale[1], scale[2]) terrain_node.setScale(scale[0], scale[1], scale[2])
# 将地形添加到场景中 # 将地形添加到场景中
terrain_node.reparentTo(self.world.render) terrain_node.reparentTo(parent_node)
from panda3d.core import BitMask32 from panda3d.core import BitMask32
# 设置地形节点的碰撞掩码 # 设置地形节点的碰撞掩码
@ -118,19 +143,43 @@ class TerrainManager:
self.terrains.append(terrain_info) self.terrains.append(terrain_info)
# 更新场景树(再次检查节点是否有效) print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}")
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
print(f"✓ 成功从 {heightmap_path} 创建地形") # 在Qt树形控件中添加对应节点
return terrain_info 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: except Exception as e:
print(f"创建地形时出错: {e}") print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}")
return None
# 处理创建结果
if not created_terrains:
print("❌ 没有成功创建任何高度图地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形")
# 返回值处理
if len(created_terrains) == 1:
return created_terrains[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_terrains] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建高度图地形过程失败: {str(e)}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return None return None
@ -138,6 +187,24 @@ class TerrainManager:
def createFlatTerrain(self, size=(0.3, 0.3), resolution=129): def createFlatTerrain(self, size=(0.3, 0.3), resolution=129):
"""创建平面地形""" """创建平面地形"""
try: try:
print(f"🔆 开始创建平面地形")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
created_terrains = []
try:
parent_item, parent_node = target_parents[0]
# 确保分辨率是2的幂次方加1 (如129, 257, 513等) # 确保分辨率是2的幂次方加1 (如129, 257, 513等)
valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] valid_resolutions = [17, 33, 65, 129, 257, 513, 1025]
if resolution not in valid_resolutions: if resolution not in valid_resolutions:
@ -171,6 +238,9 @@ class TerrainManager:
print("错误:无法生成有效的平面地形节点") print("错误:无法生成有效的平面地形节点")
return None 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}" node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain_node.setName(node_name) terrain_node.setName(node_name)
@ -178,7 +248,7 @@ class TerrainManager:
terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0)
# 将地形添加到场景中 # 将地形添加到场景中
terrain_node.reparentTo(self.world.render) terrain_node.reparentTo(parent_node)
# 为地形添加碰撞体 # 为地形添加碰撞体
from panda3d.core import BitMask32 from panda3d.core import BitMask32
@ -203,19 +273,43 @@ class TerrainManager:
self.terrains.append(terrain_info) self.terrains.append(terrain_info)
# 更新场景树(再次检查节点是否有效) print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}")
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}") # 在Qt树形控件中添加对应节点
return terrain_info 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: except Exception as e:
print(f"创建平面地形时出错: {e}") print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}")
return None
# 处理创建结果
if not created_terrains:
print("❌ 没有成功创建任何平面地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形")
# 返回值处理
if len(created_terrains) == 1:
return created_terrains[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_terrains] # 多个光源返回列表
except Exception as e:
print(f"❌ 创建平面地形过程失败: {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

View File

@ -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):
"""设置当前工具""" """设置当前工具"""

View File

@ -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未初始化地板将使用基础渲染")

View File

@ -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}")

View File

@ -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,6 +540,10 @@ 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())
if parent_gui_node:
image_node = parent_gui_node.attachNewNode(cm.generate())
else:
image_node = self.world.aspect2d.attachNewNode(cm.generate()) 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)
@ -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)
@ -1464,13 +1506,12 @@ class GUIManager:
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

127
main.py
View File

@ -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()

View File

@ -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,16 +136,13 @@ 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, "警告", "没有找到场景文件!")
return False
# 加载场景 # 加载场景
if self.world.scene_manager.loadScene(scene_file): 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) 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: with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4) json.dump(project_config, f, ensure_ascii=False, indent=4)
@ -165,14 +163,117 @@ class ProjectManager:
QMessageBox.information(parent_window, "成功", "项目加载成功!") QMessageBox.information(parent_window, "成功", "项目加载成功!")
return True return True
else: # 检查场景文件
QMessageBox.warning(parent_window, "错误", "加载场景失败!") # scene_file = os.path.join(project_path, "scenes", "scene.bam")
return False # 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: except Exception as e:
QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}") QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}")
return False return False
def openProjectForPath(self, project_path, parent_window=None):
"""通过路径打开项目
Args:
project_path: 项目路径
parent_window: 父窗口对象可选
"""
try:
if not project_path:
return False
# 检查是否是有效的项目文件夹
config_file = os.path.join(project_path, "project.json")
if not os.path.exists(config_file):
if parent_window:
QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}")
else:
print("警告: 选择的不是有效的项目文件夹!")
return False
# 读取项目配置
with open(config_file, "r", encoding="utf-8") as f:
project_config = json.load(f)
# 检查场景文件
scene_file = os.path.join(project_path, "scenes", "scene.bam")
if os.path.exists(scene_file):
# 加载场景
if self.world.scene_manager.loadScene(scene_file):
# 更新项目配置
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
# 更新项目状态
self.current_project_path = project_path
self.project_config = project_config
# 如果有父窗口更新相关UI元素
if parent_window:
# 保存当前项目路径到主窗口
parent_window.current_project_path = project_path
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
# 更新文件浏览器
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
QMessageBox.information(parent_window, "成功", "项目加载成功!")
print(f"项目 '{project_path}' 加载成功!")
return True
else:
if parent_window:
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
else:
print("错误: 加载场景失败!")
return False
except Exception as e:
error_msg = f"加载项目时发生错误:{str(e)}"
if parent_window:
QMessageBox.critical(parent_window, "错误", error_msg)
else:
print(error_msg)
return False
def saveProject(self, parent_window): def saveProject(self, parent_window):
"""保存项目""" """保存项目"""
try: try:
@ -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

View File

@ -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):

View File

@ -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
View 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
View File

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

405
ui/icon_manager_gui.py Normal file
View File

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

View File

@ -25,10 +25,39 @@ class InterfaceManager:
# 更新场景树 # 更新场景树
self.world.scene_manager.updateSceneTree() 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()
print("点击了无数据项,清除选中状态")
# def showTreeContextMenu(self, position):
# """显示树形控件的右键菜单"""
# item = self.treeWidget.itemAt(position)
# if not item:
# return
#
# # 获取节点对象
# nodePath = item.data(0, Qt.UserRole)
# if not nodePath:
# return
#
# # 创建菜单
# menu = QMenu()
#
# # 检查是否是GUI元素
# if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"):
# # GUI元素菜单
# editAction = menu.addAction("编辑")
# editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath))
#
# deleteAction = menu.addAction("删除GUI元素")
# deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath))
#
# duplicateAction = menu.addAction("复制")
# duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath))
#
# elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset":
# deleteAction = menu.addAction("删除 Cesium Tileset")
# deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item))
#
# else:
# # 为模型节点或其子节点添加删除选项
# parentItem = item.parent()
# if parentItem:
# if self.isModelOrChild(item):
# deleteAction = menu.addAction("删除")
# deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
# else:
# deleteAction = menu.addAction("删除")
# deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
#
# # 显示菜单
# menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
#
# def deleteCesiumTileset(self, nodePath, item):
# """删除 Cesium tileset"""
# try:
# # 从场景中移除
# nodePath.removeNode()
#
# # 从 tilesets 列表中移除
# if hasattr(self.world, 'scene_manager'):
# tilesets_to_remove = []
# for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
# if tileset_info['node'] == nodePath:
# tilesets_to_remove.append(i)
#
# # 从后往前删除,避免索引问题
# for i in reversed(tilesets_to_remove):
# del self.world.scene_manager.tilesets[i]
#
# # 从树形控件中移除
# parentItem = item.parent()
# if parentItem:
# parentItem.removeChild(item)
#
# print(f"成功删除 Cesium tileset: {nodePath.getName()}")
#
# # 清空属性面板和选择框
# self.world.property_panel.clearPropertyPanel() # self.world.property_panel.clearPropertyPanel()
# self.world.selection.updateSelection(None)
def showTreeContextMenu(self, position): #
"""显示树形控件的右键菜单""" # # 更新场景树
item = self.treeWidget.itemAt(position) # self.updateSceneTree()
if not item: #
return # except Exception as e:
# print(f"删除 Cesium tileset 失败: {str(e)}")
# 获取节点对象
nodePath = item.data(0, Qt.UserRole)
if not nodePath:
return
# 创建菜单
menu = QMenu()
# 检查是否是GUI元素
if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"):
# GUI元素菜单
editAction = menu.addAction("编辑")
editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath))
deleteAction = menu.addAction("删除GUI元素")
deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath))
duplicateAction = menu.addAction("复制")
duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath))
elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset":
deleteAction = menu.addAction("删除 Cesium Tileset")
deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item))
else:
#灯光节点添加特殊处理
if self.isLightNode(nodePath):
deleteAction = menu.addAction("删除灯光")
deleteAction.triggered.connect(lambda:self.deleteLightNode(nodePath,item))
else:
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
# 显示菜单
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
def isLightNode(self, nodePath):
try:
if not nodePath or nodePath.isEmpty():
return False
# 修复:统一使用 rp_light_object
if hasattr(nodePath, 'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object is not None:
return True
if hasattr(nodePath, 'getTag'):
light_type = nodePath.getTag('light_type')
if light_type in ["spot_light", "point_light"]:
return True
if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight:
return True
if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight:
return True
return False
except Exception as e:
print(f"判断节点是否是灯光节点失败: {str(e)}")
return False
def deleteLightNode(self, nodePath, item):
"""专门处理灯光节点的删除"""
try:
print(f"开始删除灯光节点: {nodePath.getName()}")
# 从RenderPipeline中移除灯光如果存在
if hasattr(nodePath, 'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
print("从RenderPipeline移除灯光")
self.world.render_pipeline.remove_light(light_object)
nodePath.clearPythonTag('rp_light_object')
if hasattr(self.world,'Spotlight') and nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
print("从Spotlight列表中删除")
if hasattr(self.world,'Pointlight') and nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
print("从Pointlight列表中移除")
if hasattr(self.world,'selection'):
if self.world.selection.selectedNode == nodePath:
self.world.selection.clearSelectionBox()
self.world.selection.clearGizmo()
self.world.selection.selectedNode = None
self.world.selection.selectedObject = None
print(f"移除节点{nodePath.getName()}")
nodePath.removeNode()
parentItem = item.parent()
if parentItem:
parentItem.removeChild(item)
print(f"成功删除灯光节点{nodePath.getName()}")
if hasattr(self.world,'property_panel'):
self.world.property_panel.clearPropertyPanel()
if hasattr(self.world,'selection'):
self.world.selection.updateSelection(None)
except Exception as e:
print(f"删除灯光节点失败: {str(e)}")
def _recursiveRemoveLights(self, nodePath):
"""递归删除节点及其子节点中的所有灯光"""
if nodePath.isEmpty():
return
# 先递归处理所有子节点
for child in nodePath.getChildren():
self._recursiveRemoveLights(child)
# 然后处理当前节点
if self.isLightNode(nodePath):
print(f"删除子灯光节点: {nodePath.getName()}")
# 从RenderPipeline中移除灯光
if hasattr(nodePath, 'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
nodePath.clearPythonTag('rp_light_object')
# 从灯光列表中移除
if hasattr(self.world, 'Spotlight') and nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
if hasattr(self.world, 'Pointlight') and nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
def deleteCesiumTileset(self, nodePath, item):
"""删除 Cesium tileset"""
try:
# 从场景中移除
nodePath.removeNode()
# 从 tilesets 列表中移除
if hasattr(self.world, 'scene_manager'):
tilesets_to_remove = []
for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
if tileset_info['node'] == nodePath:
tilesets_to_remove.append(i)
# 从后往前删除,避免索引问题
for i in reversed(tilesets_to_remove):
del self.world.scene_manager.tilesets[i]
# 从树形控件中移除
parentItem = item.parent()
if parentItem:
parentItem.removeChild(item)
print(f"成功删除 Cesium tileset: {nodePath.getName()}")
# 清空属性面板和选择框
self.world.property_panel.clearPropertyPanel()
self.world.selection.updateSelection(None)
# 更新场景树
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):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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
# 获取项目根路径 # 获取当前文件所在目录,然后向上查找项目根目录
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() 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,10 +1649,7 @@ class CustomTreeWidget(QTreeWidget):
def showContextMenu(self, position): def showContextMenu(self, position):
"""显示右键菜单 - 复用主菜单的创建动作""" """显示右键菜单 - 复用主菜单的创建动作"""
if not self.selectedItems(): if self.selectedItems():
print("没有选中的项目,不显示右键菜单")
return
item = self.selectedItems()[0] item = self.selectedItems()[0]
print(f"为项目 '{item.text(0)}' 显示右键菜单") print(f"为项目 '{item.text(0)}' 显示右键菜单")
@ -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):
"""删除指定节点 panda3Dnode- 优化和修复版本"""
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 new_qt_item:
# 展开父节点 # 展开父节点
if hasattr(parent_item, 'setExpanded'): if hasattr(parent_item, 'setExpanded'):
parent_item.setExpanded(True) 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
# # 模型节点也可以作为父节点 # # 模型节点也可以作为父节点