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">
<option name="sdkName" value="Python 3.12 (PythonProject)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (EG)" project-jdk-type="Python SDK" />
</project>

View File

@ -32,9 +32,24 @@ class QPanda3DSynchronizer(QTimer):
self.setInterval(int(dt))
self.timeout.connect(self.tick)
# def tick(self):
# taskMgr.step()
# self.qPanda3DWidget.update()
def tick(self):
taskMgr.step()
self.qPanda3DWidget.update()
try:
taskMgr.step()
self.qPanda3DWidget.update()
except AssertionError as e:
if "has_mat()" in str(e):
print("⚠️ 检测到变换矩阵错误,跳过此帧")
# 继续运行而不是崩溃
else:
raise
except Exception as e:
print(f"❌ 渲染循环错误: {e}")
import traceback
traceback.print_exc()
def get_panda_key_modifiers(evt):
@ -160,8 +175,8 @@ class QPanda3DWidget(QWidget):
def resizeEvent(self, evt):
width = evt.size().width()
height = evt.size().height()
print(f"width:{width}")
print(f"height:{height}")
#print(f"width:{width}")
#print(f"height:{height}")
from Panda3DWorld import resize_buffer
#resize_buffer(width, height)

File diff suppressed because one or more lines are too long

View File

@ -90,6 +90,10 @@ class LightManager(RPObject):
def remove_light(self, light):
""" Removes a light """
print(f'333333333333333333333333333333,{light.casts_shadows}')
# from RenderPipelineFile.rpcore.pynative.internal_light_manager import InternalLightManager
# inter = InternalLightManager()
# inter.remove_light(light)
self.internal_mgr.remove_light(light)
self.pta_max_light_index[0] = self.internal_mgr.max_light_index

View File

@ -80,6 +80,7 @@ if NATIVE_CXX_LOADED:
RPObject.global_debug("CORE", "Using native core module")
from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position
else:
print(f'343434343434343434343434343')
from rpcore import pynative as _native_module # pylint: disable=wrong-import-position
RPObject.global_debug("CORE", "Using simulated python-wrapper module")

View File

@ -161,7 +161,12 @@ public:
// Update maximum index
if (slot == _max_index) {
while (_max_index >= 0 && !_data[_max_index--]);
while (_max_index >= 0 && !_data[_max_index--]);
// 正确的修复代码
// while (_max_index >= 0 && _data[_max_index] == NULL) {
// _max_index--;
// }
}
}
}

View File

@ -116,7 +116,25 @@ class InternalLightManager(object):
source.set_slot(slot)
def remove_light(self, light):
print("111111111111111111111111111111111111111111111111")
print(f'44444444444444444444444444')
print("\n" + "=" * 50)
print(f"DEBUG: Entering remove_light for light object: {light.casts_shadows}")
if light:
print(f" - Light's Slot: {light.get_slot() if light.has_slot() else 'No Slot'}")
print(f" - Does it cast shadows? light.get_casts_shadows() -> {light.get_casts_shadows()}")
else:
print(" - Light object is None!")
print("\n --- State of Light System BEFORE removal ---")
if hasattr(self, '_lights') and hasattr(self._lights, '_data'):
# 打印出当前所有灯光对象,看看有没有异常
print(f" - Light Data Array (_data):")
for i, l_obj in enumerate(self._lights._data):
print(f" [{i}]: {l_obj}")
print(f"\n - Max Index (_max_index): {self._lights._max_index}")
print(f" - Num Entries (_num_entries): {self._lights._num_entries}")
print("=" * 50 + "\n")
assert light is not None
if not light.has_slot():
print("ERROR: Could not detach light, light was not attached!")
@ -133,7 +151,10 @@ class InternalLightManager(object):
# 关键修复先保存第一个source的slot再清理
first_source = light.get_shadow_source(0)
first_source_slot = first_source.get_slot() # 保存slot值
# --- 在这里加上打印语句 ---
print(f"DEBUG: Removing shadow sources. Start Slot: {first_source_slot}, Count: {num_sources}")
# 先发送GPU移除命令在清理之前
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
cmd_remove.push_int(first_source_slot) # 使用保存的slot值

View File

