EG/ui/panels/app_actions.py

1928 lines
83 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

import os
import datetime
import json
import webbrowser
from imgui_bundle import imgui
from direct.showbase.ShowBaseGlobal import globalClock
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import WindowProperties
from project.project_schema import ensure_project_directories
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 _model_file_has_animation(self, file_path):
"""Detect whether a model file contains animation-related structures."""
try:
if not file_path or not os.path.exists(file_path):
return False
try:
stat_info = os.stat(file_path)
cache_key = (
os.path.abspath(file_path),
int(stat_info.st_mtime_ns),
int(stat_info.st_size),
)
animation_cache = getattr(self, "_model_animation_probe_cache", None)
if animation_cache is None:
animation_cache = {}
self._model_animation_probe_cache = animation_cache
cached = animation_cache.get(cache_key)
if cached is not None:
return bool(cached)
except Exception:
cache_key = None
try:
from scene.gltf_support import probe_gltf_metadata
gltf_meta = probe_gltf_metadata(file_path)
if gltf_meta.get("is_gltf"):
result = bool(gltf_meta.get("has_animations"))
if cache_key is not None:
animation_cache.clear()
animation_cache[cache_key] = result
return result
except Exception:
pass
loader = getattr(self, "loader", None)
if not loader:
return False
normalized_path = file_path
try:
from scene import util as scene_util
normalized_path = scene_util.normalize_model_path(file_path)
except Exception:
normalized_path = file_path
model = loader.loadModel(normalized_path)
if not model or model.isEmpty():
return False
try:
has_animation = (
model.findAllMatches("**/+Character").getNumPaths() > 0
or model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
)
finally:
try:
model.removeNode()
except Exception:
pass
result = bool(has_animation)
if cache_key is not None:
animation_cache.clear()
animation_cache[cache_key] = result
return result
except Exception:
return False
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._set_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 _set_hot_reload_enabled(self, enabled):
"""统一切换脚本热重载状态并同步 UI 状态。"""
if not hasattr(self, 'script_manager') or not self.script_manager:
raise RuntimeError("脚本管理器未初始化")
self.script_manager.set_hot_reload_enabled(enabled)
self.hotReloadEnabled = self.script_manager.hot_reload_enabled
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 = self._get_selection_node()
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 = self._get_selection_node()
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):
"""处理另存为项目菜单项"""
current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None)
if current_project_path:
self.save_as_project_name = f"{os.path.basename(current_project_path)}_副本"
self.save_as_project_path = os.path.dirname(current_project_path)
else:
self.save_as_project_name = "新项目"
self.save_as_project_path = os.getcwd()
self.add_info_message("打开另存为项目对话框")
self.show_save_as_dialog = True
def _on_build_project(self):
"""处理打包项目菜单项。"""
project_manager = getattr(self, "project_manager", None)
current_project_path = getattr(project_manager, "current_project_path", None)
if current_project_path and project_manager and hasattr(project_manager, "get_project_layout"):
layout = project_manager.get_project_layout(current_project_path)
_, active_profile = project_manager._get_active_build_profile()
profile_output_dir = str((active_profile or {}).get("output_dir", "") or "").strip()
if layout and profile_output_dir:
self.build_output_path = os.path.normpath(
os.path.join(current_project_path, profile_output_dir.replace("/", os.sep)))
else:
self.build_output_path = layout.builds_root if layout else os.path.dirname(current_project_path)
else:
self.build_output_path = os.getcwd()
self.add_info_message("打开打包项目对话框")
self.show_build_project_dialog = True
def _on_build_webgl_package(self):
"""处理“打包为 WebGL”菜单项。"""
project_manager = getattr(self, "project_manager", None)
if not project_manager:
self.add_error_message("项目管理器未初始化")
return
current_project_path = getattr(project_manager, "current_project_path", None)
if not current_project_path:
self.add_warning_message("请先创建或打开项目再进行WebGL打包")
return
if not self._save_project_impl():
self.add_error_message("打包前自动保存失败已取消WebGL打包")
return
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:
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 = str(report.get("status", "failed") or "failed")
out_dir = str(report.get("output_dir", "") or "")
report_path = os.path.join(out_dir, "reports", "export_report.json") if out_dir else ""
if ok:
missing_count = len(report.get("missing_assets", []) or [])
unsupported_count = len(report.get("unsupported_assets", []) or [])
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}")
return True
self.add_error_message("WebGL打包失败")
if report_path:
self.add_warning_message(f"请检查报告: {report_path}")
return False
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()
root.attributes("-topmost", True)
selected_dir = filedialog.askdirectory(
title=title,
initialdir=initial_dir,
mustexist=False,
parent=root,
)
root.destroy()
return os.path.normpath(selected_dir) if selected_dir else ""
except Exception as exc:
self._last_directory_dialog_error = str(exc)
return ""
def _build_project_impl(self, output_path):
"""执行项目打包。"""
if not hasattr(self, "project_manager") or not self.project_manager:
self.add_error_message("项目管理器未初始化")
return False
if not output_path or not output_path.strip():
self.add_warning_message("请选择打包输出目录")
return False
try:
build_result = self.project_manager.buildPackage(output_path.strip())
if build_result:
if isinstance(build_result, dict):
cook_summary = dict(build_result.get("cook_summary", {}) or {})
exe_path = str(build_result.get("exe_path", "") or "")
report_path = str(build_result.get("report_path", "") or "")
summary_path = str(build_result.get("summary_path", "") or "")
validation_ok = bool((build_result.get("validation", {}) or {}).get("ok", True))
summary_text = (
"项目打包成功"
f" | 场景 {cook_summary.get('scene_count', 0)}"
f" | 交互节点 {cook_summary.get('interactive_node_count', 0)}"
f" | 静态节点 {cook_summary.get('static_node_count', 0)}"
)
summary_text += " | 校验通过" if validation_ok else " | 校验异常"
if exe_path:
summary_text += f" | EXE: {exe_path}"
if report_path:
summary_text += f" | 报告: {report_path}"
if summary_path:
summary_text += f" | 摘要: {summary_path}"
self.add_success_message(summary_text)
else:
self.add_success_message("项目打包成功")
return True
self.add_error_message("项目打包失败")
return False
except Exception as e:
self.add_error_message(f"项目打包失败: {e}")
return False
def _show_about_dialog(self):
"""显示关于对话框。"""
self.show_about_dialog = True
def _get_documentation_target(self):
"""返回优先级最高的本地帮助文档。"""
candidate_names = [
"PROJECT_MODULE_INDEX.md",
"IMGUI_MODULE_ANALYSIS.md",
"PROJECT_FULL_CATALOG.md",
"PROJECT_OPTIMIZATION_ANALYSIS.md",
"AGENTS.md",
]
for candidate_name in candidate_names:
candidate_path = os.path.join(os.getcwd(), candidate_name)
if os.path.isfile(candidate_path):
return candidate_path
return None
def _open_documentation(self):
"""打开本地项目文档。"""
doc_path = self._get_documentation_target()
if not doc_path:
self.add_error_message("未找到可用的项目文档")
return False
try:
if os.name == "nt":
os.startfile(doc_path)
else:
webbrowser.open(f"file://{doc_path}")
self.add_success_message(f"已打开文档: {os.path.basename(doc_path)}")
return True
except Exception as e:
self.add_error_message(f"打开文档失败: {e}")
return False
def _on_exit(self):
"""处理退出菜单项"""
self.add_info_message("退出应用程序")
self.userExit()
# ==================== 键盘事件处理函数 ====================
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._get_selection_node() or self._get_ssbo_selection_summary():
self.selection.clearSelection()
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:
point = self._get_mouse_window_point()
if point and self._is_point_in_known_imgui_rects(point):
return True
try:
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
return True
except Exception:
pass
try:
if point and imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
return True
except Exception:
pass
return False
except Exception as e:
print(f"ImGui界面检测失败: {e}")
return False
def processImGuiMouseClick(self, x, y):
"""处理ImGui鼠标点击事件返回是否消费了该事件"""
try:
point = (float(x), float(y))
if self._is_point_in_known_imgui_rects(point):
return True
try:
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
return True
except Exception:
pass
try:
if imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
return True
except Exception:
pass
return False
except Exception as e:
print(f"ImGui鼠标点击处理失败: {e}")
return False
def _get_mouse_window_point(self):
try:
if not self.mouseWatcherNode.hasMouse():
return None
mouse_pos = self.mouseWatcherNode.getMouse()
if not mouse_pos:
return None
display_size = imgui.get_io().display_size
return (
float(mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2),
float(display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)),
)
except Exception:
return None
@staticmethod
def _point_in_rect(point, rect):
if not point or not rect:
return False
x, y = point
rx, ry, rw, rh = rect
return rx <= x <= rx + rw and ry <= y <= ry + rh
def _is_point_in_known_imgui_rects(self, point):
for rect_name in (
"_resource_manager_window_rect",
"_scene_tree_window_rect",
"_property_panel_window_rect",
"_script_panel_window_rect",
"_console_window_rect",
"_toolbar_window_rect",
):
if self._point_in_rect(point, getattr(self, rect_name, None)):
return True
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 _is_history_node_valid(self, node, require_attached=False):
if node is None:
return False
try:
if node.isEmpty():
return False
except Exception:
return False
if require_attached:
try:
return bool(node.hasParent())
except Exception:
return False
return True
def _has_active_ssbo_history_selection(self):
ssbo_editor = getattr(self, "ssbo_editor", None)
if not ssbo_editor or not hasattr(ssbo_editor, "has_active_selection"):
return False
try:
return bool(ssbo_editor.has_active_selection())
except Exception:
return False
def _sync_editor_state_after_history_change(self):
ssbo_editor = getattr(self, "ssbo_editor", None)
ssbo_active = self._has_active_ssbo_history_selection()
transform = getattr(self, "newTransform", None)
selection = getattr(self, "selection", None)
if ssbo_editor:
try:
if hasattr(ssbo_editor, "_sync_pick_scene_binding"):
ssbo_editor._sync_pick_scene_binding()
except Exception:
pass
try:
if hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
ssbo_editor.sync_scene_nodes_to_pick([])
except Exception:
pass
if selection:
if ssbo_active and ssbo_editor:
try:
selected_key = getattr(ssbo_editor, "selected_name", None)
if selected_key and hasattr(ssbo_editor, "select_node"):
ssbo_editor.select_node(selected_key, sync_world_selection=False)
if hasattr(ssbo_editor, "_sync_editor_selection_reference"):
ssbo_editor._sync_editor_selection_reference(ssbo_editor.get_selection_scene_node())
except Exception:
pass
else:
selected_node = None
try:
selected_node = selection.getSelectedNode() if hasattr(selection, "getSelectedNode") else getattr(
selection, "selectedNode", None)
except Exception:
selected_node = None
if not self._is_history_node_valid(selected_node, require_attached=True):
try:
selection.clearSelection()
except Exception:
try:
selection.selectedNode = None
selection.selectedObject = None
except Exception:
pass
monitored_node = getattr(self, "_monitored_node", None)
target_node = getattr(transform, "target_node", None) if transform else None
if not self._is_history_node_valid(monitored_node, require_attached=True):
if not (ssbo_active and self._is_history_node_valid(target_node, require_attached=True)):
try:
self.stop_transform_monitoring()
except Exception:
pass
if transform and (not self._is_history_node_valid(target_node, require_attached=True)):
if not ssbo_active:
try:
transform.detach()
except Exception:
pass
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._sync_editor_state_after_history_change()
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._sync_editor_state_after_history_change()
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._sync_editor_state_after_history_change()
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._sync_editor_state_after_history_change()
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._get_selection_source_node())
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._get_selection_source_node())
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._get_selection_source_node()
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
last_created_node = None
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 self.clipboard_mode == "cut" and i < len(source_nodes):
source_node = source_nodes[i]
if source_node and not source_node.isEmpty():
try:
from core.Command_System import AttachNodeCommand, CompositeCommand, DeleteNodeCommand
attach_cmd = AttachNodeCommand(source_node, parent_node, self)
if hasattr(self, "command_manager") and self.command_manager:
last_cmd = None
if hasattr(self.command_manager, "pop_last_command"):
last_cmd = self.command_manager.pop_last_command()
if isinstance(last_cmd, DeleteNodeCommand) and getattr(last_cmd, "node",
None) == source_node:
attach_cmd.execute()
self.command_manager.record_command(CompositeCommand([last_cmd, attach_cmd]))
new_node = source_node
else:
if last_cmd is not None:
self.command_manager.record_command(last_cmd)
self.command_manager.execute_command(attach_cmd)
new_node = source_node
else:
attach_cmd.execute()
new_node = source_node
except Exception as e:
print(f"[Paste] attach existing cut node failed, fallback to recreate: {e}")
if new_node is None and hasattr(self, "command_manager") and self.command_manager:
try:
from core.Command_System import CreateNodeCommand
create_cmd = CreateNodeCommand(_paste_create_node, parent_node, world=self)
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
last_created_node = new_node
# 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 last_created_node and hasattr(self, "selection") and self.selection:
try:
self.selection.updateSelection(last_created_node)
ssbo_editor = getattr(self, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_selection"):
ssbo_editor.sync_scene_selection(last_created_node)
except Exception as e:
print(f"[Paste] select pasted node failed: {e}")
# 如果是剪切模式且粘贴成功,清空剪切板
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._get_selection_source_node()
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))
deleting_ssbo_selection = bool(
ssbo_editor and
hasattr(ssbo_editor, "has_active_selection") and
ssbo_editor.has_active_selection()
)
if deleting_ssbo_selection and hasattr(ssbo_editor, "delete_selected_source_node"):
deleted = bool(ssbo_editor.delete_selected_source_node())
if deleted:
scene_manager = getattr(self, "scene_manager", None)
if scene_manager and hasattr(scene_manager, "models"):
rebuilt_model = getattr(ssbo_editor, "model", None)
scene_manager.models = []
if rebuilt_model and not rebuilt_model.isEmpty():
scene_manager.models.append(rebuilt_model)
print(f"[SSBO] 已从源场景树删除当前选择并重建: {node_name}")
return True
if deleting_ssbo_root and ssbo_editor and hasattr(ssbo_editor, "detach_source_child"):
detached = ssbo_editor.detach_source_child(child_name=node_name)
scene_manager = getattr(self, "scene_manager", None)
if scene_manager and hasattr(scene_manager, "models"):
scene_manager.models = [
model for model in scene_manager.models
if model and not model.isEmpty() and model != node
]
rebuilt_model = getattr(ssbo_editor, "model", None)
if rebuilt_model and not rebuilt_model.isEmpty() and rebuilt_model not in scene_manager.models:
scene_manager.models.append(rebuilt_model)
if detached is not None:
try:
ssbo_editor.clear_selection(sync_world_selection=False)
except Exception:
pass
print(f"[SSBO] 已从源模型树移除并重建运行时: {node_name}")
return True
# 创建删除命令
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"""
if not hasattr(self, 'project_manager') or not self.project_manager:
return False
return self.project_manager.saveProject()
def _open_project_impl(self, project_path):
"""打开项目的具体实现不依赖Qt"""
try:
if not hasattr(self, 'project_manager') or not self.project_manager:
print("✗ 项目管理器未初始化")
self.add_error_message("项目管理器未初始化")
return False
if not self.project_manager.openProject(project_path):
self.add_error_message(f"项目打开失败: {project_path}")
return False
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"""
try:
if not self.project_manager.createNewProject(path, name):
return False
self._update_window_title(name)
return True
except Exception as e:
print(f"创建项目失败: {e}")
return False
def _save_project_as_impl(self, name, path):
"""将当前项目保存到新的项目目录。"""
if not hasattr(self, "project_manager") or not self.project_manager:
self.add_error_message("项目管理器未初始化")
return False
if not name or not path:
self.add_warning_message("项目名称和保存位置不能为空")
return False
full_project_path = os.path.normpath(os.path.join(path, name))
current_project_path = (
os.path.normpath(self.project_manager.current_project_path)
if self.project_manager.current_project_path
else None
)
if current_project_path and os.path.normcase(full_project_path) == os.path.normcase(current_project_path):
self.add_warning_message("目标路径与当前项目相同,已执行普通保存")
return self._save_project_impl()
try:
if os.path.exists(full_project_path):
if not os.path.isdir(full_project_path):
self.add_error_message("目标路径已存在且不是文件夹")
return False
if os.listdir(full_project_path):
self.add_error_message("目标目录已存在且非空,请选择空目录或新的项目名称")
return False
else:
os.makedirs(full_project_path)
previous_project_path = self.project_manager.current_project_path
previous_project_config = dict(self.project_manager.project_config or {})
previous_scene_guid = getattr(self.project_manager, "current_scene_guid", None)
previous_scripts_dir = getattr(getattr(self, "script_manager", None), "scripts_directory", "")
layout = self.project_manager.get_project_layout(full_project_path)
ensure_project_directories(layout)
project_config = self.project_manager._create_default_project_config(full_project_path, name)
self.project_manager._write_project_config(full_project_path, project_config)
source_scripts_dir = ""
if previous_project_path:
source_scripts_dir = self.project_manager.get_project_scripts_dir(previous_project_path)
if not source_scripts_dir:
source_scripts_dir = previous_scripts_dir
target_scripts_dir = self.project_manager.get_project_scripts_dir(full_project_path)
if source_scripts_dir and os.path.exists(source_scripts_dir):
self._copy_directory_contents(source_scripts_dir, target_scripts_dir)
self.project_manager.current_project_path = full_project_path
self.project_manager.project_config = project_config
self.project_manager.current_scene_guid = project_config.get("startup_scene_guid")
self.project_manager._sync_project_script_manager(full_project_path, reload_scripts=True)
self.project_manager._sync_resource_manager_root(full_project_path)
if not self.project_manager.saveProject():
self.project_manager.current_project_path = previous_project_path
self.project_manager.project_config = previous_project_config
self.project_manager.current_scene_guid = previous_scene_guid
if previous_project_path:
self.project_manager._sync_project_script_manager(previous_project_path, reload_scripts=True)
self.project_manager._sync_resource_manager_root(previous_project_path)
self._update_window_title(
os.path.basename(previous_project_path) if previous_project_path else "未命名项目")
self.add_error_message("项目另存为失败")
return False
self._update_window_title(name)
self.add_success_message(f"项目另存为成功: {name}")
return True
except Exception as e:
self.add_error_message(f"项目另存为失败: {e}")
return False
def _copy_directory_contents(self, source_dir, target_dir):
"""Copy a directory tree into target_dir, replacing existing files."""
import shutil
if not source_dir or not os.path.exists(source_dir):
return
os.makedirs(target_dir, exist_ok=True)
for root, _, files in os.walk(source_dir):
rel_root = os.path.relpath(root, source_dir)
destination_root = target_dir if rel_root == "." else os.path.join(target_dir, rel_root)
os.makedirs(destination_root, exist_ok=True)
for file_name in files:
source_file = os.path.join(root, file_name)
target_file = os.path.join(destination_root, file_name)
shutil.copy2(source_file, target_file)
def _update_window_title(self, project_name):
"""更新窗口标题"""
try:
props = WindowProperties()
props.set_title(f"MetaCore - {project_name}")
self.win.request_properties(props)
print(f"窗口标题已更新: MetaCore - {project_name}")
except Exception as e:
print(f"更新窗口标题失败: {e}")
# ==================== 路径浏览器辅助方法 ====================
def _refresh_ssbo_runtime_import_bindings(self, file_path=None, scene_package_import=False):
ssbo_editor = getattr(self, 'ssbo_editor', None)
model_np = getattr(ssbo_editor, 'model', None) if ssbo_editor else None
scene_manager = getattr(self, 'scene_manager', None)
if not model_np:
return None
normalized_model_path = file_path
if file_path and not scene_package_import:
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
asset_guid = ""
asset_path = ""
if normalized_model_path:
model_np.setTag("model_path", normalized_model_path)
model_np.setTag("saved_model_path", normalized_model_path)
if file_path:
model_np.setTag("original_path", file_path)
model_np.setTag("file", os.path.basename(file_path))
project_manager = getattr(self, "project_manager", None)
if project_manager and hasattr(project_manager, "register_project_asset"):
try:
asset_record = project_manager.register_project_asset(file_path)
if asset_record:
if asset_record.get("guid"):
asset_guid = asset_record["guid"]
model_np.setTag("asset_guid", asset_guid)
if asset_record.get("asset_path"):
asset_path = asset_record["asset_path"]
model_np.setTag("asset_path", asset_path)
except Exception as e:
print(f"[SSBO] register_project_asset failed: {e}")
model_np.setTag("is_model_root", "1")
model_np.setTag("is_scene_element", "1")
model_np.setTag("ssbo_managed", "true")
ssbo_source_root = getattr(ssbo_editor, "source_model_root", None)
source_children = []
if ssbo_source_root is not None:
try:
source_children = [child for child in ssbo_source_root.getChildren() if not child.isEmpty()]
except Exception:
try:
source_children = [child for child in ssbo_source_root.get_children() if not child.is_empty()]
except Exception:
source_children = []
if scene_package_import:
if len(source_children) == 1:
try:
model_np.setName(source_children[0].getName())
except Exception:
pass
else:
# 项目场景加载不使用文件名(scene.bam)作为场景树根名称
model_np.setName("场景模型")
elif len(source_children) > 1:
model_np.setName("\u5bfc\u5165\u6a21\u578b")
elif file_path:
model_np.setName(os.path.basename(file_path))
elif len(source_children) == 1:
try:
model_np.setName(source_children[0].getName())
except Exception:
pass
target_source_child = None
last_import_root_name = getattr(ssbo_editor, "last_import_root_name", None) if ssbo_editor else None
if source_children:
if last_import_root_name:
for source_child in source_children:
try:
if source_child.getName() == last_import_root_name:
target_source_child = source_child
break
except Exception:
continue
if target_source_child is None:
target_source_child = source_children[-1]
if target_source_child is not None:
try:
if normalized_model_path:
target_source_child.setTag("model_path", normalized_model_path)
target_source_child.setTag("saved_model_path", normalized_model_path)
if file_path:
target_source_child.setTag("original_path", file_path)
target_source_child.setTag("file", os.path.basename(file_path))
if asset_guid:
target_source_child.setTag("asset_guid", asset_guid)
if asset_path:
target_source_child.setTag("asset_path", asset_path)
target_source_child.setTag("is_model_root", "1")
target_source_child.setTag("is_scene_element", "1")
target_source_child.setTag("ssbo_managed", "true")
except Exception as e:
print(f"[SSBO] sync source child tags failed: {e}")
if scene_manager and hasattr(scene_manager, 'models'):
try:
current_models = []
existing_models = list(getattr(scene_manager, "models", []) or [])
for candidate in existing_models:
if not candidate:
continue
try:
if candidate.isEmpty():
continue
except Exception:
try:
if candidate.is_empty():
continue
except Exception:
continue
if candidate == model_np:
continue
try:
if ssbo_editor and ssbo_editor.is_source_tree_node(candidate):
continue
except Exception:
pass
current_models.append(candidate)
if source_children:
current_models.extend(
child for child in source_children
if child and not child.isEmpty()
)
elif model_np and not model_np.isEmpty():
current_models.append(model_np)
deduped_models = []
for candidate in current_models:
if not candidate or candidate.isEmpty():
continue
if candidate not in deduped_models:
deduped_models.append(candidate)
scene_manager.models = deduped_models
except Exception as e:
print(f"[SSBO] sync scene_manager.models failed: {e}")
if scene_manager:
try:
scene_manager.setupCollision(model_np)
if scene_package_import:
model_np.setTag("has_animations", "false")
model_np.setTag("has_animations_checked", "true")
model_np.setTag("scene_import_source", "project_scene_bam")
elif file_path:
scene_manager._processModelAnimations(model_np)
except Exception as e:
print(f"[SSBO] setup components failed: {e}")
return model_np
def _execute_ssbo_import_command(self, file_path):
if not getattr(self, 'ssbo_editor', None):
return None
from core.Command_System import SnapshotStateCommand
import_state = {
'source_child': None,
'root_name': None,
}
def apply_state(state):
mode = state.get('mode') if isinstance(state, dict) else state
if mode == 'after':
source_child = import_state['source_child']
source_child_valid = False
if source_child:
try:
source_child_valid = not source_child.isEmpty()
except Exception:
try:
source_child_valid = not source_child.is_empty()
except Exception:
source_child_valid = False
if source_child_valid:
self.ssbo_editor.attach_source_child(source_child, highlight_root_name=import_state['root_name'])
else:
imported_root = self.ssbo_editor.load_model(
file_path,
keep_source_model=False,
append=True,
)
import_state['source_child'] = imported_root
import_state['root_name'] = getattr(self.ssbo_editor, 'last_import_root_name', None)
return self._refresh_ssbo_runtime_import_bindings(file_path=file_path)
source_child = import_state['source_child']
if source_child:
self.ssbo_editor.detach_source_child(child_np=source_child)
return self._refresh_ssbo_runtime_import_bindings()
command = SnapshotStateCommand(
apply_state,
{'mode': 'before'},
{'mode': 'after'},
)
self.command_manager.execute_command(command)
return getattr(self.ssbo_editor, 'model', None)
def _execute_import_command(self, file_path, scene_package_import=False):
if file_path and not scene_package_import:
project_manager = getattr(self, "project_manager", None)
if project_manager and hasattr(project_manager, "import_asset_into_project"):
try:
asset_record = project_manager.import_asset_into_project(file_path, preferred_subdir="Models")
if asset_record and asset_record.get("asset_path"):
project_root = getattr(project_manager, "current_project_path", None)
if project_root:
file_path = os.path.join(project_root, asset_record["asset_path"].replace("/", os.sep))
except Exception as e:
print(f"项目资源导入失败,回退直接导入: {e}")
animated_model = bool(file_path and self._model_file_has_animation(file_path))
command_manager = getattr(self, 'command_manager', None)
if scene_package_import or not command_manager:
return self._import_model_for_runtime(file_path, scene_package_import=scene_package_import)
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None) and not animated_model:
return self._execute_ssbo_import_command(file_path)
from core.Command_System import CreateNodeCommand
command = CreateNodeCommand(
lambda _parent: self._import_model_for_runtime(file_path, scene_package_import=False),
getattr(self, 'render', None),
world=self,
)
command_manager.execute_command(command)
return command.created_node
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False, scene_package_import=False):
"""Import model through the active runtime path.
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
Legacy mode: load via SceneManager.
"""
try:
self._scene_tree_epoch = int(getattr(self, "_scene_tree_epoch", 0) or 0) + 1
except Exception:
self._scene_tree_epoch = 1
animated_model = bool(file_path and self._model_file_has_animation(file_path))
if animated_model:
prefer_scene_manager = True
ssbo_editor = getattr(self, 'ssbo_editor', None)
if ssbo_editor and hasattr(ssbo_editor, "clear_selection"):
try:
ssbo_editor.clear_selection(sync_world_selection=False)
except Exception:
pass
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
if animated_model:
print(f"[AnimationImport] 检测到动画模型跳过SSBO导入: {file_path}")
else:
try:
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
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,
keep_source_model=scene_package_import,
append=not scene_package_import,
scene_package_import=scene_package_import,
)
return self._refresh_ssbo_runtime_import_bindings(
file_path=file_path,
scene_package_import=scene_package_import,
)
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 _log_render_runtime_stats(self, label):
"""Print renderer/task stats for import/open path comparison."""
try:
win = getattr(self, "win", None)
num_regions = win.getNumDisplayRegions() if win else 0
region_lines = []
if win:
for index in range(num_regions):
try:
dr = win.getDisplayRegion(index)
camera = dr.getCamera()
camera_name = "None"
if camera and not camera.isEmpty():
camera_name = camera.getName()
region_lines.append(
f"{index}:{dr.getSort()}:{int(bool(dr.isActive()))}:{camera_name}"
)
except Exception:
continue
graphics_engine = getattr(self, "graphicsEngine", None)
num_windows = graphics_engine.getNumWindows() if graphics_engine else 0
render = getattr(self, "render", None)
camera_count = 0
special_camera_counts = {}
if render and not render.isEmpty():
try:
camera_count = render.find_all_matches("**/+Camera").get_num_paths()
except Exception:
camera_count = 0
for camera_name in (
"gizmo_overlay_cam",
"pick_camera",
"selection_outline_mask_camera",
):
try:
special_camera_counts[camera_name] = render.find_all_matches(
f"**/{camera_name}"
).get_num_paths()
except Exception:
special_camera_counts[camera_name] = 0
task_mgr = getattr(self, "taskMgr", None) or getattr(self, "task_mgr", None)
task_names = []
if task_mgr:
try:
task_names = [task.name for task in list(task_mgr.getTasks())]
except Exception:
task_names = []
interesting_tasks = [
name for name in task_names
if (
"gizmo" in str(name).lower()
or "outline" in str(name).lower()
or "pick" in str(name).lower()
or "lui" in str(name).lower()
or "canvas" in str(name).lower()
)
]
print(
f"[RenderStats:{label}] regions={num_regions} windows={num_windows} "
f"render_cameras={camera_count} special={special_camera_counts} "
f"interesting_tasks={interesting_tasks}"
)
if region_lines:
print(f"[RenderStats:{label}] region_detail={' | '.join(region_lines)}")
except Exception:
pass
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._execute_import_command(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 and not getattr(self, "use_ssbo_mouse_picking", False):
model_node.setPos(0, 0, 0)
if hasattr(self.scene_manager, 'models') and not getattr(self, "use_ssbo_mouse_picking", False):
if model_node not in self.scene_manager.models:
self.scene_manager.models.append(model_node)
if select_model:
if (
getattr(self, "use_ssbo_mouse_picking", False)
and getattr(self, "ssbo_editor", None)
and getattr(self.ssbo_editor, "last_import_tree_key", None)
):
auto_select = True
selection_cost = None
estimate_cost_fn = getattr(self.ssbo_editor, "estimate_selection_cost", None)
if callable(estimate_cost_fn):
try:
selection_cost = estimate_cost_fn(self.ssbo_editor.last_import_tree_key)
except Exception:
selection_cost = None
if selection_cost:
object_count = int(selection_cost.get("object_count", 0) or 0)
chunk_count = int(selection_cost.get("chunk_count", 0) or 0)
auto_select = not (
object_count > 64
or chunk_count > 4
or (
bool(selection_cost.get("is_top_level_like"))
and (object_count > 16 or chunk_count > 1)
)
)
if not auto_select:
print(
"[SSBOImport] Skip auto-selection for heavy model "
f"(objects={object_count}, chunks={chunk_count})"
)
if auto_select:
self.ssbo_editor.select_node(self.ssbo_editor.last_import_tree_key)
else:
try:
self.ssbo_editor.clear_selection(sync_world_selection=True)
except Exception:
pass
try:
self.ssbo_editor.force_static_chunk_idle_state()
except Exception:
pass
elif hasattr(self, 'selection') and self.selection:
self.selection.updateSelection(model_node)
if show_success_message:
self.add_success_message(f"模型导入成功: {file_name}")
try:
ssbo_editor = getattr(self, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
if controller and hasattr(controller, "get_runtime_structure_stats"):
print(f"[ManualImport] runtime_structure={controller.get_runtime_structure_stats()}")
if ssbo_editor and hasattr(ssbo_editor, "get_source_tree_stats"):
print(f"[ManualImport] source_tree={ssbo_editor.get_source_tree_stats()}")
except Exception:
pass
self._log_render_runtime_stats("manual_import")
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