import os import sys import json import datetime import subprocess import shutil class ProjectManager: def __init__(self, world): self.world = world self.current_project_path = None self.project_config = None print("✓ 项目管理系统初始化完成") # ==================== 项目生命周期管理 ==================== def createNewProject(self, project_path, project_name): """创建新项目 Args: project_path: 项目路径 project_name: 项目名称 Returns: bool: 创建是否成功 """ if not project_path or not project_name: print("错误: 项目路径和名称不能为空") return False # 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 print(f"项目 '{project_name}' 创建成功!") return True except Exception as e: print(f"创建项目失败: {str(e)}") return False def openProject(self, project_path): """打开项目 Args: project_path: 项目路径 Returns: bool: 打开是否成功 """ # print(f"\n[DEBUG] ===== 开始打开项目: {project_path} =====") try: if not project_path: print("错误: 项目路径不能为空") return False # 检查是否是有效的项目文件夹 config_file = os.path.join(project_path, "project.json") # print(f"[DEBUG] 项目配置文件路径: {config_file}") # print(f"[DEBUG] 配置文件是否存在: {os.path.exists(config_file)}") if not os.path.exists(config_file): print("错误: 选择的不是有效的项目文件夹!") return False # 读取项目配置 # print(f"[DEBUG] 开始读取项目配置文件...") with open(config_file, "r", encoding="utf-8") as f: project_config = json.load(f) # print(f"[DEBUG] 项目配置读取成功: {project_config.get('name', 'Unknown')}") # 检查场景文件 scene_file = os.path.join(project_path, "scenes", "scene.bam") # print(f"[DEBUG] 场景文件路径: {scene_file}") # print(f"[DEBUG] 场景文件是否存在: {os.path.exists(scene_file)}") if os.path.exists(scene_file): # 加载场景前的安全检查 # print(f"[DEBUG] 开始加载场景文件...") # print(f"[DEBUG] 当前world状态检查:") # print(f"[DEBUG] - render节点存在: {hasattr(self.world, 'render') and self.world.render is not None}") # print(f"[DEBUG] - loader存在: {hasattr(self.world, 'loader') and self.world.loader is not None}") # print(f"[DEBUG] - scene_manager存在: {hasattr(self.world, 'scene_manager') and self.world.scene_manager is not None}") # 检查world的基本状态 if not hasattr(self.world, 'render') or self.world.render is None: # print(f"[DEBUG] 错误: world.render不存在或为None") return False if not hasattr(self.world, 'loader') or self.world.loader is None: # print(f"[DEBUG] 错误: world.loader不存在或为None") return False # 清理可能的残留状态 try: # 清理选择状态 if hasattr(self.world, 'selection') and self.world.selection: self.world.selection.clearSelection() # print(f"[DEBUG] 选择状态已清理") # 清理GUI状态 if hasattr(self.world, 'gui_elements'): for gui_elem in self.world.gui_elements: if gui_elem and not gui_elem.isEmpty(): gui_elem.detachNode() self.world.gui_elements.clear() # print(f"[DEBUG] GUI元素已清理") # 验证BAM文件 # print(f"[DEBUG] 验证BAM文件完整性...") if os.path.getsize(scene_file) == 0: # print(f"[DEBUG] 错误: BAM文件为空") return False # 检查文件权限 if not os.access(scene_file, os.R_OK): # print(f"[DEBUG] 错误: BAM文件不可读") return False # print(f"[DEBUG] BAM文件验证通过") except Exception as e: # print(f"[DEBUG] 清理状态时发生异常: {e}") return False if self.world.scene_manager.loadScene(scene_file): # print(f"[DEBUG] 场景加载成功") # 更新项目配置 project_config["scene_file"] = os.path.relpath(scene_file, project_path) else: # print(f"[DEBUG] 场景加载失败") return False else: # print(f"[DEBUG] 场景文件不存在,跳过场景加载") pass 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 # print(f"[DEBUG] ===== 项目打开成功: {project_path} =====") print("项目加载成功!") return True except Exception as e: # print(f"[DEBUG] ===== 项目打开失败 =====") print(f"加载项目时发生错误:{str(e)}") import traceback traceback.print_exc() return False def openProjectForPath(self, project_path): """通过路径打开项目 Args: project_path: 项目路径 Returns: bool: 打开是否成功 """ try: if not project_path: return False # 检查是否是有效的项目文件夹 config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): 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 print(f"项目 '{project_path}' 加载成功!") return True except Exception as e: error_msg = f"加载项目时发生错误:{str(e)}" print(error_msg) return False def saveProject(self): """保存项目 Returns: bool: 保存是否成功 """ try: # 检查是否有当前项目路径 if not self.current_project_path: print("错误: 请先创建或打开一个项目!") 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 print("项目保存成功!") return True else: print("错误: 保存场景失败!") return False except Exception as e: print(f"保存项目时发生错误:{str(e)}") return False # ==================== 项目打包功能 ==================== def buildPackage(self, build_dir): """打包项目为可执行文件 - 按照Panda3D官方标准方法 Args: build_dir: 打包输出目录 Returns: bool: 打包是否成功 """ try: # 检查是否有当前项目路径 if not self.current_project_path: print("错误: 请先创建或打开一个项目!") 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): print("错误: 请先保存场景!") 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) else: print("错误: 保存场景失败!") return False if not build_dir: print("错误: 请指定打包输出目录!") 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) 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)}") print("打包完成!可执行文件在 build 目录中。") return True else: return False except Exception as e: print(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): with open(config_file, "r", encoding="utf-8") as f: project_config = json.load(f) # 这里可以根据实际需要提取资源引用 except Exception as e: print(f"从场景文件提取资源引用时出错: {str(e)}") return referenced_files # ==================== 辅助方法 ==================== def _clearCurrentScene(self): """清空当前场景""" try: if hasattr(self.world, 'scene_manager') and self.world.scene_manager: self.world.scene_manager.clearScene() print("✓ 当前场景已清空") except Exception as e: print(f"清空场景失败: {str(e)}") def updateWindowTitle(self, parent_window, project_name): """更新窗口标题(保留方法以兼容旧代码)""" # 这个方法现在不需要做任何事情,因为我们不再处理UI pass def _createAppFile(self, build_dir, project_name): """创建标准的应用程序入口文件""" try: app_content = f'''#!/usr/bin/env python # -*- coding: utf-8 -*- """ {project_name} - Panda3D应用程序 自动生成的入口文件 """ import sys import os # 添加当前目录到Python路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: from panda3d.core import loadPrcFileData # 设置基本的Panda3D配置 loadPrcFileData("", """ window-title {project_name} sync-video false show-frame-rate-meter true """) # 导入主应用程序类 # 注意:这里需要根据实际情况修改导入路径 from main import MyWorld # 创建并运行应用程序 app = MyWorld() app.run() except ImportError as e: print(f"导入错误: {{e}}") print("请确保已正确安装Panda3D") sys.exit(1) except Exception as e: print(f"运行错误: {{e}}") sys.exit(1) ''' app_file = os.path.join(build_dir, "main.py") with open(app_file, "w", encoding="utf-8") as f: f.write(app_content) print(f"✓ 应用程序入口文件已创建: {app_file}") except Exception as e: print(f"创建应用程序入口文件失败: {str(e)}") def _createStandardSetupFile(self, build_dir, project_name): """创建标准的setup.py文件用于打包""" try: setup_content = f'''#!/usr/bin/env python # -*- coding: utf-8 -*- """ {project_name} - Panda3D项目打包配置 """ from setuptools import setup, find_packages import sys # 确保Python版本兼容性 if sys.version_info < (3, 6): sys.exit("Python 3.6或更高版本是必需的") setup( name="{project_name}", version="1.0.0", description="基于Panda3D的3D应用程序", author="", author_email="", url="", # 包含所有Python包 packages=find_packages(), # 包含非Python文件 include_package_data=True, package_data={{ "": ["*.bam", "*.egg", "*.png", "*.jpg", "*.jpeg", "*.ogg", "*.wav", "*.mp3"] }}, # 依赖项 install_requires=[ "panda3d>=1.10.13", ], # Python版本要求 python_requires=">=3.6", # 分类信息 classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], # 入口点 entry_points={{ "console_scripts": [ "{project_name}=main:main", ], }}, ) ''' setup_file = os.path.join(build_dir, "setup.py") with open(setup_file, "w", encoding="utf-8") as f: f.write(setup_content) print(f"✓ setup.py文件已创建: {setup_file}") except Exception as e: print(f"创建setup.py文件失败: {str(e)}") def _executeStandardBuild(self, build_dir): """执行标准的Panda3D打包命令""" try: # 这里可以实现具体的打包逻辑 # 例如使用pdeploy或其他打包工具 print(f"✓ 项目文件已准备到: {build_dir}") print("注意: 请使用Panda3D的pdeploy工具进行最终打包") return True except Exception as e: print(f"执行打包命令失败: {str(e)}") return False