#!/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) self._copyScriptsToBuild(build_dir, project_path) self._copyScriptSystemToBuild(build_dir) self._copyInfoPanelSystemToBuild(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') ) 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/script_system.py文件到构建目录""" try: # 源文件路径 source_script_system = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "core", "script_system.py") # 目标目录 core_dest = os.path.join(build_dir, "core") # 如果源文件存在 if os.path.exists(source_script_system): # 确保目标目录存在 if not os.path.exists(core_dest): os.makedirs(core_dest) # 复制文件 shutil.copy2(source_script_system, os.path.join(core_dest, "script_system.py")) print("✓ core/script_system.py文件已复制到build目录") else: print("⚠️ core/script_system.py文件未找到") except Exception as e: print(f"⚠️ 复制core/script_system.py文件时出错: {str(e)}") def _copyInfoPanelSystemToBuild(self, build_dir): """复制core/script_system.py文件到构建目录""" try: # 源文件路径 source_InfoPanel_system = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "core", "InfoPanelManager.py") # 目标目录 core_dest = os.path.join(build_dir, "core") # 如果源文件存在 if os.path.exists(source_InfoPanel_system): # 确保目标目录存在 if not os.path.exists(core_dest): os.makedirs(core_dest) # 复制文件 shutil.copy2(source_InfoPanel_system, os.path.join(core_dest, "InfoPanelManager.py")) print("✓ core/InfoPanelManager.py文件已复制到build目录") else: print("⚠️ core/InfoPanelManager.py文件未找到") except Exception as e: print(f"⚠️ 复制core/InfoPanelManager.py文件时出错: {str(e)}") 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): """创建应用程序主文件 - 通过复制模板文件""" # 获取模板文件路径(假设模板文件在项目根目录下的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 _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 json # # from direct.actor.Actor import Actor # from panda3d.core import TextNode, CardMaker, TextureStage, NodePath # #获取渲染管线路径 # 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._loadFont() # # self.loadFullScene() # self.loadGUIFromJSON() # # 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 _loadFont(self): # """加载中文字体""" # self.chinese_font = None # try: # self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') # if not self.chinese_font: # print("警告: 无法加载中文字体,将使用默认字体") # else: # print("✓ 中文字体加载成功") # except: # print("警告: 无法加载中文字体,将使用默认字体") # self.chinese_font = None # # def getChineseFont(self): # """获取中文字体""" # return self.chinese_font # # 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) # # 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 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) # # def loadGUIFromJSON(self): # gui_json_path = "gui/gui_elements.json" # # try: # if os.path.exists(gui_json_path): # with open(gui_json_path, "r", encoding="utf-8") as f: # content = f.read().strip() # if content: # gui_data = json.loads(content) # self.createGUIElement(gui_data) # except Exception as e: # print(f"加载GUI元素失败: {{str(e)}}") # import traceback # traceback.print_exc() # # def createGUIElement(self,element_data): # try: # processed_names = set() # element_original_data={{}} # for i, gui_info in enumerate(element_data): # name = gui_info.get("name", f"gui_element_{{i}}") # element_original_data[name] = { # "scale": gui_info.get("scale", [1, 1, 1]), # "position": gui_info.get("position", [0, 0, 0]), # "parent_name": gui_info.get("parent_name") # } # valid_parents = set() # for gui_info in element_data: # name = gui_info.get("name", f"gui_element_{{gui_info.get('index', 0)}}") # valid_parents.add(name) # # for i ,gui_info in enumerate(element_data): # try: # gui_type = gui_info.get("type","unknown") # name = gui_info.get("name",f"gui_element_{{i}}") # position = gui_info.get("position",[0,0,0]) # scale = gui_info.get("scale",[1,1,1]) # tags = gui_info.get("tags",{{}}) # text = gui_info.get("text","") # image_path = gui_info.get("image_path","") # video_path = gui_info.get("video_path","") # bg_image_path = gui_info.get("bg_image_path","") # parent_name = gui_info.get("parent_name") # # if name in processed_names: # continue # # processed_names.add(name) # # absolute_position = list(position) # absolute_scale = list(scale) # # if parent_name and parent_name in element_original_data: # parent_data = element_original_data[parent_name] # parent_scale = parent_data["scale"] # # if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", # "2d_video_screen"]: # # 位置需要乘以父级缩放来得到绝对位置 # for j in range(min(len(absolute_position), len(parent_scale))): # absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] # # # 缩放需要乘以父级缩放来得到绝对缩放 # for j in range(min(len(absolute_scale), len(parent_scale))): # absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] # # new_element = None # # if gui_type =="3d_text": # size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 # new_element = self.createGUI3DText( # pos = tuple(absolute_position), # text = text, # size = size # ) # elif gui_type == "button": # # 确保传入正确的参数类型 # new_element = self.createGUIButton( # pos=tuple(absolute_position), # text=text, # size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, # ) # except Exception as e: # print(f"重建GUI元素失败 {{name}}: {{e}}") # import traceback # traceback.print_exc() # continue # except Exception as e: # print(f"重建GUI元素失败: {{str(e)}}") # # def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1,command=None): # from direct.gui.DirectGui import DirectButton # # button = DirectButton( # text=text, # pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 # scale=size, # size 应该是数值而不是元组 # frameColor=(0.2, 0.6, 0.8, 1), # text_font=self.getChineseFont() if self.getChineseFont() else None, # rolloverSound=None, # clickSound=None, # parent=None, # command=command # ) # # def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): # """创建3D文本GUI元素""" # try: # # 创建文本节点 # text_node = TextNode("gui_3d_text") # text_node.setText(text) # text_node.setAlign(TextNode.ACenter) # # # 设置字体(如果可用) # if self.getChineseFont(): # text_node.setFont(self.getChineseFont()) # # # 创建节点路径并添加到场景 # text_np = self.render.attachNewNode(text_node) # # # 设置位置和大小 # text_np.setPos(Vec3(pos[0], pos[1], pos[2])) # text_np.setScale(size) # # # 设置面向摄像机 # #text_np.setBillboardPointEye() # # # 设置渲染属性 # text_np.setBin("fixed", 40) # text_np.setDepthWrite(False) # # return text_np # except Exception as e: # print(f"❌ 创建3D文本失败: {{str(e)}}") # import traceback # traceback.print_exc() # return None # # 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', 'direct.stdpy.file', 'direct.stdpy.pickle', 'panda3d.core', 'panda3d.direct', 'rpcore', 'rpcore.util.movement_controller', 'rpcore.native', 'rpcore.render_pipeline', 'rplibs', 'rpplugins', 'rpplugins.scattering', 'rpplugins.pssm', 'rpplugins.godrays', 'json', 'os', 'sys', 'six', 'collections', 'collections.abs', 'weakref', 'copy', 'itertools', 'importlib', 'importlib.util', 'importlib.machinery', ], }}, # 排除的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)