Compare commits

..

27 Commits

Author SHA1 Message Date
84325cc554 Add packaging support files and assets 2026-03-15 01:43:48 +08:00
bc1abe0060 Trim redundant build script docs 2026-03-15 01:37:33 +08:00
3d994dd556 Harden Windows and Linux packaging 2026-03-15 01:20:20 +08:00
cfe92a9a80 chore: 将 imgui.ini 从 git 跟踪中移除
imgui.ini 是运行时生成的 ImGui 配置文件,
不应纳入版本控制。
已在 .gitignore 中配置忽略。
2026-03-12 16:57:37 +08:00
d784a9d9db 配置文件 2026-03-12 14:46:43 +08:00
cccfe20be8 配置文件 2026-03-12 14:46:06 +08:00
e8c4ff4260 配置文件 2026-03-12 14:43:56 +08:00
61e7498835 删除 AGENTS.md 2026-03-12 06:37:08 +00:00
12276b13af 删除 imgui.ini 2026-03-12 06:36:57 +00:00
da63dd4877 修复读取项目 2026-03-06 15:14:52 +08:00
4a25031cac 修复加载项目 2026-03-06 09:52:57 +08:00
d2cfc77fc6 修复导入 2026-03-05 09:22:50 +08:00
Hector
37b3cc30dc 模型导入成黑色问题修复。打开项目使用SSBO导入模型逻辑流畅运行。 2026-02-28 17:39:04 +08:00
Hector
036b68ef41 合并更新 2026-02-28 11:10:43 +08:00
Hector
5752559cdd Merge remote-tracking branch 'origin/geng' into IMgui_hu 2026-02-28 11:06:14 +08:00
Your Name
7386d687da feat: 接入RenderPipeline选中描边后处理 2026-02-28 10:58:34 +08:00
Hector
28d2b124cc 合并更新 2026-02-28 10:49:03 +08:00
Hector
2ac08b0582 Merge remote-tracking branch 'origin/geng' into IMgui_hu
# Conflicts:
#	imgui.ini
#	ssbo_component/ssbo_controller.py
#	ssbo_component/ssbo_editor.py
2026-02-28 09:37:33 +08:00
Your Name
86aaa21ddd ssbo 视锥剔除优化提升效果一般 2026-02-27 16:57:53 +08:00
Hector
af898947b5 拆分 2026-02-27 16:52:00 +08:00
Your Name
f3f8da7b90 Optimize SSBO root selection to avoid full dynamic activation 2026-02-27 16:11:35 +08:00
Your Name
2183d3fc3e Fix SSBO root selection behavior and optimize group sync FPS 2026-02-27 16:02:40 +08:00
Your Name
756db5b010 Fix SSBO picking sync and deletion cleanup 2026-02-27 15:39:23 +08:00
Your Name
1fd7e1d7ac Update ssbo editor 2026-02-27 14:24:11 +08:00
Your Name
53e6a829e4 Fix gizmo selection: GPU picking, group proxy, parent-child transform, pick sync 2026-02-27 11:47:15 +08:00
Hector
e917f9019d 简单实现http通信 2026-02-27 11:14:08 +08:00
Hector
c93ab3edac 修复手柄选中以及移动异常,添加Imgui的web视图 2026-02-27 10:32:57 +08:00
79 changed files with 24841 additions and 34105 deletions

3
.gitignore vendored
View File

@ -58,3 +58,6 @@ Resources/models/Women_1.glb
/venv/
/engine/
/panda3d_imgui-1.1.0-py3-none-any.whl
imgui.ini
dist
build

331
AGENTS.md
View File

@ -1,219 +1,120 @@
# EG 项目概览与开发指南
# 元泰 EG 项目开发指南
## 项目简介
EG 是一个基于 Panda3D 引擎开发的 3D 编辑器和游戏引擎集成了高级渲染管线、VR 支持、物理模拟、脚本系统等功能。该项目主要用于创建 3D 场景、游戏开发和交互式应用程序。
## 核心技术栈
- **渲染引擎**: Panda3D 1.10.15 + RenderPipeline (延迟渲染、PBR材质)
- **GUI框架**: PyQt5 + imgui_bundle (用于编辑器界面)
- **VR支持**: OpenVR 2.2.0
- **脚本系统**: Python 3.10.12
- **其他依赖**: PyQt5-WebEngine, assimp, pillow 等
## 项目架构
### 核心模块
- **main.py** - 应用程序入口点,初始化所有系统
- **Start_Run.py** - 启动脚本,设置环境变量和路径
- **core/** - 核心功能模块
- `world.py` - 世界管理器,处理场景渲染和更新
- `selection.py` - 选择系统,处理对象选择
- `event_handler.py` - 事件处理系统
- `tool_manager.py` - 工具管理器
- `script_system.py` - 脚本系统
- `patrol_system.py` - 巡逻系统
- `Command_System.py` - 命令系统
- `terrain_manager.py` - 地形管理
- `vr_manager.py` - VR 功能管理
- `collision_manager.py` - 碰撞检测管理
- `resource_manager.py` - 资源管理
- `assembly_interaction.py` - 装配交互
- `maintenance_gui.py` - 维护界面
- `render_pipeline_utils.py` - 渲染管线工具
- `InfoPanelManager.py` - 信息面板管理
- `CustomMouseController.py` - 自定义鼠标控制器
### GUI 系统
- **gui/gui_manager.py** - GUI管理器处理2D/3D界面元素
- **ui/icon_manager.py** - 图标管理器
### 场景管理
- **scene/scene_manager.py** - 场景管理器,处理模型导入、场景树构建
- **scene/util.py** - 场景工具函数
### 项目管理
- **project/project_manager.py** - 项目管理器,处理项目创建、保存、加载
### 脚本系统
- **scripts/** - 包含各种预定义脚本
- `MoverScript.py` - 移动脚本
- `RotatorScript.py` - 旋转脚本
- `ScalerScript.py` - 缩放脚本
- `ColorChangerScript.py` - 颜色变化脚本
- `FollowerScript.py` - 跟随脚本
- `BouncerScript.py` - 弹跳脚本
- `ComboAnimatorScript.py` - 组合动画脚本
### VR 系统
- **vr_actions/** - VR动作配置
- `actions.json` - VR动作定义
- `bindings_*.json` - 不同VR设备的绑定配置
### 资源管理
- **Resources/** - 资源目录
- `models/` - 3D模型
- `textures/` - 纹理贴图
- `materials/` - 材质文件
- `animations/` - 动画文件
- `icons/` - 图标资源
### 渲染管线
- **RenderPipelineFile/** - 高级渲染管线
- `rpcore/` - 渲染管线核心
- `rpplugins/` - 渲染插件
- `effects/` - 后处理效果
- `config/` - 渲染配置
## 启动和运行
### 环境要求
- Python 3.10.12
- Panda3D 1.10.15
- PyQt5 5.15.9
- OpenVR 2.2.0 (VR功能)
### 运行方式
1. **直接运行主程序**:
```bash
python Start_Run.py
```
2. **带项目路径运行**:
```bash
python Start_Run.py /path/to/project
```
3. **从main.py运行**:
```bash
python main.py
```
### 配置文件
- **config/vr_settings.json** - VR渲染配置
- **imgui.ini** - ImGui界面配置
- **.gitignore** - Git忽略文件配置
## 开发约定
### 代码风格
- 使用UTF-8编码
- 遵循PEP 8代码规范
- 类名使用驼峰命名法
- 函数和变量使用下划线命名法
- 文件头部包含模块说明和导入信息
### 脚本开发
- 所有用户脚本应继承 `ScriptBase`
- 脚本文件放在 `scripts/` 目录下
- 使用 `ScriptManager` 管理脚本生命周期
- 脚本API通过 `world` 对象提供
### 插件开发
- 插件系统支持动态加载
- 插件配置文件使用JSON格式
- 插件应实现标准接口
### VR开发
- VR动作配置在 `vr_actions/actions.json` 中定义
- 支持多种VR设备Vive、Oculus、Index
- VR渲染配置在 `config/vr_settings.json`
## 构建和部署
### 依赖安装
```bash
pip install -r requirements/requirements.txt
```
### 测试
项目包含多个测试脚本:
- `test_quick_script.py` - 快速测试脚本
- `TestMover.py` - 移动测试
- `TestRotator.py` - 旋转测试
- `TestScaler.py` - 缩放测试
### 项目文件
- 项目配置使用JSON格式
- 项目文件包含场景、资源、脚本等信息
- 使用 `ProjectManager` 管理项目生命周期
## 常见问题
### VR相关问题
1. 确保VR设备已正确连接
2. 检查OpenVR运行时是否安装
3. 验证VR动作配置是否正确
### 渲染问题
1. 检查显卡驱动是否最新
2. 确认RenderPipeline配置正确
3. 验证材质和纹理路径
### 性能优化
1. 使用合适的LOD设置
2. 优化场景复杂度
3. 调整渲染质量设置
## 扩展开发
### 添加新脚本
1. 在 `scripts/` 目录创建新脚本文件
2. 继承 `ScriptBase`
3. 实现必要的方法
4. 通过 `ScriptManager` 注册脚本
### 添加新工具
1. 在 `core/tool_manager.py` 中注册新工具
2. 实现工具逻辑
3. 添加GUI界面元素
### 添加新渲染效果
1. 在 `RenderPipelineFile/rpplugins/` 目录创建插件
2. 实现渲染逻辑
3. 添加配置选项
## 联系和支持
- 项目Git仓库: http://10.0.0.99:4000/Rowland/EG.git
- 当前分支: imgui
- 最新提交: 移除qt依赖 (33e62bd1e4c2c8d3aac15e045b419edb8992d7ff)
元泰 EG 是基于 Panda3D 的 3D 编辑器和游戏引擎。
---
*该文档由iFlow CLI自动生成最后更新时间: 2026年1月28日*
## 🚀 快速开始
### 环境要求
- **Python 3.11.x**(必须)
- Windows 10/11 64位
- Visual Studio Build Tools用于构建
### 安装步骤
```bash
# 1. 检查 Python 版本(必须是 3.11
python --version
# 2. 验证版本
python build_scripts/check_python_version.py
# 3. 安装依赖
pip install -r requirements/requirements-minimal.txt
# 4. 运行程序
python Start_Run.py
```
---
## 📦 构建安装程序
### Windows
```powershell
# 在 VS Dev Shell 中执行
.\build_scripts\build_windows.ps1 -Version "1.0.0"
```
详细步骤请参考 `build_scripts/BUILD_GUIDE.md`
---
## 🗂️ 项目结构
```
元泰 EG/
├── AGENTS.md # 本文件
├── main.py # 主程序入口
├── Start_Run.py # 启动脚本
├── requirements/ # 依赖配置
│ ├── requirements-minimal.txt # 最小依赖(推荐)
│ └── requirements.txt # 完整依赖(旧)
├── build_scripts/ # 构建脚本和文档
│ ├── build_windows.ps1 # Windows 构建脚本
│ ├── build_linux.sh # Linux 构建脚本
│ ├── BUILD_GUIDE.md # 构建指南 ⭐
│ ├── BUILD_README.md # 构建说明
│ ├── INSTALLER_GUIDE.md # 完整指南
│ ├── QUICK_REFERENCE.md # 快速参考
│ ├── ICON_GUIDE.md # 图标准备
│ ├── PYTHON_VERSION_REQUIREMENT.md # Python版本要求
│ └── PACKAGING_CHECKLIST.md # 打包检查清单
├── core/ # 核心模块
├── RenderPipelineFile/ # 渲染管线
├── Resources/ # 资源文件
└── ...
```
---
## 🛠️ 技术栈
- **Python**: 3.11.x必须
- **渲染引擎**: Panda3D 1.10.16
- **UI框架**: imgui_bundle + p3dimgui
- **VR支持**: OpenVR 2.2.0
- **图像处理**: Pillow
### 已移除的依赖
为减少体积,已移除以下未使用的依赖:
- ❌ PyQt5 (~50MB)
- ❌ PySide6 (~100MB)
- ❌ 相关 Qt 配套库 (~20MB)
**总计节省 ~170MB**
---
## 📚 重要文档
| 文档 | 位置 | 用途 |
|------|------|------|
| 构建指南 | `build_scripts/BUILD_GUIDE.md` | 从零开始构建 ⭐ |
| Python版本要求 | `build_scripts/PYTHON_VERSION_REQUIREMENT.md` | Python 3.11 安装指南 |
| 快速参考 | `build_scripts/QUICK_REFERENCE.md` | 命令速查 |
| 打包检查清单 | `build_scripts/PACKAGING_CHECKLIST.md` | 构建前检查 |
---
## ⚠️ 重要提示
1. **Python 版本**: 必须使用 3.11,构建脚本会检查
2. **构建环境**: Windows 必须在 VS Dev Shell 中构建
3. **清理旧依赖**: 如果之前有安装 PyQt5/PySide6建议卸载以节省空间
---
## 🔗 相关链接
- Python 3.11: https://www.python.org/downloads/release/python-3119/
- Visual Studio Build Tools: https://visualstudio.microsoft.com/visual-cpp-build-tools/
---
**最后更新**: 2024年

1
EG

@ -1 +0,0 @@
Subproject commit 69e2bda47e9713705ad5c45a08b6fc643a2b51f6

View File

@ -13,6 +13,7 @@ enabled:
- scattering
- skin_shading
- sky_ao
- selection_outline
- smaa
- ssr
# - clouds

View File

@ -63,5 +63,6 @@ global_stage_order:
# Finishing stages, do not insert anything below
- UpscaleStage
- SelectionOutlineStage
- FinalStage
- UpdatePreviousPipesStage

View File

@ -40,7 +40,22 @@ from rpcore.rpobject import RPObject
NATIVE_CXX_LOADED = False
# Read the configuration from the flag-file
# Fix for Nuitka/PyInstaller: try multiple methods to find the flag file
import os
current_path = dirname(realpath(__file__))
# Nuitka standalone mode fix: if __file__ is empty or invalid, use sys.argv[0]
if not current_path or not isfile(join(current_path, "use_cxx.flag")):
# Try relative to executable
exe_dir = dirname(realpath(sys.argv[0]))
current_path = join(exe_dir, "RenderPipelineFile", "rpcore", "native")
# Try one more fallback: check environment variable
if not isfile(join(current_path, "use_cxx.flag")):
rp_path = os.environ.get("RENDER_PIPELINE_PATH", "")
if rp_path:
current_path = join(rp_path, "rpcore", "native")
cxx_flag_path = join(current_path, "use_cxx.flag")
if not isfile(cxx_flag_path):
RPObject.global_error("CORE", "Could not find cxx flag, please run the setup.py!")

View File

@ -0,0 +1,2 @@
"""Selection outline plugin package."""

View File

@ -0,0 +1,3 @@
settings:
daytime_settings:

View File

@ -0,0 +1,15 @@
from rpcore.pluginbase.base_plugin import BasePlugin
from .selection_outline_stage import SelectionOutlineStage
class Plugin(BasePlugin):
name = "Selection Outline"
author = "EG Team"
description = "Adds Unity-style selected-object outline as a post-process stage."
version = "1.0"
def on_stage_setup(self):
self.stage = self.create_stage(SelectionOutlineStage)

View File

@ -0,0 +1,68 @@
from panda3d.core import PNMImage, SamplerState, Texture, Vec4
from rpcore.render_stage import RenderStage
class SelectionOutlineStage(RenderStage):
required_pipes = ["ShadedScene"]
def __init__(self, pipeline):
RenderStage.__init__(self, pipeline)
self._enabled = False
self._outline_color = Vec4(1.0, 0.55, 0.0, 1.0)
self._outline_width = 2.0
self._fill_alpha = 0.0
self._mask_tex = self._make_default_mask()
@property
def produced_pipes(self):
return {"ShadedScene": self.target.color_tex}
def create(self):
self.target = self.create_target("SelectionOutline")
self.target.add_color_attachment(bits=16)
self.target.prepare_buffer()
self._apply_inputs()
def reload_shaders(self):
self.target.shader = self.load_plugin_shader("selection_outline.frag.glsl")
self._apply_inputs()
def set_mask_texture(self, mask_texture):
self._mask_tex = mask_texture if mask_texture else self._make_default_mask()
self._apply_inputs()
def set_enabled_outline(self, enabled):
self._enabled = bool(enabled)
self._apply_inputs()
def set_outline_style(self, color=None, width_px=None, fill_alpha=None):
if color is not None:
self._outline_color = Vec4(color)
if width_px is not None:
self._outline_width = max(0.0, float(width_px))
if fill_alpha is not None:
self._fill_alpha = max(0.0, min(1.0, float(fill_alpha)))
self._apply_inputs()
def _apply_inputs(self):
enabled_val = 1.0 if self._enabled else 0.0
self.target.set_shader_input("SelectionMaskTex", self._mask_tex)
self.target.set_shader_input("SelectionOutlineEnabled", enabled_val)
self.target.set_shader_input("SelectionOutlineColor", self._outline_color)
self.target.set_shader_input("SelectionOutlineWidth", self._outline_width)
self.target.set_shader_input("SelectionFillAlpha", self._fill_alpha)
def _make_default_mask(self):
image = PNMImage(1, 1, 4)
image.fill(0.0, 0.0, 0.0)
image.alpha_fill(0.0)
texture = Texture("selection_outline_default_mask")
texture.load(image)
texture.set_minfilter(SamplerState.FT_nearest)
texture.set_magfilter(SamplerState.FT_nearest)
texture.set_wrap_u(SamplerState.WM_clamp)
texture.set_wrap_v(SamplerState.WM_clamp)
return texture

View File

@ -0,0 +1,59 @@
/**
* Selection outline post process stage.
*/
#version 430
#pragma include "render_pipeline_base.inc.glsl"
uniform sampler2D ShadedScene;
uniform sampler2D SelectionMaskTex;
uniform float SelectionOutlineEnabled;
uniform vec4 SelectionOutlineColor;
uniform float SelectionOutlineWidth; // pixels
uniform float SelectionFillAlpha; // 0..1
out vec4 result;
float sample_mask(vec2 uv) {
return textureLod(SelectionMaskTex, uv, 0).r;
}
void main() {
vec2 uv = get_texcoord();
vec3 scene_col = textureLod(ShadedScene, uv, 0).rgb;
if (SelectionOutlineEnabled < 0.5) {
result = vec4(scene_col, 1.0);
return;
}
vec2 texel = vec2(max(1.0, SelectionOutlineWidth)) / SCREEN_SIZE;
float center = sample_mask(uv);
float max_nei = 0.0;
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, 0.0)));
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, 0.0)));
max_nei = max(max_nei, sample_mask(uv + vec2(0.0, texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2(0.0, -texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, -texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, texel.y)));
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, -texel.y)));
// Outer contour only.
float edge = clamp(max_nei - center, 0.0, 1.0);
float fill = center * SelectionFillAlpha;
vec3 col = scene_col;
if (fill > 0.0) {
col = mix(col, SelectionOutlineColor.rgb, fill * SelectionOutlineColor.a);
}
if (edge > 0.0) {
col = mix(col, SelectionOutlineColor.rgb, edge * SelectionOutlineColor.a);
}
result = vec4(col, 1.0);
}

Binary file not shown.

View File

@ -1,8 +1,11 @@
import os
import sys
import traceback
from datetime import datetime
# 添加项目根目录到 Python 路径
project_root = os.path.dirname(os.path.abspath(__file__))
# 添加项目根目录到 Python 路径。打包后可通过环境变量覆盖,
# 以适配 Windows dist / Linux AppDir 等不同目录布局。
project_root = os.environ.get("EG_PROJECT_ROOT") or os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, project_root)
# 设置工作目录为项目根目录
@ -20,30 +23,34 @@ sys.path.insert(0, render_pipeline_file_path)
icons_path = os.path.join(project_root, "icons")
sys.path.insert(0, icons_path)
def _write_startup_log(message: str) -> None:
"""Write startup diagnostics next to the executable/source root."""
log_path = os.path.join(project_root, "eg_startup.log")
with open(log_path, "a", encoding="utf-8") as handle:
handle.write(f"\n[{datetime.now().isoformat(timespec='seconds')}] {message}\n")
# 现在可以导入并运行主程序
if __name__ == "__main__":
args = sys.argv[1:]
# args = "/home/tiger/桌面/Test1"
# args = "C:/Users/29381/Desktop/1"
print(f'Path is {args}')
# 将整个列表转换为字符串(包括方括号)
args_str = ''.join(args)
try:
args = sys.argv[1:]
_write_startup_log(f"Launch args: {args}")
_write_startup_log(f"Working directory: {os.getcwd()}")
from main import MyWorld
if args:
# print(f"[DEBUG] 启动时传入项目路径: {args_str}")
# 创建应用实例
app = MyWorld()
# 如果传入了项目路径,尝试打开项目
if hasattr(app, 'project_manager'):
# print(f"[DEBUG] 尝试打开项目: {args_str}")
success = app.project_manager.openProject(args_str)
# print(f"[DEBUG] 项目打开结果: {success}")
# 将整个列表转换为字符串(包括方括号)
args_str = ''.join(args)
from main import MyWorld
if args:
app = MyWorld()
if hasattr(app, 'project_manager'):
app.project_manager.openProject(args_str)
app.run()
else:
# print(f"[DEBUG] 项目管理器未初始化")
pass
app.run()
else:
# print(f"[DEBUG] 无项目路径,正常启动")
app = MyWorld()
app.run()
app = MyWorld()
app.run()
except Exception:
_write_startup_log("Unhandled exception during startup:")
_write_startup_log(traceback.format_exc())
raise

View File

@ -0,0 +1,393 @@
# 元泰 EG 构建完整指南
本文档提供从零开始构建元泰 EG 安装程序的完整步骤。
---
## 📋 环境要求
### 必须条件
| 组件 | 版本 | 说明 |
|------|------|------|
| **Python** | **3.11.x** | 必须使用 3.11,其他版本不保证兼容 |
| Windows | 10/11 | 64位系统 |
| 磁盘空间 | 5GB+ | 构建需要大量临时文件 |
| 内存 | 4GB+ | 建议 8GB |
### 检查 Python 版本
```powershell
# 检查版本
python --version
# 必须显示: Python 3.11.x
# 使用项目提供的检查脚本
python build_scripts/check_python_version.py
```
如果版本不对,请参考 `build_scripts/PYTHON_VERSION_REQUIREMENT.md` 安装正确版本。
---
## 🚀 快速开始5分钟
### 步骤 1: 安装依赖
```powershell
# 确保是 Python 3.11
python --version
# 安装最小依赖(推荐)
python -m pip install -r requirements/requirements-minimal.txt
```
### 步骤 2: 验证安装
```powershell
# 测试程序能否运行
python Start_Run.py
```
### 步骤 3: 构建安装程序
```powershell
# 进入 VS Dev Shell必须有 C++ 编译器)
# 然后执行构建脚本
.\build_scripts\build_windows.ps1 -Version "1.0.0"
```
---
## 📌 打包原则
从 2026-03 的修复开始,项目采用“保守打包优先”的策略:
- 优先保证发行版可运行,不先追求安装包最小化
- 对 `RenderPipeline`、动态插件、文件路径动态导入使用显式白名单
- 将 `RenderPipelineFile`、`Resources`、`config`、`demo` 等目录视为运行时资源,整体复制
- 保留启动日志 `eg_startup.log`,用于定位打包后启动异常
详细背景和源码整改建议请参考:
- `build_scripts/PACKAGING_RISK_AUDIT.md`
---
## 📦 依赖说明
### 最小依赖requirements-minimal.txt
只包含程序运行必需的依赖:
```
Panda3D==1.10.16 # 3D 引擎核心
panda3d-frame # Panda3D 扩展
panda3d-imgui==1.1.0 # ImGui 集成
imgui-bundle==1.92.4 # ImGui 绑定
openvr==2.2.0 # VR 支持
Pillow==11.3.0 # 图像处理
PyYAML==5.4.1 # 配置解析
psutil==5.9.0 # 系统工具
```
### 已移除的依赖
以下依赖已从项目中移除,节省约 **170MB**
| 依赖 | 节省空间 | 原因 |
|------|---------|------|
| PyQt5 | ~50 MB | 未使用 |
| PySide6 | ~100 MB | 未使用 |
| PyQt5-Qt5, PyQt5_sip 等 | ~20 MB | 配套库 |
### 清理旧依赖
如果之前安装过旧依赖,建议清理:
```powershell
# 卸载未使用的 Qt 库
python -m pip uninstall -y PyQt5 PyQt5-Qt5 PyQt5_sip PySide6 PySide6_Addons PySide6_Essentials shiboken6
# 清理 pip 缓存
python -m pip cache purge
```
---
## 🔧 详细构建步骤
### 第一步:环境准备
1. **安装 Python 3.11**
- 下载https://www.python.org/downloads/release/python-3119/
- 安装时勾选 "Add Python to PATH"
2. **安装 Visual Studio Build Tools**Windows 必需)
- 下载https://visualstudio.microsoft.com/visual-cpp-build-tools/
- 选择 "使用 C++ 的桌面开发"
- 或安装完整 Visual Studio 2022
3. **验证环境**
```powershell
# 检查 Python
python --version # Python 3.11.x
# 检查编译器(在 VS Dev Shell 中)
cl # 应显示版本信息
```
### 第二步:安装项目依赖
```powershell
# 进入项目目录
cd C:\path\to\EG
# 安装依赖
python -m pip install -r requirements/requirements-minimal.txt
# 验证安装
python -c "import panda3d.core; import imgui_bundle; print('✓ 依赖安装成功')"
```
### 第三步:运行测试
```powershell
# 测试程序能否正常启动
python Start_Run.py
```
### 第四步:打开 VS Dev Shell
**什么是 VS Dev Shell**
VS Dev ShellVisual Studio 开发人员命令提示符)是一个包含 C++ 编译器cl.exe的特殊命令行窗口Nuitka 需要它来编译 Python 代码为机器码。
**如何打开 VS Dev Shell**
#### 方法 1: 通过开始菜单(推荐)
1. 按 `Win` 键打开开始菜单
2. 搜索:`Developer Command Prompt`
- 或中文:`开发人员命令提示符`
3. 点击 **"Developer Command Prompt for VS 2022"**
4. 建议右键选择 **"以管理员身份运行"**
#### 方法 2: 通过 Visual Studio
1. 打开 **Visual Studio 2022**
2. 点击菜单栏的 **"工具"** → **"命令行"** → **"开发者命令提示符"**
#### 方法 3: 使用 PowerShell 命令
```powershell
# 如果知道 VS 安装路径
& "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64
```
**验证 VS Dev Shell 已正确打开:**
```powershell
# 在 VS Dev Shell 中执行
cl
# 应该显示类似:
# 用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.xx.xxxxx 版
```
### 第五步:构建安装程序
**VS Dev Shell** 中执行:
#### 方法 A使用构建脚本推荐
```powershell
# 1. 进入项目目录
cd C:\Users\Tellme\apps\EG
# 2. 切换到 PowerShell如果当前是 cmd
powershell
# 3. 执行构建
.\build_scripts\build_windows.ps1 -Version "1.0.0"
```
该脚本已包含以下关键修复:
- 修正 `RenderPipeline` 在打包后可能找错 `config/pipeline.yaml` 的问题
- 显式包含 `rpcore`、`rplibs`、`rpplugins`
- 自动探测并尽量纳入 `plugins`、`gltf`、`playwright`、`PyQt5` 等可选包
- 自动复制运行时资源目录
#### 方法 B手动构建
```powershell
# 在 VS Dev Shell 中
cd C:\path\to\EG
python -m nuitka `
--standalone `
--include-package=panda3d `
--include-package=direct `
--include-package=imgui_bundle `
--include-package=p3dimgui `
--include-package=openvr `
--include-package=RenderPipelineFile `
--include-package=rpcore `
--include-package=rplibs `
--include-package=rpplugins `
--follow-import-to=rpcore `
--follow-import-to=rpplugins `
--include-data-files="$env:APPDATA\Python\Python311\site-packages\imgui_bundle\glfw3.dll=imgui_bundle\glfw3.dll" `
--windows-icon-from-ico=icons/app.ico `
--windows-disable-console `
--output-dir=build/nuitka `
--jobs=4 `
Start_Run.py
```
说明:
- 当前更推荐 `standalone + 资源目录复制` 的方式,而不是 `onefile`
- `RenderPipeline`、动态插件和外部资源目录在 `onefile` 下更容易出现路径问题
### 第五步:打包资源
构建完成后,手动复制资源文件:
```powershell
# 创建输出目录
$Version = "1.0.0"
New-Item -ItemType Directory -Force -Path "dist\EG_$Version" | Out-Null
# 复制可执行文件
Copy-Item "build\nuitka\Start_Run.exe" "dist\EG_$Version\元泰.exe" -Force
# 复制资源目录
$dirs = @("config", "icons", "Resources", "tex", "templates",
"core", "gui", "project", "scene", "scripts",
"ssbo_component", "tools", "TransformGizmo", "ui",
"RenderPipelineFile", "vr_actions", "new")
foreach ($dir in $dirs) {
Copy-Item $dir "dist\EG_$Version\$dir" -Recurse -Force
}
Write-Host "✓ 打包完成: dist\EG_$Version"
```
如果使用 `build_windows.ps1`,通常不需要再手动执行这一段,因为脚本已经自动复制资源。
### Linux 构建说明
Linux 构建脚本:
```bash
./build_scripts/build_linux.sh "1.0.0"
```
Linux 脚本已按与 Windows 相同的思路补强:
- 使用保守白名单显式包含核心包
- 自动探测并尽量纳入可选依赖
- 整体复制运行时资源目录
- 在 AppRun 中设置 `EG_PROJECT_ROOT`
- 让 AppImage 内的程序从真实资源根目录启动
---
## 📁 构建输出
构建完成后,输出目录结构:
```
dist/EG_1.0.0/
├── 元泰.exe # 主程序 (~120MB)
├── config/ # 配置文件
├── icons/ # 图标资源
├── Resources/ # 模型、材质等资源
├── RenderPipelineFile/ # 渲染管线
├── ... # 其他数据目录
└── (无 .py 源代码) # 已编译为机器码
```
---
## 🐛 常见问题
### Q1: Python 版本错误
**错误**`必须使用 Python 3.11,当前版本是 3.13.5`
**解决**
```powershell
# 安装 Python 3.11
# 参考 build_scripts/PYTHON_VERSION_REQUIREMENT.md
```
### Q2: 找不到 C++ 编译器
**错误**`未找到 Visual C++ 编译器`
**解决**
1. 安装 Visual Studio Build Tools
2. 在 **VS Dev Shell** 中运行构建
### Q3: 缺少 DLL
**错误**`Cannot find glfw3.dll`
**解决**:确保 `--include-data-files` 参数正确指向 DLL 路径:
```powershell
# 注意 Python 版本路径
# Python 3.11: ...\Python311\...
# Python 3.13: ...\Python313\...
```
### Q4: ModuleNotFoundError: rpcore
**错误**`No module named 'rpcore'`
**解决**:确保使用 `--follow-import-to=rpcore`
### Q5: 找不到 pipeline.yaml / rpplugins.*
**错误示例**
- `Failed to load YAML file: File not found`
- `No such file or directory: /$$rpconfig/pipeline.yaml`
- `ModuleNotFoundError: No module named 'rpplugins.ao'`
**原因**
- `RenderPipeline` 在打包后可能把基路径识别错
- 插件通过配置文件动态加载,打包器不会稳定自动发现
**解决**
- 使用仓库中的最新 `build_windows.ps1` / `build_linux.sh`
- 确保 `RenderPipelineFile` 被整体复制
- 确保 `rpcore`、`rplibs`、`rpplugins` 被显式纳入打包
### Q6: 双击后闪退,没有控制台输出
**解决**
- 查看发行目录中的 `eg_startup.log`
- 该文件会记录启动阶段 traceback
- 遇到打包问题时,优先保留这个日志进行排查
---
## 📚 相关文档
| 文档 | 用途 |
|------|------|
| `build_scripts/README.md` | build_scripts 目录说明 |
| `build_scripts/ICON_GUIDE.md` | 图标准备 |
| `build_scripts/PACKAGING_RISK_AUDIT.md` | 打包风险与整改建议 |
| `build_scripts/PYTHON_VERSION_REQUIREMENT.md` | Python 版本要求 |
| `build_scripts/PACKAGING_CHECKLIST.md` | 打包检查清单 |
---
**最后更新**: 2024年

285
build_scripts/ICON_GUIDE.md Normal file
View File

@ -0,0 +1,285 @@
# EG 图标准备指南
构建安装程序需要准备应用程序图标,本指南说明如何创建和放置图标文件。
---
## 📋 所需图标
| 平台 | 文件名 | 格式 | 推荐尺寸 | 用途 |
|------|--------|------|---------|------|
| Windows | `icons/app.ico` | ICO | 256x256 (多尺寸) | 可执行文件和安装程序图标 |
| Linux | `icons/app.png` | PNG | 256x256 | AppImage 和桌面图标 |
---
## 🎨 图标要求
### Windows ICO 文件
ICO 文件可以包含多个尺寸的图标:
- **必须包含**: 256x256 (32位)
- **推荐包含**: 48x48, 32x32, 16x16
- **可选**: 128x128, 64x64
**格式要求**:
- 支持透明背景 (32位带 Alpha 通道)
- 大图标使用 PNG 压缩
- 小图标使用 BMP 格式
### Linux PNG 文件
- **尺寸**: 256x256 像素
- **格式**: PNG带透明通道
- **色彩**: RGBA (32位)
---
## 🔧 创建图标的方法
### 方法 1: 使用现有图片转换
如果你有 PNG/JPG 格式的 logo
**Windows (ICO)**:
```bash
# 使用 ImageMagick
convert logo.png -define icon:auto-resize=256,128,64,48,32,16 icons/app.ico
# 或在线工具
# https://convertio.co/png-ico/
# https://icoconvert.com/
```
**Linux (PNG)**:
```bash
# 直接复制或调整大小
cp logo.png icons/app.png
# 或使用 ImageMagick 调整
convert logo.png -resize 256x256 icons/app.png
```
### 方法 2: 使用专业工具设计
| 工具 | 平台 | 类型 | 链接 |
|------|------|------|------|
| Adobe Illustrator | Win/Mac | 专业矢量 | - |
| Figma | Web | 免费在线 | figma.com |
| Canva | Web | 免费在线 | canva.com |
| GIMP | Win/Mac/Linux | 免费开源 | gimp.org |
| Inkscape | Win/Mac/Linux | 免费矢量 | inkscape.org |
### 方法 3: 使用 Python 脚本生成
如果你有源图片,可以使用 Python 生成:
```python
#!/usr/bin/env python3
"""图标生成脚本"""
from PIL import Image
import os
# 源图片路径
source_image = "source_logo.png" # 你的源图片
output_dir = "icons"
# 确保输出目录存在
os.makedirs(output_dir, exist_ok=True)
# 打开源图片
img = Image.open(source_image)
# 转换为 RGBA 模式 (确保透明通道)
if img.mode != 'RGBA':
img = img.convert('RGBA')
# 生成 Windows ICO (多尺寸)
icon_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
ico_images = []
for size in icon_sizes:
resized = img.resize(size, Image.Resampling.LANCZOS)
ico_images.append(resized)
# 保存 ICO (第一个图像作为根,其余作为子图标)
ico_images[0].save(
os.path.join(output_dir, "app.ico"),
format='ICO',
sizes=[(i.width, i.height) for i in ico_images],
append_images=ico_images[1:]
)
print(f"✓ 生成: {output_dir}/app.ico")
# 生成 Linux PNG (256x256)
png_size = (256, 256)
png_img = img.resize(png_size, Image.Resampling.LANCZOS)
png_img.save(os.path.join(output_dir, "app.png"), format='PNG')
print(f"✓ 生成: {output_dir}/app.png")
print("\n图标生成完成")
```
**使用方法**:
```bash
# 安装依赖
pip install Pillow
# 运行脚本
python generate_icons.py
```
### 方法 4: 从项目中提取
如果项目中有现有图标资源:
```bash
# 查找项目中的图标文件
find . -name "*.ico" -o -name "*.png" | grep -i icon
# 复制到正确位置
cp 找到的图标文件 icons/app.ico
cp 找到的图标文件 icons/app.png
```
---
## 📁 目录结构
放置图标后,项目结构应为:
```
EG/
├── icons/
│ ├── app.ico # Windows 图标 (必需)
│ ├── app.png # Linux 图标 (必需)
│ └── ... # 其他图标资源
├── build_scripts/
├── ...
```
---
## ✅ 验证图标
### Windows
```powershell
# 检查文件存在
Test-Path icons/app.ico
# 查看 ICO 内容 (需要工具)
# 使用 Resource Hacker: http://www.angusj.com/resourcehacker/
```
### Linux
```bash
# 检查文件存在
ls -la icons/app.png
# 查看图片信息
file icons/app.png
identify icons/app.png # 需要 ImageMagick
# 预览
xdg-open icons/app.png
```
---
## 🚀 测试图标
构建完成后,验证图标是否正确显示:
### Windows
1. **可执行文件图标**:
```powershell
# 检查 dist/EG_1.0.0/EG.exe 的图标
# 在资源管理器中查看,应该显示你的图标
```
2. **安装程序图标**:
```powershell
# 检查 dist/EG_Setup_v1.0.0_x64.exe 的图标
```
### Linux
```bash
# 检查 AppImage 图标
./dist/EG-1.0.0-x86_64.AppImage --appimage-mount
ls squashfs-root/EG.png
```
---
## 🐛 常见问题
### 问题 1: ICO 文件无法识别
**症状**: Windows 显示默认图标
**解决**:
- 确保 ICO 包含 256x256 尺寸
- 使用专业工具重新生成
- 检查文件是否损坏
### 问题 2: Linux 图标不显示
**症状**: AppImage 使用默认图标
**解决**:
- 确认 `icons/app.png` 存在
- 检查文件权限: `chmod 644 icons/app.png`
- 确认 PNG 格式正确: `file icons/app.png`
### 问题 3: 图标模糊
**症状**: 图标显示模糊或锯齿
**解决**:
- 使用矢量源文件生成
- 确保包含多个尺寸
- 使用高质量缩放算法 (LANCZOS)
---
## 📦 快速开始
如果你没有现成的图标,可以先使用占位图标:
```bash
# Windows: 创建一个简单的 ICO
# 使用在线工具生成,或先跳过 (Nuitka 会使用默认图标)
# Linux: 创建一个简单的 PNG
python3 -c "
from PIL import Image
import os
os.makedirs('icons', exist_ok=True)
img = Image.new('RGBA', (256, 256), (100, 150, 255, 255))
img.save('icons/app.png')
print('Created placeholder icon')
"
```
**注意**: 发布前务必替换为正式图标!
---
## 🎨 设计建议
1. **简洁明了**: 小尺寸下依然可识别
2. **品牌一致性**: 与应用风格一致
3. **透明背景**: 适应不同主题
4. **测试多尺寸**: 确保 16x16 到 256x256 都清晰
5. **考虑暗色主题**: 在深色背景下也可见
---
**文档版本**: 1.0

View File

@ -0,0 +1,294 @@
# EG 项目打包清单
本文档详细说明安装程序中包含的内容,以及打包决策依据。
---
## 📦 打包内容总览
### 代码部分 (由 Nuitka 编译)
| 入口文件 | 处理方式 | 说明 |
|---------|---------|------|
| `Start_Run.py` | 编译为主程序 | 应用入口 |
| `main.py` | 被导入编译 | 主程序逻辑 |
| `core/` | 编译进可执行文件 | 核心模块 |
| `gui/` | 编译进可执行文件 | GUI模块 |
| `project/` | 编译进可执行文件 | 项目管理 |
| `scene/` | 编译进可执行文件 | 场景管理 |
| `ssbo_component/` | 编译进可执行文件 | SSBO组件 |
| `tools/` | 编译进可执行文件 | 工具模块 |
| `TransformGizmo/` | 编译进可执行文件 | 变换工具 |
| `ui/` | 编译进可执行文件 | UI模块 |
### 数据资源 (复制到输出目录)
| 目录/文件 | 用途 | 必要性 |
|----------|------|--------|
| `config/` | 配置文件 | ✅ 必须 |
| `icons/` | 图标资源 | ✅ 必须 |
| `RenderPipelineFile/` | 渲染管线 | ✅ 必须 |
| `Resources/` | 资源文件 | ✅ 必须 |
| `scripts/` | 脚本系统 | ✅ 必须 |
| `templates/` | 项目模板 | ✅ 必须 |
| `tex/` | 纹理资源 | ✅ 必须 |
| `vr_actions/` | VR配置 | ✅ 必须 |
| `imgui.ini` | ImGui配置 | ✅ 必须 |
---
## ⚠️ 需要确认的问题
### 问题 1: `project/` 目录
**现状**: 当前配置中 **未包含** `project/` 目录
**分析**:
- `project/` 包含 `project_manager.py`, `webgl_packager.py`, `__init__.py`
- `main.py` 中导入: `from project.project_manager import ProjectManager`
**结论**: ⚠️ **必须包含!** 否则项目管理功能无法工作
**建议操作**: 添加到 `DATA_DIRECTORIES`
```python
DATA_DIRECTORIES = [
# ... 现有目录 ...
"project", # 添加此行
]
```
---
### 问题 2: `new/` 目录
**现状**: 当前配置中 **未包含**
**分析**:
- 包含 `project.json` (空项目模板)
- 可能是"新建项目"的默认模板
**结论**: ✅ **建议包含** 作为默认项目模板
**建议操作**: 添加到 `DATA_DIRECTORIES`
---
### 问题 3: `test_project/` 目录
**现状**: 当前配置中 **未包含**
**分析**:
- 包含示例 `project.json`
- 可用于演示功能
**选项**:
1. **不包含** - 减小安装包体积
2. **包含为可选组件** - 用户可选择安装
3. **包含** - 方便新用户学习
**建议**: 选项 1 或 3根据安装包大小决定
---
### 问题 4: Python 缓存文件
**现状**: 已排除 `__pycache__`, `*.pyc`, `*.pyo`, `*.pyd`
**分析**: ✅ 正确,这些是由 Python 运行时生成的缓存,不需要分发
---
### 问题 5: 开发环境文件
**现状**: 已排除 `.git`, `.gitignore`, `.idea`, `.vscode`
**分析**: ✅ 正确,这些是开发环境配置,用户不需要
---
### 问题 6: 构建脚本
**现状**: `build_scripts/` 未包含
**分析**: ✅ 正确,构建脚本不需要分发给最终用户
---
### 问题 7: 依赖文件
**现状**: `requirements/` 未包含
**分析**:
- 最终用户不需要 `requirements.txt`(依赖已编译进可执行文件)
- 但如果提供 SDK/开发套件,可能需要
**结论**: ✅ 当前处理正确
---
## 🔧 修正后的配置
### 更新 DATA_DIRECTORIES
```python
# ==================== 数据文件配置 ====================
# 需要包含的目录 (相对于项目根目录)
DATA_DIRECTORIES = [
# 配置和资源
"config",
"icons",
"Resources",
"tex",
"templates",
# 核心模块 (这些目录中的 .py 文件会被编译,但可能包含非 .py 资源)
"core", # 核心功能
"gui", # GUI相关
"project", # ⚠️ 新增: 项目管理 (包含非代码资源)
"scene", # 场景管理
"scripts", # 脚本系统
"ssbo_component", # SSBO编辑器
"tools", # 工具模块
"TransformGizmo", # 变换Gizmo
"ui", # UI模块
# 引擎和扩展
"RenderPipelineFile", # 渲染管线
"vr_actions", # VR配置
# 示例和模板
"new", # ⚠️ 新增: 默认项目模板
# "test_project", # 可选: 示例项目
]
```
### Nuitka 包含包确认
当前配置:
```python
"include_packages": [
"Panda3D",
"direct",
"imgui_bundle",
"p3dimgui",
"openvr",
"PIL",
"yaml",
"psutil",
],
```
**需要添加**:
- 检查 `requirements.txt` 中的其他依赖是否需要显式包含
---
## 📊 打包体积预估
| 目录 | 估算大小 | 说明 |
|------|---------|------|
| `RenderPipelineFile/` | ~50-100 MB | 渲染管线核心 |
| `Resources/` | 可变 | 用户资源 |
| `tex/` | ~10-50 MB | 默认纹理 |
| `icons/` | ~1-5 MB | 图标 |
| 其他代码目录 | ~5-10 MB | Python 源文件(编译前) |
| **总计** | **~100-200 MB** | 不含用户资源 |
---
## ✅ 打包检查清单
### 构建前请确认
#### 目录配置
- [x] `config/` - 配置目录 ✅
- [x] `core/` - 核心模块 ✅
- [x] `gui/` - GUI模块 ✅
- [x] `icons/` - 图标资源 ✅
- [x] `project/` - 项目管理 ✅ **(已添加)**
- [x] `RenderPipelineFile/` - 渲染管线 ✅
- [x] `Resources/` - 资源文件 ✅
- [x] `scene/` - 场景管理 ✅
- [x] `scripts/` - 脚本系统 ✅
- [x] `ssbo_component/` - SSBO组件 ✅
- [x] `templates/` - 模板 ✅
- [x] `tex/` - 纹理 ✅
- [x] `tools/` - 工具 ✅
- [x] `TransformGizmo/` - 变换工具 ✅
- [x] `ui/` - UI模块 ✅
- [x] `vr_actions/` - VR配置 ✅
- [x] `new/` - 默认项目模板 ✅ **(已添加)**
#### 文件检查
- [ ] `icons/app.ico` - Windows图标 ⚠️ **需要添加**
- [ ] `icons/app.png` - Linux图标 ⚠️ **需要添加**
- [x] `imgui.ini` - ImGui配置 ✅
- [x] `Start_Run.py` - 入口脚本 ✅
- [x] `main.py` - 主程序 ✅
#### 清理检查
- [x] 排除了所有 `__pycache__``.pyc` 文件 ✅
- [x] 排除了开发环境文件 (`.idea`, `.vscode`) ✅
- [x] 排除了构建目录 (`build_scripts`, `dist`, `build`) ✅
- [x] 排除了 `.git` 目录 ✅
#### 功能验证
- [ ] 测试构建成功
- [ ] 测试安装程序能正常运行
- [ ] 测试便携版能正常运行
- [ ] 验证项目管理功能正常
- [ ] 验证新建项目功能正常
---
## 🧪 验证方法
构建完成后,验证以下内容:
```powershell
# Windows
# 1. 检查输出目录结构
tree /f dist\EG_1.0.0
# 2. 运行程序测试基本功能
dist\EG_1.0.0\EG.exe
# 3. 检查关键文件是否存在
Test-Path dist\EG_1.0.0\config
Test-Path dist\EG_1.0.0\Resources
Test-Path dist\EG_1.0.0\scripts
```
```bash
# Linux
# 1. 检查 AppImage 内容
./dist/EG-1.0.0-x86_64.AppImage --appimage-mount
# 2. 运行测试
./dist/EG-1.0.0-x86_64.AppImage
```
---
## 🔒 安全注意事项
1. **代码保护**: Nuitka 编译后的机器码难以逆向,但资源文件(模型、纹理)是明文的
2. **敏感配置**: 确保 `config/` 中没有包含 API 密钥等敏感信息
3. **许可证**: 如果包含第三方资源,确保许可证允许再分发
---
**文档版本**: 1.0
**最后更新**: 2024年

View File

@ -0,0 +1,246 @@
# 元泰 EG 打包风险与源码整改清单
本文档用于记录当前项目在 Windows/Linux 打包时容易导致运行失败、功能失效或行为不一致的风险点,并给出建议的整改方向。
目标原则:
- 打包优先保证可运行,不追求安装包最小化
- 对动态导入、配置驱动加载、原生扩展依赖保持保守策略
- 对已声明移除的依赖,源码中不应再保留残留引用
---
## 一、已确认的打包风险
### 1. RenderPipeline 配置与插件属于运行时动态加载
相关位置:
- [RenderPipelineFile/config/plugins.yaml](/C:/Users/Tellme/apps/EG/RenderPipelineFile/config/plugins.yaml)
- [core/world.py](/C:/Users/Tellme/apps/EG/core/world.py)
- [build_scripts/build_windows.ps1](/C:/Users/Tellme/apps/EG/build_scripts/build_windows.ps1)
问题说明:
- `RenderPipeline` 会根据配置文件在运行时动态加载 `rpplugins.*`
- 这类插件不是普通静态 import打包工具无法稳定自动发现
- 已实际出现 `ModuleNotFoundError: No module named 'rpplugins.ao'`
建议:
- 打包脚本显式包含 `rpcore`、`rplibs`、`rpplugins`
- 保留整个 `RenderPipelineFile` 目录
- 显式设置 `RenderPipeline``base_path``config_dir`
- 后续新增插件时,同步检查打包白名单
### 2. 文件路径驱动的动态模块加载
相关位置:
- [main.py](/C:/Users/Tellme/apps/EG/main.py)
- [core/script_system.py](/C:/Users/Tellme/apps/EG/core/script_system.py)
问题说明:
- 使用 `importlib.util.spec_from_file_location()` 按文件路径加载模块
- 例如 `demo/video_integration.py`
- 这类模块不会被打包器通过静态分析自动发现
建议:
- 继续将对应目录视为运行时资源并整体复制
- 更理想的方案是逐步改为标准包导入
- 所有此类动态加载点都应登记到打包白名单
### 3. 运行时外部工具依赖目录结构
相关位置:
- [core/world.py](/C:/Users/Tellme/apps/EG/core/world.py)
- [core/tool_manager.py](/C:/Users/Tellme/apps/EG/core/tool_manager.py)
问题说明:
- 程序会在运行时启动 `toolkit/day_time_editor/main.py`
- 还会启动 `toolkit/material_editor/main.py`
- 还会启动 `toolkit/plugin_configurator/main.py`
- 如果这些目录未被完整复制,主程序可能能启动,但局部功能会失效
建议:
- 将整个 `RenderPipelineFile` 视为正式运行时资源
- 子工具路径不应依赖“开发目录结构恰好存在”
- 后续可考虑增加启动前存在性检测和更友好的错误提示
---
## 二、建议项目团队整改的源码问题
### 1. 残留的 PyQt5 引用需要清理
相关位置:
- [ui/icon_manager.py](/C:/Users/Tellme/apps/EG/ui/icon_manager.py)
- [core/selection.py](/C:/Users/Tellme/apps/EG/core/selection.py)
- [scene/scene_manager_convert_tiles_mixin.py](/C:/Users/Tellme/apps/EG/scene/scene_manager_convert_tiles_mixin.py)
- [core/world.py](/C:/Users/Tellme/apps/EG/core/world.py)
问题说明:
- 项目文档中已经说明移除了 `PyQt5/PySide6`
- 但源码仍保留多处 `PyQt5` 引用
- 这会导致打包后功能行为不一致,或在特定分支下运行时报错
建议:
- 用当前实际 UI 栈替代残留 Qt 逻辑
- 删除仅为兼容历史方案保留的无效 Qt 代码
- 对必须保留的兼容逻辑,至少统一做降级处理和提示
### 2. Playwright 浏览器依赖不应处于“隐式依赖”状态
相关位置:
- [core/imgui_webview.py](/C:/Users/Tellme/apps/EG/core/imgui_webview.py)
问题说明:
- 该模块依赖 `playwright.async_api`
- 同时还依赖 Chromium 浏览器安装
- 如果发行版没有附带这些条件,对应功能会直接不可用
建议:
- 明确决定是否正式支持此功能
- 如果正式支持,则纳入依赖与打包流程
- 如果不正式支持,则在发行版中禁用并提示,而不是运行时才报错
### 3. glTF 专用加载依赖缺少明确归档
相关位置:
- [ui/panels/animation_tools.py](/C:/Users/Tellme/apps/EG/ui/panels/animation_tools.py)
问题说明:
- 这里存在运行时 `import gltf`
- 这不是启动必经路径,因此问题可能只在用户点击特定功能时暴露
建议:
- 如果该功能是正式能力,则将 `gltf` 明确加入依赖清单和打包清单
- 如果只是补充路径,应在缺模块时给出明确降级提示
### 4. 平台特定依赖应显式治理
相关位置:
- [core/tool_manager.py](/C:/Users/Tellme/apps/EG/core/tool_manager.py)
问题说明:
- 这里使用 `win32gui`、`win32con`
- 当前代码虽然做了异常兜底,但功能可能 silently fail
建议:
- 明确是否正式依赖 pywin32
- 如果依赖,则加入打包环境
- 如果不依赖,则删除窗口激活逻辑或改为可观测提示
### 5. 硬编码开发机路径需要移除
相关位置:
- [core/world.py](/C:/Users/Tellme/apps/EG/core/world.py)
问题说明:
- 存在 `/home/tiger/...` 这类硬编码本地路径
- 即使当前分支未使用,也不应保留在仓库中
建议:
- 清理所有开发机私有绝对路径
- 改为项目资源路径、配置路径或用户可配置路径
---
## 三、建议长期执行的工程规则
### 1. 建立“动态导入登记表”
所有以下模式都必须登记并同步给打包脚本:
- `importlib.import_module`
- `spec_from_file_location`
- `__import__`
- 配置文件驱动插件加载
- 子进程启动外部 Python 工具
### 2. 建立“运行时资源目录白名单”
以下内容不应依赖打包器自动分析:
- 配置文件
- shader
- 模型
- 字体
- 图标
- demo 脚本
- 插件目录
- 工具目录
### 3. 已移除依赖必须同步清理源码
如果项目文档声明:
- 已移除 `PyQt5`
- 已移除 `PySide6`
则源码中不应继续保留这些引用,避免“文档说没有,代码里还在偷偷依赖”的情况。
### 4. 可选功能必须有明确产品策略
对以下类型的依赖,需要明确归类:
- 浏览器内核
- glTF 专用加载器
- 平台专用 API
- 外部命令行工具
只能二选一:
- 正式支持并纳入安装/打包流程
- 默认禁用并提供明确提示
不应继续保持“开发环境可用、发行版碰运气”的状态。
---
## 四、当前打包策略建议
当前推荐策略:
- 尽量显式包含相关包,不追求安装包最小化
- 尽量整体复制运行时资源目录
- 对启动阶段异常保留日志文件,便于定位下一层问题
建议构建策略:
- 显式包含 `rpcore`、`rplibs`、`rpplugins`
- 尽量显式包含 `core`、`scene`、`project`、`ui`、`gui`、`scripts`、`ssbo_component`、`tools`、`TransformGizmo`
- 自动探测并尽量包含 `plugins`、`gltf`、`playwright` 等可选包
- 整体复制 `RenderPipelineFile`、`Resources`、`config`、`demo` 等目录
---
## 五、建议团队后续执行顺序
1. 先确保打包脚本采用保守策略,优先恢复发行版可运行性
2. 再逐步清理源码中的动态依赖、残留 Qt 引用、硬编码路径
3. 最后再考虑做安装包体积优化,而不是反过来
---
最后更新2026-03-14

View File

@ -0,0 +1,99 @@
# Python 版本要求
## 必须使用的版本
**Python 3.11.x**
```bash
# 检查 Python 版本
python --version
# 应显示: Python 3.11.x
```
## 为什么不支持其他版本?
| 版本 | 状态 | 原因 |
|------|------|------|
| Python 3.10 | ❌ 不支持 | Panda3D 1.10.16 在 3.10 上有兼容性问题 |
| **Python 3.11** | ✅ **推荐** | 经过完整测试,所有功能正常 |
| Python 3.12 | ⚠️ 未测试 | 可能存在兼容性问题 |
| Python 3.13 | ⚠️ 不推荐 | 当前环境使用,但部分依赖可能有警告 |
## Windows 安装 Python 3.11
### 方法 1: 官网下载
1. 访问: https://www.python.org/downloads/release/python-3119/
2. 下载: `Windows installer (64-bit)`
3. 安装时勾选:
- ✅ `Add Python to PATH`
- ✅ `Use admin privileges when installing py.exe`
### 方法 2: 使用 Chocolatey
```powershell
# 安装 Chocolatey 后
choco install python --version=3.11.9
```
### 方法 3: 使用 pyenv-win
```powershell
# 安装 pyenv-win
pip install pyenv-win
# 安装 Python 3.11
pyenv install 3.11.9
pyenv global 3.11.9
```
## 验证安装
```powershell
# 检查版本
python --version
# Python 3.11.9
# 检查 pip
python -m pip --version
# pip 24.x from ...
```
## 安装项目依赖
```powershell
# 创建虚拟环境(推荐)
python -m venv venv
.\venv\Scripts\activate
# 安装依赖
python -m pip install -r requirements/requirements-minimal.txt
```
## 构建要求
构建安装程序时也必须使用 Python 3.11
```powershell
# 确保使用 Python 3.11
python --version
# Python 3.11.x
# 然后执行构建
.\build_scripts\build_windows.ps1
```
## 常见问题
### Q: 我已经安装了 Python 3.12/3.13,需要卸载吗?
**A**: 不需要卸载,可以同时安装多个版本。使用 `py -3.11` 或指定完整路径调用 Python 3.11。
### Q: 如何切换默认 Python 版本?
**A**: 修改系统 PATH 环境变量,将 Python 3.11 的路径放到其他版本前面。
### Q: 虚拟环境中可以使用不同版本吗?
**A**: 不可以,虚拟环境继承创建它的 Python 版本。需要用 Python 3.11 创建虚拟环境。
---
**最后更新**: 2024年

143
build_scripts/README.md Normal file
View File

@ -0,0 +1,143 @@
# build_scripts 目录说明
本目录包含元泰 EG 项目的所有构建相关脚本和文档。
---
## 📁 目录结构
```
build_scripts/
├── 🔧 构建脚本
│ ├── build_windows.ps1 # Windows 主构建脚本 ⭐
│ ├── build_linux.sh # Linux 构建脚本
│ └── (已移除临时/重复入口脚本)
├── 🔍 检查脚本
│ ├── check_env.ps1 # Windows 环境检查
│ ├── check_env.sh # Linux 环境检查
│ ├── check_python_version.py # Python 版本检查 ⭐
│ └── analyze_packaging.py # 打包分析工具
└── 📖 文档
├── BUILD_GUIDE.md # 完整构建指南 ⭐⭐⭐
├── ICON_GUIDE.md # 图标准备
├── PACKAGING_RISK_AUDIT.md # 打包风险与源码整改清单 ⭐
├── PYTHON_VERSION_REQUIREMENT.md # Python 3.11 要求 ⭐
└── PACKAGING_CHECKLIST.md # 打包检查清单
```
---
## 🚀 快速开始
### 第一次构建?
请按顺序阅读:
1. `BUILD_GUIDE.md` - 完整构建步骤
2. `PYTHON_VERSION_REQUIREMENT.md` - Python 3.11 安装
3. `ICON_GUIDE.md` - 准备应用图标(可选)
4. `PACKAGING_RISK_AUDIT.md` - 打包风险与源码整改清单
### 已经配置好环境?
直接使用:
```powershell
# Windows
.\build_scripts\build_windows.ps1 -Version "1.0.0"
# Linux
./build_scripts/build_linux.sh "1.0.0"
```
---
## 📋 关键检查点
构建前请确认:
- [ ] Python 3.11.x 已安装
- [ ] `python build_scripts/check_python_version.py` 通过
- [ ] `pip install -r requirements/requirements-minimal.txt` 完成
- [ ] Visual Studio Build Tools 已安装Windows
- [ ] 在 VS Dev Shell 中运行构建
---
## 🔧 核心脚本说明
### build_windows.ps1
Windows 主构建脚本,功能:
- 检查 Python 3.11
- 检查 C++ 编译器
- 执行保守白名单 Nuitka 编译
- 自动探测并尽量纳入可选依赖
- 复制运行时资源文件
- 生成最终输出
关键策略:
- 优先保证发行版可运行,不追求最小体积
- 显式包含 `rpcore`、`rplibs`、`rpplugins`
- 整体复制 `RenderPipelineFile`、`Resources`、`config`、`demo` 等目录
### build_linux.sh
Linux 主构建脚本,功能:
- 执行保守白名单 Nuitka 编译
- 构建 AppDir / AppImage
- 显式设置 `EG_PROJECT_ROOT`,保证 AppImage 中资源路径正确
- 复制运行时资源文件
用法:
```powershell
./build_scripts/build_linux.sh "1.0.0" --clean
```
### check_python_version.py
Python 版本检查工具:
```powershell
python build_scripts/check_python_version.py
```
### analyze_packaging.py
打包内容分析工具:
```powershell
python build_scripts/analyze_packaging.py
```
---
## 📝 重要变更
### 2024年更新
- ✅ 指定 Python 3.11 为必须版本
- ✅ 移除 PyQt5/PySide6 依赖(节省 ~170MB
- ✅ 简化依赖配置
- ✅ 重新组织文档结构
### 2026年更新
- ✅ 清理重复/临时构建脚本与说明文档
- ✅ Windows/Linux 构建脚本统一改为保守白名单策略
- ✅ 新增打包风险审计文档
---
## ❓ 常见问题
**Q: 构建失败怎么办?**
A: 查看 `BUILD_GUIDE.md` 的"常见问题"章节
**Q: Python 版本不对?**
A: 阅读 `PYTHON_VERSION_REQUIREMENT.md`
**Q: 缺少图标?**
A: 参考 `ICON_GUIDE.md` 创建或下载占位图标
---
**最后更新**: 2024年

View File

@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""
EG 打包分析工具
用于分析项目结构和打包配置的完整性
"""
import os
import sys
from pathlib import Path
from typing import List, Set, Tuple
# 项目根目录
PROJECT_ROOT = Path(__file__).parent.parent.absolute()
# 当前的打包配置 (从 setup.py 同步)
DATA_DIRECTORIES = [
# 配置和资源
"config",
"icons",
"Resources",
"tex",
"templates",
# 核心模块
"core",
"gui",
"project",
"scene",
"scripts",
"ssbo_component",
"tools",
"TransformGizmo",
"ui",
# 引擎和扩展
"RenderPipelineFile",
"vr_actions",
# 项目模板
"new",
]
# 排除的模式
EXCLUDE_PATTERNS = [
"__pycache__",
"*.pyc",
"*.pyo",
"*.pyd",
".git",
".gitignore",
".idea",
".vscode",
"*.egg-info",
"*.egg",
"dist",
"build",
"*.spec",
]
# 已知的开发/构建目录 (不应该打包)
DEV_DIRECTORIES = [
".git",
".idea",
".vscode",
"build",
"build_scripts",
"dist",
"test_project", # 示例项目,可选
"__pycache__",
]
# 已知的文档文件 (可选打包)
DOC_FILES = [
"README.md",
"LICENSE.txt",
"CHANGELOG.md",
"AGENTS.md",
]
def get_directory_size(path: Path) -> Tuple[int, int]:
"""计算目录大小和文件数量"""
total_size = 0
file_count = 0
if not path.exists():
return 0, 0
for item in path.rglob("*"):
if item.is_file():
# 检查是否应该排除
should_exclude = False
for pattern in EXCLUDE_PATTERNS:
if pattern.startswith("*"):
if item.name.endswith(pattern[1:]):
should_exclude = True
break
elif pattern in str(item.relative_to(path)):
should_exclude = True
break
if not should_exclude:
total_size += item.stat().st_size
file_count += 1
return total_size, file_count
def format_size(size_bytes: int) -> str:
"""格式化文件大小"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024
return f"{size_bytes:.2f} TB"
def analyze_project_structure():
"""分析项目结构"""
print("=" * 70)
print("EG 项目打包分析")
print("=" * 70)
print()
# 获取所有目录
all_dirs = [d for d in PROJECT_ROOT.iterdir() if d.is_dir()]
all_dir_names = {d.name for d in all_dirs}
# 分类
packaged_dirs = []
dev_dirs = []
missing_dirs = []
unknown_dirs = []
for d in all_dirs:
name = d.name
if name in DATA_DIRECTORIES:
packaged_dirs.append(d)
elif name in DEV_DIRECTORIES:
dev_dirs.append(d)
elif name.startswith(".") or name in ["new", "test_project"]:
# 特殊目录
if name not in DATA_DIRECTORIES:
missing_dirs.append(d)
else:
unknown_dirs.append(d)
# 1. 已配置的打包目录
print("📦 已配置的打包目录:")
print("-" * 70)
total_size = 0
total_files = 0
for d in sorted(packaged_dirs, key=lambda x: x.name):
size, count = get_directory_size(d)
total_size += size
total_files += count
status = "" if d.exists() else "✗ 不存在"
print(f" {status:12} {d.name:25} {format_size(size):>12} ({count} 文件)")
print("-" * 70)
print(f" {'总计':12} {'':25} {format_size(total_size):>12} ({total_files} 文件)")
print()
# 2. 开发目录 (正确排除)
if dev_dirs:
print("🔧 开发/构建目录 (正确排除):")
print("-" * 70)
for d in sorted(dev_dirs, key=lambda x: x.name):
size, count = get_directory_size(d)
print(f" {'✓ 排除':12} {d.name:25} {format_size(size):>12}")
print()
# 3. 未配置的目录 (需要检查)
if unknown_dirs:
print("⚠️ 未配置的目录 (请检查是否需要打包):")
print("-" * 70)
for d in sorted(unknown_dirs, key=lambda x: x.name):
size, count = get_directory_size(d)
print(f" {'? 待确认':12} {d.name:25} {format_size(size):>12} ({count} 文件)")
# 列出部分文件帮助判断
files = list(d.iterdir())[:5]
for f in files:
file_type = "📁" if f.is_dir() else "📄"
print(f" {file_type} {f.name}")
if len(list(d.iterdir())) > 5:
print(f" ... 等 {len(list(d.iterdir())) - 5} 个文件")
print()
# 4. 检查配置中但目录不存在的
config_only = set(DATA_DIRECTORIES) - all_dir_names
if config_only:
print("⚠️ 配置中有但目录不存在:")
print("-" * 70)
for name in config_only:
print(f" ✗ 不存在 {name}")
print()
# 5. 关键文件检查
print("🔑 关键文件检查:")
print("-" * 70)
critical_files = {
"Start_Run.py": "入口脚本",
"main.py": "主程序",
"imgui.ini": "ImGui配置",
"icons/app.ico": "Windows图标",
"icons/app.png": "Linux图标",
}
for file, desc in critical_files.items():
path = PROJECT_ROOT / file
exists = "" if path.exists() else ""
print(f" {exists:3} {file:25} ({desc})")
print()
# 6. 建议
print("💡 建议:")
print("-" * 70)
suggestions = []
if unknown_dirs:
suggestions.append(f"{len(unknown_dirs)} 个未配置目录,请检查是否需要打包")
if config_only:
suggestions.append(f"配置中有 {len(config_only)} 个目录不存在,请更新配置")
# 检查图标
if not (PROJECT_ROOT / "icons/app.ico").exists():
suggestions.append("缺少 Windows 图标: icons/app.ico")
if not (PROJECT_ROOT / "icons/app.png").exists():
suggestions.append("缺少 Linux 图标: icons/app.png")
# 检查大小
if total_size > 500 * 1024 * 1024:
suggestions.append(f"打包内容较大 ({format_size(total_size)}),考虑优化")
if not suggestions:
print(" ✓ 一切正常!")
else:
for s in suggestions:
print(f"{s}")
print()
# 7. 预估输出
print("📊 预估输出:")
print("-" * 70)
# Nuitka 编译后大约增加 20-50MB 开销
estimated_exe_size = total_size + 50 * 1024 * 1024
print(f" 数据文件: {format_size(total_size)}")
print(f" 编译开销(预估): ~50 MB")
print(f" 总计(预估): {format_size(estimated_exe_size)}")
print(f" 压缩后(预估): {format_size(estimated_exe_size * 0.6)}") # 假设 60% 压缩率
print()
# 返回是否有问题
has_issues = bool(unknown_dirs or config_only or not suggestions)
return has_issues
def generate_updated_config():
"""生成更新后的配置代码"""
print("=" * 70)
print("建议的配置更新")
print("=" * 70)
print()
# 获取所有实际存在的目录
all_dirs = {d.name for d in PROJECT_ROOT.iterdir() if d.is_dir()}
# 自动分类
auto_include = []
auto_exclude = []
for name in sorted(all_dirs):
if name in DEV_DIRECTORIES:
auto_exclude.append(name)
elif name.startswith("."):
auto_exclude.append(name)
else:
auto_include.append(name)
print("# 自动生成的配置建议:")
print()
print("DATA_DIRECTORIES = [")
for name in auto_include:
comment = ""
if name == "project":
comment = " # 项目管理模块"
elif name == "new":
comment = " # 默认项目模板"
elif name == "test_project":
comment = " # 示例项目 (可选)"
print(f' "{name}",{comment}')
print("]")
print()
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description="EG 打包分析工具")
parser.add_argument("--generate", action="store_true", help="生成配置建议")
args = parser.parse_args()
if args.generate:
generate_updated_config()
else:
has_issues = analyze_project_structure()
print()
print("=" * 70)
if has_issues:
print("发现潜在问题,请检查上方输出")
sys.exit(1)
else:
print("✓ 分析完成,配置看起来正常")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,525 @@
#!/bin/bash
# EG Linux 构建脚本 - 使用 Nuitka 编译 + AppImage 打包(保守白名单版)
# 用法: ./build_linux.sh [版本号] [--clean] [--skip-compile] [--deb]
set -e
# 配置
VERSION=${1:-"1.0.0"}
CLEAN=false
SKIP_COMPILE=false
BUILD_DEB=false
# 解析参数
for arg in "$@"; do
case $arg in
--clean)
CLEAN=true
shift
;;
--skip-compile)
SKIP_COMPILE=true
shift
;;
--deb)
BUILD_DEB=true
shift
;;
esac
done
# 路径配置
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DIST_DIR="$PROJECT_ROOT/dist"
BUILD_DIR="$PROJECT_ROOT/build"
NUITKA_DIR="$BUILD_DIR/nuitka"
APPDIR="$BUILD_DIR/EG_$VERSION.AppDir"
OUTPUT_NAME="EG_$VERSION"
# 应用信息
APP_NAME="EG"
COMPANY_NAME="EG Team"
APP_DESCRIPTION="EG 3D Editor and Game Engine"
# 需要包含的数据目录
DATA_DIRS=(
# 配置和资源
"config"
"icons"
"Resources"
"tex"
"templates"
# 核心模块
"core"
"gui"
"project" # 项目管理模块
"scene"
"scripts"
"ssbo_component"
"tools"
"TransformGizmo"
"ui"
# 引擎和扩展
"RenderPipelineFile"
"vr_actions"
# 项目模板
"new" # 默认空项目模板
"demo"
)
# 需要包含的单个文件
DATA_FILES=(
"imgui.ini"
)
REQUIRED_PACKAGES=(
"panda3d"
"direct"
"imgui_bundle"
"p3dimgui"
"openvr"
"RenderPipelineFile"
"rpcore"
"rplibs"
"rpplugins"
"core"
"scene"
"project"
"ui"
"gui"
"scripts"
"ssbo_component"
"tools"
"TransformGizmo"
"vr_actions"
)
OPTIONAL_PACKAGES=(
"plugins"
"gltf"
"playwright"
"PyQt5"
)
OPTIONAL_MODULES=(
"playwright.async_api"
"win32gui"
"win32con"
"PyQt5.QtCore"
"PyQt5.QtGui"
"PyQt5.QtWidgets"
)
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 辅助函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
local step=$1
local total=$2
local msg=$3
echo ""
echo -e "${YELLOW}[$step/$total]${NC} $msg"
}
python_importable() {
local module_name="$1"
python3 -c "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('$module_name') else 1)" >/dev/null 2>&1
}
# ==================== 主流程 ====================
echo "========================================"
echo " EG Linux Builder v$VERSION"
echo "========================================"
echo "项目路径: $PROJECT_ROOT"
# 步骤 1: 清理
log_step 1 6 "清理旧构建..."
if [ "$CLEAN" = true ]; then
rm -rf "$BUILD_DIR"
rm -rf "$DIST_DIR"
log_success "已清理所有构建文件"
fi
mkdir -p "$BUILD_DIR"
mkdir -p "$DIST_DIR"
# 步骤 2: 环境检查
log_step 2 6 "检查构建环境..."
# 检查必要的命令
for cmd in python3 pip3 gcc g++; do
if ! command -v $cmd &> /dev/null; then
log_error "$cmd 未安装"
exit 1
fi
done
PYTHON_VERSION=$(python3 --version 2>&1)
log_success "Python: $PYTHON_VERSION"
# 检查并安装构建依赖
log_info "检查构建依赖..."
pip3 install -q nuitka pyinstaller 2>/dev/null || true
log_success "构建依赖已就绪"
# 安装项目依赖
log_info "检查项目依赖..."
pip3 install -q -r "$PROJECT_ROOT/requirements/requirements-minimal.txt" 2>/dev/null || log_warning "部分依赖可能安装失败"
log_success "项目依赖已就绪"
# 步骤 3: Nuitka 编译
if [ "$SKIP_COMPILE" = false ]; then
log_step 3 6 "使用 Nuitka 编译主程序..."
cd "$PROJECT_ROOT"
REQUIRED_PACKAGE_ARGS=()
for package in "${REQUIRED_PACKAGES[@]}"; do
REQUIRED_PACKAGE_ARGS+=("--include-package=$package")
done
OPTIONAL_PACKAGE_ARGS=()
for package in "${OPTIONAL_PACKAGES[@]}"; do
if python_importable "$package"; then
OPTIONAL_PACKAGE_ARGS+=("--include-package=$package")
log_info "检测到可选包并纳入打包: $package"
fi
done
OPTIONAL_MODULE_ARGS=()
for module in "${OPTIONAL_MODULES[@]}"; do
if python_importable "$module"; then
OPTIONAL_MODULE_ARGS+=("--include-module=$module")
log_info "检测到可选模块并纳入打包: $module"
fi
done
# 构建 Nuitka 选项
NUITKA_OPTS=(
"--standalone"
"--follow-import-to=rpcore"
"--follow-import-to=rpplugins"
"--follow-import-to=core"
"--follow-import-to=scene"
"--follow-import-to=project"
"--follow-import-to=ui"
"--nofollow-import-to=tkinter,test,unittest,setuptools,pip"
"--linux-onefile-icon=icons/app.png"
"--lto=no"
"--jobs=$(nproc)"
"--output-dir=$NUITKA_DIR"
"--remove-output"
)
NUITKA_OPTS+=("${REQUIRED_PACKAGE_ARGS[@]}")
NUITKA_OPTS+=("${OPTIONAL_PACKAGE_ARGS[@]}")
NUITKA_OPTS+=("${OPTIONAL_MODULE_ARGS[@]}")
log_info "开始编译 (可能需要几分钟)..."
python3 -m nuitka "${NUITKA_OPTS[@]}" Start_Run.py
if [ $? -ne 0 ]; then
log_error "Nuitka 编译失败"
exit 1
fi
log_success "编译成功"
else
log_step 3 6 "跳过编译 (--skip-compile)"
fi
# 步骤 4: 准备 AppDir
log_step 4 6 "准备 AppDir..."
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin"
mkdir -p "$APPDIR/usr/lib/EG"
mkdir -p "$APPDIR/usr/share/applications"
mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$APPDIR/usr/share/icons/hicolor/128x128/apps"
mkdir -p "$APPDIR/usr/share/icons/hicolor/64x64/apps"
mkdir -p "$APPDIR/usr/share/metainfo"
# 复制 Nuitka standalone 目录
DIST_SOURCE="$NUITKA_DIR/Start_Run.dist"
if [ ! -d "$DIST_SOURCE" ]; then
log_error "找不到编译后的 standalone 目录: $DIST_SOURCE"
exit 1
fi
cp -r "$DIST_SOURCE/"* "$APPDIR/usr/lib/EG/"
if [ -f "$APPDIR/usr/lib/EG/Start_Run.bin" ]; then
cp "$APPDIR/usr/lib/EG/Start_Run.bin" "$APPDIR/usr/bin/EG"
elif [ -f "$APPDIR/usr/lib/EG/Start_Run" ]; then
cp "$APPDIR/usr/lib/EG/Start_Run" "$APPDIR/usr/bin/EG"
else
EXE_SOURCE=$(find "$APPDIR/usr/lib/EG" -maxdepth 1 -type f -executable | head -1)
if [ -z "$EXE_SOURCE" ]; then
log_error "找不到可执行文件"
exit 1
fi
cp "$EXE_SOURCE" "$APPDIR/usr/bin/EG"
fi
chmod +x "$APPDIR/usr/bin/EG"
log_success "复制可执行文件和 standalone 运行时"
# 复制数据目录
COPIED=0
for dir in "${DATA_DIRS[@]}"; do
if [ -d "$PROJECT_ROOT/$dir" ]; then
cp -r "$PROJECT_ROOT/$dir" "$APPDIR/usr/lib/EG/"
# 清理 Python 缓存
find "$APPDIR/usr/lib/EG/$dir" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find "$APPDIR/usr/lib/EG/$dir" -name "*.pyc" -delete 2>/dev/null || true
log_success "复制目录: $dir"
((COPIED++))
else
log_warning "目录不存在: $dir"
fi
done
log_success "共复制 $COPIED 个数据目录"
# 复制单个文件
for file in "${DATA_FILES[@]}"; do
if [ -f "$PROJECT_ROOT/$file" ]; then
cp "$PROJECT_ROOT/$file" "$APPDIR/usr/lib/EG/"
log_success "复制文件: $file"
fi
done
# 创建 .desktop 文件
cat > "$APPDIR/usr/share/applications/EG.desktop" << EOF
[Desktop Entry]
Name=EG
Name[zh_CN]=EG 编辑器
Comment=$APP_DESCRIPTION
Comment[zh_CN]=EG 3D 编辑器和游戏引擎
Exec=EG
Icon=EG
Type=Application
Categories=Graphics;3DGraphics;Development;Game;
MimeType=application/x-eg-project;
Terminal=false
StartupNotify=true
StartupWMClass=EG
Keywords=3D;Editor;Game;Engine;Panda3D;
EOF
# 创建 AppStream metadata (用于软件中心)
cat > "$APPDIR/usr/share/metainfo/EG.appdata.xml" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.yourcompany.EG</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>Proprietary</project_license>
<name>EG</name>
<summary>3D Editor and Game Engine</summary>
<description>
<p>EG is a powerful 3D editor and game engine based on Panda3D.</p>
</description>
<launchable type="desktop-id">EG.desktop</launchable>
<url type="homepage">https://your-website.com</url>
<provides>
<binary>EG</binary>
</provides>
</component>
EOF
# 复制图标
for size in 256 128 64; do
ICON_SRC="$PROJECT_ROOT/icons/app_${size}.png"
if [ -f "$ICON_SRC" ]; then
cp "$ICON_SRC" "$APPDIR/usr/share/icons/hicolor/${size}x${size}/apps/EG.png"
elif [ -f "$PROJECT_ROOT/icons/app.png" ]; then
# 如果只有一张图,复制到所有尺寸
cp "$PROJECT_ROOT/icons/app.png" "$APPDIR/usr/share/icons/hicolor/${size}x${size}/apps/EG.png"
break
fi
done
# 创建 AppRun
cat > "$APPDIR/AppRun" << 'EOF'
#!/bin/bash
# AppRun for EG
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
# 设置环境变量
export PATH="${HERE}/usr/bin:${PATH}"
export EG_PROJECT_ROOT="${HERE}/usr/lib/EG"
export LD_LIBRARY_PATH="${HERE}/usr/lib/EG:${HERE}/usr/lib:${LD_LIBRARY_PATH}"
export PYTHONPATH="${HERE}/usr/lib/EG:${HERE}/usr/lib/EG/RenderPipelineFile:${PYTHONPATH}"
# 设置 XDG 目录
export XDG_DATA_DIRS="${HERE}/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
# 切换到项目根目录,确保相对资源路径与 Windows 打包版保持一致
cd "${EG_PROJECT_ROOT}" || exit 1
# 执行主程序
exec "${HERE}/usr/bin/EG" "$@"
EOF
chmod +x "$APPDIR/AppRun"
# 复制 .desktop 和图标到根目录
cp "$APPDIR/usr/share/applications/EG.desktop" "$APPDIR/"
if [ -f "$APPDIR/usr/share/icons/hicolor/256x256/apps/EG.png" ]; then
cp "$APPDIR/usr/share/icons/hicolor/256x256/apps/EG.png" "$APPDIR/EG.png"
elif [ -f "$APPDIR/usr/share/icons/hicolor/128x128/apps/EG.png" ]; then
cp "$APPDIR/usr/share/icons/hicolor/128x128/apps/EG.png" "$APPDIR/EG.png"
elif [ -f "$APPDIR/usr/share/icons/hicolor/64x64/apps/EG.png" ]; then
cp "$APPDIR/usr/share/icons/hicolor/64x64/apps/EG.png" "$APPDIR/EG.png"
fi
log_success "AppDir 准备完成"
# 步骤 5: 创建 AppImage
log_step 5 6 "创建 AppImage..."
cd "$DIST_DIR"
# 下载 appimagetool
APPIMAGETOOL="appimagetool-x86_64.AppImage"
if [ ! -f "$APPIMAGETOOL" ]; then
log_info "下载 appimagetool..."
wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$APPIMAGETOOL" || {
log_error "下载 appimagetool 失败"
exit 1
}
chmod +x "$APPIMAGETOOL"
log_success "下载完成"
fi
# 创建 AppImage
APPIMAGE_NAME="EG-${VERSION}-x86_64.AppImage"
log_info "构建 AppImage..."
# 使用 --appimage-extract-and-run 避免 fuse 依赖
ARCH=x86_64 ./"$APPIMAGETOOL" --appimage-extract-and-run "$APPDIR" "$APPIMAGE_NAME" 2>&1 | while read line; do
echo " $line"
done
if [ -f "$APPIMAGE_NAME" ]; then
chmod +x "$APPIMAGE_NAME"
SIZE=$(du -h "$APPIMAGE_NAME" | cut -f1)
log_success "AppImage 创建成功: $APPIMAGE_NAME ($SIZE)"
else
log_error "AppImage 创建失败"
exit 1
fi
# 可选: 创建 DEB 包
if [ "$BUILD_DEB" = true ]; then
log_step 5 6 "创建 DEB 包..."
DEB_DIR="$BUILD_DIR/eg_${VERSION}_amd64"
mkdir -p "$DEB_DIR/DEBIAN"
mkdir -p "$DEB_DIR/opt/EG"
mkdir -p "$DEB_DIR/usr/share/applications"
mkdir -p "$DEB_DIR/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$DEB_DIR/usr/bin"
# 控制文件
cat > "$DEB_DIR/DEBIAN/control" << EOF
Package: eg
Version: $VERSION
Section: graphics
Priority: optional
Architecture: amd64
Depends: libgl1, libopenal1, libglu1-mesa, python3 (>= 3.8)
Maintainer: $COMPANY_NAME <support@your-website.com>
Description: EG 3D Editor and Game Engine
EG is a powerful 3D editor and game engine
based on Panda3D with advanced rendering.
EOF
# 复制文件
cp -r "$APPDIR/usr/"* "$DEB_DIR/opt/EG/"
# 创建启动器
cat > "$DEB_DIR/usr/bin/EG" << EOF
#!/bin/bash
cd /opt/EG
./bin/EG "\$@"
EOF
chmod +x "$DEB_DIR/usr/bin/EG"
# 复制 desktop 文件和图标
cp "$APPDIR/usr/share/applications/EG.desktop" "$DEB_DIR/usr/share/applications/"
if [ -f "$APPDIR/usr/share/icons/hicolor/256x256/apps/EG.png" ]; then
cp "$APPDIR/usr/share/icons/hicolor/256x256/apps/EG.png" "$DEB_DIR/usr/share/icons/hicolor/256x256/apps/"
fi
# 构建 deb
dpkg-deb --build "$DEB_DIR" "$DIST_DIR/eg_${VERSION}_amd64.deb"
if [ -f "$DIST_DIR/eg_${VERSION}_amd64.deb" ]; then
SIZE=$(du -h "$DIST_DIR/eg_${VERSION}_amd64.deb" | cut -f1)
log_success "DEB 包创建成功: eg_${VERSION}_amd64.deb ($SIZE)"
fi
fi
# 步骤 6: 创建 tarball
log_step 6 6 "创建 tarball..."
cd "$BUILD_DIR"
# 创建普通 tarball
TARBALL="$DIST_DIR/EG_${VERSION}_Linux_Portable.tar.gz"
tar -czf "$TARBALL" -C "$APPDIR/usr/lib/EG" .
SIZE=$(du -h "$TARBALL" | cut -f1)
log_success "Tarball 创建成功: EG_${VERSION}_Linux_Portable.tar.gz ($SIZE)"
# ==================== 完成 ====================
echo ""
echo "========================================"
echo -e "${GREEN} 构建完成!${NC}"
echo "========================================"
echo ""
echo -e "${BLUE}输出文件:${NC}"
for file in "$DIST_DIR"/EG-*; do
if [ -f "$file" ]; then
name=$(basename "$file")
size=$(du -h "$file" | cut -f1)
echo -e " ${GREEN}${NC} $name (${size})"
fi
done
echo ""
echo -e "${BLUE}输出目录:${NC} $DIST_DIR"
echo ""
# 清理
rm -rf "$APPDIR"
log_info "临时文件已清理"

View File

@ -0,0 +1,201 @@
# 元泰 EG Windows 构建脚本 - Nuitka
param([string]$Version = "1.0.0")
$ErrorActionPreference = "Stop"
$ProjectRoot = "C:\Users\Tellme\apps\EG"
$BuildDir = "$ProjectRoot\build"
$DistDir = "$ProjectRoot\dist\EG_$Version"
$DataDirs = @(
"config",
"icons",
"Resources",
"tex",
"templates",
"core",
"gui",
"project",
"scene",
"scripts",
"ssbo_component",
"tools",
"TransformGizmo",
"ui",
"RenderPipelineFile",
"vr_actions",
"new",
"demo"
)
$DataFiles = @(
"imgui.ini"
)
$RequiredPackages = @(
"panda3d",
"direct",
"imgui_bundle",
"p3dimgui",
"openvr",
"RenderPipelineFile",
"rpcore",
"rplibs",
"rpplugins",
"core",
"scene",
"project",
"ui",
"gui",
"scripts",
"ssbo_component",
"tools",
"TransformGizmo",
"vr_actions"
)
$OptionalPackages = @(
"plugins",
"gltf",
"playwright",
"PyQt5"
)
$OptionalModules = @(
"playwright.async_api",
"win32gui",
"win32con",
"PyQt5.QtCore",
"PyQt5.QtGui",
"PyQt5.QtWidgets"
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 元泰 EG 构建 (Nuitka)" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# 必须在 VS Dev Shell 中运行
if (!(Get-Command cl -ErrorAction SilentlyContinue)) {
Write-Host "错误: 请在 VS Dev Shell 中运行此脚本" -ForegroundColor Red
exit 1
}
# 清理
Write-Host "`n[1/4] 清理..." -ForegroundColor Yellow
Remove-Item $BuildDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $DistDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
Write-Host " 完成" -ForegroundColor Green
# 设置环境
Write-Host "`n[2/4] 设置环境..." -ForegroundColor Yellow
$env:PYTHONPATH = "$ProjectRoot;$ProjectRoot\RenderPipelineFile"
$env:PYTHONDONTWRITEBYTECODE = "1"
$imguiPath = (py -3.11 -c "import imgui_bundle, os; print(os.path.dirname(imgui_bundle.__file__))").Replace('\', '/')
$glfwDll = "$imguiPath/glfw3.dll"
Write-Host " glfw3.dll: $glfwDll" -ForegroundColor Gray
function Test-PythonImportable {
param([string]$ModuleName)
& py -3.11 -c "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('$ModuleName') else 1)" | Out-Null
return ($LASTEXITCODE -eq 0)
}
$requiredPackageArgs = @()
foreach ($package in $RequiredPackages) {
$requiredPackageArgs += "--include-package=$package"
}
$detectedOptionalPackageArgs = @()
foreach ($package in $OptionalPackages) {
if (Test-PythonImportable $package) {
$detectedOptionalPackageArgs += "--include-package=$package"
Write-Host " 检测到可选包并纳入打包: $package" -ForegroundColor Gray
}
}
$detectedOptionalModuleArgs = @()
foreach ($module in $OptionalModules) {
if (Test-PythonImportable $module) {
$detectedOptionalModuleArgs += "--include-module=$module"
Write-Host " 检测到可选模块并纳入打包: $module" -ForegroundColor Gray
}
}
# Nuitka 编译
Write-Host "`n[3/4] Nuitka 编译 (预计 20-40 分钟)..." -ForegroundColor Yellow
Write-Host " 关键修复: 使用正斜杠作为目标路径" -ForegroundColor Gray
Write-Host ""
$nuitkaArgs = @(
"-m", "nuitka"
"--standalone"
"--follow-import-to=rpcore"
"--follow-import-to=rpplugins"
"--follow-import-to=core"
"--follow-import-to=scene"
"--follow-import-to=project"
"--follow-import-to=ui"
"--nofollow-import-to=tkinter,test,unittest,setuptools,pip"
# 关键: 正斜杠作为目标路径
"--include-data-files=$glfwDll=imgui_bundle/glfw3.dll"
"--windows-icon-from-ico=icons/app.ico"
"--windows-console-mode=disable"
"--windows-company-name=元泰"
"--windows-product-name=元泰 EG"
"--windows-file-version=$Version"
"--windows-product-version=$Version"
"--output-dir=$BuildDir"
"--jobs=8"
"--lto=no"
"Start_Run.py"
)
$nuitkaArgs += $requiredPackageArgs
$nuitkaArgs += $detectedOptionalPackageArgs
$nuitkaArgs += $detectedOptionalModuleArgs
& py -3.11 @nuitkaArgs
if ($LASTEXITCODE -ne 0) {
Write-Host "`n[错误] 编译失败!" -ForegroundColor Red
exit 1
}
Write-Host "`n 编译成功" -ForegroundColor Green
# 复制到输出目录
Write-Host "`n[4/4] 整理输出..." -ForegroundColor Yellow
$sourceDir = "$BuildDir\Start_Run.dist"
Copy-Item "$sourceDir\*" $DistDir -Recurse -Force
# 复制项目运行时资源。Nuitka 会收集 Python 模块,但这些目录里的配置、
# shader、模型、图标等仍然需要以真实文件形式存在于 dist 中。
foreach ($dir in $DataDirs) {
$sourcePath = Join-Path $ProjectRoot $dir
if (Test-Path $sourcePath) {
Copy-Item $sourcePath (Join-Path $DistDir $dir) -Recurse -Force
Write-Host " 已复制目录: $dir" -ForegroundColor Gray
}
}
foreach ($file in $DataFiles) {
$sourcePath = Join-Path $ProjectRoot $file
if (Test-Path $sourcePath) {
Copy-Item $sourcePath $DistDir -Force
Write-Host " 已复制文件: $file" -ForegroundColor Gray
}
}
# 重命名 exe
if (Test-Path "$DistDir\Start_Run.exe") {
Rename-Item "$DistDir\Start_Run.exe" "元泰.exe"
}
$exeSize = [math]::Round((Get-Item "$DistDir\元泰.exe").Length / 1MB, 2)
Write-Host " 元泰.exe ($exeSize MB)" -ForegroundColor Green
# 完成
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " 构建完成!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host "输出: $DistDir\元泰.exe" -ForegroundColor White
Write-Host ""
Write-Host "测试运行:" -ForegroundColor Yellow
Write-Host " $DistDir\元泰.exe" -ForegroundColor Gray

139
build_scripts/check_env.ps1 Normal file
View File

@ -0,0 +1,139 @@
#!/usr/bin/env powershell
# EG Windows 构建环境检查脚本
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " EG 构建环境检查" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
$Issues = @()
$Warnings = @()
# 检查 Python
Write-Host "`n[1/6] 检查 Python..." -ForegroundColor Yellow
try {
$PythonVersion = python --version 2>&1
if ($PythonVersion -match "Python (\d+)\.(\d+)") {
$Major = [int]$Matches[1]
$Minor = [int]$Matches[2]
if ($Major -ge 3 -and $Minor -ge 8) {
Write-Host "$PythonVersion (符合要求)" -ForegroundColor Green
} else {
Write-Host "$PythonVersion (建议 3.8+)" -ForegroundColor Yellow
$Warnings += "Python 版本较旧"
}
}
} catch {
Write-Host " ✗ Python 未找到" -ForegroundColor Red
$Issues += "需要安装 Python 3.8+"
}
# 检查 pip
Write-Host "`n[2/6] 检查 pip..." -ForegroundColor Yellow
try {
$PipVersion = pip --version 2>&1
Write-Host "$PipVersion" -ForegroundColor Green
} catch {
Write-Host " ✗ pip 未找到" -ForegroundColor Red
$Issues += "需要安装 pip"
}
# 检查 PowerShell 执行策略
Write-Host "`n[3/6] 检查 PowerShell 执行策略..." -ForegroundColor Yellow
$Policy = Get-ExecutionPolicy
if ($Policy -eq "Restricted") {
Write-Host " ✗ 当前策略: $Policy (需要修改)" -ForegroundColor Red
$Issues += "执行策略需要设置为 RemoteSigned: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser"
} else {
Write-Host " ✓ 当前策略: $Policy" -ForegroundColor Green
}
# 检查必要模块
Write-Host "`n[4/6] 检查 Python 模块..." -ForegroundColor Yellow
$Modules = @("nuitka", "pyinstaller")
foreach ($mod in $Modules) {
$result = pip show $mod 2>$null
if ($result) {
$version = ($result | Select-String "Version:").ToString().Split(":")[1].Trim()
Write-Host "$mod $version" -ForegroundColor Green
} else {
Write-Host "$mod 未安装 (将自动安装)" -ForegroundColor Yellow
}
}
# 检查项目依赖
Write-Host "`n[5/6] 检查项目依赖..." -ForegroundColor Yellow
$ReqFile = "$PSScriptRoot\..\requirements\requirements.txt"
if (Test-Path $ReqFile) {
Write-Host " ✓ 找到 requirements.txt" -ForegroundColor Green
# 检查几个关键包
$KeyPackages = @("Panda3D", "PyQt5", "openvr", "imgui-bundle")
foreach ($pkg in $KeyPackages) {
$result = pip show $pkg 2>$null
if ($result) {
Write-Host "$pkg" -ForegroundColor Green
} else {
Write-Host "$pkg 未安装" -ForegroundColor Yellow
}
}
} else {
Write-Host " ✗ 未找到 requirements.txt" -ForegroundColor Red
$Warnings += "未找到依赖文件"
}
# 检查 Inno Setup
Write-Host "`n[6/6] 检查 Inno Setup..." -ForegroundColor Yellow
$ISCCPaths = @(
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
"C:\Program Files\Inno Setup 6\ISCC.exe"
)
$Found = $false
foreach ($path in $ISCCPaths) {
if (Test-Path $path) {
Write-Host " ✓ 找到: $path" -ForegroundColor Green
$Found = $true
break
}
}
if (!$Found) {
Write-Host " ⚠ Inno Setup 未找到 (可选,用于创建安装程序)" -ForegroundColor Yellow
Write-Host " 下载: https://jrsoftware.org/isdl.php" -ForegroundColor Gray
$Warnings += "Inno Setup 未安装 (可选)"
}
# 检查编译器
Write-Host "`n[7/6] 检查 C++ 编译器..." -ForegroundColor Yellow
$CL = Get-Command cl -ErrorAction SilentlyContinue
$GCC = Get-Command gcc -ErrorAction SilentlyContinue
if ($CL) {
Write-Host " ✓ 找到 MSVC (cl)" -ForegroundColor Green
} elseif ($GCC) {
Write-Host " ✓ 找到 GCC" -ForegroundColor Green
} else {
Write-Host " ⚠ 未找到 C++ 编译器" -ForegroundColor Yellow
Write-Host " Nuitka 将尝试自动安装 MinGW64 或使用已安装的 Visual Studio" -ForegroundColor Gray
$Warnings += "未找到 C++ 编译器 (Nuitka 会尝试自动解决)"
}
# 总结
Write-Host "`n========================================" -ForegroundColor Cyan
if ($Issues.Count -eq 0 -and $Warnings.Count -eq 0) {
Write-Host " ✓ 环境检查通过!" -ForegroundColor Green
Write-Host " 可以开始构建" -ForegroundColor Green
exit 0
} else {
if ($Issues.Count -gt 0) {
Write-Host " 发现问题 ($($Issues.Count)):" -ForegroundColor Red
foreach ($issue in $Issues) {
Write-Host " - $issue" -ForegroundColor Red
}
}
if ($Warnings.Count -gt 0) {
Write-Host " 警告 ($($Warnings.Count)):" -ForegroundColor Yellow
foreach ($warn in $Warnings) {
Write-Host " - $warn" -ForegroundColor Yellow
}
}
exit 1
}

137
build_scripts/check_env.sh Normal file
View File

@ -0,0 +1,137 @@
#!/bin/bash
# EG Linux 构建环境检查脚本
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
ISSUES=()
WARNINGS=()
echo "========================================"
echo -e "${BLUE} EG 构建环境检查${NC}"
echo "========================================"
# 检查 Python
echo ""
echo "[1/6] 检查 Python..."
if command -v python3 &> /dev/null; then
VERSION=$(python3 --version 2>&1)
echo -e " ${GREEN}${NC} $VERSION"
else
echo -e " ${RED}${NC} Python3 未找到"
ISSUES+=("需要安装 Python 3.8+")
fi
# 检查 pip
echo ""
echo "[2/6] 检查 pip..."
if command -v pip3 &> /dev/null; then
VERSION=$(pip3 --version 2>&1 | awk '{print $2}')
echo -e " ${GREEN}${NC} pip $VERSION"
else
echo -e " ${RED}${NC} pip3 未找到"
ISSUES+=("需要安装 pip3")
fi
# 检查 GCC
echo ""
echo "[3/6] 检查 GCC..."
if command -v gcc &> /dev/null; then
VERSION=$(gcc --version 2>&1 | head -1)
echo -e " ${GREEN}${NC} $VERSION"
else
echo -e " ${RED}${NC} GCC 未找到"
ISSUES+=("需要安装 GCC: sudo apt-get install build-essential")
fi
# 检查 G++
echo ""
echo "[4/6] 检查 G++..."
if command -v g++ &> /dev/null; then
VERSION=$(g++ --version 2>&1 | head -1)
echo -e " ${GREEN}${NC} $VERSION"
else
echo -e " ${RED}${NC} G++ 未找到"
ISSUES+=("需要安装 G++: sudo apt-get install build-essential")
fi
# 检查 Python 模块
echo ""
echo "[5/6] 检查 Python 模块..."
MODULES=("nuitka" "pyinstaller")
for mod in "${MODULES[@]}"; do
if pip3 show "$mod" &> /dev/null; then
VERSION=$(pip3 show "$mod" 2>/dev/null | grep Version | awk '{print $2}')
echo -e " ${GREEN}${NC} $mod $VERSION"
else
echo -e " ${YELLOW}${NC} $mod 未安装 (将自动安装)"
fi
done
# 检查项目依赖
echo ""
echo "[6/6] 检查项目依赖..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REQ_FILE="$SCRIPT_DIR/../requirements/requirements.txt"
if [ -f "$REQ_FILE" ]; then
echo -e " ${GREEN}${NC} 找到 requirements.txt"
# 检查关键包
KEY_PKGS=("Panda3D" "PyQt5" "openvr" "imgui-bundle")
for pkg in "${KEY_PKGS[@]}"; do
# 转换为小写检查
pkg_lower=$(echo "$pkg" | tr '[:upper:]' '[:lower:]')
if pip3 show "$pkg_lower" &> /dev/null; then
echo -e " ${GREEN}${NC} $pkg"
else
echo -e " ${YELLOW}${NC} $pkg 未安装"
fi
done
else
echo -e " ${YELLOW}${NC} 未找到 requirements.txt"
WARNINGS+=("未找到依赖文件")
fi
# 检查额外工具
echo ""
echo "[7/6] 检查额外工具..."
if command -v wget &> /dev/null; then
echo -e " ${GREEN}${NC} wget"
else
echo -e " ${YELLOW}${NC} wget 未安装 (用于下载 appimagetool)"
WARNINGS+=("建议安装 wget")
fi
# 检查 dpkg (用于构建 deb)
if command -v dpkg-deb &> /dev/null; then
echo -e " ${GREEN}${NC} dpkg-deb (可构建 DEB 包)"
else
echo -e " ${YELLOW}${NC} dpkg-deb 未安装 (无法构建 DEB 包)"
fi
# 总结
echo ""
echo "========================================"
if [ ${#ISSUES[@]} -eq 0 ] && [ ${#WARNINGS[@]} -eq 0 ]; then
echo -e "${GREEN} ✓ 环境检查通过!${NC}"
echo -e "${GREEN} 可以开始构建${NC}"
exit 0
else
if [ ${#ISSUES[@]} -gt 0 ]; then
echo -e "${RED} 发现问题 (${#ISSUES[@]}):${NC}"
for issue in "${ISSUES[@]}"; do
echo -e " ${RED}- $issue${NC}"
done
fi
if [ ${#WARNINGS[@]} -gt 0 ]; then
echo -e "${YELLOW} 警告 (${#WARNINGS[@]}):${NC}"
for warn in "${WARNINGS[@]}"; do
echo -e " ${YELLOW}- $warn${NC}"
done
fi
exit 1
fi

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""
元泰 EG Python 版本检查脚本
确保使用正确的 Python 版本
"""
import sys
def check_python_version():
"""检查 Python 版本是否为 3.11"""
version_info = sys.version_info
version_string = f"{version_info.major}.{version_info.minor}.{version_info.micro}"
print("=" * 50)
print("元泰 EG Python 版本检查")
print("=" * 50)
print(f"当前 Python 版本: {version_string}")
print(f"可执行文件路径: {sys.executable}")
print()
if version_info[:2] == (3, 11):
print("✅ Python 版本正确 (3.11.x)")
print()
print("可以继续安装依赖和构建:")
print(" pip install -r requirements/requirements-minimal.txt")
return 0
else:
print(f"❌ Python 版本错误!")
print(f" 需要: Python 3.11.x")
print(f" 当前: Python {version_string}")
print()
print("请参考 build_scripts/PYTHON_VERSION_REQUIREMENT.md 安装正确版本")
return 1
if __name__ == "__main__":
sys.exit(check_python_version())

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -450,7 +450,8 @@ class EventHandler:
print(f"点击的是碰撞节点: {hitNode.getName()}")
# 碰撞节点的父节点应该是模型
parent = hitNode.getParent()
if parent in self.world.models:
model_list = self.world.models if hasattr(self.world, 'models') else []
if parent in model_list or parent.hasTag('is_model_root'):
selectedModel = parent
print(f"找到对应的模型: {selectedModel.getName()}")
else:
@ -460,13 +461,15 @@ class EventHandler:
current = hitNode
while current != self.world.render:
# 检查是否是模型
if current in self.world.models:
model_list = self.world.models if hasattr(self.world, 'models') else []
if current in model_list or current.hasTag('is_model_root'):
selectedModel = current
print(f"找到模型节点: {selectedModel.getName()}")
break
# 检查是否是模型的子节点
for model in self.world.models:
model_list = self.world.models if hasattr(self.world, 'models') else []
for model in model_list:
if current.getParent() == model or current.isAncestorOf(model):
selectedModel = model
print(f"找到父模型: {selectedModel.getName()}")

225
core/imgui_webview.py Normal file
View File

@ -0,0 +1,225 @@
"""
imgui_webview.py
后台 playwright 无头浏览器 + 截图 Panda3D 纹理 ImGui 面板显示
"""
from __future__ import annotations
import threading
import io
import time
class ImGuiWebView:
"""
后台线程运行 playwright Chromium定期截图并转换为 Panda3D 纹理
ImGui 直接用 imgui.image() 显示纹理鼠标/滚轮事件转发给浏览器
"""
def __init__(self, width: int = 1280, height: int = 720):
self.view_width = width
self.view_height = height
# 截图数据bytes
self._screenshot: bytes | None = None
self._screenshot_lock = threading.Lock()
self.tex_dirty = False # 有新截图待上传 GPU
# 状态
self.current_url = ""
self.title = ""
self.is_loading = False
self.error: str | None = None
# 待处理指令(由 ImGui 线程写,浏览器线程读)
self._cmd_navigate: str | None = None
self._cmd_click: tuple | None = None # (x_ratio, y_ratio)
self._cmd_scroll: float | None = None # pixels
self._cmd_back = False
self._cmd_forward = False
self._cmd_reload = False
self._lock = threading.Lock()
self._running = False
self._thread: threading.Thread | None = None
# ------------------------------------------------------------------ #
# 公开控制 API由 ImGui 线程调用,线程安全)
# ------------------------------------------------------------------ #
def start(self, url: str):
if self._running:
return
self._running = True
self._cmd_navigate = url
self._thread = threading.Thread(target=self._run, daemon=True,
name="imgui-webview")
self._thread.start()
def stop(self):
self._running = False
def navigate(self, url: str):
if not url.startswith(('http://', 'https://', 'file://')):
url = 'https://' + url
with self._lock:
self._cmd_navigate = url
self.is_loading = True
def click(self, x_ratio: float, y_ratio: float):
with self._lock:
self._cmd_click = (x_ratio, y_ratio)
def scroll(self, delta_px: float):
with self._lock:
self._cmd_scroll = delta_px
def go_back(self):
with self._lock:
self._cmd_back = True
def go_forward(self):
with self._lock:
self._cmd_forward = True
def reload(self):
with self._lock:
self._cmd_reload = True
def get_screenshot_bytes(self) -> bytes | None:
with self._screenshot_lock:
return self._screenshot
# ------------------------------------------------------------------ #
# 内部线程
# ------------------------------------------------------------------ #
def _run(self):
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(self._async_run())
except Exception as exc:
self.error = f"WebView 线程异常: {exc}"
import traceback; traceback.print_exc()
finally:
loop.close()
async def _async_run(self):
try:
from playwright.async_api import async_playwright
except ImportError:
self.error = (
"playwright 未安装。\n"
"请运行: pip install playwright\n"
"然后运行: playwright install chromium"
)
self._running = False
return
try:
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
ctx = await browser.new_context(
viewport={"width": self.view_width,
"height": self.view_height},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/121.0.0.0 Safari/537.36"
),
)
page = await ctx.new_page()
# 初次导航
start_url = self._cmd_navigate or "about:blank"
self._cmd_navigate = None
await self._goto(page, start_url)
await self._snap(page)
# 主循环
import asyncio
while self._running:
await asyncio.sleep(0.05)
with self._lock:
nav_url = self._cmd_navigate; self._cmd_navigate = None
clk = self._cmd_click; self._cmd_click = None
scr = self._cmd_scroll; self._cmd_scroll = None
do_back = self._cmd_back; self._cmd_back = False
do_fwd = self._cmd_forward; self._cmd_forward = False
do_reload = self._cmd_reload; self._cmd_reload = False
changed = False
if nav_url:
self.is_loading = True
await self._goto(page, nav_url)
changed = True
self.is_loading = False
if do_back:
await page.go_back()
await asyncio.sleep(0.5)
changed = True
if do_fwd:
await page.go_forward()
await asyncio.sleep(0.5)
changed = True
if do_reload:
await page.reload(wait_until="domcontentloaded")
changed = True
if clk is not None:
xr, yr = clk
x = int(xr * self.view_width)
y = int(yr * self.view_height)
await page.mouse.click(x, y)
await asyncio.sleep(0.4)
changed = True
if scr is not None:
await page.evaluate(
f"window.scrollBy(0, {int(scr)})"
)
await asyncio.sleep(0.15)
changed = True
if changed:
self.current_url = page.url
try:
self.title = await page.title()
except Exception:
pass
await self._snap(page)
await browser.close()
except Exception as exc:
self.error = str(exc)
import traceback; traceback.print_exc()
finally:
self._running = False
async def _goto(self, page, url: str):
import asyncio
try:
await page.goto(url, wait_until="domcontentloaded", timeout=20_000)
self.current_url = page.url
try:
self.title = await page.title()
except Exception:
pass
except Exception as exc:
print(f"[WebView] 导航失败 {url}: {exc}")
async def _snap(self, page):
"""截图并更新 self._screenshot"""
try:
data = await page.screenshot(type="png", full_page=False)
with self._screenshot_lock:
self._screenshot = data
self.tex_dirty = True
except Exception as exc:
print(f"[WebView] 截图失败: {exc}")

File diff suppressed because it is too large Load Diff

View File

@ -1,495 +0,0 @@
from direct.showbase.ShowBaseGlobal import globalClock
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import Point3, Vec3
import math
class PatrolSystem:
"""巡检系统类"""
def __init__(self, world):
"""初始化巡检系统
Args:
world: 核心世界对象引用
"""
self.world = world
# 巡检状态
self.is_patrolling = False
self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...]
self.current_patrol_index = 0
self.patrol_task = None
# 巡检参数
self.patrol_speed = 5.0 # 巡检移动速度(单位/秒)
self.patrol_turn_speed = 90.0 # 转向速度(度/秒)
self.patrol_wait_timer = 0.0
self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back"
# 相机状态保存
self.original_cam_pos = None
self.original_cam_hpr = None
print("✓ 巡检系统初始化完成")
def add_patrol_point(self, position, heading=None, wait_time=3.0):
if heading is None:
if self.patrol_points:
last_pos = self.patrol_points[-1][0]
direction_x = position[0] - last_pos.x
direction_y = position[1] - last_pos.y
direction_z = position[2] - last_pos.z
import math
h=math.degrees(math.atan2(-direction_x,-direction_y))
distance_xy = math.sqrt(direction_x**2+direction_y**2)
p = math.degrees(math.atan2(direction_z,distance_xy))
p = max(-89,min(89,p))
r=0
heading = (h,p,r)
else:
# 使用当前相机朝向
current_hpr = self.world.cam.getHpr()
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
pos = Point3(position[0], position[1], position[2])
hpr = Vec3(heading[0], heading[1], heading[2])
self.patrol_points.append((pos, hpr, wait_time))
print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}")
# 在 PatrolSystem 类中添加以下方法
def add_auto_heading_patrol_point(self, position, wait_time=3.0):
"""添加自动计算朝向的巡检点(朝向路径前进方向)
Args:
position: 相机位置 (x, y, z)
wait_time: 在该点停留时间
"""
heading = None # 将自动计算朝向
# 复用原有的 add_patrol_point 方法
self.add_patrol_point(position, heading, wait_time)
def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0):
"""添加朝向指定位置的巡检点
Args:
position: 相机位置 (x, y, z)
look_at_position: 相机朝向的目标位置 (x, y, z)
wait_time: 在该点停留时间
"""
import math
# 计算从当前位置到目标位置的方向向量
direction_x = look_at_position[0] - position[0]
direction_y = look_at_position[1] - position[1]
direction_z = look_at_position[2] - position[2]
# 计算HPR朝向
h = math.degrees(math.atan2(-direction_x, -direction_y))
distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2)
p = math.degrees(math.atan2(direction_z, distance_xy))
p = max(-89, min(89, p)) # 限制pitch角度在合理范围内
r = 0 # roll通常为0
heading = (h, p, r)
self.add_patrol_point(position, heading, wait_time)
def clear_patrol_points(self):
"""清空所有巡检点"""
self.patrol_points = []
print("✓ 巡检点已清空")
def set_patrol_speed(self, move_speed, turn_speed=None):
"""设置巡检速度
Args:
move_speed: 移动速度单位/
turn_speed: 转向速度/如果为None则保持当前值
"""
self.patrol_speed = move_speed
if turn_speed is not None:
self.patrol_turn_speed = turn_speed
print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}")
def start_patrol(self):
"""开始巡检"""
if not self.patrol_points:
print("✗ 没有设置巡检点,无法开始巡检")
return False
if self.is_patrolling:
print("⚠ 巡检已在进行中")
return True
# 保存当前相机状态
self.original_cam_pos = Point3(self.world.cam.getPos())
self.original_cam_hpr = Vec3(self.world.cam.getHpr())
# 重置巡检状态
self.current_patrol_index = 0
self.patrol_state = "moving"
self.patrol_wait_timer = 0.0
self.is_patrolling = True
# 启动巡检任务
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点")
return True
def stop_patrol(self):
"""停止巡检"""
if not self.is_patrolling:
print("⚠ 巡检未在进行中")
return False
# 停止巡检任务
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = None
self.is_patrolling = False
self.patrol_state = "moving"
self.patrol_wait_timer = 0.0
print("✓ 巡检已停止")
return True
def pause_patrol(self):
"""暂停巡检"""
if not self.is_patrolling:
print("⚠ 巡检未在进行中")
return False
if self.patrol_task:
taskMgr.remove(self.patrol_task)
self.patrol_task = None
print("✓ 巡检已暂停")
return True
def resume_patrol(self):
"""恢复巡检"""
if self.is_patrolling:
print("⚠ 巡检已在进行中")
return False
if not self.patrol_points:
print("✗ 没有设置巡检点")
return False
self.is_patrolling = True
self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task")
print("✓ 巡检已恢复")
return True
def reset_to_original_position(self):
"""重置相机到原始位置"""
if self.original_cam_pos and self.original_cam_hpr:
self.world.cam.setPos(self.original_cam_pos)
self.world.cam.setHpr(self.original_cam_hpr)
print("✓ 相机已重置到原始位置")
return True
else:
print("✗ 没有保存的原始位置")
return False
def _patrol_task(self, task):
"""巡检主任务"""
try:
if not self.is_patrolling or not self.patrol_points:
return task.done
# 获取当前巡检点
current_point = self.patrol_points[self.current_patrol_index]
target_pos, target_hpr, wait_time = current_point
# 根据当前状态执行不同操作
if self.patrol_state == "moving":
self._handle_moving_state(target_pos)
elif self.patrol_state == "turning_to_target":
self._handle_turning_to_target_state(target_hpr)
elif self.patrol_state == "waiting":
self._handle_waiting_state(wait_time)
elif self.patrol_state == "turning_back":
self._handle_turning_back_state()
return task.cont
except Exception as e:
print(f"巡检任务出错: {e}")
import traceback
traceback.print_exc()
return task.done
def _handle_moving_state(self, target_pos):
"""处理移动状态"""
current_pos = self.world.cam.getPos()
distance = (target_pos - current_pos).length()
if distance < 0.1: # 到达目标点
print(f"✓ 到达巡检点 {self.current_patrol_index + 1}")
self.patrol_state = "turning_to_target"
return
# 计算移动方向和距离
direction = target_pos - current_pos
direction.normalize()
# 计算目标朝向(看向目标点)
target_hpr = self._look_at_to_hpr(direction)
current_hpr = self.world.cam.getHpr()
# 平滑转向到目标朝向
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
# 计算本帧应转动的角度
dt = globalClock.getDt()
turn_amount = self.patrol_turn_speed * dt
# 逐步转向目标角度
new_hpr = Vec3(current_hpr)
if abs(h_diff) > turn_amount:
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
else:
new_hpr.x = target_hpr.x
if abs(p_diff) > turn_amount:
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
else:
new_hpr.y = target_hpr.y
if abs(r_diff) > turn_amount:
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
else:
new_hpr.z = target_hpr.z
self.world.cam.setHpr(new_hpr)
# 计算本帧应移动的距离
move_distance = self.patrol_speed * dt
# 如果移动距离大于剩余距离,则直接移动到目标点
if move_distance >= distance:
self.world.cam.setPos(target_pos)
else:
# 否则按方向移动
new_pos = current_pos + direction * move_distance
self.world.cam.setPos(new_pos)
def _handle_turning_to_target_state(self, target_hpr):
"""处理转向目标状态"""
# 检查是否需要朝向下一个点
if target_hpr == "look_next":
# 计算朝向下一个点的方向
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
next_point_pos = self.patrol_points[next_index][0]
current_pos = self.world.cam.getPos()
direction = next_point_pos - current_pos
direction.normalize()
# 计算目标朝向
target_hpr = self._look_at_to_hpr(direction)
current_hpr = self.world.cam.getHpr()
# 计算角度差
h_diff = self._normalize_angle(target_hpr.x - current_hpr.x)
p_diff = self._normalize_angle(target_hpr.y - current_hpr.y)
r_diff = self._normalize_angle(target_hpr.z - current_hpr.z)
# 检查是否已完成转向
if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0:
print(f"✓ 完成转向,开始停留")
self.patrol_state = "waiting"
self.patrol_wait_timer = 0.0
return
# 计算本帧应转动的角度
dt = globalClock.getDt()
turn_amount = self.patrol_turn_speed * dt
# 逐步转向目标角度
new_hpr = Vec3(current_hpr)
if abs(h_diff) > turn_amount:
new_hpr.x += turn_amount if h_diff > 0 else -turn_amount
else:
new_hpr.x = target_hpr.x
if abs(p_diff) > turn_amount:
new_hpr.y += turn_amount if p_diff > 0 else -turn_amount
else:
new_hpr.y = target_hpr.y
if abs(r_diff) > turn_amount:
new_hpr.z += turn_amount if r_diff > 0 else -turn_amount
else:
new_hpr.z = target_hpr.z
self.world.cam.setHpr(new_hpr)
def _handle_waiting_state(self, wait_time):
"""处理等待状态"""
self.patrol_wait_timer += globalClock.getDt()
if self.patrol_wait_timer >= wait_time:
print(f"✓ 停留结束,准备转回原朝向")
self.patrol_state = "turning_back"
# 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法
def _handle_turning_back_state(self):
"""处理转回原朝向状态"""
# 直接完成转向状态,进入移动状态
print(f"✓ 停留结束,开始移动到下一个点")
# 移动到下一个巡检点
next_index = (self.current_patrol_index + 1) % len(self.patrol_points)
self.current_patrol_index = next_index
self.patrol_state = "moving"
return
def _normalize_angle(self, angle):
"""规范化角度到-180到180度之间"""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def _look_at_to_hpr(self, direction):
"""将方向向量转换为HPR角度"""
# 简化的转换,实际应用中可能需要更精确的计算
h = math.degrees(math.atan2(-direction.x, -direction.y))
p = math.degrees(math.asin(direction.z))
return Vec3(h, p, 0)
def get_patrol_status(self):
"""获取巡检状态信息"""
return {
"is_patrolling": self.is_patrolling,
"current_point": self.current_patrol_index,
"total_points": len(self.patrol_points),
"state": self.patrol_state,
"wait_timer": self.patrol_wait_timer
}
def list_patrol_points(self):
"""列出所有巡检点"""
if not self.patrol_points:
print("没有设置巡检点")
return
print(f"巡检点列表 (共{len(self.patrol_points)}个):")
for i, (pos, hpr, wait_time) in enumerate(self.patrol_points):
current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else ""
print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) "
f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) "
f"停留:{wait_time}{current_marker}")
def remove_patrol_point(self, index):
"""移除指定索引的巡检点"""
if 0 <= index < len(self.patrol_points):
removed_point = self.patrol_points.pop(index)
print(
f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})")
# 调整当前索引
if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points:
self.current_patrol_index = len(self.patrol_points) - 1
elif self.current_patrol_index >= len(self.patrol_points):
self.current_patrol_index = 0
else:
print(f"✗ 无效的巡检点索引: {index}")
def insert_patrol_point(self, index, position, heading=None, wait_time=3.0):
"""在指定位置插入巡检点"""
if index < 0 or index > len(self.patrol_points):
print(f"✗ 无效的插入位置: {index}")
return
if heading is None:
# 使用当前相机朝向
current_hpr = self.world.cam.getHpr()
heading = (current_hpr.x, current_hpr.y, current_hpr.z)
pos = Point3(position[0], position[1], position[2])
hpr = Vec3(heading[0], heading[1], heading[2])
self.patrol_points.insert(index, (pos, hpr, wait_time))
print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}")
def update_patrol_point(self, index, position=None, heading=None, wait_time=None):
"""更新指定巡检点的信息"""
if 0 <= index < len(self.patrol_points):
pos, hpr, wt = self.patrol_points[index]
if position is not None:
pos = Point3(position[0], position[1], position[2])
if heading is not None:
hpr = Vec3(heading[0], heading[1], heading[2])
if wait_time is not None:
wt = wait_time
self.patrol_points[index] = (pos, hpr, wt)
print(f"✓ 更新巡检点 {index + 1}")
else:
print(f"✗ 无效的巡检点索引: {index}")
def goto_patrol_point(self, index):
"""直接跳转到指定巡检点"""
if not self.patrol_points:
print("✗ 没有设置巡检点")
return False
if 0 <= index < len(self.patrol_points):
pos, hpr, _ = self.patrol_points[index]
self.world.cam.setPos(pos)
self.world.cam.setHpr(hpr)
self.current_patrol_index = index
print(f"✓ 跳转到巡检点 {index + 1}")
return True
else:
print(f"✗ 无效的巡检点索引: {index}")
return False
def cleanup(self):
"""清理巡检系统资源"""
self.stop_patrol()
self.clear_patrol_points()
self.original_cam_pos = None
self.original_cam_hpr = None
print("✓ 巡检系统资源已清理")
# 使用示例和便捷函数
def create_default_patrol_route(patrol_system):
"""创建默认的巡检路线(示例)"""
# 清空现有巡检点
patrol_system.clear_patrol_points()
# 添加一些示例巡检点
patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1前方低位置
patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2中央高位置
patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3右侧位置
patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4左侧位置
print("✓ 默认巡检路线已创建")

View File

@ -389,6 +389,12 @@ class ResourceManager:
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
if src.suffix.lower() == '.fbx':
fbm_src = src.with_name(src.stem + '.fbm')
if fbm_src.exists() and fbm_src.is_dir():
fbm_dst = destination_root / fbm_src.name
if not fbm_dst.exists():
shutil.copytree(fbm_src, fbm_dst)
imported.append(dst)
except Exception as e:
errors.append(f"导入失败 {src}: {e}")

View File

@ -14,6 +14,7 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta
TransparencyAttrib, Vec4, CollisionCapsule)
from direct.task.TaskManagerGlobal import taskMgr
import math
from core.selection_outline import SelectionOutlineManager
class SelectionSystem:
"""选择和变换系统类"""
@ -30,6 +31,17 @@ class SelectionSystem:
self.selectedNode = None
self.selectionBox = None # 选择框
self.selectionBoxTarget = None # 选择框跟踪的目标节点
self.show_selection_box = False
self.enable_unity_outline = True
self.outline_manager = getattr(self.world, "_selection_outline_manager", None)
if not self.outline_manager:
self.outline_manager = SelectionOutlineManager(
self.world,
enabled=self.enable_unity_outline,
)
setattr(self.world, "_selection_outline_manager", self.outline_manager)
else:
self.outline_manager.set_enabled(self.enable_unity_outline)
# 坐标轴工具Gizmo相关
self.gizmo = None # 坐标轴
@ -207,6 +219,21 @@ class SelectionSystem:
import traceback
traceback.print_exc()
def _updateSelectionOutline(self, nodePath):
"""Update Unity-like selection outline visuals."""
try:
if not getattr(self, "outline_manager", None):
return
if not self.enable_unity_outline:
self.outline_manager.clear()
return
if nodePath and not nodePath.isEmpty():
self.outline_manager.set_targets([nodePath])
else:
self.outline_manager.clear()
except Exception as e:
print(f"update selection outline failed: {e}")
def updateSelectionBoxGeometry(self):
"""更新选择框的几何形状和位置"""
try:
@ -1963,10 +1990,30 @@ class SelectionSystem:
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
self.world.command_manager.execute_command(command)
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
except Exception as e:
print(f"创建撤销命令时出错: {e}")
# 同步碰撞体
try:
target = self.gizmoTarget
if target:
# 寻找它的所属模型根节点
root_model = target
while root_model and root_model != self.world.render:
model_list = self.world.models if hasattr(self.world, 'models') else []
if root_model in model_list or root_model.hasTag('is_model_root'):
break
root_model = root_model.getParent()
# 如果这个节点属于某个模型,或者是模型自己,更新该模型的碰撞边界
if root_model and hasattr(self.world, 'models') and root_model in self.world.models:
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'refreshCollisionBounds'):
self.world.scene_manager.refreshCollisionBounds(root_model)
except Exception as e:
print(f"同步模型碰撞体失败: {e}")
# 恢复所有轴的颜色
for axis_name in ["x", "y", "z"]:
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
@ -2045,8 +2092,12 @@ class SelectionSystem:
# 创建选择框
#print("创建选择框...")
self.createSelectionBox(nodePath)
if self.selectionBox:
if self.show_selection_box:
self.createSelectionBox(nodePath)
else:
self.clearSelectionBox()
if (not self.show_selection_box) or self.selectionBox:
box_name = "Unknown"
if self.selectionBox and not self.selectionBox.isEmpty():
box_name = self.selectionBox.getName()
@ -2056,6 +2107,7 @@ class SelectionSystem:
# 创建坐标轴
#print("创建坐标轴...")
self._updateSelectionOutline(nodePath)
self.createGizmo(nodePath)
if self.gizmo:
gizmo_name = "Unknown"
@ -2068,6 +2120,7 @@ class SelectionSystem:
else:
print("清除选择...")
self.clearSelectionBox()
self._updateSelectionOutline(None)
self.clearGizmo()
print("✓ 取消选择")
@ -2145,6 +2198,40 @@ class SelectionSystem:
"""获取当前选中的节点"""
return self.selectedNode
def deleteSelectedNode(self):
"""兼容旧接口:删除当前选中节点。"""
node = self.selectedNode
if not node or node.isEmpty():
return False
try:
if node.getName() == "render":
return False
except Exception:
return False
self._deleting_node = True
try:
# 优先走应用层统一删除链路(命令系统/SSBO清理/消息等)。
if hasattr(self.world, "_delete_node") and callable(self.world._delete_node):
deleted = bool(self.world._delete_node(node))
else:
deleted = False
scene_manager = getattr(self.world, "scene_manager", None)
if scene_manager and hasattr(scene_manager, "models") and node in scene_manager.models:
scene_manager.models.remove(node)
try:
node.removeNode()
deleted = True
except Exception:
deleted = False
if deleted:
self.clearSelection()
return deleted
finally:
self._deleting_node = False
def hasSelection(self):
"""检查是否有选中的节点"""
return self.selectedNode is not None
@ -2156,6 +2243,9 @@ class SelectionSystem:
if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()):
self.clearSelectionBox()
if self.selectedNode and self.selectedNode.isEmpty():
self._updateSelectionOutline(None)
def setupGizmoCollision(self):
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
return
@ -2875,6 +2965,7 @@ class SelectionSystem:
# 清理其他资源
self.clearSelectionBox()
self._updateSelectionOutline(None)
self.clearGizmo()
def clearSelection(self):
"""清除当前选择"""
@ -2882,6 +2973,7 @@ class SelectionSystem:
self.selectedNode = None
self.selectedObject = None
self.clearSelectionBox()
self._updateSelectionOutline(None)
self.clearGizmo()
# 清除树形控件中的选择

303
core/selection_outline.py Normal file
View File

@ -0,0 +1,303 @@
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import (
BitMask32,
Camera,
FrameBufferProperties,
GraphicsOutput,
GraphicsPipe,
NodePath,
Shader,
Texture,
Vec4,
WindowProperties,
)
class SelectionOutlineManager:
"""Selection mask manager feeding RenderPipeline SelectionOutlineStage."""
OUTLINE_PREFIX = "selectionOutline"
def __init__(
self,
app,
enabled=True,
outline_color=Vec4(1.0, 0.55, 0.0, 1.0),
outline_width_px=2.0,
fill_alpha=0.0,
max_targets=128,
):
self.app = app
self.enabled = bool(enabled)
self.outline_color = Vec4(outline_color)
self.outline_width_px = max(0.0, float(outline_width_px))
self.fill_alpha = max(0.0, min(1.0, float(fill_alpha)))
self.max_targets = max(1, int(max_targets))
self._task_name = "selection_outline_mask_sync"
self._tracked = [] # [(source_np, clone_np), ...]
self._stage_missing_warned = False
self._mask_root = NodePath(f"{self.OUTLINE_PREFIX}_mask_root")
self._mask_buffer = None
self._mask_texture = None
self._mask_cam = None
self._mask_cam_np = None
self._mask_shader = self._build_mask_shader()
self._buffer_size = (0, 0)
@staticmethod
def _is_empty(np):
if not np:
return True
if hasattr(np, "isEmpty"):
return np.isEmpty()
if hasattr(np, "is_empty"):
return np.is_empty()
return False
@classmethod
def is_outline_node(cls, node_path):
if not node_path or cls._is_empty(node_path):
return False
name = node_path.getName() if hasattr(node_path, "getName") else ""
if name.startswith(cls.OUTLINE_PREFIX):
return True
try:
if node_path.hasPythonTag("selection_outline"):
return True
except Exception:
pass
return False
def set_enabled(self, enabled):
self.enabled = bool(enabled)
if not self.enabled:
self.clear()
self._apply_stage_inputs()
def set_targets(self, targets):
if not self.enabled:
self.clear()
self._apply_stage_inputs()
return
self._ensure_mask_resources()
self.clear()
if not targets:
self._apply_stage_inputs()
return
seen = set()
valid = []
for target in targets:
if self._is_empty(target) or self.is_outline_node(target):
continue
key = str(target)
if key in seen:
continue
seen.add(key)
valid.append(target)
if len(valid) >= self.max_targets:
break
for source in valid:
self._clone_target(source)
if self._tracked:
self._start_sync_task()
self._sync_once()
print(f"[SelectionOutline] targets={len(self._tracked)} active")
else:
print("[SelectionOutline] no valid targets for outline")
self._apply_stage_inputs()
def clear(self):
self._stop_sync_task()
for _, clone_np in self._tracked:
if not self._is_empty(clone_np):
clone_np.removeNode()
self._tracked = []
self._apply_stage_inputs()
def cleanup(self):
self.clear()
self._destroy_mask_resources()
def _build_mask_shader(self):
return Shader.make(
Shader.SL_GLSL,
"""
#version 430
in vec4 p3d_Vertex;
uniform mat4 p3d_ModelViewProjectionMatrix;
void main() {
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
}
""",
"""
#version 430
out vec4 result;
void main() {
result = vec4(1.0, 1.0, 1.0, 1.0);
}
""",
)
def _clone_target(self, source):
try:
clone_np = source.copyTo(self._mask_root)
if self._is_empty(clone_np):
return
if not self._node_has_geom(clone_np):
clone_np.removeNode()
return
clone_np.setName(f"{self.OUTLINE_PREFIX}_{source.getName()}")
clone_np.setPythonTag("selection_outline", True)
clone_np.setCollideMask(BitMask32.allOff())
clone_np.setMat(self.app.render, source.getMat(self.app.render))
self._tracked.append((source, clone_np))
except Exception as exc:
print(f"[SelectionOutline] clone failed: {exc}")
def _node_has_geom(self, np):
if self._is_empty(np):
return False
try:
node = np.node()
if node and hasattr(node, "isGeomNode") and node.isGeomNode():
return True
except Exception:
pass
try:
return not np.find("**/+GeomNode").isEmpty()
except Exception:
return False
def _start_sync_task(self):
taskMgr.remove(self._task_name)
taskMgr.add(self._sync_task, self._task_name)
def _stop_sync_task(self):
taskMgr.remove(self._task_name)
def _sync_task(self, task):
self._sync_once()
return task.cont
def _sync_once(self):
if not self.enabled:
self._apply_stage_inputs()
return
self._ensure_mask_resources()
alive = []
for source, clone_np in self._tracked:
if self._is_empty(source) or self._is_empty(clone_np):
if not self._is_empty(clone_np):
clone_np.removeNode()
continue
clone_np.setMat(self.app.render, source.getMat(self.app.render))
alive.append((source, clone_np))
self._tracked = alive
self._apply_stage_inputs()
def _get_stage(self):
rp = getattr(self.app, "render_pipeline", None)
if not rp or not getattr(rp, "stage_mgr", None):
return None
return rp.stage_mgr.get_stage("SelectionOutlineStage")
def _apply_stage_inputs(self):
stage = self._get_stage()
if not stage:
if not self._stage_missing_warned:
print("[SelectionOutline] SelectionOutlineStage not found; plugin may be disabled.")
self._stage_missing_warned = True
return
self._stage_missing_warned = False
stage.set_outline_style(
color=self.outline_color,
width_px=self.outline_width_px,
fill_alpha=self.fill_alpha,
)
stage.set_mask_texture(self._mask_texture)
stage.set_enabled_outline(self.enabled and bool(self._tracked))
def _get_window_size(self):
if not getattr(self.app, "win", None):
return 1, 1
return max(1, self.app.win.getXSize()), max(1, self.app.win.getYSize())
def _ensure_mask_resources(self):
size = self._get_window_size()
if size != self._buffer_size:
self._destroy_mask_resources()
self._buffer_size = size
if self._mask_buffer:
return
if not getattr(self.app, "graphicsEngine", None) or not getattr(self.app, "win", None):
return
w, h = self._buffer_size
win_props = WindowProperties()
win_props.setSize(w, h)
fb_props = FrameBufferProperties()
fb_props.setRgbaBits(8, 8, 8, 8)
fb_props.setDepthBits(24)
self._mask_buffer = self.app.graphicsEngine.make_output(
self.app.pipe,
"selection_outline_mask",
-80,
fb_props,
win_props,
GraphicsPipe.BFRefuseWindow,
self.app.win.getGsg(),
self.app.win,
)
if not self._mask_buffer:
print("[SelectionOutline] failed to create mask buffer")
return
self._mask_texture = Texture("selection_outline_mask_tex")
self._mask_texture.setMinfilter(Texture.FTNearest)
self._mask_texture.setMagfilter(Texture.FTNearest)
self._mask_buffer.addRenderTexture(self._mask_texture, GraphicsOutput.RTMBindOrCopy)
self._mask_buffer.setClearColor(Vec4(0, 0, 0, 0))
self._mask_buffer.setClearColorActive(True)
self._mask_buffer.setActive(True)
self._mask_cam = Camera("selection_outline_mask_camera")
self._mask_cam.setScene(self._mask_root)
self._mask_cam.setLens(self.app.camLens)
self._mask_cam_np = self.app.cam.attachNewNode(self._mask_cam)
dr = self._mask_buffer.makeDisplayRegion()
dr.setCamera(self._mask_cam_np)
state_np = NodePath("selection_outline_mask_state")
state_np.setShader(self._mask_shader, 10000)
state_np.setLightOff(1)
state_np.setMaterialOff(1)
state_np.setTextureOff(1)
state_np.setColorOff(1)
self._mask_cam.setInitialState(state_np.getState())
def _destroy_mask_resources(self):
if self._mask_cam_np and not self._is_empty(self._mask_cam_np):
self._mask_cam_np.removeNode()
self._mask_cam_np = None
self._mask_cam = None
if self._mask_buffer and getattr(self.app, "graphicsEngine", None):
try:
self.app.graphicsEngine.removeWindow(self._mask_buffer)
except Exception:
pass
self._mask_buffer = None
self._mask_texture = None

View File

@ -14,8 +14,9 @@ from direct.showbase.ShowBase import ShowBase
from direct.showbase.ShowBaseGlobal import globalClock
from scene.scene_manager import SceneManager
# 设置 RenderPipelineFile 路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 设置 RenderPipelineFile 路径。打包后可通过环境变量显式指定项目根目录,
# 避免 AppImage / 独立发行目录的布局差异导致资源定位错误。
project_root = os.environ.get("EG_PROJECT_ROOT") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
render_pipeline_path = os.path.join(project_root, "RenderPipelineFile")
sys.path.insert(0, render_pipeline_path)
@ -65,8 +66,16 @@ class CoreWorld(ShowBase):
if is_fullscreen:
loadPrcFileData("", "fullscreen #t")
# 创建渲染管线
# 创建渲染管线。打包后 Nuitka 会把 rpcore/rplibs 平铺到发行目录,
# 自动探测到的 base_path 可能变成 dist 根目录,导致找不到
# RenderPipelineFile/config/pipeline.yaml这里显式指回真实管线目录。
self.render_pipeline = RenderPipeline()
rp_base_dir = os.path.join(project_root, "RenderPipelineFile")
rp_config_dir = os.path.join(rp_base_dir, "config")
if os.path.isdir(rp_base_dir):
self.render_pipeline.mount_mgr.base_path = rp_base_dir
if os.path.isdir(rp_config_dir):
self.render_pipeline.mount_mgr.config_dir = rp_config_dir
self.render_pipeline.pre_showbase_init()
# 强制开启多线程支持 (Video播放必需)

File diff suppressed because it is too large Load Diff

BIN
icons/app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

BIN
icons/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

222
imgui.ini
View File

@ -1,222 +0,0 @@
[Window][Debug##Default]
Pos=28,465
Size=400,400
Collapsed=0
[Window][Explore: render]
Pos=60,60
Size=410,761
Collapsed=0
[Window][Node Placer "camera"]
Pos=486,83
Size=472,216
Collapsed=0
[Window][Time Slider "pandaPace"]
Pos=-69,89
Size=745,76
Collapsed=0
[Window][Dear ImGui Demo]
Pos=241,149
Size=832,45
Collapsed=0
[Window][工具栏]
Pos=241,20
Size=908,74
Collapsed=0
DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
Size=239,468
Collapsed=0
DockId=0x00000007,0
[Window][属性面板]
Pos=1151,20
Size=229,730
Collapsed=0
DockId=0x00000003,0
[Window][控制台]
Pos=0,490
Size=239,260
Collapsed=0
DockId=0x00000008,0
[Window][脚本管理]
Pos=1540,20
Size=380,390
Collapsed=0
DockId=0x00000003,1
[Window][中文显示测试]
Pos=60,60
Size=135,263
Collapsed=0
[Window][WindowOverViewport_11111111]
Pos=0,20
Size=1380,730
Collapsed=0
[Window][测试窗口1]
Pos=60,60
Size=121,82
Collapsed=0
[Window][测试窗口2]
Pos=60,60
Size=121,82
Collapsed=0
[Window][测试窗口3]
Pos=60,60
Size=93,65
Collapsed=0
[Window][新建项目]
Pos=760,354
Size=400,300
Collapsed=0
[Window][选择路径]
Pos=660,254
Size=600,500
Collapsed=0
[Window][打开项目]
Pos=710,304
Size=500,400
Collapsed=0
[Window][导入模型]
Pos=660,254
Size=600,500
Collapsed=0
[Window][资源管理器]
Pos=241,464
Size=908,286
Collapsed=0
DockId=0x00000006,0
[Window][创建3D文本]
Pos=60,60
Size=88,202
Collapsed=0
[Window][创建GUI按钮]
Pos=60,60
Size=93,226
Collapsed=0
[Window][创建3D图片]
Pos=60,60
Size=88,226
Collapsed=0
[Window][创建点光源]
Pos=60,60
Size=109,274
Collapsed=0
[Window][创建GUI标签]
Pos=60,60
Size=93,226
Collapsed=0
[Window][创建聚光灯]
Pos=60,60
Size=89,250
Collapsed=0
[Window][颜色选择器]
Pos=810,304
Size=300,400
Collapsed=0
[Window][选择diffuse纹理文件##texture_dialog]
Pos=660,304
Size=600,400
Collapsed=0
[Window][创建GUI图片]
Pos=60,60
Size=101,226
Collapsed=0
[Window][LUI编辑器]
Pos=1628,412
Size=292,597
Collapsed=0
DockId=0x00000004,0
[Window][LUI测试控制面板]
Pos=6,10
Size=300,200
Collapsed=1
[Window][选择高度图文件]
Pos=60,60
Size=596,498
Collapsed=0
[Window][创建平面地形]
Pos=61,60
Size=238,176
Collapsed=0
[Window][选择normal纹理文件##texture_dialog]
Pos=660,304
Size=600,400
Collapsed=0
[Window][选择metallic纹理文件##texture_dialog]
Pos=660,304
Size=600,400
Collapsed=0
[Window][选择roughness纹理文件##texture_dialog]
Pos=660,304
Size=600,400
Collapsed=0
[Window][VR状态]
Pos=162,152
Size=138,296
Collapsed=0
[Window][VR设置]
Pos=474,130
Size=120,384
Collapsed=0
[Window][选择emission纹理文件##texture_dialog]
Pos=660,304
Size=600,400
Collapsed=0
[Window][Web面板]
Pos=373,98
Size=942,580
Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1380,730 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1689,989 Split=X
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=239,989 Split=Y Selected=0xE0015051
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1448,989 Split=Y
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,74 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,913 Split=Y
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,625 CentralNode=1
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,286 Selected=0x3A2E05C3
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=229,989 Split=Y Selected=0x3188AB8D
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7

1178
main.py

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import json
import datetime
import subprocess
import shutil
from project.webgl_packager import WebGLPackager
class ProjectManager:
@ -11,6 +12,7 @@ class ProjectManager:
self.world = world
self.current_project_path = None
self.project_config = None
self.last_webgl_export_report = None
print("✓ 项目管理系统初始化完成")
@ -300,6 +302,52 @@ class ProjectManager:
return False
# ==================== 项目打包功能 ====================
def buildWebGLPackage(self, output_dir):
"""将当前项目打包为 WebGL 静态站点目录。
Args:
output_dir (str): 用户选择的输出根目录
Returns:
bool: success/partial 返回 Truefailed 返回 False
"""
try:
if not self.current_project_path:
print("错误: 请先创建或打开一个项目!")
return False
if not output_dir:
print("错误: 请指定 WebGL 打包输出目录!")
return False
project_path = os.path.normpath(self.current_project_path)
output_dir = os.path.normpath(output_dir)
packager = WebGLPackager(self.world)
report = packager.package(project_path, output_dir)
self.last_webgl_export_report = report
status = report.get("status", "failed")
report_path = os.path.join(
report.get("output_dir", ""),
"reports",
"export_report.json",
)
if status in ("success", "partial"):
print(f"WebGL打包完成: {status}")
print(f"输出目录: {report.get('output_dir', '')}")
print(f"报告路径: {report_path}")
return True
print("WebGL打包失败")
print(f"报告路径: {report_path}")
return False
except Exception as e:
print(f"WebGL打包过程出错: {str(e)}")
return False
def buildPackage(self, build_dir):
"""打包项目为可执行文件 - 按照Panda3D官方标准方法
@ -478,7 +526,6 @@ class ProjectManager:
def _copyScriptSystemToBuild(self,build_dir):
core_files = [
"script_system.py",
"InfoPanelManager.py",
"CustomMouseController.py"
]
@ -840,4 +887,4 @@ setup(
except Exception as e:
print(f"执行打包命令失败: {str(e)}")
return False
return False

995
project/webgl_packager.py Normal file
View File

@ -0,0 +1,995 @@
"""WebGL project packager for EG editor (Three.js static scene export)."""
from __future__ import annotations
import datetime
import json
import os
import re
import shutil
import stat
import subprocess
import tempfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
class WebGLPackager:
"""Export current EG scene into a static WebGL package directory."""
BASIS_MATRIX_ROW_MAJOR = [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
-1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
]
ENERGY_TO_INTENSITY_SCALE = 0.001
def __init__(self, world):
self.world = world
self.scene_manager = getattr(world, "scene_manager", None)
self._project_path = ""
self._output_root = ""
self._assets_model_dir = ""
self._assets_texture_dir = ""
self._copied_source_to_uri: Dict[str, str] = {}
self._name_counter: Dict[str, int] = {}
self._node_id_by_pointer: Dict[int, str] = {}
self.report: Dict[str, Any] = {
"status": "failed",
"warnings": [],
"missing_assets": [],
"unsupported_assets": [],
"converted_assets": [],
"copied_assets": [],
"output_dir": "",
}
def package(self, project_path: str, output_dir: str) -> Dict[str, Any]:
"""Export project as WebGL static site and return export report."""
self._project_path = os.path.normpath(project_path)
project_name = os.path.basename(self._project_path.rstrip(os.sep)) or "project"
self._output_root = os.path.normpath(os.path.join(output_dir, f"{project_name}_webgl"))
self._assets_model_dir = os.path.join(self._output_root, "assets", "models")
self._assets_texture_dir = os.path.join(self._output_root, "assets", "textures")
self.report["output_dir"] = self._output_root
try:
if not os.path.isdir(self._project_path):
self._fail(f"项目路径不存在: {self._project_path}")
return self.report
self._prepare_output_dir()
self._copy_templates()
scene_manifest = self._build_scene_manifest(project_name)
self._write_json(
os.path.join(self._output_root, "scene", "scene_webgl.json"),
scene_manifest,
)
self._write_preview_scripts()
status = "success"
if self.report["missing_assets"] or self.report["unsupported_assets"]:
status = "partial"
self.report["status"] = status
except Exception as exc:
self._fail(f"WebGL打包失败: {exc}")
finally:
self._write_json(
os.path.join(self._output_root, "reports", "export_report.json"),
self.report,
)
return self.report
def _prepare_output_dir(self) -> None:
if os.path.isdir(self._output_root):
shutil.rmtree(self._output_root)
os.makedirs(os.path.join(self._output_root, "js"), exist_ok=True)
os.makedirs(os.path.join(self._output_root, "vendor"), exist_ok=True)
os.makedirs(self._assets_model_dir, exist_ok=True)
os.makedirs(self._assets_texture_dir, exist_ok=True)
os.makedirs(os.path.join(self._output_root, "scene"), exist_ok=True)
os.makedirs(os.path.join(self._output_root, "reports"), exist_ok=True)
def _copy_templates(self) -> None:
repo_root = Path(__file__).resolve().parent.parent
template_root = repo_root / "templates" / "webgl"
if not template_root.exists():
raise FileNotFoundError(f"模板目录不存在: {template_root}")
file_mapping = {
"index.html": "index.html",
"style.css": "style.css",
"viewer.js": os.path.join("js", "viewer.js"),
}
for src_name, dst_rel in file_mapping.items():
src = template_root / src_name
dst = Path(self._output_root) / dst_rel
if not src.exists():
raise FileNotFoundError(f"模板文件不存在: {src}")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(src), str(dst))
vendor_src = template_root / "vendor"
vendor_dst = Path(self._output_root) / "vendor"
if vendor_src.exists():
for entry in vendor_src.iterdir():
if entry.is_file():
shutil.copy2(str(entry), str(vendor_dst / entry.name))
self._try_resolve_vendor_files(vendor_dst)
# Placeholder marker warning
placeholder_file = vendor_dst / "three.module.min.js"
if placeholder_file.exists():
content = placeholder_file.read_text(encoding="utf-8", errors="ignore")
if "EG_VENDOR_PLACEHOLDER" in content:
self.report["warnings"].append(
"当前 vendor 为占位文件,请替换为官方 three.module.min.js / OrbitControls.js / GLTFLoader.js 后再预览。"
)
def _try_resolve_vendor_files(self, vendor_dst: Path) -> None:
"""Try to replace template placeholders with system-installed Three.js modules."""
lookup_roots = [
Path("/usr/share/javascript/three"),
Path("/usr/share/nodejs/three"),
Path("/usr/lib/node_modules/three"),
Path.home() / ".local/lib/node_modules/three",
]
targets = {
"three.module.min.js": [
"build/three.module.min.js",
"build/three.module.js",
],
"OrbitControls.js": [
"examples/jsm/controls/OrbitControls.js",
],
"GLTFLoader.js": [
"examples/jsm/loaders/GLTFLoader.js",
],
}
for dst_name, rel_candidates in targets.items():
dst_path = vendor_dst / dst_name
if not dst_path.exists():
continue
try:
content = dst_path.read_text(encoding="utf-8", errors="ignore")
except Exception:
content = ""
# Only replace placeholders.
if "EG_VENDOR_PLACEHOLDER" not in content:
continue
found_source = None
for root in lookup_roots:
if not root.exists():
continue
for rel in rel_candidates:
candidate = root / rel
if candidate.exists() and candidate.is_file():
found_source = candidate
break
if found_source:
break
if found_source:
shutil.copy2(str(found_source), str(dst_path))
def _build_scene_manifest(self, project_name: str) -> Dict[str, Any]:
render = getattr(self.world, "render", None)
if not render:
raise RuntimeError("world.render 不可用")
export_nodes: List[Any] = []
model_nodes = self._collect_model_nodes()
spot_nodes = self._collect_valid_nodes(getattr(self.scene_manager, "Spotlight", []))
point_nodes = self._collect_valid_nodes(getattr(self.scene_manager, "Pointlight", []))
ground_node = self._get_default_ground_node()
export_nodes.extend(model_nodes)
export_nodes.extend(spot_nodes)
export_nodes.extend(point_nodes)
if ground_node is not None:
export_nodes.append(ground_node)
# Unique by pointer
uniq: Dict[int, Any] = {}
for node in export_nodes:
uniq[id(node)] = node
export_nodes = list(uniq.values())
for index, node in enumerate(export_nodes, start=1):
self._node_id_by_pointer[id(node)] = f"node_{index:04d}"
nodes_json: List[Dict[str, Any]] = []
for node in model_nodes:
entry = self._build_model_node_entry(node)
if entry:
nodes_json.append(entry)
for node in point_nodes:
entry = self._build_light_node_entry(node, kind="point_light")
if entry:
nodes_json.append(entry)
for node in spot_nodes:
entry = self._build_light_node_entry(node, kind="spot_light")
if entry:
nodes_json.append(entry)
if ground_node is not None:
entry = self._build_ground_node_entry(ground_node)
if entry:
nodes_json.append(entry)
manifest = {
"meta": {
"format_version": "1.0",
"project_name": project_name,
"exported_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
},
"coordinate": {
"source": "panda3d_zup",
"target": "threejs_yup",
"matrix_convention": "panda_row_vector_row_major",
"basis_matrix": self.BASIS_MATRIX_ROW_MAJOR,
},
"camera": self._build_camera_entry(),
"environment": self._build_environment_entry(),
"nodes": nodes_json,
}
return manifest
def _collect_model_nodes(self) -> List[Any]:
if not self.scene_manager:
return []
all_models = self._collect_valid_nodes(getattr(self.scene_manager, "models", []))
light_ptrs = {id(n) for n in self._collect_valid_nodes(getattr(self.scene_manager, "Spotlight", []))}
light_ptrs |= {id(n) for n in self._collect_valid_nodes(getattr(self.scene_manager, "Pointlight", []))}
models: List[Any] = []
for node in all_models:
if id(node) in light_ptrs:
continue
if node.hasTag("light_type"):
continue
if node.getName() in {"render", "camera", "cam"}:
continue
models.append(node)
return models
@staticmethod
def _collect_valid_nodes(nodes: List[Any]) -> List[Any]:
valid = []
for node in nodes or []:
if not node:
continue
try:
if node.isEmpty():
continue
except Exception:
continue
valid.append(node)
return valid
def _get_default_ground_node(self):
ground = getattr(self.world, "ground", None)
if not ground:
return None
try:
if ground.isEmpty():
return None
return ground
except Exception:
return None
def _build_camera_entry(self) -> Dict[str, Any]:
render = getattr(self.world, "render", None)
cam = getattr(self.world, "cam", None) or getattr(self.world, "camera", None)
default = {
"matrix_local_row_major": [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
-50.0,
20.0,
1.0,
],
"fov_deg": 80.0,
"near": 0.1,
"far": 10000.0,
}
if not cam or not render:
return default
try:
cam_mat = cam.getMat(render)
fov = 80.0
near = 0.1
far = 10000.0
lens = cam.node().getLens() if cam.node() else None
if lens:
try:
fov = float(lens.getFov()[0])
except Exception:
pass
try:
near = float(lens.getNear())
except Exception:
pass
try:
far = float(lens.getFar())
except Exception:
pass
return {
"matrix_local_row_major": self._mat4_to_row_major_list(cam_mat),
"fov_deg": fov,
"near": near,
"far": far,
}
except Exception:
return default
def _build_environment_entry(self) -> Dict[str, Any]:
ambient = getattr(self.world, "ambient_light", None)
directional = getattr(self.world, "directional_light", None)
ambient_color = [0.2, 0.2, 0.2]
directional_color = [0.8, 0.8, 0.8]
directional_dir = [0.0, 0.0, -1.0]
if ambient and not ambient.isEmpty():
try:
c = ambient.node().getColor()
ambient_color = [float(c[0]), float(c[1]), float(c[2])]
except Exception:
pass
if directional and not directional.isEmpty():
try:
c = directional.node().getColor()
directional_color = [float(c[0]), float(c[1]), float(c[2])]
except Exception:
pass
try:
q = directional.getQuat(getattr(self.world, "render", directional.getParent()))
fwd = q.getForward()
directional_dir = [float(fwd[0]), float(fwd[1]), float(fwd[2])]
except Exception:
pass
return {
"include_default_ground": True,
"ambient_light": {
"color": ambient_color,
"intensity": 1.0,
},
"directional_light": {
"color": directional_color,
"intensity": 1.0,
"direction": directional_dir,
},
}
def _build_model_node_entry(self, node) -> Optional[Dict[str, Any]]:
node_id = self._node_id_by_pointer.get(id(node))
if not node_id:
return None
parent_id, mat = self._get_parent_and_matrix(node)
node_name = node.getName() or node_id
model_source, source_tag = self._resolve_model_source(node)
if not model_source:
self.report["missing_assets"].append(
{
"node": node_name,
"reason": "model_path_not_found",
"tags_checked": ["model_path", "saved_model_path", "original_path", "file"],
}
)
return None
model_uri = self._prepare_model_asset(model_source, node_name)
if not model_uri:
self.report["unsupported_assets"].append(
{
"node": node_name,
"source": model_source,
"reason": "model_conversion_failed",
}
)
return None
material_override = self._extract_material_override(node)
textures = self._collect_and_copy_texture_overrides(node, model_source)
entry: Dict[str, Any] = {
"id": node_id,
"name": node_name,
"kind": "model",
"parent_id": parent_id,
"matrix_local_row_major": self._mat4_to_row_major_list(mat),
"model": {"uri": model_uri},
"material_override": material_override,
"source_model_tag": source_tag,
}
if textures:
entry["texture_overrides"] = textures
return entry
def _build_light_node_entry(self, node, kind: str) -> Optional[Dict[str, Any]]:
node_id = self._node_id_by_pointer.get(id(node))
if not node_id:
return None
parent_id, mat = self._get_parent_and_matrix(node)
node_name = node.getName() or node_id
light_obj = node.getPythonTag("rp_light_object") if node.hasPythonTag("rp_light_object") else None
color = [1.0, 1.0, 1.0]
energy = self._safe_float(node.getTag("light_energy"), 5000.0) if node.hasTag("light_energy") else 5000.0
radius = self._safe_float(node.getTag("light_radius"), 30.0) if node.hasTag("light_radius") else 30.0
spot_fov = self._safe_float(node.getTag("light_fov"), 45.0) if node.hasTag("light_fov") else 45.0
inner_ratio = 0.4
if light_obj is not None:
try:
c = getattr(light_obj, "color", None)
if c is not None:
color = [float(c[0]), float(c[1]), float(c[2])]
except Exception:
pass
try:
energy = float(getattr(light_obj, "energy", energy))
except Exception:
pass
try:
radius = float(getattr(light_obj, "radius", radius))
except Exception:
pass
try:
if hasattr(light_obj, "fov"):
spot_fov = float(getattr(light_obj, "fov", spot_fov))
except Exception:
pass
try:
if hasattr(light_obj, "inner_radius"):
inner_ratio = float(getattr(light_obj, "inner_radius", inner_ratio))
except Exception:
pass
intensity = max(0.0, energy * self.ENERGY_TO_INTENSITY_SCALE)
return {
"id": node_id,
"name": node_name,
"kind": kind,
"parent_id": parent_id,
"matrix_local_row_major": self._mat4_to_row_major_list(mat),
"light": {
"color": color,
"intensity": intensity,
"range": max(0.0, radius),
"spot_angle_deg": max(1.0, spot_fov),
"inner_cone_ratio": max(0.0, min(1.0, inner_ratio)),
},
}
def _build_ground_node_entry(self, node) -> Optional[Dict[str, Any]]:
node_id = self._node_id_by_pointer.get(id(node))
if not node_id:
return None
parent_id, mat = self._get_parent_and_matrix(node)
node_name = node.getName() or "ground"
color = [0.8, 0.8, 0.8, 1.0]
roughness = 1.0
metallic = 0.0
material = None
try:
material = node.getMaterial()
except Exception:
material = None
if material:
try:
if material.hasBaseColor():
c = material.getBaseColor()
color = [float(c[0]), float(c[1]), float(c[2]), float(c[3])]
except Exception:
pass
try:
roughness = float(material.getRoughness())
except Exception:
pass
try:
metallic = float(material.getMetallic())
except Exception:
pass
return {
"id": node_id,
"name": node_name,
"kind": "ground",
"parent_id": parent_id,
"matrix_local_row_major": self._mat4_to_row_major_list(mat),
"ground": {
"width": 100.0,
"height": 100.0,
},
"material_override": {
"base_color": [color[0], color[1], color[2], 1.0],
"roughness": roughness,
"metallic": metallic,
"opacity": 1.0,
},
}
def _resolve_model_source(self, node) -> Tuple[Optional[str], str]:
tags = ["model_path", "saved_model_path", "original_path", "file"]
for tag in tags:
if not node.hasTag(tag):
continue
value = (node.getTag(tag) or "").strip()
if not value:
continue
resolved = self._resolve_asset_path(value)
if resolved:
return resolved, tag
return None, ""
def _resolve_asset_path(self, candidate: str) -> Optional[str]:
candidate = (candidate or "").strip()
if not candidate:
return None
if candidate.startswith(("http://", "https://")):
return None
# Absolute path: use directly.
if os.path.isabs(candidate):
abs_candidate = os.path.normpath(candidate)
if os.path.exists(abs_candidate):
return abs_candidate
return None
# Relative path resolution policy.
search_roots = [
self._project_path,
os.path.join(self._project_path, "models"),
os.path.join(self._project_path, "Resources"),
os.path.join(self._project_path, "scenes", "resources"),
os.getcwd(),
]
for root in search_roots:
full = os.path.normpath(os.path.join(root, candidate))
if os.path.exists(full):
return full
return None
def _prepare_model_asset(self, source_path: str, node_name: str) -> Optional[str]:
source_path = os.path.normpath(source_path)
ext = os.path.splitext(source_path)[1].lower()
if ext in {".gltf", ".glb"}:
return self._copy_asset_to_models(source_path)
with tempfile.TemporaryDirectory(prefix="eg_webgl_conv_") as temp_dir:
conversion_source = source_path
if ext == ".bam":
temp_egg = os.path.join(temp_dir, self._sanitize_filename(Path(source_path).stem) + ".egg")
if not self._run_tool_command(["bam2egg", source_path, temp_egg], timeout=120):
return None
temp_obj = os.path.join(temp_dir, self._sanitize_filename(Path(source_path).stem) + ".obj")
if not self._run_tool_command(["egg2obj", temp_egg, temp_obj], timeout=120):
return None
conversion_source = temp_obj
elif ext == ".egg":
temp_obj = os.path.join(temp_dir, self._sanitize_filename(Path(source_path).stem) + ".obj")
if not self._run_tool_command(["egg2obj", source_path, temp_obj], timeout=120):
return None
conversion_source = temp_obj
target_filename = self._unique_filename(node_name or Path(source_path).stem, ".glb")
target_abs = os.path.join(self._assets_model_dir, target_filename)
converter = self._convert_to_glb(conversion_source, target_abs)
if not converter:
return None
self.report["converted_assets"].append(
{
"source": source_path,
"converted_from": conversion_source,
"target": os.path.relpath(target_abs, self._output_root).replace("\\", "/"),
"converter": converter,
}
)
return os.path.relpath(target_abs, self._output_root).replace("\\", "/")
def _convert_to_glb(self, source_path: str, target_path: str) -> str:
scene_manager = self.scene_manager
if scene_manager is None:
return ""
conversion_order = [
"_convertWithBlender",
"_convertWithFBX2glTF",
"_convertWithAssimp",
]
for method_name in conversion_order:
method = getattr(scene_manager, method_name, None)
if not callable(method):
continue
try:
ok = method(source_path, target_path, None)
except TypeError:
ok = method(source_path, target_path)
except Exception:
ok = False
if ok and os.path.exists(target_path):
return method_name
return ""
def _run_tool_command(self, args: List[str], timeout: int) -> bool:
executable = shutil.which(args[0])
if not executable:
self.report["warnings"].append(f"缺少转换工具: {args[0]}")
return False
try:
result = subprocess.run(
args,
check=False,
capture_output=True,
text=True,
timeout=timeout,
)
except Exception as exc:
self.report["warnings"].append(f"执行命令失败: {' '.join(args)} ({exc})")
return False
if result.returncode != 0:
stderr = (result.stderr or "").strip()
stdout = (result.stdout or "").strip()
detail = stderr or stdout or f"exit={result.returncode}"
self.report["warnings"].append(f"命令失败: {' '.join(args)} -> {detail}")
return False
return True
def _copy_asset_to_models(self, source_path: str) -> str:
source_path = os.path.normpath(source_path)
if source_path in self._copied_source_to_uri:
return self._copied_source_to_uri[source_path]
ext = os.path.splitext(source_path)[1].lower() or ".bin"
safe_name = self._unique_filename(Path(source_path).stem, ext)
target_abs = os.path.join(self._assets_model_dir, safe_name)
shutil.copy2(source_path, target_abs)
rel_uri = os.path.relpath(target_abs, self._output_root).replace("\\", "/")
self._copied_source_to_uri[source_path] = rel_uri
self.report["copied_assets"].append(
{
"source": source_path,
"target": rel_uri,
"type": "model",
}
)
return rel_uri
def _collect_and_copy_texture_overrides(self, node, model_source: str) -> List[Dict[str, Any]]:
textures: List[Dict[str, Any]] = []
texture_pairs = self._extract_texture_stage_and_paths(node)
if not texture_pairs:
return textures
model_dir = os.path.dirname(model_source)
for stage_name, tex_path in texture_pairs:
abs_path = self._resolve_texture_path(tex_path, model_dir)
if not abs_path:
continue
rel_uri = self._copy_asset_to_textures(abs_path)
if rel_uri:
textures.append({"stage": stage_name, "uri": rel_uri})
return textures
def _extract_texture_stage_and_paths(self, node) -> List[Tuple[str, str]]:
pairs: List[Tuple[str, str]] = []
seen: set = set()
nodes_to_scan = [node]
try:
geom_paths = node.findAllMatches("**/+GeomNode")
for i in range(geom_paths.getNumPaths()):
nodes_to_scan.append(geom_paths.getPath(i))
except Exception:
pass
for np in nodes_to_scan:
try:
stages = np.findAllTextureStages()
except Exception:
continue
try:
stage_count = stages.getNumTextureStages()
except Exception:
stage_count = 0
for idx in range(stage_count):
try:
stage = stages.getTextureStage(idx)
texture = np.getTexture(stage)
except Exception:
continue
if not texture:
continue
tex_path = ""
try:
if texture.hasFullpath():
tex_path = texture.getFullpath().toOsSpecific()
except Exception:
tex_path = ""
if not tex_path:
continue
stage_name = stage.getName() if stage else f"stage_{idx}"
key = (stage_name, tex_path)
if key in seen:
continue
seen.add(key)
pairs.append(key)
return pairs
def _resolve_texture_path(self, path_hint: str, model_dir: str) -> Optional[str]:
path_hint = (path_hint or "").strip()
if not path_hint:
return None
if os.path.isabs(path_hint) and os.path.exists(path_hint):
return os.path.normpath(path_hint)
search_roots = [
model_dir,
self._project_path,
os.path.join(self._project_path, "scenes", "resources"),
os.getcwd(),
]
for root in search_roots:
full = os.path.normpath(os.path.join(root, path_hint))
if os.path.exists(full):
return full
return None
def _copy_asset_to_textures(self, source_path: str) -> Optional[str]:
source_path = os.path.normpath(source_path)
cache_key = f"texture::{source_path}"
if cache_key in self._copied_source_to_uri:
return self._copied_source_to_uri[cache_key]
ext = os.path.splitext(source_path)[1].lower() or ".png"
safe_name = self._unique_filename(Path(source_path).stem, ext)
target_abs = os.path.join(self._assets_texture_dir, safe_name)
try:
shutil.copy2(source_path, target_abs)
except Exception:
return None
rel_uri = os.path.relpath(target_abs, self._output_root).replace("\\", "/")
self._copied_source_to_uri[cache_key] = rel_uri
self.report["copied_assets"].append(
{
"source": source_path,
"target": rel_uri,
"type": "texture",
}
)
return rel_uri
def _extract_material_override(self, node) -> Dict[str, Any]:
base_color = [1.0, 1.0, 1.0, 1.0]
roughness = 0.5
metallic = 0.0
material = None
try:
material = node.getMaterial()
except Exception:
material = None
if material:
try:
if material.hasBaseColor():
c = material.getBaseColor()
base_color = [float(c[0]), float(c[1]), float(c[2]), float(c[3])]
except Exception:
pass
try:
roughness = float(material.getRoughness())
except Exception:
pass
try:
metallic = float(material.getMetallic())
except Exception:
pass
else:
try:
c = node.getColor()
base_color = [float(c[0]), float(c[1]), float(c[2]), float(c[3])]
except Exception:
pass
opacity = max(0.0, min(1.0, float(base_color[3])))
if opacity >= 0.999:
opacity = 1.0
elif opacity <= 0.001:
opacity = 0.0
return {
"base_color": base_color,
"roughness": roughness,
"metallic": metallic,
"opacity": opacity,
}
def _get_parent_and_matrix(self, node) -> Tuple[Optional[str], Any]:
render = getattr(self.world, "render", None)
parent_id = None
try:
parent = node.getParent()
except Exception:
parent = None
if parent and not parent.isEmpty() and id(parent) in self._node_id_by_pointer:
parent_id = self._node_id_by_pointer[id(parent)]
try:
return parent_id, node.getMat(parent)
except Exception:
pass
if render:
try:
return None, node.getMat(render)
except Exception:
pass
try:
return None, node.getMat()
except Exception:
return None, node.getTransform().getMat()
@staticmethod
def _mat4_to_row_major_list(mat4_obj) -> List[float]:
try:
values = []
for r in range(4):
for c in range(4):
values.append(float(mat4_obj.getCell(r, c)))
return values
except Exception:
return [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
]
@staticmethod
def _safe_float(value: Any, default: float) -> float:
try:
return float(value)
except Exception:
return float(default)
def _unique_filename(self, stem: str, suffix: str) -> str:
safe_stem = self._sanitize_filename(stem) or "asset"
key = f"{safe_stem}{suffix.lower()}"
index = self._name_counter.get(key, 0)
self._name_counter[key] = index + 1
if index == 0:
return f"{safe_stem}{suffix}"
return f"{safe_stem}_{index:03d}{suffix}"
@staticmethod
def _sanitize_filename(name: str) -> str:
normalized = re.sub(r"[^A-Za-z0-9._-]+", "_", str(name or "")).strip("._")
return normalized or "asset"
def _write_preview_scripts(self) -> None:
sh_path = os.path.join(self._output_root, "run_preview.sh")
bat_path = os.path.join(self._output_root, "run_preview.bat")
sh_content = "#!/usr/bin/env bash\npython3 -m http.server 8000\n"
bat_content = "@echo off\npython -m http.server 8000\n"
with open(sh_path, "w", encoding="utf-8") as f:
f.write(sh_content)
with open(bat_path, "w", encoding="utf-8") as f:
f.write(bat_content)
current_mode = os.stat(sh_path).st_mode
os.chmod(sh_path, current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
@staticmethod
def _write_json(path: str, payload: Dict[str, Any]) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
def _fail(self, message: str) -> None:
self.report["status"] = "failed"
self.report["warnings"].append(message)

View File

@ -0,0 +1,30 @@
# 元泰 EG 最小依赖配置 (Python 3.11)
# 注意: 必须使用 Python 3.11,其他版本不保证兼容性
# Python 版本要求
# python_version == "3.11"
# 核心 3D 引擎
Panda3D==1.10.16
panda3d-frame==22.10.post1
panda3d-imgui==1.1.0
# ImGui 绑定
imgui-bundle==1.92.4
# VR 支持
openvr>=2.5.101
# 图像处理
Pillow==11.3.0
# YAML 配置解析
PyYAML==5.4.1
# 其他工具
psutil==5.9.0
# 注意: 已移除以下未使用的依赖
# - PyQt5 (未使用,节省 ~50MB)
# - PySide6 (未使用,节省 ~100MB)
# - PyQt5-Qt5, PyQt5_sip, PySide6_Addons, PySide6_Essentials

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,516 @@
"""Scene manager conversion and tileset operations."""
import os
import shutil
import time
import json
import aiohttp
import asyncio
import inspect
from pathlib import Path
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib
)
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
from scene import util
class SceneManagerConvertTilesMixin:
def _get_tree_widget(self):
"""安全获取树形控件"""
try:
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'treeWidget')):
return self.world.interface_manager.treeWidget
except AttributeError:
pass
return None
def _shouldConvertToGLB(self, filepath):
"""判断是否应该转换为GLB格式"""
ext = os.path.splitext(filepath)[1].lower()
# 需要转换的格式FBX, OBJ, DAE等但不转换已经是GLB/GLTF的
convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend']
return ext in convert_formats
def _convertToGLBWithProgress(self, filepath):
"""带进度显示的GLB转换"""
try:
from PyQt5.QtWidgets import QProgressDialog, QApplication
from PyQt5.QtCore import Qt
# 创建进度对话框
progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100)
progress.setWindowTitle("模型格式转换")
progress.setWindowModality(Qt.WindowModal)
progress.show()
QApplication.processEvents()
try:
result = self._convertToGLB(filepath, progress)
progress.hide()
return result
except Exception as e:
progress.hide()
print(f"转换过程出错: {e}")
return None
except ImportError:
# 如果没有 PyQt5直接转换
return self._convertToGLB(filepath)
def _convertToGLB(self, filepath, progress=None):
"""将模型文件转换为GLB格式"""
try:
print(f"[GLB转换] 开始转换: {filepath}")
if progress:
progress.setValue(10)
progress.setLabelText("准备转换...")
from PyQt5.QtWidgets import QApplication
QApplication.processEvents()
# 准备输出路径
base_name = os.path.splitext(os.path.basename(filepath))[0]
output_dir = os.path.dirname(filepath)
glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb")
# 如果已经存在转换后的文件,直接使用
if os.path.exists(glb_path):
# 检查文件时间,如果原文件更新则重新转换
original_time = os.path.getmtime(filepath)
converted_time = os.path.getmtime(glb_path)
if converted_time > original_time:
print(f"[GLB转换] 使用现有转换文件: {glb_path}")
if progress:
progress.setValue(100)
return glb_path
if progress:
progress.setValue(20)
progress.setLabelText("尝试 Blender 转换...")
QApplication.processEvents()
# 方法1: 使用 Blender 进行转换
if self._convertWithBlender(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(60)
progress.setLabelText("尝试 FBX2glTF 转换...")
QApplication.processEvents()
# 方法2: 使用 FBX2glTF (如果可用)
if self._convertWithFBX2glTF(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(80)
progress.setLabelText("尝试 Assimp 转换...")
QApplication.processEvents()
# 方法3: 使用 Assimp
if self._convertWithAssimp(filepath, glb_path, progress):
return glb_path
#print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换")
return None
except Exception as e:
print(f"[GLB转换] 转换过程出错: {e}")
return None
def _convertWithBlender(self, input_path, output_path, progress=None):
"""使用 Blender 进行转换"""
try:
import subprocess
import tempfile
print(f"[Blender转换] {input_path}{output_path}")
# 创建 Blender 脚本
script_content = f'''
import bpy
import sys
import os
# 清理默认场景
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
print("开始导入文件...")
# 根据文件类型选择导入方法
input_file = "{input_path}"
output_file = "{output_path}"
try:
ext = os.path.splitext(input_file)[1].lower()
if ext == '.fbx':
bpy.ops.import_scene.fbx(filepath=input_file)
elif ext == '.obj':
bpy.ops.import_scene.obj(filepath=input_file)
elif ext == '.dae':
bpy.ops.wm.collada_import(filepath=input_file)
elif ext == '.blend':
bpy.ops.wm.open_mainfile(filepath=input_file)
else:
print(f"不支持的格式: {{ext}}")
sys.exit(1)
print("导入成功开始导出GLB...")
# 导出为 GLB保留动画
bpy.ops.export_scene.gltf(
filepath=output_file,
export_format='GLB',
export_animations=True,
export_force_sampling=True,
export_frame_range=True,
export_current_frame=False,
export_skins=True,
export_morph=True,
export_lights=True,
export_cameras=False
)
print("GLB导出成功")
except Exception as e:
print(f"转换失败: {{e}}")
sys.exit(1)
'''
# 写入临时脚本文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
temp_file.write(script_content)
script_path = temp_file.name
try:
# 执行 Blender 转换
result = subprocess.run([
'blender', '--background', '--python', script_path
], capture_output=True, text=True, timeout=180)
# 清理临时文件
os.unlink(script_path)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[Blender转换] 转换成功")
return True
else:
print(f"[Blender转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[Blender转换] 转换超时")
return False
except FileNotFoundError:
print(f"[Blender转换] Blender 未安装")
return False
except Exception as e:
print(f"[Blender转换] 转换过程出错: {e}")
return False
def _convertWithFBX2glTF(self, input_path, output_path, progress=None):
"""使用 FBX2glTF 进行转换仅支持FBX"""
try:
import subprocess
if not input_path.lower().endswith('.fbx'):
return False
print(f"[FBX2glTF转换] {input_path}{output_path}")
# 使用 FBX2glTF 转换
result = subprocess.run([
'FBX2glTF', input_path, '--output', output_path, '--binary'
], capture_output=True, text=True, timeout=120)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[FBX2glTF转换] 转换成功")
return True
else:
print(f"[FBX2glTF转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[FBX2glTF转换] 转换超时")
return False
except FileNotFoundError:
print(f"[FBX2glTF转换] FBX2glTF 未安装")
return False
except Exception as e:
print(f"[FBX2glTF转换] 转换过程出错: {e}")
return False
def _convertWithAssimp(self, input_path, output_path, progress=None):
"""使用 PyAssimp 进行转换"""
try:
import pyassimp
print(f"[PyAssimp转换] {input_path}{output_path}")
# 加载模型
scene = pyassimp.load(input_path)
if not scene:
print(f"[PyAssimp转换] 加载模型失败")
return False
if progress:
progress.setValue(30)
# 导出为GLB格式
pyassimp.export(scene, output_path, "glb2")
if progress:
progress.setValue(80)
# 释放资源
pyassimp.release(scene)
if os.path.exists(output_path):
print(f"[PyAssimp转换] 转换成功")
return True
else:
print(f"[PyAssimp转换] 转换失败: 输出文件未生成")
return False
except ImportError:
print(f"[PyAssimp转换] PyAssimp 未安装")
return False
except Exception as e:
print(f"[PyAssimp转换] 转换过程出错: {e}")
return False
def _check_async_task(self, panda3d_task):
# 检查 asyncio 任务是否完成
if hasattr(self, '_current_asyncio_task'):
if self._current_asyncio_task.done():
try:
self._current_asyncio_task.result()
except Exception as e:
print(f"异步任务出错:{e}")
# 返回 Panda3D 任务管理器的完成状态
return panda3d_task.done # 注意是 done 而不是 DONE
# 返回 Panda3D 任务管理器的继续状态
return panda3d_task.cont # 注意是 cont 而不是 CONTINUE
def _parse_tileset(self,tileset_data,tileset_info):
try:
root = tileset_data.get('root',{})
self._parse_tile(root,tileset_info['node'],tileset_info)
print("✓ Tileset 解析完成")
except Exception as e:
print(f"✗ Tileset 解析出错: {e}")
def _parse_tile(self, tile_data, parent_node, tileset_info):
try:
# 获取tileID
tile_id = f"tile_{len(tileset_info['tiles'])}"
print(f"创建tile节点: {tile_id}")
# 创建tile节点
tile_node = parent_node.attachNewNode(tile_id)
tileset_info['tiles'][tile_id] = {
'node': tile_node,
'data': tile_data,
'loaded': False
}
# 如果有内容,创建占位几何体
if 'content' in tile_data:
print(f"为tile {tile_id} 创建几何体")
self._create_tile_geometry(tile_node)
# 递归解析子tiles
children = tile_data.get('children', [])
print(f"Tile {tile_id}{len(children)} 个子节点")
for child_data in children:
self._parse_tile(child_data, tile_node, tileset_info)
except Exception as e:
print(f"✗ Tile 解析出错: {e}")
import traceback
traceback.print_exc()
def _create_tile_geometry(self,parent_node):
"""为 tile 创建占位几何体"""
try:
# 创建一个简单的立方体作为占位符
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('tile_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
vertices = [
(-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5)
]
for vert in vertices:
vertex.addData3f(*vert)
normal.addData3f(0, 0, 1)
color.addData4f(0.2, 0.6, 0.8, 1.0)
# 创建几何体
geom = Geom(vdata)
# 创建面
prim = GeomTriangles(Geom.UHStatic)
# 底面
prim.addVertices(0, 1, 2)
prim.addVertices(0, 2, 3)
# 顶面
prim.addVertices(4, 7, 6)
prim.addVertices(4, 6, 5)
# 前面
prim.addVertices(0, 4, 5)
prim.addVertices(0, 5, 1)
# 后面
prim.addVertices(2, 6, 7)
prim.addVertices(2, 7, 3)
# 左面
prim.addVertices(0, 3, 7)
prim.addVertices(0, 7, 4)
# 右面
prim.addVertices(1, 5, 6)
prim.addVertices(1, 6, 2)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tile_geometry')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(1000) # 放大以便观察
# 添加材质
material = Material()
material.setBaseColor((0.2, 0.6, 0.8, 1.0))
material.setSpecular((0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
cube_node.setMaterial(material)
except Exception as e:
print(f"✗ 创建 tile 几何体出错: {e}")
def _create_placeholder_geometry(self, parent_node):
"""创建一个简单的占位符几何体,让用户能看到节点"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
# 创建简单的立方体作为占位符
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
size = 1.0
vertices = [
# 前面 (Z+)
(-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size),
# 后面 (Z-)
(-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size),
# 左面 (X-)
(-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size),
# 右面 (X+)
(size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size),
# 上面 (Y+)
(-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size),
# 下面 (Y-)
(-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size)
]
normals = [
# 前面法线
(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
# 后面法线
(0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
# 左面法线
(-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0),
# 右面法线
(1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0),
# 上面法线
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0),
# 下面法线
(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0)
]
# 青色
face_colors = [
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色
(0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色
(0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色
(0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色
(0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色
]
for i, vert in enumerate(vertices):
vertex.addData3f(*vert)
normal.addData3f(*normals[i])
color.addData4f(*face_colors[i])
# 创建几何体
geom = Geom(vdata)
# 创建面(每个面两个三角形)
prim = GeomTriangles(Geom.UHStatic)
# 每个面4个顶点生成2个三角形
for face in range(6): # 6个面
base_index = face * 4
# 第一个三角形
prim.addVertices(base_index, base_index + 1, base_index + 2)
# 第二个三角形
prim.addVertices(base_index, base_index + 2, base_index + 3)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tileset_placeholder')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(5) # 设置合适的大小
# 设置双面渲染
cube_node.setTwoSided(True)
# 添加材质
material = Material()
material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
cube_node.setMaterial(material)
# 添加标识标签
cube_node.setTag("element_type", "cesium_placeholder")
print("✓ 占位符几何体创建完成")
return cube_node
except Exception as e:
print(f"✗ 创建占位符几何体出错: {e}")
import traceback
traceback.print_exc()
return None

View File

@ -0,0 +1,19 @@
"""Scene manager implementation composed from focused mixins."""
from scene.scene_manager_convert_tiles_mixin import SceneManagerConvertTilesMixin
from scene.scene_manager_io_mixin import SceneManagerIOMixin
from scene.scene_manager_light_mixin import SceneManagerLightMixin
from scene.scene_manager_model_mixin import SceneManagerModelMixin
from scene.scene_manager_serialization_mixin import SceneManagerSerializationMixin
class SceneManagerImpl(
SceneManagerSerializationMixin,
SceneManagerConvertTilesMixin,
SceneManagerLightMixin,
SceneManagerIOMixin,
SceneManagerModelMixin,
):
"""Composed scene manager implementation."""
pass

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,455 @@
"""Scene manager light operations."""
import os
import shutil
import time
import json
import aiohttp
import asyncio
import inspect
from pathlib import Path
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib
)
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
from scene import util
class SceneManagerLightMixin:
def _recreateSpotLight(self, light_node):
"""重新创建聚光灯"""
try:
from RenderPipelineFile.rpcore import SpotLight
from panda3d.core import Vec3
from core.render_pipeline_utils import get_render_pipeline
# 创建聚光灯对象
light = SpotLight()
light.direction = Vec3(0, 0, -1)
light.fov = 70
light.set_color_from_temperature(5 * 1000.0)
# 恢复保存的属性
if light_node.hasTag("light_energy"):
light.energy = float(light_node.getTag("light_energy"))
else:
light.energy = 5000
light.radius = 1000
light.casts_shadows = True
light.shadow_map_resolution = 256
light_pos = light_node.getPos()
light.setPos(light_pos)
# 添加到渲染管线
render_pipeline = get_render_pipeline()
render_pipeline.add_light(light)
# 保存光源对象引用
light_node.setPythonTag("rp_light_object", light)
# 添加到管理列表(去重)
if light_node not in self.Spotlight:
self.Spotlight.append(light_node)
# 确保灯光节点有正确的标签,以便在场景树更新时被识别
if not light_node.hasTag("is_scene_element"):
light_node.setTag("is_scene_element", "1")
light_node.setTag("is_scene_element", "1")
light_node.setTag("element_type", "spotlight")
light_node.setTag("tree_item_type", "LIGHT_NODE")
if light_node.hasTag("stored_energy"):
stored_energy = float(light_node.getTag("stored_energy"))
if stored_energy > 0:
light_node.setTag("stored_energy", str(stored_energy))
user_visible = True
if light_node.hasTag("user_visible"):
user_visible = light_node.getTag("user_visible").lower() == "true"
light_node.setPythonTag("user_visible",user_visible)
if not user_visible:
self.toggleLightVisibility(light_node,False)
except Exception as e:
print(f"重新创建聚光灯失败: {str(e)}")
import traceback
traceback.print_exc()
def _recreatePointLight(self, light_node):
"""重新创建点光源"""
try:
from RenderPipelineFile.rpcore import PointLight
from core.render_pipeline_utils import get_render_pipeline
# 创建点光源对象
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())
# 添加到渲染管线
render_pipeline = get_render_pipeline()
render_pipeline.add_light(light)
# 保存光源对象引用
light_node.setPythonTag("rp_light_object", light)
# 添加到管理列表(去重)
if light_node not in self.Pointlight:
self.Pointlight.append(light_node)
# 确保灯光节点有正确的标签,以便在场景树更新时被识别
if not light_node.hasTag("is_scene_element"):
light_node.setTag("is_scene_element", "1")
light_node.setTag("is_scene_element", "1")
light_node.setTag("element_type", "pointlight")
light_node.setTag("tree_item_type", "LIGHT_NODE")
if light_node.hasTag("stored_energy"):
stored_energy = float(light_node.getTag("stored_energy"))
if stored_energy > 0:
light_node.setTag("stored_energy", str(stored_energy))
user_visible = True
if light_node.hasTag("user_visible"):
user_visible = light_node.getTag("user_visible").lower()=="true"
light_node.setPythonTag("user_visible",user_visible)
if not user_visible:
self.toggleLightVisibility(light_node,False)
except Exception as e:
print(f"重新创建点光源失败: {str(e)}")
import traceback
traceback.print_exc()
def isLightObject(self, nodePath):
"""检查是否为灯光对象"""
try:
if not nodePath:
return False
# 方法1: 检查PythonTag
if nodePath.hasPythonTag("rp_light_object"):
rp_light = nodePath.getPythonTag("rp_light_object")
if rp_light is not None:
return True
# 方法2: 检查element_type标签
if nodePath.hasTag("element_type"):
element_type = nodePath.getTag("element_type")
if element_type in ["spotlight", "pointlight"]:
return True
# 方法3: 检查tree_item_type标签
if nodePath.hasTag("tree_item_type"):
tree_item_type = nodePath.getTag("tree_item_type")
if tree_item_type == "LIGHT_NODE":
return True
# 方法4: 通过名称模式匹配(作为后备方案)
node_name = nodePath.getName().lower()
if "spotlight" in node_name or "pointlight" in node_name:
return True
return False
except Exception as e:
print(f"检查灯光对象时出错: {e}")
import traceback
traceback.print_exc()
return False
def toggleLightVisibility(self, light_node, visible):
"""切换灯光可见性"""
try:
print(f"切换灯光可见性: {light_node.getName()}, 可见={visible}")
# 保存用户可见性状态到该特定节点
light_node.setPythonTag("user_visible", visible)
# 获取该特定灯光对象
rp_light_object = light_node.getPythonTag("rp_light_object")
if not rp_light_object:
print(f"错误: {light_node.getName()} 未找到RP灯光对象引用")
return
# 获取RenderPipeline实例
from core.render_pipeline_utils import get_render_pipeline
render_pipeline = get_render_pipeline()
if not render_pipeline:
print("错误: 无法获取RenderPipeline实例")
return
try:
if visible:
if light_node.hasTag("stored_energy"):
stored_energy = float(light_node.getTag("stored_energy"))
rp_light_object.energy=stored_energy
print(f"已恢复灯光强度: {light_node.getName()}, 能量={stored_energy}")
# 启用特定灯光
# render_pipeline.add_light(rp_light_object)
# print(f"已添加灯光到渲染管线: {light_node.getName()}")
else:
# 禁用特定灯光
current_energy = rp_light_object.energy
if current_energy != 0.0:
light_node.setTag("stored_energy", str(current_energy))
elif light_node.hasTag("stored_energy"):
stored_energy = float(light_node.getTag("stored_energy"))
current_energy = stored_energy
else:
current_energy = 0.0
rp_light_object.energy = 0.0
print(f"已禁用灯光: {light_node.getName()}, 保存的能量={current_energy}")
# render_pipeline.remove_light(rp_light_object)
# print(f"已从渲染管线移除灯光: {light_node.getName()}")
except Exception as e:
print(f"操作RenderPipeline灯光时出错: {e}")
# 控制节点显示状态(可选,主要是视觉上的)
if visible:
light_node.show()
else:
light_node.hide()
print(f"灯光可见性设置完成: {visible}")
except Exception as e:
print(f"切换灯光可见性失败: {str(e)}")
import traceback
traceback.print_exc()
def _recreateLightFromData(self, node_data, parent_node, name):
"""根据数据重建光源"""
try:
light_type = node_data.get('tags', {}).get('light_type', 'spot_light')
# 创建光源
if light_type == 'spot_light':
light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0)))
else: # point_light
light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0)))
if light_node:
# 设置名称
light_node.setName(name)
# 恢复其他属性
light_data = node_data.get('light_data', {})
rp_light = light_node.getPythonTag("rp_light_object")
if rp_light and light_data:
if 'energy' in light_data:
rp_light.energy = light_data['energy']
if 'radius' in light_data:
rp_light.radius = light_data['radius']
if 'fov' in light_data and hasattr(rp_light, 'fov'):
rp_light.fov = light_data['fov']
if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'):
rp_light.inner_radius = light_data['inner_radius']
if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'):
rp_light.casts_shadows = light_data['casts_shadows']
if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'):
rp_light.shadow_map_resolution = light_data['shadow_map_resolution']
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name', 'light_type']:
light_node.setTag(tag_key, str(tag_value))
return light_node
except Exception as e:
print(f"重建光源失败: {e}")
return None
def createSpotLight(self, pos=(0, 0, 5)):
"""创建聚光灯
Args:
pos (tuple): 光源位置 (x, y, z)
Returns:
NodePath: 创建的聚光灯节点
"""
try:
# 检查是否使用RenderPipeline
if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline:
from RenderPipelineFile.rpcore import SpotLight
from core.render_pipeline_utils import get_render_pipeline
# 创建RenderPipeline聚光灯
from panda3d.core import Vec3
spotlight = SpotLight()
spotlight.direction = Vec3(0, 0, -1) # 向下照射
spotlight.fov = 70 # 聚光角度
spotlight.set_color_from_temperature(6500) # 日光色温
spotlight.energy = 5000 # 光照强度
spotlight.radius = 1000 # 光照范围
spotlight.casts_shadows = True # 启用阴影
spotlight.shadow_map_resolution = 256 # 阴影分辨率
spotlight.setPos(pos)
# 添加到RenderPipeline
render_pipeline = get_render_pipeline()
if render_pipeline:
render_pipeline.add_light(spotlight)
print(f"✓ RenderPipeline聚光灯创建成功位置: {pos}")
# 创建包装节点用于场景树显示
light_name = f"Spotlight_{len(self.Spotlight)}"
spotlight_node = self.world.render.attachNewNode(light_name)
spotlight_node.setPos(*pos)
spotlight_node.setTag("light_type", "spot_light")
spotlight_node.setTag("is_scene_element", "1")
spotlight_node.setTag("tree_item_type", "LIGHT_NODE")
spotlight_node.setTag("light_energy", str(getattr(spotlight, "energy", 5000)))
spotlight_node.setTag("created_by_user", "1")
spotlight_node.setTag("element_type", "spotlight")
spotlight_node.setPythonTag("rp_light_object", spotlight)
self.Spotlight.append(spotlight_node)
return spotlight_node
else:
print("✗ 无法获取RenderPipeline实例")
return None
else:
# 使用标准Panda3D光源
from panda3d.core import Spotlight, PerspectiveLens
# 创建聚光灯
spotlight = Spotlight('spotlight')
spotlight.setColor((1, 1, 1, 1)) # 白色光
spotlight.setLens(PerspectiveLens())
# 创建光源节点
spotlight_node = self.world.render.attachNewNode(spotlight)
spotlight_node.setPos(pos)
spotlight_node.setTag("light_type", "spot_light")
spotlight_node.setTag("is_scene_element", "1")
spotlight_node.setTag("tree_item_type", "LIGHT_NODE")
spotlight_node.setTag("created_by_user", "1")
spotlight_node.setTag("element_type", "spotlight")
# 设置聚光灯方向(向下照射)
spotlight_node.lookAt(pos[0], pos[1], pos[2] - 5) # 向下看5个单位
# 设置聚光灯范围
spotlight.setExponent(1.0) # 聚光指数
spotlight.setAttenuation((1, 0.1, 0.01)) # 衰减
# 添加到光源列表
self.Spotlight.append(spotlight_node)
# 启用光源
self.world.render.setLight(spotlight_node)
# 启用阴影
if hasattr(spotlight, 'setShadowCaster'):
spotlight.setShadowCaster(True)
print(f"✓ 标准聚光灯创建成功,位置: {pos}")
return spotlight_node
except Exception as e:
print(f"✗ 创建聚光灯失败: {e}")
return None
def createPointLight(self, pos=(0, 0, 5)):
"""创建点光源
Args:
pos (tuple): 光源位置 (x, y, z)
Returns:
NodePath: 创建的点光源节点
"""
try:
# 检查是否使用RenderPipeline
if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline:
from RenderPipelineFile.rpcore import PointLight
from core.render_pipeline_utils import get_render_pipeline
# 创建RenderPipeline点光源
pointlight = PointLight()
pointlight.set_color_from_temperature(6500) # 日光色温
pointlight.energy = 3000 # 光照强度
pointlight.radius = 1000 # 光照范围
pointlight.casts_shadows = True # 启用阴影
pointlight.shadow_map_resolution = 256 # 阴影分辨率
pointlight.setPos(pos)
# 添加到RenderPipeline
render_pipeline = get_render_pipeline()
if render_pipeline:
render_pipeline.add_light(pointlight)
print(f"✓ RenderPipeline点光源创建成功位置: {pos}")
# 创建包装节点用于场景树显示
light_name = f"Pointlight_{len(self.Pointlight)}"
pointlight_node = self.world.render.attachNewNode(light_name)
pointlight_node.setPos(*pos)
pointlight_node.setTag("light_type", "point_light")
pointlight_node.setTag("is_scene_element", "1")
pointlight_node.setTag("tree_item_type", "LIGHT_NODE")
pointlight_node.setTag("light_energy", str(getattr(pointlight, "energy", 3000)))
pointlight_node.setTag("created_by_user", "1")
pointlight_node.setTag("element_type", "pointlight")
pointlight_node.setPythonTag("rp_light_object", pointlight)
self.Pointlight.append(pointlight_node)
return pointlight_node
else:
print("✗ 无法获取RenderPipeline实例")
return None
else:
# 使用标准Panda3D光源
from panda3d.core import PointLight
# 创建点光源
pointlight = PointLight('pointlight')
pointlight.setColor((1, 1, 1, 1)) # 白色光
pointlight.setAttenuation((1, 0.1, 0.01)) # 衰减设置
# 创建光源节点
pointlight_node = self.world.render.attachNewNode(pointlight)
pointlight_node.setPos(pos)
pointlight_node.setTag("light_type", "point_light")
pointlight_node.setTag("is_scene_element", "1")
pointlight_node.setTag("tree_item_type", "LIGHT_NODE")
pointlight_node.setTag("created_by_user", "1")
pointlight_node.setTag("element_type", "pointlight")
# 添加到光源列表
self.Pointlight.append(pointlight_node)
# 启用光源
self.world.render.setLight(pointlight_node)
# 启用阴影
if hasattr(pointlight, 'setShadowCaster'):
pointlight.setShadowCaster(True)
print(f"✓ 标准点光源创建成功,位置: {pos}")
return pointlight_node
except Exception as e:
print(f"✗ 创建点光源失败: {e}")
return None

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,863 @@
"""Scene manager node serialization operations."""
import os
import shutil
import time
import json
import aiohttp
import asyncio
import inspect
from pathlib import Path
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib
)
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
from scene import util
class SceneManagerSerializationMixin:
def serializeNode(self, node):
"""序列化节点为字典数据"""
try:
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
# 特殊处理不同类型的节点
if hasattr(node.node(), 'getClassType'):
node_class = node.node().getClassType().getName()
node_data['node_class'] = node_class
# 递归序列化子节点
for child in node.getChildren():
# 跳过辅助节点
if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')):
child_data = self.serializeNode(child)
if child_data:
node_data['children'].append(child_data)
return node_data
except Exception as e:
print(f"序列化节点 {node.getName()} 失败: {e}")
import traceback
traceback.print_exc()
return None
def deserializeNode(self, node_data, parent_node):
"""从字典数据反序列化节点"""
try:
# 创建新节点
node_name = node_data.get('name', 'node')
new_node = parent_node.attachNewNode(node_name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
new_node.setTag(tag_key, tag_value)
# 根据节点类型进行特殊处理
node_type = node_data.get('type', '')
node_class = node_data.get('node_class', '')
# 特殊处理光源节点
if 'light_type' in node_data.get('tags', {}):
light_type = node_data['tags']['light_type']
if light_type == 'spot_light':
self._recreateSpotLight(new_node)
elif light_type == 'point_light':
self._recreatePointLight(new_node)
# 递归创建子节点
for child_data in node_data.get('children', []):
self.deserializeNode(child_data, new_node)
return new_node
except Exception as e:
print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}")
import traceback
traceback.print_exc()
return None
def serializeNodeForCopy(self, node):
"""序列化节点用于复制操作,完整保存视觉属性"""
try:
if not node or node.isEmpty():
return None
node_data = {
'name': node.getName(),
'type': type(node.node()).__name__,
'pos': (node.getX(), node.getY(), node.getZ()),
'hpr': (node.getH(), node.getP(), node.getR()),
'scale': (node.getSx(), node.getSy(), node.getSz()),
'tags': {},
'children': []
}
# 保存所有标签
try:
if hasattr(node, 'getTagKeys'):
for tag_key in node.getTagKeys():
node_data['tags'][tag_key] = node.getTag(tag_key)
except Exception as e:
print(f"获取标签时出错: {e}")
# 保存视觉属性
try:
# 保存颜色属性
if hasattr(node, 'getColor'):
color = node.getColor()
node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW())
# 保存材质属性
if hasattr(node, 'getMaterial'):
material = node.getMaterial()
if material:
material_data = {}
material_data['base_color'] = (
material.getBaseColor().getX(),
material.getBaseColor().getY(),
material.getBaseColor().getZ(),
material.getBaseColor().getW()
)
material_data['ambient'] = (
material.getAmbient().getX(),
material.getAmbient().getY(),
material.getAmbient().getZ(),
material.getAmbient().getW()
)
material_data['diffuse'] = (
material.getDiffuse().getX(),
material.getDiffuse().getY(),
material.getDiffuse().getZ(),
material.getDiffuse().getW()
)
material_data['specular'] = (
material.getSpecular().getX(),
material.getSpecular().getY(),
material.getSpecular().getZ(),
material.getSpecular().getW()
)
material_data['shininess'] = material.getShininess()
node_data['material'] = material_data
except Exception as e:
print(f"保存视觉属性时出错: {e}")
# 根据节点类型保存特定信息
if node.hasTag("tree_item_type"):
node_type = node.getTag("tree_item_type")
node_data['node_type'] = node_type
# 保存特定类型节点的额外信息
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
# 保存光源特定信息
rp_light = node.getPythonTag("rp_light_object")
if rp_light:
node_data['light_data'] = {
'energy': getattr(rp_light, 'energy', 5000),
'radius': getattr(rp_light, 'radius', 1000),
'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None,
'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light,
'inner_radius') else None,
'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light,
'casts_shadows') else True,
'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr(
rp_light, 'shadow_map_resolution') else 256
}
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
"GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]:
# 保存GUI元素特定信息
node_data['gui_data'] = self._serializeGUIData(node)
elif node_type == "IMPORTED_MODEL_NODE":
# 保存模型特定信息
node_data['model_data'] = self._serializeModelData(node)
return node_data
except Exception as e:
print(f"序列化节点失败: {e}")
import traceback
traceback.print_exc()
return None
def _serializeGUIData(self, node):
"""序列化GUI元素数据"""
try:
gui_data = {}
# 保存GUI相关的通用属性
if node.hasTag("gui_type"):
gui_data['gui_type'] = node.getTag("gui_type")
# 保存文本内容(如果有的话)
if node.hasTag("text"):
gui_data['text'] = node.getTag("text")
# 保存其他GUI相关标签
gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size']
for tag in gui_tags:
if node.hasTag(tag):
gui_data[tag] = node.getTag(tag)
return gui_data
except Exception as e:
print(f"序列化GUI数据失败: {e}")
return {}
def _serializeModelTextures(self, node):
"""序列化模型纹理信息"""
try:
texture_data = {}
# 获取节点的所有纹理阶段
from panda3d.core import TextureStage
texture_stages = node.findAllTextureStages()
if texture_stages.getNumTextureStages() > 0:
texture_data['textures'] = {}
# 遍历所有纹理阶段
for i in range(texture_stages.getNumTextureStages()):
stage = texture_stages.getTextureStage(i)
stage_name = stage.getName()
# 获取该阶段的纹理
texture = node.getTexture(stage)
if texture:
# 保存纹理信息
texture_info = {
'stage_name': stage_name,
'stage_mode': stage.getMode(),
'stage_sort': stage.getSort(), # 保存纹理阶段排序
'texture_path': texture.getFullpath().toOsSpecific() if texture.hasFullpath() else '',
'texture_name': texture.getName(),
'wrap_u': texture.getWrapU(),
'wrap_v': texture.getWrapV(),
'minfilter': texture.getMinfilter(),
'magfilter': texture.getMagfilter(),
'anisotropic_degree': texture.getAnisotropicDegree()
}
# 保存颜色比例和偏移(使用安全的方法)
try:
texture_info['color_scale'] = tuple(node.getTextureScale(stage))
except:
texture_info['color_scale'] = (1.0, 1.0, 1.0, 1.0)
try:
texture_info['color_offset'] = tuple(node.getTextureOffset(stage))
except:
texture_info['color_offset'] = (0.0, 0.0, 0.0, 0.0)
texture_data['textures'][stage_name] = texture_info
return texture_data
except Exception as e:
print(f"序列化模型纹理时出错: {e}")
return {}
def _serializeModelData(self, node):
"""序列化模型数据,包括材质和纹理信息"""
try:
model_data = {}
# 保存模型相关的标签
model_tags = ['model_path', 'file', 'element_type']
for tag in model_tags:
if node.hasTag(tag):
model_data[tag] = node.getTag(tag)
# 保存材质信息
try:
# 获取模型的材质信息
if hasattr(node, 'getState'):
state = node.getState()
if state:
# 保存基础颜色信息(使用正确的方法)
from panda3d.core import ColorAttrib
color_attrib = state.getAttrib(ColorAttrib)
if color_attrib and not color_attrib.isOff():
color = color_attrib.getColor()
model_data['base_color'] = (
color.getX(),
color.getY(),
color.getZ(),
color.getW()
)
# 保存其他材质属性
from panda3d.core import MaterialAttrib
material_attrib = state.getAttrib(MaterialAttrib.getClassType())
if material_attrib:
material = material_attrib.getMaterial()
if material:
# 保存基础颜色
base_color = material.getBaseColor()
model_data['material_base_color'] = (
base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW()
)
# 保存环境光颜色
ambient_color = material.getAmbient()
model_data['material_ambient_color'] = (
ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(),
ambient_color.getW()
)
# 保存漫反射颜色
diffuse_color = material.getDiffuse()
model_data['material_diffuse_color'] = (
diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(),
diffuse_color.getW()
)
# 保存高光颜色
specular_color = material.getSpecular()
model_data['material_specular_color'] = (
specular_color.getX(), specular_color.getY(), specular_color.getZ(),
specular_color.getW()
)
# 保存粗糙度和金属度等参数
model_data['material_roughness'] = material.getRoughness()
model_data['material_metallic'] = material.getMetallic()
# 保存自发光颜色
emission_color = material.getEmission()
model_data['material_emission_color'] = (
emission_color.getX(), emission_color.getY(), emission_color.getZ(),
emission_color.getW()
)
# 保存光泽度
model_data['material_shininess'] = material.getShininess()
# 保存透明度信息
from panda3d.core import TransparencyAttrib
transparency_attrib = state.getAttrib(TransparencyAttrib.getClassType())
if transparency_attrib:
model_data['transparency_mode'] = transparency_attrib.get_mode()
except Exception as e:
print(f"保存材质信息时出错: {e}")
# 保存纹理信息
try:
texture_data = self._serializeModelTextures(node)
if texture_data:
model_data['texture_data'] = texture_data
except Exception as e:
print(f"保存纹理信息时出错: {e}")
return model_data
except Exception as e:
print(f"序列化模型数据失败: {e}")
return {}
def recreateNodeFromData(self, node_data, parent_node):
"""根据数据重建节点,并确保在场景树中显示"""
try:
if not node_data or not parent_node or parent_node.isEmpty():
return None
print(f"正在重建节点 {node_data}")
node_type = node_data.get('node_type', '')
original_name = node_data.get('name', 'node')
# 生成唯一名称
unique_name = self._generateUniqueName(original_name, parent_node)
# 根据节点类型调用相应的重建方法
new_node = None
if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]:
new_node = self._recreateLightFromData(node_data, parent_node, unique_name)
elif node_type == "CESIUM_TILESET_NODE":
new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name)
elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE",
"GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]:
new_node = self._recreateGUIFromData(node_data, parent_node, unique_name)
elif node_type == "IMPORTED_MODEL_NODE":
new_node = self._recreateModelFromData(node_data, parent_node, unique_name)
else:
# 创建普通节点
new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name)
# 如果成功创建节点,确保它在场景树中显示
if new_node:
# 尝试更新场景树以显示新节点
try:
if hasattr(self.world, 'interface_manager') and self.world.interface_manager:
# 查找父节点在场景树中的对应项
parent_item = self._findTreeItemForNode(parent_node)
if parent_item:
# 添加新节点到场景树
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE")
except Exception as e:
print(f"添加节点到场景树时出错: {e}")
return new_node
except Exception as e:
print(f"重建节点失败: {e}")
import traceback
traceback.print_exc()
return None
def _findTreeItemForNode(self, node):
"""根据节点查找对应的场景树项"""
try:
if hasattr(self.world, 'interface_manager') and self.world.interface_manager:
tree_widget = self.world.interface_manager.treeWidget
if tree_widget:
# 遍历场景树查找匹配的节点项
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
result = self._findTreeItemForNodeRecursive(item, node)
if result:
return result
return None
except Exception as e:
print(f"查找场景树项时出错: {e}")
return None
def _findTreeItemForNodeRecursive(self, item, target_node):
"""递归查找场景树项"""
try:
# 检查当前项是否匹配
item_node = getattr(item, 'node_path', None)
if not item_node:
item_node = getattr(item, 'node', None)
if item_node and item_node == target_node:
return item
# 递归检查子项
for i in range(item.childCount()):
child_item = item.child(i)
result = self._findTreeItemForNodeRecursive(child_item, target_node)
if result:
return result
return None
except Exception as e:
print(f"递归查找场景树项时出错: {e}")
return None
def _recreateTilesetFromData(self, node_data, parent_node, name):
"""根据数据重建Tileset"""
try:
tileset_url = node_data.get('tileset_url', '')
if not tileset_url:
return None
# 使用现有方法加载tileset
position = node_data.get('pos', (0, 0, 0))
tileset_node = self.load_cesium_tileset(tileset_url, position)
if tileset_node:
# 设置名称
tileset_node.setName(name)
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
tileset_node.setTag(tag_key, str(tag_value))
return tileset_node
except Exception as e:
print(f"重建Tileset失败: {e}")
return None
def _recreateGUIFromData(self, node_data, parent_node, name):
"""根据数据重建GUI元素"""
try:
gui_data = node_data.get('gui_data', {})
#gui_type = gui_data.get('gui_type', '')
gui_type = node_data.get("tags").get("gui_type", "")
print(f"正在重建GUI元素: {gui_type}")
print(f"正在重建GUI元素: {node_data}")
# 根据GUI类型调用相应的创建方法
new_gui_element = None
if gui_type == "button" and hasattr(self.world, 'createGUIButton'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
print(pos,text,size)
new_gui_element = self.world.createGUIButton(pos,text,size)
elif gui_type == "label" and hasattr(self.world, 'createGUILabel'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
new_gui_element = self.world.createGUILabel(pos,text,size)
elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'):
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags').get('gui_text', '')
size = node_data.get('scale', 1)
new_gui_element = self.world.createGUIEntry(pos,text,size)
elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'):
pos = node_data.get('pos', (0, 0, 0))
image_path = node_data.get('tags').get('image_path', '')
size = node_data.get('size', 1)
new_gui_element = self.world.createGUI2DImage(pos, image_path, size)
elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'):
print("正在创建3D文本!!!")
pos = node_data.get('pos', (0, 0, 0))
text = node_data.get('tags', {}).get('gui_text', '')
scale = node_data.get('scale', 1)
if isinstance(scale, (list, tuple)):
scale = scale[0] if len(scale) > 0 else 1
print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}")
new_gui_element = self.world.createGUI3DText(pos, text, scale)
elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'):
pos = node_data.get('pos', (0, 0, 0))
image_path = node_data.get('tags').get('gui_image_path', '')
scale = node_data.get('scale', (1, 1))
if isinstance(scale, (int, float)):
scale = (scale, scale)
elif isinstance(scale, (list, tuple)) and len(scale) >= 2:
scale = (scale[0], scale[1])
else:
scale = (1, 1)
print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}")
new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale)
elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'):
pos = node_data.get('pos', (0, 0, 0))
video_path = node_data.get('tags').get('video_path', '')
scale = node_data.get('scale', (1, 1,1))
new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path)
elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'):
pos = node_data.get('pos', (0, 0, 0))
video_path = node_data.get('tags').get('video_path', '')
scale = node_data.get('scale', (1, 1, 1))
new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path)
if new_gui_element:
# 设置名称和变换
if hasattr(new_gui_element, 'setName'):
new_gui_element.setName(name)
# 设置位置、旋转、缩放
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
if hasattr(new_gui_element, 'setPos'):
new_gui_element.setPos(*pos)
if hasattr(new_gui_element, 'setHpr'):
new_gui_element.setHpr(*hpr)
if hasattr(new_gui_element, 'setScale'):
new_gui_element.setScale(*scale)
# 恢复文本内容
if 'text' in gui_data and hasattr(new_gui_element, 'setText'):
new_gui_element.setText(gui_data['text'])
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']:
new_gui_element.setTag(tag_key, str(tag_value))
print(f"GUI元素重建成功: {name}")
return new_gui_element
except Exception as e:
print(f"重建GUI元素失败: {e}")
import traceback
traceback.print_exc()
return None
def _recreateModelFromData(self, node_data, parent_node, name):
"""根据数据重建模型,保持材质"""
try:
model_data = node_data.get('model_data', {})
model_path = model_data.get('model_path', model_data.get('file', ''))
if not model_path or not os.path.exists(model_path):
# 如果原始模型文件不存在,创建一个基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
# 导入模型,保持原有参数
model = self.importModel(
model_path,
apply_unit_conversion=False, # 已经处理过的模型不需要再次转换
normalize_scales=False, # 保持原有缩放
auto_convert_to_glb=False # 已经处理过的模型不需要再次转换
)
if model:
# 设置名称
model.setName(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
model.setPos(*pos)
model.setHpr(*hpr)
model.setScale(*scale)
# 恢复材质和纹理信息
try:
self._restoreModelMaterial(model, model_data)
except Exception as e:
print(f"恢复模型材质时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
model.setTag(tag_key, str(tag_value))
# 添加到模型列表
if model not in self.models:
self.models.append(model)
return model
except Exception as e:
print(f"重建模型失败: {e}")
# 出错时创建基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
def _restoreModelTextures(self, model, texture_data):
"""恢复模型纹理"""
try:
if not texture_data or 'textures' not in texture_data:
return
from panda3d.core import TextureStage, SamplerState
textures_info = texture_data['textures']
# 为每个纹理阶段恢复纹理
for stage_name, texture_info in textures_info.items():
# 创建纹理阶段
stage = TextureStage(stage_name)
stage.setMode(texture_info.get('stage_mode', TextureStage.M_modulate))
# 恢复纹理阶段排序
stage.setSort(texture_info.get('stage_sort', 0)) # 默认为0p3d_Texture0
# 加载纹理
texture_path = texture_info['texture_path']
if texture_path and os.path.exists(texture_path):
texture = self.world.loader.loadTexture(texture_path)
if texture:
# 设置纹理属性
texture.setWrapU(texture_info.get('wrap_u', SamplerState.WM_repeat))
texture.setWrapV(texture_info.get('wrap_v', SamplerState.WM_repeat))
texture.setMinfilter(texture_info.get('minfilter', SamplerState.FT_linear))
texture.setMagfilter(texture_info.get('magfilter', SamplerState.FT_linear))
texture.setAnisotropicDegree(texture_info.get('anisotropic_degree', 1))
# 应用纹理到模型
model.setTexture(stage, texture, 1) # 1 表示强制应用
# 恢复颜色比例和偏移(使用安全的方法)
if 'color_scale' in texture_info:
try:
model.setTextureScale(stage, *texture_info['color_scale'])
except Exception as e:
print(f"恢复纹理比例失败: {e}")
if 'color_offset' in texture_info:
try:
model.setTextureOffset(stage, *texture_info['color_offset'])
except Exception as e:
print(f"恢复纹理偏移失败: {e}")
print(f"恢复纹理: {stage_name} <- {texture_path}")
else:
print(f"纹理文件不存在或路径为空: {texture_path}")
except Exception as e:
print(f"恢复模型纹理时出错: {e}")
def _restoreModelMaterial(self, model, model_data):
"""恢复模型材质和纹理"""
try:
# 恢复基础颜色
if 'base_color' in model_data:
from panda3d.core import ColorAttrib
base_color = model_data['base_color']
color = (base_color[0], base_color[1], base_color[2], base_color[3])
model.setColor(color)
# 恢复复杂材质属性
if any(key.startswith('material_') for key in model_data.keys()):
from panda3d.core import Material
# 创建新材质或获取现有材质
material = Material()
# 恢复基础颜色
if 'material_base_color' in model_data:
base_color = model_data['material_base_color']
material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3]))
# 恢复环境光颜色
if 'material_ambient_color' in model_data:
ambient_color = model_data['material_ambient_color']
material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]))
# 恢复漫反射颜色
if 'material_diffuse_color' in model_data:
diffuse_color = model_data['material_diffuse_color']
material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]))
# 恢复高光颜色
if 'material_specular_color' in model_data:
specular_color = model_data['material_specular_color']
material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3]))
# 恢复自发光颜色
if 'material_emission_color' in model_data:
emission_color = model_data['material_emission_color']
material.setEmission((emission_color[0], emission_color[1], emission_color[2], emission_color[3]))
# 恢复粗糙度和金属度
if 'material_roughness' in model_data:
material.setRoughness(model_data['material_roughness'])
if 'material_metallic' in model_data:
material.setMetallic(model_data['material_metallic'])
# 恢复光泽度
if 'material_shininess' in model_data:
material.setShininess(model_data['material_shininess'])
# 应用材质到模型
model.setMaterial(material)
# 恢复透明度设置
if 'transparency_mode' in model_data:
from panda3d.core import TransparencyAttrib
transparency_mode = model_data['transparency_mode']
model.setTransparency(transparency_mode)
# 恢复纹理信息
if 'texture_data' in model_data:
self._restoreModelTextures(model, model_data['texture_data'])
except Exception as e:
print(f"恢复材质失败: {e}")
def _createBasicNodeFromData(self, node_data, parent_node, name):
"""创建基本节点,保持视觉属性"""
try:
new_node = parent_node.attachNewNode(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
# 恢复视觉属性
try:
# 恢复颜色
if 'color' in node_data:
color_data = node_data['color']
new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3])
# 恢复材质
if 'material' in node_data:
from panda3d.core import Material
material_data = node_data['material']
material = Material()
if 'base_color' in material_data:
bc = material_data['base_color']
material.setBaseColor((bc[0], bc[1], bc[2], bc[3]))
if 'ambient' in material_data:
ac = material_data['ambient']
material.setAmbient((ac[0], ac[1], ac[2], ac[3]))
if 'diffuse' in material_data:
dc = material_data['diffuse']
material.setDiffuse((dc[0], dc[1], dc[2], dc[3]))
if 'specular' in material_data:
sc = material_data['specular']
material.setSpecular((sc[0], sc[1], sc[2], sc[3]))
if 'shininess' in material_data:
material.setShininess(material_data['shininess'])
new_node.setMaterial(material)
except Exception as e:
print(f"恢复视觉属性时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
new_node.setTag(tag_key, str(tag_value))
return new_node
except Exception as e:
print(f"创建基本节点失败: {e}")
return None
def _generateUniqueName(self, base_name, parent_node):
"""生成唯一节点名称"""
try:
# 移除可能的数字后缀
import re
import time
name_base = re.sub(r'_\d+$', '', base_name)
# 查找现有同名节点
counter = 1
unique_name = base_name
while True:
# 检查父节点下是否已存在同名子节点
existing_node = parent_node.find(unique_name)
if existing_node.isEmpty():
break
unique_name = f"{name_base}_{counter}"
counter += 1
if counter > 1000: # 防止无限循环
break
return unique_name
except Exception as e:
print(f"生成唯一名称时出错: {e}")
return f"{base_name}_{int(time.time())}"

5
scene/tree_roles.py Normal file
View File

@ -0,0 +1,5 @@
"""Shared tree data-role constants (Qt-independent)."""
# Qt.ItemDataRole.UserRole
TREE_USER_ROLE = 256

View File

@ -56,16 +56,29 @@ class CrossPlatformPathHandler:
exists = os.path.exists(filepath)
return exists
def _panda3d_normalize(self, filepath):
"""使用Panda3D标准化路径"""
try:
panda_filename = Filename.from_os_specific(filepath)
normalized_path = panda_filename.get_fullpath()
print(f"✓ Panda3D标准化: {normalized_path}")
return normalized_path
except Exception as e:
print(f"⚠️ Panda3D标准化失败: {e}")
return filepath
def _panda3d_normalize(self, filepath):
"""使用Panda3D标准化路径"""
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
ctor = getattr(Filename, ctor_name, None)
if not ctor:
continue
try:
panda_filename = ctor(filepath)
normalized_path = panda_filename.get_fullpath()
if normalized_path:
print(f"✓ Panda3D标准化: {normalized_path}")
return normalized_path
except Exception:
continue
try:
panda_filename = Filename(filepath)
normalized_path = panda_filename.get_fullpath()
if normalized_path:
print(f"✓ Panda3D标准化: {normalized_path}")
return normalized_path
except Exception as e:
print(f"⚠️ Panda3D标准化失败: {e}")
return filepath
def _attempt_path_fixes(self, filepath):
"""尝试各种路径修复方法"""
@ -293,4 +306,4 @@ def normalize_model_path(filepath):
def suggest_file_placement(filename):
"""便捷函数:建议文件放置位置"""
return path_handler.suggest_file_placement(filename)
return path_handler.suggest_file_placement(filename)

View File

@ -1,51 +1,193 @@

import math
from panda3d.core import (
GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter,
InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums,
BoundingSphere, NodePath, GeomNode, Texture, SamplerState,
Point3, BoundingBox, Quat
)
import struct
import time
class ObjectController:
"""
物体控制器 (No Custom Shader Mode)
====================================
Uses RP's default rendering (no rp.set_effect) for maximum FPS.
Vertex colors baked for picking. Movement modifies vertex data directly.
Stores original vertex positions per object for rotation/translation.
混合架构控制器 (Chunked Static + Dynamic Editing)
================================================
- 默认: 每个 chunk 使用 flatten 后的静态表示
- 编辑: 被选中对象所属 chunk 切换为动态表示直接改 NodePath 变换
- 提交: 离开 chunk 时仅重建该 chunk 的静态表示
"""
def __init__(self):
def __init__(self, chunk_size=64, chunk_world_size=40.0):
self.chunk_size = max(8, int(chunk_size))
self.chunk_world_size = max(8.0, float(chunk_world_size))
self._reset_state()
def _reset_state(self):
self.name_to_ids = {}
self.id_to_name = {}
self.key_to_node = {}
self.node_list = []
self.display_names = {}
self.global_transforms = [] # Original transforms (for center/position)
self.id_to_chunk = {} # global_id -> (chunk_key, local_idx)
self.chunks = {} # chunk_key -> dict with 'node' key
# Vertex index: local_id -> list of (geom_node_np, geom_idx, [row_indices])
self.vertex_index = {}
# Original vertex positions: local_id -> list of (Vec3,) matching row order
self.original_positions = {}
# Current position offsets: local_id -> Vec3 delta
self.global_transforms = []
self.position_offsets = {}
self.vertex_index = {}
self.original_positions = {}
self.local_to_global_id = {}
self.local_transform_state = {}
self.local_transform_base_positions = {}
self.pick_vertex_index = {}
self.virtual_tree = None
self.virtual_tree_meta = None
self.model = None
self.pick_model = None
self.chunk_node = None # Single chunk node
self._source_model_name = ""
self._source_model_stem = ""
self.id_to_chunk = {} # global_id -> chunk_id
self.id_to_object_np = {} # global_id -> dynamic object nodepath
self.id_to_pick_np = {} # global_id -> pick-scene nodepath
# chunk_id -> {
# "dynamic_np": NodePath,
# "static_np": NodePath or None,
# "members": [global_id],
# "dirty": bool,
# "dynamic_enabled": bool
# }
self.chunks = {}
self.active_chunks = set()
self._next_chunk_id = 0
# spatial cell key -> [chunk_id, ...]
self._cell_to_chunks = {}
# UI hierarchy metadata (matches source model parent/child structure)
self.tree_root_key = None
self.tree_nodes = {}
self._path_to_tree_key = {}
def _register_tree_node(self, key, display_name, parent_key):
self.tree_nodes[key] = {
"name": display_name,
"parent": parent_key,
"children": [],
"local_ids": [],
}
self.display_names[key] = display_name
self.name_to_ids[key] = []
if parent_key is not None and parent_key in self.tree_nodes:
self.tree_nodes[parent_key]["children"].append(key)
def _build_scene_tree(self, root_np):
"""Capture source model hierarchy for UI (independent from render batching)."""
self.tree_root_key = "0"
def walk(np, parent_key, key):
display_name = np.get_name() or "Unnamed"
self._register_tree_node(key, display_name, parent_key)
self._path_to_tree_key[str(np)] = key
children = list(np.get_children())
for i, child in enumerate(children):
walk(child, key, f"{key}/{i}")
walk(root_np, None, self.tree_root_key)
def _get_model_world_mat(self):
"""Return current model net transform matrix (to top/root)."""
if not self.model:
return LMatrix4f.ident_mat()
try:
if self.model.isEmpty():
return LMatrix4f.ident_mat()
except Exception:
try:
if self.model.is_empty():
return LMatrix4f.ident_mat()
except Exception:
pass
try:
return LMatrix4f(self.model.getNetTransform().getMat())
except Exception:
try:
# snake_case fallback in newer Panda3D bindings
return LMatrix4f(self.model.get_net_transform().get_mat())
except Exception:
pass
try:
top = self.model.getTop()
if top and not top.isEmpty():
return LMatrix4f(self.model.getMat(top))
except Exception:
pass
try:
return LMatrix4f(self.model.getMat())
except Exception:
return LMatrix4f.ident_mat()
def get_model_world_mat(self):
"""Public accessor for current model net transform matrix."""
return self._get_model_world_mat()
def _local_point_to_world(self, local_pos):
"""Convert a local-space point to world-space based on model net transform."""
mat = self._get_model_world_mat()
p = Point3(float(local_pos.x), float(local_pos.y), float(local_pos.z))
wp = mat.xformPoint(p)
return Vec3(wp.x, wp.y, wp.z)
def _world_vec_to_local(self, world_vec):
"""Convert a world-space vector to model-local space."""
mat = self._get_model_world_mat()
inv = LMatrix4f(mat)
try:
inv.invertInPlace()
except Exception:
try:
inv.invert_in_place()
except Exception:
return Vec3(world_vec)
v = Vec3(world_vec)
lv = inv.xformVec(v)
return Vec3(lv.x, lv.y, lv.z)
def world_vector_to_model_local(self, world_vec):
"""Public converter from world delta vector to model-local delta vector."""
return self._world_vec_to_local(world_vec)
def get_model_world_quat(self):
"""Return current model world quaternion."""
if not self.model:
return Quat.identQuat()
try:
if self.model.isEmpty():
return Quat.identQuat()
except Exception:
pass
try:
top = self.model.getTop()
if top and not top.isEmpty():
return Quat(self.model.getQuat(top))
except Exception:
pass
try:
return Quat(self.model.getQuat())
except Exception:
return Quat.identQuat()
def world_quat_delta_to_model_local(self, delta_quat_world):
"""
Convert world-space delta quaternion to model-local delta quaternion.
local = inv(model_world_rot) * world_delta * model_world_rot
"""
if delta_quat_world is None:
return Quat.identQuat()
model_q = self.get_model_world_quat()
inv_model_q = Quat(model_q)
inv_model_q.invertInPlace()
local_q = inv_model_q * Quat(delta_quat_world) * model_q
local_q.normalize()
return local_q
def _build_original_hierarchy_key(self, np, model_root):
"""Capture hierarchy path before flatten/reparent."""
@ -60,6 +202,71 @@ class ObjectController:
if not parts:
return np.get_name() or "Unnamed"
return "/".join(parts)
def _aggregate_tree_ids(self, key):
node = self.tree_nodes[key]
agg_ids = list(node["local_ids"])
for child_key in node["children"]:
agg_ids.extend(self._aggregate_tree_ids(child_key))
self.name_to_ids[key] = agg_ids
return agg_ids
def _build_tree_preorder(self, key, out):
out.append(key)
for child_key in self.tree_nodes[key]["children"]:
self._build_tree_preorder(child_key, out)
def should_hide_tree_node(self, key):
"""
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
This keeps `model.glb` as the visible root in the UI.
"""
node = self.tree_nodes.get(key)
if not node:
return False
if node["parent"] != self.tree_root_key:
return False
name = (node["name"] or "").strip().lower()
if name != "root":
return False
# Keep node visible if it actually carries direct geoms.
if node["local_ids"]:
return False
return len(node["children"]) > 0
def _encode_id_color(self, vdata, object_id):
if not vdata.has_column("color"):
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
vdata.set_format(new_fmt)
low = object_id & 0xFF
high = (object_id >> 8) & 0xFF
r = low / 255.0
g = high / 255.0
writer = GeomVertexWriter(vdata, InternalName.make("color"))
for row in range(vdata.get_num_rows()):
writer.set_row(row)
writer.set_data4f(r, g, 0.0, 1.0)
def _ensure_chunk(self, root_np, chunk_id):
if chunk_id in self.chunks:
return self.chunks[chunk_id]
dynamic_np = root_np.attach_new_node(f"chunk_{chunk_id:04d}_dynamic")
dynamic_np.stash()
chunk_data = {
"dynamic_np": dynamic_np,
"static_np": None,
"members": [],
"dirty": False,
"dynamic_enabled": False,
}
self.chunks[chunk_id] = chunk_data
return chunk_data
def _is_wrapper_segment(self, segment):
s = (segment or "").strip().lower()
@ -99,20 +306,21 @@ class ObjectController:
self.local_to_global_id = {}
self.local_transform_state = {}
self.local_transform_base_positions = {}
self.pick_vertex_index = {}
self.virtual_tree = None
self.virtual_tree_meta = None
self.pick_model = None
model_name = (model.get_name() or "").strip()
self._source_model_name = model_name.lower()
self._source_model_stem = model_name.rsplit(".", 1)[0].lower() if "." in model_name else model_name.lower()
global_id_counter = 0
chunk_key = model.get_name() or "default"
# No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py)
self.chunk_node = model
self.chunks[chunk_key] = {'node': model, 'base_id': 0}
# Cache original hierarchy path BEFORE flatten/reparent.
original_keys = {}
for np in geom_nodes:
@ -121,12 +329,12 @@ class ObjectController:
# Flatten hierarchy
for np in geom_nodes:
np.wrt_reparent_to(model)
local_idx = 0
for np in geom_nodes:
gnode = np.node()
if gnode.get_num_parents() > 1:
parent = np.get_parent()
if not parent.is_empty():
@ -134,57 +342,57 @@ class ObjectController:
np.detach_node()
np = new_np
gnode = np.node()
unique_key = original_keys.get(id(np), str(np))
display_name = np.get_name() or f"Object_{global_id_counter}"
if unique_key not in self.name_to_ids:
self.name_to_ids[unique_key] = []
self.key_to_node[unique_key] = np
self.node_list.append(unique_key)
self.display_names[unique_key] = display_name
# Save original transform
mat_double = np.get_mat()
original_transform = LMatrix4f(mat_double)
for i in range(gnode.get_num_geoms()):
geom = gnode.modify_geom(i)
vdata = geom.modify_vertex_data()
if not vdata.has_column("color"):
new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
vdata.set_format(new_format)
# Encode Local ID in R/G
low = local_idx % 256
high = local_idx // 256
r = low / 255.0
g = high / 255.0
writer = GeomVertexWriter(vdata, InternalName.make("color"))
for row in range(vdata.get_num_rows()):
writer.set_row(row)
writer.set_data4f(r, g, 0.0, 1.0)
self.global_transforms.append(original_transform)
self.id_to_chunk[global_id_counter] = (chunk_key, local_idx)
self.name_to_ids[unique_key].append(global_id_counter)
self.id_to_name[global_id_counter] = unique_key
self.local_to_global_id[local_idx] = global_id_counter
self.position_offsets[local_idx] = Vec3(0, 0, 0)
global_id_counter += 1
local_idx += 1
# DO NOT reset transform — keep world-space positions
# Flatten directly on model — NO set_final, allows per-geom frustum culling
model.flatten_strong()
t1 = time.time()
print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms")
# Build vertex index AFTER flatten
self._build_vertex_index(model)
self._init_local_transform_state()
@ -192,6 +400,7 @@ class ObjectController:
# Keep ID colors only in picking clone to avoid affecting visible shading.
self.pick_model = model.copy_to(NodePath("ssbo_pick_root"))
self._build_pick_vertex_index(self.pick_model)
self._set_uniform_vertex_color(model, 1.0, 1.0, 1.0, 1.0)
t2 = time.time()
@ -219,6 +428,114 @@ class ObjectController:
writer.set_row(row)
writer.set_data4f(r, g, b, a)
def _build_tree_preorder(self, key, out):
out.append(key)
for child_key in self.tree_nodes[key]["children"]:
self._build_tree_preorder(child_key, out)
def should_hide_tree_node(self, key):
"""
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
This keeps `model.glb` as the visible root in the UI.
"""
node = self.tree_nodes.get(key)
if not node:
return False
if node["parent"] != self.tree_root_key:
return False
name = (node["name"] or "").strip().lower()
if name != "root":
return False
# Keep node visible if it actually carries direct geoms.
if node["local_ids"]:
return False
return len(node["children"]) > 0
def _encode_id_color(self, vdata, object_id):
if not vdata.has_column("color"):
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
vdata.set_format(new_fmt)
low = object_id & 0xFF
high = (object_id >> 8) & 0xFF
r = low / 255.0
g = high / 255.0
writer = GeomVertexWriter(vdata, InternalName.make("color"))
for row in range(vdata.get_num_rows()):
writer.set_row(row)
writer.set_data4f(r, g, 0.0, 1.0)
def _ensure_chunk(self, root_np, chunk_id):
if chunk_id in self.chunks:
return self.chunks[chunk_id]
dynamic_np = root_np.attach_new_node(f"chunk_{chunk_id:04d}_dynamic")
dynamic_np.stash()
chunk_data = {
"dynamic_np": dynamic_np,
"static_np": None,
"members": [],
"dirty": False,
"dynamic_enabled": False,
}
self.chunks[chunk_id] = chunk_data
return chunk_data
def _get_cell_key_from_pos(self, pos):
inv = 1.0 / self.chunk_world_size
return (
int(math.floor(pos.x * inv)),
int(math.floor(pos.y * inv)),
int(math.floor(pos.z * inv)),
)
def _allocate_spatial_chunk(self, root_np, world_pos):
"""
Allocate object into a spatially-local chunk for better frustum culling.
Objects in the same world cell are grouped together, and overflow creates
another chunk for that same cell.
"""
cell_key = self._get_cell_key_from_pos(world_pos)
chunk_ids = self._cell_to_chunks.setdefault(cell_key, [])
for chunk_id in chunk_ids:
chunk = self.chunks.get(chunk_id)
if chunk and len(chunk["members"]) < self.chunk_size:
return chunk_id, chunk
chunk_id = self._next_chunk_id
self._next_chunk_id += 1
chunk_ids.append(chunk_id)
return chunk_id, self._ensure_chunk(root_np, chunk_id)
def _rebuild_static_chunk(self, chunk_id):
chunk = self.chunks.get(chunk_id)
if not chunk:
return
old_static = chunk.get("static_np")
if old_static and not old_static.is_empty():
old_static.remove_node()
static_np = chunk["dynamic_np"].copy_to(self.model)
static_np.set_name(f"chunk_{chunk_id:04d}_static")
static_np.unstash()
static_np.flatten_strong()
chunk["static_np"] = static_np
chunk["dirty"] = False
# Keep visibility coherent with current mode after rebuild.
if chunk["dynamic_enabled"]:
static_np.stash()
else:
static_np.unstash()
def build_virtual_hierarchy(self):
"""Build a readonly virtual tree from node_list path keys."""
root = {
@ -403,6 +720,228 @@ class ObjectController:
self.vertex_index[uid].append((gn_np, gi, rows))
self.original_positions[uid].append(pos.copy())
def _set_chunk_dynamic(self, chunk_id, enabled):
chunk = self.chunks.get(chunk_id)
if not chunk:
return
if enabled:
if chunk["dynamic_enabled"]:
return
chunk["dynamic_np"].unstash()
if chunk["static_np"] and not chunk["static_np"].is_empty():
chunk["static_np"].stash()
chunk["dynamic_enabled"] = True
self.active_chunks.add(chunk_id)
return
if not chunk["dynamic_enabled"]:
return
if chunk["static_np"] and not chunk["static_np"].is_empty():
chunk["static_np"].unstash()
chunk["dynamic_np"].stash()
chunk["dynamic_enabled"] = False
self.active_chunks.discard(chunk_id)
def _resolve_chunk_and_local_idx(self, global_id):
"""
Compatibility helper for merged branches:
- legacy: id_to_chunk[gid] -> (chunk_id, local_idx)
- current: id_to_chunk[gid] -> chunk_id (local_idx defaults to gid)
"""
mapping = self.id_to_chunk.get(global_id)
if mapping is None:
return None, None
if isinstance(mapping, (tuple, list)):
if not mapping:
return None, None
chunk_id = mapping[0]
local_idx = mapping[1] if len(mapping) > 1 else global_id
return chunk_id, local_idx
return mapping, global_id
def set_active_ids(self, active_ids):
"""切换编辑激活集合,仅保留 active_ids 对应 chunk 为动态模式。"""
target_chunks = set()
for obj_id in active_ids:
chunk_id, _ = self._resolve_chunk_and_local_idx(obj_id)
if chunk_id is not None:
target_chunks.add(chunk_id)
# Demote no-longer-active chunks. Dirty chunks are re-baked before demotion.
for chunk_id in list(self.active_chunks):
if chunk_id in target_chunks:
continue
if self.chunks[chunk_id]["dirty"]:
self._rebuild_static_chunk(chunk_id)
self._set_chunk_dynamic(chunk_id, False)
# Promote target chunks.
for chunk_id in target_chunks:
self._set_chunk_dynamic(chunk_id, True)
def bake_ids_and_collect(self, model):
"""
构建混合架构:
1) 把每个 geom 拆成可独立编辑的动态对象
2) chunk 生成 flatten 后的静态副本
"""
t0 = time.time()
self._reset_state()
geom_nodes = list(model.find_all_matches("**/+GeomNode"))
print(f"[控制器] 找到 {len(geom_nodes)} 个 GeomNode")
# Build hierarchy metadata first so UI can mirror source model tree.
self._build_scene_tree(model)
root_name = model.get_name() or "scene"
scene_root = NodePath(root_name)
pick_root = NodePath(root_name + "_pick")
self.model = scene_root
self.pick_model = pick_root
global_id = 0
for np in geom_nodes:
gnode = np.node()
owner_key = self._path_to_tree_key.get(str(np), self.tree_root_key)
world_mat = LMatrix4f(np.get_mat(model))
for gi in range(gnode.get_num_geoms()):
# Render geometry stays untouched (keep original material/color behavior).
render_geom = gnode.get_geom(gi).make_copy()
render_gnode = GeomNode(f"obj_{global_id}")
render_gnode.add_geom(render_geom, gnode.get_geom_state(gi))
# Picking geometry gets encoded ID in vertex color.
pick_geom = gnode.get_geom(gi).make_copy()
pick_vdata = pick_geom.modify_vertex_data()
self._encode_id_color(pick_vdata, global_id)
pick_gnode = GeomNode(f"pick_{global_id}")
pick_gnode.add_geom(pick_geom, gnode.get_geom_state(gi))
world_pos = world_mat.get_row3(3)
chunk_id, chunk = self._allocate_spatial_chunk(scene_root, world_pos)
obj_np = chunk["dynamic_np"].attach_new_node(render_gnode)
obj_np.set_mat(world_mat)
pick_np = pick_root.attach_new_node(pick_gnode)
pick_np.set_mat(world_mat)
chunk["members"].append(global_id)
self.id_to_chunk[global_id] = chunk_id
self.id_to_object_np[global_id] = obj_np
self.id_to_pick_np[global_id] = pick_np
self.tree_nodes[owner_key]["local_ids"].append(global_id)
self.id_to_name[global_id] = owner_key
self.global_transforms.append(LMatrix4f(world_mat))
self.position_offsets[global_id] = Vec3(0, 0, 0)
global_id += 1
t1 = time.time()
print(f"[控制器] Dynamic object build took {(t1 - t0) * 1000:.0f}ms")
for chunk_id in sorted(self.chunks):
self._rebuild_static_chunk(chunk_id)
self._set_chunk_dynamic(chunk_id, False)
t2 = time.time()
print(f"[控制器] Static chunk flatten took {(t2 - t1) * 1000:.0f}ms")
print(f"[控制器] Built {len(self.chunks)} chunks, {global_id} objects")
print(f"[控制器] Spatial chunking: cell={self.chunk_world_size:.1f}, max_members={self.chunk_size}")
# Fill per-node aggregate IDs and build deterministic preorder list for UI.
self._aggregate_tree_ids(self.tree_root_key)
self.node_list = []
self._build_tree_preorder(self.tree_root_key, self.node_list)
model.remove_node()
return global_id
def _build_pick_vertex_index(self, pick_root):
"""
Build local_id -> [(geom_node_np, geom_idx, row_indices_array)] for pick model.
This keeps GPU-picking geometry writable in sync with visible geometry edits.
"""
import numpy as np
self.pick_vertex_index = {}
if not pick_root:
return
for gn_np in pick_root.find_all_matches("**/+GeomNode"):
gnode = gn_np.node()
for gi in range(gnode.get_num_geoms()):
geom = gnode.get_geom(gi)
vdata = geom.get_vertex_data()
num_rows = vdata.get_num_rows()
if num_rows == 0:
continue
fmt = vdata.get_format()
color_col = fmt.get_column(InternalName.make("color"))
if color_col is None:
continue
color_array_idx = fmt.get_array_with(InternalName.make("color"))
color_start = color_col.get_start()
color_array_format = fmt.get_array(color_array_idx)
color_stride = color_array_format.get_stride()
color_handle = vdata.get_array(color_array_idx).get_handle()
color_raw = bytes(color_handle.get_data())
color_buf = np.frombuffer(color_raw, dtype=np.uint8).reshape(num_rows, color_stride)
num_components = color_col.get_num_components()
component_bytes = color_col.get_component_bytes()
if component_bytes == 4:
color_data = np.ndarray(
(num_rows, num_components),
dtype=np.float32,
buffer=color_buf[:, color_start:color_start + num_components * 4].tobytes()
)
r_vals = (color_data[:, 0] * 255.0 + 0.5).astype(np.int32)
g_vals = (color_data[:, 1] * 255.0 + 0.5).astype(np.int32)
elif component_bytes == 1:
color_bytes = color_buf[:, color_start:color_start + num_components].copy()
r_vals = color_bytes[:, 0].astype(np.int32)
g_vals = color_bytes[:, 1].astype(np.int32)
else:
continue
local_ids = r_vals + (g_vals << 8)
sort_idx = np.argsort(local_ids)
sorted_ids = local_ids[sort_idx]
boundaries = np.where(np.diff(sorted_ids) != 0)[0] + 1
id_groups = np.split(sort_idx, boundaries)
group_ids = sorted_ids[np.concatenate([[0], boundaries])]
for k in range(len(group_ids)):
uid = int(group_ids[k])
rows = id_groups[k]
if uid not in self.pick_vertex_index:
self.pick_vertex_index[uid] = []
self.pick_vertex_index[uid].append((gn_np, gi, rows))
def _apply_vertices_to_pick(self, local_idx, entry_idx, new_pos):
"""Mirror one transformed vertex group to pick-model geometry."""
pick_entries = self.pick_vertex_index.get(local_idx)
if not pick_entries or entry_idx >= len(pick_entries):
return
pick_gn_np, pick_gi, pick_rows = pick_entries[entry_idx]
gnode = pick_gn_np.node()
geom = gnode.modify_geom(pick_gi)
vdata = geom.modify_vertex_data()
writer = GeomVertexWriter(vdata, "vertex")
max_rows = min(len(pick_rows), len(new_pos))
for j in range(max_rows):
writer.set_row(int(pick_rows[j]))
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
def _init_local_transform_state(self):
"""Initialize transform state for each local_idx after vertex index is ready."""
self.local_transform_state = {}
@ -424,10 +963,9 @@ class ObjectController:
return local_indices
seen = set()
for global_id in global_ids:
mapping = self.id_to_chunk.get(global_id)
if not mapping:
_, local_idx = self._resolve_chunk_and_local_idx(global_id)
if local_idx is None:
continue
_, local_idx = mapping
if local_idx in seen:
continue
if local_idx not in self.vertex_index:
@ -437,7 +975,7 @@ class ObjectController:
return local_indices
def get_local_pivot(self, local_idx):
"""Get pivot for one local object (world-space center)."""
"""Get pivot for one local object (model-local center)."""
global_id = self.local_to_global_id.get(local_idx)
if global_id is None:
return Vec3(0, 0, 0)
@ -457,7 +995,8 @@ class ObjectController:
valid += 1
if valid == 0:
return Vec3(0, 0, 0)
return acc / float(valid)
center_local = acc / float(valid)
return self._local_point_to_world(center_local)
def begin_transform_session(self, local_indices):
"""Create immutable baseline snapshot for one gizmo drag session."""
@ -543,6 +1082,7 @@ class ObjectController:
for j in range(len(rows)):
writer.set_row(int(rows[j]))
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
self._apply_vertices_to_pick(local_idx, i, new_pos)
def _quat_to_np_mat3(self, quat):
"""Convert Panda3D Quat to 3x3 numpy rotation matrix."""
@ -571,7 +1111,7 @@ class ObjectController:
], dtype=np.float32)
def create_ssbo(self):
"""No SSBO needed — using RP default rendering."""
"""No SSBO needed in hybrid mode."""
return None
def move_object(self, global_id, delta):
@ -584,10 +1124,28 @@ class ObjectController:
if global_id not in self.id_to_chunk:
return
_, local_idx = self.id_to_chunk[global_id]
if local_idx not in self.vertex_index:
chunk_id, local_idx = self._resolve_chunk_and_local_idx(global_id)
if local_idx is None:
return
# Hybrid chunk mode (current) may move NodePaths directly without
# vertex_index/original_positions populated.
if local_idx not in self.vertex_index or local_idx not in self.original_positions:
obj_np = self.id_to_object_np.get(global_id)
if not obj_np or obj_np.is_empty():
return
next_pos = obj_np.get_pos() + delta
if hasattr(obj_np, "set_fluid_pos"):
obj_np.set_fluid_pos(next_pos)
else:
obj_np.set_pos(next_pos)
pick_np = self.id_to_pick_np.get(global_id)
if pick_np and not pick_np.is_empty():
pick_np.set_mat(self.model, obj_np.get_mat(self.model))
if chunk_id is not None and chunk_id in self.chunks:
self.chunks[chunk_id]["dirty"] = True
self.position_offsets[local_idx] = self.position_offsets.get(local_idx, Vec3(0)) + delta
return
# Accumulate offset
@ -611,28 +1169,35 @@ class ObjectController:
for j in range(len(rows)):
writer.set_row(int(rows[j]))
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
self._apply_vertices_to_pick(local_idx, i, new_pos)
def get_world_pos(self, global_id):
"""Get current world position of an object."""
if global_id not in self.id_to_chunk:
if not self.model:
return Vec3(0, 0, 0)
obj_np = self.id_to_object_np.get(global_id)
if obj_np and not obj_np.is_empty():
p = obj_np.get_pos(self.model)
return self._local_point_to_world(Vec3(p))
_, local_idx = self._resolve_chunk_and_local_idx(global_id)
if local_idx is None:
return Vec3(0, 0, 0)
_, local_idx = self.id_to_chunk[global_id]
original_mat = self.global_transforms[global_id]
original_pos = original_mat.get_row3(3)
offset = self.position_offsets.get(local_idx, Vec3(0))
return Vec3(original_pos) + offset
local_pos = Vec3(original_pos) + offset
return self._local_point_to_world(local_pos)
def get_object_center(self, global_id):
"""Get the original center position of an object (for rotation pivot)."""
if global_id >= len(self.global_transforms):
return Vec3(0, 0, 0)
mat = self.global_transforms[global_id]
return Vec3(mat.get_row3(3))
def get_transform(self, global_id):
"""Get original transform."""
if global_id >= len(self.global_transforms):
return LMatrix4f.ident_mat()
return self.global_transforms[global_id]

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,7 @@
# -*- coding: utf-8 -*-
"""
测试动画模型 - Panda3D应用程序
使用Panda3D引擎编辑器创建
打包
"""
from __future__ import print_function
@ -14,7 +13,6 @@ from direct.actor.Actor import Actor
from direct.showbase.ShowBaseGlobal import globalClock
from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \
Point3
from core.InfoPanelManager import InfoPanelManager
# 获取渲染管线路径
# 在文件开头添加sys导入如果还没有的话
import sys
@ -98,8 +96,6 @@ class MainApp(ShowBase):
# 加载所有脚本e
self.script_manager.load_all_scripts_from_directory()
self.info_panel_manager = InfoPanelManager(self)
try:
# 再导入controller模块
from rpcore.util.movement_controller import MovementController
@ -539,8 +535,6 @@ class MainApp(ShowBase):
video_path=video_path,
size=absolute_scale
)
elif gui_type == "info_panel":
new_element = self.info_panel_manager.onCreateSampleInfoPanel()
if "scripts" in gui_info and new_element:
self.processScripts(new_element,gui_info["scripts"])

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EG WebGL Scene</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="app">
<canvas id="scene-canvas"></canvas>
<div id="status" class="status">Loading scene...</div>
</div>
<script type="module" src="./js/viewer.js"></script>
</body>
</html>

61
templates/webgl/style.css Normal file
View File

@ -0,0 +1,61 @@
:root {
--bg: #0f1115;
--panel: rgba(16, 20, 28, 0.88);
--text: #d6dde9;
--ok: #77d0b9;
--warn: #e2b272;
--err: #f27878;
}
* {
box-sizing: border-box;
}
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: radial-gradient(circle at 20% 20%, #1b2230 0%, var(--bg) 50%, #090b10 100%);
color: var(--text);
font-family: "Segoe UI", "SF Pro Text", "PingFang SC", sans-serif;
}
#scene-canvas {
width: 100%;
height: 100%;
display: block;
}
.status {
position: fixed;
left: 16px;
bottom: 16px;
max-width: min(640px, calc(100vw - 32px));
padding: 10px 12px;
background: var(--panel);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
backdrop-filter: blur(4px);
line-height: 1.45;
white-space: pre-wrap;
font-size: 13px;
}
.status.ok {
border-color: rgba(119, 208, 185, 0.5);
color: var(--ok);
}
.status.warn {
border-color: rgba(226, 178, 114, 0.55);
color: var(--warn);
}
.status.error {
border-color: rgba(242, 120, 120, 0.65);
color: var(--err);
}

3923
templates/webgl/vendor/GLTFLoader.js vendored Normal file

File diff suppressed because it is too large Load Diff

1229
templates/webgl/vendor/OrbitControls.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

386
templates/webgl/viewer.js Normal file
View File

@ -0,0 +1,386 @@
const statusEl = document.getElementById("status");
const canvas = document.getElementById("scene-canvas");
function setStatus(message, level = "warn") {
if (!statusEl) return;
statusEl.textContent = message;
statusEl.className = `status ${level}`;
}
function rowMajorToMatrix4(THREE, m) {
const mat = new THREE.Matrix4();
mat.set(
m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11],
m[12], m[13], m[14], m[15],
);
return mat;
}
function pandaRowMajorToMatrix4(THREE, m) {
const mat = new THREE.Matrix4();
// Panda's matrix data uses row-vector convention (translation on last row).
// Three.js expects column-vector convention (translation on last column).
mat.set(
m[0], m[4], m[8], m[12],
m[1], m[5], m[9], m[13],
m[2], m[6], m[10], m[14],
m[3], m[7], m[11], m[15],
);
return mat;
}
function convertNodeMatrix(THREE, sourceMatRowMajor, basis, basisInv, matrixConvention = "panda_row_vector_row_major") {
const src = matrixConvention === "panda_row_vector_row_major"
? pandaRowMajorToMatrix4(THREE, sourceMatRowMajor)
: rowMajorToMatrix4(THREE, sourceMatRowMajor);
return basis.clone().multiply(src).multiply(basisInv);
}
function toColorArray(color, fallback = [1, 1, 1]) {
if (!Array.isArray(color) || color.length < 3) return fallback;
return [Number(color[0]) || 0, Number(color[1]) || 0, Number(color[2]) || 0];
}
function applyMaterialOverride(THREE, root, override) {
if (!override) return;
root.traverse((obj) => {
if (!obj.isMesh || !obj.material) return;
const list = Array.isArray(obj.material) ? obj.material : [obj.material];
for (const mat of list) {
if (mat.color && Array.isArray(override.base_color)) {
const [r, g, b] = override.base_color;
mat.color.setRGB(r ?? 1, g ?? 1, b ?? 1);
}
if (Object.prototype.hasOwnProperty.call(override, "roughness") && "roughness" in mat) {
mat.roughness = Number(override.roughness);
}
if (Object.prototype.hasOwnProperty.call(override, "metallic") && "metalness" in mat) {
mat.metalness = Number(override.metallic);
}
if (Object.prototype.hasOwnProperty.call(override, "opacity")) {
const opacity = THREE.MathUtils.clamp(Number(override.opacity), 0, 1);
const isTransparent = opacity < 0.999;
mat.opacity = opacity;
mat.transparent = isTransparent;
// Prevent "see-through solid mesh" when source GLTF had transparent pipeline state.
mat.depthWrite = !isTransparent;
mat.depthTest = true;
mat.blending = isTransparent ? THREE.NormalBlending : THREE.NoBlending;
if (!isTransparent && "alphaTest" in mat) {
mat.alphaTest = 0;
}
if (!isTransparent && "transmission" in mat) {
mat.transmission = 0;
}
}
mat.needsUpdate = true;
}
});
}
function textureSlotByStage(stageName) {
const key = String(stageName || "").toLowerCase();
if (key.includes("normal")) return "normalMap";
if (key.includes("rough")) return "roughnessMap";
if (key.includes("metal")) return "metalnessMap";
if (key.includes("emission") || key.includes("emissive")) return "emissiveMap";
if (key.includes("ao")) return "aoMap";
if (key.includes("alpha") || key.includes("opacity")) return "alphaMap";
return "map";
}
function applyTextureOverrides(THREE, root, textureOverrides, textureLoader) {
if (!Array.isArray(textureOverrides) || textureOverrides.length === 0) return;
const texBySlot = new Map();
for (const item of textureOverrides) {
if (!item || !item.uri) continue;
const slot = textureSlotByStage(item.stage);
if (texBySlot.has(slot)) continue;
try {
const tex = textureLoader.load(item.uri);
tex.flipY = false;
texBySlot.set(slot, tex);
} catch (err) {
console.warn("Texture load failed:", item.uri, err);
}
}
if (texBySlot.size === 0) return;
root.traverse((obj) => {
if (!obj.isMesh || !obj.material) return;
const list = Array.isArray(obj.material) ? obj.material : [obj.material];
for (const mat of list) {
for (const [slot, tex] of texBySlot.entries()) {
if (slot in mat) {
mat[slot] = tex;
}
}
mat.needsUpdate = true;
}
});
}
function directionToThree(THREE, direction, basis) {
const d = new THREE.Vector3(
Number(direction?.[0] ?? 0),
Number(direction?.[1] ?? 0),
Number(direction?.[2] ?? -1),
);
d.applyMatrix4(basis);
if (d.lengthSq() < 1e-6) d.set(0, 0, -1);
return d.normalize();
}
async function bootstrap() {
setStatus("Loading WebGL dependencies...");
let THREE;
let OrbitControls;
let GLTFLoader;
try {
THREE = await import("../vendor/three.module.min.js");
({ OrbitControls } = await import("../vendor/OrbitControls.js"));
({ GLTFLoader } = await import("../vendor/GLTFLoader.js"));
} catch (err) {
setStatus(
[
"Failed to load local Three.js vendor files.",
"Please replace vendor placeholders with official files:",
"- vendor/three.module.min.js",
"- vendor/OrbitControls.js",
"- vendor/GLTFLoader.js",
"",
String(err),
].join("\n"),
"error",
);
throw err;
}
setStatus("Loading scene manifest...");
const response = await fetch("../scene/scene_webgl.json", { cache: "no-cache" });
if (!response.ok) {
throw new Error(`Failed to load scene manifest: HTTP ${response.status}`);
}
const data = await response.json();
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: false,
});
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x11151c);
const cameraData = data.camera || {};
const camera = new THREE.PerspectiveCamera(
Number(cameraData.fov_deg ?? 80),
window.innerWidth / Math.max(1, window.innerHeight),
Number(cameraData.near ?? 0.1),
Number(cameraData.far ?? 10000),
);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 0);
const basis = rowMajorToMatrix4(
THREE,
Array.isArray(data.coordinate?.basis_matrix)
? data.coordinate.basis_matrix
: [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1],
);
const basisInv = basis.clone().invert();
const matrixConvention = String(data.coordinate?.matrix_convention || "panda_row_vector_row_major");
if (Array.isArray(cameraData.matrix_local_row_major) && cameraData.matrix_local_row_major.length === 16) {
const camMat = convertNodeMatrix(
THREE,
cameraData.matrix_local_row_major,
basis,
basisInv,
matrixConvention,
);
camera.matrix.copy(camMat);
camera.matrix.decompose(camera.position, camera.quaternion, camera.scale);
camera.matrixAutoUpdate = true;
camera.updateMatrix();
} else {
camera.position.set(0, -50, 20);
camera.lookAt(0, 0, 0);
}
const env = data.environment || {};
if (env.ambient_light) {
const c = toColorArray(env.ambient_light.color, [0.2, 0.2, 0.2]);
const amb = new THREE.AmbientLight(new THREE.Color(c[0], c[1], c[2]), Number(env.ambient_light.intensity ?? 1));
scene.add(amb);
}
if (env.directional_light) {
const c = toColorArray(env.directional_light.color, [0.8, 0.8, 0.8]);
const dirLight = new THREE.DirectionalLight(
new THREE.Color(c[0], c[1], c[2]),
Number(env.directional_light.intensity ?? 1),
);
const dir = directionToThree(THREE, env.directional_light.direction, basis);
dirLight.position.copy(dir.clone().multiplyScalar(-40));
dirLight.target.position.set(0, 0, 0);
scene.add(dirLight);
scene.add(dirLight.target);
}
const nodeMap = new Map();
const pendingModelLoads = [];
const gltfLoader = new GLTFLoader();
const textureLoader = new THREE.TextureLoader();
for (const node of Array.isArray(data.nodes) ? data.nodes : []) {
let obj;
if (node.kind === "point_light") {
const light = node.light || {};
const c = toColorArray(light.color, [1, 1, 1]);
obj = new THREE.PointLight(
new THREE.Color(c[0], c[1], c[2]),
Number(light.intensity ?? 1),
Number(light.range ?? 0),
);
} else if (node.kind === "spot_light") {
const light = node.light || {};
const c = toColorArray(light.color, [1, 1, 1]);
const angle = THREE.MathUtils.degToRad(Number(light.spot_angle_deg ?? 45));
const spot = new THREE.SpotLight(
new THREE.Color(c[0], c[1], c[2]),
Number(light.intensity ?? 1),
Number(light.range ?? 0),
angle,
1 - Number(light.inner_cone_ratio ?? 0.4),
);
spot.target.position.set(0, 0, -1);
obj = spot;
} else if (node.kind === "ground") {
const g = node.ground || {};
const width = Number(g.width ?? 100);
const height = Number(g.height ?? 100);
const m = node.material_override || {};
const bc = Array.isArray(m.base_color) ? m.base_color : [0.8, 0.8, 0.8, 1];
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color(Number(bc[0] ?? 0.8), Number(bc[1] ?? 0.8), Number(bc[2] ?? 0.8)),
roughness: Number(m.roughness ?? 1),
metalness: Number(m.metallic ?? 0),
transparent: Number(m.opacity ?? 1) < 1,
opacity: Number(m.opacity ?? 1),
side: THREE.DoubleSide,
});
obj = new THREE.Mesh(new THREE.PlaneGeometry(width, height), mat);
obj.receiveShadow = true;
} else {
obj = new THREE.Group();
const modelUri = node.model?.uri;
if (modelUri) {
const p = new Promise((resolve) => {
gltfLoader.load(
modelUri,
(gltf) => {
const root = gltf.scene || (Array.isArray(gltf.scenes) ? gltf.scenes[0] : null);
if (root) {
applyMaterialOverride(THREE, root, node.material_override || null);
applyTextureOverrides(THREE, root, node.texture_overrides || [], textureLoader);
obj.add(root);
}
resolve();
},
undefined,
(err) => {
console.warn(`Failed to load model ${modelUri}:`, err);
resolve();
},
);
});
pendingModelLoads.push(p);
}
}
obj.name = node.name || node.id || "node";
if (Array.isArray(node.matrix_local_row_major) && node.matrix_local_row_major.length === 16) {
const converted = convertNodeMatrix(THREE, node.matrix_local_row_major, basis, basisInv, matrixConvention);
obj.matrixAutoUpdate = false;
obj.matrix.copy(converted);
obj.matrix.decompose(obj.position, obj.quaternion, obj.scale);
}
nodeMap.set(node.id, obj);
}
for (const node of Array.isArray(data.nodes) ? data.nodes : []) {
const obj = nodeMap.get(node.id);
if (!obj) continue;
const parent = node.parent_id ? nodeMap.get(node.parent_id) : null;
if (parent) {
parent.add(obj);
} else {
scene.add(obj);
}
if (obj.isSpotLight && obj.target) {
obj.add(obj.target);
}
}
await Promise.all(pendingModelLoads);
const resize = () => {
const w = window.innerWidth;
const h = Math.max(1, window.innerHeight);
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
};
window.addEventListener("resize", resize);
resize();
setStatus(
`Scene ready. Nodes: ${(data.nodes || []).length}.\nUse mouse to orbit, wheel to zoom.`,
"ok",
);
const clock = new THREE.Clock();
const tick = () => {
requestAnimationFrame(tick);
const dt = clock.getDelta();
if (dt >= 0) {
controls.update();
renderer.render(scene, camera);
}
};
tick();
}
bootstrap().catch((err) => {
console.error(err);
setStatus(`Viewer bootstrap failed:\n${String(err)}`, "error");
});

1
ui/LUI/__init__.py Normal file
View File

@ -0,0 +1 @@
"""LUI split modules."""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1724
ui/LUI/lui_manager_editor.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

69
ui/LUI/lui_shared.py Normal file
View File

@ -0,0 +1,69 @@
"""Shared LUI bootstrap and imports."""
import os
import sys
from pathlib import Path
import panda3d.core as p3d
from panda3d.core import NodePath, CardMaker
from imgui_bundle import imgui, imgui_ctx
UI_DIR = Path(__file__).resolve().parents[1]
if str(UI_DIR) not in sys.path:
sys.path.insert(0, str(UI_DIR))
BUILTIN_DIR = UI_DIR / "Builtin"
if str(BUILTIN_DIR) not in sys.path:
sys.path.insert(0, str(BUILTIN_DIR))
import panda3d
panda_dir = os.path.dirname(panda3d.__file__)
if hasattr(os, "add_dll_directory"):
try:
os.add_dll_directory(panda_dir)
os.add_dll_directory(str(UI_DIR))
except Exception as e:
print(f"Warning: Failed to add DLL directory: {e}")
try:
import lui
panda3d.lui = lui
sys.modules["panda3d.lui"] = lui
from Builtin.LUIRegion import LUIRegion
from Builtin.LUIInputHandler import LUIInputHandler
from Builtin.LUIButton import LUIButton
from Builtin.LUILabel import LUILabel
from Builtin.LUIInputField import LUIInputField
from Builtin.LUISlider import LUISlider
from Builtin.LUIFrame import LUIFrame
from Builtin.LUISkin import LUIDefaultSkin
from Builtin.LUISprite import LUISprite
from Builtin.LUIObject import LUIObject
from Builtin.LUICheckbox import LUICheckbox
from Builtin.LUIProgressbar import LUIProgressbar
from Builtin.LUISelectbox import LUISelectbox
from Builtin.LUIScrollableRegion import LUIScrollableRegion
from Builtin.LUITabbedFrame import LUITabbedFrame
from Builtin.LUIVerticalLayout import LUIVerticalLayout
from Builtin.LUIHorizontalLayout import LUIHorizontalLayout
except ImportError as e:
print(f"Error: Failed to import LUI: {e}")
lui = None
LUIRegion = None
LUIInputHandler = None
LUIButton = None
LUILabel = None
LUIInputField = None
LUISlider = None
LUIFrame = None
LUIDefaultSkin = None
LUISprite = None
LUIObject = None
LUICheckbox = None
LUIProgressbar = None
LUISelectbox = None
LUIScrollableRegion = None
LUITabbedFrame = None
LUIVerticalLayout = None
LUIHorizontalLayout = None

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ from pathlib import Path
from direct.actor.Actor import Actor
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import NodePath, PartSubset
from panda3d.core import NodePath, PartSubset, Filename
class _BoundAnimationProxy:
@ -704,7 +704,22 @@ class AnimationTools:
def _try_create_actor_from_source(source, source_desc):
try:
actor = Actor(source)
resolved_source = source
if isinstance(source, (str, os.PathLike)):
src_text = os.fspath(source)
resolved_source = src_text
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
ctor = getattr(Filename, ctor_name, None)
if not ctor:
continue
try:
candidate = ctor(src_text).get_fullpath()
if candidate:
resolved_source = candidate
break
except Exception:
continue
actor = Actor(resolved_source)
# 无论是否已检测到动画名,都显式绑定一次,避免“有名字但无可播放控制”
try:
actor.bindAllAnims(allowAsyncBind=False)
@ -1139,7 +1154,10 @@ class AnimationTools:
# 路径 Actor 失败后,再尝试把文件作为普通模型加载并 autoBind
try:
loaded_model = self.loader.loadModel(p)
model_source = p
if isinstance(p, (str, os.PathLike)):
model_source = Filename.from_os_specific(os.fspath(p))
loaded_model = self.loader.loadModel(model_source)
if loaded_model and not loaded_model.isEmpty():
proxy = _try_create_autobind_proxy(loaded_model, f"文件路径({p})", owns_node=True)
if proxy:

View File

@ -256,12 +256,105 @@ class AppActions:
self.add_info_message("另存为项目(功能待实现)")
# TODO: 实现另存为对话框
# self.show_save_as_dialog = True
def _on_build_webgl_package(self):
"""处理“打包为 WebGL”菜单项。"""
if not hasattr(self, "project_manager") or not self.project_manager:
self.add_error_message("项目管理器未初始化")
return
if not self.project_manager.current_project_path:
self.add_warning_message("请先创建或打开项目再进行WebGL打包")
return
# 导出前先保存当前项目
if not self._save_project_impl():
self.add_error_message("打包前自动保存失败已取消WebGL打包")
return
current_project_path = self.project_manager.current_project_path
initial_dir = os.path.dirname(current_project_path) if current_project_path else os.getcwd()
selected_dir = self._select_directory_system_dialog("选择 WebGL 打包输出目录", initial_dir)
if not selected_dir:
dialog_error = getattr(self, "_last_directory_dialog_error", "")
if dialog_error:
# Fallback: when system dialog is unavailable (e.g. missing tkinter),
# reuse the built-in path browser so user can still choose directory.
self.add_warning_message(f"系统目录选择器不可用: {dialog_error}")
self.path_browser_mode = "webgl_build"
self.path_browser_current_path = initial_dir if os.path.isdir(initial_dir) else os.getcwd()
self.path_browser_selected_path = self.path_browser_current_path
self.show_path_browser = True
self._pending_webgl_package = True
self._refresh_path_browser()
self.add_info_message("已切换到内置路径浏览器,请选择输出目录并点击确定")
else:
self.add_info_message("已取消WebGL打包")
return
self._execute_webgl_package(selected_dir)
def _execute_webgl_package(self, selected_dir):
"""执行WebGL打包并输出消息。"""
ok = self.project_manager.buildWebGLPackage(selected_dir)
report = getattr(self.project_manager, "last_webgl_export_report", None) or {}
status = report.get("status", "failed")
out_dir = report.get("output_dir", "")
report_path = os.path.join(out_dir, "reports", "export_report.json") if out_dir else ""
if ok:
missing_count = len(report.get("missing_assets", []))
unsupported_count = len(report.get("unsupported_assets", []))
if status == "partial":
self.add_warning_message(
f"WebGL打包部分成功: 缺失资源 {missing_count},不支持资源 {unsupported_count}"
)
else:
self.add_success_message("WebGL打包成功")
if out_dir:
self.add_info_message(f"输出目录: {out_dir}")
if report_path:
self.add_info_message(f"报告: {report_path}")
else:
self.add_error_message("WebGL打包失败")
if report_path:
self.add_warning_message(f"请检查报告: {report_path}")
def _on_exit(self):
"""处理退出菜单项"""
self.add_info_message("退出应用程序")
self.userExit()
def _select_directory_system_dialog(self, title, initial_dir=""):
"""打开系统目录选择器并返回目录路径。"""
self._last_directory_dialog_error = ""
try:
import tkinter as tk
from tkinter import filedialog
if not initial_dir or (not os.path.isdir(initial_dir)):
initial_dir = os.getcwd()
root = tk.Tk()
root.withdraw()
try:
root.attributes("-topmost", True)
except Exception:
pass
selected = filedialog.askdirectory(
title=title or "选择目录",
initialdir=initial_dir,
mustexist=True,
)
root.destroy()
return os.path.normpath(selected) if selected else ""
except Exception as e:
print(f"目录选择器打开失败: {e}")
self._last_directory_dialog_error = str(e)
return ""
# ==================== 键盘事件处理函数 ====================
@ -735,6 +828,9 @@ class AppActions:
node_name = node.getName() or "未命名节点"
parent = node.getParent()
ssbo_editor = getattr(self, "ssbo_editor", None)
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
deleting_ssbo_root = bool(ssbo_model and (node == ssbo_model))
# 创建删除命令
if hasattr(self, 'command_manager') and self.command_manager:
@ -747,6 +843,12 @@ class AppActions:
print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}")
self._perform_node_cleanup(node)
node.removeNode()
if deleting_ssbo_root and ssbo_editor:
try:
ssbo_editor.on_model_deleted(node)
except Exception as e:
print(f"[SSBO] 删除模型后清理失败: {e}")
print(f"[删除] 成功删除节点: {node_name}")
return True
@ -921,6 +1023,8 @@ class AppActions:
# 检查场景文件
scene_file = os.path.join(project_path, "scenes", "scene.bam")
if os.path.exists(scene_file):
if getattr(self, "use_ssbo_mouse_picking", False) and callable(getattr(self, "_import_model_for_runtime", None)):
self.use_ssbo_scene_import = True
# 加载场景
try:
if self.scene_manager.loadScene(scene_file):
@ -1027,17 +1131,12 @@ class AppActions:
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False):
"""Import model through the active runtime path.
SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model).
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
Legacy mode: load via SceneManager.
"""
if prefer_scene_manager:
if hasattr(self, 'scene_manager') and self.scene_manager:
return self.scene_manager.importModel(file_path)
return None
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
try:
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
# Clear selection/gizmo first, then import as an additional scene model.
if hasattr(self, 'selection') and self.selection:
try:
self.selection.clearSelection()
@ -1047,32 +1146,24 @@ class AppActions:
except Exception:
pass
# Remove legacy scene-manager models to avoid duplicate rendering
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
for m in list(self.scene_manager.models):
try:
if m and not m.isEmpty():
m.removeNode()
except Exception:
pass
self.scene_manager.models = []
# Replace previous SSBO model
old_model = getattr(self.ssbo_editor, 'model', None)
if old_model is not None:
try:
if not old_model.isEmpty():
old_model.removeNode()
except Exception:
pass
self.ssbo_editor.load_model(file_path)
model_np = getattr(self.ssbo_editor, 'model', None)
normalized_import_path = str(file_path).replace("\\", "/").lower()
is_project_scene_bam = (
normalized_import_path.endswith(".bam")
and "/scenes/" in normalized_import_path
)
# Keep legacy ray-pick fallback usable by adding a collision body.
if model_np:
try:
from scene import util as scene_util
normalized_model_path = scene_util.normalize_model_path(file_path)
except Exception:
normalized_model_path = file_path
# Apply vital tags manually since SSBO overrides SceneManager loader
model_np.setTag("model_path", file_path)
model_np.setTag("model_path", normalized_model_path)
model_np.setTag("original_path", file_path)
model_np.setTag("saved_model_path", normalized_model_path)
model_np.setTag("is_model_root", "1")
model_np.setTag("is_scene_element", "1")
model_np.setTag("file", os.path.basename(file_path))
@ -1080,8 +1171,14 @@ class AppActions:
if hasattr(self, 'scene_manager') and self.scene_manager:
try:
self.scene_manager.setupCollision(model_np)
self.scene_manager._processModelAnimations(model_np)
if not is_project_scene_bam:
self.scene_manager.setupCollision(model_np)
self.scene_manager._processModelAnimations(model_np)
else:
model_np.setTag("has_animations", "false")
model_np.setTag("has_animations_checked", "true")
if hasattr(self.scene_manager, "models") and model_np not in self.scene_manager.models:
self.scene_manager.models.append(model_np)
except Exception as e:
print(f"[SSBO] setup components failed: {e}")
return model_np
@ -1131,33 +1228,35 @@ class AppActions:
self.add_error_message("模型导入失败")
return None
if hasattr(self.scene_manager, 'processMaterials'):
self.scene_manager.processMaterials(model_node)
if show_info_message:
self.add_info_message("已应用默认材质")
try:
model_node.clearMaterial()
model_node.clearTexture()
# SSBO 模式下保留原始材质/纹理,避免模型发黑。
if not getattr(self, "use_ssbo_mouse_picking", False):
if hasattr(self.scene_manager, 'processMaterials'):
self.scene_manager.processMaterials(model_node)
if show_info_message:
self.add_info_message("已应用默认材质")
try:
color = model_node.getColor()
if color and len(color) >= 4 and color == (1, 1, 1, 1):
model_node.clearMaterial()
model_node.clearTexture()
if hasattr(self.scene_manager, 'processMaterials'):
self.scene_manager.processMaterials(model_node)
try:
color = model_node.getColor()
if color and len(color) >= 4 and color == (1, 1, 1, 1):
model_node.setColor(0.8, 0.8, 0.8, 1.0)
elif not color:
model_node.setColor(0.8, 0.8, 0.8, 1.0)
except Exception:
model_node.setColor(0.8, 0.8, 0.8, 1.0)
elif not color:
model_node.setColor(0.8, 0.8, 0.8, 1.0)
except Exception:
model_node.setColor(0.8, 0.8, 0.8, 1.0)
except Exception as e:
self.add_warning_message(f"材质处理警告: {e}")
except Exception as e:
self.add_warning_message(f"材质处理警告: {e}")
if set_origin:
model_node.setPos(0, 0, 0)
if hasattr(self.scene_manager, 'models'):
if hasattr(self.scene_manager, 'models') and model_node not in self.scene_manager.models:
self.scene_manager.models.append(model_node)
if select_model and hasattr(self, 'selection') and self.selection:
@ -1172,7 +1271,7 @@ class AppActions:
def _on_import_model(self):
"""处理导入模型菜单项"""
self.add_info_message("打开导入模型对话框")
self.add_info_message("打开系统文件选择器")
self.show_import_dialog = True

View File

@ -78,57 +78,6 @@ class CreateActions:
except Exception as e:
self.add_error_message(f"创建平面失败: {str(e)}")
def _on_create_3d_text(self):
"""创建3D文本"""
self.show_3d_text_dialog = True
def _on_create_3d_image(self):
"""创建3D图片"""
self.show_3d_image_dialog = True
def _on_create_gui_button(self):
"""创建GUI按钮"""
self.show_gui_button_dialog = True
def _on_create_gui_label(self):
"""创建GUI标签"""
self.show_gui_label_dialog = True
def _on_create_gui_entry(self):
"""创建GUI输入框"""
self.show_gui_entry_dialog = True
def _on_create_gui_image(self):
"""创建GUI图片"""
self.show_gui_image_dialog = True
def _on_create_video_screen(self):
"""创建视频屏幕"""
self.show_video_screen_dialog = True
def _on_create_2d_video_screen(self):
"""创建2D视频屏幕"""
self.show_2d_video_screen_dialog = True
def _on_create_spherical_video(self):
"""创建球形视频"""
self.show_spherical_video_dialog = True
def _on_create_virtual_screen(self):
"""创建虚拟屏幕"""
self.show_virtual_screen_dialog = True
def _on_create_spot_light(self):
"""创建聚光灯"""
self.show_spot_light_dialog = True

View File

@ -265,88 +265,80 @@ class DialogPanels:
def _draw_import_dialog(self):
"""绘制导入模型对话框"""
"""使用系统文件选择器导入模型。"""
if not self.show_import_dialog:
return
# 设置对话框标志
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
# 获取屏幕尺寸,居中显示对话框
display_size = imgui.get_io().display_size
dialog_width = 600
dialog_height = 500
imgui.set_next_window_size((dialog_width, dialog_height))
imgui.set_next_window_pos(
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
)
with imgui_ctx.begin("导入模型", True, flags) as window:
if not window.opened:
self.show_import_dialog = False
return
imgui.text("选择要导入的模型文件")
imgui.separator()
# 文件路径输入
imgui.text("文件路径:")
changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512)
if changed:
self.import_file_path = file_path
imgui.same_line()
if imgui.button("浏览..."):
self.path_browser_mode = "import_model"
self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd()
self.show_path_browser = True
self._refresh_path_browser()
imgui.separator()
# 支持的格式说明
imgui.text("支持的文件格式:")
formats_text = ", ".join(self.supported_formats)
imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text)
imgui.separator()
# 文件预览信息
if self.import_file_path and os.path.exists(self.import_file_path):
file_size = os.path.getsize(self.import_file_path)
imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
file_ext = os.path.splitext(self.import_file_path)[1].lower()
if file_ext in self.supported_formats:
imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持")
else:
imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式")
else:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径")
imgui.separator()
# 按钮区域
can_import = (self.import_file_path and
os.path.exists(self.import_file_path) and
os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats)
# 根据状态设置按钮颜色
if can_import:
if imgui.button("导入"):
self._import_model()
self.show_import_dialog = False
else:
# 禁用状态的按钮(灰色显示)
imgui.push_style_color(imgui.Col_.button, (0.3, 0.3, 0.3, 1.0))
imgui.button("导入")
imgui.pop_style_color()
imgui.same_line()
if imgui.button("取消"):
self.show_import_dialog = False
# 立即关闭标记,防止每帧重复弹窗
self.show_import_dialog = False
selected_path = self._select_model_file_system_dialog()
if not selected_path:
self.add_info_message("已取消导入模型")
return
self.import_file_path = selected_path
self._import_model()
def _select_model_file_system_dialog(self):
"""弹出系统文件选择器并返回模型路径。"""
try:
import tkinter as tk
from tkinter import filedialog
initial_dir = os.path.dirname(self.import_file_path) if self.import_file_path else ""
if not initial_dir or (not os.path.isdir(initial_dir)):
candidates = [
os.path.join(os.getcwd(), "Resources", "models"),
os.path.join(os.getcwd(), "Resources"),
os.getcwd(),
]
initial_dir = next((p for p in candidates if os.path.isdir(p)), os.getcwd())
normalized_exts = []
for ext in getattr(self, "supported_formats", []):
ext = str(ext).strip().lower()
if not ext:
continue
if not ext.startswith("."):
ext = f".{ext}"
if ext not in normalized_exts:
normalized_exts.append(ext)
model_patterns = " ".join(f"*{ext}" for ext in normalized_exts) or "*.*"
filetypes = [
("模型文件", model_patterns),
("All Files", "*.*"),
]
root = tk.Tk()
root.withdraw()
try:
root.attributes("-topmost", True)
except Exception:
pass
selected_path = filedialog.askopenfilename(
title="选择要导入的模型文件",
initialdir=initial_dir,
filetypes=filetypes,
)
root.destroy()
if not selected_path:
return ""
selected_path = os.path.normpath(selected_path)
file_ext = os.path.splitext(selected_path)[1].lower()
if normalized_exts and file_ext not in normalized_exts:
self.add_error_message(f"不支持的文件格式: {file_ext}")
return ""
self.add_info_message(f"已选择文件: {selected_path}")
return selected_path
except Exception as e:
self.add_error_message(f"打开系统文件选择器失败: {e}")
return ""
def _refresh_path_browser(self):
@ -410,435 +402,16 @@ class DialogPanels:
# 导入模型模式:使用选择的文件路径
self.import_file_path = self.path_browser_selected_path
self.add_info_message(f"已选择文件: {self.import_file_path}")
elif self.path_browser_mode == "webgl_build":
output_dir = self.path_browser_current_path
self.add_info_message(f"已选择WebGL输出目录: {output_dir}")
if getattr(self, "_pending_webgl_package", False):
self._pending_webgl_package = False
if hasattr(self, "_execute_webgl_package"):
self._execute_webgl_package(output_dir)
except Exception as e:
self.add_error_message(f"应用路径失败: {e}")
# ==================== 创建功能对话框实现 ====================
def _draw_gui_button_dialog(self):
"""绘制GUI按钮创建对话框"""
if not self.show_gui_button_dialog:
return
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
with imgui_ctx.begin("创建GUI按钮", self.show_gui_button_dialog, flags) as window:
if not window:
self.show_gui_button_dialog = False
return
# 初始化参数
if 'button_text' not in self.dialog_params:
self.dialog_params['button_text'] = "按钮"
if 'button_pos' not in self.dialog_params:
self.dialog_params['button_pos'] = [0.0, 0.0, 0.0]
if 'button_size' not in self.dialog_params:
self.dialog_params['button_size'] = [0.1, 0.1, 0.1]
imgui.text("GUI按钮参数设置")
imgui.separator()
# 文本输入
changed, self.dialog_params['button_text'] = imgui.input_text("按钮文本", self.dialog_params['button_text'], 256)
# 位置输入
changed, x = imgui.input_float("X坐标", self.dialog_params['button_pos'][0])
if changed:
self.dialog_params['button_pos'][0] = x
changed, y = imgui.input_float("Y坐标", self.dialog_params['button_pos'][1])
if changed:
self.dialog_params['button_pos'][1] = y
changed, z = imgui.input_float("Z坐标", self.dialog_params['button_pos'][2])
if changed:
self.dialog_params['button_pos'][2] = z
# 大小输入
changed, width = imgui.input_float("宽度", self.dialog_params['button_size'][0])
if changed:
self.dialog_params['button_size'][0] = width
changed, height = imgui.input_float("高度", self.dialog_params['button_size'][1])
if changed:
self.dialog_params['button_size'][1] = height
imgui.separator()
# 按钮
if imgui.button("创建"):
try:
pos = tuple(self.dialog_params['button_pos'])
text = self.dialog_params['button_text']
size = tuple(self.dialog_params['button_size'][:2])
result = self.createGUIButton(pos, text, size)
self.add_success_message(f"GUI按钮创建成功: {text}")
self.show_gui_button_dialog = False
except Exception as e:
self.add_error_message(f"创建GUI按钮失败: {str(e)}")
imgui.same_line()
if imgui.button("取消"):
self.show_gui_button_dialog = False
def _draw_gui_label_dialog(self):
"""绘制GUI标签创建对话框"""
if not self.show_gui_label_dialog:
return
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
with imgui_ctx.begin("创建GUI标签", self.show_gui_label_dialog, flags) as window:
if not window:
self.show_gui_label_dialog = False
return
# 初始化参数
if 'label_text' not in self.dialog_params:
self.dialog_params['label_text'] = "标签"
if 'label_pos' not in self.dialog_params:
self.dialog_params['label_pos'] = [0.0, 0.0, 0.0]
if 'label_size' not in self.dialog_params:
self.dialog_params['label_size'] = [0.1, 0.1, 0.1]
imgui.text("GUI标签参数设置")
imgui.separator()
# 文本输入
changed, self.dialog_params['label_text'] = imgui.input_text("标签文本", self.dialog_params['label_text'], 256)
# 位置输入
changed, x = imgui.input_float("X坐标", self.dialog_params['label_pos'][0])
if changed:
self.dialog_params['label_pos'][0] = x
changed, y = imgui.input_float("Y坐标", self.dialog_params['label_pos'][1])
if changed:
self.dialog_params['label_pos'][1] = y
changed, z = imgui.input_float("Z坐标", self.dialog_params['label_pos'][2])
if changed:
self.dialog_params['label_pos'][2] = z
# 大小输入
changed, width = imgui.input_float("宽度", self.dialog_params['label_size'][0])
if changed:
self.dialog_params['label_size'][0] = width
changed, height = imgui.input_float("高度", self.dialog_params['label_size'][1])
if changed:
self.dialog_params['label_size'][1] = height
imgui.separator()
# 按钮
if imgui.button("创建"):
try:
pos = tuple(self.dialog_params['label_pos'])
text = self.dialog_params['label_text']
size = tuple(self.dialog_params['label_size'][:2])
result = self.createGUILabel(pos, text, size)
self.add_success_message(f"GUI标签创建成功: {text}")
self.show_gui_label_dialog = False
except Exception as e:
self.add_error_message(f"创建GUI标签失败: {str(e)}")
imgui.same_line()
if imgui.button("取消"):
self.show_gui_label_dialog = False
def _draw_gui_entry_dialog(self):
"""绘制GUI输入框创建对话框"""
if not self.show_gui_entry_dialog:
return
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
with imgui_ctx.begin("创建GUI输入框", self.show_gui_entry_dialog, flags) as window:
if not window:
self.show_gui_entry_dialog = False
return
# 初始化参数
if 'entry_pos' not in self.dialog_params:
self.dialog_params['entry_pos'] = [0.0, 0.0, 0.0]
if 'entry_size' not in self.dialog_params:
self.dialog_params['entry_size'] = [0.2, 0.05, 0.1]
imgui.text("GUI输入框参数设置")
imgui.separator()
# 位置输入
changed, x = imgui.input_float("X坐标", self.dialog_params['entry_pos'][0])
if changed:
self.dialog_params['entry_pos'][0] = x
changed, y = imgui.input_float("Y坐标", self.dialog_params['entry_pos'][1])
if changed:
self.dialog_params['entry_pos'][1] = y
changed, z = imgui.input_float("Z坐标", self.dialog_params['entry_pos'][2])
if changed:
self.dialog_params['entry_pos'][2] = z
# 大小输入
changed, width = imgui.input_float("宽度", self.dialog_params['entry_size'][0])
if changed:
self.dialog_params['entry_size'][0] = width
changed, height = imgui.input_float("高度", self.dialog_params['entry_size'][1])
if changed:
self.dialog_params['entry_size'][1] = height
imgui.separator()
# 按钮
if imgui.button("创建"):
try:
pos = tuple(self.dialog_params['entry_pos'])
size = tuple(self.dialog_params['entry_size'][:2])
result = self.createGUIEntry(pos, size)
self.add_success_message("GUI输入框创建成功")
self.show_gui_entry_dialog = False
except Exception as e:
self.add_error_message(f"创建GUI输入框失败: {str(e)}")
imgui.same_line()
if imgui.button("取消"):
self.show_gui_entry_dialog = False
def _draw_gui_image_dialog(self):
"""绘制GUI图片创建对话框"""
if not self.show_gui_image_dialog:
return
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
with imgui_ctx.begin("创建GUI图片", self.show_gui_image_dialog, flags) as window:
if not window:
self.show_gui_image_dialog = False
return
# 初始化参数
if 'image_pos' not in self.dialog_params:
self.dialog_params['image_pos'] = [0.0, 0.0, 0.0]
if 'image_size' not in self.dialog_params:
self.dialog_params['image_size'] = [0.2, 0.2, 0.1]
if 'image_path' not in self.dialog_params:
self.dialog_params['image_path'] = ""
imgui.text("GUI图片参数设置")
imgui.separator()
# 位置输入
changed, x = imgui.input_float("X坐标", self.dialog_params['image_pos'][0])
if changed:
self.dialog_params['image_pos'][0] = x
changed, y = imgui.input_float("Y坐标", self.dialog_params['image_pos'][1])
if changed:
self.dialog_params['image_pos'][1] = y
changed, z = imgui.input_float("Z坐标", self.dialog_params['image_pos'][2])
if changed:
self.dialog_params['image_pos'][2] = z
# 大小输入
changed, width = imgui.input_float("宽度", self.dialog_params['image_size'][0])
if changed:
self.dialog_params['image_size'][0] = width
changed, height = imgui.input_float("高度", self.dialog_params['image_size'][1])
if changed:
self.dialog_params['image_size'][1] = height
# 图片路径
changed, self.dialog_params['image_path'] = imgui.input_text("图片路径", self.dialog_params['image_path'], 512)
imgui.separator()
# 按钮
if imgui.button("创建"):
try:
pos = tuple(self.dialog_params['image_pos'])
size = tuple(self.dialog_params['image_size'][:2])
image_path = self.dialog_params['image_path']
result = self.createGUIImage(pos, image_path, size)
self.add_success_message("GUI图片创建成功")
self.show_gui_image_dialog = False
except Exception as e:
self.add_error_message(f"创建GUI图片失败: {str(e)}")
imgui.same_line()
if imgui.button("取消"):
self.show_gui_image_dialog = False
def _draw_3d_text_dialog(self):
"""绘制3D文本创建对话框"""
if not self.show_3d_text_dialog:
return
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
with imgui_ctx.begin("创建3D文本", self.show_3d_text_dialog, flags) as window:
if not window:
self.show_3d_text_dialog = False
return
# 初始化参数
if 'text3d_text' not in self.dialog_params:
self.dialog_params['text3d_text'] = "3D文本"
if 'text3d_pos' not in self.dialog_params:
self.dialog_params['text3d_pos'] = [0.0, 0.0, 0.0]
if 'text3d_size' not in self.dialog_params:
self.dialog_params['text3d_size'] = 1.0
imgui.text("3D文本参数设置")
imgui.separator()
# 文本输入
changed, self.dialog_params['text3d_text'] = imgui.input_text("文本内容", self.dialog_params['text3d_text'], 256)
# 位置输入
changed, x = imgui.input_float("X坐标", self.dialog_params['text3d_pos'][0])
if changed:
self.dialog_params['text3d_pos'][0] = x
changed, y = imgui.input_float("Y坐标", self.dialog_params['text3d_pos'][1])
if changed:
self.dialog_params['text3d_pos'][1] = y
changed, z = imgui.input_float("Z坐标", self.dialog_params['text3d_pos'][2])
if changed:
self.dialog_params['text3d_pos'][2] = z
# 大小输入
changed, self.dialog_params['text3d_size'] = imgui.input_float("文本大小", self.dialog_params['text3d_size'])
imgui.separator()
# 按钮
if imgui.button("创建"):
try:
pos = tuple(self.dialog_params['text3d_pos'])
text = self.dialog_params['text3d_text']
size = self.dialog_params['text3d_size']
result = self.create3DText(pos, text, size)
self.add_success_message(f"3D文本创建成功: {text}")
self.show_3d_text_dialog = False
except Exception as e:
self.add_error_message(f"创建3D文本失败: {str(e)}")
imgui.same_line()
if imgui.button("取消"):
self.show_3d_text_dialog = False
def _draw_3d_image_dialog(self):
"""绘制3D图片创建对话框"""
if not self.show_3d_image_dialog:
return
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
with imgui_ctx.begin("创建3D图片", self.show_3d_image_dialog, flags) as window:
if not window:
self.show_3d_image_dialog = False
return
# 初始化参数
if 'image3d_pos' not in self.dialog_params:
self.dialog_params['image3d_pos'] = [0.0, 0.0, 0.0]
if 'image3d_size' not in self.dialog_params:
self.dialog_params['image3d_size'] = [1.0, 1.0, 1.0]
if 'image3d_path' not in self.dialog_params:
self.dialog_params['image3d_path'] = ""
imgui.text("3D图片参数设置")
imgui.separator()
# 位置输入
changed, x = imgui.input_float("X坐标", self.dialog_params['image3d_pos'][0])
if changed:
self.dialog_params['image3d_pos'][0] = x
changed, y = imgui.input_float("Y坐标", self.dialog_params['image3d_pos'][1])
if changed:
self.dialog_params['image3d_pos'][1] = y
changed, z = imgui.input_float("Z坐标", self.dialog_params['image3d_pos'][2])
if changed:
self.dialog_params['image3d_pos'][2] = z
# 大小输入
changed, width = imgui.input_float("宽度", self.dialog_params['image3d_size'][0])
if changed:
self.dialog_params['image3d_size'][0] = width
changed, height = imgui.input_float("高度", self.dialog_params['image3d_size'][1])
if changed:
self.dialog_params['image3d_size'][1] = height
# 图片路径
changed, self.dialog_params['image3d_path'] = imgui.input_text("图片路径", self.dialog_params['image3d_path'], 512)
imgui.separator()
# 按钮
if imgui.button("创建"):
try:
pos = tuple(self.dialog_params['image3d_pos'])
size = tuple(self.dialog_params['image3d_size'][:2])
image_path = self.dialog_params['image3d_path']
result = self.create3DImage(pos, image_path, size)
self.add_success_message("3D图片创建成功")
self.show_3d_image_dialog = False
except Exception as e:
self.add_error_message(f"创建3D图片失败: {str(e)}")
imgui.same_line()
if imgui.button("取消"):
self.show_3d_image_dialog = False
# 添加其他创建对话框的占位符方法
def _draw_video_screen_dialog(self):
"""绘制视频屏幕创建对话框"""
if not self.show_video_screen_dialog:
return
self.show_video_screen_dialog = False
self.add_info_message("视频屏幕创建功能开发中...")
def _draw_2d_video_screen_dialog(self):
"""绘制2D视频屏幕创建对话框"""
if not self.show_2d_video_screen_dialog:
return
self.show_2d_video_screen_dialog = False
self.add_info_message("2D视频屏幕创建功能开发中...")
def _draw_spherical_video_dialog(self):
"""绘制球形视频创建对话框"""
if not self.show_spherical_video_dialog:
return
self.show_spherical_video_dialog = False
self.add_info_message("球形视频创建功能开发中...")
def _draw_virtual_screen_dialog(self):
"""绘制虚拟屏幕创建对话框"""
if not self.show_virtual_screen_dialog:
return
self.show_virtual_screen_dialog = False
self.add_info_message("虚拟屏幕创建功能开发中...")
def _draw_spot_light_dialog(self):
"""绘制聚光灯创建对话框"""
@ -1457,5 +1030,3 @@ class DialogPanels:
except Exception as e:
print(f"刷新高度图浏览器时出错: {e}")
self.heightmap_browser_items = []
# ==================== 导入功能实现 ====================

View File

@ -40,7 +40,7 @@ class EditorPanels:
def _ensure_web_panel_state(self):
if not hasattr(self.app, "web_panel_url_input") or not self.app.web_panel_url_input:
self.app.web_panel_url_input = "https://www.example.com"
self.app.web_panel_url_input = "https://www.baidu.com"
if not hasattr(self.app, "_imgui_webview"):
self.app._imgui_webview = None
if not hasattr(self.app, "_imgui_webview_tex_id"):
@ -171,6 +171,8 @@ class EditorPanels:
self.app._on_save_project()
if imgui.menu_item("另存为", "", False, True)[1]:
self.app._on_save_as_project()
if imgui.menu_item("打包为 WebGL", "", False, True)[1]:
self.app._on_build_webgl_package()
imgui.separator()
if imgui.menu_item("退出", "Alt+F4", False, True)[1]:
self.app._on_exit()
@ -217,33 +219,33 @@ class EditorPanels:
self.app._on_create_plane()
# 3D GUI子菜单
with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
if three_d_gui_menu:
if imgui.menu_item("3D文本", "", False, True)[1]:
self.app._on_create_3d_text()
if imgui.menu_item("3D图片", "", False, True)[1]:
self.app._on_create_3d_image()
# with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
# if three_d_gui_menu:
# if imgui.menu_item("3D文本", "", False, True)[1]:
# self.app._on_create_3d_text()
# if imgui.menu_item("3D图片", "", False, True)[1]:
# self.app._on_create_3d_image()
# GUI子菜单
with imgui_ctx.begin_menu("GUI") as gui_menu:
if gui_menu:
if imgui.menu_item("创建按钮", "", False, True)[1]:
self.app._on_create_gui_button()
if imgui.menu_item("创建标签", "", False, True)[1]:
self.app._on_create_gui_label()
if imgui.menu_item("创建输入框", "", False, True)[1]:
self.app._on_create_gui_entry()
if imgui.menu_item("创建图片", "", False, True)[1]:
self.app._on_create_gui_image()
imgui.separator()
if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
self.app._on_create_video_screen()
if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
self.app._on_create_2d_video_screen()
if imgui.menu_item("创建球形视频", "", False, True)[1]:
self.app._on_create_spherical_video()
if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
self.app._on_create_virtual_screen()
# # GUI子菜单
# with imgui_ctx.begin_menu("GUI") as gui_menu:
# if gui_menu:
# if imgui.menu_item("创建按钮", "", False, True)[1]:
# self.app._on_create_gui_button()
# if imgui.menu_item("创建标签", "", False, True)[1]:
# self.app._on_create_gui_label()
# if imgui.menu_item("创建输入框", "", False, True)[1]:
# self.app._on_create_gui_entry()
# if imgui.menu_item("创建图片", "", False, True)[1]:
# self.app._on_create_gui_image()
# imgui.separator()
# if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
# self.app._on_create_video_screen()
# if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
# self.app._on_create_2d_video_screen()
# if imgui.menu_item("创建球形视频", "", False, True)[1]:
# self.app._on_create_spherical_video()
# if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
# self.app._on_create_virtual_screen()
# 光源子菜单
with imgui_ctx.begin_menu("光源") as light_menu:
@ -278,10 +280,10 @@ class EditorPanels:
# 信息面板子菜单
with imgui_ctx.begin_menu("信息面板") as info_panel_menu:
if info_panel_menu:
if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
self.app._on_create_2d_sample_panel()
if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
self.app._on_create_3d_sample_panel()
# if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
# self.app._on_create_2d_sample_panel()
# if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
# self.app._on_create_3d_sample_panel()
if imgui.menu_item("Web面板", "", False, True)[1]:
self.app._on_create_web_panel()
@ -318,10 +320,10 @@ class EditorPanels:
imgui.separator()
# 编辑工具
if imgui.menu_item("光照编辑", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("光照编辑")
if imgui.menu_item("图形编辑", "", False, True)[1]:
self.app.tool_manager.setCurrentTool("图形编辑")
# if imgui.menu_item("光照编辑", "", False, True)[1]:
# self.app.tool_manager.setCurrentTool("光照编辑")
# if imgui.menu_item("图形编辑", "", False, True)[1]:
# self.app.tool_manager.setCurrentTool("图形编辑")
imgui.separator()
@ -610,8 +612,33 @@ class EditorPanels:
# 处理节点选择
if imgui.is_item_clicked():
# In SSBO mode, clicking model root should finally bind gizmo to
# SSBO group proxy (not legacy model root). So run legacy
# selection first, then force SSBO root selection at the end.
force_ssbo_root_key = None
ssbo_editor_ref = None
try:
ssbo_editor = getattr(self.app, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
root_key = getattr(controller, "tree_root_key", None) if controller else None
if (
ssbo_editor and controller and ssbo_model and node == ssbo_model
and root_key and root_key in controller.tree_nodes
):
force_ssbo_root_key = root_key
ssbo_editor_ref = ssbo_editor
except Exception:
pass
if hasattr(self.app, 'selection') and self.app.selection:
self.app.selection.updateSelection(node)
if force_ssbo_root_key and ssbo_editor_ref:
try:
ssbo_editor_ref.select_node(force_ssbo_root_key)
except Exception:
pass
# Clear LUI selection when a scene node is selected
if hasattr(self.app, 'lui_manager'):
self.app.lui_manager.selected_index = -1
@ -655,7 +682,7 @@ class EditorPanels:
imgui.pop_style_color()
def _draw_ssbo_virtual_children(self, node):
"""Draw SSBO controller nodes as virtual children for scene tree."""
"""Draw SSBO controller tree_nodes as virtual children for scene tree."""
ssbo_editor = getattr(self.app, "ssbo_editor", None)
if not ssbo_editor:
return False
@ -664,66 +691,59 @@ class EditorPanels:
if not model or model != node or not controller:
return False
tree_root = controller.get_virtual_hierarchy() if hasattr(controller, "get_virtual_hierarchy") else None
if not tree_root or not tree_root.get("children"):
root_key = getattr(controller, "tree_root_key", None)
if not root_key or root_key not in controller.tree_nodes:
imgui.text_disabled("(无可用子节点)")
return True
for name in sorted(tree_root["children"].keys()):
child = tree_root["children"][name]
self._draw_ssbo_virtual_tree_node(ssbo_editor, child, "ssbo_root")
root_node = controller.tree_nodes[root_key]
if not root_node["children"]:
imgui.text_disabled("(无可用子节点)")
return True
for child_key in root_node["children"]:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
return True
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, tree_node, unique_id_prefix, depth=0):
"""Recursively draw virtual SSBO hierarchy in scene tree."""
if not tree_node:
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
node_data = controller.tree_nodes.get(key)
if not node_data:
return
path = tree_node.get("path", "")
display = tree_node.get("display_name") or tree_node.get("name") or path
leaf_key = tree_node.get("leaf_key")
group_key = tree_node.get("group_key")
children = tree_node.get("children", {}) or {}
label = f"{display}##{unique_id_prefix}_{path}"
# Skip redundant wrapper nodes (e.g. ROOT), show their children instead.
if controller.should_hide_tree_node(key):
for child_key in node_data["children"]:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
return
# Leaf: selectable to trigger SSBO selection.
if not children and leaf_key:
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
display = controller.display_names.get(key, key)
obj_count = len(controller.name_to_ids.get(key, []))
children = node_data["children"]
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
if not children:
# Leaf node: selectable
label = f"{display} ({obj_count})##{key}"
if imgui.selectable(label, is_selected)[0]:
ssbo_editor.select_node(leaf_key)
ssbo_editor.select_node(key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
return
# Non-leaf: tree node only for hierarchy display.
opened = imgui.tree_node(label)
# Clicking non-leaf row selects its aggregate group so parent transform affects children.
if group_key and imgui.is_item_clicked(0):
ssbo_editor.select_node(group_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if opened:
# If this node is also a selectable leaf, render selectable entry first.
if group_key:
is_group_selected = (getattr(ssbo_editor, "selected_name", None) == group_key)
if imgui.selectable(f"[整体] {display}##group_{unique_id_prefix}_{path}", is_group_selected)[0]:
ssbo_editor.select_node(group_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if leaf_key:
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
if imgui.selectable(f"[节点] {display}##leaf_{unique_id_prefix}_{path}", is_selected)[0]:
ssbo_editor.select_node(leaf_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
for child_name in sorted(children.keys()):
self._draw_ssbo_virtual_tree_node(
ssbo_editor,
children[child_name],
unique_id_prefix,
depth + 1,
)
imgui.tree_pop()
else:
# Branch node: tree node
flags = imgui.TreeNodeFlags_.open_on_arrow
if is_selected:
flags |= imgui.TreeNodeFlags_.selected
label = f"{display} ({obj_count})##{key}"
opened = imgui.tree_node_ex(label, flags)
if imgui.is_item_clicked(0):
ssbo_editor.select_node(key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
if opened:
for child_key in children:
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
imgui.tree_pop()
def _show_node_context_menu(self, node, name, node_type):
"""显示节点右键菜单"""
self.app._context_menu_node = True
@ -2211,7 +2231,10 @@ class EditorPanels:
# 删除对象
imgui.same_line()
if imgui.button("删除"):
if hasattr(self, 'selection') and self.selection:
if hasattr(self.app, "_on_delete") and callable(self.app._on_delete):
self.app._on_delete()
elif hasattr(self, 'selection') and self.selection and hasattr(self.selection, "deleteSelectedNode"):
# 兼容旧选择系统接口
self.selection.deleteSelectedNode()
@ -2439,4 +2462,3 @@ class EditorPanels:
except Exception as e:
print(f"绘制着色模型面板失败: {e}")

View File

@ -339,51 +339,6 @@ class ObjectFactory:
except Exception as e:
print(f"✗ 创建平面失败: {e}")
return None
def create2DSamplePanel(self):
"""创建2D示例面板"""
try:
from core.InfoPanelManager import createSampleInfoPanel
result = createSampleInfoPanel(self.render)
return result
except Exception as e:
print(f"创建2D示例面板失败: {e}")
return None
def create3DSamplePanel(self):
"""创建3D实例面板"""
try:
if hasattr(self, 'info_panel_manager') and self.info_panel_manager:
# 创建3D信息面板
panel_id = f"3d_sample_{int(time.time())}"
result = self.info_panel_manager.create3DInfoPanel(
panel_id=panel_id,
position=(0, 0, 2),
size=(1.0, 0.6),
bg_color=(0.15, 0.25, 0.35, 0.95),
border_color=(0.3, 0.5, 0.7, 1.0),
title_color=(0.7, 0.9, 1.0, 1.0),
content_color=(0.95, 0.95, 0.95, 1.0)
)
# 添加示例内容
if result:
sample_data = {
"标题": "3D信息面板",
"状态": "运行中",
"创建时间": time.strftime("%Y-%m-%d %H:%M:%S"),
"位置": f"X:0, Y:0, Z:2"
}
self.info_panel_manager.updatePanelContent(panel_id, content=sample_data)
return result
return None
except Exception as e:
print(f"创建3D示例面板失败: {e}")
return None
def createWebPanel(self, url="https://www.example.com"):
"""创建Web面板"""

View File

@ -0,0 +1,753 @@
"""Delegated panel/runtime methods for MyWorld."""
class PanelDelegates:
def _draw_menu_bar(self):
self.editor_panels.draw_menu_bar()
def _draw_toolbar(self):
self.editor_panels.draw_toolbar()
def _draw_scene_tree(self, *args, **kwargs):
return self.editor_panels._draw_scene_tree(*args, **kwargs)
def _build_scene_tree(self, *args, **kwargs):
return self.editor_panels._build_scene_tree(*args, **kwargs)
def _draw_scene_node(self, *args, **kwargs):
return self.editor_panels._draw_scene_node(*args, **kwargs)
def _show_node_context_menu(self, *args, **kwargs):
return self.editor_panels._show_node_context_menu(*args, **kwargs)
def _draw_resource_manager(self, *args, **kwargs):
return self.editor_panels._draw_resource_manager(*args, **kwargs)
def _draw_property_panel(self, *args, **kwargs):
return self.editor_panels._draw_property_panel(*args, **kwargs)
def _draw_web_panel(self, *args, **kwargs):
return self.editor_panels._draw_web_panel(*args, **kwargs)
def _draw_node_properties(self, *args, **kwargs):
return self.editor_panels._draw_node_properties(*args, **kwargs)
def _getActor(self, *args, **kwargs):
return self.animation_tools._getActor(*args, **kwargs)
def _getModelFormat(self, *args, **kwargs):
return self.animation_tools._getModelFormat(*args, **kwargs)
def _processAnimationNames(self, *args, **kwargs):
return self.animation_tools._processAnimationNames(*args, **kwargs)
def _isLikelyBoneGroup(self, *args, **kwargs):
return self.animation_tools._isLikelyBoneGroup(*args, **kwargs)
def _analyzeAnimationQuality(self, *args, **kwargs):
return self.animation_tools._analyzeAnimationQuality(*args, **kwargs)
def _playAnimation(self, *args, **kwargs):
return self.animation_tools._playAnimation(*args, **kwargs)
def _pauseAnimation(self, *args, **kwargs):
return self.animation_tools._pauseAnimation(*args, **kwargs)
def _stopAnimation(self, *args, **kwargs):
return self.animation_tools._stopAnimation(*args, **kwargs)
def _loopAnimation(self, *args, **kwargs):
return self.animation_tools._loopAnimation(*args, **kwargs)
def _setAnimationSpeed(self, *args, **kwargs):
return self.animation_tools._setAnimationSpeed(*args, **kwargs)
def _clear_animation_cache(self, *args, **kwargs):
return self.animation_tools._clear_animation_cache(*args, **kwargs)
def _get_node_type_from_node(self, node):
"""从节点判断其类型"""
# 检查是否为GUI元素
if hasattr(node, 'getPythonTag') and node.getPythonTag('gui_element'):
return "GUI元素"
# 检查是否为光源
node_name = node.getName() or ""
if "light" in node_name.lower() or "Light" in node_name:
return "光源"
# 检查是否为相机
if "camera" in node_name.lower() or "Camera" in node_name:
return "相机"
# 检查是否为模型
if hasattr(self, 'scene_manager') and self.scene_manager:
if hasattr(self.scene_manager, 'models') and node in self.scene_manager.models:
return "模型"
# 默认为几何体
return "几何体"
def _draw_status_badges(self, *args, **kwargs):
return self.editor_panels._draw_status_badges(*args, **kwargs)
def _draw_transform_properties(self, *args, **kwargs):
return self.editor_panels._draw_transform_properties(*args, **kwargs)
def _draw_gui_properties(self, *args, **kwargs):
return self.editor_panels._draw_gui_properties(*args, **kwargs)
def _draw_light_properties(self, *args, **kwargs):
return self.editor_panels._draw_light_properties(*args, **kwargs)
def _draw_model_properties(self, *args, **kwargs):
return self.editor_panels._draw_model_properties(*args, **kwargs)
def _draw_animation_properties(self, *args, **kwargs):
return self.editor_panels._draw_animation_properties(*args, **kwargs)
def _draw_collision_properties(self, *args, **kwargs):
return self.editor_panels._draw_collision_properties(*args, **kwargs)
def _draw_shape_specific_parameters(self, *args, **kwargs):
return self.editor_panels._draw_shape_specific_parameters(*args, **kwargs)
def _draw_sphere_parameters(self, *args, **kwargs):
return self.editor_panels._draw_sphere_parameters(*args, **kwargs)
def _draw_box_parameters(self, *args, **kwargs):
return self.editor_panels._draw_box_parameters(*args, **kwargs)
def _draw_capsule_parameters(self, *args, **kwargs):
return self.editor_panels._draw_capsule_parameters(*args, **kwargs)
def _draw_plane_parameters(self, *args, **kwargs):
return self.editor_panels._draw_plane_parameters(*args, **kwargs)
def _draw_property_actions(self, *args, **kwargs):
return self.editor_panels._draw_property_actions(*args, **kwargs)
def _draw_appearance_properties(self, *args, **kwargs):
return self.editor_panels._draw_appearance_properties(*args, **kwargs)
def _draw_material_properties(self, *args, **kwargs):
return self.editor_panels._draw_material_properties(*args, **kwargs)
def _draw_shading_model_panel(self, *args, **kwargs):
return self.editor_panels._draw_shading_model_panel(*args, **kwargs)
def _apply_gui_font(self, *args, **kwargs):
return self.property_helpers._apply_gui_font(*args, **kwargs)
def _apply_gui_font_size(self, *args, **kwargs):
return self.property_helpers._apply_gui_font_size(*args, **kwargs)
def _apply_gui_font_style(self, *args, **kwargs):
return self.property_helpers._apply_gui_font_style(*args, **kwargs)
def _has_collision(self, *args, **kwargs):
return self.property_helpers._has_collision(*args, **kwargs)
def _get_current_collision_shape(self, *args, **kwargs):
return self.property_helpers._get_current_collision_shape(*args, **kwargs)
def _get_current_collision_shape_type(self, *args, **kwargs):
return self.property_helpers._get_current_collision_shape_type(*args, **kwargs)
def _get_collision_position_offset(self, *args, **kwargs):
return self.property_helpers._get_collision_position_offset(*args, **kwargs)
def _is_collision_visible(self, *args, **kwargs):
return self.property_helpers._is_collision_visible(*args, **kwargs)
def _add_collision_to_node(self, *args, **kwargs):
return self.property_helpers._add_collision_to_node(*args, **kwargs)
def _remove_collision_from_node(self, *args, **kwargs):
return self.property_helpers._remove_collision_from_node(*args, **kwargs)
def _toggle_collision_visibility(self, *args, **kwargs):
return self.property_helpers._toggle_collision_visibility(*args, **kwargs)
def _update_collision_position(self, *args, **kwargs):
return self.property_helpers._update_collision_position(*args, **kwargs)
def _get_shape_type_from_name(self, *args, **kwargs):
return self.property_helpers._get_shape_type_from_name(*args, **kwargs)
def _get_sphere_radius(self, *args, **kwargs):
return self.property_helpers._get_sphere_radius(*args, **kwargs)
def _update_sphere_radius(self, *args, **kwargs):
return self.property_helpers._update_sphere_radius(*args, **kwargs)
def _get_box_size(self, *args, **kwargs):
return self.property_helpers._get_box_size(*args, **kwargs)
def _update_box_size(self, *args, **kwargs):
return self.property_helpers._update_box_size(*args, **kwargs)
def _get_capsule_radius(self, *args, **kwargs):
return self.property_helpers._get_capsule_radius(*args, **kwargs)
def _update_capsule_radius(self, *args, **kwargs):
return self.property_helpers._update_capsule_radius(*args, **kwargs)
def _get_capsule_height(self, *args, **kwargs):
return self.property_helpers._get_capsule_height(*args, **kwargs)
def _update_capsule_height(self, *args, **kwargs):
return self.property_helpers._update_capsule_height(*args, **kwargs)
def _get_plane_normal(self, *args, **kwargs):
return self.property_helpers._get_plane_normal(*args, **kwargs)
def _update_plane_normal(self, *args, **kwargs):
return self.property_helpers._update_plane_normal(*args, **kwargs)
def _manual_collision_detection(self, *args, **kwargs):
return self.property_helpers._manual_collision_detection(*args, **kwargs)
def _update_node_name(self, *args, **kwargs):
return self.property_helpers._update_node_name(*args, **kwargs)
def _get_material_base_color(self, *args, **kwargs):
return self.property_helpers._get_material_base_color(*args, **kwargs)
def _update_material_base_color(self, *args, **kwargs):
return self.property_helpers._update_material_base_color(*args, **kwargs)
def _update_material_roughness(self, *args, **kwargs):
return self.property_helpers._update_material_roughness(*args, **kwargs)
def _update_material_metallic(self, *args, **kwargs):
return self.property_helpers._update_material_metallic(*args, **kwargs)
def _update_material_ior(self, *args, **kwargs):
return self.property_helpers._update_material_ior(*args, **kwargs)
def _apply_material_preset(self, *args, **kwargs):
return self.property_helpers._apply_material_preset(*args, **kwargs)
def _apply_material_to_node(self, *args, **kwargs):
return self.property_helpers._apply_material_to_node(*args, **kwargs)
def _reset_material(self, *args, **kwargs):
return self.property_helpers._reset_material(*args, **kwargs)
def _select_texture_for_material(self, *args, **kwargs):
return self.property_helpers._select_texture_for_material(*args, **kwargs)
def _apply_texture_to_material(self, *args, **kwargs):
return self.property_helpers._apply_texture_to_material(*args, **kwargs)
def _clear_all_textures(self, *args, **kwargs):
return self.property_helpers._clear_all_textures(*args, **kwargs)
def _display_current_textures(self, *args, **kwargs):
return self.property_helpers._display_current_textures(*args, **kwargs)
def _update_shading_model(self, *args, **kwargs):
return self.property_helpers._update_shading_model(*args, **kwargs)
def _update_transparency(self, *args, **kwargs):
return self.property_helpers._update_transparency(*args, **kwargs)
def _draw_texture_file_dialog(self, *args, **kwargs):
return self.property_helpers._draw_texture_file_dialog(*args, **kwargs)
def start_transform_monitoring(self, *args, **kwargs):
return self.property_helpers.start_transform_monitoring(*args, **kwargs)
def stop_transform_monitoring(self, *args, **kwargs):
return self.property_helpers.stop_transform_monitoring(*args, **kwargs)
def _update_last_transform_values(self, *args, **kwargs):
return self.property_helpers._update_last_transform_values(*args, **kwargs)
def _check_transform_changes(self, *args, **kwargs):
return self.property_helpers._check_transform_changes(*args, **kwargs)
def update_transform_monitoring(self, *args, **kwargs):
return self.property_helpers.update_transform_monitoring(*args, **kwargs)
def show_color_picker(self, *args, **kwargs):
return self.property_helpers.show_color_picker(*args, **kwargs)
def _draw_color_picker(self, *args, **kwargs):
return self.property_helpers._draw_color_picker(*args, **kwargs)
def _apply_color_selection(self, *args, **kwargs):
return self.property_helpers._apply_color_selection(*args, **kwargs)
def _draw_color_button(self, *args, **kwargs):
return self.property_helpers._draw_color_button(*args, **kwargs)
def _refresh_available_fonts(self, *args, **kwargs):
return self.property_helpers._refresh_available_fonts(*args, **kwargs)
def show_font_selector(self, *args, **kwargs):
return self.property_helpers.show_font_selector(*args, **kwargs)
def _draw_font_selector(self, *args, **kwargs):
return self.property_helpers._draw_font_selector(*args, **kwargs)
def _apply_font_selection(self, *args, **kwargs):
return self.property_helpers._apply_font_selection(*args, **kwargs)
def _draw_font_selector_button(self, *args, **kwargs):
return self.property_helpers._draw_font_selector_button(*args, **kwargs)
def _draw_console(self, *args, **kwargs):
return self.script_panels._draw_console(*args, **kwargs)
def _draw_script_panel(self, *args, **kwargs):
return self.script_panels._draw_script_panel(*args, **kwargs)
def _draw_script_status_group(self, *args, **kwargs):
return self.script_panels._draw_script_status_group(*args, **kwargs)
def _draw_create_script_group(self, *args, **kwargs):
return self.script_panels._draw_create_script_group(*args, **kwargs)
def _draw_available_scripts_group(self, *args, **kwargs):
return self.script_panels._draw_available_scripts_group(*args, **kwargs)
def _draw_script_mounting_group(self, *args, **kwargs):
return self.script_panels._draw_script_mounting_group(*args, **kwargs)
def _toggle_hot_reload(self, *args, **kwargs):
return self.app_actions._toggle_hot_reload(*args, **kwargs)
def _create_new_script(self, *args, **kwargs):
return self.app_actions._create_new_script(*args, **kwargs)
def _refresh_scripts_list(self, *args, **kwargs):
return self.app_actions._refresh_scripts_list(*args, **kwargs)
def _reload_all_scripts(self, *args, **kwargs):
return self.app_actions._reload_all_scripts(*args, **kwargs)
def _on_script_selected(self, *args, **kwargs):
return self.app_actions._on_script_selected(*args, **kwargs)
def _edit_script(self, *args, **kwargs):
return self.app_actions._edit_script(*args, **kwargs)
def _mount_script_to_selected(self, *args, **kwargs):
return self.app_actions._mount_script_to_selected(*args, **kwargs)
def _unmount_script_from_selected(self, *args, **kwargs):
return self.app_actions._unmount_script_from_selected(*args, **kwargs)
def _on_new_project(self, *args, **kwargs):
return self.app_actions._on_new_project(*args, **kwargs)
def _on_open_project(self, *args, **kwargs):
return self.app_actions._on_open_project(*args, **kwargs)
def _on_save_project(self, *args, **kwargs):
return self.app_actions._on_save_project(*args, **kwargs)
def _on_save_as_project(self, *args, **kwargs):
return self.app_actions._on_save_as_project(*args, **kwargs)
def _on_build_webgl_package(self, *args, **kwargs):
return self.app_actions._on_build_webgl_package(*args, **kwargs)
def _execute_webgl_package(self, *args, **kwargs):
return self.app_actions._execute_webgl_package(*args, **kwargs)
def _on_exit(self, *args, **kwargs):
return self.app_actions._on_exit(*args, **kwargs)
def _on_ctrl_pressed(self, *args, **kwargs):
return self.app_actions._on_ctrl_pressed(*args, **kwargs)
def _on_ctrl_released(self, *args, **kwargs):
return self.app_actions._on_ctrl_released(*args, **kwargs)
def _on_alt_pressed(self, *args, **kwargs):
return self.app_actions._on_alt_pressed(*args, **kwargs)
def _on_alt_released(self, *args, **kwargs):
return self.app_actions._on_alt_released(*args, **kwargs)
def _on_n_pressed(self, *args, **kwargs):
return self.app_actions._on_n_pressed(*args, **kwargs)
def _on_o_pressed(self, *args, **kwargs):
return self.app_actions._on_o_pressed(*args, **kwargs)
def _on_f4_pressed(self, *args, **kwargs):
return self.app_actions._on_f4_pressed(*args, **kwargs)
def _on_delete_pressed(self, *args, **kwargs):
return self.app_actions._on_delete_pressed(*args, **kwargs)
def _on_escape_pressed(self, *args, **kwargs):
return self.app_actions._on_escape_pressed(*args, **kwargs)
def _on_wheel_up(self, *args, **kwargs):
return self.app_actions._on_wheel_up(*args, **kwargs)
def _on_wheel_down(self, *args, **kwargs):
return self.app_actions._on_wheel_down(*args, **kwargs)
def _is_mouse_over_imgui(self, *args, **kwargs):
return self.app_actions._is_mouse_over_imgui(*args, **kwargs)
def processImGuiMouseClick(self, *args, **kwargs):
return self.app_actions.processImGuiMouseClick(*args, **kwargs)
def add_message(self, *args, **kwargs):
return self.app_actions.add_message(*args, **kwargs)
def add_success_message(self, *args, **kwargs):
return self.app_actions.add_success_message(*args, **kwargs)
def add_error_message(self, *args, **kwargs):
return self.app_actions.add_error_message(*args, **kwargs)
def add_warning_message(self, *args, **kwargs):
return self.app_actions.add_warning_message(*args, **kwargs)
def add_info_message(self, *args, **kwargs):
return self.app_actions.add_info_message(*args, **kwargs)
def _on_undo(self, *args, **kwargs):
return self.app_actions._on_undo(*args, **kwargs)
def _on_redo(self, *args, **kwargs):
return self.app_actions._on_redo(*args, **kwargs)
def _on_copy(self, *args, **kwargs):
return self.app_actions._on_copy(*args, **kwargs)
def _on_cut(self, *args, **kwargs):
return self.app_actions._on_cut(*args, **kwargs)
def _on_paste(self, *args, **kwargs):
return self.app_actions._on_paste(*args, **kwargs)
def _on_delete(self, *args, **kwargs):
return self.app_actions._on_delete(*args, **kwargs)
def _delete_node(self, *args, **kwargs):
return self.app_actions._delete_node(*args, **kwargs)
def _perform_node_cleanup(self, *args, **kwargs):
return self.app_actions._perform_node_cleanup(*args, **kwargs)
def _create_new_project(self, *args, **kwargs):
return self.app_actions._create_new_project(*args, **kwargs)
def _open_project_path(self, *args, **kwargs):
return self.app_actions._open_project_path(*args, **kwargs)
def _save_project_impl(self, *args, **kwargs):
return self.app_actions._save_project_impl(*args, **kwargs)
def _open_project_impl(self, *args, **kwargs):
return self.app_actions._open_project_impl(*args, **kwargs)
def _create_new_project_impl(self, *args, **kwargs):
return self.app_actions._create_new_project_impl(*args, **kwargs)
def _update_window_title(self, *args, **kwargs):
return self.app_actions._update_window_title(*args, **kwargs)
def _import_model_for_runtime(self, *args, **kwargs):
return self.app_actions._import_model_for_runtime(*args, **kwargs)
def _on_import_model(self, *args, **kwargs):
return self.app_actions._on_import_model(*args, **kwargs)
def _import_model(self, *args, **kwargs):
return self.app_actions._import_model(*args, **kwargs)
def _import_model_with_menu_logic(self, *args, **kwargs):
return self.app_actions._import_model_with_menu_logic(*args, **kwargs)
def _draw_new_project_dialog(self, *args, **kwargs):
return self.dialog_panels._draw_new_project_dialog(*args, **kwargs)
def _draw_open_project_dialog(self, *args, **kwargs):
return self.dialog_panels._draw_open_project_dialog(*args, **kwargs)
def _draw_path_browser(self, *args, **kwargs):
return self.dialog_panels._draw_path_browser(*args, **kwargs)
def _draw_import_dialog(self, *args, **kwargs):
return self.dialog_panels._draw_import_dialog(*args, **kwargs)
def _refresh_path_browser(self, *args, **kwargs):
return self.dialog_panels._refresh_path_browser(*args, **kwargs)
def _apply_selected_path(self, *args, **kwargs):
return self.dialog_panels._apply_selected_path(*args, **kwargs)
def _draw_spot_light_dialog(self, *args, **kwargs):
return self.dialog_panels._draw_spot_light_dialog(*args, **kwargs)
def _draw_point_light_dialog(self, *args, **kwargs):
return self.dialog_panels._draw_point_light_dialog(*args, **kwargs)
def _draw_terrain_dialog(self, *args, **kwargs):
return self.dialog_panels._draw_terrain_dialog(*args, **kwargs)
def _draw_script_dialog(self, *args, **kwargs):
return self.dialog_panels._draw_script_dialog(*args, **kwargs)
def _draw_script_browser(self, *args, **kwargs):
return self.dialog_panels._draw_script_browser(*args, **kwargs)
def _refresh_script_browser(self, *args, **kwargs):
return self.dialog_panels._refresh_script_browser(*args, **kwargs)
def _draw_heightmap_browser(self, *args, **kwargs):
return self.dialog_panels._draw_heightmap_browser(*args, **kwargs)
def _refresh_heightmap_browser(self, *args, **kwargs):
return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs)
def setup_drag_drop_support(self):
"""初始化拖拽支持。"""
return self.model_drag_drop.setup_drag_drop_support()
def _is_point_in_resource_manager(self, drop_pos):
"""判断投放坐标是否命中资源管理器窗口。"""
return self.model_drag_drop.is_point_in_resource_manager(drop_pos)
def _resolve_resource_drop_target_dir(self, drop_pos):
"""根据投放坐标解析资源管理器中的目标目录。"""
return self.model_drag_drop.resolve_resource_drop_target_dir(drop_pos)
def _process_external_drop_events(self):
"""处理外部拖入(系统文件拖拽)事件。"""
return self.model_drag_drop.process_external_drop_events()
def _handle_external_drop(self, file_paths, drop_pos=None):
"""把外部拖入文件分发到资源管理器或场景。"""
return self.model_drag_drop.handle_external_drop(file_paths, drop_pos)
def add_dragged_file(self, file_path):
"""添加拖拽的文件"""
return self.model_drag_drop.add_dragged_file(file_path)
def clear_dragged_files(self):
"""清空拖拽文件列表"""
return self.model_drag_drop.clear_dragged_files()
def process_dragged_files(self):
"""处理拖拽的文件"""
return self.model_drag_drop.process_dragged_files()
def _import_model_from_path(self, file_path):
"""从路径导入模型的内部方法"""
return self.model_drag_drop.import_model_from_path(file_path)
def _draw_drag_drop_interface(self, *args, **kwargs):
return self.interaction_panels._draw_drag_drop_interface(*args, **kwargs)
def _handle_drag_drop_completion(self, *args, **kwargs):
return self.interaction_panels._handle_drag_drop_completion(*args, **kwargs)
def _draw_drag_overlay(self, *args, **kwargs):
return self.interaction_panels._draw_drag_overlay(*args, **kwargs)
def _draw_drag_status(self, *args, **kwargs):
return self.interaction_panels._draw_drag_status(*args, **kwargs)
def _draw_context_menus(self, *args, **kwargs):
return self.interaction_panels._draw_context_menus(*args, **kwargs)
def _delete_node_simple(self, *args, **kwargs):
return self.interaction_panels._delete_node_simple(*args, **kwargs)
def _copy_node(self, *args, **kwargs):
return self.interaction_panels._copy_node(*args, **kwargs)
def _on_create_empty_object(self, *args, **kwargs):
return self.create_actions._on_create_empty_object(*args, **kwargs)
def _on_create_cube(self, *args, **kwargs):
return self.create_actions._on_create_cube(*args, **kwargs)
def _on_create_sphere(self, *args, **kwargs):
return self.create_actions._on_create_sphere(*args, **kwargs)
def _on_create_cylinder(self, *args, **kwargs):
return self.create_actions._on_create_cylinder(*args, **kwargs)
def _on_create_plane(self, *args, **kwargs):
return self.create_actions._on_create_plane(*args, **kwargs)
def _on_create_3d_text(self, *args, **kwargs):
return self.create_actions._on_create_3d_text(*args, **kwargs)
def _on_create_3d_image(self, *args, **kwargs):
return self.create_actions._on_create_3d_image(*args, **kwargs)
def _on_create_gui_button(self, *args, **kwargs):
return self.create_actions._on_create_gui_button(*args, **kwargs)
def _on_create_gui_label(self, *args, **kwargs):
return self.create_actions._on_create_gui_label(*args, **kwargs)
def _on_create_gui_entry(self, *args, **kwargs):
return self.create_actions._on_create_gui_entry(*args, **kwargs)
def _on_create_gui_image(self, *args, **kwargs):
return self.create_actions._on_create_gui_image(*args, **kwargs)
def _on_create_video_screen(self, *args, **kwargs):
return self.create_actions._on_create_video_screen(*args, **kwargs)
def _on_create_2d_video_screen(self, *args, **kwargs):
return self.create_actions._on_create_2d_video_screen(*args, **kwargs)
def _on_create_spherical_video(self, *args, **kwargs):
return self.create_actions._on_create_spherical_video(*args, **kwargs)
def _on_create_virtual_screen(self, *args, **kwargs):
return self.create_actions._on_create_virtual_screen(*args, **kwargs)
def _on_create_spot_light(self, *args, **kwargs):
return self.create_actions._on_create_spot_light(*args, **kwargs)
def _on_create_point_light(self, *args, **kwargs):
return self.create_actions._on_create_point_light(*args, **kwargs)
def _on_create_flat_terrain(self, *args, **kwargs):
return self.create_actions._on_create_flat_terrain(*args, **kwargs)
def _on_create_heightmap_terrain(self, *args, **kwargs):
return self.create_actions._on_create_heightmap_terrain(*args, **kwargs)
def _on_create_script(self, *args, **kwargs):
return self.create_actions._on_create_script(*args, **kwargs)
def _on_load_script(self, *args, **kwargs):
return self.create_actions._on_load_script(*args, **kwargs)
def _on_reload_all_scripts(self, *args, **kwargs):
return self.create_actions._on_reload_all_scripts(*args, **kwargs)
def _on_open_scripts_manager(self, *args, **kwargs):
return self.create_actions._on_open_scripts_manager(*args, **kwargs)
def _on_create_2d_sample_panel(self, *args, **kwargs):
return self.create_actions._on_create_2d_sample_panel(*args, **kwargs)
def _on_create_3d_sample_panel(self, *args, **kwargs):
return self.create_actions._on_create_3d_sample_panel(*args, **kwargs)
def _on_create_web_panel(self, *args, **kwargs):
return self.create_actions._on_create_web_panel(*args, **kwargs)
def createEmptyObject(self, *args, **kwargs):
return self.object_factory.createEmptyObject(*args, **kwargs)
def create3DText(self, *args, **kwargs):
return self.object_factory.create3DText(*args, **kwargs)
def create3DImage(self, *args, **kwargs):
return self.object_factory.create3DImage(*args, **kwargs)
def createCube(self, *args, **kwargs):
return self.object_factory.createCube(*args, **kwargs)
def createSphere(self, *args, **kwargs):
return self.object_factory.createSphere(*args, **kwargs)
def createCylinder(self, *args, **kwargs):
return self.object_factory.createCylinder(*args, **kwargs)
def createPlane(self, *args, **kwargs):
return self.object_factory.createPlane(*args, **kwargs)
def create2DSamplePanel(self, *args, **kwargs):
return self.object_factory.create2DSamplePanel(*args, **kwargs)
def create3DSamplePanel(self, *args, **kwargs):
return self.object_factory.create3DSamplePanel(*args, **kwargs)
def createWebPanel(self, *args, **kwargs):
return self.object_factory.createWebPanel(*args, **kwargs)
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
return self.runtime_actions.createGUIButton(pos, text, size)
def _create_simple_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1):
return self.runtime_actions._create_simple_gui_button(pos, text, size)
def _on_gui_button_click(self):
return self.runtime_actions._on_gui_button_click()
def _toggle_vr_mode(self):
return self.runtime_actions._toggle_vr_mode()
def _exit_vr_mode(self):
return self.runtime_actions._exit_vr_mode()
def _show_vr_status(self):
return self.runtime_actions._show_vr_status()
def _show_vr_settings(self):
return self.runtime_actions._show_vr_settings()
def _show_vr_performance_report(self):
return self.runtime_actions._show_vr_performance_report()
def _get_chinese_font(self):
return self.runtime_actions._get_chinese_font()
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
return self.runtime_actions.createGUILabel(pos, text, size)
def _create_simple_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08):
return self.runtime_actions._create_simple_gui_label(pos, text, size)
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
return self.runtime_actions.createGUIEntry(pos, placeholder, size)
def _create_simple_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
return self.runtime_actions._create_simple_gui_entry(pos, placeholder, size)
def createGUIImage(self, pos=(0, 0, 0), image_path=None, size=2):
return self.runtime_actions.createGUIImage(pos, image_path, size)
def _create_simple_gui_image(self, pos=(0, 0, 0), image_path=None, size=2):
return self.runtime_actions._create_simple_gui_image(pos, image_path, size)
def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None):
return self.runtime_actions.createVideoScreen(pos, size, video_path)
def create2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
return self.runtime_actions.create2DVideoScreen(pos, size, video_path)
def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None):
return self.runtime_actions.createSphericalVideo(pos, radius, video_path)
def createVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
return self.runtime_actions.createVirtualScreen(pos, size, text)
def createSpotLight(self, pos=(0, 0, 5)):
return self.runtime_actions.createSpotLight(pos)
def createPointLight(self, pos=(0, 0, 5)):
return self.runtime_actions.createPointLight(pos)
def createFlatTerrain(self, size=(10, 10), resolution=129):
return self.runtime_actions.createFlatTerrain(size, resolution)
def createTerrainFromHeightMap(self, heightmap_path, scale=(1.0, 1.0, 10.0)):
return self.runtime_actions.createTerrainFromHeightMap(heightmap_path, scale)
def createScript(self, script_name, template="basic"):
return self.runtime_actions.createScript(script_name, template)
def loadScript(self, script_path):
return self.runtime_actions.loadScript(script_path)

View File

@ -0,0 +1,396 @@
import os
from pathlib import Path
class RuntimeActions:
"""Runtime-facing creation and utility actions extracted from main world."""
def __init__(self, app):
self.app = app
def __getattr__(self, name):
return getattr(self.app, name)
def __setattr__(self, name, value):
if name == "app" or name in self.__dict__ or hasattr(type(self), name):
object.__setattr__(self, name, value)
else:
setattr(self.app, name, value)
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self._create_simple_gui_button(pos, text, size)
return None
except Exception as e:
print(f"创建GUI按钮失败: {e}")
return None
def _create_simple_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建简单的GUI按钮不依赖QT树形控件"""
try:
from direct.gui.DirectGui import DirectButton
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
font = self._get_chinese_font()
if isinstance(size, (list, tuple)) and len(size) >= 2:
scale_value = size[0]
else:
scale_value = size
button = DirectButton(
text=text,
pos=gui_pos,
scale=scale_value,
text_font=font,
command=self._on_gui_button_click
)
button_wrapper = type("GUIElement", (), {})()
button_wrapper.node = button
button_wrapper.name = text
button_wrapper.gui_type = "GUI_BUTTON"
button_wrapper.position = pos
button_wrapper.size = size
self.gui_manager.gui_elements.append(button_wrapper)
print(f"✓ GUI按钮创建成功: {text}")
return button_wrapper
except Exception as e:
print(f"✗ 创建简单GUI按钮失败: {e}")
return None
def _on_gui_button_click(self):
"""GUI按钮点击事件处理"""
print("GUI按钮被点击了")
def _toggle_vr_mode(self):
"""切换VR模式"""
if self.vr_manager:
if self.vr_manager.is_enabled():
self._exit_vr_mode()
else:
self.vr_manager.enable()
self.add_info_message("已进入VR模式")
else:
self.add_error_message("VR管理器未初始化")
def _exit_vr_mode(self):
"""退出VR模式"""
if self.vr_manager:
self.vr_manager.disable()
self.add_info_message("已退出VR模式")
def _show_vr_status(self):
"""显示VR状态"""
if self.vr_manager:
status = "已启用" if self.vr_manager.is_enabled() else "未启用"
self.add_info_message(f"VR状态: {status}")
if self.vr_manager.is_enabled():
devices = self.vr_manager.get_connected_devices()
if devices:
self.add_info_message(f"连接的设备: {', '.join(devices)}")
else:
self.add_info_message("未检测到VR设备")
else:
self.add_error_message("VR管理器未初始化")
def _show_vr_settings(self):
"""显示VR设置"""
if self.vr_manager:
self.add_info_message("VR设置对话框待实现")
else:
self.add_error_message("VR管理器未初始化")
def _show_vr_performance_report(self):
"""显示VR性能报告"""
if self.vr_manager and self.vr_manager.is_enabled():
report = self.vr_manager.get_performance_report()
self.add_info_message(f"VR性能报告: {report}")
else:
self.add_info_message("VR未启用或管理器未初始化")
def _get_chinese_font(self):
"""获取中文字体"""
try:
from panda3d.core import TextNode
font_paths = [
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/System/Library/Fonts/PingFang.ttc",
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/msyh.ttc",
]
for font_path in font_paths:
if Path(font_path).exists():
try:
font = self.loader.loadFont(font_path)
print(f"✓ 为GUI加载中文字体成功: {font_path}")
return font
except Exception:
print(f"⚠️ 字体加载失败,尝试下一个: {font_path}")
continue
print("⚠️ 无法加载中文字体,使用默认字体")
return TextNode.getDefaultFont()
except Exception as e:
print(f"⚠️ 获取中文字体失败: {e}")
from panda3d.core import TextNode
return TextNode.getDefaultFont()
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self._create_simple_gui_label(pos, text, size)
return None
except Exception as e:
print(f"创建GUI标签失败: {e}")
return None
def _create_simple_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建简单的GUI标签不依赖QT树形控件"""
try:
from direct.gui.DirectGui import DirectLabel
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
font = self._get_chinese_font()
if isinstance(size, (list, tuple)) and len(size) >= 2:
scale_value = size[0]
else:
scale_value = size
label = DirectLabel(
text=text,
pos=gui_pos,
scale=scale_value,
text_font=font,
text_fg=(1, 1, 1, 1)
)
label_wrapper = type("GUIElement", (), {})()
label_wrapper.node = label
label_wrapper.name = text
label_wrapper.gui_type = "GUI_LABEL"
label_wrapper.position = pos
label_wrapper.size = size
self.gui_manager.gui_elements.append(label_wrapper)
print(f"✓ GUI标签创建成功: {text}")
return label_wrapper
except Exception as e:
print(f"✗ 创建简单GUI标签失败: {e}")
return None
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self._create_simple_gui_entry(pos, placeholder, size)
return None
except Exception as e:
print(f"创建GUI输入框失败: {e}")
return None
def _create_simple_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建简单的GUI输入框不依赖QT树形控件"""
try:
from direct.gui.DirectGui import DirectEntry
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
font = self._get_chinese_font()
if isinstance(size, (list, tuple)) and len(size) >= 2:
scale_value = size[0]
else:
scale_value = size
entry = DirectEntry(
text=placeholder,
pos=gui_pos,
scale=scale_value,
text_font=font,
width=20,
numLines=1,
focus=1
)
entry_wrapper = type("GUIElement", (), {})()
entry_wrapper.node = entry
entry_wrapper.name = placeholder
entry_wrapper.gui_type = "GUI_ENTRY"
entry_wrapper.position = pos
entry_wrapper.size = size
self.gui_manager.gui_elements.append(entry_wrapper)
print(f"✓ GUI输入框创建成功: {placeholder}")
return entry_wrapper
except Exception as e:
print(f"✗ 创建简单GUI输入框失败: {e}")
return None
def createGUIImage(self, pos=(0, 0, 0), image_path=None, size=2):
"""创建2D GUI图片"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self._create_simple_gui_image(pos, image_path, size)
return None
except Exception as e:
print(f"创建GUI图片失败: {e}")
return None
def _create_simple_gui_image(self, pos=(0, 0, 0), image_path=None, size=2):
"""创建简单的GUI图片不依赖QT树形控件"""
try:
from direct.gui.DirectGui import DirectFrame
from panda3d.core import Filename
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
if isinstance(size, (list, tuple)) and len(size) >= 2:
scale_value = size[0]
else:
scale_value = size
if image_path and os.path.exists(image_path):
tex = self.loader.loadTexture(Filename.fromOsSpecific(image_path))
image = DirectFrame(
image=tex,
pos=gui_pos,
scale=scale_value
)
image_name = os.path.basename(image_path)
else:
image = DirectFrame(
frameColor=(0.5, 0.5, 0.5, 1.0),
frameSize=(-scale_value, scale_value, -scale_value, scale_value),
pos=gui_pos
)
image_name = "占位符图片"
image_wrapper = type("GUIElement", (), {})()
image_wrapper.node = image
image_wrapper.name = image_name
image_wrapper.gui_type = "GUI_IMAGE"
image_wrapper.position = pos
image_wrapper.size = size
image_wrapper.image_path = image_path
self.gui_manager.gui_elements.append(image_wrapper)
print(f"✓ GUI图片创建成功: {image_name}")
return image_wrapper
except Exception as e:
print(f"✗ 创建简单GUI图片失败: {e}")
return None
def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None):
"""创建视频屏幕"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self.gui_manager.createVideoScreen(pos, size, video_path)
return None
except Exception as e:
print(f"创建视频屏幕失败: {e}")
return None
def create2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
"""创建2D视频屏幕"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self.gui_manager.createGUI2DVideoScreen(pos, size, video_path)
return None
except Exception as e:
print(f"创建2D视频屏幕失败: {e}")
return None
def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None):
"""创建360度视频"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self.gui_manager.createSphericalVideo(pos, radius, video_path)
return None
except Exception as e:
print(f"创建球形视频失败: {e}")
return None
def createVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建虚拟屏幕"""
try:
if hasattr(self, "gui_manager") and self.gui_manager:
return self.gui_manager.createGUIVirtualScreen(pos, size, text)
return None
except Exception as e:
print(f"创建虚拟屏幕失败: {e}")
return None
def createSpotLight(self, pos=(0, 0, 5)):
"""创建聚光灯"""
try:
if hasattr(self, "scene_manager") and self.scene_manager:
return self.scene_manager.createSpotLight(pos)
return None
except Exception as e:
print(f"创建聚光灯失败: {e}")
return None
def createPointLight(self, pos=(0, 0, 5)):
"""创建点光源"""
try:
if hasattr(self, "scene_manager") and self.scene_manager:
return self.scene_manager.createPointLight(pos)
return None
except Exception as e:
print(f"创建点光源失败: {e}")
return None
def createFlatTerrain(self, size=(10, 10), resolution=129):
"""创建平面地形"""
try:
if hasattr(self, "terrain_manager") and self.terrain_manager:
return self.terrain_manager.createFlatTerrain(size, resolution)
return None
except Exception as e:
print(f"创建平面地形失败: {e}")
return None
def createTerrainFromHeightMap(self, heightmap_path, scale=(1.0, 1.0, 10.0)):
"""从高度图创建地形"""
try:
if hasattr(self, "terrain_manager") and self.terrain_manager:
return self.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale)
return None
except Exception as e:
print(f"创建高度图地形失败: {e}")
return None
def createScript(self, script_name, template="basic"):
"""创建脚本"""
try:
if hasattr(self, "script_manager") and self.script_manager:
return self.script_manager.createScript(script_name, template)
return None
except Exception as e:
print(f"创建脚本失败: {e}")
return None
def loadScript(self, script_path):
"""加载脚本"""
try:
if hasattr(self, "script_manager") and self.script_manager:
return self.script_manager.loadScript(script_path)
return None
except Exception as e:
print(f"加载脚本失败: {e}")
return None

File diff suppressed because it is too large Load Diff