EG/ui/panels/app_actions.py

1164 lines
48 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

import os
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_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.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()
# 创建删除命令
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()
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):
# 加载场景
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):
"""Import model through the active runtime path.
SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model).
Legacy mode: load via SceneManager.
"""
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
try:
# Remove legacy scene-manager models to avoid duplicate rendering
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
for m in list(self.scene_manager.models):
try:
if m and not m.isEmpty():
m.removeNode()
except Exception:
pass
self.scene_manager.models = []
# Replace previous SSBO model
old_model = getattr(self.ssbo_editor, 'model', None)
if old_model is not None:
try:
if not old_model.isEmpty():
old_model.removeNode()
except Exception:
pass
self.ssbo_editor.load_model(file_path)
model_np = getattr(self.ssbo_editor, 'model', None)
# Keep legacy ray-pick fallback usable by adding a collision body.
if model_np:
# Apply vital tags manually since SSBO overrides SceneManager loader
model_np.setTag("model_path", file_path)
model_np.setTag("original_path", file_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:
self.scene_manager.setupCollision(model_np)
self.scene_manager._processModelAnimations(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 _on_import_model(self):
"""处理导入模型菜单项"""
self.add_info_message("打开导入模型对话框")
self.show_import_dialog = True
def _import_model(self):
"""导入模型的具体实现"""
try:
if not self.import_file_path:
self.add_error_message("请选择要导入的文件")
return
if not os.path.exists(self.import_file_path):
self.add_error_message(f"文件不存在: {self.import_file_path}")
return
# 检查文件格式
file_ext = os.path.splitext(self.import_file_path)[1].lower()
if file_ext not in self.supported_formats:
self.add_error_message(f"不支持的文件格式: {file_ext}")
return
# 调用场景管理器导入模型
if hasattr(self, 'scene_manager') and self.scene_manager:
self.add_info_message(f"正在导入模型: {os.path.basename(self.import_file_path)}")
# 导入模型
model_node = self._import_model_for_runtime(self.import_file_path)
if model_node:
# 添加材质处理确保颜色正常
if hasattr(self.scene_manager, 'processMaterials'):
self.scene_manager.processMaterials(model_node)
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:
# 如果getColor失败直接设置默认颜色
model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰
except Exception as e:
self.add_warning_message(f"材质处理警告: {e}")
# 设置模型位置
model_node.setPos(0, 0, 0)
# 添加到场景管理器的模型列表
if hasattr(self.scene_manager, 'models'):
self.scene_manager.models.append(model_node)
# 选中新导入的模型
if hasattr(self, 'selection') and self.selection:
self.selection.updateSelection(model_node)
self.add_success_message(f"模型导入成功: {os.path.basename(self.import_file_path)}")
else:
self.add_error_message("模型导入失败")
else:
self.add_error_message("场景管理器未初始化")
except Exception as e:
self.add_error_message(f"导入模型失败: {e}")
# 清空导入路径
self.import_file_path = ""