2419 lines
110 KiB
Python
2419 lines
110 KiB
Python
from imgui_bundle import imgui, imgui_ctx
|
||
import os
|
||
from pathlib import Path
|
||
from panda3d.core import PNMImage, StringStream, Texture
|
||
|
||
|
||
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_texture"):
|
||
self.app._imgui_webview_texture = None
|
||
if not hasattr(self.app, "_imgui_webview_tex_id"):
|
||
self.app._imgui_webview_tex_id = 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
|
||
self.app._imgui_webview_texture = 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
|
||
|
||
pnm = PNMImage()
|
||
if not pnm.read(StringStream(jpeg_bytes), "imgui_webview.png"):
|
||
webview.tex_dirty = False
|
||
return
|
||
# p3dimgui 纹理坐标系与网页截图存在Y轴方向差异,先在像素层修正
|
||
pnm.flip(False, True, False)
|
||
|
||
# 某些后端在“已注册纹理原位更新”时存在稳定性问题。
|
||
# 改为每次创建新纹理并替换旧纹理ID,避免原位更新导致崩溃。
|
||
texture = Texture("imgui_web_panel_texture")
|
||
texture.load(pnm)
|
||
new_tex_id = self.app.imgui.loadTexture(texture)
|
||
|
||
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
|
||
|
||
self.app._imgui_webview_texture = texture
|
||
self.app._imgui_webview_tex_id = new_tex_id
|
||
|
||
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}")
|
||
|
||
|