1928 lines
83 KiB
Python
1928 lines
83 KiB
Python
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
|