EG/project/project_manager.py
Hector 450fcbb123 Merge remote-tracking branch 'origin/geng_migrate_on_imgui_hu' into hu_migrate_on_geng
# Conflicts:
#	imgui.ini
#	ui/panels/editor_panels_top.py
2026-03-18 15:16:07 +08:00

1312 lines
52 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.

import os
import sys
import json
import datetime
import subprocess
import shutil
import importlib.util
import py_compile
class ProjectManager:
def __init__(self, world):
self.world = world
self.current_project_path = None
self.project_config = None
print("✓ 项目管理系统初始化完成")
def _get_repo_root(self):
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def get_project_scripts_dir(self, project_path=None):
project_path = os.path.normpath(project_path or self.current_project_path or "")
if not project_path:
return ""
return os.path.join(project_path, "scripts")
def _ensure_project_scripts_dir(self, project_path):
scripts_dir = self.get_project_scripts_dir(project_path)
if scripts_dir:
os.makedirs(scripts_dir, exist_ok=True)
return scripts_dir
def _sync_project_script_manager(self, project_path, reload_scripts=True):
script_manager = getattr(self.world, "script_manager", None)
if not script_manager:
return ""
scripts_dir = self._ensure_project_scripts_dir(project_path)
script_manager.set_scripts_directory(
scripts_dir,
create=True,
reload_scripts=reload_scripts,
)
return scripts_dir
def _restore_project_context(self, project_path, project_config, reload_scripts=True):
normalized_project_path = os.path.normpath(project_path) if project_path else None
self.current_project_path = normalized_project_path
self.project_config = project_config
script_manager = getattr(self.world, "script_manager", None)
if not script_manager:
return
if normalized_project_path:
self._sync_project_script_manager(normalized_project_path, reload_scripts=reload_scripts)
else:
script_manager.set_scripts_directory(
"scripts",
create=True,
reload_scripts=reload_scripts,
)
def _sanitize_build_name(self, project_name):
invalid_chars = '<>:"/\\|?*'
safe_name = "".join("_" if char in invalid_chars else char for char in str(project_name or "").strip())
safe_name = safe_name.rstrip(". ")
return safe_name or "EGProject"
# ==================== 项目生命周期管理 ====================
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)
self._ensure_project_scripts_dir(full_project_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()
# 更新项目状态,确保后续保存脚本元数据时能拿到项目路径
self.current_project_path = full_project_path
self.project_config = project_config
self._sync_project_script_manager(full_project_path, reload_scripts=True)
# 自动保存初始场景
scene_file = os.path.join(scenes_path, "scene.bam")
if self.world.scene_manager.saveScene(scene_file, full_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("初始场景保存失败")
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')}")
previous_project_path = self.current_project_path
previous_project_config = self.project_config
self.current_project_path = os.path.normpath(project_path)
self.project_config = project_config
self._sync_project_script_manager(self.current_project_path, reload_scripts=True)
# 检查场景文件
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")
self._restore_project_context(
previous_project_path,
previous_project_config,
reload_scripts=True,
)
return False
if not hasattr(self.world, 'loader') or self.world.loader is None:
# print(f"[DEBUG] 错误: world.loader不存在或为None")
self._restore_project_context(
previous_project_path,
previous_project_config,
reload_scripts=True,
)
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文件为空")
self._restore_project_context(
previous_project_path,
previous_project_config,
reload_scripts=True,
)
return False
# 检查文件权限
if not os.access(scene_file, os.R_OK):
# print(f"[DEBUG] 错误: BAM文件不可读")
self._restore_project_context(
previous_project_path,
previous_project_config,
reload_scripts=True,
)
return False
# print(f"[DEBUG] BAM文件验证通过")
except Exception as e:
# print(f"[DEBUG] 清理状态时发生异常: {e}")
self._restore_project_context(
previous_project_path,
previous_project_config,
reload_scripts=True,
)
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] 场景加载失败")
self._restore_project_context(
previous_project_path,
previous_project_config,
reload_scripts=True,
)
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 = os.path.normpath(project_path)
self.project_config = project_config
# print(f"[DEBUG] ===== 项目打开成功: {project_path} =====")
print("项目加载成功!")
return True
except Exception as e:
self._restore_project_context(
locals().get("previous_project_path"),
locals().get("previous_project_config"),
reload_scripts=True,
)
# 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
previous_project_path = self.current_project_path
previous_project_config = self.project_config
# 检查是否是有效的项目文件夹
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)
self.current_project_path = os.path.normpath(project_path)
self.project_config = project_config
self._sync_project_script_manager(self.current_project_path, reload_scripts=True)
# 检查场景文件
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)
else:
self._restore_project_context(
previous_project_path,
previous_project_config,
reload_scripts=True,
)
return False
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 = os.path.normpath(project_path)
self.project_config = project_config
print(f"项目 '{project_path}' 加载成功!")
return True
except Exception as e:
self._restore_project_context(
locals().get("previous_project_path"),
locals().get("previous_project_config"),
reload_scripts=True,
)
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 self._save_scene_atomically(scene_file, project_path):
# 更新项目配置文件
config_file = os.path.join(project_path, "project.json")
project_config = self.project_config or {}
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 _save_scene_atomically(self, scene_file, project_path):
"""先保存到临时文件,成功后再原子替换正式场景文件。"""
scene_file = os.path.normpath(scene_file)
scene_dir = os.path.dirname(scene_file)
scene_name, scene_ext = os.path.splitext(os.path.basename(scene_file))
temp_scene_file = os.path.join(scene_dir, f"{scene_name}.tmp{scene_ext}")
final_gui_info_file = scene_file.replace('.bam', '_gui.json')
temp_gui_info_file = temp_scene_file.replace('.bam', '_gui.json')
final_lui_info_file = scene_file.replace('.bam', '_lui.json')
temp_lui_info_file = temp_scene_file.replace('.bam', '_lui.json')
for temp_path in (temp_scene_file, temp_gui_info_file, temp_lui_info_file):
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
try:
if not self.world.scene_manager.saveScene(temp_scene_file, project_path):
return False
os.replace(temp_scene_file, scene_file)
if os.path.exists(temp_gui_info_file):
os.replace(temp_gui_info_file, final_gui_info_file)
if os.path.exists(temp_lui_info_file):
os.replace(temp_lui_info_file, final_lui_info_file)
return True
except Exception as e:
print(f"原子保存场景失败: {e}")
return False
finally:
for temp_path in (temp_scene_file, temp_gui_info_file, temp_lui_info_file):
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
# ==================== 项目打包功能 ====================
def buildPackage(self, build_dir):
"""将当前项目打包为最终运行程序。"""
try:
if not self.current_project_path:
print("错误: 请先创建或打开一个项目!")
return False
project_path = os.path.normpath(self.current_project_path)
project_display_name = (
(self.project_config or {}).get("name")
or os.path.basename(project_path)
or "EGProject"
)
project_name = self._sanitize_build_name(project_display_name)
if not build_dir:
print("错误: 请指定打包输出目录!")
return False
scene_file = os.path.join(project_path, "scenes", "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("已取消场景中的物体选中状态")
self._sync_project_script_manager(project_path, reload_scripts=True)
if not self.saveProject():
print("错误: 保存场景失败!")
return False
output_root = os.path.normpath(build_dir)
staging_root = os.path.join(output_root, f".{project_name}_staging")
source_root = os.path.join(staging_root, "source")
assets_root = os.path.join(staging_root, "assets")
dist_dir = os.path.join(output_root, project_name)
if os.path.exists(staging_root):
shutil.rmtree(staging_root)
os.makedirs(source_root, exist_ok=True)
os.makedirs(assets_root, exist_ok=True)
if os.path.exists(dist_dir):
shutil.rmtree(dist_dir)
shutil.copy2(scene_file, os.path.join(assets_root, "scene.bam"))
self.copy_folder(os.path.join(project_path, "scenes", "resources"), assets_root)
self._merge_folder_contents(
os.path.join(project_path, "Resources"),
os.path.join(assets_root, "resources"),
)
if not self._saveGUIElementsToJSON(assets_root, project_path):
print("错误: 导出 GUI 数据失败!")
return False
self._saveCameraStateToJSON(assets_root)
self._copyScriptsToBuild(assets_root, project_path)
self._copyRenderPipelineRuntime(assets_root)
runtime_entry = self._createProjectRuntimeEntry(source_root, project_display_name)
if not runtime_entry:
print("错误: 生成项目运行时入口失败")
return False
success = self._executeProjectBuild(
source_root=source_root,
assets_root=assets_root,
build_root=output_root,
project_name=project_name,
)
if not success:
return False
if os.path.exists(staging_root):
shutil.rmtree(staging_root, ignore_errors=True)
exe_path = os.path.join(dist_dir, f"{project_name}.exe")
print(f"打包完成!项目运行程序: {exe_path}")
return True
except Exception as e:
print(f"打包过程出错:{str(e)}")
return False
def _merge_folder_contents(self, source_folder, destination_folder):
"""将 source_folder 的内容合并到 destination_folder。"""
if not source_folder or not os.path.exists(source_folder):
return False
os.makedirs(destination_folder, exist_ok=True)
for root, _, files in os.walk(source_folder):
rel_root = os.path.relpath(root, source_folder)
target_root = destination_folder if rel_root == "." else os.path.join(destination_folder, rel_root)
os.makedirs(target_root, exist_ok=True)
for file_name in files:
source_file = os.path.join(root, file_name)
target_file = os.path.join(target_root, file_name)
shutil.copy2(source_file, target_file)
return True
def _load_runtime_template(self):
template_path = os.path.join(self._get_repo_root(), "templates", "main_template.py")
with open(template_path, "r", encoding="utf-8") as f:
return f.read()
def _createProjectRuntimeEntry(self, source_root, project_name):
try:
template_content = self._load_runtime_template()
safe_project_name = project_name.replace("\\", "\\\\").replace('"', '\\"')
runtime_source = template_content.replace("__EG_PROJECT_NAME__", safe_project_name)
runtime_entry = os.path.join(source_root, "main.py")
with open(runtime_entry, "w", encoding="utf-8") as f:
f.write(runtime_source)
return runtime_entry
except Exception as e:
print(f"创建项目运行时入口失败: {e}")
return ""
def _executeProjectBuild(self, source_root, assets_root, build_root, project_name):
repo_root = self._get_repo_root()
work_root = os.path.join(build_root, f".{project_name}_work")
spec_root = os.path.join(build_root, f".{project_name}_spec")
for directory in (work_root, spec_root):
if os.path.exists(directory):
shutil.rmtree(directory)
os.makedirs(directory, exist_ok=True)
pyinstaller_runner = None
pyinstaller_candidates = [
[sys.executable, "-m", "PyInstaller"],
[sys.executable, "-m", "pyinstaller"],
["pyinstaller"],
]
for candidate in pyinstaller_candidates:
pyinstaller_probe = subprocess.run(
[*candidate, "--version"],
capture_output=True,
text=True,
check=False,
)
if pyinstaller_probe.returncode == 0:
pyinstaller_runner = candidate
break
if not pyinstaller_runner:
print("错误: 当前环境未安装 PyInstaller无法执行项目打包。")
return False
sep = ";" if os.name == "nt" else ":"
command = [
*pyinstaller_runner,
"--noconfirm",
"--clean",
"--windowed",
"--name",
project_name,
"--distpath",
build_root,
"--workpath",
work_root,
"--specpath",
spec_root,
"--paths",
source_root,
"--paths",
repo_root,
"--paths",
os.path.join(repo_root, "RenderPipelineFile"),
"--exclude-module",
"imgui_bundle",
"--exclude-module",
"p3dimgui",
"--hidden-import",
"core",
"--hidden-import",
"core.script_system",
"--hidden-import",
"core.CustomMouseController",
"--hidden-import",
"ssbo_component.runtime_importer",
"--hidden-import",
"ssbo_component.ssbo_controller",
"--hidden-import",
"rpcore",
"--collect-submodules",
"rpcore",
"--collect-submodules",
"rpplugins",
"--collect-submodules",
"rplibs",
]
panda_binary_dir = self._get_panda3d_binary_dir()
panda_display_binaries = [
"libpandagl.dll",
"libp3windisplay.dll",
"libpandadx9.dll",
"libp3tinydisplay.dll",
"cgGL.dll",
"cgD3D9.dll",
"d3dx9_43.dll",
]
if panda_binary_dir:
for binary_name in panda_display_binaries:
binary_path = os.path.join(panda_binary_dir, binary_name)
if os.path.exists(binary_path):
command.extend(["--add-binary", f"{binary_path}{sep}."])
data_mappings = [
(os.path.join(assets_root, "scene.bam"), "."),
(os.path.join(assets_root, "camera_state.json"), "."),
(os.path.join(assets_root, "gui"), "gui"),
(os.path.join(assets_root, "resources"), "resources"),
(os.path.join(assets_root, "scripts"), "scripts"),
(os.path.join(assets_root, "RenderPipelineFile"), "RenderPipelineFile"),
]
for source_path, target_path in data_mappings:
if os.path.exists(source_path):
command.extend(["--add-data", f"{source_path}{sep}{target_path}"])
command.append(os.path.join(source_root, "main.py"))
result = subprocess.run(
command,
cwd=source_root,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
print("项目打包失败。")
output = (result.stdout or "") + "\n" + (result.stderr or "")
print(output[-4000:])
return False
for directory in (work_root, spec_root):
if os.path.exists(directory):
shutil.rmtree(directory, ignore_errors=True)
print(f"✓ 项目已打包到: {os.path.join(build_root, project_name)}")
return True
def _get_panda3d_binary_dir(self):
try:
panda3d_spec = importlib.util.find_spec("panda3d.core")
if panda3d_spec and panda3d_spec.origin:
panda_package_dir = os.path.dirname(os.path.dirname(os.path.abspath(panda3d_spec.origin)))
candidate_dirs = [
os.path.join(panda_package_dir, "bin"),
os.path.join(os.path.dirname(panda_package_dir), "bin"),
]
for candidate_dir in candidate_dirs:
if os.path.isdir(candidate_dir):
return candidate_dir
except Exception as e:
print(f"⚠️ 获取 Panda3D 二进制目录失败: {e}")
return ""
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):
"""将项目脚本编译后复制到构建目录,避免直接暴露源码。"""
try:
scripts_dest = os.path.join(build_dir, "scripts")
scripts_src = self.get_project_scripts_dir(project_path)
if os.path.exists(scripts_dest):
shutil.rmtree(scripts_dest)
os.makedirs(scripts_dest, exist_ok=True)
if os.path.exists(scripts_src):
compiled_count = 0
fallback_count = 0
for root, _, files in os.walk(scripts_src):
rel_root = os.path.relpath(root, scripts_src)
target_root = scripts_dest if rel_root == "." else os.path.join(scripts_dest, rel_root)
os.makedirs(target_root, exist_ok=True)
for file_name in files:
if file_name.startswith("__"):
continue
source_file = os.path.join(root, file_name)
if file_name.endswith(".py"):
compiled_file = os.path.join(
target_root,
f"{os.path.splitext(file_name)[0]}.pyc",
)
try:
py_compile.compile(source_file, cfile=compiled_file, doraise=True)
compiled_count += 1
except py_compile.PyCompileError as e:
fallback_file = os.path.join(target_root, file_name)
shutil.copy2(source_file, fallback_file)
fallback_count += 1
print(f"⚠️ 编译脚本失败,已回退复制源码: {source_file} -> {fallback_file} ({e})")
elif not file_name.endswith((".pyc", ".pyo", ".log")):
shutil.copy2(source_file, os.path.join(target_root, file_name))
print(f"✓ Scripts已导出到 build/scripts编译 {compiled_count} 个脚本")
if fallback_count:
print(f"⚠️ {fallback_count} 个脚本因编译失败保留为 .py 源码")
else:
print("⚠️ 项目中没有scripts目录")
except Exception as e:
print(f"⚠️ 复制脚本文件时出错: {str(e)}")
def _copyRenderPipelineRuntime(self, build_dir):
"""复制运行时需要的 RenderPipeline 资源,并将源码编译为 pyc。"""
try:
source_root = os.path.join(self._get_repo_root(), "RenderPipelineFile")
target_root = os.path.join(build_dir, "RenderPipelineFile")
if os.path.exists(target_root):
shutil.rmtree(target_root)
if not os.path.exists(source_root):
print("⚠️ RenderPipelineFile 文件夹未找到")
return False
blocked_dirs = {"__pycache__", ".git", ".vscode", "samples", "toolkit"}
blocked_root_files = {"setup.py", "start_daytime_editor.py", "start_plugin_configurator.py"}
blocked_relative_dirs = {
os.path.normpath(os.path.join("rplibs", "yaml", "yaml_py2")),
}
for root, dirs, files in os.walk(source_root):
relative_root = os.path.relpath(root, source_root)
normalized_relative_root = os.path.normpath(relative_root)
dirs[:] = [
name for name in dirs
if name not in blocked_dirs
and os.path.normpath(os.path.join(normalized_relative_root, name)) not in blocked_relative_dirs
]
target_dir = target_root if relative_root == "." else os.path.join(target_root, relative_root)
os.makedirs(target_dir, exist_ok=True)
for file_name in files:
if file_name.endswith((".pyc", ".pyo", ".log")):
continue
if relative_root == "." and file_name in blocked_root_files:
continue
source_file = os.path.join(root, file_name)
if file_name.endswith(".py"):
compiled_file = os.path.join(
target_dir,
f"{os.path.splitext(file_name)[0]}.pyc",
)
try:
py_compile.compile(source_file, cfile=compiled_file, doraise=True)
except py_compile.PyCompileError as e:
print(f"⚠️ RenderPipeline 脚本编译失败,保留源码: {source_file} ({e})")
shutil.copy2(source_file, os.path.join(target_dir, file_name))
else:
shutil.copy2(source_file, os.path.join(target_dir, file_name))
print("✓ RenderPipeline 运行时资源已导出")
return True
except Exception as e:
print(f"⚠️ 导出 RenderPipeline 运行时资源失败: {e}")
return False
def _copyScriptSystemToBuild(self,build_dir):
core_files = [
"script_system.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 _saveCameraStateToJSON(self, build_dir):
"""保存当前主相机状态,供打包运行时恢复。"""
try:
camera = getattr(self.world, "camera", None) or getattr(self.world, "cam", None)
if not camera or camera.isEmpty():
return False
camera_state = {
"position": list(camera.getPos()),
"rotation": list(camera.getHpr()),
"camera_control_enabled": bool(getattr(self.world, "camera_control_enabled", True)),
}
camera_state_path = os.path.join(build_dir, "camera_state.json")
with open(camera_state_path, "w", encoding="utf-8") as f:
json.dump(camera_state, f, ensure_ascii=False, indent=4)
print(f"✓ 主相机状态已保存到 {camera_state_path}")
return True
except Exception as e:
print(f"⚠️ 保存主相机状态失败: {e}")
return False
def _updateResourcePaths(self,gui_info,project_path,build_dir):
project_path = os.path.normpath(project_path)
project_resources_dir = os.path.join(project_path, "Resources")
scene_resources_dir = os.path.join(project_path, "scenes", "resources")
def normalize_resource_path(old_path):
if not old_path:
return old_path
old_path = str(old_path).strip()
if not old_path:
return old_path
if old_path.startswith(("http://", "https://")):
return old_path
normalized_path = os.path.normpath(old_path.replace("/", os.sep))
if normalized_path == "resources" or normalized_path.startswith(f"resources{os.sep}"):
return normalized_path.replace("\\", "/")
candidate_paths = []
if os.path.isabs(normalized_path):
candidate_paths.append(normalized_path)
else:
candidate_paths.extend(
[
os.path.join(project_resources_dir, normalized_path),
os.path.join(scene_resources_dir, normalized_path),
os.path.join(project_path, normalized_path),
]
)
resolved_path = ""
for candidate in candidate_paths:
if os.path.exists(candidate):
resolved_path = os.path.normpath(candidate)
break
if not resolved_path and os.path.isabs(normalized_path):
resolved_path = normalized_path
if resolved_path:
for base_dir in (project_resources_dir, scene_resources_dir):
try:
if os.path.exists(base_dir) and os.path.commonpath([resolved_path, base_dir]) == os.path.normpath(base_dir):
relative_path = os.path.relpath(resolved_path, base_dir)
return os.path.join("resources", relative_path).replace("\\", "/")
except ValueError:
continue
try:
relative_path = os.path.relpath(resolved_path, project_path)
if not relative_path.startswith(".."):
return os.path.join("resources", relative_path).replace("\\", "/")
except ValueError:
pass
return os.path.join("resources", os.path.basename(resolved_path)).replace("\\", "/")
return os.path.join("resources", os.path.basename(normalized_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.15
imgui-bundle
Pillow>=9.0.1
numpy>=1.24
aiohttp>=3.9
openvr==2.2.0
pyassimp>=5.2.5
"""
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.15",
"imgui-bundle",
"Pillow>=9.0.1",
"numpy>=1.24",
"aiohttp>=3.9",
"openvr==2.2.0",
"pyassimp>=5.2.5",
],
# Python版本要求
python_requires=">=3.11",
# 分类信息
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