EG/project/project_manager.py
2025-07-02 09:49:59 +08:00

600 lines
21 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
项目管理器 - 负责项目的生命周期管理
处理项目创建、打开、保存、打包等功能
"""
import os
import sys
import json
import re
import datetime
import subprocess
import shutil
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QLineEdit, QPushButton,
QLabel, QDialogButtonBox, QFileDialog, QMessageBox, QMainWindow
)
from PyQt5.QtCore import Qt
# 导入自定义对话框
from ui.widgets import NewProjectDialog
class ProjectManager:
"""项目管理器 - 统一管理项目的生命周期"""
def __init__(self, world):
"""初始化项目管理器
Args:
world: 主程序world对象引用
"""
self.world = world
self.current_project_path = None
self.project_config = None
print("✓ 项目管理系统初始化完成")
# ==================== 项目生命周期管理 ====================
def createNewProject(self, parent_window):
"""创建新项目"""
dialog = NewProjectDialog(parent_window)
if dialog.exec_() != QDialog.Accepted:
return False
project_path = dialog.projectPath
project_name = dialog.projectName
full_project_path = os.path.join(project_path, project_name)
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": project_name,
"path": full_project_path,
"created_at": 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.0",
"engine_version": "1.0.0"
}
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)
print(f"项目配置文件已创建: {config_file}")
# 清空当前场景
self._clearCurrentScene()
# 自动保存初始场景
scene_file = os.path.join(scenes_path, "scene.bam")
if self.world.scene_manager.saveScene(scene_file):
# 更新配置文件中的场景路径
project_config["scene_file"] = os.path.relpath(scene_file, full_project_path)
with open(config_file, "w", encoding="utf-8") as f:
json.dump(project_config, f, ensure_ascii=False, indent=4)
print(f"初始场景已保存到: {scene_file}")
else:
print("初始场景保存失败")
# 更新项目状态
self.current_project_path = full_project_path
self.project_config = project_config
# 更新文件浏览器到新项目路径
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(full_project_path))
# 更新窗口标题
self.updateWindowTitle(parent_window, project_name)
# 保存当前项目路径到主窗口
parent_window.current_project_path = full_project_path
# 显示成功消息
QMessageBox.information(parent_window, "成功", f"项目 '{project_name}' 创建成功!")
return True
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"创建项目失败: {str(e)}")
return False
def openProject(self, parent_window):
"""打开项目"""
try:
# 获取项目路径
project_path = QFileDialog.getExistingDirectory(
parent_window,
"选择项目文件夹",
"",
QFileDialog.ShowDirsOnly
)
if not project_path:
return False
# 检查是否是有效的项目文件夹
config_file = os.path.join(project_path, "project.json")
if not os.path.exists(config_file):
QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!")
return False
# 读取项目配置
with open(config_file, "r", encoding="utf-8") as f:
project_config = json.load(f)
# 检查场景文件
scene_file = os.path.join(project_path, "scenes", "scene.bam")
if not os.path.exists(scene_file):
QMessageBox.warning(parent_window, "警告", "没有找到场景文件!")
return False
# 加载场景
if self.world.scene_manager.loadScene(scene_file):
# 更新项目配置
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.current_project_path = project_path
self.project_config = project_config
# 保存当前项目路径到主窗口
parent_window.current_project_path = project_path
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
# 更新文件浏览器
if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'):
parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path))
QMessageBox.information(parent_window, "成功", "项目加载成功!")
return True
else:
QMessageBox.warning(parent_window, "错误", "加载场景失败!")
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"加载项目时发生错误:{str(e)}")
return False
def saveProject(self, parent_window):
"""保存项目"""
try:
# 检查是否有当前项目路径
if not self.current_project_path:
# 尝试从主窗口获取
if hasattr(parent_window, 'current_project_path'):
self.current_project_path = parent_window.current_project_path
else:
QMessageBox.warning(parent_window, "警告", "请先创建或打开一个项目!")
return False
project_path = self.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.world.scene_manager.saveScene(scene_file):
# 更新项目配置文件
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_config = project_config
# 更新窗口标题
project_name = os.path.basename(project_path)
self.updateWindowTitle(parent_window, project_name)
QMessageBox.information(parent_window, "成功", "项目保存成功!")
return True
else:
QMessageBox.warning(parent_window, "错误", "保存场景失败!")
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"保存项目时发生错误:{str(e)}")
return False
# ==================== 项目打包功能 ====================
def buildPackage(self, parent_window):
"""打包项目为可执行文件"""
try:
# 检查是否有当前项目路径
if not self.current_project_path:
if hasattr(parent_window, 'current_project_path'):
self.current_project_path = parent_window.current_project_path
else:
QMessageBox.warning(parent_window, "警告", "请先创建或打开一个项目!")
return False
project_path = self.current_project_path
scenes_path = os.path.join(project_path, "scenes")
scene_file = os.path.join(scenes_path, "scene.bam")
# 检查场景文件是否存在
if not os.path.exists(scene_file):
QMessageBox.warning(parent_window, "警告", "请先保存场景!")
return False
# 创建构建目录
build_dir = os.path.join(project_path, "build")
if not os.path.exists(build_dir):
os.makedirs(build_dir)
# 创建打包文件
self._createBuildFiles(build_dir, scene_file)
# 执行打包命令
success = self._executeBuild(build_dir, parent_window)
if success:
QMessageBox.information(parent_window, "成功", "打包完成!\n可执行文件在build/dist目录中。")
return True
else:
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"打包过程出错:{str(e)}")
return False
def _createBuildFiles(self, build_dir, scene_file):
"""创建打包所需的文件"""
# 创建requirements.txt
requirements_code = '''panda3d>=1.10.13
setuptools>=65.5.1
'''
requirements_path = os.path.join(build_dir, "requirements.txt")
with open(requirements_path, "w", encoding="utf-8") as f:
f.write(requirements_code)
# 创建viewer.py文件 - 内容将在下一个方法中实现
self._createViewerFile(build_dir)
# 复制场景文件
shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam"))
# 创建setup.py文件
self._createSetupFile(build_dir)
def _createViewerFile(self, build_dir):
"""创建查看器文件"""
viewer_code = '''import sys
from direct.showbase.ShowBase import ShowBase
from panda3d.core import WindowProperties, Vec3, Point3
from panda3d.core import AmbientLight, DirectionalLight
from panda3d.core import loadPrcFileData
# 配置窗口和禁用音频
loadPrcFileData("", """
win-size 1280 720
window-title Scene Viewer
audio-library-name null
notify-level-audio error
""")
class SceneViewer(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 设置背景色
self.setBackgroundColor(0.5, 0.5, 0.5)
# 设置相机
self.cam.setPos(0, -50, 20)
self.cam.lookAt(0, 0, 0)
# 添加光照
alight = AmbientLight('alight')
alight.setColor((0.2, 0.2, 0.2, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
# 加载场景
scene = self.loader.loadModel("scene.bam")
if scene:
scene.reparentTo(self.render)
# 设置相机控制
self.accept("wheel_up", self.wheelForward)
self.accept("wheel_down", self.wheelBackward)
self.accept("mouse2", self.startOrbit)
self.accept("mouse2-up", self.stopOrbit)
self.orbiting = False
self.lastMouseX = 0
self.lastMouseY = 0
# 启用每帧更新
self.taskMgr.add(self.updateCamera, "updateCamera")
def wheelForward(self):
# Move camera forward
forward = self.cam.getQuat().getForward()
self.cam.setPos(self.cam.getPos() + forward * 2)
def wheelBackward(self):
# Move camera backward
forward = self.cam.getQuat().getForward()
self.cam.setPos(self.cam.getPos() - forward * 2)
def startOrbit(self):
# Start orbit camera
if base.mouseWatcherNode.hasMouse():
self.orbiting = True
self.lastMouseX = base.mouseWatcherNode.getMouseX()
self.lastMouseY = base.mouseWatcherNode.getMouseY()
def stopOrbit(self):
# Stop orbit camera
self.orbiting = False
def updateCamera(self, task):
# Update camera position
if self.orbiting and base.mouseWatcherNode.hasMouse():
mouseX = base.mouseWatcherNode.getMouseX()
mouseY = base.mouseWatcherNode.getMouseY()
deltaX = mouseX - self.lastMouseX
deltaY = mouseY - self.lastMouseY
# Update camera direction
self.cam.setH(self.cam.getH() - deltaX * 50)
newP = self.cam.getP() + deltaY * 50
self.cam.setP(min(max(newP, -89), 89))
self.lastMouseX = mouseX
self.lastMouseY = mouseY
return task.cont
app = SceneViewer()
app.run()
'''
viewer_path = os.path.join(build_dir, "viewer.py")
with open(viewer_path, "w", encoding="utf-8") as f:
f.write(viewer_code)
def _createSetupFile(self, build_dir):
"""创建setup.py文件"""
setup_code = '''from setuptools import setup
from direct.dist.commands import bdist_apps
import sys
platform_specific = {
"win32": {
"build_apps": {
"console_apps": {},
"gui_apps": {
"SceneViewer": "viewer.py",
},
"include_patterns": [
"scene.bam",
"requirements.txt",
],
"plugins": [
"pandagl",
"pandaegg",
"p3openal_audio",
],
"platforms": [
"win_amd64"
],
"include_modules": {
"*": [
"direct.showbase.ShowBase",
"direct.task",
"direct.actor",
"direct.interval",
"panda3d.core",
]
},
"exclude_modules": {
"*": [
"PyQt5",
"tkinter",
]
},
}
},
"linux": {
"build_apps": {
"console_apps": {},
"gui_apps": {
"SceneViewer": "viewer.py",
},
"include_patterns": [
"scene.bam",
"requirements.txt",
"/usr/lib/x86_64-linux-gnu/libopenal.so*",
],
"plugins": [
"pandagl",
"pandaegg",
"p3openal_audio",
],
"platforms": [
"linux_x86_64"
],
"include_modules": {
"*": [
"direct.showbase.ShowBase",
"direct.task",
"direct.actor",
"direct.interval",
"panda3d.core",
]
},
"exclude_modules": {
"*": [
"PyQt5",
"tkinter",
]
},
}
}
}
# 根据平台选择配置
platform = "linux" if sys.platform.startswith("linux") else "win32"
options = platform_specific[platform]
setup(
name="SceneViewer",
version="1.0",
options=options,
install_requires=[
"panda3d>=1.10.13",
],
)
'''
setup_path = os.path.join(build_dir, "setup.py")
with open(setup_path, "w", encoding="utf-8") as f:
f.write(setup_code)
def _executeBuild(self, build_dir, parent_window):
"""执行打包命令"""
try:
# 显示详细输出
process = subprocess.Popen(
[sys.executable, "setup.py", "bdist_apps"],
cwd=build_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
# 实时显示输出
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
# 获取错误输出
stderr = process.stderr.read()
if stderr:
print("错误输出:", stderr)
if process.returncode == 0:
return True
else:
QMessageBox.critical(parent_window, "错误", f"打包失败,返回码:{process.returncode}")
return False
except Exception as e:
QMessageBox.critical(parent_window, "错误", f"打包失败:{str(e)}")
return False
# ==================== 工具方法 ====================
def updateWindowTitle(self, window, project_name=None):
"""更新窗口标题"""
base_title = "引擎编辑器"
if project_name:
window.setWindowTitle(f"{base_title} - {project_name}")
else:
window.setWindowTitle(base_title)
def _clearCurrentScene(self):
"""清空当前场景"""
# 移除所有模型
for model in self.world.models:
model.removeNode()
self.world.models.clear()
# 清空GUI元素
for gui_element in self.world.gui_elements:
gui_element.removeNode()
self.world.gui_elements.clear()
# 更新场景树
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
def getCurrentProjectPath(self):
"""获取当前项目路径"""
return self.current_project_path
def getCurrentProjectConfig(self):
"""获取当前项目配置"""
return self.project_config
def isProjectOpen(self):
"""检查是否有项目打开"""
return self.current_project_path is not None
def getProjectName(self):
"""获取当前项目名称"""
if self.current_project_path:
return os.path.basename(self.current_project_path)
return None
def getProjectInfo(self):
"""获取项目信息"""
if not self.project_config:
return None
return {
"name": self.project_config.get("name", "未知项目"),
"path": self.project_config.get("path", ""),
"created_at": self.project_config.get("created_at", ""),
"last_modified": self.project_config.get("last_modified", ""),
"version": self.project_config.get("version", "1.0.0"),
"engine_version": self.project_config.get("engine_version", "1.0.0")
}
# ==================== 便利函数 ====================
def updateWindowTitle(window, project_name=None):
"""更新窗口标题 - 独立便利函数"""
base_title = "引擎编辑器"
if project_name:
window.setWindowTitle(f"{base_title} - {project_name}")
else:
window.setWindowTitle(base_title)