EG/project/project_manager.py
2025-09-29 11:25:19 +08:00

1341 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)
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_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:
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)