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