#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 项目管理器 - 负责项目的生命周期管理 处理项目创建、打开、保存、打包等功能 """ import os import sys import json 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) full_project_path = os.path.normpath(os.path.join(project_path, project_name)) print(f"full_project_path: {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": 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 os.path.exists(scene_file): # 加载场景 if self.world.scene_manager.loadScene(scene_file): # 更新项目配置 project_config["scene_file"] = os.path.relpath(scene_file, project_path) project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 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 # 检查场景文件 # 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 openProjectForPath(self, project_path, parent_window=None): """通过路径打开项目 Args: project_path: 项目路径 parent_window: 父窗口对象(可选) """ try: if not project_path: return False # 检查是否是有效的项目文件夹 config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): if parent_window: QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!") else: print("警告: 选择的不是有效的项目文件夹!") 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 os.path.exists(scene_file): # 加载场景 if self.world.scene_manager.loadScene(scene_file): # 更新项目配置 project_config["scene_file"] = os.path.relpath(scene_file, project_path) project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 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 # 如果有父窗口,更新相关UI元素 if parent_window: # 保存当前项目路径到主窗口 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, "成功", "项目加载成功!") print(f"项目 '{project_path}' 加载成功!") return True else: if parent_window: QMessageBox.warning(parent_window, "错误", "加载场景失败!") else: print("错误: 加载场景失败!") return False except Exception as e: error_msg = f"加载项目时发生错误:{str(e)}" if parent_window: QMessageBox.critical(parent_window, "错误", error_msg) else: print(error_msg) 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): """打包项目为可执行文件 - 按照Panda3D官方标准方法""" 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._createStandardBuildFiles(build_dir, project_path, scene_file) # 执行打包命令 success = self._executeStandardBuild(build_dir, parent_window) if success: QMessageBox.information(parent_window, "成功", "打包完成!\n可执行文件在 build/dist/ 目录中。\n" "支持的格式:\n" "- Windows: .exe 安装程序\n" "- Linux: .tar.gz 压缩包\n" "- 通用: .zip 压缩包") return True else: return False except Exception as e: QMessageBox.critical(parent_window, "错误", f"打包过程出错:{str(e)}") return False def _createStandardBuildFiles(self, build_dir, project_path, scene_file): """创建标准的Panda3D打包文件""" project_name = os.path.basename(project_path) # 确保构建目录存在 if not os.path.exists(build_dir): os.makedirs(build_dir) # 复制场景文件到构建目录 shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam")) # 创建标准的应用程序入口文件 self._createAppFile(build_dir, project_name) # 创建标准的setup.py文件 self._createStandardSetupFile(build_dir, project_name) def _createAppFile(self, build_dir, project_name): """创建应用程序主文件""" app_code = f'''#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ {project_name} - Panda3D应用程序 使用Panda3D引擎编辑器创建 """ import sys import os from direct.showbase.ShowBase import ShowBase from panda3d.core import (loadPrcFileData, WindowProperties, AmbientLight, DirectionalLight, Point3, Vec3) # 配置Panda3D loadPrcFileData("", """ win-size 1280 720 window-title {project_name} show-frame-rate-meter 1 sync-video 1 want-directtools #f want-tk #f audio-library-name p3openal_audio """) class {project_name.replace(' ', '').replace('-', '')}App(ShowBase): """应用程序主类""" def __init__(self): ShowBase.__init__(self) print(f"启动 {project_name}...") # 设置窗口属性 self.setupWindow() # 设置光照 self.setupLighting() # 加载场景 self.loadScene() # 设置相机控制 self.setupControls() print("✓ 应用程序初始化完成") def setupWindow(self): """设置窗口""" # 设置背景色 self.setBackgroundColor(0.2, 0.2, 0.2) # 设置窗口属性 props = WindowProperties() props.setTitle("{project_name}") self.win.requestProperties(props) def setupLighting(self): """设置光照系统""" # 环境光 alight = AmbientLight('alight') alight.setColor((0.3, 0.3, 0.3, 1)) alnp = self.render.attachNewNode(alight) self.render.setLight(alnp) # 定向光(模拟太阳光) dlight = DirectionalLight('dlight') dlight.setColor((0.8, 0.8, 0.8, 1)) dlight.setDirection(Vec3(-1, -1, -1)) dlnp = self.render.attachNewNode(dlight) self.render.setLight(dlnp) def loadScene(self): """加载场景""" try: # 查找场景文件 scene_file = "scene.bam" if not os.path.exists(scene_file): print("警告: 没有找到场景文件,创建默认场景") self.createDefaultScene() return # 加载场景 scene = self.loader.loadModel(scene_file) if scene: scene.reparentTo(self.render) print("✓ 场景加载成功") # 自动调整相机位置 self.adjustCamera() else: print("警告: 场景加载失败,创建默认场景") self.createDefaultScene() except Exception as e: print(f"加载场景时出错: {{str(e)}}") self.createDefaultScene() def createDefaultScene(self): """创建默认场景""" # 加载默认的环境模型 env = self.loader.loadModel("models/environment") if env: env.reparentTo(self.render) env.setScale(0.25) env.setPos(-8, 42, 0) # 创建一个简单的立方体作为示例 from panda3d.core import CardMaker cm = CardMaker("ground") cm.setFrame(-10, 10, -10, 10) ground = self.render.attachNewNode(cm.generate()) ground.setP(-90) ground.setColor(0.5, 0.8, 0.5, 1) def adjustCamera(self): """调整相机位置以查看场景""" # 计算场景边界 bounds = self.render.getBounds() if bounds and not bounds.isEmpty(): center = bounds.getCenter() radius = bounds.getRadius() # 设置相机位置 distance = radius * 3 self.cam.setPos(center.x, center.y - distance, center.z + radius) self.cam.lookAt(center) else: # 默认相机位置 self.cam.setPos(0, -20, 5) self.cam.lookAt(0, 0, 0) def setupControls(self): """设置相机控制""" # 启用鼠标控制 self.accept("wheel_up", self.zoomIn) self.accept("wheel_down", self.zoomOut) # 键盘控制说明 print("\\n=== 控制说明 ===") print("鼠标滚轮: 缩放") print("ESC: 退出") print("================\\n") # ESC键退出 self.accept("escape", sys.exit) def zoomIn(self): """放大""" pos = self.cam.getPos() lookAt = Point3(0, 0, 0) # 假设看向原点 direction = (lookAt - pos).normalized() newPos = pos + direction * 2 self.cam.setPos(newPos) def zoomOut(self): """缩小""" pos = self.cam.getPos() lookAt = Point3(0, 0, 0) # 假设看向原点 direction = (lookAt - pos).normalized() newPos = pos - direction * 2 self.cam.setPos(newPos) def main(): """主函数""" try: app = {project_name.replace(' ', '').replace('-', '')}App() app.run() except Exception as e: print(f"应用程序启动失败: {{str(e)}}") import traceback traceback.print_exc() input("按Enter键退出...") if __name__ == "__main__": main() ''' app_path = os.path.join(build_dir, "main.py") with open(app_path, "w", encoding="utf-8") as f: f.write(app_code) def _createStandardSetupFile(self, build_dir, project_name): """创建标准的setup.py文件 - 按照Panda3D官方文档""" setup_code = f'''#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ {project_name} 打包配置文件 使用 Panda3D 标准打包工具 """ from setuptools import setup # 应用程序配置 APP_NAME = "{project_name}" APP_VERSION = "1.0.0" MAIN_SCRIPT = "main.py" setup( name=APP_NAME, version=APP_VERSION, # Panda3D 打包选项 options={{ 'build_apps': {{ # GUI应用程序 'gui_apps': {{ APP_NAME: MAIN_SCRIPT, }}, # 包含的文件模式 'include_patterns': [ '*.bam', # 场景文件 '*.egg', # 模型文件 '*.jpg', '*.png', # 纹理文件 '*.wav', '*.ogg', # 音频文件 '*.ttf', '*.otf', # 字体文件 ], # 排除的文件模式 'exclude_patterns': [ '*.pyc', '__pycache__/**', '.git/**', '.vscode/**', '*.log', ], # Panda3D 插件 'plugins': [ 'pandagl', # OpenGL渲染器 'pandaegg', # Egg文件支持 'p3openal_audio', # OpenAL音频 ], # 包含的Python模块 'include_modules': {{ '*': [ 'direct.showbase.ShowBase', 'direct.task', 'direct.actor', 'direct.interval', 'panda3d.core', 'panda3d.direct', ], }}, # 排除的Python模块(减小体积) 'exclude_modules': {{ '*': [ 'tkinter', # Tkinter GUI 'matplotlib', # 绘图库 'numpy', # 数值计算(如果不需要) 'scipy', # 科学计算(如果不需要) 'PIL', # 图像处理(如果不需要) 'wx', # wxPython 'PyQt5', # Qt界面库 'setuptools', # 安装工具 'distutils', # 分发工具 ], }}, # 平台设置 'platforms': [ 'win_amd64', # Windows 64位 'linux_x86_64', # Linux 64位 # 'macosx_10_9_x86_64', # macOS(如果需要) ], # 优化设置 'strip_docstrings': True, # 移除文档字符串 }}, }}, # 标准setuptools选项 author="Panda3D 引擎编辑器", author_email="user@example.com", description=f"{{APP_NAME}} - 使用Panda3D创建的3D应用程序", long_description="这是一个使用Panda3D引擎编辑器创建的3D应用程序。", # 依赖项 install_requires=[ 'panda3d>=1.10.13', ], # Python版本要求 python_requires='>=3.7', # 分类信息 classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: End Users/Desktop', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Topic :: Games/Entertainment', 'Topic :: Multimedia :: Graphics :: 3D Rendering', ], # 使用现代的许可证表达式 license='MIT', ) ''' setup_path = os.path.join(build_dir, "setup.py") with open(setup_path, "w", encoding="utf-8") as f: f.write(setup_code) def _executeStandardBuild(self, build_dir, parent_window): """执行标准的Panda3D打包命令""" try: print(f"开始打包,工作目录: {build_dir}") # 首先尝试 bdist_apps(推荐方式) print("执行标准打包命令: python setup.py bdist_apps") process = subprocess.Popen( [sys.executable, "setup.py", "bdist_apps"], cwd=build_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, encoding='utf-8' ) # 实时显示输出 stdout_lines = [] stderr_lines = [] while True: output = process.stdout.readline() if output == '' and process.poll() is not None: break if output: print(output.strip()) stdout_lines.append(output.strip()) # 获取错误输出 stderr = process.stderr.read() if stderr: print("错误输出:", stderr) stderr_lines.append(stderr) # 检查返回码 if process.returncode == 0: print("✓ 打包成功完成") return True else: # 如果bdist_apps失败,尝试build_apps print(f"bdist_apps 失败 (返回码: {process.returncode}),尝试 build_apps...") return self._tryBuildApps(build_dir, parent_window) except Exception as e: print(f"执行打包命令时出错: {str(e)}") QMessageBox.critical(parent_window, "错误", f"打包失败:{str(e)}") return False def _tryBuildApps(self, build_dir, parent_window): """尝试使用 build_apps 命令""" try: print("执行备用打包命令: python setup.py build_apps") process = subprocess.Popen( [sys.executable, "setup.py", "build_apps"], cwd=build_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, encoding='utf-8' ) # 实时显示输出 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: print("✓ build_apps 成功完成") return True else: error_msg = f"打包失败,返回码:{process.returncode}" if stderr: error_msg += f"\\n错误信息:{stderr}" QMessageBox.critical(parent_window, "错误", error_msg) return False except Exception as e: QMessageBox.critical(parent_window, "错误", f"执行 build_apps 失败:{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)