@ -208,6 +208,7 @@ class RenderPipeline(RPObject):
def remove_light(self, light):
""" Removes a previously attached light, check out the LightManager
remove_light documentation for further information. """
print(f'222222222222222222222222222,{light.casts_shadows}')
self.light_mgr.remove_light(light)
def load_ies_profile(self, filename):

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
from xml.sax.handler import property_encoding
from PyQt5.QtCore import Qt
from direct.gui.DirectGui import DirectFrame, DirectLabel
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import TextNode, Vec4, NodePath
@ -144,7 +145,7 @@ class InfoPanelManager(DirectObject):
text_scale=0.045,
text_fg=content_color,
text_align=TextNode.ALeft,
text_wordwrap=500, # 设置一个非常大的值,几乎不自动换行
text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行
pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05),
parent=panel_node,
relief=None,
@ -182,12 +183,60 @@ class InfoPanelManager(DirectObject):
panel_node.setTag("gui_type", "info_panel")
panel_node.setTag("panel_id", panel_id)
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
panel_node.setTag("is_gui_element",'1')
panel_node.setTag("tree_item_type","INFO_PANEL")
panel_node.setTag("supports_3d_position_editing","1")
# 如果有背景图片,保存背景图片路径
if bg_image:
panel_node.setTag("bg_image_path", bg_image)
if not visible:
panel_node.hide()
# 将面板添加到场景树
#self._addPanelToSceneTree(panel_node, panel_id)
return panel_node
def _addPanelToSceneTree(self, panel_node, panel_id):
"""
将信息面板添加到场景树中
"""
try:
# 获取树形控件
if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
# 查找根节点项
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
root_item = item
break
if root_item:
# 使用现有的 add_node_to_tree_widget 方法添加节点
qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL")
if qt_item:
print(f"✅ 信息面板 {panel_id} 已添加到场景树")
# 选中创建的节点
tree_widget.setCurrentItem(qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(panel_node, qt_item)
else:
print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败")
else:
print("⚠️ 未找到场景树根节点,无法添加信息面板")
else:
print("⚠️ 无法访问场景树控件,信息面板未添加到场景树")
except Exception as e:
print(f"❌ 添加信息面板到场景树时出错: {e}")
import traceback
traceback.print_exc()
def setPanelBackgroundImage(self, panel_id, image_path):
"""
为指定面板设置背景图片
@ -317,11 +366,10 @@ class InfoPanelManager(DirectObject):
return True
# 更新 registerDataSource 方法以更好地处理不同类型面板
def registerDataSource(self, panel_id, data_callback, update_interval=1.0):
"""
注册数据源定期更新面板内容
data_callback: 返回数据的回调函数
update_interval: 更新间隔
注册数据源定期更新面板内容 - 改进版
"""
if panel_id not in self.panels:
print(f"面板 {panel_id} 不存在")
@ -335,7 +383,8 @@ class InfoPanelManager(DirectObject):
data_source = {
'callback': data_callback,
'interval': update_interval,
'stop': False
'stop': False,
'panel_type': '3d' if self._is3DPanel(panel_id) else '2d' if self._is2DPanel(panel_id) else 'unknown'
}
self.data_sources[panel_id] = data_source
@ -346,9 +395,10 @@ class InfoPanelManager(DirectObject):
return True
# 在 InfoPanelManager 类中修复 _updateDataThread 方法
def _updateDataThread(self, panel_id):
"""
数据更新线程
数据更新线程 - 最终修复版
"""
while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']:
try:
@ -357,7 +407,18 @@ class InfoPanelManager(DirectObject):
# 更新面板内容
if data and panel_id in self.panels:
self.updatePanelContent(panel_id, content=data)
panel_type = self.data_sources[panel_id].get('panel_type', 'unknown')
if panel_type == '2d':
self.updatePanelContent(panel_id, content=data)
elif panel_type == '3d':
self.update3DPanelContent(panel_id, content=data)
else:
# 尝试自动检测
if self._is2DPanel(panel_id):
self.updatePanelContent(panel_id, content=data)
elif self._is3DPanel(panel_id):
self.update3DPanelContent(panel_id, content=data)
# 等待下次更新
interval = self.data_sources[panel_id]['interval']
@ -365,8 +426,29 @@ class InfoPanelManager(DirectObject):
except Exception as e:
print(f"更新面板 {panel_id} 数据时出错: {e}")
import traceback
traceback.print_exc()
time.sleep(1.0) # 出错时等待1秒再重试
# 在 InfoPanelManager 类中添加以下方法
def _is3DPanel(self, panel_id):
"""
判断面板是否为3D面板
"""
if panel_id not in self.panels:
return False
panel_data = self.panels[panel_id]
return 'content_node' in panel_data and 'content_label' not in panel_data
def _is2DPanel(self, panel_id):
"""
判断面板是否为2D面板
"""
if panel_id not in self.panels:
return False
panel_data = self.panels[panel_id]
return 'content_label' in panel_data and 'content_node' not in panel_data
def unregisterDataSource(self, panel_id):
"""
注销数据源
@ -447,9 +529,7 @@ class InfoPanelManager(DirectObject):
-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05
)
# 设置一个非常大的换行值,几乎不自动换行
panel_data['content_label']['text_wordwrap'] = 500
print(f"更新面板换行: 设置为500几乎不换行")
panel_data['content_label']['text_wordwrap'] = 0
# 如果有背景图片,也需要更新其大小
if 'bg_image' in panel_data and panel_data['bg_image']:
@ -493,8 +573,9 @@ class InfoPanelManager(DirectObject):
if 'content_size' in properties:
panel_data['content_label']['text_scale'] = properties['content_size']
props['content_size'] = properties['content_size']
current_size = props.get('size',(1.0,0.6))
# 当字体大小改变时,仍然保持较大的换行值
panel_data['content_label']['text_wordwrap'] = 500
panel_data['content_label']['text_wordwrap'] = 0
# 更新背景图片
if 'bg_image' in properties:
@ -657,10 +738,247 @@ class InfoPanelManager(DirectObject):
return True
def create3DInfoPanel(self, panel_id, position=(0, 0, 0), size=(1.0, 0.6),
bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0),
title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0),
visible=True, font=None, bg_image=None):
"""
创建简化版3D信息面板 - 只显示文字无面板背景避免闪烁
"""
# 如果面板已存在,先移除它
if panel_id in self.panels:
self.removePanel(panel_id)
# 确保父节点存在
parent_node = self.parent if self.parent else self.world.render
# 根据面板ID确定标题和内容
title, content = self._getPanelContent(panel_id)
# 创建主节点,便于统一管理
panel_node = parent_node.attachNewNode(f"info_panel_3d_{panel_id}")
panel_node.setPos(position[0], position[1], position[2])
# 直接创建文字节点,不创建面板背景和边框
from panda3d.core import TextNode
# 创建标题文本
title_text_node = TextNode(f'title_{panel_id}')
title_text_node.setText(title)
title_text_node.setTextColor(*title_color)
title_text_node.setAlign(TextNode.ACenter)
if font:
title_text_node.setFont(font)
title_text = panel_node.attachNewNode(title_text_node)
title_text.setScale(0.06)
title_text.setPos(0, 0, size[1] / 4) # 将标题放在上方
# 创建内容文本
content_text_node = TextNode(f'content_{panel_id}')
content_text_node.setText(content)
content_text_node.setTextColor(*content_color)
content_text_node.setAlign(TextNode.ALeft)
content_text_node.setWordwrap(size[0] * 2) # 根据面板宽度设置换行
if font:
content_text_node.setFont(font)
content_text = panel_node.attachNewNode(content_text_node)
content_text.setScale(0.045)
content_text.setPos(-size[0] / 2, 0, size[1] / 4 - 0.1) # 将内容放在标题下方
# 保存引用
self.panels[panel_id] = {
'node': panel_node,
'title_text': title_text,
'content_text': content_text,
'title_node': title_text_node,
'content_node': content_text_node,
'properties': {
'size': size,
'position': position,
'title_color': title_color,
'content_color': content_color,
'font': font
}
}
# 设置GUI类型标记和支持3D编辑的标记
panel_node.setTag("gui_type", "info_panel_3d")
panel_node.setTag("panel_id", panel_id)
panel_node.setTag("is_gui_element", "1") # 添加此标记确保节点被识别为GUI元素
panel_node.setTag("is_scene_element", "1") # 添加此标记确保节点被识别为场景元素
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
panel_node.setTag("tree_item_type", "INFO_PANEL_3D") # 添加树节点类型标记
# 如果有背景图片,保存背景图片路径
if bg_image:
panel_node.setTag("bg_image_path", bg_image)
if not visible:
panel_node.hide()
# 将面板添加到场景树
#self._addPanelToSceneTree(panel_node, panel_id)
return panel_node
def update3DPanelContent(self, panel_id, title=None, content=None):
"""
更新3D面板内容
"""
if panel_id not in self.panels:
print(f"面板 {panel_id} 不存在")
return False
panel_data = self.panels[panel_id]
if title is not None and 'title_node' in panel_data:
panel_data['title_node'].setText(title)
if content is not None and 'content_node' in panel_data:
panel_data['content_node'].setText(content)
return True
def update3DPanelProperties(self, panel_id, **properties):
"""
更新3D面板属性
"""
if panel_id not in self.panels:
print(f"面板 {panel_id} 不存在")
return False
panel_data = self.panels[panel_id]
props = panel_data['properties']
# 更新位置
if 'position' in properties:
pos = properties['position']
panel_data['node'].setPos(pos[0], pos[1], pos[2])
props['position'] = pos
# 更新大小
if 'size' in properties:
size = properties['size']
props['size'] = size
# 由于3D面板使用CardMaker创建需要重新创建几何体
print("注意3D面板大小调整需要重新创建面板几何体")
# 更新背景颜色
if 'bg_color' in properties:
bg_color = properties['bg_color']
if 'panel_bg' in panel_data:
from panda3d.core import Material
material = Material()
material.setDiffuse(Vec4(*bg_color))
material.setAmbient(Vec4(*bg_color[:3], 1.0))
panel_data['panel_bg'].setMaterial(material, 1)
props['bg_color'] = bg_color
# 更新边框颜色
if 'border_color' in properties:
border_color = properties['border_color']
from panda3d.core import Material
border_mat = Material()
border_mat.setDiffuse(Vec4(*border_color))
for border in panel_data['borders'].values():
border.setMaterial(border_mat, 1)
props['border_color'] = border_color
# 更新标题颜色
if 'title_color' in properties:
title_color = properties['title_color']
if 'title_node' in panel_data:
panel_data['title_node'].setTextColor(*title_color)
props['title_color'] = title_color
# 更新内容颜色
if 'content_color' in properties:
content_color = properties['content_color']
if 'content_node' in panel_data:
panel_data['content_node'].setTextColor(*content_color)
props['content_color'] = content_color
# 更新标题
if 'title' in properties:
if 'title_node' in panel_data:
panel_data['title_node'].setText(properties['title'])
# 更新内容
if 'content' in properties:
if 'content_node' in panel_data:
panel_data['content_node'].setText(properties['content'])
# 更新字体大小
if 'title_size' in properties:
if 'title_text' in panel_data:
panel_data['title_text'].setScale(properties['title_size'])
props['title_size'] = properties['title_size']
if 'content_size' in properties:
if 'content_text' in panel_data:
panel_data['content_text'].setScale(properties['content_size'])
props['content_size'] = properties['content_size']
return True
def create3DHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None,
position=(0, 0, 0), size=(1.0, 0.6),
bg_color=(0.15, 0.15, 0.15, 0.9),
border_color=(0.3, 0.3, 0.3, 1.0),
title_color=(1.0, 1.0, 1.0, 1.0),
content_color=(0.9, 0.9, 0.9, 1.0),
update_interval=30.0, font=None):
"""
创建3D HTTP信息面板
"""
# 创建面板
domain = urlparse(url).netloc or url[:30]
title = f"HTTP数据: {domain}"
panel_node = self.create3DInfoPanel(
panel_id=panel_id,
position=position,
size=size,
bg_color=bg_color,
border_color=border_color,
title_color=title_color,
content_color=content_color,
font=font
)
# 更新标题
self.update3DPanelContent(panel_id, title=title)
# 立即获取并显示数据
content = fetchHTTPData(url, method, headers, data)
self.update3DPanelContent(panel_id, content=content)
# 注册数据源,定期更新
def http_data_callback():
return fetchHTTPData(url, method, headers, data)
self.registerDataSource(panel_id, http_data_callback, update_interval)
# 保存HTTP请求信息便于后续更新
if panel_id not in self.data_sources:
self.data_sources[panel_id] = {}
self.data_sources[panel_id]['http_info'] = {
'url': url,
'method': method,
'headers': headers,
'data': data
}
return panel_node
# 在 add_methods_to_property_panel 函数中添加以下方法
def add_methods_to_property_panel(property_panel_instance):
# ... (原有代码保持不变)
import types
def createHTTPInfoPanel(self, url, panel_id="http_info", method="GET", headers=None, data=None,
position=(0.8, 0.0), size=(0.4, 0.3), update_interval=30.0):
@ -721,12 +1039,233 @@ class InfoPanelManager(DirectObject):
print(f"✗ 更新HTTP信息面板失败: {e}")
return False
def create3DRealtimeDataPanel(self, data_callback=None, update_interval=1.0):
"""创建3D实时数据面板"""
try:
# 确保父节点已设置
if self.info_panel_manager.parent is None and hasattr(self, 'world'):
self.info_panel_manager.setParent(self.world.render)
# 创建3D实时数据面板
panel_node = self.info_panel_manager.create3DInfoPanel(
panel_id="realtime_data_3d",
position=(0, 0, 0),
size=(0.35, 0.3),
bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景
border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框
title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题
content_color=(0.95, 0.95, 0.95, 1.0)
)
# 设置标签
panel_node.setTag("element_type", "info_panel_3d")
panel_node.setTag("is_scene_element", "1")
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
# 如果提供了数据回调函数,则注册数据源
if data_callback:
# 立即显示初始数据
initial_data = data_callback()
self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=initial_data)
# 注册数据源
self.info_panel_manager.registerDataSource("realtime_data_3d", data_callback, update_interval)
else:
# 使用默认数据
default_data = "等待数据..."
self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=default_data)
print("✓ 已创建3D实时数据面板")
return panel_node
except Exception as e:
print(f"✗ 创建3D实时数据面板失败: {e}")
return None
def create3DModelInfoPanel(self, model):
"""为模型创建3D信息面板"""
try:
# 确保父节点已设置
if self.info_panel_manager.parent is None and hasattr(self, 'world'):
self.info_panel_manager.setParent(self.world.render)
# 获取模型信息
model_name = model.getName() if hasattr(model, 'getName') else 'Unknown'
num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0
# 创建面板内容
content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}"
# 创建或更新面板
panel_node = self.info_panel_manager.create3DInfoPanel(
panel_id="model_info_3d",
position=(2, 0, 0), # 默认放在模型旁边
size=(0.35, 0.25),
bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景
border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框
title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题
content_color=(0.95, 0.95, 0.95, 1.0)
)
# 更新面板内容为模型特定信息
self.info_panel_manager.update3DPanelContent("model_info_3d",
title="模型信息",
content=content)
# 设置标签
panel_node.setTag("element_type", "info_panel_3d")
panel_node.setTag("is_scene_element", "1")
panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑
print(f"✓ 已创建3D模型信息面板: {model_name}")
return panel_node
except Exception as e:
print(f"✗ 创建3D模型信息面板失败: {e}")
return None
# 在绑定方法的部分添加3D面板方法
property_panel_instance.create3DRealtimeDataPanel = types.MethodType(create3DRealtimeDataPanel,
property_panel_instance)
property_panel_instance.create3DModelInfoPanel = types.MethodType(create3DModelInfoPanel,
property_panel_instance)
# 将新方法绑定到实例
import types
# ... (原有绑定保持不变)
property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance)
property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance)
def serializePanelData(self, panel_id):
"""序列化面板数据用于保存"""
if panel_id not in self.panels:
return None
panel_data = self.panels[panel_id]
props = panel_data['properties']
# 获取面板类型
panel_type = "2d"
if panel_data['node'].hasTag("gui_type"):
gui_type = panel_data['node'].getTag("gui_type")
if "3d" in gui_type.lower():
panel_type = "3d"
# 构建序列化数据
serialized_data = {
'panel_id': panel_id,
'panel_type': panel_type,
'position': props.get('position', (0, 0) if panel_type == "2d" else (0, 0, 0)),
'size': props.get('size', (1.0, 0.6)),
'bg_color': props.get('bg_color', (0.15, 0.15, 0.15, 0.9)),
'border_color': props.get('border_color', (0.3, 0.3, 0.3, 1.0)),
'title_color': props.get('title_color', (1.0, 1.0, 1.0, 1.0)),
'content_color': props.get('content_color', (0.9, 0.9, 0.9, 1.0)),
'title': panel_data['title_label'].getText() if 'title_label' in panel_data else "信息面板",
'content': panel_data[
'content_label'].getText() if 'content_label' in panel_data else "" if 'content_node' not in panel_data else
panel_data['content_node'].getText(),
'font_path': props.get('font', None),
'bg_image': props.get('bg_image', None),
'visible': not panel_data['node'].isHidden()
}
# 添加HTTP面板特有数据
if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]:
serialized_data['http_info'] = self.data_sources[panel_id]['http_info']
return serialized_data
def getAllPanelData(self):
"""获取所有面板的序列化数据"""
panel_data_list = []
for panel_id in self.panels:
data = self.serializePanelData(panel_id)
if data:
panel_data_list.append(data)
return panel_data_list
def recreatePanelFromData(self, panel_data):
"""从序列化数据重新创建面板"""
try:
panel_id = panel_data['panel_id']
panel_type = panel_data['panel_type']
position = panel_data['position']
size = panel_data['size']
# 重建面板
if panel_type == "3d":
panel_node = self.create3DInfoPanel(
panel_id=panel_id,
position=position,
size=size,
bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)),
border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)),
title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)),
content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)),
visible=panel_data.get('visible', True),
font=panel_data.get('font_path', None),
bg_image=panel_data.get('bg_image', None)
)
# 更新内容
self.update3DPanelContent(
panel_id,
title=panel_data.get('title', '信息面板'),
content=panel_data.get('content', '')
)
else:
panel_node = self.createInfoPanel(
panel_id=panel_id,
position=(position[0], position[1]) if len(position) >= 2 else (0, 0),
size=size,
bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)),
border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)),
title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)),
content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)),
visible=panel_data.get('visible', True),
font=panel_data.get('font_path', None),
bg_image=panel_data.get('bg_image', None)
)
# 更新内容
self.updatePanelContent(
panel_id,
title=panel_data.get('title', '信息面板'),
content=panel_data.get('content', '')
)
# 设置标签
if panel_node:
panel_node.setTag("element_type", "info_panel")
panel_node.setTag("is_scene_element", "1")
panel_node.setTag("supports_3d_position_editing", "1")
# 如果是HTTP面板重新注册数据源
if 'http_info' in panel_data:
http_info = panel_data['http_info']
self.createHTTPInfoPanel(
panel_id=panel_id,
url=http_info['url'],
method=http_info.get('method', 'GET'),
headers=http_info.get('headers', None),
data=http_info.get('data', None),
position=position,
size=size,
update_interval=self.data_sources.get(panel_id, {}).get('interval',
30.0) if panel_id in self.data_sources else 30.0
)
print(f"✓ 信息面板 {panel_id} 已重建")
return panel_node
except Exception as e:
print(f"✗ 重建信息面板 {panel_data.get('panel_id', 'unknown')} 失败: {e}")
import traceback
traceback.print_exc()
return None
# 示例数据源函数
def getRealtimeData():
@ -777,6 +1316,7 @@ def add_methods_to_property_panel(property_panel_instance):
"""
property_panel 实例添加 DirectGUI 信息面板支持
"""
import types
# 添加信息面板管理器作为类属性
if not hasattr(property_panel_instance, 'info_panel_manager'):
@ -982,7 +1522,6 @@ def add_methods_to_property_panel(property_panel_instance):
property_panel_instance.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance)
property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance)
def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5):
"""
获取HTTP数据的通用函数

View File

@ -309,40 +309,81 @@ class CollisionManager:
CollisionPlane, CollisionPolygon, Plane, Vec3
)
bounds = model.getBounds()
if bounds.isEmpty():
# 获取考虑变换后的实际尺寸和中心
transformed_info = self._getTransformedModelInfo(model)
if not transformed_info:
# 默认小球体
return CollisionSphere(Point3(0, 0, 0), 1.0)
center = bounds.getCenter()
radius = bounds.getRadius()
center = transformed_info['center']
radius = transformed_info['radius']
actual_size = transformed_info['size']
scale_factor = transformed_info['scale_factor']
# 自动选择最适合的形状
if shape_type == 'auto':
shape_type = self._determineOptimalShape(model, bounds)
shape_type = self._determineOptimalShape(model, transformed_info)
if shape_type == 'sphere':
return CollisionSphere(center, kwargs.get('radius', radius))
# 优化球形碰撞体
sphere_radius = kwargs.get('radius', radius)
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
sphere_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
return CollisionSphere(sphere_center, sphere_radius)
elif shape_type == 'box':
# 创建包围盒
min_point = bounds.getMin()
max_point = bounds.getMax()
# 优化盒型碰撞体 - 更精确的尺寸和位置控制(考虑缩放)
# 获取自定义尺寸,如果没有提供则使用变换后的实际尺寸
width = kwargs.get('width', actual_size.x)
length = kwargs.get('length', actual_size.y)
height = kwargs.get('height', actual_size.z)
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
box_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
# 计算盒子的最小和最大点(基于偏移后的中心)
half_width = width / 2
half_length = length / 2
half_height = height / 2
min_point = Point3(box_center.x - half_width, box_center.y - half_length, box_center.z - half_height)
max_point = Point3(box_center.x + half_width, box_center.y + half_length, box_center.z + half_height)
return CollisionBox(min_point, max_point)
elif shape_type == 'capsule':
# 创建胶囊体(适合角色)
height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z))
radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3))
point_a = Point3(center.x, center.y, bounds.getMin().z + radius)
point_b = Point3(center.x, center.y, bounds.getMax().z - radius)
return CollisionCapsule(point_a, point_b, radius)
# 优化胶囊体碰撞 - 更合理的比例和位置控制(考虑缩放)
# 使用变换后的实际高度,或自定义高度
custom_height = kwargs.get('height', actual_size.z)
# 更合理的半径计算:基于变换后模型宽度的平均值
default_radius = min(actual_size.x, actual_size.y) / 2.5 # 稍微小于模型的宽度
custom_radius = kwargs.get('radius', min(default_radius, custom_height * 0.4))
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
capsule_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
# 计算胶囊体的两个端点(确保半径不会超出高度)
effective_height = max(custom_height, custom_radius * 2.1) # 确保高度至少是半径的2倍多一点
point_a = Point3(capsule_center.x, capsule_center.y, capsule_center.z - effective_height/2 + custom_radius)
point_b = Point3(capsule_center.x, capsule_center.y, capsule_center.z + effective_height/2 - custom_radius)
return CollisionCapsule(point_a, point_b, custom_radius)
elif shape_type == 'plane':
# 创建平面(适合地面、墙面)
# 优化平面碰撞 - 支持位置偏移和更灵活的法向量
normal = kwargs.get('normal', Vec3(0, 0, 1))
point = kwargs.get('point', center)
plane = Plane(normal, point)
# 支持位置偏移
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
plane_point = kwargs.get('point', Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z))
# 标准化法向量
normal.normalize()
plane = Plane(normal, plane_point)
return CollisionPlane(plane)
elif shape_type == 'polygon':
@ -364,13 +405,13 @@ class CollisionManager:
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
return collision_poly
else:
print("⚠️ 多边形至少需要3个顶点回退到球体")
#print("⚠️ 多边形至少需要3个顶点回退到球体")
return CollisionSphere(center, radius)
def _determineOptimalShape(self, model, bounds):
def _determineOptimalShape(self, model, transformed_info):
"""根据模型特征自动确定最适合的碰撞体形状"""
# 获取模型尺寸比例
size = bounds.getMax() - bounds.getMin()
# 获取变换后的模型尺寸比例
size = transformed_info['size']
max_dim = max(size.x, size.y, size.z)
min_dim = min(size.x, size.y, size.z)
@ -399,6 +440,87 @@ class CollisionManager:
else: # 其他用包围盒
return 'box'
def _getTransformedModelInfo(self, model):
"""获取考虑变换后的模型信息
Args:
model: 模型节点
Returns:
dict: 包含变换后的尺寸中心半径等信息
"""
try:
# 获取原始包围盒
bounds = model.getBounds()
if bounds.isEmpty():
return None
# 获取模型的变换矩阵
transform = model.getTransform()
scale = model.getScale()
# 计算缩放因子(取三轴缩放的平均值)
scale_factor = (abs(scale.x) + abs(scale.y) + abs(scale.z)) / 3.0
# 获取原始尺寸
original_size = bounds.getMax() - bounds.getMin()
# 应用缩放到尺寸
actual_size = Vec3(
original_size.x * abs(scale.x),
original_size.y * abs(scale.y),
original_size.z * abs(scale.z)
)
# 获取变换后的中心点(在世界坐标系中)
original_center = bounds.getCenter()
if hasattr(model, 'getPos'):
# 模型在世界坐标系中的位置
world_center = model.getPos(model.getParent() if model.getParent() else model)
center = Point3(world_center.x, world_center.y, world_center.z)
else:
# 如果无法获取世界位置,使用原始中心
center = original_center
# 计算变换后的半径(考虑缩放)
original_radius = bounds.getRadius()
transformed_radius = original_radius * scale_factor
# 调试信息
print(f"🔍 模型 {model.getName()} 变换信息:")
print(f" 原始尺寸: {original_size}")
print(f" 缩放因子: {scale}")
print(f" 变换后尺寸: {actual_size}")
print(f" 变换后半径: {transformed_radius:.2f}")
return {
'center': center,
'radius': transformed_radius,
'size': actual_size,
'scale_factor': scale_factor,
'original_size': original_size,
'scale': scale,
'transform': transform
}
except Exception as e:
print(f"⚠️ 获取模型变换信息失败: {e}")
# 回退到原始包围盒
bounds = model.getBounds()
if bounds.isEmpty():
return None
original_size = bounds.getMax() - bounds.getMin()
return {
'center': bounds.getCenter(),
'radius': bounds.getRadius(),
'size': original_size,
'scale_factor': 1.0,
'original_size': original_size,
'scale': Vec3(1, 1, 1),
'transform': None
}
def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
"""创建鼠标射线"""
# 组合掩码

View File

@ -167,7 +167,7 @@ class EventHandler:
picker.addCollider(pickerNP, queue)
picker.traverse(self.world.render)
print(f"碰撞检测结果数量: {queue.getNumEntries()}")
#print(f"碰撞检测结果数量: {queue.getNumEntries()}")
# 射线检测结果处理
hitPos = None
@ -184,13 +184,13 @@ class EventHandler:
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
# 优先检查是否点击了坐标轴
print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
if self.world.selection.gizmo:
print("准备检查坐标轴点击...")
#print("准备检查坐标轴点击...")
try:
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
if gizmoAxis:
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
#print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# 开始坐标轴拖拽
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
pickerNP.removeNode()
@ -203,16 +203,16 @@ class EventHandler:
traceback.print_exc()
print("继续处理模型选择...")
print("继续处理碰撞结果...")
#print("继续处理碰撞结果...")
if hitPos and hitNode:
print(f"✓ 检测到碰撞,开始处理点击事件")
print(f"GUI编辑模式: {self.world.guiEditMode}")
print(f"当前工具: {self.world.currentTool}")
#print(f"✓ 检测到碰撞,开始处理点击事件")
#print(f"GUI编辑模式: {self.world.guiEditMode}")
#print(f"当前工具: {self.world.currentTool}")
# 处理GUI编辑模式
if self.world.guiEditMode:
print("处理GUI编辑模式点击")
#print("处理GUI编辑模式点击")
# 检查是否点击了GUI元素
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
if clickedGUI:
@ -411,103 +411,6 @@ class EventHandler:
import traceback
traceback.print_exc()
def mousePressEventRight(self,evt):
"""处理鼠标右键按下事件"""
print(f"当前工具: {self.world.currentTool}")
# 检查是否是地形编辑模式
if self.world.currentTool == "地形编辑":
self._handleTerrainEdit(evt, "subtract") # 降低地形
return
# 其他右键处理逻辑可以在这里添加
print("鼠标右键事件处理")
def _handleTerrainEdit(self,evt,operation):
try:
x = evt.get('x',0)
y = evt.get('y',0)
winWidth,winHeight = self.world.getWindowSize()
mx = 2.0 * x/float(winWidth) - 1.0
my = 1.0 -2.0*y/float(winHeight)
nearPoint = Point3()
farPoint = Point3()
self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint)
worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint)
worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint)
picker = CollisionTraverser()
queue = CollisionHandlerQueue()
pickerNode = CollisionNode('terrain_edit_ray')
pickerNP = self.world.cam.attachNewNode(pickerNode)
from panda3d.core import BitMask32
pickerNode.setFromCollideMask(BitMask32.allOn()) # 检查所有碰撞
# 使用相机坐标系的点创建射线
direction = farPoint - nearPoint
direction.normalize()
pickerNode.addSolid(CollisionRay(nearPoint, direction))
picker.addCollider(pickerNP, queue)
picker.traverse(self.world.render)
print(f"地形碰撞检测结果数量: {queue.getNumEntries()}")
# 射线检测结果处理
hitPos = None
hitNode = None
if queue.getNumEntries() > 0:
# 遍历所有碰撞结果,找到地形节点
for i in range(queue.getNumEntries()):
entry = queue.getEntry(i)
collided_node = entry.getIntoNodePath()
print(f"碰撞到节点: {collided_node.getName()}")
# 检查是否是地形节点
for terrain_info in self.world.terrain_manager.terrains:
terrain_node = terrain_info['node']
if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node):
hitPos = entry.getSurfacePoint(self.world.render)
hitNode = collided_node
print(f"找到地形节点: {terrain_node.getName()}")
# 修改地形高度
x_pos, y_pos = hitPos.getX(), hitPos.getY()
success = self.world.modifyTerrainHeight(
terrain_info, x_pos, y_pos, radius=3.0, strength=0.3, operation=operation)
if success:
print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})")
# 显示射线
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
else:
print("✗ 地形编辑失败")
break
if hitPos:
break
if not hitPos:
print("没有检测到地形碰撞")
# 显示射线(无碰撞)
self.showClickRay(worldNearPoint, worldFarPoint)
# 清理碰撞检测节点
pickerNP.removeNode()
print("地形编辑处理完成")
except Exception as e:
print(f"地形编辑处理出错: {e}")
import traceback
traceback.print_exc()
def _handleSelectionClick(self, hitNode):
"""处理选择工具的点击事件"""
print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}")
@ -541,43 +444,52 @@ class EventHandler:
selectedModel = model
print(f"找到父模型: {selectedModel.getName()}")
break
if selectedModel:
break
current = current.getParent()
if selectedModel:
print(f"✓ 最终选中模型: {selectedModel.getName()}")
#print(f"✓ 最终选中模型: {selectedModel.getName()}")
self.world.selection.handleMouseClick(selectedModel)
# 更新选择状态并显示选择框和坐标轴
self.world.selection.updateSelection(selectedModel)
# 在树形控件中查找并选中对应的项
if self.world.interface_manager.treeWidget:
print("查找树形控件中的对应项...")
#print("查找树形控件中的对应项...")
root = self.world.interface_manager.treeWidget.invisibleRootItem()
foundItem = None
for i in range(root.childCount()):
sceneItem = root.child(i)
if sceneItem.text(0) == "场景":
print(f"在场景节点下查找...")
#print(f"在场景节点下查找...")
foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem)
if foundItem:
print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}")
try:
self.world.interface_manager.treeWidget.itemClicked.disconnect()
except TypeError:
pass
self.world.interface_manager.treeWidget.setCurrentItem(foundItem)
self.world.property_panel.updatePropertyPanel(foundItem)
self.world.interface_manager.treeWidget.itemClicked.connect(
self.world.interface_manager.onTreeItemClicked)
else:
print("× 在树形控件中没有找到对应项")
break
if not foundItem:
print("× 没有找到场景节点或对应的树形项")
else:
print("× 树形控件不存在")
else:
print("× 没有找到可选择的模型节点")
self.world.selection.updateSelection(None)
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""

495
core/patrol_system.py Normal file
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

@ -253,7 +253,7 @@ class ScriptLoader:
for component in components_to_remove:
self.script_manager.remove_script_from_object(component.game_object, script_name)
# 从sys.modules中移除
module = self.loaded_modules[script_name]
if module.__name__ in sys.modules:
@ -294,6 +294,31 @@ class ScriptLoader:
return len(changed_scripts) > 0
def find_script_file(self, script_name: str) -> Optional[str]:
"""根据脚本名称查找脚本文件路径"""
# 首先检查已加载的脚本
if script_name in self.loaded_modules:
module = self.loaded_modules[script_name]
if hasattr(module, '__file__') and module.__file__:
return module.__file__
# 在已知的文件路径中查找
for file_path in self.file_mtimes.keys():
file_name = os.path.splitext(os.path.basename(file_path))[0]
if file_name == script_name:
return file_path
# 在脚本目录中查找
scripts_dir = self.script_manager.scripts_directory
if os.path.exists(scripts_dir):
for file_name in os.listdir(scripts_dir):
if file_name.endswith('.py'):
base_name = os.path.splitext(file_name)[0]
if base_name == script_name:
return os.path.join(scripts_dir, file_name)
return None
class ScriptAPI:
"""脚本API - 提供给脚本使用的API接口"""
@ -671,18 +696,62 @@ class {class_name}(ScriptBase):
return False
script_components = self.object_scripts[game_object]
removed = False
for component in script_components[:]: # 复制列表以避免修改时出错
if component.script_instance.__class__.__name__ == script_name:
# 从引擎移除
self.engine.remove_script_component(component)
# 从对象脚本列表移除
script_components.remove(component)
removed = True
print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}")
return True
if not script_components:
del self.object_scripts[game_object]
# 更新节点上保存的脚本信息标签
if removed:
self._update_node_script_tags_after_removal(game_object, script_name)
return False
return removed
def _update_node_script_tags_after_removal(self, game_object, removed_script_name):
"""在移除脚本后更新节点标签"""
try:
# 获取对象上剩余的脚本
remaining_scripts = self.get_scripts_on_object(game_object)
if not remaining_scripts:
# 如果没有其他脚本,清除所有脚本标签
if game_object.hasTag("has_scripts"):
game_object.clearTag("has_scripts")
if game_object.hasTag("scripts_info"):
game_object.clearTag("scripts_info")
print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签")
else:
# 如果还有其他脚本,更新脚本信息标签
script_info_list = []
for script_component in remaining_scripts:
script_name = script_component.script_name
script_class = script_component.script_instance.__class__
script_file = self.loader.find_script_file(script_name) or ""
script_info_list.append({
"name": script_name,
"file": script_file
})
import json
game_object.setTag("has_scripts", "true")
game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False))
print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本")
except Exception as e:
print(f"更新节点标签失败: {e}")
def get_scripts_on_object(self, game_object) -> List[ScriptComponent]:
"""获取对象上的所有脚本"""
return self.object_scripts.get(game_object, [])
@ -744,6 +813,36 @@ class {class_name}(ScriptBase):
print("==================\n")
def save_object_scripts(self,game_object,node_data:dict):
try:
if game_object in self.object_scripts:
scripts_data = []
for script_commponent in self.object_scripts[game_object]:
script_info = {
'script_name':script_commponent.script_name,
'enabled':script_commponent.enabled,
'script_class':script_commponent.script_instance.__class__.__name__
}
scripts_data.append(script_info)
if scripts_data:
node_data['scripts'] = scripts_data
print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}")
except Exception as e:
print(f"保存对象脚本信息失败: {e}")
traceback.print_exc()
# def restore_object_scripts(self,game_object,node_data:dict):
# try:
# if 'scripts' in node_data:
# scripts_data = node_data['scripts']
# restored_count = 0
# for script_info in scripts_data:
# script_name = script_info.get('script_name')
# enabled = script_info.get('enabled',True)
#
# #检查脚本是否可用
# if script_name in self.loader.script_classes:
#为
# 添加全局便捷函数让脚本更容易使用API
def get_script_api():

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,13 @@
# core/terrain_manager.py
import time
import urllib
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
from scene import util
class TerrainManager:
"""地形管理类"""
@ -31,106 +35,151 @@ class TerrainManager:
return None
try:
# 创建GeoMipTerrain对象
terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain = GeoMipTerrain(terrain_name)
print(f"🔆 开始创建高度图地形")
# 加载高度图
height_image = PNMImage(Filename.fromOsSpecific(heightmap_path))
# 检查并调整图像尺寸为2的幂次方加1
width, height = height_image.getXSize(), height_image.getYSize()
print(f"原始图像尺寸: {width}x{height}")
# 找到最接近的有效尺寸
valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049]
target_size = 129 # 默认尺寸
# 选择最接近的尺寸
max_dim = max(width, height)
for size in valid_sizes:
if size >= max_dim:
target_size = size
break
else:
target_size = valid_sizes[-1] # 使用最大尺寸
# 如果需要,调整图像尺寸
if width != target_size or height != target_size:
print(f"调整图像尺寸从 {width}x{height}{target_size}x{target_size}")
# 使用正确的图像缩放方法
resized_image = PNMImage(target_size, target_size)
resized_image.quickFilterFrom(height_image)
height_image = resized_image
# 使用正确的方法设置高度图
terrain.setHeightfield(height_image)
# 设置地形参数
terrain.setBruteforce(True) # 使用LOD
terrain.setBlockSize(32)
# terrain.setNearFarThreshold(50.0,200.0)
# 生成地形
terrain.generate()
# 获取地形节点
terrain_node = terrain.getRoot()
if terrain_node.isEmpty():
print("错误:无法生成有效的地形节点")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}"
terrain_node.setName(node_name)
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
center_offset = (target_size - 1) / 2
terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5)
created_terrains = []
try:
parent_item, parent_node = target_parents[0]
# 设置缩放
terrain_node.setScale(scale[0], scale[1], scale[2])
# 创建GeoMipTerrain对象
terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain = GeoMipTerrain(terrain_name)
# 将地形添加到场景中
terrain_node.reparentTo(self.world.render)
# 加载高度图
height_image = PNMImage(Filename.fromOsSpecific(heightmap_path))
from panda3d.core import BitMask32
# 设置地形节点的碰撞掩码
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 为地形的所有子节点也设置碰撞掩码
for child in terrain_node.getChildren():
child.setCollideMask(BitMask32.bit(2))
# 检查并调整图像尺寸为2的幂次方加1
width, height = height_image.getXSize(), height_image.getYSize()
print(f"原始图像尺寸: {width}x{height}")
# 添加材质
self._applyTerrainMaterial(terrain_node)
# 找到最接近的有效尺寸
valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049]
target_size = 129 # 默认尺寸
terrain_node.setPythonTag("selectable", True)
# 选择最接近的尺寸
max_dim = max(width, height)
for size in valid_sizes:
if size >= max_dim:
target_size = size
break
else:
target_size = valid_sizes[-1] # 使用最大尺寸
# 保存地形信息(包括高度图的副本)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
'scale': scale,
'name': node_name
}
# 如果需要,调整图像尺寸
if width != target_size or height != target_size:
print(f"调整图像尺寸从 {width}x{height}{target_size}x{target_size}")
# 使用正确的图像缩放方法
resized_image = PNMImage(target_size, target_size)
resized_image.quickFilterFrom(height_image)
height_image = resized_image
self.terrains.append(terrain_info)
# 使用正确的方法设置高度图
terrain.setHeightfield(height_image)
# 更新场景树(再次检查节点是否有效)
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
# 设置地形参数
terrain.setBruteforce(True) # 使用LOD
print(f"✓ 成功从 {heightmap_path} 创建地形")
return terrain_info
terrain.setBlockSize(32)
# terrain.setNearFarThreshold(50.0,200.0)
# 生成地形
terrain.generate()
# 获取地形节点
terrain_node = terrain.getRoot()
if terrain_node.isEmpty():
print("错误:无法生成有效的地形节点")
return None
terrain_node.setTag("is_scene_element", "1")
terrain_node.setTag("tree_item_type", "TERRAIN_NODE")
node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}"
terrain_node.setName(node_name)
center_offset = (target_size - 1) / 2
terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5)
# 设置缩放
terrain_node.setScale(scale[0], scale[1], scale[2])
# 将地形添加到场景中
terrain_node.reparentTo(parent_node)
from panda3d.core import BitMask32
# 设置地形节点的碰撞掩码
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 为地形的所有子节点也设置碰撞掩码
for child in terrain_node.getChildren():
child.setCollideMask(BitMask32.bit(2))
# 添加材质
self._applyTerrainMaterial(terrain_node)
terrain_node.setPythonTag("selectable", True)
# 保存地形信息(包括高度图的副本)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
'scale': scale,
'name': node_name
}
self.terrains.append(terrain_info)
print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}")
return None
# 处理创建结果
if not created_terrains:
print("❌ 没有成功创建任何高度图地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形")
# 返回值处理
if len(created_terrains) == 1:
return created_terrains[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_terrains] # 多个光源返回列表
except Exception as e:
print(f"创建地形时出错: {e}")
print(f"❌ 创建高度图地形过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
@ -138,84 +187,129 @@ class TerrainManager:
def createFlatTerrain(self, size=(0.3, 0.3), resolution=129):
"""创建平面地形"""
try:
# 确保分辨率是2的幂次方加1 (如129, 257, 513等)
valid_resolutions = [17, 33, 65, 129, 257, 513, 1025]
if resolution not in valid_resolutions:
# 找到最接近的有效分辨率
closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution))
print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}")
resolution = closest_res
print(f"🔆 开始创建平面地形")
# 创建GeoMipTerrain对象
terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain = GeoMipTerrain(terrain_name)
# 创建一个平面高度图尺寸必须是2^n+1
height_image = PNMImage(resolution, resolution)
height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围)
# 使用正确的方法设置高度图
terrain.setHeightfield(height_image)
terrain.setBruteforce(True) # 设置LOD
# 设置地形参数
terrain.setBlockSize(32) # 设置块大小
# 生成地形
terrain.generate()
# 获取地形节点
terrain_node = terrain.getRoot()
if terrain_node.isEmpty():
print("错误:无法生成有效的平面地形节点")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
print("❌ 无法访问树形控件")
return None
node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain_node.setName(node_name)
# 获取目标父节点列表
target_parents = tree_widget.get_target_parents_for_creation()
if not target_parents:
print("❌ 没有找到有效的父节点")
return None
center_offset = (resolution - 1) / 2
terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0)
created_terrains = []
# 将地形添加到场景中
terrain_node.reparentTo(self.world.render)
try:
parent_item, parent_node = target_parents[0]
# 确保分辨率是2的幂次方加1 (如129, 257, 513等)
valid_resolutions = [17, 33, 65, 129, 257, 513, 1025]
if resolution not in valid_resolutions:
# 找到最接近的有效分辨率
closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution))
print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}")
resolution = closest_res
# 为地形添加碰撞体
from panda3d.core import BitMask32
# 设置地形节点的碰撞掩码
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 为地形的所有子节点也设置碰撞掩码
for child in terrain_node.getChildren():
child.setCollideMask(BitMask32.bit(2))
# 创建GeoMipTerrain对象
terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain = GeoMipTerrain(terrain_name)
# 添加材质
self._applyTerrainMaterial(terrain_node)
# 创建一个平面高度图尺寸必须是2^n+1
height_image = PNMImage(resolution, resolution)
height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围)
# 保存地形信息(包括高度图)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': None,
'heightfield': height_image, # 保存高度图
'scale': (size[0], size[1], 50),
'name': node_name
}
# 使用正确的方法设置高度图
terrain.setHeightfield(height_image)
terrain.setBruteforce(True) # 设置LOD
self.terrains.append(terrain_info)
# 设置地形参数
terrain.setBlockSize(32) # 设置块大小
# 更新场景树(再次检查节点是否有效)
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
# 生成地形
terrain.generate()
print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}")
return terrain_info
# 获取地形节点
terrain_node = terrain.getRoot()
if terrain_node.isEmpty():
print("错误:无法生成有效的平面地形节点")
return None
terrain_node.setTag("is_scene_element", "1")
terrain_node.setTag("tree_item_type", "TERRAIN_NODE")
node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain_node.setName(node_name)
center_offset = (resolution - 1) / 2
terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0)
# 将地形添加到场景中
terrain_node.reparentTo(parent_node)
# 为地形添加碰撞体
from panda3d.core import BitMask32
# 设置地形节点的碰撞掩码
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 为地形的所有子节点也设置碰撞掩码
for child in terrain_node.getChildren():
child.setCollideMask(BitMask32.bit(2))
# 添加材质
self._applyTerrainMaterial(terrain_node)
# 保存地形信息(包括高度图)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': None,
'heightfield': height_image, # 保存高度图
'scale': (size[0], size[1], 50),
'name': node_name
}
self.terrains.append(terrain_info)
print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
except Exception as e:
print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}")
return None
# 处理创建结果
if not created_terrains:
print("❌ 没有成功创建任何平面地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形")
# 返回值处理
if len(created_terrains) == 1:
return created_terrains[0][0] # 单个光源返回NodePath
else:
return [light_np for light_np, _ in created_terrains] # 多个光源返回列表
except Exception as e:
print(f"创建平面地形时出错: {e}")
print(f"❌ 创建平面地形过程失败: {str(e)}")
import traceback
traceback.print_exc()
return None
@ -390,7 +484,6 @@ class TerrainManager:
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}")
print(f"✓ 重新设置了地形碰撞体")
return modified
except Exception as e:
@ -405,14 +498,17 @@ class TerrainManager:
# 从场景中移除地形节点
terrain_node = terrain_info['node']
if terrain_node and not terrain_node.isEmpty():
terrain_node.removeNode()
tree_widget = self._get_tree_widget()
if tree_widget:
tree_widget.delete_items(tree_widget.selectedItems())
# terrain_node.removeNode()
#
# # 从列表中移除
# self.terrains.remove(terrain_info)
# 从列表中移除
self.terrains.remove(terrain_info)
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
# # 更新场景树
# if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
# self.world.scene_manager.updateSceneTree()
print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}")
@ -461,6 +557,7 @@ class TerrainManager:
if terrain_info in self.terrains:
terrain_node = terrain_info['node']
if terrain_node and os.path.exists(texture_path):
texture_path = util.normalize_model_path(texture_path)
# 加载纹理
texture = self.world.loader.loadTexture(texture_path)
if texture:
@ -476,3 +573,88 @@ class TerrainManager:
except Exception as e:
print(f"应用地形纹理时出错: {e}")
return False
def saveTerrainData(self, terrain_info, filename):
"""保存地形数据到文件"""
try:
terrain_node = terrain_info['node']
heightfield = terrain_info['heightfield']
# 保存高度图到文件
if heightfield:
# 创建保存路径
terrain_dir = os.path.join(os.path.dirname(filename), "terrains")
if not os.path.exists(terrain_dir):
os.makedirs(terrain_dir)
# 生成唯一的地形文件名
terrain_filename = f"terrain_{terrain_info.get('name', 'unnamed')}_{int(time.time())}.png"
terrain_path = os.path.join(terrain_dir, terrain_filename)
# 保存高度图
heightfield.write(Filename.fromOsSpecific(terrain_path))
# 保存地形信息到标签
terrain_node.setTag("terrain_heightmap_path", terrain_path)
terrain_node.setTag("terrain_scale", str(terrain_info['scale']))
print(f"✓ 地形数据已保存: {terrain_path}")
return terrain_path
return None
except Exception as e:
print(f"保存地形数据时出错: {e}")
return None
def _recreateTerrain(self, terrain_node):
"""重新创建地形"""
try:
print(f"重新创建地形: {terrain_node.getName()}")
# 获取保存的地形信息
heightmap_path = terrain_node.getTag("terrain_heightmap_path") if terrain_node.hasTag(
"terrain_heightmap_path") else None
scale_str = terrain_node.getTag("terrain_scale") if terrain_node.hasTag("terrain_scale") else "(1, 1, 1)"
# 解析缩放信息
try:
scale_str = scale_str.strip("()")
scale_values = [float(x.strip()) for x in scale_str.split(",")]
scale = (scale_values[0], scale_values[1], scale_values[2]) if len(scale_values) >= 3 else (1, 1, 1)
except:
scale = (1, 1, 1)
# 恢复位置信息
if terrain_node.hasTag("transform_pos"):
try:
pos_str = terrain_node.getTag("transform_pos")
pos_str = pos_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()')
pos_values = [float(x.strip()) for x in pos_str.split(',')]
if len(pos_values) >= 3:
terrain_node.setPos(pos_values[0], pos_values[1], pos_values[2])
except Exception as e:
print(f"恢复地形位置失败: {e}")
# 如果有高度图路径,重新创建地形
if heightmap_path and os.path.exists(heightmap_path):
# 使用现有方法重新创建地形
new_terrain = self.createTerrainFromHeightMap(heightmap_path, scale)
if new_terrain:
# 删除旧的地形节点
if not terrain_node.isEmpty():
terrain_node.removeNode()
return new_terrain
# 如果没有高度图或创建失败,创建一个平面地形作为替代
print("使用平面地形作为替代")
new_terrain = self.createFlatTerrain(size=(scale[0], scale[1]), resolution=129)
if new_terrain:
# 删除旧的地形节点
if not terrain_node.isEmpty():
terrain_node.removeNode()
return new_terrain
return terrain_node
except Exception as e:
print(f"重新创建地形失败: {e}")
return terrain_node

View File

@ -5,6 +5,7 @@ class ToolManager:
"""初始化工具管理器"""
self.world = world
self.currentTool = "选择" # 默认工具为选择工具
print(f"当前工具: {self.currentTool}")
def setCurrentTool(self, tool):
"""设置当前工具"""

View File

@ -288,6 +288,8 @@ class CoreWorld(Panda3DWorld):
self.cam.setPos(0, -50, 20)
self.cam.lookAt(0, 0, 0)
self.camLens.setFov(80)
self.cam.setTag("is_scene_element", "1")
self.cam.setTag("tree_item_type", "CAMERA_NODE")
print("✓ 相机设置完成")
def _setupLighting(self):
@ -319,18 +321,61 @@ class CoreWorld(Panda3DWorld):
# 创建地板节点
self.ground = self.render.attachNewNode(cm.generate())
self.ground.setP(-90)
self.ground.setZ(-0.1)
self.ground.setZ(-1.0)
self.ground.setColor(0.8, 0.8, 0.8, 1)
# self.ground.setTag("is_scene_element", "1")
# self.ground.setTag("tree_item_type", "SCENE_NODE")
# 创建支持贴图的材质
mat = Material()
mat.setName("GroundMaterial")
color = LColor(1, 1, 1, 0.8)
mat.set_base_color(color)
mat.set_roughness(0.5) # 设置合适的初始粗糙度
mat.set_roughness(1) # 设置合适的初始粗糙度
mat.set_metallic(0.5) # 设置较低的初始金属性
self.ground.set_material(mat)
#创建第二个相同的地面,位置稍有偏移
self.ground2 = self.render.attachNewNode(cm.generate())
self.ground2.setH(-90)
self.ground2.setZ(-1.0)
self.ground2.setX(50) # 在X轴方向偏移
self.ground2.setZ(49) # 在X轴方向偏移
self.ground2.setColor(0.8, 0.8, 0.8, 1)
self.ground2.set_material(mat)
# 创建第三个相同的地面,位置在另一个方向
self.ground3 = self.render.attachNewNode(cm.generate())
self.ground3.setH(90)
self.ground3.setZ(-1.0)
self.ground3.setX(-50) # 在X轴负方向偏移
self.ground3.setZ(49) # 在X轴负方向偏移
self.ground3.setColor(0.8, 0.8, 0.8, 1)
self.ground3.set_material(mat)
self.ground4 = self.render.attachNewNode(cm.generate())
# self.ground3.setR(90)
self.ground4.setZ(-1.0)
self.ground4.setY(50) # 在X轴负方向偏移
self.ground4.setZ(49) # 在X轴负方向偏移
self.ground4.setColor(0.8, 0.8, 0.8, 1)
self.ground4.set_material(mat)
self.ground5 = self.render.attachNewNode(cm.generate())
self.ground5.setP(180)
self.ground5.setZ(-1)
self.ground5.setY(-50) # 在X轴负方向偏移
self.ground5.setZ(49) # 在X轴负方向偏移
self.ground5.setColor(0.8, 0.8, 0.8, 1)
self.ground5.set_material(mat)
self.ground6 = self.render.attachNewNode(cm.generate())
self.ground6.setP(90)
self.ground6.setZ(-1)
self.ground6.setZ(99) # 在X轴负方向偏移
self.ground6.setColor(0.8, 0.8, 0.8, 1)
self.ground6.set_material(mat)
# 应用默认PBR效果确保支持贴图
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
@ -347,6 +392,72 @@ class CoreWorld(Panda3DWorld):
},
50
)
# 为其他两个地面也应用相同的效果
self.render_pipeline.set_effect(
self.ground2,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground3,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground4,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground5,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
self.render_pipeline.set_effect(
self.ground6,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": True,
"render_envmap": True
},
50
)
print("✓ 地板PBR效果已应用")
else:
print("⚠️ RenderPipeline未初始化地板将使用基础渲染")

View File

@ -359,7 +359,7 @@ class GizmoDragTestWorld(Panda3DWorld):
distance = distance_to_line(
(mouseX, mouseY), center_screen, axis_screen
)
print(f"{axis_label}距离: {distance:.2f}")
#print(f"{axis_label}距离: {distance:.2f}")
if distance < click_threshold:
print(f"✓ 点击了{axis_label}")

View File

@ -24,78 +24,80 @@ except ImportError:
WEB_ENGINE_AVAILABLE = False
print("⚠️ QtWebEngineWidgets 不可用Cesium 集成功能将被禁用")
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
# 参数类型检查和转换
if isinstance(size, (list, tuple)):
if len(size) >= 2:
x_size, y_size = float(size[0]), float(size[1])
else:
x_size = y_size = float(size[0]) if size else 1.0
else:
x_size = y_size = float(size)
# 创建卡片
cm = CardMaker('gui_3d_image')
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
# 创建3D图像节点
image_node = self.world.render.attachNewNode(cm.generate())
image_node.setPos(*pos)
# 为3D图像创建独立的材质
material = Material(f"image-material-{len(self.gui_elements)}")
material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
image_node.setMaterial(material, 1)
image_node.setTransparency(TransparencyAttrib.MAlpha)
# 如果提供了图像路径,则加载纹理
if image_path:
self.update3DImageTexture(image_node, image_path)
# 应用PBR效果如果可用
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.set_effect(
image_node,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": False,
"render_envmap": True,
"disable_children_effects": True
},
50
)
print("✓ GUI 3D图像PBR效果已应用")
except Exception as e:
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
# 为GUI元素添加标识效仿3D文本方法
image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
if image_path:
image_node.setTag("gui_image_path", image_path)
image_node.setTag("is_gui_element", "1")
self.gui_elements.append(image_node)
# 更新场景树
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
return image_node
# def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
# from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
#
# # 参数类型检查和转换
# if isinstance(size, (list, tuple)):
# if len(size) >= 2:
# x_size, y_size = float(size[0]), float(size[1])
# else:
# x_size = y_size = float(size[0]) if size else 1.0
# else:
# x_size = y_size = float(size)
#
# # 创建卡片
# cm = CardMaker('gui_3d_image')
# cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
#
# # 创建3D图像节点
# image_node = self.world.render.attachNewNode(cm.generate())
# image_node.setPos(*pos)
#
# # 为3D图像创建独立的材质
# material = Material(f"image-material-{len(self.gui_elements)}")
# material.setBaseColor(LColor(1, 1, 1, 1))
# material.setDiffuse(LColor(1, 1, 1, 1))
# material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
# material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
# material.setShininess(10.0)
# material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
# image_node.setMaterial(material, 1)
#
# image_node.setTransparency(TransparencyAttrib.MAlpha)
#
# # 如果提供了图像路径,则加载纹理
# if image_path:
# self.update3DImageTexture(image_node, image_path)
#
# # 应用PBR效果如果可用
# try:
# if hasattr(self, 'render_pipeline') and self.render_pipeline:
# self.render_pipeline.set_effect(
# image_node,
# "effects/default.yaml",
# {
# "normal_mapping": True,
# "render_gbuffer": True,
# "alpha_testing": False,
# "parallax_mapping": False,
# "render_shadow": False,
# "render_envmap": True,
# "disable_children_effects": True
# },
# 50
# )
# print("✓ GUI 3D图像PBR效果已应用")
# except Exception as e:
# print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
#
# # 为GUI元素添加标识效仿3D文本方法
# image_node.setTag("gui_type", "3d_image")
# image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
# image_node.setTag("is_scene_element", "1")
# image_node.setTag("tree_item_type", "GUI_3DIMAGE")
# if image_path:
# image_node.setTag("gui_image_path", image_path)
# image_node.setTag("is_gui_element", "1")
#
# self.gui_elements.append(image_node)
#
# # 更新场景树
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
#
# print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
# return image_node
class GUIManager:
"""GUI元素管理系统类"""
@ -124,7 +126,7 @@ class GUIManager:
# ==================== GUI元素创建方法 ====================
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=(0.1,0.1,0.1)):
"""创建2D GUI按钮 - 支持多选创建和GUI父子关系优化版本"""
try:
from direct.gui.DirectGui import DirectButton
@ -180,17 +182,35 @@ class GUIManager:
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
rolloverSound=None,
clickSound=None,
parent=parent_gui_node # 设置GUI父节点
parent=parent_gui_node
)
if not hasattr(button,'_tags'):
button._tags = {}
# button._tags["gui_type"] = "button"
# button._tags["gui_id"] = f"button_{len(self.gui_elements)}"
# button._tags["gui_text"] = text
# button._tags["is_gui_element"] = "1"
# button._tags["is_scene_element"] = "1"
# button._tags["saved_gui_type"] = "button"
# button._tags["gui_element_type"] = "button"
# button._tags["created_by_user"] = "1"
# button._tags["name"] = button_name
# button.setName(button_name)
# 设置节点标签
button.setTag("gui_type", "button")
button.setTag("gui_id", f"button_{len(self.gui_elements)}")
button.setTag("gui_text", text)
button.setTag("is_gui_element", "1")
button.setTag("is_scene_element", "1")
button.setTag("is_scene_element", "1") # 确保这个标签被设置
button.setTag("tree_item_type", "GUI_BUTTON")
button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性
button.setTag("gui_element_type", "button")
button.setTag("created_by_user", "1")
button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
button.setTag("name", button_name)
button.setName(button_name)
# 如果有GUI父节点建立引用关系
@ -200,6 +220,7 @@ class GUIManager:
# 添加到GUI元素列表
self.gui_elements.append(button)
button.reparentTo(self.world.aspect2d)
print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}")
@ -221,11 +242,11 @@ class GUIManager:
return None
# 选中最后创建的按钮并更新场景树
if created_buttons:
last_button, last_qt_item = created_buttons[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_button, last_qt_item)
# if created_buttons:
# last_button, last_qt_item = created_buttons[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮")
@ -301,9 +322,11 @@ class GUIManager:
label.setTag("gui_id", f"label_{len(self.gui_elements)}")
label.setTag("gui_text", text)
label.setTag("is_gui_element", "1")
label.setTag("tree_item_type", "GUI_LABEL")
label.setTag("is_scene_element", "1")
label.setTag("created_by_user", "1")
label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
label.setTag("name",label_name)
label.setName(label_name)
# 如果有GUI父节点建立引用关系
@ -334,11 +357,11 @@ class GUIManager:
return None
# 选中最后创建的标签并更新场景树
if created_labels:
last_label, last_qt_item = created_labels[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_label, last_qt_item)
# if created_labels:
# last_label, last_qt_item = created_labels[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_label, last_qt_item)
print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签")
@ -394,6 +417,10 @@ class GUIManager:
parent_gui_node = None
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
font = None
if hasattr(self.world,'getChineseFont'):
font = self.world.getChineseFont()
entry = DirectEntry(
text="",
pos=gui_pos,
@ -404,7 +431,9 @@ class GUIManager:
numLines=1,
width=12,
focus=0,
parent=parent_gui_node # 设置GUI父节点
parent=parent_gui_node, # 设置GUI父节点
text_font = font,
frameSize=(-0.1,0.1,-0.05,0.05)
)
# 设置节点标签
@ -412,9 +441,11 @@ class GUIManager:
entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
entry.setTag("gui_placeholder", placeholder)
entry.setTag("is_gui_element", "1")
entry.setTag("tree_item_type", "GUI_ENTRY")
entry.setTag("is_scene_element", "1")
entry.setTag("created_by_user", "1")
entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
entry.setTag("name",entry_name)
entry.setName(entry_name)
# 如果有GUI父节点建立引用关系
@ -445,11 +476,11 @@ class GUIManager:
return None
# 选中最后创建的输入框并更新场景树
if created_entries:
last_entry, last_qt_item = created_entries[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_entry, last_qt_item)
# if created_entries:
# last_entry, last_qt_item = created_entries[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_entry, last_qt_item)
print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
@ -509,7 +540,11 @@ class GUIManager:
cm = CardMaker('gui-2d-image')
cm.setFrame(-size, size, -size, size)
image_node = self.world.aspect2d.attachNewNode(cm.generate())
# image_node = self.world.aspect2d.attachNewNode(cm.generate())
if parent_gui_node:
image_node = parent_gui_node.attachNewNode(cm.generate())
else:
image_node = self.world.aspect2d.attachNewNode(cm.generate())
image_node.setPos(gui_pos)
image_node.setBin('fixed', 0)
image_node.setDepthWrite(False)
@ -522,6 +557,7 @@ class GUIManager:
# 如果提供了图像路径,则加载纹理
if image_path:
try:
image_node.setTag("image_path", image_path)
texture = self.world.loader.loadTexture(image_path)
if texture:
image_node.setTexture(texture, 1)
@ -541,8 +577,10 @@ class GUIManager:
image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}")
image_node.setTag("is_gui_element", "1")
image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_IMAGE")
image_node.setTag("created_by_user", "1")
image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
image_node.setTag("name",image_name)
image_node.setName(image_name)
# 如果有GUI父节点建立引用关系
@ -573,11 +611,11 @@ class GUIManager:
return None
# 选中最后创建的按钮并更新场景树
if created_2dimage:
last_button, last_qt_item = created_2dimage[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_button, last_qt_item)
# if created_2dimage:
# last_button, last_qt_item = created_2dimage[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_button, last_qt_item)
print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮")
@ -692,7 +730,9 @@ class GUIManager:
textNodePath.setTag("gui_text", text)
textNodePath.setTag("is_gui_element", "1")
textNodePath.setTag("is_scene_element", "1")
textNodePath.setTag("tree_item_type", "GUI_3DTEXT")
textNodePath.setTag("created_by_user", "1")
textNodePath.setTag("name", text_name)
# 添加到GUI元素列表
self.gui_elements.append(textNodePath)
@ -717,11 +757,11 @@ class GUIManager:
return None
# 选中最后创建的文本并更新场景树
if created_texts:
last_text, last_qt_item = created_texts[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_text, last_qt_item)
# if created_texts:
# last_text, last_qt_item = created_texts[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_text, last_qt_item)
print(f"🎉 总共创建了 {len(created_texts)} 个3D文本")
@ -740,6 +780,7 @@ class GUIManager:
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
"""创建3D空间图片"""
try:
from panda3d.core import TextNode
from PyQt5.QtCore import Qt
@ -779,7 +820,8 @@ class GUIManager:
cm.setFrame(-x_size / 2, x_size / 2, -y_size / 2, y_size / 2)
# 创建3D图像节点
image_node = self.world.render.attachNewNode(cm.generate())
# image_node = self.world.render.attachNewNode(cm.generate())
image_node = parent_node.attachNewNode(cm.generate())
image_node.setPos(*pos)
# 为3D图像创建独立的材质
@ -824,10 +866,12 @@ class GUIManager:
image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
if image_path:
image_node.setTag("gui_image_path", image_path)
image_node.setTag("image_path", image_path)
image_node.setTag("is_gui_element", "1")
image_node.setTag("is_scene_element", "1")
image_node.setTag("tree_item_type", "GUI_3DIMAGE")
image_node.setTag("created_by_user", "1")
image_node.setTag("name",image_name)
# 添加到GUI元素列表
self.gui_elements.append(image_node)
@ -852,11 +896,11 @@ class GUIManager:
return None
# 选中最后创建的文本并更新场景树
if created_3dimage:
last_image, last_qt_item = created_3dimage[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
tree_widget.update_selection_and_properties(last_image, last_qt_item)
# if created_3dimage:
# last_image, last_qt_item = created_3dimage[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# tree_widget.update_selection_and_properties(last_image, last_qt_item)
print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本")
@ -872,7 +916,7 @@ class GUIManager:
traceback.print_exc()
return None
def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None):
"""创建3D视频播放屏幕 - 添加占位符纹理支持"""
try:
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
@ -895,7 +939,7 @@ class GUIManager:
size = float(size)
except (ValueError, TypeError):
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}")
size = 0.2
size = 0.2*5
print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}")
@ -944,7 +988,9 @@ class GUIManager:
video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}")
video_screen.setTag("is_gui_element", "1")
video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1")
video_screen.setTag("name",screen_name)
# 设置视频路径标签
if video_path and os.path.exists(video_path):
@ -1042,12 +1088,12 @@ class GUIManager:
return None
# 选中最后创建的视频屏幕
if created_videoscreens:
last_screen_np, last_qt_item = created_videoscreens[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
# if created_videoscreens:
# last_screen_np, last_qt_item = created_videoscreens[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕")
@ -1125,6 +1171,7 @@ class GUIManager:
# 检查是否有播放方法
if hasattr(movie_texture, 'play'):
try:
self.loadVideoFile(video_screen, video_screen.getTag("video_path"))
movie_texture.play()
print(f"▶️ 继续播放视频: {video_screen.getName()}")
return True
@ -1135,9 +1182,7 @@ class GUIManager:
print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}")
return False
else:
print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}")
self._debugVideoScreenTextures(video_screen)
return False
self.loadVideoFile(video_screen,video_screen.getTag("video_path"))
except Exception as e:
print(f"❌ 播放视频失败: {e}")
import traceback
@ -1264,7 +1309,6 @@ class GUIManager:
def loadVideoFile(self, video_screen, video_path):
"""为视频屏幕加载新的视频文件"""
try:
from panda3d.core import Texture, TextureStage
import os
if not os.path.exists(video_path):
@ -1422,8 +1466,6 @@ class GUIManager:
print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}")
size = 0.2
print(f"📺 开始创建2D视频屏幕位置: {pos}, 尺寸: {size}, 视频路径: {video_path}")
# 获取树形控件
tree_widget = self._get_tree_widget()
if not tree_widget:
@ -1450,7 +1492,7 @@ class GUIManager:
frameColor=(1, 1, 1, 1), # 默认背景色
pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标
parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d,
suppressMouse=True
suppressMouse=True,
)
video_screen.setName(screen_name)
@ -1458,19 +1500,18 @@ class GUIManager:
# 设置透明度支持
video_screen.setTransparency(TransparencyAttrib.MAlpha)
# 设置2D视频屏幕特有的标签
#设置2D视频屏幕特有的标签
video_screen.setTag("gui_type", "2d_video_screen")
video_screen.setTag("gui_id", f"2d_video_screen_{len(self.gui_elements)}")
video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}")
video_screen.setTag("is_gui_element", "1")
video_screen.setTag("is_scene_element", "1")
video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN")
video_screen.setTag("created_by_user", "1")
video_screen.setTag("name",screen_name)
# 设置视频路径标签
if video_path and os.path.exists(video_path):
video_screen.setTag("video_path", video_path)
else:
video_screen.setTag("video_path", "")
video_screen.setTag("video_path", video_path if video_path else "")
print(f"🔧 设置2D视频屏幕标签 - video_path: {video_path if video_path else ''}")
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
@ -1543,12 +1584,12 @@ class GUIManager:
return None
# 选中最后创建的视频屏幕
if created_videoscreens:
last_screen_np, last_qt_item = created_videoscreens[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
# if created_videoscreens:
# last_screen_np, last_qt_item = created_videoscreens[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕")
@ -1569,7 +1610,11 @@ class GUIManager:
try:
import os
if not os.path.exists(video_path):
video_screen.setTag("video_path",video_path)
path = video_screen.getTag("video_path")
print(f"🔧 更新2D视频屏幕标签 - video_path: {path}")
if not video_path or not os.path.exists(video_path):
print(f"❌ 2D视频文件不存在: {video_path}")
return False
@ -1581,7 +1626,6 @@ class GUIManager:
# 保存视频纹理引用
video_screen.setPythonTag("movie_texture", movie_texture)
video_screen.setTag("video_path", video_path)
print(f"✅ 成功加载新2D视频: {video_path}")
return True
@ -1776,6 +1820,7 @@ class GUIManager:
# 设置标签以便识别和管理
sphere_np.setTag("gui_type", "spherical_video")
sphere_np.setTag("is_gui_element", "1")
sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO")
sphere_np.setTag("video_path", video_path or "")
sphere_np.setTag("original_radius", str(radius))
@ -1808,12 +1853,12 @@ class GUIManager:
return None
# 选中最后创建的球形视频
if created_spherical_videos:
last_sphere_np, last_qt_item = created_spherical_videos[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
# if created_spherical_videos:
# last_sphere_np, last_qt_item = created_spherical_videos[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频")
@ -1988,6 +2033,7 @@ class GUIManager:
virtual_screen.setTag("gui_text", text)
virtual_screen.setTag("is_gui_element", "1")
virtual_screen.setTag("is_scene_element", "1")
virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen")
virtual_screen.setTag("created_by_user", "1")
# 添加到GUI元素列表
@ -2013,12 +2059,12 @@ class GUIManager:
return None
# 选中最后创建的虚拟屏幕
if created_screens:
last_screen_np, last_qt_item = created_screens[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
# if created_screens:
# last_screen_np, last_qt_item = created_screens[-1]
# if last_qt_item:
# tree_widget.setCurrentItem(last_qt_item)
# # 更新选择和属性面板
# tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕")
@ -2070,7 +2116,9 @@ class GUIManager:
pass
return None
# 暂无滑块功能
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
pass
"""创建2D GUI滑块"""
from direct.gui.DirectGui import DirectSlider
@ -2105,19 +2153,22 @@ class GUIManager:
"""删除GUI元素"""
try:
if gui_element in self.gui_elements:
# 移除GUI元素
if hasattr(gui_element, 'removeNode'):
gui_element.removeNode()
elif hasattr(gui_element, 'destroy'):
gui_element.destroy()
# 从列表中移除
self.gui_elements.remove(gui_element)
# # 移除GUI元素
# if hasattr(gui_element, 'removeNode'):
# gui_element.removeNode()
# elif hasattr(gui_element, 'destroy'):
# gui_element.destroy()
#
# # 从列表中移除
# self.gui_elements.remove(gui_element)
# 更新场景树
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
tree_widget = self._get_tree_widget()
if tree_widget:
tree_widget.delete_items(tree_widget.selectedItems())
# if hasattr(self.world, 'updateSceneTree'):
# self.world.updateSceneTree()
print(f"删除GUI元素: {gui_element}")
return True
@ -2910,7 +2961,7 @@ class GUIManager:
heightSpinBox.valueChanged.connect(
lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v))
transform_layout.addWidget(heightSpinBox, 4, 3)
else:
# 3D GUI组件使用世界坐标
transform_layout.addWidget(QLabel("位置"), 0, 0)
@ -2990,11 +3041,11 @@ class GUIManager:
transform_layout.addWidget(scale_z, 3, 3)
transform_group.setLayout(transform_layout)
self._propertyLayout.addWidget(transform_group)
# 缩放属性
if hasattr(gui_element, 'getScale'):
scale = gui_element.getScale()
scaleSpinBox = QDoubleSpinBox()
scaleSpinBox.setRange(0.01, 100)
scaleSpinBox.setSingleStep(0.1)
@ -3207,7 +3258,7 @@ class GUIManager:
try:
gui_type = gui_element.getTag("gui_type")
if gui_type in ["3d_text", "3d_image", "video_screen","info_panel"]:
if gui_type in ["3d_text", "3d_image", "video_screen","info_panel","info_panel_3d"]:
current_pos = gui_element.getPos()
if axis == "x":
@ -3220,7 +3271,7 @@ class GUIManager:
return False
gui_element.setPos(*new_pos)
print(f"✓ 更新3D GUI元素位置: {axis}={value}")
#print(f"✓ 更新3D GUI元素位置: {axis}={value}")
return True
else:
print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}")
@ -3241,7 +3292,7 @@ class GUIManager:
if value == 0:
value = 0.01
if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel"]:
if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel","info_panel_3d"]:
# 3D元素处理
if axis == "x":
new_scale = (value, current_scale.getY(), current_scale.getZ())

BIN
icons/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

133
main.py
View File

@ -22,6 +22,7 @@ from core.script_system import ScriptManager
from core.vr_manager import VRManager
from core.vr_input_handler import VRInputHandler
from core.alvr_streamer import ALVRStreamer
from core.patrol_system import PatrolSystem
from gui.gui_manager import GUIManager
from core.terrain_manager import TerrainManager
from scene.scene_manager import SceneManager
@ -55,7 +56,16 @@ class MyWorld(CoreWorld):
# 初始化选择和变换系统
self.selection = SelectionSystem(self)
# 绑定F键用于聚焦选中节点
self.accept("f", self.onFocusKeyPressed)
self.accept("F", self.onFocusKeyPressed) # 大写F
#初始化巡检系统
self.patrol_system = PatrolSystem(self)
self.accept("p",self.onPatrolKeyPressed)
self.accept("P",self.onPatrolKeyPressed)
# 初始化事件处理系统
self.event_handler = EventHandler(self)
@ -225,7 +235,7 @@ class MyWorld(CoreWorld):
"""创建2D GUI文本输入框"""
return self.gui_manager.createGUIEntry(pos, placeholder, size)
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1):
"""创建3D空间文本"""
return self.gui_manager.createGUI3DText(pos, text, size)
@ -233,9 +243,9 @@ class MyWorld(CoreWorld):
"""创建3D图片"""
return self.gui_manager.createGUI3DImage(pos,text,size)
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2):
def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=1):
"""创建2D GUI图片"""
return self.gui_manager.createGUI2DImage(pos, image_path, size)
return self.gui_manager.createGUI2DImage(pos, image_path, size*0.2)
def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None):
"""创建视频屏幕"""
@ -492,10 +502,6 @@ class MyWorld(CoreWorld):
"""异步导入模型"""
return self.scene_manager.importModelAsync(filepath)
def loadAnimatedModel(self, model_path, anims=None):
"""加载带动画的模型"""
return self.scene_manager.loadAnimatedModel(model_path, anims)
# 材质和几何体处理方法 - 代理到scene_manager
def processMaterials(self, model):
"""处理模型材质"""
@ -814,6 +820,97 @@ class MyWorld(CoreWorld):
"""获取碰撞统计"""
return self.collision_manager.getCollisionStatistics()
def setupKeyboardEvents(self):
"""设置键盘事件"""
try:
# 绑定 F 键用于聚焦选中节点
self.accept("f", self.onFocusKeyPressed)
self.accept("F", self.onFocusKeyPressed) # 大写F
print("✓ 键盘事件绑定完成")
except Exception as e:
print(f"设置键盘事件失败: {e}")
def onFocusKeyPressed(self):
"""处理 F 键按下事件"""
try:
#print("检测到 F 键按下")
# 检查是否有选中的节点
if hasattr(self, 'selection') and self.selection.selectedNode:
#print(f"当前选中节点: {self.selection.selectedNode.getName()}")
# 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本)
# self.selection.focusCameraOnSelectedNode() # 无动画版本
self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本
else:
print("当前没有选中任何节点")
except Exception as e:
print(f"处理 F 键事件失败: {e}")
def onPatrolKeyPressed(self):
"""处理 P 键按下事件 - 控制巡检系统"""
try:
print("检测到 P 键按下")
if not self.patrol_system.is_patrolling:
# 如果巡检系统没有点,创建默认巡检路线
if not self.patrol_system.patrol_points:
self.createDefaultPatrolRoute()
# 开始巡检
if self.patrol_system.start_patrol():
print("✓ 巡检已开始")
else:
print("✗ 巡检启动失败")
else:
# 停止巡检
if self.patrol_system.stop_patrol():
print("✓ 巡检已停止")
else:
print("✗ 巡检停止失败")
except Exception as e:
print(f"处理 P 键事件失败: {e}")
def createDefaultPatrolRoute(self):
"""创建默认巡检路线(使用自动朝向)"""
try:
# 清空现有巡检点
self.patrol_system.clear_patrol_points()
# 添加巡检点使用None表示朝向下一个点
self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5)
self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5)
self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5)
self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5)
# 最后一个点可以指定特定的朝向或者也设为None继续循环
self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5)
print("✓ 默认自动朝向巡检路线已创建")
self.patrol_system.list_patrol_points()
except Exception as e:
print(f"创建默认自动朝向巡检路线失败: {e}")
def _serializeNode(self, node):
"""序列化节点数据"""
try:
return self.world.scene_manager.serializeNode(node)
except Exception as e:
print(f"序列化节点失败: {e}")
return None
def _deserializeNode(self, node_data, parent_node):
"""反序列化节点数据"""
try:
return self.world.scene_manager.deserializeNode(node_data, parent_node)
except Exception as e:
print(f"反序列化节点失败: {e}")
return None
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能
@ -830,6 +927,7 @@ def createNewProject(parent_window):
def saveProject(appw):
"""保存项目 - 代理到project_manager"""
world = appw.centralWidget().world
return world.project_manager.saveProject(appw)
@ -838,19 +936,26 @@ def openProject(appw):
world = appw.centralWidget().world
return world.project_manager.openProject(appw)
def openProjectForPath(project_path, appw):
"""打开项目 - 代理到project_manager"""
world = appw.centralWidget().world
return world.project_manager.openProjectForPath(project_path, appw)
def buildPackage(appw):
"""打包项目 - 代理到project_manager"""
world = appw.centralWidget().world
return world.project_manager.buildPackage(appw)
if __name__ == "__main__":
def run(args = None):
world = MyWorld()
# 使用新的UI模块创建主窗口
from ui.main_window import setup_main_window
app, main_window = setup_main_window(world)
print(f'Path is {args}')
app, main_window = setup_main_window(world, args)
# 启动应用程序
sys.exit(app.exec_())
sys.exit(app.exec_())
if __name__ == "__main__":
run()

View File

@ -9,7 +9,6 @@
import os
import sys
import json
import re
import datetime
import subprocess
import shutil
@ -47,7 +46,9 @@ class ProjectManager:
project_path = dialog.projectPath
project_name = dialog.projectName
full_project_path = os.path.join(project_path, project_name)
# full_project_path = os.path.join(project_path, project_name)
full_project_path = os.path.normpath(os.path.join(project_path, project_name))
print(f"full_project_path: {full_project_path}")
try:
# 创建项目文件夹结构
@ -72,13 +73,13 @@ class ProjectManager:
json.dump(project_config, f, ensure_ascii=False, indent=4)
print(f"项目配置文件已创建: {config_file}")
# 清空当前场景
self._clearCurrentScene()
# 自动保存初始场景
scene_file = os.path.join(scenes_path, "scene.bam")
if self.world.scene_manager.saveScene(scene_file):
if self.world.scene_manager.saveScene(scene_file, project_path):
# 更新配置文件中的场景路径
project_config["scene_file"] = os.path.relpath(scene_file, full_project_path)
with open(config_file, "w", encoding="utf-8") as f:
@ -122,57 +123,157 @@ class ProjectManager:
if not project_path:
return False
# 检查是否是有效的项目文件夹
config_file = os.path.join(project_path, "project.json")
if not os.path.exists(config_file):
QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!")
return False
# 读取项目配置
with open(config_file, "r", encoding="utf-8") as f:
project_config = json.load(f)
# 检查场景文件
scene_file = os.path.join(project_path, "scenes", "scene.bam")
if not os.path.exists(scene_file):
QMessageBox.warning(parent_window, "警告", "没有找到场景文件!")
return False
# 加载场景
if self.world.scene_manager.loadScene(scene_file):
# 更新项目配置
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
# 更新项目状态
self.current_project_path = project_path
self.project_config = project_config
# 保存当前项目路径到主窗口
parent_window.current_project_path = project_path
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
# 更新文件浏览器
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
QMessageBox.information(parent_window, "成功", "项目加载成功!")
return True
else:
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
return False
if os.path.exists(scene_file):
# 加载场景
if self.world.scene_manager.loadScene(scene_file):
# 更新项目配置
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
# 更新项目状态
self.current_project_path = project_path
self.project_config = project_config
# 保存当前项目路径到主窗口
parent_window.current_project_path = project_path
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
# 更新文件浏览器
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
QMessageBox.information(parent_window, "成功", "项目加载成功!")
return True
# 检查场景文件
# scene_file = os.path.join(project_path, "scenes", "scene.bam")
# if not os.path.exists(scene_file):
# QMessageBox.warning(parent_window, "警告", "没有找到场景文件!")
# return False
#
# # 加载场景
# if self.world.scene_manager.loadScene(scene_file):
# # 更新项目配置
# project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# project_config["scene_file"] = os.path.relpath(scene_file, project_path)
#
# with open(config_file, "w", encoding="utf-8") as f:
# json.dump(project_config, f, ensure_ascii=False, indent=4)
#
# # 更新项目状态
# self.current_project_path = project_path
# self.project_config = project_config
#
# # 保存当前项目路径到主窗口
# parent_window.current_project_path = project_path
#
# # 更新窗口标题
# project_name = os.path.basename(project_path)
# self.updateWindowTitle(parent_window, project_name)
#
# # 更新文件浏览器
# if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
# parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
#
# QMessageBox.information(parent_window, "成功", "项目加载成功!")
# return True
# else:
# QMessageBox.warning(parent_window, "错误", "加载场景失败!")
# return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}")
return False
def openProjectForPath(self, project_path, parent_window=None):
"""通过路径打开项目
Args:
project_path: 项目路径
parent_window: 父窗口对象可选
"""
try:
if not project_path:
return False
# 检查是否是有效的项目文件夹
config_file = os.path.join(project_path, "project.json")
if not os.path.exists(config_file):
if parent_window:
QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}")
else:
print("警告: 选择的不是有效的项目文件夹!")
return False
# 读取项目配置
with open(config_file, "r", encoding="utf-8") as f:
project_config = json.load(f)
# 检查场景文件
scene_file = os.path.join(project_path, "scenes", "scene.bam")
if os.path.exists(scene_file):
# 加载场景
if self.world.scene_manager.loadScene(scene_file):
# 更新项目配置
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
# 更新项目状态
self.current_project_path = project_path
self.project_config = project_config
# 如果有父窗口更新相关UI元素
if parent_window:
# 保存当前项目路径到主窗口
parent_window.current_project_path = project_path
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
# 更新文件浏览器
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
QMessageBox.information(parent_window, "成功", "项目加载成功!")
print(f"项目 '{project_path}' 加载成功!")
return True
else:
if parent_window:
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
else:
print("错误: 加载场景失败!")
return False
except Exception as e:
error_msg = f"加载项目时发生错误:{str(e)}"
if parent_window:
QMessageBox.critical(parent_window, "错误", error_msg)
else:
print(error_msg)
return False
def saveProject(self, parent_window):
"""保存项目"""
try:
@ -201,7 +302,7 @@ class ProjectManager:
return False
# 保存场景
if self.world.scene_manager.saveScene(scene_file):
if self.world.scene_manager.saveScene(scene_file, project_path):
# 更新项目配置文件
config_file = os.path.join(project_path, "project.json")
if os.path.exists(config_file):

File diff suppressed because it is too large Load Diff

View File

@ -25,9 +25,9 @@ class CrossPlatformPathHandler:
def normalize_model_path(self, filepath):
"""标准化模型文件路径"""
try:
print(f"\n=== 路径标准化处理 ===")
print(f"原始路径: {filepath}")
print(f"当前系统: {self.system}")
#print(f"\n=== 路径标准化处理 ===")
#print(f"原始路径: {filepath}")
#print(f"当前系统: {self.system}")
# 步骤1: 检查原始路径是否存在
if self._check_file_exists(filepath):
@ -54,10 +54,6 @@ class CrossPlatformPathHandler:
def _check_file_exists(self, filepath):
"""检查文件是否存在"""
exists = os.path.exists(filepath)
if exists:
print(f"✓ 文件存在: {filepath}")
else:
print(f"⚠️ 文件不存在: {filepath}")
return exists
def _panda3d_normalize(self, filepath):

View File

@ -21,6 +21,10 @@ class RotatorScript(ScriptBase):
self.log(f"旋转速度: {self.rotation_speed_y}度/秒")
def update(self, dt):
# 检查 gameObject 是否存在且不为空
if not self.gameObject or self.gameObject.isEmpty():
print("RotatorScript: gameObject is empty or None, skipping update")
return
"""每帧更新"""
if not self.is_rotating:
return

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

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.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
from PyQt5.sip import wrapinstance
from panda3d.core import ModelRoot
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import ModelRoot, NodePath, CollisionNode
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from scene import util
@ -29,7 +30,67 @@ class NewProjectDialog(QDialog):
super().__init__(parent)
self.setWindowTitle("新建项目")
self.setMinimumWidth(500)
# 设置对话框样式与主窗口保持一致
self.setStyleSheet("""
QDialog {
background-color: #252538;
color: #e0e0ff;
}
QGroupBox {
background-color: #2d2d44;
border: 1px solid #3a3a4a;
border-radius: 6px;
margin-top: 1ex; /* 保持这个设置 */
color: #e0e0ff;
font-weight: 500;
padding-top: 10px; /* 增加顶部内边距为标题留出空间 */
}
QGroupBox::title {
padding: 0 8px;
color: #c0c0e0;
font-weight: 500;
}
QLineEdit {
background-color: #2d2d44;
color: #e0e0ff;
border: 1px solid #3a3a4a;
border-radius: 4px;
padding: 6px;
}
QLineEdit:disabled {
background-color: #1e1e2e;
color: #8888aa;
}
QPushButton {
background-color: #8b5cf6;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
}
QPushButton:hover {
background-color: #7c3aed;
}
QPushButton:pressed {
background-color: #6d28d9;
}
QPushButton:disabled {
background-color: #4c4c6e;
color: #8888aa;
}
QLabel {
color: #e0e0ff;
}
QLabel:disabled {
color: #8888aa;
}
QDialogButtonBox QPushButton {
min-width: 80px;
}
""")
# 创建布局
layout = QVBoxLayout(self)
@ -155,7 +216,7 @@ class CustomPanda3DWidget(QPanda3DWidget):
event.acceptProposedAction()
else:
event.ignore()
def wheelEvent(self, event):
"""处理滚轮事件"""
if event.angleDelta().y() > 0:
@ -606,8 +667,27 @@ class CustomAssetsTreeWidget(QTreeWidget):
"""获取项目根路径下的Resources文件夹考虑跨平台"""
import os
# 获取项目根路径
project_root = os.getcwd()
# 获取当前文件所在目录,然后向上查找项目根目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上查找直到找到项目根目录(包含特定标识文件或文件夹)
project_root = current_dir
max_depth = 10 # 限制向上查找的深度
depth = 0
while depth < max_depth:
# 检查是否是项目根目录(可以根据实际情况调整判断条件)
if (os.path.exists(os.path.join(project_root, "main.py")) or
os.path.exists(os.path.join(project_root, "setup.py")) or
os.path.exists(os.path.join(project_root, ".git"))):
break
parent_dir = os.path.dirname(project_root)
if parent_dir == project_root: # 已经到达文件系统根目录
# 回退到使用当前工作目录
project_root = os.getcwd()
break
project_root = parent_dir
depth += 1
# 构建Resources文件夹路径跨平台
resources_path = os.path.join(project_root, "Resources")
@ -624,6 +704,28 @@ class CustomAssetsTreeWidget(QTreeWidget):
return resources_path
# def getProjectRootPath(self):
# """获取项目根路径下的Resources文件夹考虑跨平台"""
# import os
#
# # 获取项目根路径
# project_root = os.getcwd()
#
# # 构建Resources文件夹路径跨平台
# resources_path = os.path.join(project_root, "Resources")
#
# # 如果Resources文件夹不存在创建它
# if not os.path.exists(resources_path):
# try:
# os.makedirs(resources_path, exist_ok=True)
# print(f"创建Resources文件夹: {resources_path}")
# except OSError as e:
# print(f"无法创建Resources文件夹: {e}")
# # 如果无法创建,回退到项目根路径
# return project_root
#
# return resources_path
def load_file_tree(self):
"""加载树形视图"""
self.clear()
@ -654,7 +756,7 @@ class CustomAssetsTreeWidget(QTreeWidget):
if os.path.exists(directory) and directory not in self.watched_directories:
if self.file_watcher.addPath(directory):
self.watched_directories.add(directory)
print(f"开始监控目录:{directory}")
#print(f"开始监控目录:{directory}")
try:
for item in os.listdir(directory):
item_path = os.path.join(directory,item)
@ -1002,7 +1104,6 @@ class CustomAssetsTreeWidget(QTreeWidget):
internal_paths.append(filepath)
# 检查是否是模型文件(用于向外拖拽)
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
print(f"模型路ing{QUrl.fromLocalFile(filepath)}")
urls.append(QUrl.fromLocalFile(filepath))
# 设置内部拖拽数据
@ -1210,14 +1311,83 @@ class CustomConsoleDockWidget(QWidget):
self.autoScrollBtn = QPushButton("自动滚动")
self.autoScrollBtn.setCheckable(True)
self.autoScrollBtn.setChecked(True)
self.autoScrollBtn.setStyleSheet("""
QPushButton {
background-color: #2d2d44;
color: #e0e0ff;
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
}
QPushButton:checked {
background-color: #8b5cf6;
color: white;
border: 1px solid #7c3aed;
}
QPushButton:hover {
background-color: #3a3a4a;
}
QPushButton:checked:hover {
background-color: #7c3aed;
}
QPushButton:pressed {
background-color: #6d28d9;
}
""")
toolbar.addWidget(self.autoScrollBtn)
# 时间戳开关
self.timestampBtn = QPushButton("显示时间")
self.timestampBtn.setCheckable(True)
self.timestampBtn.setChecked(True)
self.timestampBtn.setStyleSheet("""
QPushButton {
background-color: #2d2d44;
color: #e0e0ff;
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
}
QPushButton:checked {
background-color: #8b5cf6;
color: white;
border: 1px solid #7c3aed;
}
QPushButton:hover {
background-color: #3a3a4a;
}
QPushButton:checked:hover {
background-color: #7c3aed;
}
QPushButton:pressed {
background-color: #6d28d9;
}
""")
toolbar.addWidget(self.timestampBtn)
self.fpsLabel = QLabel("FPS:0.0")
self.fpsLabel.setStyleSheet("""
QLabel {
background-color: #2d2d44;
color: #80ff80;
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
font-family: 'Consolas', 'Monaco', monospace;
}
""")
self.fpsLabel.setMinimumWidth(100)
self.fpsLabel.setAlignment(Qt.AlignCenter)
toolbar.addWidget(self.fpsLabel)
# 帧率更新定时器
self.fpsTimer = QTimer()
self.fpsTimer.timeout.connect(self.updateFPS)
self.fpsTimer.start(1000) # 每秒更新一次
toolbar.addStretch()
layout.addLayout(toolbar)
@ -1259,6 +1429,34 @@ class CustomConsoleDockWidget(QWidget):
# 添加欢迎信息
self.addMessage("🎮 编辑器控制台已启动", "INFO")
def updateFPS(self):
try:
if hasattr(self.world,'clock'):
fps = self.world.clock.getAverageFrameRate()
self.fpsLabel.setText(f"FPS:{fps:.1f}")
# 根据帧率设置颜色
if fps >= 50:
color = "#80ff80" # 绿色 - 优秀
elif fps >= 30:
color = "#ffff80" # 黄色 - 一般
else:
color = "#ff8080" # 红色 - 较差
self.fpsLabel.setStyleSheet(f"""
QLabel {{
background-color: #2d2d44;
color: {color};
border: 1px solid #3a3a4a;
padding: 6px 12px;
border-radius: 4px;
font-weight: 500;
font-family: 'Consolas', 'Monaco', monospace;
}}
""")
except Exception as e:
pass # 静默处理错误,避免影响控制台功能
def setupConsoleRedirect(self):
"""设置控制台重定向"""
import sys
@ -1369,42 +1567,66 @@ class CustomTreeWidget(QTreeWidget):
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
# self.selectedItems = None
self.initData()
self.setupUI() # 初始化界面
self.setupContextMenu() # 初始化右键菜单
self.setupDragDrop() # 设置拖拽功能
self.original_scales={}
self.setStyleSheet("""
/* 设置折叠状态下带子节点的箭头颜色 */
QTreeWidget::branch:has-children:!open {
color: #8b5cf6; /* 紫色 */
}
/* 设置展开状态下带子节点的箭头颜色 */
QTreeWidget::branch:has-children:open {
color: #9ca3af; /* 灰色,提供状态变化反馈 */
}
/* 鼠标悬停在任意箭头上时颜色变亮 */
QTreeWidget::branch:hover {
color: #a78bfa; /* 亮紫色 */
}
""")
def initData(self):
"""初始化变量"""
# 定义2D GUI元素类型
self.gui_2d_types = {
"GUI_BUTTON", # DirectButton
"GUI_LABEL", # DirectLabel
"GUI_ENTRY", # DirectEntry
"GUI_IMAGE",
"GUI_BUTTON", # GUI 按钮
"GUI_LABEL", # GUI 标签
"GUI_ENTRY", # GUI 输入框
"GUI_IMAGE", # GUI 图片
"GUI_2D_VIDEO_SCREEN", # GUI 2D视频
"GUI_SPHERICAL_VIDEO", # GUI 3D球形视频
"GUI_NODE" # 其他2D GUI容器
}
# 定义3D GUI元素类型
self.gui_3d_types = {
"GUI_3DTEXT", # 3D TextNode
"GUI_3DIMAGE",
"GUI_VIRTUAL_SCREEN" # Virtual Screen
"GUI_3DTEXT", # 3D 文本节点
"GUI_3DIMAGE", # 3D 图片节点
"GUI_VIRTUAL_SCREEN", # 3D视频
"GUI_VirtualScreen" # 3D虚拟视频
}
# 定义3D场景节点类型可以接受3D GUI元素和其他3D场景元素
self.scene_3d_types = {
"SCENE_ROOT",
"SCENE_NODE",
"LIGHT_NODE",
"LIGHT_NODE", # 灯节点
"CAMERA_NODE",
"IMPORTED_MODEL_NODE",
"MODEL_NODE"
"IMPORTED_MODEL_NODE", # 导入模型节点
"MODEL_NODE",
"TERRAIN_NODE", # 地形节点
"CESIUM_TILESET_NODE" # 3D Tileset
}
# 这是一个最佳实践,它让代码的意图变得非常清晰。
self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types)
def setupUI(self):
"""初始化UI设置"""
self.setHeaderHidden(True)
@ -1427,12 +1649,9 @@ class CustomTreeWidget(QTreeWidget):
def showContextMenu(self, position):
"""显示右键菜单 - 复用主菜单的创建动作"""
if not self.selectedItems():
print("没有选中的项目,不显示右键菜单")
return
item = self.selectedItems()[0]
print(f"为项目 '{item.text(0)}' 显示右键菜单")
if self.selectedItems():
item = self.selectedItems()[0]
print(f"为项目 '{item.text(0)}' 显示右键菜单")
# 创建右键菜单
menu = QMenu(self)
@ -1682,7 +1901,9 @@ class CustomTreeWidget(QTreeWidget):
# 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下
if is_dragged_2d_gui:
if is_target_2d_gui:
if target_type == "SCENE_ROOT":
return True
elif is_target_2d_gui:
print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}")
return True
elif is_target_3d_gui:
@ -1701,7 +1922,7 @@ class CustomTreeWidget(QTreeWidget):
elif is_dragged_3d_gui:
if is_target_3d_scene:
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
return True
return False
elif is_target_2d_gui:
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
@ -1727,7 +1948,7 @@ class CustomTreeWidget(QTreeWidget):
elif is_target_3d_gui:
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)}")
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
return True
return False
else:
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)}")
return False
@ -1773,6 +1994,20 @@ class CustomTreeWidget(QTreeWidget):
def dragMoveEvent(self, event):
"""处理拖动事件"""
indicator_pos = self.dropIndicatorPosition()
indicator_str = "Unknown"
if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem:
indicator_str = "OnItem"
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem:
indicator_str = "AboveItem"
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem:
indicator_str = "BelowItem"
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport:
indicator_str = "OnViewport"
#print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
if event.source() != self:
event.ignore()
return
@ -1811,6 +2046,8 @@ class CustomTreeWidget(QTreeWidget):
selected_items = self.selectedItems()
if selected_items:
# 执行删除操作
# if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE":
# self._preprocess_light_items_for_deletion(selected_items)
self.delete_items(selected_items)
else:
# 没有选中任何项目,执行默认操作
@ -1818,12 +2055,58 @@ class CustomTreeWidget(QTreeWidget):
else:
super().keyPressEvent(event)
def _preprocess_light_items_for_deletion(self, selected_items):
"""预处理灯光节点删除,特别处理最后一个灯光节点的问题"""
if not selected_items:
return selected_items
# 检查选中的项目中是否包含灯光节点
light_items = []
for item in selected_items:
node_type = item.data(0, Qt.UserRole + 1)
if node_type == "LIGHT_NODE":
light_items.append(item)
# 如果没有灯光节点,直接返回
if not light_items:
return selected_items
# 检查是否只有最后一个灯光节点被选中
processed_items = list(selected_items) # 创建副本
for item in light_items:
panda_node = item.data(0, Qt.UserRole)
if not panda_node:
continue
# 获取灯光类型
if hasattr(panda_node, 'getTag'):
light_type = panda_node.getTag("light_type")
# 检查是否是最后一个Spotlight
if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and
self.world.Spotlight and self.world.Spotlight[-1] == panda_node and
len(self.world.Spotlight) > 1):
print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}")
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
# 检查是否是最后一个Pointlight
elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and
self.world.Pointlight and self.world.Pointlight[-1] == panda_node and
len(self.world.Pointlight) > 1):
print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}")
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
return processed_items
def delete_items(self, selected_items):
"""删除选中的item - 简化版本"""
if not selected_items:
return
# 过滤掉不能删除的节点
# 1. 过滤掉不能删除的节点
deletable_items = []
for item in selected_items:
node_type = item.data(0, Qt.UserRole + 1)
@ -1839,7 +2122,7 @@ class CustomTreeWidget(QTreeWidget):
QMessageBox.information(self, "提示", "没有可删除的节点")
return
# 确认删除
# 2. 确认删除
item_count = len(deletable_items)
if item_count == 1:
message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?"
@ -1854,11 +2137,19 @@ class CustomTreeWidget(QTreeWidget):
if reply != QMessageBox.Yes:
return
# 执行删除
# 默认选中场景根节点,通常是第一个顶级节点
#next_item_to_select = self.topLevelItem(0)
# 3. 执行删除循环
deleted_count = 0
for item in deletable_items:
try:
# 在删除前,记录其父节点,作为删除后的新选择
# 选择最后一个被删除项的父节点作为新的焦点
if item.parent():
next_item_to_select = item.parent()
panda_node = item.data(0, Qt.UserRole)
if panda_node:
# 清理选择状态
if (hasattr(self.world, 'selection') and
@ -1874,38 +2165,32 @@ class CustomTreeWidget(QTreeWidget):
if hasattr(panda_node, 'getPythonTag'):
light_object = panda_node.getPythonTag('rp_light_object')
if light_object and hasattr(self.world, 'render_pipeline'):
try:
self.world.render_pipeline.remove_light(light_object)
print(f"移除灯光{panda_node.getName()}")
except Exception as e:
print(f"移除灯光失败: {str(e)}")
panda_node.clearPythonTag('rp_light_object')
#self.world.render_pipeline.remove_light(light_object)
if hasattr(self.world,'gui_manager') and hasattr(self.world.gui_manager,'gui_elements'):
if panda_node in self.world.gui_manager.gui_elements:
self.world.gui_manager.gui_elements.remove(panda_node)
print(f"从gui_elements列表中移除{panda_node.getName()}")
print(f'11111111111111111111111111,{light_object.casts_shadows}')
self.world.render_pipeline.remove_light(light_object)
# 从world列表中移除
if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements:
self.world.gui_elements.remove(panda_node)
if hasattr(self.world, 'models') and panda_node in self.world.models:
self.world.models.remove(panda_node)
if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
self.world.Spotlight.remove(panda_node)
if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
self.world.Pointlight.remove(panda_node)
if hasattr(self.world, 'terrains') and panda_node in self.world.terrains:
self.world.terrains.remove(panda_node)
if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets:
# self.world.tilesets.remove(panda_node)
# 从 tilesets 列表中移除
if hasattr(self.world, 'scene_manager'):
tilesets_to_remove = []
for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
if tileset_info['node'] == panda_node:
tilesets_to_remove.append(i)
# if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
# self.world.Spotlight.remove(panda_node)
if hasattr(self.world,'Spotlight'):
self.world.Spotlight = [light for light in self.world.Spotlight if light != panda_node]
if panda_node in self.world.Spotlight:
print(f"从Spotlight列表中移除{panda_node.getName()}")
# if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
# self.world.Pointlight.remove(panda_node)
if hasattr(self.world,'Pointlight'):
self.world.Pointlight = [light for light in self.world.Pointlight if light != panda_node]
if panda_node in self.world.Pointlight:
print(f"从Pointlight列表中移除{panda_node.getName()}")
# 从后往前删除,避免索引问题
for i in reversed(tilesets_to_remove):
del self.world.scene_manager.tilesets[i]
# 从Panda3D场景中移除
try:
@ -1935,7 +2220,210 @@ class CustomTreeWidget(QTreeWidget):
# if hasattr(self.world, 'property_panel'):
# self.world.property_panel.clearPropertyPanel()
print(f"✅ 已删除 {deleted_count} 个节点")
# 4. 删除操作完成后更新UI ---
if deleted_count > 0:
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
self.update_selection_and_properties(None, None)
def delete_item(self, panda_node):
"""删除指定节点 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):
@ -1966,52 +2454,79 @@ class CustomTreeWidget(QTreeWidget):
def add_node_to_tree_widget(self, node, parent_item, node_type):
"""将node元素添加到树形控件"""
if hasattr(node, 'getTag'):
if node.hasTag('tree_item_type'):
print(f"node0: {node.getName()},{node.getTag('tree_item_type')}")
tree_type = node.getTag('tree_item_type')
else:
node.setTag('tree_item_type', node_type)
else:
print(f"node2: {node.getName()},{node_type}")
tree_type = node_type
# BLACK_LIST 和依赖项导入保持不变
BLACK_LIST = {'', '**', 'temp', 'collision'}
from panda3d.core import CollisionNode, ModelRoot
from PyQt5.QtWidgets import QTreeWidgetItem
from PyQt5.QtCore import Qt
from panda3d.core import CollisionNode
# 1. 修改内部函数,让它返回创建的节点
def addNodeToTree(node, parentItem, force=False):
"""内部递归函数,现在会返回创建的顶级节点项"""
if not force and should_skip(node):
return None # 如果跳过返回None
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
nodeItem.setData(0, Qt.UserRole, node)
nodeItem.setData(0, Qt.UserRole + 1, tree_type)
for child in node.getChildren():
# 递归调用但我们只关心顶级的nodeItem
addNodeToTree(child, nodeItem, force=False)
return nodeItem # <-- 新增返回创建的QTreeWidgetItem
def should_skip(node):
name = node.getName()
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance(
node.node(), ModelRoot) or name == ""
def addNodeToTree(node, parentItem, force=False):
if not force and should_skip(node):
return None
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
nodeItem.setData(0, Qt.UserRole, node)
nodeItem.setData(0, Qt.UserRole + 1, node_type)
for child in node.getChildren():
addNodeToTree(child, nodeItem, force=False)
return nodeItem
# 使用一个变量来确保无论哪个分支都有返回值
new_qt_item = None
node_name = ""
try:
from PyQt5.QtWidgets import QTreeWidgetItem
from PyQt5.QtCore import Qt
if tree_type == "IMPORTED_MODEL_NODE":
# getTag('file') 可能是你自己设置的tag这里假设它存在
node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName()
# 初始化new_qt_item变量
new_qt_item = None
if node_type == "IMPORTED_MODEL_NODE":
node_name = node.getTag("file") if hasattr(node, 'getTag') else "model"
# 2. 接收 addNodeToTree 的返回值
new_qt_item = addNodeToTree(node, parent_item, force=True)
else:
node_name = node.getName() if hasattr(node, 'getName') else "node"
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
new_qt_item.setData(0, Qt.UserRole, node)
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
new_qt_item.setData(0, Qt.UserRole + 1, tree_type)
# 展开父节点
if hasattr(parent_item, 'setExpanded'):
parent_item.setExpanded(True)
# 确保 new_qt_item 成功创建后再继续操作
if new_qt_item:
# 展开父节点
if hasattr(parent_item, 'setExpanded'):
parent_item.setExpanded(True)
print(f"✅ Qt树节点添加成功: {node_name}")
return new_qt_item
print(f"✅ Qt树节点添加成功: {node_name}")
return new_qt_item
else:
# 如果 addNodeToTree 因为 should_skip 返回了 None
print(f" 节点 {node_name} 被跳过,未添加到树中。")
return None
except Exception as e:
import traceback
print(f"❌ 添加node到树形控件失败: {str(e)}")
traceback.print_exc() # 打印更详细的错误堆栈,方便调试
return None
def update_selection_and_properties(self, node, qt_item):
@ -2085,7 +2600,7 @@ class CustomTreeWidget(QTreeWidget):
node_type = item.data(0, Qt.UserRole + 1)
# 场景根节点和普通场景节点可以作为父节点
if node_type in self.gui_3d_types and self.scene_3d_types:
if node_type in self.valid_3d_parent_types:
return True
# # 模型节点也可以作为父节点