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, UniversalMessageDialog class ProjectManager: def __init__(self, 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_path): # 更新配置文件中的场景路径 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 # 显示成功消息 UniversalMessageDialog.show_success( parent_window, "成功", f"项目 '{project_name}' 创建成功!", show_cancel=False, confirm_text="确认" ) return True except Exception as e: UniversalMessageDialog.show_error( parent_window, "错误", f"创建项目失败: {str(e)}", show_cancel=False, confirm_text="确认" ) 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): UniversalMessageDialog.show_warning( parent_window, "警告", "选择的不是有效的项目文件夹!", show_cancel=False, confirm_text="确认" ) 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)) UniversalMessageDialog.show_success( parent_window, "成功", "项目加载成功!", show_cancel=False, confirm_text="确认" ) 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: UniversalMessageDialog.show_error( parent_window, "错误", f"加载项目时发生错误:{str(e)}", show_cancel=False, confirm_text="确认" ) 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: UniversalMessageDialog.show_warning( parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}", show_cancel=False, confirm_text="确认" ) 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)) UniversalMessageDialog.show_success( parent_window, "成功", "项目加载成功!", show_cancel=False, confirm_text="确认" ) print(f"项目 '{project_path}' 加载成功!") return True else: if parent_window: UniversalMessageDialog.show_warning( parent_window, "错误", "加载场景失败!", show_cancel=False, confirm_text="确认" ) else: print("错误: 加载场景失败!") return False except Exception as e: error_msg = f"加载项目时发生错误:{str(e)}" if parent_window: UniversalMessageDialog.show_error( parent_window, "错误", error_msg, show_cancel=False, confirm_text="确认" ) 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: UniversalMessageDialog.show_warning( parent_window, "警告", "请先创建或打开一个项目!", show_cancel=False, confirm_text="确认" ) 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, 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_config = project_config # 更新窗口标题 project_name = os.path.basename(project_path) self.updateWindowTitle(parent_window, project_name) UniversalMessageDialog.show_success( parent_window, "成功", "项目保存成功!", show_cancel=False, confirm_text="确认" ) return True else: UniversalMessageDialog.show_warning( parent_window, "错误", "保存场景失败!", show_cancel=False, confirm_text="确认" ) return False except Exception as e: UniversalMessageDialog.show_error( parent_window, "错误", f"保存项目时发生错误:{str(e)}", show_cancel=False, confirm_text="确认" ) 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 if hasattr(self.world,'selection') and self.world.selection: self.world.selection.clearSelection() print("已取消场景中的物体选中状态") if self.world.scene_manager.saveScene(scene_file,project_path): print("场景保存成功") 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) else: QMessageBox.warning(parent_window,"错误","保存场景失败!") return False build_dir = QFileDialog.getExistingDirectory( parent_window, "选择打包输出目录", project_path, QFileDialog.ShowDirsOnly ) if not build_dir: return False # 创建构建目录 build_dir = os.path.join(build_dir, "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: # 根据操作系统创建对应的启动脚本文件 try: import stat # 检查操作系统类型 if os.name == 'nt': # Windows系统 run_bat_path = os.path.join(build_dir, "run.bat") with open(run_bat_path, "w") as f: f.write("@echo off\n") f.write("python main.py %*\n") else: # Unix-like系统 (Linux, macOS等) run_sh_path = os.path.join(build_dir, "run.sh") with open(run_sh_path, "w") as f: f.write("#!/bin/bash\n") f.write("python3.10 main.py \"$@\"\n") # 为Unix-like系统添加执行权限 st = os.stat(run_sh_path) os.chmod(run_sh_path, st.st_mode | stat.S_IEXEC) except Exception as e: print(f"创建启动脚本文件失败: {str(e)}") QMessageBox.information(parent_window, "成功", "打包完成!\n可执行文件在 build 目录中。\n") 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")) # 复制Resources文件夹到build目录 source_resources = os.path.join(project_path, "scenes", "resources") self.copy_folder(source_resources, build_dir) self._saveGUIElementsToJSON(build_dir, project_path) self._copyScriptsToBuild(build_dir, project_path) self._copyScriptSystemToBuild(build_dir) source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile") dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile") if os.path.exists(source_render_pipeline): if os.path.exists(dest_render_pipeline): shutil.rmtree(dest_render_pipeline) shutil.copytree( source_render_pipeline, dest_render_pipeline, ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log','samples') ) print("✓ RenderPipelineFile文件夹已复制到build目录") else: print("⚠️ RenderPipelineFile文件夹未找到") # 创建标准的应用程序入口文件 self._createAppFile(build_dir, project_name) # 创建标准的setup.py文件 self._createStandardSetupFile(build_dir, project_name) #创建requirements.txt文件 self._createRequirementsFile(build_dir) def _copyScriptsToBuild(self, build_dir, project_path): """复制脚本文件到构建目录的scripts文件夹""" try: # 创建目标scripts目录 scripts_dest = os.path.join(build_dir, "scripts") # 正确的源scripts目录路径 scripts_src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts") # 如果上面的路径不存在,尝试项目路径下的scripts目录 if not os.path.exists(scripts_src): scripts_src = os.path.join(project_path, "scripts") if os.path.exists(scripts_src): # 直接复制整个scripts目录 if os.path.exists(scripts_dest): shutil.rmtree(scripts_dest) shutil.copytree( scripts_src, scripts_dest, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') ) print("✓ Scripts目录已复制到build/scripts") else: # 创建空的scripts目录 if not os.path.exists(scripts_dest): os.makedirs(scripts_dest) print("⚠️ 项目中没有scripts目录") except Exception as e: print(f"⚠️ 复制脚本文件时出错: {str(e)}") def _copyScriptSystemToBuild(self,build_dir): core_files = [ "script_system.py", "InfoPanelManager.py", "CustomMouseController.py" ] source_core_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"core") core_dest = os.path.join(build_dir,"core") if not os.path.exists(core_dest): os.makedirs(core_dest) for file_name in core_files: source_file = os.path.join(source_core_dir,file_name) if os.path.exists(source_file): shutil.copy2(source_file,os.path.join(core_dest,file_name)) def _saveGUIElementsToJSON(self, build_dir, project_path): """保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致""" try: # 创建目标gui目录 gui_dest = os.path.join(build_dir, "gui") if not os.path.exists(gui_dest): os.makedirs(gui_dest) # 收集所有GUI元素信息 gui_data = [] # 获取当前场景中的GUI元素 if hasattr(self.world, 'gui_elements'): for gui_node in self.world.gui_elements: if gui_node and not gui_node.isEmpty(): # 使用_collectGUIElementInfo方法收集信息 gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node) if gui_info: self._updateResourcePaths(gui_info,project_path,build_dir) gui_data.append(gui_info) print(f"收集GUI元素信息: {gui_info['name']}") # 保存GUI信息到JSON文件 gui_file_path = os.path.join(gui_dest, "gui_elements.json") with open(gui_file_path, "w", encoding="utf-8") as f: json.dump(gui_data, f, ensure_ascii=False, indent=4) print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素") return True except Exception as e: print(f"⚠️ 保存GUI元素时出错: {str(e)}") import traceback traceback.print_exc() return False def _updateResourcePaths(self,gui_info,project_path,build_dir): def normalize_resource_path(old_path): if not old_path: return old_path if old_path.startswith(("http://","https://")): return old_path if os.path.isabs(old_path): try: rel_path = os.path.relpath(old_path,project_path) return os.path.join("resources",os.path.basename(rel_path)).replace("\\","/") except Exception: return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") else: return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") if "image_path" in gui_info and gui_info["image_path"]: gui_info["image_path"] = normalize_resource_path(gui_info["image_path"]) if "video_path" in gui_info and gui_info["video_path"]: gui_info["video_path"] = normalize_resource_path(gui_info["video_path"]) if "bg_image_path" in gui_info and gui_info["bg_image_path"]: gui_info["bg_image_path"] = normalize_resource_path(gui_info["bg_image_path"]) def _createRequirementsFile(self,build_dir): requirements_content = """panda3d>=1.10.13""" requirements_path = os.path.join(build_dir,"requirements.txt") with open(requirements_path,"w",encoding="utf-8") as f: f.write(requirements_content) def copy_folder(self, source_folder, destination_folder): """将一个文件夹从源路径复制到目标路径下的resources文件夹中 Args: source_folder (str): 源文件夹路径 destination_folder (str): 目标文件夹路径 """ try: # 创建resources文件夹作为目标 resources_dest = os.path.join(destination_folder, "resources") # 确保目标目录存在 if not os.path.exists(destination_folder): os.makedirs(destination_folder) # 如果目标resources文件夹已存在,先删除 if os.path.exists(resources_dest): shutil.rmtree(resources_dest) # 检查源文件夹是否存在 if os.path.exists(source_folder): # 复制整个文件夹到resources目录下 shutil.copytree( source_folder, resources_dest, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') ) print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}") return True else: print(f"⚠️ 源文件夹不存在: {source_folder}") # 即使源文件夹不存在,也创建空的resources目录 if not os.path.exists(resources_dest): os.makedirs(resources_dest) return False except Exception as e: print(f"⚠️ 复制文件夹时出错: {str(e)}") return False def _copyResourcesToBuild(self, build_dir, project_path): """复制GUI资源到构建目录的resources文件夹""" try: # 创建目标resources目录 resources_dest = os.path.join(build_dir, "resources") # 源Resources目录 resources_src = os.path.join(project_path, "Resources") if os.path.exists(resources_src): # 直接复制整个Resources目录 if os.path.exists(resources_dest): shutil.rmtree(resources_dest) shutil.copytree( resources_src, resources_dest, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') ) print("✓ Resources目录已复制到build/resources") # 统计复制的文件数量 file_count = 0 for root, dirs, files in os.walk(resources_dest): file_count += len(files) print(f"✓ 共复制了 {file_count} 个资源文件") else: # 创建空的resources目录 if not os.path.exists(resources_dest): os.makedirs(resources_dest) print("⚠️ 项目中没有Resources目录") except Exception as e: print(f"⚠️ 复制资源文件时出错: {str(e)}") def _collectResourceFiles(self, project_path): """收集项目中GUI使用的资源文件""" resource_files = set() try: # 收集Resources目录中的所有文件(这是最主要的资源来源) resources_dir = os.path.join(project_path, "Resources") if os.path.exists(resources_dir): for root, dirs, files in os.walk(resources_dir): for file in files: file_path = os.path.join(root, file) # 收集所有文件,不仅仅是媒体文件 resource_files.add(file_path) # 同时收集场景中引用的特定资源 scene_file = os.path.join(project_path, "scenes", "scene.bam") if os.path.exists(scene_file): # 从场景文件中提取资源引用 referenced_files = self._extractResourcesFromScene(scene_file, project_path) for file_path in referenced_files: if os.path.isabs(file_path): if os.path.exists(file_path): resource_files.add(file_path) else: # 相对路径 full_path = os.path.join(project_path, file_path) if os.path.exists(full_path): resource_files.add(full_path) except Exception as e: print(f"收集资源文件时出错: {str(e)}") return list(resource_files) def _extractResourcesFromScene(self, scene_file, project_path): """从场景文件中提取资源引用""" referenced_files = [] try: # 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑 # 由于直接解析BAM文件比较复杂,我们采用间接方式 # 检查项目配置文件或其他元数据文件中可能包含的资源引用 config_file = os.path.join(project_path, "project.json") if os.path.exists(config_file): try: with open(config_file, "r", encoding="utf-8") as f: project_config = json.load(f) # 如果配置中有资源列表信息,可以在这里处理 # 这里暂时保持简单实现 except Exception as e: print(f"读取项目配置时出错: {str(e)}") except Exception as e: print(f"从场景提取资源引用时出错: {str(e)}") return referenced_files def _isMediaFile(self, file_path): """判断是否为媒体文件(图片或视频)""" media_extensions = { # 图片格式 '.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff', # 视频格式 '.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv' } _, ext = os.path.splitext(file_path.lower()) return ext in media_extensions def _createAppFile(self, build_dir, project_name): """创建应用程序主文件 - 通过复制模板文件""" # 获取模板文件路径(假设模板文件在项目根目录下的templates文件夹中) template_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "templates", "main_template.py") # 目标文件路径 app_path = os.path.join(build_dir, "main.py") # 检查模板文件是否存在 if os.path.exists(template_path): # 直接复制模板文件 shutil.copy2(template_path, app_path) print(f"✓ 应用程序主文件已从模板创建: {app_path}") def _createStandardSetupFile(self, build_dir, project_name): """创建优化的标准setup.py文件""" 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', '*.tga', '*.dds', # 纹理文件 '*.wav', '*.ogg', '*.mp3', # 音频文件 '*.ttf', '*.otf', # 字体文件 'config/*.yaml', # 配置文件 'shaders/**/*.glsl', # 着色器文件 ], # 排除的文件模式 'exclude_patterns': [ '*.pyc', '__pycache__/**', '.git/**', '.vscode/**', '*.log', '**/*.tmp', '**/temp/**', 'samples/**', # 示例文件 '**/.DS_Store', '**/Thumbs.db', ], # Panda3D 插件 'plugins': [ 'pandagl', # OpenGL渲染器 'pandaegl', # EGL支持 'p3assimp', # Assimp模型导入 'p3ffmpeg', # FFmpeg视频支持 'p3openal_audio', # OpenAL音频 ], # 包含的Python模块 'include_modules': {{ '*': [ # 核心模块 'direct.showbase.ShowBase', 'direct.task', 'direct.actor', 'direct.interval', 'direct.gui', 'direct.stdpy.file', 'direct.stdpy.pickle', 'panda3d.core', 'panda3d.direct', # 渲染管线相关 'rpcore', 'rpcore.util.movement_controller', 'rpcore.native', 'rpcore.render_pipeline', 'rplibs', 'rpplugins', # 标准库 'json', 'os', 'sys', 'math', 'random', 'collections', 'copy', 'itertools', 'importlib', ], }}, # 排除的Python模块(减小体积) 'exclude_modules': {{ '*': [ # GUI框架 'tkinter', 'matplotlib', 'numpy', 'scipy', 'PIL', 'wx', 'PyQt5', 'PySide2', 'kivy', # 开发工具 'setuptools', 'pip', 'wheel', 'twine', 'pytest', 'unittest', # 其他不必要的模块 'sqlite3', 'xmlrpc', 'ftplib', 'telnetlib', ], }}, # 平台设置 'platforms': [ 'win_amd64', # Windows 64位 'linux_x86_64', # Linux 64位 ], # 优化设置 'strip_docstrings': True, # 移除文档字符串 'optimize': 1, # 字节码优化级别 }}, }}, # 标准setuptools选项 author="Panda3D Engine Editor User", 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}") # 显示打包进度提示 QMessageBox.information(parent_window, "打包进行中", "正在打包项目,请稍候...\n" "此过程可能需要几分钟时间。\n" "请勿关闭程序。") # 使用线程执行打包以避免界面冻结 import threading from PyQt5.QtCore import pyqtSignal, QObject class BuildSignals(QObject): finished = pyqtSignal(bool, str) progress = pyqtSignal(str) signals = BuildSignals() def build_process(): try: print("执行标准打包命令: python setup.py bdist_apps") signals.progress.emit("正在初始化打包过程...") process = subprocess.Popen( [sys.executable, "setup.py", "bdist_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()) if "copying" in output.lower(): signals.progress.emit(f"正在处理文件: {output.strip()[-50:]}") elif "building" in output.lower(): signals.progress.emit(output.strip()) # 获取错误输出 stderr = process.stderr.read() if stderr: print("错误输出:", stderr) except Exception as e: print(f"执行打包命令时出错: {str(e)}") signals.finished.emit(False, f"打包失败:{str(e)}") # 连接信号 def on_finished(success, message): if success: QMessageBox.information(parent_window, "成功", message) else: QMessageBox.critical(parent_window, "错误", message) def on_progress(message): print(f"[打包进度] {message}") signals.finished.connect(on_finished) signals.progress.connect(on_progress) # 启动打包线程 build_thread = threading.Thread(target=build_process) build_thread.daemon = True build_thread.start() return True # 线程已启动 except Exception as e: print(f"启动打包过程时出错: {str(e)}") QMessageBox.critical(parent_window, "错误", f"启动打包失败:{str(e)}") return False def _tryBuildAppsThreaded(self, build_dir, signals): """尝试使用 build_apps 命令(线程安全版本)""" try: print("执行备用打包命令: python setup.py build_apps") signals.progress.emit("正在执行备用打包方法...") 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()) signals.progress.emit(output.strip()) stderr = process.stderr.read() if stderr: print("错误输出:", stderr) if process.returncode == 0: print("✓ build_apps 成功完成") signals.finished.emit(True, "打包成功完成!\n可执行文件在 dist/ 目录中。") else: error_msg = f"打包失败,返回码:{process.returncode}" if stderr: error_msg += f"\n错误信息:{stderr}" signals.finished.emit(False, error_msg) except Exception as e: signals.finished.emit(False, f"执行 build_apps 失败:{str(e)}") 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)