from imgui_bundle import imgui, imgui_ctx from pathlib import Path class EditorPanelsLeftMixin: """Auto-split mixin from editor_panels.py.""" 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(): # In SSBO mode, clicking model root should finally bind gizmo to # SSBO group proxy (not legacy model root). So run legacy # selection first, then force SSBO root selection at the end. force_ssbo_root_key = None ssbo_editor_ref = None try: ssbo_editor = getattr(self.app, "ssbo_editor", None) controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None root_key = getattr(controller, "tree_root_key", None) if controller else None if ( ssbo_editor and controller and ssbo_model and node == ssbo_model and root_key and root_key in controller.tree_nodes ): force_ssbo_root_key = root_key ssbo_editor_ref = ssbo_editor except Exception: pass if hasattr(self.app, 'selection') and self.app.selection: self.app.selection.updateSelection(node) if force_ssbo_root_key and ssbo_editor_ref: try: ssbo_editor_ref.select_node(force_ssbo_root_key) except Exception: pass # 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 tree_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 root_key = getattr(controller, "tree_root_key", None) if not root_key or root_key not in controller.tree_nodes: imgui.text_disabled("(无可用子节点)") return True root_node = controller.tree_nodes[root_key] if not root_node["children"]: imgui.text_disabled("(无可用子节点)") return True for child_key in root_node["children"]: self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key) return True def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key): """Recursively draw SSBO tree_nodes hierarchy in scene tree.""" node_data = controller.tree_nodes.get(key) if not node_data: return # Skip redundant wrapper nodes (e.g. ROOT), show their children instead. if controller.should_hide_tree_node(key): for child_key in node_data["children"]: self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key) return display = controller.display_names.get(key, key) obj_count = len(controller.name_to_ids.get(key, [])) children = node_data["children"] is_selected = (getattr(ssbo_editor, "selected_name", None) == key) if not children: # Leaf node: selectable label = f"{display} ({obj_count})##{key}" if imgui.selectable(label, is_selected)[0]: ssbo_editor.select_node(key) if hasattr(self.app, "lui_manager"): self.app.lui_manager.selected_index = -1 else: # Branch node: tree node flags = imgui.TreeNodeFlags_.open_on_arrow if is_selected: flags |= imgui.TreeNodeFlags_.selected label = f"{display} ({obj_count})##{key}" opened = imgui.tree_node_ex(label, flags) if imgui.is_item_clicked(0): ssbo_editor.select_node(key) if hasattr(self.app, "lui_manager"): self.app.lui_manager.selected_index = -1 if opened: for child_key in children: self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key) 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 self._update_resource_manager_window_rect(rm) imgui.text("文件浏览器") imgui.separator() self._draw_resource_toolbar_and_filters(rm) imgui.separator() rm.refresh_if_needed() dirs, files = rm.get_directory_contents(rm.current_path) self._draw_resource_directory_entries(rm, dirs) self._draw_resource_file_entries(rm, files) self._draw_resource_context_menu(rm) def _update_resource_manager_window_rect(self, rm): """更新资源管理器窗口矩形与根拖拽目标。""" 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), )) def _draw_resource_directory_entries(self, rm, dirs): """绘制当前目录下的文件夹列表。""" for dir_path in dirs: if not rm.should_show_file(dir_path): continue self._draw_resource_directory_entry(rm, dir_path) def _draw_resource_directory_entry(self, rm, dir_path: Path): """绘制单个文件夹节点(含一层展开内容)。""" 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 = self._load_resource_icon(icon_name) 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(): self._handle_resource_item_selection(rm, dir_path, is_selected) 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): self._open_resource_item_context_menu(rm, dir_path) if node_open: self._draw_resource_directory_children(rm, dir_path) imgui.tree_pop() def _draw_resource_directory_children(self, rm, parent_dir: Path): """绘制文件夹展开后的子目录与子文件(保持原有顺序)。""" subdirs, subfiles = rm.get_directory_contents(parent_dir) for subdir in subdirs: if not rm.should_show_file(subdir): continue self._draw_resource_subdir_entry(rm, subdir) for subfile in subfiles: if not rm.should_show_file(subfile): continue self._draw_resource_subfile_entry(rm, subfile) def _draw_resource_subdir_entry(self, rm, subdir: Path): """绘制一级子目录节点。""" subicon_name = rm.get_file_icon(subdir.name, is_folder=True) sub_is_selected = subdir in rm.selected_files subicon_texture = self._load_resource_icon(subicon_name) 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(): self._handle_resource_item_selection(rm, subdir, sub_is_selected) 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): self._open_resource_item_context_menu(rm, subdir) if sub_node_open: imgui.tree_pop() def _draw_resource_subfile_entry(self, rm, subfile: Path): """绘制一级子文件项。""" subicon_name = rm.get_file_icon(subfile.name) sub_is_selected = subfile in rm.selected_files subicon_texture = self._load_resource_icon(subicon_name) 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: self._handle_resource_item_selection(rm, subfile, sub_is_selected) if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): self._handle_resource_file_double_click(rm, subfile) if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): self._open_resource_item_context_menu(rm, subfile) def _draw_resource_file_entries(self, rm, files): """绘制当前目录下的文件列表。""" for file_path in files: if not rm.should_show_file(file_path): continue self._draw_resource_file_entry(rm, file_path) def _draw_resource_file_entry(self, rm, file_path: Path): """绘制顶层文件项。""" icon_name = rm.get_file_icon(file_path.name) is_selected = file_path in rm.selected_files icon_texture = self._load_resource_icon(icon_name) 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: self._handle_resource_item_selection(rm, file_path, is_selected) if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): self._handle_resource_file_double_click(rm, file_path) if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): self._open_resource_item_context_menu(rm, file_path) def _draw_resource_toolbar_and_filters(self, rm): """绘制资源管理器顶部工具条与筛选输入。保持原有按钮顺序与文案。""" 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 Exception: pass # 搜索框 changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) def _load_resource_icon(self, icon_name: str): """加载资源图标;失败时返回 None。""" try: return self.app.style_manager.load_icon(f"file_types/{icon_name}") except Exception: return None def _handle_resource_item_selection(self, rm, target_path: Path, is_selected: bool): """处理资源项选中逻辑(支持 Ctrl 多选)。""" if imgui.get_io().key_ctrl: if is_selected: rm.selected_files.discard(target_path) else: rm.selected_files.add(target_path) else: rm.selected_files.clear() rm.selected_files.add(target_path) rm.focused_file = target_path def _open_resource_item_context_menu(self, rm, target_path: Path): """打开资源项右键菜单。""" rm.show_context_menu = True rm.context_menu_file = target_path rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) imgui.open_popup("resource_context_menu") def _handle_resource_file_double_click(self, rm, file_path: Path): """处理文件双击:模型导入,其他文件打开。""" 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) def _draw_resource_context_menu(self, rm): """绘制资源管理器右键菜单。保持原有菜单项与顺序。""" 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