EG/ui/panels/editor_panels.py

2443 lines
111 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from imgui_bundle import imgui, imgui_ctx
import os
import io
import tempfile
import time
from pathlib import Path
from panda3d.core import Filename
try:
from PIL import Image
except Exception: # pragma: no cover - pillow may be missing in minimal env
Image = None
class EditorPanels:
"""Centralized UI panel renderer for top-level editor panels."""
def __init__(self, app):
self.app = app
def __getattr__(self, name):
return getattr(self.app, name)
def __setattr__(self, name, value):
if name == "app" or name in self.__dict__ or hasattr(type(self), name):
object.__setattr__(self, name, value)
else:
setattr(self.app, name, value)
def _on_create_web_panel(self):
"""创建或激活 ImGui Web 面板。"""
self._ensure_web_panel_state()
self.app.showWebPanel = True
webview = getattr(self.app, "_imgui_webview", None)
if webview and getattr(webview, "_running", False):
return True
return self._start_imgui_webview(self.app.web_panel_url_input)
def _ensure_web_panel_state(self):
if not hasattr(self.app, "web_panel_url_input") or not self.app.web_panel_url_input:
self.app.web_panel_url_input = "https://www.example.com"
if not hasattr(self.app, "_imgui_webview"):
self.app._imgui_webview = None
if not hasattr(self.app, "_imgui_webview_tex_id"):
self.app._imgui_webview_tex_id = None
if not hasattr(self.app, "_imgui_webview_texture_path"):
self.app._imgui_webview_texture_path = None
def _start_imgui_webview(self, url):
self._ensure_web_panel_state()
self._stop_imgui_webview()
try:
target_url = (url or "").strip()
if not target_url:
target_url = "https://www.example.com"
if not target_url.startswith(("http://", "https://", "file://")):
target_url = "https://" + target_url
from core.imgui_webview import ImGuiWebView
webview = ImGuiWebView(width=1280, height=720)
webview.start(target_url)
self.app._imgui_webview = webview
return True
except Exception as e:
self.app.add_error_message(f"启动Web视图失败: {e}")
return False
def _stop_imgui_webview(self):
webview = getattr(self.app, "_imgui_webview", None)
if webview:
try:
webview.stop()
except Exception:
pass
self.app._imgui_webview = None
tex_id = getattr(self.app, "_imgui_webview_tex_id", None)
if tex_id:
try:
self.app.imgui.removeTexture(tex_id)
except Exception:
pass
self.app._imgui_webview_tex_id = None
texture_path = getattr(self.app, "_imgui_webview_texture_path", None)
if texture_path:
try:
Path(texture_path).unlink(missing_ok=True)
except Exception:
pass
self.app._imgui_webview_texture_path = None
def _navigate_web_panel(self):
self._ensure_web_panel_state()
url = (self.app.web_panel_url_input or "").strip()
if not url:
return
webview = getattr(self.app, "_imgui_webview", None)
if not webview or not getattr(webview, "_running", False):
self._start_imgui_webview(url)
return
webview.navigate(url)
def _update_web_panel_texture(self):
webview = getattr(self.app, "_imgui_webview", None)
if not webview:
return
if not webview.tex_dirty:
return
jpeg_bytes = webview.get_screenshot_bytes()
if not jpeg_bytes:
return
try:
if Image is None:
self.app.add_warning_message("缺少 Pillow无法更新Web纹理")
return
img = Image.open(io.BytesIO(jpeg_bytes)).convert("RGBA")
# p3dimgui 纹理坐标系与网页截图存在Y轴方向差异先在像素层修正
img = img.transpose(Image.FLIP_TOP_BOTTOM)
temp_dir = Path(tempfile.gettempdir()) / "eg_imgui_webpanel"
temp_dir.mkdir(parents=True, exist_ok=True)
temp_path = temp_dir / f"webview_{time.time_ns()}.png"
img.save(temp_path, format="PNG")
panda_path = Filename.fromOsSpecific(str(temp_path)).getFullpath()
new_tex_id = self.app.imgui.loadTexture(panda_path)
old_tex_id = getattr(self.app, "_imgui_webview_tex_id", None)
if old_tex_id:
try:
self.app.imgui.removeTexture(old_tex_id)
except Exception:
pass
old_texture_path = getattr(self.app, "_imgui_webview_texture_path", None)
if old_texture_path:
try:
Path(old_texture_path).unlink(missing_ok=True)
except Exception:
pass
self.app._imgui_webview_tex_id = new_tex_id
self.app._imgui_webview_texture_path = str(temp_path)
except Exception as e:
self.app.add_warning_message(f"Web纹理更新失败: {e}")
finally:
webview.tex_dirty = False
@staticmethod
def _clamp01(value):
return 0.0 if value < 0.0 else 1.0 if value > 1.0 else value
def draw_menu_bar(self):
"""绘制菜单栏"""
with imgui_ctx.begin_main_menu_bar() as main_menu:
if main_menu:
# 文件菜单
with imgui_ctx.begin_menu("文件") as file_menu:
if file_menu:
if imgui.menu_item("新建项目", "Ctrl+N", False, True)[1]:
self.app._on_new_project()
if imgui.menu_item("打开项目", "Ctrl+O", False, True)[1]:
self.app._on_open_project()
imgui.separator()
if imgui.menu_item("保存", "Ctrl+S", False, True)[1]:
self.app._on_save_project()
if imgui.menu_item("另存为", "", False, True)[1]:
self.app._on_save_as_project()
imgui.separator()
if imgui.menu_item("退出", "Alt+F4", False, True)[1]:
self.app._on_exit()
# 编辑菜单
with imgui_ctx.begin_menu("编辑") as edit_menu:
if edit_menu:
if imgui.menu_item("撤销", "Ctrl+Z", False, True)[1]:
self.app._on_undo()
if imgui.menu_item("重做", "Ctrl+Y", False, True)[1]:
self.app._on_redo()
imgui.separator()
if imgui.menu_item("剪切", "Ctrl+X", False, True)[1]:
self.app._on_cut()
if imgui.menu_item("复制", "Ctrl+C", False, True)[1]:
self.app._on_copy()
if imgui.menu_item("粘贴", "Ctrl+V", False, True)[1]:
self.app._on_paste()
imgui.separator()
if imgui.menu_item("删除", "Del", False, True)[1]:
self.app._on_delete()
# 创建菜单
with imgui_ctx.begin_menu("创建") as create_menu:
if create_menu:
if imgui.menu_item("导入模型", "", False, True)[1]:
self.app._on_import_model()
imgui.separator()
if imgui.menu_item("空对象", "", False, True)[1]:
self.app._on_create_empty_object()
# 3D对象子菜单
with imgui_ctx.begin_menu("3D对象") as three_d_menu:
if three_d_menu:
if imgui.menu_item("立方体", "", False, True)[1]:
self.app._on_create_cube()
if imgui.menu_item("球体", "", False, True)[1]:
self.app._on_create_sphere()
if imgui.menu_item("圆柱体", "", False, True)[1]:
self.app._on_create_cylinder()
if imgui.menu_item("平面", "", False, True)[1]:
self.app._on_create_plane()
# 3D GUI子菜单
with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
if three_d_gui_menu:
if imgui.menu_item("3D文本", "", False, True)[1]:
self.app._on_create_3d_text()
if imgui.menu_item("3D图片", "", False, True)[1]:
self.app._on_create_3d_image()
# GUI子菜单
with imgui_ctx.begin_menu("GUI") as gui_menu:
if gui_menu:
if imgui.menu_item("创建按钮", "", False, True)[1]:
self.app._on_create_gui_button()
if imgui.menu_item("创建标签", "", False, True)[1]:
self.app._on_create_gui_label()
if imgui.menu_item("创建输入框", "", False, True)[1]:
self.app._on_create_gui_entry()
if imgui.menu_item("创建图片", "", False, True)[1]:
self.app._on_create_gui_image()
imgui.separator()
if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
self.app._on_create_video_screen()
if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
self.app._on_create_2d_video_screen()
if imgui.menu_item("创建球形视频", "", False, True)[1]:
self.app._on_create_spherical_video()
if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
self.app._on_create_virtual_screen()
# 光源子菜单
with imgui_ctx.begin_menu("光源") as light_menu:
if light_menu:
if imgui.menu_item("聚光灯", "", False, True)[1]:
self.app._on_create_spot_light()
if imgui.menu_item("点光源", "", False, True)[1]:
self.app._on_create_point_light()
# 地形子菜单
with imgui_ctx.begin_menu("地形") as terrain_menu:
if terrain_menu:
if imgui.menu_item("创建平面地形", "", False, True)[1]:
self.app._on_create_flat_terrain()
if imgui.menu_item("从高度图创建地形", "", False, True)[1]:
self.app._on_create_heightmap_terrain()
# 脚本子菜单
with imgui_ctx.begin_menu("脚本") as script_menu:
if script_menu:
if imgui.menu_item("创建脚本...", "", False, True)[1]:
self.app._on_create_script()
if imgui.menu_item("加载脚本文件...", "", False, True)[1]:
self.app._on_load_script()
imgui.separator()
if imgui.menu_item("重载所有脚本", "", False, True)[1]:
self.app._on_reload_all_scripts()
_, self.app.hotReloadEnabled = imgui.menu_item("启用热重载", "", self.app.hotReloadEnabled, True)
if imgui.menu_item("脚本管理器", "", False, True)[1]:
self.app._on_open_scripts_manager()
# 信息面板子菜单
with imgui_ctx.begin_menu("信息面板") as info_panel_menu:
if info_panel_menu:
if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
self.app._on_create_2d_sample_panel()
if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
self.app._on_create_3d_sample_panel()
if imgui.menu_item("Web面板", "", False, True)[1]:
self.app._on_create_web_panel()
# 视图菜单
with imgui_ctx.begin_menu("视图") as view_menu:
if view_menu:
_, self.app.showToolbar = imgui.menu_item("工具栏", "", self.app.showToolbar, True)
_, self.app.showSceneTree = imgui.menu_item("场景树", "", self.app.showSceneTree, True)
_, self.app.showResourceManager = imgui.menu_item("资源管理器", "", self.app.showResourceManager, True)
_, self.app.showPropertyPanel = imgui.menu_item("属性面板", "", self.app.showPropertyPanel, True)
_, self.app.showConsole = imgui.menu_item("控制台", "", self.app.showConsole, True)
_, self.app.showScriptPanel = imgui.menu_item("脚本管理", "", self.app.showScriptPanel, True)
_, self.app.showLUIEditor = imgui.menu_item("LUI编辑器", "", self.app.showLUIEditor, True)
prev_show_web_panel = self.app.showWebPanel
_, self.app.showWebPanel = imgui.menu_item("Web面板", "", self.app.showWebPanel, True)
if prev_show_web_panel and not self.app.showWebPanel:
self._stop_imgui_webview()
elif (not prev_show_web_panel) and self.app.showWebPanel:
self._on_create_web_panel()
# 工具菜单
with imgui_ctx.begin_menu("工具") as tools_menu:
if tools_menu:
# 工具切换选项
if imgui.menu_item("选择工具", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("选择")
if imgui.menu_item("移动工具", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("移动")
if imgui.menu_item("旋转工具", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("旋转")
if imgui.menu_item("缩放工具", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("缩放")
imgui.separator()
# 编辑工具
if imgui.menu_item("光照编辑", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("光照编辑")
if imgui.menu_item("图形编辑", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("图形编辑")
imgui.separator()
# VR子菜单
with imgui_ctx.begin_menu("VR") as vr_menu:
if vr_menu:
if imgui.menu_item("进入VR模式", "", False, True)[1]:
self.app._toggle_vr_mode()
if imgui.menu_item("退出VR模式", "", False, True)[1]:
self.app._exit_vr_mode()
imgui.separator()
if imgui.menu_item("VR状态", "", False, True)[1]:
self.app._show_vr_status()
if imgui.menu_item("VR设置", "", False, True)[1]:
self.app._show_vr_settings()
imgui.separator()
# VR调试子菜单
with imgui_ctx.begin_menu("VR调试") as vr_debug_menu:
if vr_debug_menu:
_, self.app.vr_debug_enabled = imgui.menu_item("启用调试输出", "", self.app.vr_debug_enabled, True)
if imgui.menu_item("立即显示性能报告", "", False, True)[1]:
self.app._show_vr_performance_report()
imgui.separator()
# 输出模式
with imgui_ctx.begin_menu("输出模式") as output_menu:
if output_menu:
if imgui.menu_item("简短模式", "", not self.app.vr_detailed_mode, True)[1]:
self.app.vr_detailed_mode = False
if imgui.menu_item("详细模式", "", self.app.vr_detailed_mode, True)[1]:
self.app.vr_detailed_mode = True
imgui.separator()
_, self.app.vr_performance_monitor = imgui.menu_item("启用性能监控", "", self.app.vr_performance_monitor, True)
# 窗口菜单 - 已隐藏
# with imgui_ctx.begin_menu("窗口") as window_menu:
# if window_menu:
# _, self.app.showDemoWindow = imgui.menu_item("ImGui演示", "", self.app.showDemoWindow, True)
# if self.app.testTexture:
# imgui.menu_item("关闭纹理测试", "", False, True)
# else:
# imgui.menu_item("显示纹理测试", "", False, True)
# 帮助菜单
with imgui_ctx.begin_menu("帮助") as help_menu:
if help_menu:
imgui.menu_item("关于", "", False, True)
imgui.menu_item("文档", "", False, True)
# 右侧显示FPS
imgui.set_cursor_pos_x(imgui.get_window_size().x - 140)
imgui.text("%.2f FPS (%.2f ms)" % (imgui.get_io().framerate, 1000.0 / imgui.get_io().framerate))
def draw_toolbar(self):
"""绘制工具栏"""
# 工具栏可以保持无标题栏,但允许移动和调整大小
flags = self.app.style_manager.get_window_flags("toolbar")
with self.app.style_manager.begin_styled_window("工具栏", self.app.showToolbar, flags):
self.app.showToolbar = True # 确保窗口保持打开
# 选择工具按钮
select_active = self.app.tool_manager.isSelectionTool()
if self.app.icons.get('select'):
tint_col = (1.0, 1.0, 0.0, 1.0) if select_active else (1.0, 1.0, 1.0, 1.0)
if self.app.style_manager.image_button(self.app.icons['select'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("选择")
if imgui.is_item_hovered():
imgui.set_tooltip("选择工具 (Q)")
imgui.same_line()
else:
if imgui.button("选择##select_tool"):
self.app.tool_manager.setCurrentTool("选择")
if select_active:
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
imgui.same_line()
# 移动工具按钮
move_active = self.app.tool_manager.isMoveTool()
if self.app.icons.get('move'):
# 使用不同颜色表示活动状态
tint_col = (1.0, 1.0, 0.0, 1.0) if move_active else (1.0, 1.0, 1.0, 1.0) # 活动时显示黄色
if self.app.style_manager.image_button(self.app.icons['move'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("移动")
if imgui.is_item_hovered():
imgui.set_tooltip("移动工具 (W)")
imgui.same_line()
else:
if imgui.button("移动##move_tool"):
self.app.tool_manager.setCurrentTool("移动")
if move_active:
# 为活动按钮添加背景色
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
imgui.same_line()
# 旋转工具按钮
rotate_active = self.app.tool_manager.isRotateTool()
if self.app.icons.get('rotate'):
tint_col = (1.0, 1.0, 0.0, 1.0) if rotate_active else (1.0, 1.0, 1.0, 1.0)
if self.app.style_manager.image_button(self.app.icons['rotate'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("旋转")
if imgui.is_item_hovered():
imgui.set_tooltip("旋转工具 (E)")
imgui.same_line()
else:
if imgui.button("旋转##rotate_tool"):
self.app.tool_manager.setCurrentTool("旋转")
if rotate_active:
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
imgui.same_line()
# 缩放工具按钮
scale_active = self.app.tool_manager.isScaleTool()
if self.app.icons.get('scale'):
tint_col = (1.0, 1.0, 0.0, 1.0) if scale_active else (1.0, 1.0, 1.0, 1.0)
if self.app.style_manager.image_button(self.app.icons['scale'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("缩放")
if imgui.is_item_hovered():
imgui.set_tooltip("缩放工具 (R)")
else:
if imgui.button("缩放##scale_tool"):
self.app.tool_manager.setCurrentTool("缩放")
if scale_active:
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
imgui.same_line()
imgui.separator()
imgui.same_line()
# 工具按钮已移除(导入、保存、播放)
def _draw_scene_tree(self):
"""绘制场景树面板"""
# 使用更少的限制性标志允许docking
flags = (imgui.WindowFlags_.no_collapse)
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags):
self.app.showSceneTree = True # 确保窗口保持打开
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._scene_tree_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
imgui.text("场景层级")
imgui.separator()
# 构建动态场景树
self._build_scene_tree()
def _build_scene_tree(self):
"""构建动态场景树"""
# 渲染节点
if imgui.tree_node("渲染"):
# 环境光
if hasattr(self.app, 'ambient_light') and self.app.ambient_light:
self._draw_scene_node(self.app.ambient_light, "环境光", "light")
# 聚光灯
if hasattr(self.app, 'scene_manager') and self.app.scene_manager:
if hasattr(self.app.scene_manager, 'Spotlight') and self.app.scene_manager.Spotlight:
for i, spotlight in enumerate(self.app.scene_manager.Spotlight):
self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light")
if hasattr(self.app.scene_manager, 'Pointlight') and self.app.scene_manager.Pointlight:
for i, pointlight in enumerate(self.app.scene_manager.Pointlight):
self._draw_scene_node(pointlight, f"点光源_{i+1}", "light")
# 地板
if hasattr(self.app, 'ground') and self.app.ground:
self._draw_scene_node(self.app.ground, "地板", "geometry")
imgui.tree_pop()
# 相机节点
if imgui.tree_node("相机"):
if hasattr(self.app, 'camera') and self.app.camera:
self._draw_scene_node(self.app.camera, "主相机", "camera")
imgui.tree_pop()
# 3D模型节点
if imgui.tree_node("模型"):
models = []
if hasattr(self.app, 'scene_manager') and self.app.scene_manager and hasattr(self.app.scene_manager, 'models'):
models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()])
# SSBO模式下模型可能不在 scene_manager.models 中,补充显示 ssbo_editor.model
ssbo_editor = getattr(self.app, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models:
models.append(ssbo_model)
if models:
for i, model in enumerate(models):
self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model")
else:
imgui.text("(空)")
imgui.tree_pop()
# if imgui.tree_node("GUI元素"):
# if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'):
# if self.app.gui_manager.gui_elements:
# for gui_element in self.app.gui_manager.gui_elements:
# if gui_element and hasattr(gui_element,'node'):
# gui_type = getattr(gui_element,'gui_type','GUI_UNKNOWN')
# display_name = getattr(gui_element,'name',gui_type)
# self._draw_scene_node(gui_element.node,display_name,"gui",gui_type)
# else:
# imgui.text("(空)")
# else:
# imgui.text("(空)")
# imgui.tree_pop()
# LUI元素节点
if imgui.tree_node("GUI元素"):
if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
self.app.lui_manager.draw_component_tree()
imgui.tree_pop()
# if imgui.tree_node("LUI元素"):
# if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
# if self.app.lui_manager.components:
# for comp in self.app.lui_manager.components:
# if 'node' in comp:
# self._draw_scene_node(comp['node'], comp['name'], "ui")
# if self.app.lui_manager.canvases:
# for canvas in self.app.lui_manager.canvases:
# if imgui.tree_node(f"Canvas: {canvas['name']}"):
# # 实际上组件已经在 node 下了,可以通过 _draw_scene_node 递归显示
# # 但为了清晰,我们可以手动列出或者依赖递归
# self._draw_scene_node(canvas['node'], canvas['name'], "geometry")
# imgui.tree_pop()
# else:
# imgui.text("(空)")
# else:
# imgui.text("(空)")
# imgui.tree_pop()
def _draw_scene_node(self, node, name, node_type, gui_subtype=None):
"""绘制单个场景节点"""
if not node or node.isEmpty():
return
# 检查是否被选中
is_selected = (hasattr(self.app, 'selection') and self.app.selection and
hasattr(self.app.selection, 'selectedNode') and
self.app.selection.selectedNode == node)
# 节点可见性
is_visible = node.is_hidden() == False
# 设置选择颜色
if is_selected:
imgui.push_style_color(imgui.Col_.text, (0.2, 0.6, 1.0, 1.0))
node_open = False
try:
# 显示节点
node_open = imgui.tree_node(name)
# 处理节点选择
if imgui.is_item_clicked():
if hasattr(self.app, 'selection') and self.app.selection:
self.app.selection.updateSelection(node)
# Clear LUI selection when a scene node is selected
if hasattr(self.app, 'lui_manager'):
self.app.lui_manager.selected_index = -1
if self.app.is_dragging and imgui.is_item_hovered():
self.app._drag_scene_tree_hover_node = node
# 右键菜单
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
self._show_node_context_menu(node, name, node_type)
# 显示节点属性
imgui.same_line()
if is_visible:
imgui.text_colored((0.5, 1.0, 0.5, 1.0), "可见")
else:
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "隐藏")
if node_open:
# SSBO模型使用虚拟层级显示避免 flatten 后真实子级丢失)
if self._draw_ssbo_virtual_children(node):
pass
elif node.getNumChildren() > 0:
for i in range(node.getNumChildren()):
child = node.getChild(i)
if not child or child.isEmpty():
continue
child_name = child.getName() or f"child_{i+1}"
# 过滤碰撞辅助节点,避免污染场景树
if child_name.startswith("modelCollision_"):
continue
self._draw_scene_node(child, child_name, node_type)
# tree_pop moved to finally
except Exception as e:
print(f"绘制场景节点时出错: {e}")
finally:
if node_open:
imgui.tree_pop()
# Ensure style stack is balanced.
if is_selected:
imgui.pop_style_color()
def _draw_ssbo_virtual_children(self, node):
"""Draw SSBO controller nodes as virtual children for scene tree."""
ssbo_editor = getattr(self.app, "ssbo_editor", None)
if not ssbo_editor:
return False
model = getattr(ssbo_editor, "model", None)
controller = getattr(ssbo_editor, "controller", None)
if not model or model != node or not controller:
return False
tree_root = controller.get_virtual_hierarchy() if hasattr(controller, "get_virtual_hierarchy") else None
if not tree_root or not tree_root.get("children"):
imgui.text_disabled("(无可用子节点)")
return True
for name in sorted(tree_root["children"].keys()):
child = tree_root["children"][name]
self._draw_ssbo_virtual_tree_node(ssbo_editor, child, "ssbo_root")
return True
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, tree_node, unique_id_prefix, depth=0):
"""Recursively draw virtual SSBO hierarchy in scene tree."""
if not tree_node:
return
path = tree_node.get("path", "")
display = tree_node.get("display_name") or tree_node.get("name") or path
leaf_key = tree_node.get("leaf_key")
group_key = tree_node.get("group_key")
children = tree_node.get("children", {}) or {}
label = f"{display}##{unique_id_prefix}_{path}"
# Leaf: selectable to trigger SSBO selection.
if not children and leaf_key:
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
if imgui.selectable(label, is_selected)[0]:
ssbo_editor.select_node(leaf_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
return
# Non-leaf: tree node only for hierarchy display.
opened = imgui.tree_node(label)
# Clicking non-leaf row selects its aggregate group so parent transform affects children.
if group_key and imgui.is_item_clicked(0):
ssbo_editor.select_node(group_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if opened:
# If this node is also a selectable leaf, render selectable entry first.
if group_key:
is_group_selected = (getattr(ssbo_editor, "selected_name", None) == group_key)
if imgui.selectable(f"[整体] {display}##group_{unique_id_prefix}_{path}", is_group_selected)[0]:
ssbo_editor.select_node(group_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if leaf_key:
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
if imgui.selectable(f"[节点] {display}##leaf_{unique_id_prefix}_{path}", is_selected)[0]:
ssbo_editor.select_node(leaf_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
for child_name in sorted(children.keys()):
self._draw_ssbo_virtual_tree_node(
ssbo_editor,
children[child_name],
unique_id_prefix,
depth + 1,
)
imgui.tree_pop()
def _show_node_context_menu(self, node, name, node_type):
"""显示节点右键菜单"""
self.app._context_menu_node = True
self.app._context_menu_target = node
def _draw_resource_manager(self):
"""绘制资源管理器面板"""
flags = self.app.style_manager.get_window_flags("panel")
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags):
self.app.showResourceManager = True
rm = self.app.resource_manager
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._resource_manager_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
self.app._resource_drop_targets.append((
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
str(rm.current_path),
))
imgui.text("文件浏览器")
imgui.separator()
if imgui.button(""):
rm.navigate_back()
imgui.same_line()
if imgui.button(""):
rm.navigate_forward()
imgui.same_line()
if imgui.button(""):
rm.navigate_up()
imgui.same_line()
if imgui.button("主页"):
rm.navigate_to(rm.project_root / "Resources")
imgui.same_line()
if imgui.button("刷新"):
rm.force_refresh()
# 自动刷新开关
imgui.same_line()
changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled)
if changed:
rm.set_auto_refresh(rm.auto_refresh_enabled)
imgui.same_line()
imgui.text(" ")
imgui.same_line()
# 路径输入框
changed, new_path = imgui.input_text("路径", str(rm.current_path), 256)
if changed:
try:
rm.navigate_to(Path(new_path))
except:
pass
# 搜索框
changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256)
imgui.separator()
if rm.refresh_if_needed():
pass
dirs, files = rm.get_directory_contents(rm.current_path)
for dir_path in dirs:
if not rm.should_show_file(dir_path):
continue
icon_name = rm.get_file_icon(dir_path.name, is_folder=True)
is_selected = dir_path in rm.selected_files
if is_selected:
imgui.push_style_color(imgui.Col_.header, (100/255, 150/255, 200/255, 1.0))
icon_texture = None
try:
icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}")
except:
pass
if icon_texture:
imgui.image(icon_texture, (16, 16))
imgui.same_line()
node_open = imgui.tree_node(f"{dir_path.name}##dir_{dir_path}")
else:
node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}##dir_{dir_path}")
if is_selected:
imgui.pop_style_color()
self._append_drop_target_from_last_item(dir_path)
self._start_resource_drag_if_needed(rm, dir_path)
if imgui.is_item_clicked():
if imgui.get_io().key_ctrl:
if is_selected:
rm.selected_files.discard(dir_path)
else:
rm.selected_files.add(dir_path)
else:
rm.selected_files.clear()
rm.selected_files.add(dir_path)
rm.focused_file = dir_path
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
rm.navigate_to(dir_path)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True
rm.context_menu_file = dir_path
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
if node_open:
subdirs, subfiles = rm.get_directory_contents(dir_path)
for subdir in subdirs:
if not rm.should_show_file(subdir):
continue
subicon_name = rm.get_file_icon(subdir.name, is_folder=True)
sub_is_selected = subdir in rm.selected_files
subicon_texture = None
try:
subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}")
except:
pass
if subicon_texture:
imgui.image(subicon_texture, (16, 16))
imgui.same_line()
sub_node_open = imgui.tree_node(f" {subdir.name}##dir_{subdir}")
else:
sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}##dir_{subdir}")
self._append_drop_target_from_last_item(subdir)
self._start_resource_drag_if_needed(rm, subdir)
if imgui.is_item_clicked():
if imgui.get_io().key_ctrl:
if sub_is_selected:
rm.selected_files.discard(subdir)
else:
rm.selected_files.add(subdir)
else:
rm.selected_files.clear()
rm.selected_files.add(subdir)
rm.focused_file = subdir
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
rm.navigate_to(subdir)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True
rm.context_menu_file = subdir
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
if sub_node_open:
imgui.tree_pop()
for subfile in subfiles:
if not rm.should_show_file(subfile):
continue
subicon_name = rm.get_file_icon(subfile.name)
sub_is_selected = subfile in rm.selected_files
subicon_texture = None
try:
subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}")
except:
pass
if subicon_texture:
imgui.image(subicon_texture, (16, 16))
imgui.same_line()
selected = imgui.selectable(f" {subfile.name}##file_{subfile}", sub_is_selected)
else:
selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}##file_{subfile}", sub_is_selected)
selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected)
self._start_resource_drag_if_needed(rm, subfile)
if selected_clicked:
if imgui.get_io().key_ctrl:
if sub_is_selected:
rm.selected_files.discard(subfile)
else:
rm.selected_files.add(subfile)
else:
rm.selected_files.clear()
rm.selected_files.add(subfile)
rm.focused_file = subfile
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
if self._is_model_file(subfile):
self.app._import_model_for_runtime(str(subfile))
self.app.add_info_message(f"正在导入模型: {subfile.name}")
else:
rm.open_file(subfile)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True
rm.context_menu_file = subfile
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
imgui.tree_pop()
for file_path in files:
if not rm.should_show_file(file_path):
continue
icon_name = rm.get_file_icon(file_path.name)
is_selected = file_path in rm.selected_files
icon_texture = None
try:
icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}")
except:
pass
if icon_texture:
imgui.image(icon_texture, (16, 16))
imgui.same_line()
selected = imgui.selectable(f"{file_path.name}##file_{file_path}", is_selected)
else:
selected = imgui.selectable(f"[{icon_name.upper()}] {file_path.name}##file_{file_path}", is_selected)
selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected)
self._start_resource_drag_if_needed(rm, file_path)
if selected_clicked:
if imgui.get_io().key_ctrl:
if is_selected:
rm.selected_files.discard(file_path)
else:
rm.selected_files.add(file_path)
else:
rm.selected_files.clear()
rm.selected_files.add(file_path)
rm.focused_file = file_path
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
if self._is_model_file(file_path):
self.app.add_info_message(f"正在导入模型: {file_path.name}")
self.app._import_model_for_runtime(str(file_path))
else:
rm.open_file(file_path)
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True
rm.context_menu_file = file_path
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
if rm.context_menu_file:
imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1]))
if imgui.begin_popup("resource_context_menu"):
if rm.context_menu_file and rm.context_menu_file.is_dir():
if imgui.menu_item("打开")[1]:
rm.navigate_to(rm.context_menu_file)
imgui.separator()
if imgui.menu_item("重命名")[1]:
print(f"重命名文件夹: {rm.context_menu_file.name}")
if imgui.menu_item("删除")[1]:
print(f"删除文件夹: {rm.context_menu_file.name}")
elif rm.context_menu_file:
if imgui.menu_item("打开")[1]:
rm.open_file(rm.context_menu_file)
imgui.separator()
if imgui.menu_item("导入到场景")[1]:
if self._is_model_file(rm.context_menu_file):
self.app.add_info_message(f"正在导入模型: {rm.context_menu_file.name}")
self.app._import_model_for_runtime(str(rm.context_menu_file))
if imgui.menu_item("重命名")[1]:
print(f"重命名文件: {rm.context_menu_file.name}")
if imgui.menu_item("删除")[1]:
print(f"删除文件: {rm.context_menu_file.name}")
if rm.context_menu_file:
imgui.separator()
if imgui.menu_item("复制路径")[1]:
imgui.set_clipboard_text(str(rm.context_menu_file))
self.app.add_info_message("路径已复制到剪贴板")
if imgui.menu_item("在文件管理器中显示")[1]:
import platform
import subprocess
if platform.system() == "Windows":
subprocess.run(["explorer", "/select,", str(rm.context_menu_file)])
elif platform.system() == "Darwin":
subprocess.run(["open", "-R", str(rm.context_menu_file)])
else:
subprocess.run(["xdg-open", str(rm.context_menu_file.parent)])
if imgui.is_mouse_clicked(0) and not imgui.is_window_hovered():
rm.context_menu_file = None
rm.show_context_menu = False
imgui.close_current_popup()
imgui.end_popup()
@staticmethod
def _is_model_file(path: Path) -> bool:
return path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']
def _append_drop_target_from_last_item(self, target_path: Path):
item_min = imgui.get_item_rect_min()
item_max = imgui.get_item_rect_max()
width = float(item_max.x - item_min.x)
height = float(item_max.y - item_min.y)
if width <= 0 or height <= 0:
return
self.app._resource_drop_targets.append((
float(item_min.x),
float(item_min.y),
width,
height,
str(target_path),
))
def _start_resource_drag_if_needed(self, rm, fallback_path: Path):
if not imgui.is_item_active() or not imgui.is_mouse_dragging(0):
return
drag_files = list(rm.selected_files) if rm.selected_files else [fallback_path]
rm.start_drag(drag_files)
self.app.is_dragging = True
self.app.show_drag_overlay = True
def _draw_web_panel(self):
"""绘制 Web 面板ImGui + 后台浏览器截图)。"""
self._ensure_web_panel_state()
if not self.app.showWebPanel:
self._stop_imgui_webview()
return
flags = self.app.style_manager.get_window_flags("panel")
with self.app.style_manager.begin_styled_window("Web面板", self.app.showWebPanel, flags) as (_, opened):
if not opened:
self.app.showWebPanel = False
self._stop_imgui_webview()
return
self.app.showWebPanel = True
changed, self.app.web_panel_url_input = imgui.input_text(
"URL", self.app.web_panel_url_input, 1024
)
if changed:
self.app.web_panel_url_input = self.app.web_panel_url_input.strip()
imgui.same_line()
if imgui.button("访问"):
self._navigate_web_panel()
webview = getattr(self.app, "_imgui_webview", None)
if not webview:
if not self._start_imgui_webview(self.app.web_panel_url_input):
imgui.text_colored((1.0, 0.4, 0.4, 1.0), "Web视图启动失败")
return
webview = self.app._imgui_webview
imgui.same_line()
if imgui.button("后退"):
webview.go_back()
imgui.same_line()
if imgui.button("前进"):
webview.go_forward()
imgui.same_line()
if imgui.button("刷新"):
webview.reload()
current_url = webview.current_url or self.app.web_panel_url_input
if current_url:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), current_url)
if webview.error:
imgui.text_colored((1.0, 0.4, 0.4, 1.0), webview.error)
imgui.separator()
self._update_web_panel_texture()
tex_id = getattr(self.app, "_imgui_webview_tex_id", None)
available = imgui.get_content_region_avail()
display_w = max(float(available.x), 64.0)
display_h = max(float(available.y), 64.0)
if tex_id:
imgui.image(tex_id, (display_w, display_h))
if imgui.is_item_hovered():
mouse_wheel = imgui.get_io().mouse_wheel
if abs(mouse_wheel) > 1e-4:
webview.scroll(-mouse_wheel * 120.0)
if imgui.is_mouse_clicked(0):
item_min = imgui.get_item_rect_min()
item_max = imgui.get_item_rect_max()
item_w = max(float(item_max.x - item_min.x), 1.0)
item_h = max(float(item_max.y - item_min.y), 1.0)
mouse_pos = imgui.get_mouse_pos()
x_ratio = self._clamp01((float(mouse_pos.x) - float(item_min.x)) / item_w)
y_ratio = self._clamp01((float(mouse_pos.y) - float(item_min.y)) / item_h)
webview.click(x_ratio, y_ratio)
elif webview.is_loading:
imgui.text("网页加载中...")
else:
imgui.text("正在初始化Web视图...")
def _draw_property_panel(self):
"""绘制属性面板"""
# 使用面板类型的窗口标志支持docking
flags = self.app.style_manager.get_window_flags("panel")
with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags):
self.app.showPropertyPanel = True # 确保窗口保持打开
# --- LUI Component Properties ---
# 优先检查 LUI 组件选择
if hasattr(self.app, 'lui_manager'):
lui_selected_index = getattr(self.app.lui_manager, "selected_index", -1)
if lui_selected_index >= 0 and self.app.lui_manager.luiFunction:
self.app.lui_manager.luiFunction._draw_component_properties(
self.app.lui_manager,
lui_selected_index
)
return
# --- Scene Node Properties ---
# 获取当前选中的节点
selected_node = None
if hasattr(self.app, 'selection') and self.app.selection and hasattr(self.app.selection, 'selectedNode'):
selected_node = self.app.selection.selectedNode
# SSBO mode may select a proxy node for gizmo operations.
# Resolve proxy back to a real scene node so property panel stays meaningful.
try:
if (selected_node and not selected_node.isEmpty() and
selected_node.hasTag("is_ssbo_proxy")):
ssbo_editor = getattr(self.app, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
if ssbo_editor and controller:
resolved = None
if getattr(ssbo_editor, "selected_ids", None):
first_gid = ssbo_editor.selected_ids[0]
key = controller.id_to_name.get(first_gid)
if key:
resolved = controller.key_to_node.get(key)
if (resolved is None or resolved.isEmpty()) and getattr(ssbo_editor, "selected_name", None):
resolved = controller.key_to_node.get(ssbo_editor.selected_name)
if resolved and not resolved.isEmpty():
selected_node = resolved
except Exception:
pass
if selected_node and not selected_node.isEmpty():
self._draw_node_properties(selected_node)
else:
# 无选中对象时显示提示模仿Qt版本的空状态样式
imgui.spacing()
imgui.spacing()
# 居中显示提示信息
window_width = imgui.get_window_width()
text_width = 200 # 估算文本宽度
text_pos_x = (window_width - text_width) / 2
imgui.set_cursor_pos_x(text_pos_x)
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "🔍 未选择任何对象")
imgui.set_cursor_pos_x(text_pos_x - 20)
imgui.text("请从场景树中选择一个对象")
imgui.set_cursor_pos_x(text_pos_x + 10)
imgui.text("以查看其属性")
imgui.spacing()
imgui.spacing()
# 添加一些分隔线和装饰
imgui.separator()
# 显示快速提示
imgui.text("💡 快速提示:")
imgui.bullet_text("单击场景树中的对象进行选择")
imgui.bullet_text("使用 F 键快速聚焦到选中对象")
imgui.bullet_text("使用 Delete 键删除选中对象")
def _draw_node_properties(self, node):
"""绘制节点属性"""
if not node or node.isEmpty():
# 停止变换监控
self.app.stop_transform_monitoring()
return
# 检查是否需要重新启动变换监控
if self.app._monitored_node != node:
self.app.stop_transform_monitoring()
self.app.start_transform_monitoring(node)
# 获取节点基本信息
node_name = node.getName() or "未命名节点"
node_type = self.app._get_node_type_from_node(node)
# 添加一些间距模仿Qt版本的布局
imgui.spacing()
# 物体名称组使用Qt版本的样式
if imgui.collapsing_header("物体名称"):
# 第一行:可见性复选框和名称输入
user_visible = node.getPythonTag("user_visible")
if user_visible is None:
user_visible = True
node.setPythonTag("user_visible", True)
# 可见性复选框模仿Qt版本的样式
changed, is_visible = imgui.checkbox("##visibility", user_visible)
if changed:
node.setPythonTag("user_visible", is_visible)
if is_visible:
node.show()
else:
node.hide()
imgui.same_line()
imgui.text("可见")
imgui.same_line()
imgui.spacing()
imgui.same_line()
# 名称输入框模仿Qt版本的样式
imgui.text("名称:")
imgui.same_line()
changed, new_name = imgui.input_text("##name", node_name, 256)
if changed and hasattr(self.app, 'selection'):
# 更新场景树中的名称
self.app._update_node_name(node, new_name)
# 添加分隔线
imgui.separator()
# 状态徽章模仿Qt版本的徽章样式
self.app._draw_status_badges(node)
imgui.spacing()
# 变换属性组
if imgui.collapsing_header("变换 Transform"):
self.app._draw_transform_properties(node)
# 根据节点类型显示特定属性组
if node_type == "GUI元素":
if imgui.collapsing_header("GUI信息"):
self.app._draw_gui_properties(node)
elif node_type == "光源":
if imgui.collapsing_header("光源属性"):
self.app._draw_light_properties(node)
elif node_type == "模型":
if imgui.collapsing_header("模型属性"):
self.app._draw_model_properties(node)
# 动画控制组(只对模型显示)
if imgui.collapsing_header("动画控制"):
self.app._draw_animation_properties(node)
# 外观属性组(通用)
if imgui.collapsing_header("外观属性"):
self.app._draw_appearance_properties(node)
# 碰撞检测组
if imgui.collapsing_header("碰撞检测"):
self.app._draw_collision_properties(node)
# 操作按钮组
if imgui.collapsing_header("操作"):
self.app._draw_property_actions(node)
def _draw_status_badges(self, node):
"""绘制状态徽章模仿Qt版本的徽章样式"""
imgui.text("状态标签: ")
# 可见性状态徽章
is_visible = not node.is_hidden()
visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0)
visibility_text = "可见" if is_visible else "隐藏"
imgui.same_line()
imgui.text_colored(visibility_color, f"[{visibility_text}]")
# 节点类型徽章
node_type = self._get_node_type_from_node(node)
type_colors = {
"GUI元素": (0.188, 0.404, 0.753, 1.0), # 主题蓝色
"光源": (1.0, 0.8, 0.2, 1.0), # 黄色
"模型": (0.6, 0.8, 1.0, 1.0), # 浅蓝色
"相机": (0.8, 0.8, 0.2, 1.0), # 橙色
"几何体": (0.5, 0.5, 0.5, 1.0), # 灰色
}
if node_type in type_colors:
imgui.same_line()
imgui.text_colored(type_colors[node_type], f"[{node_type}]")
# 功能性徽章
badges = []
# 碰撞体徽章
has_collision = hasattr(node, 'getChild') and any('Collision' in child.getName() for child in node.getChildren() if child.getName())
if has_collision:
badges.append(("碰撞", (0.2, 0.4, 0.8, 1.0))) # 蓝色
# 脚本徽章
has_script = hasattr(node, 'getPythonTag') and node.getPythonTag('script')
if has_script:
badges.append(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色
# 动画徽章优化检测逻辑避免重复创建Actor
has_animation = False
if node_type == "模型": # 只对模型类型进行动画检测
model_path = node.getTag("model_path") if node.hasTag("model_path") else ""
likely_anim_format = bool(model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx', '.bam', '.egg')))
# 优先使用场景标签(导入/加载时会写入)
if node.hasTag("has_animations"):
has_animation = node.getTag("has_animations").lower() == "true"
# 再做轻量结构检测(不依赖 Actor
if not has_animation:
try:
has_character = node.findAllMatches("**/+Character").getNumPaths() > 0
has_bundle = node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
has_animation = has_character or has_bundle
if has_animation:
node.setTag("has_animations", "true")
node.setTag("can_create_actor_from_memory", "true")
except Exception:
pass
# 最后才尝试 Actor 检测(只缓存“有动画”,避免把失败结果永久缓存)
cached_result = node.getPythonTag('animation')
if cached_result is True:
has_animation = True
elif not has_animation and likely_anim_format:
try:
actor = self._getActor(node)
if actor and actor.getAnimNames():
has_animation = True
node.setTag("has_animations", "true")
node.setPythonTag('animation', True)
print(f"[动画检测] {node.getName()}: 有动画")
except Exception as e:
print(f"动画检测失败: {e}")
elif cached_result is False and not likely_anim_format:
has_animation = False
else:
# 对于非模型类型,检查已有的动画标签
has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation')
if has_animation:
badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色
# 材质徽章
has_material = hasattr(node, 'getMaterial') and node.getMaterial()
if has_material:
badges.append(("材质", (0.8, 0.6, 0.2, 1.0))) # 金色
# 绘制功能性徽章
for badge_text, badge_color in badges:
imgui.same_line()
imgui.text_colored(badge_color, f"[{badge_text}]")
# 如果没有特殊徽章,显示默认状态
if not badges:
imgui.same_line()
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "[标准对象]")
def _draw_transform_properties(self, node):
"""绘制变换属性"""
# 位置组
if imgui.collapsing_header("位置 Position"):
# 相对位置
imgui.text("相对位置")
pos = node.getPos()
# X坐标
changed, new_x = imgui.input_float("X##pos_x", pos.x, 0.1, 1.0, "%.3f")
if changed: node.setX(new_x)
# Y坐标
changed, new_y = imgui.input_float("Y##pos_y", pos.y, 0.1, 1.0, "%.3f")
if changed: node.setY(new_y)
# Z坐标
changed, new_z = imgui.input_float("Z##pos_z", pos.z, 0.1, 1.0, "%.3f")
if changed: node.setZ(new_z)
# 世界位置
imgui.text("世界位置")
world_pos = node.getPos(self.render)
imgui.text(f"世界 X: {world_pos.x:.3f}")
imgui.text(f"世界 Y: {world_pos.y:.3f}")
imgui.text(f"世界 Z: {world_pos.z:.3f}")
# 位置操作按钮
if imgui.button("重置位置##reset_pos"):
node.setPos(0, 0, 0)
imgui.same_line()
if imgui.button("复制位置##copy_pos"):
self._clipboard_pos = (pos.x, pos.y, pos.z)
imgui.same_line()
if imgui.button("粘贴位置##paste_pos") and hasattr(self, '_clipboard_pos'):
node.setPos(self._clipboard_pos[0], self._clipboard_pos[1], self._clipboard_pos[2])
# 旋转组
if imgui.collapsing_header("旋转 Rotation"):
hpr = node.getHpr()
# HPR旋转
imgui.text("HPR 旋转 (度)")
changed, new_h = imgui.input_float("H##rot_h", hpr.x, 1.0, 10.0, "%.1f")
if changed: node.setH(new_h)
changed, new_p = imgui.input_float("P##rot_p", hpr.y, 1.0, 10.0, "%.1f")
if changed: node.setP(new_p)
changed, new_r = imgui.input_float("R##rot_r", hpr.z, 1.0, 10.0, "%.1f")
if changed: node.setR(new_r)
# 旋转操作按钮
if imgui.button("重置旋转##reset_rot"):
node.setHpr(0, 0, 0)
imgui.same_line()
if imgui.button("随机旋转##random_rot"):
import random
node.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360))
# 缩放组
if imgui.collapsing_header("缩放 Scale"):
scale = node.getScale()
# XYZ缩放
imgui.text("XYZ 缩放")
changed, new_sx = imgui.input_float("X##scale_x", scale.x, 0.1, 1.0, "%.3f")
if changed: node.setSx(new_sx)
changed, new_sy = imgui.input_float("Y##scale_y", scale.y, 0.1, 1.0, "%.3f")
if changed: node.setSy(new_sy)
changed, new_sz = imgui.input_float("Z##scale_z", scale.z, 0.1, 1.0, "%.3f")
if changed: node.setSz(new_sz)
# 统一缩放
if imgui.button("统一缩放##uniform_scale"):
uniform_scale = (scale.x + scale.y + scale.z) / 3.0
node.setScale(uniform_scale, uniform_scale, uniform_scale)
imgui.same_line()
if imgui.button("重置缩放##reset_scale"):
node.setScale(1, 1, 1)
imgui.same_line()
if imgui.button("翻倍##double_scale"):
node.setScale(scale.x * 2, scale.y * 2, scale.z * 2)
def _draw_gui_properties(self, node):
"""绘制GUI元素属性"""
# 获取GUI元素
gui_element = None
if hasattr(node, 'getPythonTag'):
gui_element = node.getPythonTag('gui_element')
if not gui_element:
imgui.text("无GUI元素数据")
return
# GUI类型信息
gui_type = getattr(gui_element, 'gui_type', 'UNKNOWN')
imgui.text(f"GUI类型: {gui_type}")
# 基本属性
if imgui.collapsing_header("基本属性"):
# 文本内容 (适用于按钮、标签等)
if hasattr(gui_element, 'text'):
changed, new_text = imgui.input_text("文本内容", gui_element.text, 256)
if changed and hasattr(self, 'gui_manager'):
self.gui_manager.editGUIElement(gui_element, 'text', new_text)
# GUI ID
gui_id = getattr(gui_element, 'id', '')
changed, new_id = imgui.input_text("GUI ID", gui_id, 64)
if changed and hasattr(self, 'gui_manager'):
gui_element.id = new_id
# 变换属性
if imgui.collapsing_header("变换属性"):
# 位置
pos = gui_element.getPos()
imgui.text("位置")
if gui_type in ["button", "label", "entry", "2d_image"]:
# 2D GUI组件使用屏幕坐标
imgui.text("屏幕坐标")
logical_x = pos.getX() / 0.1
logical_z = pos.getZ() / 0.1
changed, new_x = imgui.input_float("X##gui_pos_x", logical_x, 1.0, 10.0, "%.1f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setX(new_x * 0.1)
changed, new_z = imgui.input_float("Y##gui_pos_y", logical_z, 1.0, 10.0, "%.1f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setZ(new_z * 0.1)
else:
# 3D GUI组件使用世界坐标
imgui.text("世界坐标")
changed, new_x = imgui.input_float("X##gui_world_x", pos.getX(), 0.1, 1.0, "%.3f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setX(new_x)
changed, new_y = imgui.input_float("Y##gui_world_y", pos.getY(), 0.1, 1.0, "%.3f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setY(new_y)
changed, new_z = imgui.input_float("Z##gui_world_z", pos.getZ(), 0.1, 1.0, "%.3f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setZ(new_z)
# 缩放
scale = gui_element.getScale()
imgui.text("缩放")
changed, new_sx = imgui.input_float("X##gui_scale_x", scale.getX(), 0.1, 1.0, "%.3f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setSx(new_sx)
changed, new_sy = imgui.input_float("Y##gui_scale_y", scale.getY(), 0.1, 1.0, "%.3f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setSy(new_sy)
changed, new_sz = imgui.input_float("Z##gui_scale_z", scale.getZ(), 0.1, 1.0, "%.3f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setSz(new_sz)
# 旋转
hpr = gui_element.getHpr()
imgui.text("旋转")
changed, new_h = imgui.input_float("H##gui_rot_h", hpr.getX(), 1.0, 10.0, "%.1f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setH(new_h)
changed, new_p = imgui.input_float("P##gui_rot_p", hpr.getY(), 1.0, 10.0, "%.1f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setP(new_p)
changed, new_r = imgui.input_float("R##gui_rot_r", hpr.getZ(), 1.0, 10.0, "%.1f")
if changed and hasattr(self, 'gui_manager'):
gui_element.setR(new_r)
# 外观属性
if imgui.collapsing_header("外观属性"):
# 大小
if hasattr(gui_element, 'size'):
size = gui_element.size
imgui.text("大小")
changed, new_w = imgui.input_float("宽度", size[0], 1.0, 10.0, "%.1f")
if changed and hasattr(self, 'gui_manager'):
new_size = (new_w, size[1])
self.gui_manager.editGUIElement(gui_element, 'size', new_size)
changed, new_h = imgui.input_float("高度", size[1], 1.0, 10.0, "%.1f")
if changed and hasattr(self, 'gui_manager'):
new_size = (size[0], new_h)
self.gui_manager.editGUIElement(gui_element, 'size', new_size)
# 颜色
if hasattr(gui_element, 'getColor'):
try:
color = gui_element.getColor()
# 确保颜色是有效的
if not color or (hasattr(color, '__len__') and len(color) < 3):
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
except:
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
imgui.text("颜色")
# 获取颜色值
if hasattr(color, 'getX'):
# 如果是Panda3D的Vec4对象
r, g, b = color.getX(), color.getY(), color.getZ()
else:
# 如果是元组或其他格式
r, g, b = color[0], color[1], color[2]
changed, new_r = imgui.slider_float("R##gui_color_r", r, 0.0, 1.0)
if changed: gui_element.setColor(new_r, g, b, 1.0)
changed, new_g = imgui.slider_float("G##gui_color_g", g, 0.0, 1.0)
if changed: gui_element.setColor(r, new_g, b, 1.0)
changed, new_b = imgui.slider_float("B##gui_color_b", b, 0.0, 1.0)
if changed: gui_element.setColor(r, g, new_b, 1.0)
# 透明度
imgui.text("透明度")
current_alpha = getattr(gui_element, 'alpha', 1.0)
changed, new_alpha = imgui.slider_float("Alpha", current_alpha, 0.0, 1.0)
if changed:
gui_element.alpha = new_alpha
if hasattr(gui_element, 'setTransparency'):
# 将0.0-1.0范围转换为Panda3D的透明度格式
panda_transparency = int((1.0 - new_alpha) * 255)
gui_element.setTransparency(panda_transparency)
# 渲染顺序
imgui.text("渲染顺序")
current_sort = getattr(gui_element, 'sort', 0)
changed, new_sort = imgui.input_int("Sort Order", current_sort)
if changed:
gui_element.sort = new_sort
if hasattr(gui_element, 'setBin'):
gui_element.setBin('fixed', new_sort)
# 字体设置适用于文本类型的GUI元素
if gui_type in ["button", "label", "entry"]:
imgui.text("字体设置")
# 字体选择
current_font = getattr(gui_element, 'font_path', '')
if imgui.button(f"字体: {Path(current_font).name if current_font else '默认'}##font_select"):
self.show_font_selector(
gui_element,
'font_path',
current_font,
lambda font_path: self._apply_gui_font(gui_element, font_path)
)
# 字体大小
current_size = getattr(gui_element, 'font_size', 12)
changed, new_size = imgui.slider_float("字体大小", current_size, 8.0, 72.0)
if changed:
gui_element.font_size = new_size
self._apply_gui_font_size(gui_element, new_size)
# 字体样式
imgui.text("字体样式")
is_bold = getattr(gui_element, 'font_bold', False)
changed, new_bold = imgui.checkbox("粗体", is_bold)
if changed:
gui_element.font_bold = new_bold
self._apply_gui_font_style(gui_element)
imgui.same_line()
is_italic = getattr(gui_element, 'font_italic', False)
changed, new_italic = imgui.checkbox("斜体", is_italic)
if changed:
gui_element.font_italic = new_italic
self._apply_gui_font_style(gui_element)
def _draw_light_properties(self, node):
"""绘制光源属性"""
imgui.text("光源属性")
# 光源颜色
if hasattr(node, 'getColor'):
try:
color = node.getColor()
# 确保颜色是有效的
if not color or len(color) < 3:
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
except:
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
changed, new_r = imgui.drag_float("颜色 R", color[0], 0.01, 0.0, 1.0)
if changed: node.setColor(new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
changed, new_g = imgui.drag_float("颜色 G", color[1], 0.01, 0.0, 1.0)
if changed: node.setColor(color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
changed, new_b = imgui.drag_float("颜色 B", color[2], 0.01, 0.0, 1.0)
if changed: node.setColor(color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
# 光源强度
imgui.text("光源强度: (暂不支持编辑)")
def _draw_model_properties(self, node):
"""绘制模型属性"""
# 获取模型信息
model_path = node.getTag("model_path") if node.hasTag("model_path") else "未知"
imgui.text("模型路径:")
imgui.same_line()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_path)
# 模型基本信息
imgui.text("模型名称:")
imgui.same_line()
model_name = node.getName() or "未命名模型"
imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_name)
# 模型位置信息
imgui.text("位置:")
imgui.same_line()
pos = node.getPos()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{pos.x:.2f} Y:{pos.y:.2f} Z:{pos.z:.2f}")
# 模型缩放信息
imgui.text("缩放:")
imgui.same_line()
scale = node.getScale()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{scale.x:.2f} Y:{scale.y:.2f} Z:{scale.z:.2f}")
# 模型旋转信息
imgui.text("旋转:")
imgui.same_line()
hpr = node.getHpr()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"H:{hpr.x:.1f}° P:{hpr.y:.1f}° R:{hpr.z:.1f}°")
def _draw_animation_properties(self, node):
"""绘制动画控制属性面板(优化版本,使用缓存避免重复计算)"""
anim_node = node
try:
if hasattr(self, "_resolve_animation_owner_model"):
resolved = self._resolve_animation_owner_model(node)
if resolved and not resolved.isEmpty():
anim_node = resolved
except Exception:
pass
# 路径兜底:当 anim_node 缺少路径时,从 scene_manager.models 中反查祖先模型
try:
needs_path = (not anim_node.hasTag("model_path")) or (not anim_node.getTag("model_path"))
if needs_path and hasattr(self, "scene_manager") and self.scene_manager:
models = getattr(self.scene_manager, "models", [])
for model in list(models):
try:
if not model or model.isEmpty():
continue
if (model == anim_node or model.isAncestorOf(anim_node) or anim_node.isAncestorOf(model)):
if model.hasTag("model_path") and model.getTag("model_path"):
anim_node.setTag("model_path", model.getTag("model_path"))
if model.hasTag("original_path") and model.getTag("original_path"):
anim_node.setTag("original_path", model.getTag("original_path"))
break
if model.hasTag("saved_model_path") and model.getTag("saved_model_path"):
anim_node.setTag("model_path", model.getTag("saved_model_path"))
break
except Exception:
continue
except Exception:
pass
# 先刷新一次模型动画标签,避免“导入后未初始化”导致误判
try:
if hasattr(self, "scene_manager") and self.scene_manager and hasattr(self.scene_manager, "_processModelAnimations"):
self.scene_manager._processModelAnimations(anim_node)
except Exception:
pass
has_animation_tag = anim_node.hasTag("has_animations") and anim_node.getTag("has_animations").lower() == "true"
has_animation_nodes = False
try:
has_animation_nodes = (
anim_node.findAllMatches("**/+Character").getNumPaths() > 0 or
anim_node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
)
except Exception:
pass
# 检查是否已经缓存了动画信息
cached_anim_info = anim_node.getPythonTag("cached_anim_info")
cached_processed_names = anim_node.getPythonTag("cached_processed_names")
# 如果之前缓存的是“格式未知”,但现在已有路径,强制重建缓存
try:
now_has_path = anim_node.hasTag("model_path") and bool(anim_node.getTag("model_path"))
if now_has_path and isinstance(cached_anim_info, str) and "格式: 未知" in cached_anim_info:
cached_anim_info = None
cached_processed_names = None
anim_node.setPythonTag("cached_anim_info", None)
anim_node.setPythonTag("cached_processed_names", None)
anim_node.setPythonTag("animation", None)
self._clear_animation_cache(anim_node)
except Exception:
pass
# 如果节点已被检测为有动画,但缓存是“无动画”,强制重新检测一次
if (has_animation_tag or has_animation_nodes) and (cached_anim_info == "无动画" or cached_processed_names == []):
cached_anim_info = None
cached_processed_names = None
anim_node.setPythonTag("cached_anim_info", None)
anim_node.setPythonTag("cached_processed_names", None)
anim_node.setPythonTag("animation", None)
# 只有在没有缓存时才进行完整的动画检测和处理
if cached_anim_info is None or cached_processed_names is None:
# 获取Actor
actor = self._getActor(anim_node)
if not actor:
if has_animation_tag or has_animation_nodes:
imgui.text_colored((1.0, 0.7, 0.3, 1.0), "检测到动画结构但当前未成功绑定Actor")
else:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画")
return
# 获取和分析动画名称
anim_names = actor.getAnimNames()
processed_names = self._processAnimationNames(anim_node, anim_names)
if not processed_names:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
# 只在明确无动画时缓存空结果,避免误缓存导致后续无法重试
if not (has_animation_tag or has_animation_nodes):
anim_node.setPythonTag("cached_processed_names", [])
anim_node.setPythonTag("cached_anim_info", "无动画")
return
anim_node.setTag("has_animations", "true")
anim_node.setPythonTag("animation", True)
# 计算并缓存动画信息
format_info = self._getModelFormat(anim_node)
animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info)
info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}"
if animation_info:
info_text += f" | {animation_info}"
# 缓存结果
anim_node.setPythonTag("cached_anim_info", info_text)
anim_node.setPythonTag("cached_processed_names", processed_names)
else:
# 使用缓存的数据
info_text = cached_anim_info
processed_names = cached_processed_names
# 如果缓存的空结果,直接返回
if not processed_names:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
return
# 显示动画信息(使用缓存的数据)
imgui.text("信息:")
imgui.same_line()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), info_text)
imgui.spacing()
# 动画选择下拉框
imgui.text("动画名称:")
imgui.same_line()
# 获取当前选中的动画
current_anim = anim_node.getPythonTag("selected_animation")
valid_original_names = [original_name for _, original_name in processed_names]
if current_anim is None or current_anim not in valid_original_names:
current_anim = processed_names[0][1] if processed_names else ""
anim_node.setPythonTag("selected_animation", current_anim)
# 查找当前动画的索引
current_index = 0
for i, (display_name, original_name) in enumerate(processed_names):
if original_name == current_anim:
current_index = i
break
# 创建下拉框选项
animation_options = [display_name for display_name, _ in processed_names]
changed, new_index = imgui.combo("##animation_combo", current_index, animation_options)
if changed and new_index < len(processed_names):
selected_display, selected_original = processed_names[new_index]
anim_node.setPythonTag("selected_animation", selected_original)
print(f"选择动画: {selected_display} (原始名称: {selected_original})")
imgui.spacing()
# 控制按钮组
imgui.text("控制:")
# 播放按钮
if imgui.button("播放##play_animation"):
self._playAnimation(anim_node)
imgui.same_line()
# 暂停按钮
if imgui.button("暂停##pause_animation"):
self._pauseAnimation(anim_node)
imgui.same_line()
# 停止按钮
if imgui.button("停止##stop_animation"):
self._stopAnimation(anim_node)
imgui.same_line()
# 循环按钮
if imgui.button("循环##loop_animation"):
self._loopAnimation(anim_node)
imgui.spacing()
# 播放速度控制
imgui.text("播放速度:")
imgui.same_line()
# 获取当前速度
current_speed = anim_node.getPythonTag("anim_speed")
if current_speed is None:
current_speed = 1.0
anim_node.setPythonTag("anim_speed", current_speed)
# 速度滑块
changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f")
if changed:
anim_node.setPythonTag("anim_speed", new_speed)
self._setAnimationSpeed(anim_node, new_speed)
imgui.same_line()
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速")
def _draw_collision_properties(self, node):
"""绘制碰撞检测属性"""
if not node or node.isEmpty():
return
try:
# 检查节点是否已有碰撞
has_collision = self._has_collision(node)
# 碰撞状态徽章
imgui.text("状态:")
imgui.same_line()
if has_collision:
imgui.text_colored((0.0, 0.8, 0.0, 1.0), "🟢 已启用")
else:
imgui.text_colored((0.8, 0.0, 0.0, 1.0), "🔴 未启用")
imgui.separator()
# 碰撞形状选择
imgui.text("碰撞形状:")
imgui.same_line()
# 碰撞形状选项
collision_shapes = ["球形 (Sphere)", "盒型 (Box)", "胶囊体 (Capsule)", "平面 (Plane)", "自动选择 (Auto)"]
# 获取当前形状
current_shape = self._get_current_collision_shape(node) if has_collision else getattr(self.app, '_selected_collision_shape', "球形 (Sphere)")
if has_collision: self.app._selected_collision_shape = current_shape
# 形状选择下拉框
current_index = collision_shapes.index(current_shape) if current_shape in collision_shapes else 0
changed, selected_index = imgui.combo("##collision_shape", current_index, collision_shapes)
if changed:
# 始终更新选择的形状
selected_shape = collision_shapes[selected_index]
self.app._selected_collision_shape = selected_shape
# 如果已经有碰撞体,询问用户是否要重新创建
if has_collision:
self._remove_collision_from_node(node)
self._add_collision_to_node(node)
imgui.separator()
# 位置偏移控件
imgui.text("位置偏移:")
# 获取当前位置偏移
pos_offset = self._get_collision_position_offset(node)
# X位置
changed, new_x = imgui.drag_float("X##collision_pos_x", pos_offset[0], 0.1, -100.0, 100.0, "%.2f")
if changed and has_collision:
self._update_collision_position(node, 'x', new_x)
# Y位置
changed, new_y = imgui.drag_float("Y##collision_pos_y", pos_offset[1], 0.1, -100.0, 100.0, "%.2f")
if changed and has_collision:
self._update_collision_position(node, 'y', new_y)
# Z位置
changed, new_z = imgui.drag_float("Z##collision_pos_z", pos_offset[2], 0.1, -100.0, 100.0, "%.2f")
if changed and has_collision:
self._update_collision_position(node, 'z', new_z)
# 形状特定参数(始终显示,但根据状态启用/禁用)
shape_type = self._get_current_collision_shape_type(node)
self._draw_shape_specific_parameters(node, shape_type, has_collision)
imgui.separator()
# 操作按钮
if has_collision:
# 显示/隐藏碰撞体按钮
is_visible = self._is_collision_visible(node)
visibility_text = "隐藏碰撞体" if is_visible else "显示碰撞体"
if imgui.button(visibility_text):
self._toggle_collision_visibility(node)
imgui.same_line()
# 移除碰撞按钮
if imgui.button("移除碰撞"):
self._remove_collision_from_node(node)
else:
# 添加碰撞按钮
if imgui.button("添加碰撞"):
self._add_collision_to_node(node)
imgui.separator()
# 碰撞检测触发模式
imgui.text("触发模式:")
# 自动检测开关
auto_enabled = self.collision_manager.model_collision_enabled if hasattr(self, 'collision_manager') else False
changed, new_auto = imgui.checkbox("自动检测", auto_enabled)
if changed and hasattr(self, 'collision_manager'):
self.collision_manager.enableModelCollisionDetection(new_auto, 0.1, 0.5)
imgui.same_line()
# 手动检测按钮
if imgui.button("立即检测"):
if hasattr(self, 'collision_manager'):
self._manual_collision_detection()
except Exception as e:
print(f"绘制碰撞属性失败: {e}")
import traceback
traceback.print_exc()
def _draw_shape_specific_parameters(self, node, shape_type, has_collision=True):
"""绘制形状特定参数"""
try:
if shape_type == "sphere":
self._draw_sphere_parameters(node, has_collision)
elif shape_type == "box":
self._draw_box_parameters(node, has_collision)
elif shape_type == "capsule":
self._draw_capsule_parameters(node, has_collision)
elif shape_type == "plane":
self._draw_plane_parameters(node, has_collision)
except Exception as e:
print(f"绘制形状参数失败: {e}")
def _draw_sphere_parameters(self, node, has_collision=True):
"""绘制球形参数"""
try:
imgui.text("球形参数:")
imgui.same_line()
# 获取当前半径
radius = self._get_sphere_radius(node)
# 半径调整
changed, new_radius = imgui.drag_float("半径##sphere_radius", radius, 0.1, 0.1, 100.0, "%.2f")
if changed and has_collision:
self._update_sphere_radius(node, new_radius)
except Exception as e:
print(f"绘制球形参数失败: {e}")
def _draw_box_parameters(self, node, has_collision=True):
"""绘制盒型参数"""
try:
imgui.text("盒型参数:")
# 获取当前尺寸
size = self._get_box_size(node)
# 尺寸调整
changed, new_x = imgui.drag_float("长度##box_length", size[0], 0.1, 0.1, 100.0, "%.2f")
if changed and has_collision:
self._update_box_size(node, 'x', new_x)
changed, new_y = imgui.drag_float("宽度##box_width", size[1], 0.1, 0.1, 100.0, "%.2f")
if changed and has_collision:
self._update_box_size(node, 'y', new_y)
changed, new_z = imgui.drag_float("高度##box_height", size[2], 0.1, 0.1, 100.0, "%.2f")
if changed and has_collision:
self._update_box_size(node, 'z', new_z)
except Exception as e:
print(f"绘制盒型参数失败: {e}")
def _draw_capsule_parameters(self, node, has_collision=True):
"""绘制胶囊体参数"""
try:
imgui.text("胶囊体参数:")
# 获取当前参数
radius = self._get_capsule_radius(node)
height = self._get_capsule_height(node)
# 半径调整
changed, new_radius = imgui.drag_float("半径##capsule_radius", radius, 0.1, 0.1, 100.0, "%.2f")
if changed and has_collision:
self._update_capsule_radius(node, new_radius)
# 高度调整
changed, new_height = imgui.drag_float("高度##capsule_height", height, 0.1, 0.1, 100.0, "%.2f")
if changed and has_collision:
self._update_capsule_height(node, new_height)
except Exception as e:
print(f"绘制胶囊体参数失败: {e}")
def _draw_plane_parameters(self, node, has_collision=True):
"""绘制平面参数"""
try:
imgui.text("平面参数:")
# 获取当前法向量
normal = self._get_plane_normal(node)
# 法向量调整
changed, new_x = imgui.drag_float("法向量 X##plane_normal_x", normal[0], 0.1, -1.0, 1.0, "%.2f")
if changed and has_collision:
self._update_plane_normal(node, 'x', new_x)
changed, new_y = imgui.drag_float("法向量 Y##plane_normal_y", normal[1], 0.1, -1.0, 1.0, "%.2f")
if changed and has_collision:
self._update_plane_normal(node, 'y', new_y)
changed, new_z = imgui.drag_float("法向量 Z##plane_normal_z", normal[2], 0.1, -1.0, 1.0, "%.2f")
if changed and has_collision:
self._update_plane_normal(node, 'z', new_z)
except Exception as e:
print(f"绘制平面参数失败: {e}")
def _draw_property_actions(self, node):
"""绘制属性操作按钮"""
# 重置变换
if imgui.button("重置变换"):
node.setPos(0, 0, 0)
node.setHpr(0, 0, 0)
node.setScale(1, 1, 1)
imgui.same_line()
# 切换可见性
is_visible = not node.is_hidden()
visibility_text = "隐藏" if is_visible else "显示"
if imgui.button(visibility_text):
if is_visible:
node.hide()
else:
node.show()
imgui.same_line()
# 聚焦到对象
if imgui.button("聚焦"):
if hasattr(self, 'selection') and self.selection:
self.selection.focusCameraOnSelectedNodeAdvanced()
# 删除对象
imgui.same_line()
if imgui.button("删除"):
if hasattr(self, 'selection') and self.selection:
self.selection.deleteSelectedNode()
def _draw_appearance_properties(self, node):
"""绘制外观属性"""
# 颜色属性
if hasattr(node, 'getColor'):
imgui.text("颜色")
try:
color = node.getColor()
# 确保颜色是有效的
if not color or len(color) < 3:
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
except:
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
# 颜色滑块
changed, new_r = imgui.slider_float("R##color_r", color[0], 0.0, 1.0)
if changed:
new_color = (new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
node.setColor(new_color)
color = new_color
changed, new_g = imgui.slider_float("G##color_g", color[1], 0.0, 1.0)
if changed:
new_color = (color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
node.setColor(new_color)
color = new_color
changed, new_b = imgui.slider_float("B##color_b", color[2], 0.0, 1.0)
if changed:
new_color = (color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
node.setColor(new_color)
color = new_color
# 只有当颜色有alpha通道时才显示alpha滑块
if len(color) > 3:
changed, new_a = imgui.slider_float("A##color_a", color[3], 0.0, 1.0)
if changed:
new_color = (color[0], color[1], color[2], new_a)
node.setColor(new_color)
color = new_color
# 颜色预览和选择器
imgui.text("颜色预览")
color_with_alpha = (color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0)
if imgui.color_button("颜色预览##preview", color_with_alpha, 0, (100, 30)):
# 点击颜色按钮打开颜色选择器
self.show_color_picker(node, 'color', color_with_alpha)
imgui.same_line()
if imgui.button("选择颜色##color_picker_btn"):
self.show_color_picker(node, 'color', (color.x, color.y, color.z, color.w))
# 透明度
if hasattr(node, 'setTransparency') and hasattr(node, 'getTransparency'):
imgui.text("透明度")
current_transparency = node.getTransparency()
# 将当前的透明度值转换为0.0-1.0范围用于显示
display_transparency = 1.0 - current_transparency if current_transparency <= 1 else 0.0
changed, new_transparency = imgui.slider_float("透明度", display_transparency, 0.0, 1.0)
if changed:
# 将0.0-1.0范围转换回Panda3D的透明度格式
panda_transparency = int((1.0 - new_transparency) * 255)
node.setTransparency(panda_transparency)
# 材质属性
self._draw_material_properties(node)
# 渲染状态
imgui.text("渲染状态")
if imgui.button("应用材质"):
self._apply_material_to_node(node)
imgui.same_line()
if imgui.button("重置材质"):
self._reset_material(node)
def _draw_material_properties(self, node):
"""绘制材质属性"""
materials = node.find_all_materials()
if not materials:
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质")
return
for i, material in enumerate(materials):
material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}"
if imgui.collapsing_header(f"材质: {material_name}"):
# 材质基础颜色
base_color = self._get_material_base_color(material)
if base_color:
imgui.text("基础颜色")
changed, new_r = imgui.slider_float(f"R##mat_r_{i}", base_color[0], 0.0, 1.0)
if changed:
self._update_material_base_color(material, 'r', new_r)
base_color = (new_r, base_color[1], base_color[2], base_color[3])
changed, new_g = imgui.slider_float(f"G##mat_g_{i}", base_color[1], 0.0, 1.0)
if changed:
self._update_material_base_color(material, 'g', new_g)
base_color = (base_color[0], new_g, base_color[2], base_color[3])
changed, new_b = imgui.slider_float(f"B##mat_b_{i}", base_color[2], 0.0, 1.0)
if changed:
self._update_material_base_color(material, 'b', new_b)
base_color = (base_color[0], base_color[1], new_b, base_color[3])
changed, new_a = imgui.slider_float(f"A##mat_a_{i}", base_color[3], 0.0, 1.0)
if changed:
self._update_material_base_color(material, 'a', new_a)
base_color = (base_color[0], base_color[1], base_color[2], new_a)
# PBR属性
if hasattr(material, 'roughness') and material.roughness is not None:
imgui.text("PBR属性")
try:
roughness_value = float(material.roughness)
changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0)
if changed:
self._update_material_roughness(material, new_roughness)
except:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "粗糙度: 不可用")
if hasattr(material, 'metallic') and material.metallic is not None:
try:
metallic_value = float(material.metallic)
changed, new_metallic = imgui.slider_float(f"金属性##metal_{i}", metallic_value, 0.0, 1.0)
if changed:
self._update_material_metallic(material, new_metallic)
except:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "金属性: 不可用")
if hasattr(material, 'refractive_index') and material.refractive_index is not None:
try:
ior_value = float(material.refractive_index)
changed, new_ior = imgui.slider_float(f"折射率##ior_{i}", ior_value, 1.0, 3.0)
if changed:
self._update_material_ior(material, new_ior)
except:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "折射率: 不可用")
# 材质预设
imgui.text("材质预设")
presets = ["默认", "金属", "塑料", "玻璃", "木材", "混凝土"]
current_preset = 0 # 默认选择
if imgui.begin_combo(f"预设##preset_{i}", presets[current_preset]):
for j, preset_name in enumerate(presets):
if imgui.selectable(preset_name, j == current_preset):
self._apply_material_preset(material, preset_name)
imgui.end_combo()
# 纹理信息
imgui.text("纹理贴图")
if imgui.button(f"选择漫反射贴图##diffuse_{i}"):
self._select_texture_for_material(node, material, "diffuse")
imgui.same_line()
if imgui.button(f"选择法线贴图##normal_{i}"):
self._select_texture_for_material(node, material, "normal")
imgui.same_line()
if imgui.button(f"选择粗糙度贴图##roughness_{i}"):
self._select_texture_for_material(node, material, "roughness")
if imgui.button(f"选择金属性贴图##metallic_{i}"):
self._select_texture_for_material(node, material, "metallic")
imgui.same_line()
if imgui.button(f"选择自发光贴图##emission_{i}"):
self._select_texture_for_material(node, material, "emission")
imgui.same_line()
if imgui.button(f"清除所有贴图##clear_{i}"):
self._clear_all_textures(node)
# 着色模型选择
self._draw_shading_model_panel(material, i)
# 显示当前纹理信息
self._display_current_textures(node, material)
def _draw_shading_model_panel(self, material, material_index):
"""绘制着色模型选择面板"""
try:
imgui.text("着色模型")
# RenderPipeline支持的着色模型
shading_models = ["默认", "自发光", "透明"]
current_model = 0 # 默认选择
# 安全地获取当前着色模型
try:
if hasattr(material, 'emission') and material.emission is not None:
current_model = int(material.emission.x)
except:
current_model = 0
# 着色模型选择
if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_model]):
for j, model_name in enumerate(shading_models):
if imgui.selectable(model_name, j == current_model):
self._update_shading_model(material, j)
imgui.end_combo()
# 如果是透明着色模型,添加透明度控制
if current_model == 3: # 透明着色模型
imgui.text("透明度设置")
try:
if hasattr(material, 'shading_model_param0'):
current_opacity = material.shading_model_param0
else:
current_opacity = 1.0
changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0)
if changed:
self._update_transparency(material, new_opacity)
except:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用")
except Exception as e:
print(f"绘制着色模型面板失败: {e}")