#!/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_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 # 显示成功消息 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, "警告", f"选择的不是有效的项目文件夹!{project_path}") 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, 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) 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")) # 复制Resources文件夹到build目录 source_resources = os.path.join(project_path, "scenes", "resources") self.copy_folder(source_resources, build_dir) self._saveGUIElementsToJSON(build_dir, project_path) 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') ) 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 _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: 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 _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): """创建应用程序主文件""" app_code = f'''#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ {project_name} - Panda3D应用程序 使用Panda3D引擎编辑器创建 """ from __future__ import print_function #获取渲染管线路径 import sys import os render_pipeline_path = 'RenderPipelineFile' project_root = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0,project_root) sys.path.insert(0,render_pipeline_path) import math from random import random,randint,seed from panda3d.core import Vec3,load_prc_file_data,Filename from direct.showbase.ShowBase import ShowBase os.chdir(os.path.dirname(os.path.realpath(__file__))) class MainApp(ShowBase): def __init__(self): load_prc_file_data("",""" win-size 1200 720 window-title Render """) pipeline_path = "../../" if not os.path.isfile(os.path.join(pipeline_path,"setup.py")): pipeline_path = "../../RenderPipeline" sys.path.insert(0,pipeline_path) from rpcore import RenderPipeline,SpotLight self.render_pipeline = RenderPipeline() self.render_pipeline.create(self) from rpcore.util.movement_controller import MovementController self.render_pipeline.daytime_mgr.time = "12:00" self.loadFullScene() self.controller = MovementController(self) self.controller.set_initial_position( Vec3(-7.5,-5.3,1.8),Vec3(-5.9,-4.0,1.6)) self.controller.setup() base.accept("l",self.tour) def loadFullScene(self): """加载完整场景,包括所有元素""" try: scene_file = "scene.bam" if os.path.exists(scene_file): # 使用readBamFile加载完整场景 from panda3d.core import BamCache BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) if scene: scene.reparentTo(self.render) self.render_pipeline.prepare_scene(scene) print("✓ 完整场景加载成功") # 处理场景中的各种元素 self.processSceneElements(scene) else: print("⚠️ 场景文件加载失败") else: print("⚠️ 未找到场景文件") except Exception as e: print(f"加载完整场景时出错: {{str(e)}}") import traceback traceback.print_exc() def processSceneElements(self, scene): """处理场景中的各种元素""" try: # 处理光源 self.processLights(scene) # 处理GUI元素 self.processGUIElements(scene) # 处理其他特殊元素 self.processSpecialElements(scene) except Exception as e: print(f"处理场景元素时出错: {{str(e)}}") def processLights(self, scene): """处理场景中的光源""" try: # 查找并处理点光源 point_lights = scene.findAllMatches("**/=element_type=point_light") for light_node in point_lights: try: from RenderPipelineFile.rpcore import PointLight light = PointLight() # 恢复光源属性 if light_node.hasTag("light_energy"): light.energy = float(light_node.getTag("light_energy")) else: light.energy = 5000 light.radius = 1000 light.inner_radius = 0.4 light.set_color_from_temperature(5 * 1000.0) light.casts_shadows = True light.shadow_map_resolution = 256 light.setPos(light_node.getPos()) self.render_pipeline.add_light(light) print(f"✓ 点光源 {{light_node.getName()}} 恢复成功") except Exception as e: print(f"恢复点光源 {{light_node.getName()}} 失败: {{str(e)}}") # 查找并处理聚光灯 spot_lights = scene.findAllMatches("**/=element_type=spot_light") for light_node in spot_lights: try: from RenderPipelineFile.rpcore import SpotLight light = SpotLight() # 恢复光源属性 if light_node.hasTag("light_energy"): light.energy = float(light_node.getTag("light_energy")) else: light.energy = 5000 light.radius = 1000 light.inner_radius = 0.4 light.set_color_from_temperature(5 * 1000.0) light.casts_shadows = True light.shadow_map_resolution = 256 light.setPos(light_node.getPos()) self.render_pipeline.add_light(light) print(f"✓ 聚光灯 {{light_node.getName()}} 恢复成功") except Exception as e: print(f"恢复聚光灯 {{light_node.getName()}} 失败: {{str(e)}}") except Exception as e: print(f"处理光源时出错: {{str(e)}}") def processGUIElements(self, scene): """处理场景中的GUI元素""" try: # 查找并处理2D图像 images_2d = scene.findAllMatches("**/=gui_type=image_2d") for img_node in images_2d: try: # GUI元素通常在场景加载时自动处理 print(f"✓ 2D图像 {{img_node.getName()}} 已加载") except Exception as e: print(f"处理2D图像 {{img_node.getName()}} 失败: {{str(e)}}") except Exception as e: print(f"处理GUI元素时出错: {{str(e)}}") def processSpecialElements(self, scene): """处理特殊元素""" try: # 处理Cesium Tilesets tilesets = scene.findAllMatches("**/=element_type=cesium_tileset") for tileset_node in tilesets: try: # Tilesets需要特殊处理,这里只是标记 print(f"✓ Cesium Tileset {{tileset_node.getName()}} 已识别") except Exception as e: print(f"处理Cesium Tileset {{tileset_node.getName()}} 失败: {{str(e)}}") except Exception as e: print(f"处理特殊元素时出错: {{str(e)}}") def tour(self): mopath = ( (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), ) self.controller.play_motion_path(mopath,3.0) MainApp().run() ''' 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)