600 lines
21 KiB
Python
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) |