import os from imgui_bundle import imgui from direct.showbase.ShowBaseGlobal import globalClock from direct.task.TaskManagerGlobal import taskMgr from panda3d.core import WindowProperties class AppActions: """Project, input, messaging, edit actions, and import workflows.""" 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 _resolve_cut_copy_node(self, node): """Resolve selection to a stable scene root for copy/cut/paste.""" if not node or node.isEmpty(): return node current = node while current and not current.isEmpty(): if current.getName() == "render": break if current.hasTag("tree_item_type") or current.hasTag("is_model_root") or current.hasTag("is_scene_element"): return current current = current.getParent() return node def _toggle_hot_reload(self): """切换热重载状态""" if hasattr(self, 'script_manager') and self.script_manager: try: current_state = getattr(self.script_manager, 'hot_reload_enabled', False) self.script_manager.hot_reload_enabled = not current_state new_state = "启用" if not current_state else "禁用" self.add_success_message(f"热重载已{new_state}") print(f"[脚本系统] 热重载已{new_state}") except Exception as e: self.add_error_message(f"切换热重载失败: {str(e)}") print(f"[脚本系统] 切换热重载失败: {e}") def _create_new_script(self): """创建新脚本""" if not hasattr(self, '_new_script_name') or not self._new_script_name.strip(): self.add_error_message("请输入脚本名称") return script_name = self._new_script_name.strip() if not script_name.endswith('.py'): script_name += '.py' # 确定模板类型 template_map = { 0: "basic", 1: "movement", 2: "rotation", 3: "scale", 4: "animation" } template_type = template_map.get(getattr(self, '_selected_template', 0), "basic") try: if hasattr(self, 'script_manager') and self.script_manager: result = self.script_manager.create_script_file(script_name, template_type) if result: self.add_success_message(f"脚本 {script_name} 创建成功") print(f"[脚本系统] 创建脚本成功: {script_name}") # 刷新脚本列表 self._refresh_scripts_list() else: self.add_error_message(f"脚本 {script_name} 创建失败") else: self.add_error_message("脚本管理器未初始化") except Exception as e: self.add_error_message(f"创建脚本失败: {str(e)}") print(f"[脚本系统] 创建脚本失败: {e}") def _refresh_scripts_list(self): """刷新脚本列表""" try: if hasattr(self, 'script_manager') and self.script_manager: # 这里可以添加缓存逻辑,避免频繁刷新 available_scripts = self.script_manager.get_available_scripts() print(f"[脚本系统] 刷新脚本列表: {len(available_scripts)} 个脚本") self.add_success_message(f"脚本列表已刷新,共 {len(available_scripts)} 个脚本") else: self.add_error_message("脚本管理器未初始化") except Exception as e: self.add_error_message(f"刷新脚本列表失败: {str(e)}") print(f"[脚本系统] 刷新脚本列表失败: {e}") def _reload_all_scripts(self): """重载所有脚本""" try: if hasattr(self, 'script_manager') and self.script_manager: # 获取所有可用脚本并逐个重载 available_scripts = self.script_manager.get_available_scripts() success_count = 0 for script_name in available_scripts: if self.script_manager.reload_script(script_name): success_count += 1 self.add_success_message(f"重载完成: {success_count}/{len(available_scripts)} 个脚本成功") print(f"[脚本系统] 重载脚本: {success_count}/{len(available_scripts)} 成功") else: self.add_error_message("脚本管理器未初始化") except Exception as e: self.add_error_message(f"重载脚本失败: {str(e)}") print(f"[脚本系统] 重载脚本失败: {e}") def _on_script_selected(self, script_name): """处理脚本选择事件""" print(f"[脚本系统] 选择脚本: {script_name}") self.add_info_message(f"已选择脚本: {script_name}") def _edit_script(self, script_name): """编辑脚本""" try: if hasattr(self, 'script_manager') and self.script_manager: # 获取脚本信息 script_info = self.script_manager.get_script_info(script_name) if script_info and script_info.get("file"): script_path = script_info["file"] # 打开系统默认编辑器 import subprocess import platform system = platform.system() try: if system == "Windows": subprocess.run(['notepad', script_path]) elif system == "Darwin": # macOS subprocess.run(['open', script_path]) else: # Linux subprocess.run(['xdg-open', script_path]) self.add_success_message(f"已打开脚本编辑器: {script_name}") print(f"[脚本系统] 编辑脚本: {script_path}") except Exception as e: self.add_error_message(f"打开编辑器失败: {str(e)}") else: self.add_error_message(f"找不到脚本文件: {script_name}") else: self.add_error_message("脚本管理器未初始化") except Exception as e: self.add_error_message(f"编辑脚本失败: {str(e)}") print(f"[脚本系统] 编辑脚本失败: {e}") def _mount_script_to_selected(self, script_name): """挂载脚本到选中对象""" selected_node = None if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): selected_node = self.selection.selectedNode if not selected_node or selected_node.isEmpty(): self.add_error_message("请先选择一个对象") return try: if hasattr(self, 'script_manager') and self.script_manager: script_component = self.script_manager.add_script_to_object(selected_node, script_name) if script_component: self.add_success_message(f"脚本 {script_name} 已挂载到 {selected_node.getName()}") print(f"[脚本系统] 挂载脚本: {script_name} -> {selected_node.getName()}") else: self.add_error_message(f"挂载脚本 {script_name} 失败") else: self.add_error_message("脚本管理器未初始化") except Exception as e: self.add_error_message(f"挂载脚本失败: {str(e)}") print(f"[脚本系统] 挂载脚本失败: {e}") def _unmount_script_from_selected(self, script_name): """从选中对象卸载脚本""" selected_node = None if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): selected_node = self.selection.selectedNode if not selected_node or selected_node.isEmpty(): self.add_error_message("请先选择一个对象") return try: if hasattr(self, 'script_manager') and self.script_manager: result = self.script_manager.remove_script_from_object(selected_node, script_name) if result: self.add_success_message(f"脚本 {script_name} 已从 {selected_node.getName()} 卸载") print(f"[脚本系统] 卸载脚本: {script_name} <- {selected_node.getName()}") else: self.add_error_message(f"卸载脚本 {script_name} 失败") else: self.add_error_message("脚本管理器未初始化") except Exception as e: self.add_error_message(f"卸载脚本失败: {str(e)}") print(f"[脚本系统] 卸载脚本失败: {e}") # ==================== 菜单处理函数 ==================== def _on_new_project(self): """处理新建项目菜单项""" self.add_info_message("打开新建项目对话框") self.show_new_project_dialog = True def _on_open_project(self): """处理打开项目菜单项""" self.add_info_message("打开项目对话框") self.show_open_project_dialog = True def _on_save_project(self): """处理保存项目菜单项""" if hasattr(self, 'project_manager') and self.project_manager: try: # 检查是否有当前项目路径 if not self.project_manager.current_project_path: self.add_warning_message("没有当前项目路径,请先创建或打开项目") self.show_save_as_dialog = True return # 直接调用保存逻辑,避免Qt依赖 if self._save_project_impl(): self.add_success_message("项目保存成功") else: self.add_error_message("项目保存失败") except Exception as e: self.add_error_message(f"项目保存失败: {e}") else: self.add_error_message("项目管理器未初始化") def _on_save_as_project(self): """处理另存为项目菜单项""" self.add_info_message("另存为项目(功能待实现)") # TODO: 实现另存为对话框 # self.show_save_as_dialog = True def _on_build_webgl_package(self): """处理“打包为 WebGL”菜单项。""" if not hasattr(self, "project_manager") or not self.project_manager: self.add_error_message("项目管理器未初始化") return if not self.project_manager.current_project_path: self.add_warning_message("请先创建或打开项目,再进行WebGL打包") return # 导出前先保存当前项目 if not self._save_project_impl(): self.add_error_message("打包前自动保存失败,已取消WebGL打包") return current_project_path = self.project_manager.current_project_path initial_dir = os.path.dirname(current_project_path) if current_project_path else os.getcwd() selected_dir = self._select_directory_system_dialog("选择 WebGL 打包输出目录", initial_dir) if not selected_dir: dialog_error = getattr(self, "_last_directory_dialog_error", "") if dialog_error: # Fallback: when system dialog is unavailable (e.g. missing tkinter), # reuse the built-in path browser so user can still choose directory. self.add_warning_message(f"系统目录选择器不可用: {dialog_error}") self.path_browser_mode = "webgl_build" self.path_browser_current_path = initial_dir if os.path.isdir(initial_dir) else os.getcwd() self.path_browser_selected_path = self.path_browser_current_path self.show_path_browser = True self._pending_webgl_package = True self._refresh_path_browser() self.add_info_message("已切换到内置路径浏览器,请选择输出目录并点击确定") else: self.add_info_message("已取消WebGL打包") return self._execute_webgl_package(selected_dir) def _execute_webgl_package(self, selected_dir): """执行WebGL打包并输出消息。""" ok = self.project_manager.buildWebGLPackage(selected_dir) report = getattr(self.project_manager, "last_webgl_export_report", None) or {} status = report.get("status", "failed") out_dir = report.get("output_dir", "") report_path = os.path.join(out_dir, "reports", "export_report.json") if out_dir else "" if ok: missing_count = len(report.get("missing_assets", [])) unsupported_count = len(report.get("unsupported_assets", [])) if status == "partial": self.add_warning_message( f"WebGL打包部分成功: 缺失资源 {missing_count},不支持资源 {unsupported_count}" ) else: self.add_success_message("WebGL打包成功") if out_dir: self.add_info_message(f"输出目录: {out_dir}") if report_path: self.add_info_message(f"报告: {report_path}") else: self.add_error_message("WebGL打包失败") if report_path: self.add_warning_message(f"请检查报告: {report_path}") def _on_exit(self): """处理退出菜单项""" self.add_info_message("退出应用程序") self.userExit() def _select_directory_system_dialog(self, title, initial_dir=""): """打开系统目录选择器并返回目录路径。""" self._last_directory_dialog_error = "" try: import tkinter as tk from tkinter import filedialog if not initial_dir or (not os.path.isdir(initial_dir)): initial_dir = os.getcwd() root = tk.Tk() root.withdraw() try: root.attributes("-topmost", True) except Exception: pass selected = filedialog.askdirectory( title=title or "选择目录", initialdir=initial_dir, mustexist=True, ) root.destroy() return os.path.normpath(selected) if selected else "" except Exception as e: print(f"目录选择器打开失败: {e}") self._last_directory_dialog_error = str(e) return "" # ==================== 键盘事件处理函数 ==================== def _on_ctrl_pressed(self): """Ctrl键按下""" self.ctrl_pressed = True def _on_ctrl_released(self): """Ctrl键释放""" self.ctrl_pressed = False def _on_alt_pressed(self): """Alt键按下""" self.alt_pressed = True def _on_alt_released(self): """Alt键释放""" self.alt_pressed = False def _on_n_pressed(self): """N键按下 - 检查Ctrl+N组合键""" if self.ctrl_pressed: self._on_new_project() def _on_o_pressed(self): """O键按下 - 检查Ctrl+O组合键""" if self.ctrl_pressed: self._on_open_project() def _on_f4_pressed(self): """F4键按下 - 检查Alt+F4组合键""" if self.alt_pressed: self._on_exit() # 移除了单独的按键处理方法,现在直接使用组合键事件 def _on_delete_pressed(self): """Delete键按下 - 删除选中节点""" self._on_delete() def _on_escape_pressed(self): """Escape键按下 - 取消所有选区""" # 1. 取消 LUI 组件选择 if hasattr(self, 'lui_manager'): if self.lui_manager.selected_index >= 0: self.lui_manager.selected_index = -1 print("✓ 已取消LUI组件选中") # 2. 取消 3D 场景选择 if hasattr(self, 'selection') and self.selection: if self.selection.selectedNode: self.selection.selectedNode = None self.selection.clearSelectionBox() self.selection.clearGizmo() print("✓ 已取消场景节点选中") def _on_wheel_up(self): """滚轮向上滚动 - 相机前进""" try: if not self.camera_control_enabled: return # 检查鼠标是否在ImGui窗口上 if self._is_mouse_over_imgui(): return # 沿相机前向向量移动 forward = self.camera.getMat().getRow3(1) distance = 20.0 * globalClock.getDt() currentPos = self.camera.getPos() newPos = currentPos + forward * distance self.camera.setPos(newPos) except Exception as e: print(f"滚轮前进失败: {e}") def _on_wheel_down(self): """滚轮向下滚动 - 相机后退""" try: # 检查鼠标是否在ImGui窗口上 if self._is_mouse_over_imgui(): return # 沿相机前向向量移动 forward = self.camera.getMat().getRow3(1) distance = -20.0 * globalClock.getDt() currentPos = self.camera.getPos() newPos = currentPos + forward * distance self.camera.setPos(newPos) except Exception as e: print(f"滚轮后退失败: {e}") def _is_mouse_over_imgui(self): """检测鼠标是否在ImGui窗口上""" try: # 检查是否有任何ImGui窗口想要捕获鼠标 if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse: return True # 检查鼠标是否在任何ImGui窗口内 mouse_pos = self.mouseWatcherNode.getMouse() if not mouse_pos: return False # 简单的边界检查(可以根据需要扩展) display_size = imgui.get_io().display_size mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2 mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2) # 检查是否在常见的ImGui界面区域内 # 这里可以根据实际的ImGui窗口位置进行更精确的检测 if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏) return True if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏) return True if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域 return True return False except Exception as e: print(f"ImGui界面检测失败: {e}") return False def processImGuiMouseClick(self, x, y): """处理ImGui鼠标点击事件,返回是否消费了该事件""" try: # ImGui优先策略:如果ImGui想要捕获鼠标,则由ImGui处理 if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse: return True # 检查是否有任何ImGui窗口悬停 try: if imgui.is_any_window_hovered(): return True except AttributeError: # 如果方法不存在,跳过这个检查 pass # 检查鼠标是否在ImGui界面区域内 if self._is_mouse_over_imgui(): return True # 如果以上条件都不满足,则让3D场景处理该事件 return False except Exception as e: print(f"ImGui鼠标点击处理失败: {e}") return False # ==================== 消息系统 ==================== def add_message(self, text, color=(1.0, 1.0, 1.0, 1.0)): """添加消息到消息列表,同时输出到终端""" import datetime timestamp = datetime.datetime.now().strftime("%H:%M:%S") # 输出到终端 print(f"[{timestamp}] {text}") # 添加到GUI消息列表 self.messages.append({ 'text': text, 'color': color, 'timestamp': timestamp }) # 限制消息数量 if len(self.messages) > self.max_messages: self.messages = self.messages[-self.max_messages:] def add_success_message(self, text): """添加成功消息""" self.add_message(f"✓ {text}", (0.176, 1.0, 0.769, 1.0)) def add_error_message(self, text): """添加错误消息""" self.add_message(f"✗ {text}", (1.0, 0.3, 0.3, 1.0)) def add_warning_message(self, text): """添加警告消息""" self.add_message(f"⚠ {text}", (0.953, 0.616, 0.471, 1.0)) def add_info_message(self, text): """添加信息消息""" self.add_message(f"ℹ {text}", (0.157, 0.620, 1.0, 1.0)) # ==================== 编辑菜单功能实现 ==================== def _on_undo(self): """处理撤销操作""" try: # 1) 优先使用命令系统(删除/创建等命令) if hasattr(self, 'command_manager') and self.command_manager and self.command_manager.can_undo(): success = self.command_manager.undo() if success: self.add_success_message("撤销操作成功") return self.add_error_message("撤销操作失败") return # 2) 回退到 TransformGizmo 历史(拖拽位移/旋转/缩放) tg = getattr(self, 'newTransform', None) if tg and hasattr(tg, 'undo_last') and tg.undo_last(): self.add_success_message("撤销操作成功") return self.add_warning_message("没有可撤销的操作") except Exception as e: self.add_error_message(f"撤销操作失败: {e}") def _on_redo(self): """处理重做操作""" try: # 1) 优先使用命令系统 if hasattr(self, 'command_manager') and self.command_manager and self.command_manager.can_redo(): success = self.command_manager.redo() if success: self.add_success_message("重做操作成功") return self.add_error_message("重做操作失败") return # 2) 回退到 TransformGizmo 重做栈 tg = getattr(self, 'newTransform', None) if tg and hasattr(tg, 'redo_last') and tg.redo_last(): self.add_success_message("重做操作成功") return self.add_warning_message("没有可重做的操作") except Exception as e: self.add_error_message(f"重做操作失败: {e}") def _on_copy(self): """处理复制操作""" try: if not hasattr(self, 'selection') or not self.selection: self.add_error_message("选择系统未初始化") return # 获取当前选中的节点 selected_node = self._resolve_cut_copy_node(self.selection.selectedNode) if not selected_node: self.add_warning_message("没有选中的节点") return # 检查节点有效性(不能复制根节点) if selected_node.getName() == "render": self.add_warning_message("不能复制根节点") return # 序列化节点 if hasattr(self, 'scene_manager') and self.scene_manager: node_data = self.scene_manager.serializeNodeForCopy(selected_node) if node_data: self.clipboard = [node_data] # Keep live source nodes for robust copy->paste clone path. self.clipboard_source_nodes = [selected_node] self.clipboard_mode = "copy" self.add_success_message(f"已复制节点: {selected_node.getName()}") else: self.add_error_message("节点序列化失败") else: self.add_error_message("场景管理器未初始化") except Exception as e: self.add_error_message(f"复制操作失败: {e}") def _on_cut(self): """处理剪切操作""" try: if not hasattr(self, 'selection') or not self.selection: self.add_error_message("选择系统未初始化") return # 获取当前选中的节点 selected_node = self._resolve_cut_copy_node(self.selection.selectedNode) if not selected_node: self.add_warning_message("没有选中的节点") return # 检查节点有效性(不能剪切根节点和系统节点) node_name = selected_node.getName() if node_name == "render": self.add_warning_message("不能剪切根节点") return # 序列化节点 if hasattr(self, 'scene_manager') and self.scene_manager: node_data = self.scene_manager.serializeNodeForCopy(selected_node) if node_data: self.clipboard = [node_data] # Cut preserves the source node references for cloning in paste self.clipboard_source_nodes = [selected_node] self.clipboard_mode = "cut" # 删除原节点 if self._delete_node(selected_node): self.selection.clearSelection() self.add_success_message(f"已剪切节点: {node_name}") else: self.add_error_message(f"剪切失败,无法删除节点: {node_name}") else: self.add_error_message("节点序列化失败") else: self.add_error_message("场景管理器未初始化") except Exception as e: self.add_error_message(f"剪切操作失败: {e}") def _on_paste(self): """处理粘贴操作""" try: if not self.clipboard: self.add_warning_message("剪切板为空") return if not hasattr(self, 'scene_manager') or not self.scene_manager: self.add_error_message("场景管理器未初始化") return # 确定粘贴目标父节点 parent_node = None if hasattr(self, 'selection') and self.selection: selected_node = self.selection.selectedNode if selected_node and not selected_node.isEmpty(): # Paste as sibling by default (not as child of selected node), # which matches editor expectations and avoids "pasted but invisible". if selected_node.getName() == "render": parent_node = selected_node else: p = selected_node.getParent() parent_node = p if p and not p.isEmpty() else self.render # 如果没有选中节点,使用渲染根节点 if not parent_node: parent_node = self.render # 反序列化并添加节点 created_any = False source_nodes = getattr(self, "clipboard_source_nodes", []) or [] for i, node_data in enumerate(self.clipboard): def _paste_create_node(parent): created = None # Copy mode: prefer direct NodePath clone to preserve visual geometry. if self.clipboard_mode in ("copy", "cut") and i < len(source_nodes): source_node = source_nodes[i] if source_node and not source_node.isEmpty(): try: if self.clipboard_mode == "cut": source_node.reparentTo(parent) created = source_node else: created = source_node.copyTo(parent) if hasattr(self.scene_manager, "_generateUniqueName"): unique_name = self.scene_manager._generateUniqueName(source_node.getName(), parent) created.setName(unique_name) # Preserve model source tags so later cut/paste can rebuild real model. for _tag in ("model_path", "saved_model_path", "original_path", "file", "element_type"): if source_node.hasTag(_tag): created.setTag(_tag, source_node.getTag(_tag)) # Offset slightly so the new node can be seen immediately. created.setPos(created.getX() + 0.2, created.getY() + 0.2, created.getZ()) except Exception: created = None # Fallback: recreate from serialized data. if not created: if hasattr(self.scene_manager, "recreateNodeFromData"): created = self.scene_manager.recreateNodeFromData(node_data, parent) else: created = self.scene_manager.deserializeNode(node_data, parent) return created new_node = None if hasattr(self, "command_manager") and self.command_manager: try: from core.Command_System import CreateNodeCommand create_cmd = CreateNodeCommand(_paste_create_node, parent_node) self.command_manager.execute_command(create_cmd) new_node = create_cmd.created_node except Exception as e: print(f"[Paste] command create failed, fallback direct create: {e}") new_node = _paste_create_node(parent_node) else: new_node = _paste_create_node(parent_node) if new_node: created_any = True # Ensure pasted model can be picked by legacy collision fallback. try: if hasattr(self, "scene_manager") and self.scene_manager: if not new_node.hasTag("tree_item_type"): new_node.setTag("tree_item_type", "IMPORTED_MODEL_NODE") new_node.setTag("is_model_root", "1") new_node.setTag("is_scene_element", "1") self.scene_manager.setupCollision(new_node) if hasattr(self.scene_manager, "models") and new_node not in self.scene_manager.models: self.scene_manager.models.append(new_node) except Exception as _e: print(f"[Paste] setup collision for pasted node failed: {_e}") self.add_success_message(f"已粘贴节点: {new_node.getName()}") else: self.add_error_message("节点反序列化失败") # 如果是剪切模式且粘贴成功,清空剪切板 if created_any and self.clipboard_mode == "cut": self.clipboard = [] self.clipboard_source_nodes = [] self.clipboard_mode = "" except Exception as e: self.add_error_message(f"粘贴操作失败: {e}") def _on_delete(self): """处理删除操作""" try: if not hasattr(self, 'selection') or not self.selection: self.add_error_message("选择系统未初始化") return # 获取当前选中的节点 selected_node = self.selection.selectedNode if not selected_node: self.add_warning_message("没有选中的节点") return # 检查节点有效性(不能删除根节点) node_name = selected_node.getName() if node_name == "render": self.add_warning_message("不能删除根节点") return # 删除节点 if hasattr(self, 'scene_manager') and self.scene_manager: self._delete_node(selected_node) self.selection.clearSelection() self.add_success_message(f"已删除节点: {node_name}") else: self.add_error_message("场景管理器未初始化") except Exception as e: self.add_error_message(f"删除操作失败: {e}") def _delete_node(self, node): """删除节点的通用方法 - 使用命令系统""" try: if not node or node.isEmpty(): return False node_name = node.getName() or "未命名节点" parent = node.getParent() ssbo_editor = getattr(self, "ssbo_editor", None) ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None deleting_ssbo_root = bool(ssbo_model and (node == ssbo_model)) # 创建删除命令 if hasattr(self, 'command_manager') and self.command_manager: from core.Command_System import DeleteNodeCommand command = DeleteNodeCommand(node, parent, self) self.command_manager.execute_command(command) print(f"[命令系统] 创建删除命令: {node_name}") else: # 备用方案:直接删除并执行清理 print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}") self._perform_node_cleanup(node) node.removeNode() if deleting_ssbo_root and ssbo_editor: try: ssbo_editor.on_model_deleted(node) except Exception as e: print(f"[SSBO] 删除模型后清理失败: {e}") print(f"[删除] 成功删除节点: {node_name}") return True except Exception as e: print(f"[删除] 删除节点失败: {e}") return False def _perform_node_cleanup(self, node): """执行节点清理逻辑""" try: node_name = node.getName() or "未命名节点" # 从场景管理器的模型列表中移除(如果是模型) if hasattr(self, 'scene_manager') and self.scene_manager: if node in self.scene_manager.models: self.scene_manager.models.remove(node) print(f"[场景管理器] 从模型列表移除: {node_name}") # 停止所有与该节点相关的脚本 if hasattr(self, 'script_manager') and self.script_manager: try: # 移除该节点上的所有脚本 if node in self.script_manager.object_scripts: del self.script_manager.object_scripts[node] print(f"[脚本系统] 移除节点 {node_name} 的所有脚本") except Exception as e: print(f"[脚本系统] 移除脚本失败: {e}") # 清理碰撞体 if hasattr(self, 'collision_manager') and self.collision_manager: try: self.collision_manager.remove_collision_for_node(node) print(f"[碰撞系统] 移除节点 {node_name} 的碰撞体") except Exception as e: print(f"[碰撞系统] 移除碰撞体失败: {e}") # 清理Actor缓存(如果有动画) if hasattr(self, '_actor_cache') and node in self._actor_cache: actor = self._actor_cache[node] try: # 清理相关任务 taskMgr.remove(f"maintain_anim_pos_{id(actor)}") # 清理Actor if not actor.isEmpty(): actor.cleanup() actor.removeNode() print(f"[动画系统] 清理节点 {node_name} 的Actor缓存") except Exception as e: print(f"[动画系统] 清理Actor缓存失败: {e}") finally: del self._actor_cache[node] except Exception as e: print(f"[清理] 节点清理失败: {e}") # ==================== 对话框绘制函数 ==================== def _create_new_project(self, name, path): """创建新项目的实际实现""" if not hasattr(self, 'project_manager') or not self.project_manager: print("✗ 项目管理器未初始化") return try: if self._create_new_project_impl(name, path): print(f"✓ 项目创建成功: {name}") else: print(f"✗ 项目创建失败: {name}") except Exception as e: print(f"✗ 项目创建失败: {e}") def _open_project_path(self, path): """打开项目的实际实现""" if not hasattr(self, 'project_manager') or not self.project_manager: print("✗ 项目管理器未初始化") return try: print(f"打开项目: {path}") if self._open_project_impl(path): print(f"✓ 项目打开成功: {path}") else: print(f"✗ 项目打开失败: {path}") except Exception as e: print(f"✗ 项目打开失败: {e}") # ==================== 项目管理具体实现 ==================== def _save_project_impl(self): """保存项目的具体实现(不依赖Qt)""" import json import datetime import os project_path = self.project_manager.current_project_path scenes_path = os.path.join(project_path, "scenes") # 固定的场景文件名 scene_file = os.path.join(scenes_path, "scene.bam") # 如果存在旧文件,先删除 if os.path.exists(scene_file): try: os.remove(scene_file) print(f"已删除旧场景文件: {scene_file}") except Exception as e: print(f"删除旧场景文件失败: {str(e)}") return False # 保存场景 if self.scene_manager.saveScene(scene_file, project_path): # 更新项目配置文件 config_file = os.path.join(project_path, "project.json") if os.path.exists(config_file): with open(config_file, "r", encoding="utf-8") as f: project_config = json.load(f) # 更新最后修改时间 project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 记录场景文件路径 project_config["scene_file"] = os.path.relpath(scene_file, project_path) with open(config_file, "w", encoding="utf-8") as f: json.dump(project_config, f, ensure_ascii=False, indent=4) # 更新项目配置 self.project_manager.project_config = project_config return True return False def _open_project_impl(self, project_path): """打开项目的具体实现(不依赖Qt)""" import json import datetime import os try: # 检查项目管理器是否已初始化 if not hasattr(self, 'project_manager') or not self.project_manager: print("✗ 项目管理器未初始化") self.add_error_message("项目管理器未初始化") return False # 检查场景管理器是否已初始化 if not hasattr(self, 'scene_manager') or not self.scene_manager: print("✗ 场景管理器未初始化") self.add_error_message("场景管理器未初始化") return False # 检查是否是有效的项目文件夹 config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): print(f"⚠ 选择的不是有效的项目文件夹: {project_path}") self.add_warning_message(f"选择的不是有效的项目文件夹: {project_path}") return False # 读取项目配置 try: with open(config_file, "r", encoding="utf-8") as f: project_config = json.load(f) except Exception as e: print(f"✗ 读取项目配置文件失败: {e}") self.add_error_message(f"读取项目配置文件失败: {e}") return False # 检查场景文件 scene_file = os.path.join(project_path, "scenes", "scene.bam") if os.path.exists(scene_file): if getattr(self, "use_ssbo_mouse_picking", False) and callable(getattr(self, "_import_model_for_runtime", None)): self.use_ssbo_scene_import = True # 加载场景 try: if self.scene_manager.loadScene(scene_file): # 更新项目配置 project_config["scene_file"] = os.path.relpath(scene_file, project_path) print(f"✓ 场景加载成功: {scene_file}") else: print(f"⚠ 场景加载失败: {scene_file}") self.add_warning_message(f"场景加载失败: {scene_file}") except Exception as e: print(f"✗ 加载场景时发生错误: {e}") self.add_error_message(f"加载场景时发生错误: {e}") # 继续执行,不阻止项目打开 # 更新项目配置 project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: with open(config_file, "w", encoding="utf-8") as f: json.dump(project_config, f, ensure_ascii=False, indent=4) except Exception as e: print(f"✗ 保存项目配置失败: {e}") self.add_error_message(f"保存项目配置失败: {e}") # 更新项目状态 self.project_manager.current_project_path = project_path self.project_manager.project_config = project_config # 更新窗口标题 project_name = os.path.basename(project_path) self._update_window_title(project_name) print(f"✓ 项目打开成功: {project_path}") self.add_success_message(f"项目打开成功: {project_name}") return True except Exception as e: print(f"✗ 打开项目时发生错误: {e}") self.add_error_message(f"打开项目时发生错误: {e}") return False def _create_new_project_impl(self, name, path): """创建新项目的具体实现(不依赖Qt)""" import json import datetime import os full_project_path = os.path.normpath(os.path.join(path, name)) print(f"创建项目路径: {full_project_path}") try: # 创建项目文件夹结构 os.makedirs(full_project_path) os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹 os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹 scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹 os.makedirs(scenes_path) # 创建项目配置文件 project_config = { "name": name, "path": full_project_path, "created": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "version": "1.0", "scene_file": "scenes/scene.bam" } # 保存项目配置 config_file = os.path.join(full_project_path, "project.json") with open(config_file, "w", encoding="utf-8") as f: json.dump(project_config, f, ensure_ascii=False, indent=4) # 保存初始场景 scene_file = os.path.join(scenes_path, "scene.bam") self.scene_manager.saveScene(scene_file, full_project_path) # 更新项目管理器状态 self.project_manager.current_project_path = full_project_path self.project_manager.project_config = project_config # 更新窗口标题 self._update_window_title(name) return True except Exception as e: print(f"创建项目失败: {e}") return False def _update_window_title(self, project_name): """更新窗口标题""" try: props = WindowProperties() props.set_title(f"EG Engine - {project_name}") self.win.request_properties(props) print(f"窗口标题已更新: EG Engine - {project_name}") except Exception as e: print(f"更新窗口标题失败: {e}") # ==================== 路径浏览器辅助方法 ==================== def _import_model_for_runtime(self, file_path, prefer_scene_manager=False): """Import model through the active runtime path. SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager). Legacy mode: load via SceneManager. """ if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None): try: # Clear selection/gizmo first, then import as an additional scene model. if hasattr(self, 'selection') and self.selection: try: self.selection.clearSelection() except Exception: try: self.selection.updateSelection(None) except Exception: pass self.ssbo_editor.load_model(file_path) model_np = getattr(self.ssbo_editor, 'model', None) normalized_import_path = str(file_path).replace("\\", "/").lower() is_project_scene_bam = ( normalized_import_path.endswith(".bam") and "/scenes/" in normalized_import_path ) # Keep legacy ray-pick fallback usable by adding a collision body. if model_np: try: from scene import util as scene_util normalized_model_path = scene_util.normalize_model_path(file_path) except Exception: normalized_model_path = file_path # Apply vital tags manually since SSBO overrides SceneManager loader model_np.setTag("model_path", normalized_model_path) model_np.setTag("original_path", file_path) model_np.setTag("saved_model_path", normalized_model_path) model_np.setTag("is_model_root", "1") model_np.setTag("is_scene_element", "1") model_np.setTag("file", os.path.basename(file_path)) model_np.setName(os.path.basename(file_path)) if hasattr(self, 'scene_manager') and self.scene_manager: try: if not is_project_scene_bam: self.scene_manager.setupCollision(model_np) self.scene_manager._processModelAnimations(model_np) else: model_np.setTag("has_animations", "false") model_np.setTag("has_animations_checked", "true") if hasattr(self.scene_manager, "models") and model_np not in self.scene_manager.models: self.scene_manager.models.append(model_np) except Exception as e: print(f"[SSBO] setup components failed: {e}") return model_np except Exception as e: print(f"[SSBO] load_model failed: {e}") return None # Legacy fallback if hasattr(self, 'scene_manager') and self.scene_manager: return self.scene_manager.importModel(file_path) return None def _import_model_with_menu_logic( self, file_path, select_model=True, set_origin=True, show_info_message=True, show_success_message=True, ): """统一的单文件导入入口,保持与菜单导入一致的处理流程。""" try: if not file_path: self.add_error_message("请选择要导入的文件") return None normalized_path = os.fspath(file_path) if not os.path.exists(normalized_path): self.add_error_message(f"文件不存在: {normalized_path}") return None file_ext = os.path.splitext(normalized_path)[1].lower() if file_ext not in self.supported_formats: self.add_error_message(f"不支持的文件格式: {file_ext}") return None if not hasattr(self, 'scene_manager') or not self.scene_manager: self.add_error_message("场景管理器未初始化") return None file_name = os.path.basename(normalized_path) if show_info_message: self.add_info_message(f"正在导入模型: {file_name}") model_node = self._import_model_for_runtime(normalized_path) if not model_node: self.add_error_message("模型导入失败") return None # SSBO 模式下保留原始材质/纹理,避免模型发黑。 if not getattr(self, "use_ssbo_mouse_picking", False): if hasattr(self.scene_manager, 'processMaterials'): self.scene_manager.processMaterials(model_node) if show_info_message: self.add_info_message("已应用默认材质") try: model_node.clearMaterial() model_node.clearTexture() if hasattr(self.scene_manager, 'processMaterials'): self.scene_manager.processMaterials(model_node) try: color = model_node.getColor() if color and len(color) >= 4 and color == (1, 1, 1, 1): model_node.setColor(0.8, 0.8, 0.8, 1.0) elif not color: model_node.setColor(0.8, 0.8, 0.8, 1.0) except Exception: model_node.setColor(0.8, 0.8, 0.8, 1.0) except Exception as e: self.add_warning_message(f"材质处理警告: {e}") if set_origin: model_node.setPos(0, 0, 0) if hasattr(self.scene_manager, 'models') and model_node not in self.scene_manager.models: self.scene_manager.models.append(model_node) if select_model and hasattr(self, 'selection') and self.selection: self.selection.updateSelection(model_node) if show_success_message: self.add_success_message(f"模型导入成功: {file_name}") return model_node except Exception as e: self.add_error_message(f"导入模型失败: {e}") return None def _on_import_model(self): """处理导入模型菜单项""" self.add_info_message("打开系统文件选择器") self.show_import_dialog = True def _import_model(self): """导入模型的具体实现""" model_node = self._import_model_with_menu_logic(self.import_file_path) self.import_file_path = "" return model_node