Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84325cc554 | |||
| bc1abe0060 | |||
| 3d994dd556 | |||
| cfe92a9a80 | |||
| d784a9d9db | |||
| cccfe20be8 | |||
| e8c4ff4260 | |||
| 61e7498835 | |||
| 12276b13af | |||
| da63dd4877 | |||
| 4a25031cac | |||
| d2cfc77fc6 | |||
|
|
37b3cc30dc | ||
|
|
036b68ef41 | ||
|
|
5752559cdd | ||
|
|
7386d687da | ||
|
|
28d2b124cc | ||
|
|
2ac08b0582 | ||
|
|
86aaa21ddd | ||
|
|
af898947b5 | ||
|
|
f3f8da7b90 | ||
|
|
2183d3fc3e | ||
|
|
756db5b010 | ||
|
|
1fd7e1d7ac | ||
|
|
53e6a829e4 | ||
|
|
e917f9019d | ||
|
|
c93ab3edac |
3
.gitignore
vendored
3
.gitignore
vendored
@ -58,3 +58,6 @@ Resources/models/Women_1.glb
|
|||||||
/venv/
|
/venv/
|
||||||
/engine/
|
/engine/
|
||||||
/panda3d_imgui-1.1.0-py3-none-any.whl
|
/panda3d_imgui-1.1.0-py3-none-any.whl
|
||||||
|
imgui.ini
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
|||||||
331
AGENTS.md
331
AGENTS.md
@ -1,219 +1,120 @@
|
|||||||
# EG 项目概览与开发指南
|
# 元泰 EG 项目开发指南
|
||||||
|
|
||||||
## 项目简介
|
元泰 EG 是基于 Panda3D 的 3D 编辑器和游戏引擎。
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*该文档由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
EG
@ -1 +0,0 @@
|
|||||||
Subproject commit 69e2bda47e9713705ad5c45a08b6fc643a2b51f6
|
|
||||||
@ -13,6 +13,7 @@ enabled:
|
|||||||
- scattering
|
- scattering
|
||||||
- skin_shading
|
- skin_shading
|
||||||
- sky_ao
|
- sky_ao
|
||||||
|
- selection_outline
|
||||||
- smaa
|
- smaa
|
||||||
- ssr
|
- ssr
|
||||||
# - clouds
|
# - clouds
|
||||||
|
|||||||
@ -63,5 +63,6 @@ global_stage_order:
|
|||||||
|
|
||||||
# Finishing stages, do not insert anything below
|
# Finishing stages, do not insert anything below
|
||||||
- UpscaleStage
|
- UpscaleStage
|
||||||
|
- SelectionOutlineStage
|
||||||
- FinalStage
|
- FinalStage
|
||||||
- UpdatePreviousPipesStage
|
- UpdatePreviousPipesStage
|
||||||
|
|||||||
@ -40,7 +40,22 @@ from rpcore.rpobject import RPObject
|
|||||||
NATIVE_CXX_LOADED = False
|
NATIVE_CXX_LOADED = False
|
||||||
|
|
||||||
# Read the configuration from the flag-file
|
# 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__))
|
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")
|
cxx_flag_path = join(current_path, "use_cxx.flag")
|
||||||
if not isfile(cxx_flag_path):
|
if not isfile(cxx_flag_path):
|
||||||
RPObject.global_error("CORE", "Could not find cxx flag, please run the setup.py!")
|
RPObject.global_error("CORE", "Could not find cxx flag, please run the setup.py!")
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
"""Selection outline plugin package."""
|
||||||
|
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
settings:
|
||||||
|
daytime_settings:
|
||||||
|
|
||||||
15
RenderPipelineFile/rpplugins/selection_outline/plugin.py
Normal file
15
RenderPipelineFile/rpplugins/selection_outline/plugin.py
Normal 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)
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
@ -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.
57
Start_Run.py
57
Start_Run.py
@ -1,8 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# 添加项目根目录到 Python 路径
|
# 添加项目根目录到 Python 路径。打包后可通过环境变量覆盖,
|
||||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
# 以适配 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)
|
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")
|
icons_path = os.path.join(project_root, "icons")
|
||||||
sys.path.insert(0, icons_path)
|
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__":
|
if __name__ == "__main__":
|
||||||
args = sys.argv[1:]
|
try:
|
||||||
# args = "/home/tiger/桌面/Test1"
|
args = sys.argv[1:]
|
||||||
# args = "C:/Users/29381/Desktop/1"
|
_write_startup_log(f"Launch args: {args}")
|
||||||
print(f'Path is {args}')
|
_write_startup_log(f"Working directory: {os.getcwd()}")
|
||||||
# 将整个列表转换为字符串(包括方括号)
|
|
||||||
args_str = ''.join(args)
|
|
||||||
|
|
||||||
from main import MyWorld
|
# 将整个列表转换为字符串(包括方括号)
|
||||||
if args:
|
args_str = ''.join(args)
|
||||||
# print(f"[DEBUG] 启动时传入项目路径: {args_str}")
|
|
||||||
# 创建应用实例
|
from main import MyWorld
|
||||||
app = MyWorld()
|
|
||||||
# 如果传入了项目路径,尝试打开项目
|
if args:
|
||||||
if hasattr(app, 'project_manager'):
|
app = MyWorld()
|
||||||
# print(f"[DEBUG] 尝试打开项目: {args_str}")
|
if hasattr(app, 'project_manager'):
|
||||||
success = app.project_manager.openProject(args_str)
|
app.project_manager.openProject(args_str)
|
||||||
# print(f"[DEBUG] 项目打开结果: {success}")
|
app.run()
|
||||||
else:
|
else:
|
||||||
# print(f"[DEBUG] 项目管理器未初始化")
|
app = MyWorld()
|
||||||
pass
|
app.run()
|
||||||
app.run()
|
except Exception:
|
||||||
else:
|
_write_startup_log("Unhandled exception during startup:")
|
||||||
# print(f"[DEBUG] 无项目路径,正常启动")
|
_write_startup_log(traceback.format_exc())
|
||||||
app = MyWorld()
|
raise
|
||||||
app.run()
|
|
||||||
|
|||||||
393
build_scripts/BUILD_GUIDE.md
Normal file
393
build_scripts/BUILD_GUIDE.md
Normal 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 Shell(Visual 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
285
build_scripts/ICON_GUIDE.md
Normal 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
|
||||||
294
build_scripts/PACKAGING_CHECKLIST.md
Normal file
294
build_scripts/PACKAGING_CHECKLIST.md
Normal 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年
|
||||||
246
build_scripts/PACKAGING_RISK_AUDIT.md
Normal file
246
build_scripts/PACKAGING_RISK_AUDIT.md
Normal 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
|
||||||
99
build_scripts/PYTHON_VERSION_REQUIREMENT.md
Normal file
99
build_scripts/PYTHON_VERSION_REQUIREMENT.md
Normal 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
143
build_scripts/README.md
Normal 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年
|
||||||
326
build_scripts/analyze_packaging.py
Normal file
326
build_scripts/analyze_packaging.py
Normal 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()
|
||||||
525
build_scripts/build_linux.sh
Normal file
525
build_scripts/build_linux.sh
Normal 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 "临时文件已清理"
|
||||||
201
build_scripts/build_windows.ps1
Normal file
201
build_scripts/build_windows.ps1
Normal 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
139
build_scripts/check_env.ps1
Normal 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
137
build_scripts/check_env.sh
Normal 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
|
||||||
36
build_scripts/check_python_version.py
Normal file
36
build_scripts/check_python_version.py
Normal 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
@ -450,7 +450,8 @@ class EventHandler:
|
|||||||
print(f"点击的是碰撞节点: {hitNode.getName()}")
|
print(f"点击的是碰撞节点: {hitNode.getName()}")
|
||||||
# 碰撞节点的父节点应该是模型
|
# 碰撞节点的父节点应该是模型
|
||||||
parent = hitNode.getParent()
|
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
|
selectedModel = parent
|
||||||
print(f"找到对应的模型: {selectedModel.getName()}")
|
print(f"找到对应的模型: {selectedModel.getName()}")
|
||||||
else:
|
else:
|
||||||
@ -460,13 +461,15 @@ class EventHandler:
|
|||||||
current = hitNode
|
current = hitNode
|
||||||
while current != self.world.render:
|
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
|
selectedModel = current
|
||||||
print(f"找到模型节点: {selectedModel.getName()}")
|
print(f"找到模型节点: {selectedModel.getName()}")
|
||||||
break
|
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):
|
if current.getParent() == model or current.isAncestorOf(model):
|
||||||
selectedModel = model
|
selectedModel = model
|
||||||
print(f"找到父模型: {selectedModel.getName()}")
|
print(f"找到父模型: {selectedModel.getName()}")
|
||||||
|
|||||||
225
core/imgui_webview.py
Normal file
225
core/imgui_webview.py
Normal 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
@ -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("✓ 默认巡检路线已创建")
|
|
||||||
|
|
||||||
@ -389,6 +389,12 @@ class ResourceManager:
|
|||||||
shutil.copytree(src, dst)
|
shutil.copytree(src, dst)
|
||||||
else:
|
else:
|
||||||
shutil.copy2(src, dst)
|
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)
|
imported.append(dst)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"导入失败 {src}: {e}")
|
errors.append(f"导入失败 {src}: {e}")
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta
|
|||||||
TransparencyAttrib, Vec4, CollisionCapsule)
|
TransparencyAttrib, Vec4, CollisionCapsule)
|
||||||
from direct.task.TaskManagerGlobal import taskMgr
|
from direct.task.TaskManagerGlobal import taskMgr
|
||||||
import math
|
import math
|
||||||
|
from core.selection_outline import SelectionOutlineManager
|
||||||
|
|
||||||
class SelectionSystem:
|
class SelectionSystem:
|
||||||
"""选择和变换系统类"""
|
"""选择和变换系统类"""
|
||||||
@ -30,6 +31,17 @@ class SelectionSystem:
|
|||||||
self.selectedNode = None
|
self.selectedNode = None
|
||||||
self.selectionBox = None # 选择框
|
self.selectionBox = None # 选择框
|
||||||
self.selectionBoxTarget = 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)相关
|
# 坐标轴工具(Gizmo)相关
|
||||||
self.gizmo = None # 坐标轴
|
self.gizmo = None # 坐标轴
|
||||||
@ -207,6 +219,21 @@ class SelectionSystem:
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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):
|
def updateSelectionBoxGeometry(self):
|
||||||
"""更新选择框的几何形状和位置"""
|
"""更新选择框的几何形状和位置"""
|
||||||
try:
|
try:
|
||||||
@ -1963,10 +1990,30 @@ class SelectionSystem:
|
|||||||
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
|
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
|
||||||
self.world.command_manager.execute_command(command)
|
self.world.command_manager.execute_command(command)
|
||||||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||||
|
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"创建撤销命令时出错: {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"]:
|
for axis_name in ["x", "y", "z"]:
|
||||||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||||||
@ -2045,8 +2092,12 @@ class SelectionSystem:
|
|||||||
|
|
||||||
# 创建选择框
|
# 创建选择框
|
||||||
#print("创建选择框...")
|
#print("创建选择框...")
|
||||||
self.createSelectionBox(nodePath)
|
if self.show_selection_box:
|
||||||
if self.selectionBox:
|
self.createSelectionBox(nodePath)
|
||||||
|
else:
|
||||||
|
self.clearSelectionBox()
|
||||||
|
|
||||||
|
if (not self.show_selection_box) or self.selectionBox:
|
||||||
box_name = "Unknown"
|
box_name = "Unknown"
|
||||||
if self.selectionBox and not self.selectionBox.isEmpty():
|
if self.selectionBox and not self.selectionBox.isEmpty():
|
||||||
box_name = self.selectionBox.getName()
|
box_name = self.selectionBox.getName()
|
||||||
@ -2056,6 +2107,7 @@ class SelectionSystem:
|
|||||||
|
|
||||||
# 创建坐标轴
|
# 创建坐标轴
|
||||||
#print("创建坐标轴...")
|
#print("创建坐标轴...")
|
||||||
|
self._updateSelectionOutline(nodePath)
|
||||||
self.createGizmo(nodePath)
|
self.createGizmo(nodePath)
|
||||||
if self.gizmo:
|
if self.gizmo:
|
||||||
gizmo_name = "Unknown"
|
gizmo_name = "Unknown"
|
||||||
@ -2068,6 +2120,7 @@ class SelectionSystem:
|
|||||||
else:
|
else:
|
||||||
print("清除选择...")
|
print("清除选择...")
|
||||||
self.clearSelectionBox()
|
self.clearSelectionBox()
|
||||||
|
self._updateSelectionOutline(None)
|
||||||
self.clearGizmo()
|
self.clearGizmo()
|
||||||
print("✓ 取消选择")
|
print("✓ 取消选择")
|
||||||
|
|
||||||
@ -2145,6 +2198,40 @@ class SelectionSystem:
|
|||||||
"""获取当前选中的节点"""
|
"""获取当前选中的节点"""
|
||||||
return self.selectedNode
|
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):
|
def hasSelection(self):
|
||||||
"""检查是否有选中的节点"""
|
"""检查是否有选中的节点"""
|
||||||
return self.selectedNode is not None
|
return self.selectedNode is not None
|
||||||
@ -2156,6 +2243,9 @@ class SelectionSystem:
|
|||||||
if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()):
|
if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()):
|
||||||
self.clearSelectionBox()
|
self.clearSelectionBox()
|
||||||
|
|
||||||
|
if self.selectedNode and self.selectedNode.isEmpty():
|
||||||
|
self._updateSelectionOutline(None)
|
||||||
|
|
||||||
def setupGizmoCollision(self):
|
def setupGizmoCollision(self):
|
||||||
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
|
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
|
||||||
return
|
return
|
||||||
@ -2875,6 +2965,7 @@ class SelectionSystem:
|
|||||||
|
|
||||||
# 清理其他资源
|
# 清理其他资源
|
||||||
self.clearSelectionBox()
|
self.clearSelectionBox()
|
||||||
|
self._updateSelectionOutline(None)
|
||||||
self.clearGizmo()
|
self.clearGizmo()
|
||||||
def clearSelection(self):
|
def clearSelection(self):
|
||||||
"""清除当前选择"""
|
"""清除当前选择"""
|
||||||
@ -2882,6 +2973,7 @@ class SelectionSystem:
|
|||||||
self.selectedNode = None
|
self.selectedNode = None
|
||||||
self.selectedObject = None
|
self.selectedObject = None
|
||||||
self.clearSelectionBox()
|
self.clearSelectionBox()
|
||||||
|
self._updateSelectionOutline(None)
|
||||||
self.clearGizmo()
|
self.clearGizmo()
|
||||||
|
|
||||||
# 清除树形控件中的选择
|
# 清除树形控件中的选择
|
||||||
|
|||||||
303
core/selection_outline.py
Normal file
303
core/selection_outline.py
Normal 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
|
||||||
@ -14,8 +14,9 @@ from direct.showbase.ShowBase import ShowBase
|
|||||||
from direct.showbase.ShowBaseGlobal import globalClock
|
from direct.showbase.ShowBaseGlobal import globalClock
|
||||||
from scene.scene_manager import SceneManager
|
from scene.scene_manager import SceneManager
|
||||||
|
|
||||||
# 设置 RenderPipelineFile 路径
|
# 设置 RenderPipelineFile 路径。打包后可通过环境变量显式指定项目根目录,
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
# 避免 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")
|
render_pipeline_path = os.path.join(project_root, "RenderPipelineFile")
|
||||||
sys.path.insert(0, render_pipeline_path)
|
sys.path.insert(0, render_pipeline_path)
|
||||||
|
|
||||||
@ -65,8 +66,16 @@ class CoreWorld(ShowBase):
|
|||||||
if is_fullscreen:
|
if is_fullscreen:
|
||||||
loadPrcFileData("", "fullscreen #t")
|
loadPrcFileData("", "fullscreen #t")
|
||||||
|
|
||||||
# 创建渲染管线
|
# 创建渲染管线。打包后 Nuitka 会把 rpcore/rplibs 平铺到发行目录,
|
||||||
|
# 自动探测到的 base_path 可能变成 dist 根目录,导致找不到
|
||||||
|
# RenderPipelineFile/config/pipeline.yaml,这里显式指回真实管线目录。
|
||||||
self.render_pipeline = RenderPipeline()
|
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()
|
self.render_pipeline.pre_showbase_init()
|
||||||
|
|
||||||
# 强制开启多线程支持 (Video播放必需)
|
# 强制开启多线程支持 (Video播放必需)
|
||||||
|
|||||||
4027
gui/gui_manager.py
4027
gui/gui_manager.py
File diff suppressed because it is too large
Load Diff
BIN
icons/app.ico
Normal file
BIN
icons/app.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 459 B |
BIN
icons/app.png
Normal file
BIN
icons/app.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
222
imgui.ini
222
imgui.ini
@ -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
|
|
||||||
|
|
||||||
@ -4,6 +4,7 @@ import json
|
|||||||
import datetime
|
import datetime
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
|
from project.webgl_packager import WebGLPackager
|
||||||
|
|
||||||
class ProjectManager:
|
class ProjectManager:
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ class ProjectManager:
|
|||||||
self.world = world
|
self.world = world
|
||||||
self.current_project_path = None
|
self.current_project_path = None
|
||||||
self.project_config = None
|
self.project_config = None
|
||||||
|
self.last_webgl_export_report = None
|
||||||
|
|
||||||
print("✓ 项目管理系统初始化完成")
|
print("✓ 项目管理系统初始化完成")
|
||||||
|
|
||||||
@ -300,6 +302,52 @@ class ProjectManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# ==================== 项目打包功能 ====================
|
# ==================== 项目打包功能 ====================
|
||||||
|
|
||||||
|
def buildWebGLPackage(self, output_dir):
|
||||||
|
"""将当前项目打包为 WebGL 静态站点目录。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_dir (str): 用户选择的输出根目录
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: success/partial 返回 True,failed 返回 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):
|
def buildPackage(self, build_dir):
|
||||||
"""打包项目为可执行文件 - 按照Panda3D官方标准方法
|
"""打包项目为可执行文件 - 按照Panda3D官方标准方法
|
||||||
@ -478,7 +526,6 @@ class ProjectManager:
|
|||||||
def _copyScriptSystemToBuild(self,build_dir):
|
def _copyScriptSystemToBuild(self,build_dir):
|
||||||
core_files = [
|
core_files = [
|
||||||
"script_system.py",
|
"script_system.py",
|
||||||
"InfoPanelManager.py",
|
|
||||||
"CustomMouseController.py"
|
"CustomMouseController.py"
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -840,4 +887,4 @@ setup(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"执行打包命令失败: {str(e)}")
|
print(f"执行打包命令失败: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
995
project/webgl_packager.py
Normal file
995
project/webgl_packager.py
Normal 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)
|
||||||
30
requirements/requirements-minimal.txt
Normal file
30
requirements/requirements-minimal.txt
Normal 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
516
scene/scene_manager_convert_tiles_mixin.py
Normal file
516
scene/scene_manager_convert_tiles_mixin.py
Normal 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
|
||||||
19
scene/scene_manager_impl.py
Normal file
19
scene/scene_manager_impl.py
Normal 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
|
||||||
1736
scene/scene_manager_io_mixin.py
Normal file
1736
scene/scene_manager_io_mixin.py
Normal file
File diff suppressed because it is too large
Load Diff
455
scene/scene_manager_light_mixin.py
Normal file
455
scene/scene_manager_light_mixin.py
Normal 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
|
||||||
1157
scene/scene_manager_model_mixin.py
Normal file
1157
scene/scene_manager_model_mixin.py
Normal file
File diff suppressed because it is too large
Load Diff
863
scene/scene_manager_serialization_mixin.py
Normal file
863
scene/scene_manager_serialization_mixin.py
Normal 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)) # 默认为0(p3d_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
5
scene/tree_roles.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Shared tree data-role constants (Qt-independent)."""
|
||||||
|
|
||||||
|
# Qt.ItemDataRole.UserRole
|
||||||
|
TREE_USER_ROLE = 256
|
||||||
|
|
||||||
@ -56,16 +56,29 @@ class CrossPlatformPathHandler:
|
|||||||
exists = os.path.exists(filepath)
|
exists = os.path.exists(filepath)
|
||||||
return exists
|
return exists
|
||||||
|
|
||||||
def _panda3d_normalize(self, filepath):
|
def _panda3d_normalize(self, filepath):
|
||||||
"""使用Panda3D标准化路径"""
|
"""使用Panda3D标准化路径"""
|
||||||
try:
|
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||||||
panda_filename = Filename.from_os_specific(filepath)
|
ctor = getattr(Filename, ctor_name, None)
|
||||||
normalized_path = panda_filename.get_fullpath()
|
if not ctor:
|
||||||
print(f"✓ Panda3D标准化: {normalized_path}")
|
continue
|
||||||
return normalized_path
|
try:
|
||||||
except Exception as e:
|
panda_filename = ctor(filepath)
|
||||||
print(f"⚠️ Panda3D标准化失败: {e}")
|
normalized_path = panda_filename.get_fullpath()
|
||||||
return filepath
|
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):
|
def _attempt_path_fixes(self, filepath):
|
||||||
"""尝试各种路径修复方法"""
|
"""尝试各种路径修复方法"""
|
||||||
@ -293,4 +306,4 @@ def normalize_model_path(filepath):
|
|||||||
|
|
||||||
def suggest_file_placement(filename):
|
def suggest_file_placement(filename):
|
||||||
"""便捷函数:建议文件放置位置"""
|
"""便捷函数:建议文件放置位置"""
|
||||||
return path_handler.suggest_file_placement(filename)
|
return path_handler.suggest_file_placement(filename)
|
||||||
|
|||||||
@ -1,51 +1,193 @@
|
|||||||
|
|
||||||
|
import math
|
||||||
from panda3d.core import (
|
from panda3d.core import (
|
||||||
GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter,
|
GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter,
|
||||||
InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums,
|
InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums,
|
||||||
BoundingSphere, NodePath, GeomNode, Texture, SamplerState,
|
BoundingSphere, NodePath, GeomNode, Texture, SamplerState,
|
||||||
Point3, BoundingBox, Quat
|
Point3, BoundingBox, Quat
|
||||||
)
|
)
|
||||||
import struct
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
class ObjectController:
|
class ObjectController:
|
||||||
"""
|
"""
|
||||||
物体控制器 (No Custom Shader Mode)
|
混合架构控制器 (Chunked Static + Dynamic Editing)
|
||||||
====================================
|
================================================
|
||||||
Uses RP's default rendering (no rp.set_effect) for maximum FPS.
|
- 默认: 每个 chunk 使用 flatten 后的静态表示
|
||||||
Vertex colors baked for picking. Movement modifies vertex data directly.
|
- 编辑: 被选中对象所属 chunk 切换为动态表示,直接改 NodePath 变换
|
||||||
Stores original vertex positions per object for rotation/translation.
|
- 提交: 离开 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.name_to_ids = {}
|
||||||
self.id_to_name = {}
|
self.id_to_name = {}
|
||||||
self.key_to_node = {}
|
self.key_to_node = {}
|
||||||
self.node_list = []
|
self.node_list = []
|
||||||
self.display_names = {}
|
self.display_names = {}
|
||||||
self.global_transforms = [] # Original transforms (for center/position)
|
self.global_transforms = []
|
||||||
|
|
||||||
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.position_offsets = {}
|
self.position_offsets = {}
|
||||||
|
self.vertex_index = {}
|
||||||
|
self.original_positions = {}
|
||||||
self.local_to_global_id = {}
|
self.local_to_global_id = {}
|
||||||
self.local_transform_state = {}
|
self.local_transform_state = {}
|
||||||
self.local_transform_base_positions = {}
|
self.local_transform_base_positions = {}
|
||||||
|
self.pick_vertex_index = {}
|
||||||
self.virtual_tree = None
|
self.virtual_tree = None
|
||||||
self.virtual_tree_meta = None
|
self.virtual_tree_meta = None
|
||||||
|
|
||||||
self.model = None
|
self.model = None
|
||||||
self.pick_model = None
|
self.pick_model = None
|
||||||
self.chunk_node = None # Single chunk node
|
self.id_to_chunk = {} # global_id -> chunk_id
|
||||||
self._source_model_name = ""
|
self.id_to_object_np = {} # global_id -> dynamic object nodepath
|
||||||
self._source_model_stem = ""
|
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):
|
def _build_original_hierarchy_key(self, np, model_root):
|
||||||
"""Capture hierarchy path before flatten/reparent."""
|
"""Capture hierarchy path before flatten/reparent."""
|
||||||
@ -60,6 +202,71 @@ class ObjectController:
|
|||||||
if not parts:
|
if not parts:
|
||||||
return np.get_name() or "Unnamed"
|
return np.get_name() or "Unnamed"
|
||||||
return "/".join(parts)
|
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):
|
def _is_wrapper_segment(self, segment):
|
||||||
s = (segment or "").strip().lower()
|
s = (segment or "").strip().lower()
|
||||||
@ -99,20 +306,21 @@ class ObjectController:
|
|||||||
self.local_to_global_id = {}
|
self.local_to_global_id = {}
|
||||||
self.local_transform_state = {}
|
self.local_transform_state = {}
|
||||||
self.local_transform_base_positions = {}
|
self.local_transform_base_positions = {}
|
||||||
|
self.pick_vertex_index = {}
|
||||||
self.virtual_tree = None
|
self.virtual_tree = None
|
||||||
self.virtual_tree_meta = None
|
self.virtual_tree_meta = None
|
||||||
self.pick_model = None
|
self.pick_model = None
|
||||||
model_name = (model.get_name() or "").strip()
|
model_name = (model.get_name() or "").strip()
|
||||||
self._source_model_name = model_name.lower()
|
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()
|
self._source_model_stem = model_name.rsplit(".", 1)[0].lower() if "." in model_name else model_name.lower()
|
||||||
|
|
||||||
global_id_counter = 0
|
global_id_counter = 0
|
||||||
chunk_key = model.get_name() or "default"
|
chunk_key = model.get_name() or "default"
|
||||||
|
|
||||||
# No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py)
|
# No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py)
|
||||||
self.chunk_node = model
|
self.chunk_node = model
|
||||||
self.chunks[chunk_key] = {'node': model, 'base_id': 0}
|
self.chunks[chunk_key] = {'node': model, 'base_id': 0}
|
||||||
|
|
||||||
# Cache original hierarchy path BEFORE flatten/reparent.
|
# Cache original hierarchy path BEFORE flatten/reparent.
|
||||||
original_keys = {}
|
original_keys = {}
|
||||||
for np in geom_nodes:
|
for np in geom_nodes:
|
||||||
@ -121,12 +329,12 @@ class ObjectController:
|
|||||||
# Flatten hierarchy
|
# Flatten hierarchy
|
||||||
for np in geom_nodes:
|
for np in geom_nodes:
|
||||||
np.wrt_reparent_to(model)
|
np.wrt_reparent_to(model)
|
||||||
|
|
||||||
local_idx = 0
|
local_idx = 0
|
||||||
|
|
||||||
for np in geom_nodes:
|
for np in geom_nodes:
|
||||||
gnode = np.node()
|
gnode = np.node()
|
||||||
|
|
||||||
if gnode.get_num_parents() > 1:
|
if gnode.get_num_parents() > 1:
|
||||||
parent = np.get_parent()
|
parent = np.get_parent()
|
||||||
if not parent.is_empty():
|
if not parent.is_empty():
|
||||||
@ -134,57 +342,57 @@ class ObjectController:
|
|||||||
np.detach_node()
|
np.detach_node()
|
||||||
np = new_np
|
np = new_np
|
||||||
gnode = np.node()
|
gnode = np.node()
|
||||||
|
|
||||||
unique_key = original_keys.get(id(np), str(np))
|
unique_key = original_keys.get(id(np), str(np))
|
||||||
display_name = np.get_name() or f"Object_{global_id_counter}"
|
display_name = np.get_name() or f"Object_{global_id_counter}"
|
||||||
|
|
||||||
if unique_key not in self.name_to_ids:
|
if unique_key not in self.name_to_ids:
|
||||||
self.name_to_ids[unique_key] = []
|
self.name_to_ids[unique_key] = []
|
||||||
self.key_to_node[unique_key] = np
|
self.key_to_node[unique_key] = np
|
||||||
self.node_list.append(unique_key)
|
self.node_list.append(unique_key)
|
||||||
self.display_names[unique_key] = display_name
|
self.display_names[unique_key] = display_name
|
||||||
|
|
||||||
# Save original transform
|
# Save original transform
|
||||||
mat_double = np.get_mat()
|
mat_double = np.get_mat()
|
||||||
original_transform = LMatrix4f(mat_double)
|
original_transform = LMatrix4f(mat_double)
|
||||||
|
|
||||||
for i in range(gnode.get_num_geoms()):
|
for i in range(gnode.get_num_geoms()):
|
||||||
geom = gnode.modify_geom(i)
|
geom = gnode.modify_geom(i)
|
||||||
vdata = geom.modify_vertex_data()
|
vdata = geom.modify_vertex_data()
|
||||||
|
|
||||||
if not vdata.has_column("color"):
|
if not vdata.has_column("color"):
|
||||||
new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||||
vdata.set_format(new_format)
|
vdata.set_format(new_format)
|
||||||
|
|
||||||
# Encode Local ID in R/G
|
# Encode Local ID in R/G
|
||||||
low = local_idx % 256
|
low = local_idx % 256
|
||||||
high = local_idx // 256
|
high = local_idx // 256
|
||||||
r = low / 255.0
|
r = low / 255.0
|
||||||
g = high / 255.0
|
g = high / 255.0
|
||||||
|
|
||||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||||
for row in range(vdata.get_num_rows()):
|
for row in range(vdata.get_num_rows()):
|
||||||
writer.set_row(row)
|
writer.set_row(row)
|
||||||
writer.set_data4f(r, g, 0.0, 1.0)
|
writer.set_data4f(r, g, 0.0, 1.0)
|
||||||
|
|
||||||
self.global_transforms.append(original_transform)
|
self.global_transforms.append(original_transform)
|
||||||
self.id_to_chunk[global_id_counter] = (chunk_key, local_idx)
|
self.id_to_chunk[global_id_counter] = (chunk_key, local_idx)
|
||||||
self.name_to_ids[unique_key].append(global_id_counter)
|
self.name_to_ids[unique_key].append(global_id_counter)
|
||||||
self.id_to_name[global_id_counter] = unique_key
|
self.id_to_name[global_id_counter] = unique_key
|
||||||
self.local_to_global_id[local_idx] = global_id_counter
|
self.local_to_global_id[local_idx] = global_id_counter
|
||||||
self.position_offsets[local_idx] = Vec3(0, 0, 0)
|
self.position_offsets[local_idx] = Vec3(0, 0, 0)
|
||||||
|
|
||||||
global_id_counter += 1
|
global_id_counter += 1
|
||||||
local_idx += 1
|
local_idx += 1
|
||||||
|
|
||||||
# DO NOT reset transform — keep world-space positions
|
# DO NOT reset transform — keep world-space positions
|
||||||
|
|
||||||
# Flatten directly on model — NO set_final, allows per-geom frustum culling
|
# Flatten directly on model — NO set_final, allows per-geom frustum culling
|
||||||
model.flatten_strong()
|
model.flatten_strong()
|
||||||
|
|
||||||
t1 = time.time()
|
t1 = time.time()
|
||||||
print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms")
|
print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms")
|
||||||
|
|
||||||
# Build vertex index AFTER flatten
|
# Build vertex index AFTER flatten
|
||||||
self._build_vertex_index(model)
|
self._build_vertex_index(model)
|
||||||
self._init_local_transform_state()
|
self._init_local_transform_state()
|
||||||
@ -192,6 +400,7 @@ class ObjectController:
|
|||||||
|
|
||||||
# Keep ID colors only in picking clone to avoid affecting visible shading.
|
# Keep ID colors only in picking clone to avoid affecting visible shading.
|
||||||
self.pick_model = model.copy_to(NodePath("ssbo_pick_root"))
|
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)
|
self._set_uniform_vertex_color(model, 1.0, 1.0, 1.0, 1.0)
|
||||||
|
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
@ -219,6 +428,114 @@ class ObjectController:
|
|||||||
writer.set_row(row)
|
writer.set_row(row)
|
||||||
writer.set_data4f(r, g, b, a)
|
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):
|
def build_virtual_hierarchy(self):
|
||||||
"""Build a readonly virtual tree from node_list path keys."""
|
"""Build a readonly virtual tree from node_list path keys."""
|
||||||
root = {
|
root = {
|
||||||
@ -403,6 +720,228 @@ class ObjectController:
|
|||||||
self.vertex_index[uid].append((gn_np, gi, rows))
|
self.vertex_index[uid].append((gn_np, gi, rows))
|
||||||
self.original_positions[uid].append(pos.copy())
|
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):
|
def _init_local_transform_state(self):
|
||||||
"""Initialize transform state for each local_idx after vertex index is ready."""
|
"""Initialize transform state for each local_idx after vertex index is ready."""
|
||||||
self.local_transform_state = {}
|
self.local_transform_state = {}
|
||||||
@ -424,10 +963,9 @@ class ObjectController:
|
|||||||
return local_indices
|
return local_indices
|
||||||
seen = set()
|
seen = set()
|
||||||
for global_id in global_ids:
|
for global_id in global_ids:
|
||||||
mapping = self.id_to_chunk.get(global_id)
|
_, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||||
if not mapping:
|
if local_idx is None:
|
||||||
continue
|
continue
|
||||||
_, local_idx = mapping
|
|
||||||
if local_idx in seen:
|
if local_idx in seen:
|
||||||
continue
|
continue
|
||||||
if local_idx not in self.vertex_index:
|
if local_idx not in self.vertex_index:
|
||||||
@ -437,7 +975,7 @@ class ObjectController:
|
|||||||
return local_indices
|
return local_indices
|
||||||
|
|
||||||
def get_local_pivot(self, local_idx):
|
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)
|
global_id = self.local_to_global_id.get(local_idx)
|
||||||
if global_id is None:
|
if global_id is None:
|
||||||
return Vec3(0, 0, 0)
|
return Vec3(0, 0, 0)
|
||||||
@ -457,7 +995,8 @@ class ObjectController:
|
|||||||
valid += 1
|
valid += 1
|
||||||
if valid == 0:
|
if valid == 0:
|
||||||
return Vec3(0, 0, 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):
|
def begin_transform_session(self, local_indices):
|
||||||
"""Create immutable baseline snapshot for one gizmo drag session."""
|
"""Create immutable baseline snapshot for one gizmo drag session."""
|
||||||
@ -543,6 +1082,7 @@ class ObjectController:
|
|||||||
for j in range(len(rows)):
|
for j in range(len(rows)):
|
||||||
writer.set_row(int(rows[j]))
|
writer.set_row(int(rows[j]))
|
||||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
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):
|
def _quat_to_np_mat3(self, quat):
|
||||||
"""Convert Panda3D Quat to 3x3 numpy rotation matrix."""
|
"""Convert Panda3D Quat to 3x3 numpy rotation matrix."""
|
||||||
@ -571,7 +1111,7 @@ class ObjectController:
|
|||||||
], dtype=np.float32)
|
], dtype=np.float32)
|
||||||
|
|
||||||
def create_ssbo(self):
|
def create_ssbo(self):
|
||||||
"""No SSBO needed — using RP default rendering."""
|
"""No SSBO needed in hybrid mode."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def move_object(self, global_id, delta):
|
def move_object(self, global_id, delta):
|
||||||
@ -584,10 +1124,28 @@ class ObjectController:
|
|||||||
|
|
||||||
if global_id not in self.id_to_chunk:
|
if global_id not in self.id_to_chunk:
|
||||||
return
|
return
|
||||||
|
|
||||||
_, local_idx = self.id_to_chunk[global_id]
|
chunk_id, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||||
|
if local_idx is None:
|
||||||
if local_idx not in self.vertex_index:
|
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
|
return
|
||||||
|
|
||||||
# Accumulate offset
|
# Accumulate offset
|
||||||
@ -611,28 +1169,35 @@ class ObjectController:
|
|||||||
for j in range(len(rows)):
|
for j in range(len(rows)):
|
||||||
writer.set_row(int(rows[j]))
|
writer.set_row(int(rows[j]))
|
||||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
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):
|
def get_world_pos(self, global_id):
|
||||||
"""Get current world position of an object."""
|
if not self.model:
|
||||||
if global_id not in self.id_to_chunk:
|
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)
|
return Vec3(0, 0, 0)
|
||||||
_, local_idx = self.id_to_chunk[global_id]
|
|
||||||
|
|
||||||
original_mat = self.global_transforms[global_id]
|
original_mat = self.global_transforms[global_id]
|
||||||
original_pos = original_mat.get_row3(3)
|
original_pos = original_mat.get_row3(3)
|
||||||
offset = self.position_offsets.get(local_idx, Vec3(0))
|
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):
|
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):
|
if global_id >= len(self.global_transforms):
|
||||||
return Vec3(0, 0, 0)
|
return Vec3(0, 0, 0)
|
||||||
mat = self.global_transforms[global_id]
|
mat = self.global_transforms[global_id]
|
||||||
return Vec3(mat.get_row3(3))
|
return Vec3(mat.get_row3(3))
|
||||||
|
|
||||||
def get_transform(self, global_id):
|
def get_transform(self, global_id):
|
||||||
"""Get original transform."""
|
|
||||||
if global_id >= len(self.global_transforms):
|
if global_id >= len(self.global_transforms):
|
||||||
return LMatrix4f.ident_mat()
|
return LMatrix4f.ident_mat()
|
||||||
return self.global_transforms[global_id]
|
return self.global_transforms[global_id]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
测试动画模型 - Panda3D应用程序
|
打包
|
||||||
使用Panda3D引擎编辑器创建
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
@ -14,7 +13,6 @@ from direct.actor.Actor import Actor
|
|||||||
from direct.showbase.ShowBaseGlobal import globalClock
|
from direct.showbase.ShowBaseGlobal import globalClock
|
||||||
from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \
|
from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \
|
||||||
Point3
|
Point3
|
||||||
from core.InfoPanelManager import InfoPanelManager
|
|
||||||
# 获取渲染管线路径
|
# 获取渲染管线路径
|
||||||
# 在文件开头添加sys导入(如果还没有的话)
|
# 在文件开头添加sys导入(如果还没有的话)
|
||||||
import sys
|
import sys
|
||||||
@ -98,8 +96,6 @@ class MainApp(ShowBase):
|
|||||||
# 加载所有脚本e
|
# 加载所有脚本e
|
||||||
self.script_manager.load_all_scripts_from_directory()
|
self.script_manager.load_all_scripts_from_directory()
|
||||||
|
|
||||||
self.info_panel_manager = InfoPanelManager(self)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 再导入controller模块
|
# 再导入controller模块
|
||||||
from rpcore.util.movement_controller import MovementController
|
from rpcore.util.movement_controller import MovementController
|
||||||
@ -539,8 +535,6 @@ class MainApp(ShowBase):
|
|||||||
video_path=video_path,
|
video_path=video_path,
|
||||||
size=absolute_scale
|
size=absolute_scale
|
||||||
)
|
)
|
||||||
elif gui_type == "info_panel":
|
|
||||||
new_element = self.info_panel_manager.onCreateSampleInfoPanel()
|
|
||||||
|
|
||||||
if "scripts" in gui_info and new_element:
|
if "scripts" in gui_info and new_element:
|
||||||
self.processScripts(new_element,gui_info["scripts"])
|
self.processScripts(new_element,gui_info["scripts"])
|
||||||
|
|||||||
16
templates/webgl/index.html
Normal file
16
templates/webgl/index.html
Normal 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
61
templates/webgl/style.css
Normal 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
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
1229
templates/webgl/vendor/OrbitControls.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
8
templates/webgl/vendor/three.module.min.js
vendored
Normal file
8
templates/webgl/vendor/three.module.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
386
templates/webgl/viewer.js
Normal file
386
templates/webgl/viewer.js
Normal 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
1
ui/LUI/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""LUI split modules."""
|
||||||
1247
ui/LUI/lui_function_components.py
Normal file
1247
ui/LUI/lui_function_components.py
Normal file
File diff suppressed because it is too large
Load Diff
1707
ui/LUI/lui_function_properties.py
Normal file
1707
ui/LUI/lui_function_properties.py
Normal file
File diff suppressed because it is too large
Load Diff
1724
ui/LUI/lui_manager_editor.py
Normal file
1724
ui/LUI/lui_manager_editor.py
Normal file
File diff suppressed because it is too large
Load Diff
1458
ui/LUI/lui_manager_interaction.py
Normal file
1458
ui/LUI/lui_manager_interaction.py
Normal file
File diff suppressed because it is too large
Load Diff
69
ui/LUI/lui_shared.py
Normal file
69
ui/LUI/lui_shared.py
Normal 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
|
||||||
2590
ui/lui_function.py
2590
ui/lui_function.py
File diff suppressed because it is too large
Load Diff
3255
ui/lui_manager.py
3255
ui/lui_manager.py
File diff suppressed because it is too large
Load Diff
6724
ui/main_window.py
6724
ui/main_window.py
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from direct.actor.Actor import Actor
|
from direct.actor.Actor import Actor
|
||||||
from direct.task.TaskManagerGlobal import taskMgr
|
from direct.task.TaskManagerGlobal import taskMgr
|
||||||
from panda3d.core import NodePath, PartSubset
|
from panda3d.core import NodePath, PartSubset, Filename
|
||||||
|
|
||||||
|
|
||||||
class _BoundAnimationProxy:
|
class _BoundAnimationProxy:
|
||||||
@ -704,7 +704,22 @@ class AnimationTools:
|
|||||||
|
|
||||||
def _try_create_actor_from_source(source, source_desc):
|
def _try_create_actor_from_source(source, source_desc):
|
||||||
try:
|
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:
|
try:
|
||||||
actor.bindAllAnims(allowAsyncBind=False)
|
actor.bindAllAnims(allowAsyncBind=False)
|
||||||
@ -1139,7 +1154,10 @@ class AnimationTools:
|
|||||||
|
|
||||||
# 路径 Actor 失败后,再尝试把文件作为普通模型加载并 autoBind
|
# 路径 Actor 失败后,再尝试把文件作为普通模型加载并 autoBind
|
||||||
try:
|
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():
|
if loaded_model and not loaded_model.isEmpty():
|
||||||
proxy = _try_create_autobind_proxy(loaded_model, f"文件路径({p})", owns_node=True)
|
proxy = _try_create_autobind_proxy(loaded_model, f"文件路径({p})", owns_node=True)
|
||||||
if proxy:
|
if proxy:
|
||||||
|
|||||||
@ -256,12 +256,105 @@ class AppActions:
|
|||||||
self.add_info_message("另存为项目(功能待实现)")
|
self.add_info_message("另存为项目(功能待实现)")
|
||||||
# TODO: 实现另存为对话框
|
# TODO: 实现另存为对话框
|
||||||
# self.show_save_as_dialog = True
|
# 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):
|
def _on_exit(self):
|
||||||
"""处理退出菜单项"""
|
"""处理退出菜单项"""
|
||||||
self.add_info_message("退出应用程序")
|
self.add_info_message("退出应用程序")
|
||||||
self.userExit()
|
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 "未命名节点"
|
node_name = node.getName() or "未命名节点"
|
||||||
parent = node.getParent()
|
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:
|
if hasattr(self, 'command_manager') and self.command_manager:
|
||||||
@ -747,6 +843,12 @@ class AppActions:
|
|||||||
print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}")
|
print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}")
|
||||||
self._perform_node_cleanup(node)
|
self._perform_node_cleanup(node)
|
||||||
node.removeNode()
|
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}")
|
print(f"[删除] 成功删除节点: {node_name}")
|
||||||
return True
|
return True
|
||||||
@ -921,6 +1023,8 @@ class AppActions:
|
|||||||
# 检查场景文件
|
# 检查场景文件
|
||||||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||||
if os.path.exists(scene_file):
|
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:
|
try:
|
||||||
if self.scene_manager.loadScene(scene_file):
|
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):
|
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False):
|
||||||
"""Import model through the active runtime path.
|
"""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.
|
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):
|
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
||||||
try:
|
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:
|
if hasattr(self, 'selection') and self.selection:
|
||||||
try:
|
try:
|
||||||
self.selection.clearSelection()
|
self.selection.clearSelection()
|
||||||
@ -1047,32 +1146,24 @@ class AppActions:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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)
|
self.ssbo_editor.load_model(file_path)
|
||||||
model_np = getattr(self.ssbo_editor, 'model', None)
|
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.
|
# Keep legacy ray-pick fallback usable by adding a collision body.
|
||||||
if model_np:
|
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
|
# 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("original_path", file_path)
|
||||||
|
model_np.setTag("saved_model_path", normalized_model_path)
|
||||||
model_np.setTag("is_model_root", "1")
|
model_np.setTag("is_model_root", "1")
|
||||||
model_np.setTag("is_scene_element", "1")
|
model_np.setTag("is_scene_element", "1")
|
||||||
model_np.setTag("file", os.path.basename(file_path))
|
model_np.setTag("file", os.path.basename(file_path))
|
||||||
@ -1080,8 +1171,14 @@ class AppActions:
|
|||||||
|
|
||||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||||
try:
|
try:
|
||||||
self.scene_manager.setupCollision(model_np)
|
if not is_project_scene_bam:
|
||||||
self.scene_manager._processModelAnimations(model_np)
|
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:
|
except Exception as e:
|
||||||
print(f"[SSBO] setup components failed: {e}")
|
print(f"[SSBO] setup components failed: {e}")
|
||||||
return model_np
|
return model_np
|
||||||
@ -1131,33 +1228,35 @@ class AppActions:
|
|||||||
self.add_error_message("模型导入失败")
|
self.add_error_message("模型导入失败")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if hasattr(self.scene_manager, 'processMaterials'):
|
# SSBO 模式下保留原始材质/纹理,避免模型发黑。
|
||||||
self.scene_manager.processMaterials(model_node)
|
if not getattr(self, "use_ssbo_mouse_picking", False):
|
||||||
if show_info_message:
|
|
||||||
self.add_info_message("已应用默认材质")
|
|
||||||
|
|
||||||
try:
|
|
||||||
model_node.clearMaterial()
|
|
||||||
model_node.clearTexture()
|
|
||||||
|
|
||||||
if hasattr(self.scene_manager, 'processMaterials'):
|
if hasattr(self.scene_manager, 'processMaterials'):
|
||||||
self.scene_manager.processMaterials(model_node)
|
self.scene_manager.processMaterials(model_node)
|
||||||
|
if show_info_message:
|
||||||
|
self.add_info_message("已应用默认材质")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
color = model_node.getColor()
|
model_node.clearMaterial()
|
||||||
if color and len(color) >= 4 and color == (1, 1, 1, 1):
|
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)
|
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||||
elif not color:
|
except Exception as e:
|
||||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
self.add_warning_message(f"材质处理警告: {e}")
|
||||||
except Exception:
|
|
||||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
|
||||||
except Exception as e:
|
|
||||||
self.add_warning_message(f"材质处理警告: {e}")
|
|
||||||
|
|
||||||
if set_origin:
|
if set_origin:
|
||||||
model_node.setPos(0, 0, 0)
|
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)
|
self.scene_manager.models.append(model_node)
|
||||||
|
|
||||||
if select_model and hasattr(self, 'selection') and self.selection:
|
if select_model and hasattr(self, 'selection') and self.selection:
|
||||||
@ -1172,7 +1271,7 @@ class AppActions:
|
|||||||
|
|
||||||
def _on_import_model(self):
|
def _on_import_model(self):
|
||||||
"""处理导入模型菜单项"""
|
"""处理导入模型菜单项"""
|
||||||
self.add_info_message("打开导入模型对话框")
|
self.add_info_message("打开系统文件选择器")
|
||||||
self.show_import_dialog = True
|
self.show_import_dialog = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -78,57 +78,6 @@ class CreateActions:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.add_error_message(f"创建平面失败: {str(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):
|
def _on_create_spot_light(self):
|
||||||
"""创建聚光灯"""
|
"""创建聚光灯"""
|
||||||
self.show_spot_light_dialog = True
|
self.show_spot_light_dialog = True
|
||||||
|
|||||||
@ -265,88 +265,80 @@ class DialogPanels:
|
|||||||
|
|
||||||
|
|
||||||
def _draw_import_dialog(self):
|
def _draw_import_dialog(self):
|
||||||
"""绘制导入模型对话框"""
|
"""使用系统文件选择器导入模型。"""
|
||||||
if not self.show_import_dialog:
|
if not self.show_import_dialog:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 设置对话框标志
|
# 立即关闭标记,防止每帧重复弹窗
|
||||||
flags = (imgui.WindowFlags_.no_resize |
|
self.show_import_dialog = False
|
||||||
imgui.WindowFlags_.no_collapse |
|
|
||||||
imgui.WindowFlags_.modal)
|
selected_path = self._select_model_file_system_dialog()
|
||||||
|
if not selected_path:
|
||||||
# 获取屏幕尺寸,居中显示对话框
|
self.add_info_message("已取消导入模型")
|
||||||
display_size = imgui.get_io().display_size
|
return
|
||||||
dialog_width = 600
|
|
||||||
dialog_height = 500
|
self.import_file_path = selected_path
|
||||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
self._import_model()
|
||||||
imgui.set_next_window_pos(
|
|
||||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
def _select_model_file_system_dialog(self):
|
||||||
)
|
"""弹出系统文件选择器并返回模型路径。"""
|
||||||
|
try:
|
||||||
with imgui_ctx.begin("导入模型", True, flags) as window:
|
import tkinter as tk
|
||||||
if not window.opened:
|
from tkinter import filedialog
|
||||||
self.show_import_dialog = False
|
|
||||||
return
|
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)):
|
||||||
imgui.text("选择要导入的模型文件")
|
candidates = [
|
||||||
imgui.separator()
|
os.path.join(os.getcwd(), "Resources", "models"),
|
||||||
|
os.path.join(os.getcwd(), "Resources"),
|
||||||
# 文件路径输入
|
os.getcwd(),
|
||||||
imgui.text("文件路径:")
|
]
|
||||||
changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512)
|
initial_dir = next((p for p in candidates if os.path.isdir(p)), os.getcwd())
|
||||||
if changed:
|
|
||||||
self.import_file_path = file_path
|
normalized_exts = []
|
||||||
|
for ext in getattr(self, "supported_formats", []):
|
||||||
imgui.same_line()
|
ext = str(ext).strip().lower()
|
||||||
if imgui.button("浏览..."):
|
if not ext:
|
||||||
self.path_browser_mode = "import_model"
|
continue
|
||||||
self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd()
|
if not ext.startswith("."):
|
||||||
self.show_path_browser = True
|
ext = f".{ext}"
|
||||||
self._refresh_path_browser()
|
if ext not in normalized_exts:
|
||||||
|
normalized_exts.append(ext)
|
||||||
imgui.separator()
|
|
||||||
|
model_patterns = " ".join(f"*{ext}" for ext in normalized_exts) or "*.*"
|
||||||
# 支持的格式说明
|
filetypes = [
|
||||||
imgui.text("支持的文件格式:")
|
("模型文件", model_patterns),
|
||||||
formats_text = ", ".join(self.supported_formats)
|
("All Files", "*.*"),
|
||||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text)
|
]
|
||||||
|
|
||||||
imgui.separator()
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
# 文件预览信息
|
try:
|
||||||
if self.import_file_path and os.path.exists(self.import_file_path):
|
root.attributes("-topmost", True)
|
||||||
file_size = os.path.getsize(self.import_file_path)
|
except Exception:
|
||||||
imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
|
pass
|
||||||
|
|
||||||
file_ext = os.path.splitext(self.import_file_path)[1].lower()
|
selected_path = filedialog.askopenfilename(
|
||||||
if file_ext in self.supported_formats:
|
title="选择要导入的模型文件",
|
||||||
imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持")
|
initialdir=initial_dir,
|
||||||
else:
|
filetypes=filetypes,
|
||||||
imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式")
|
)
|
||||||
else:
|
root.destroy()
|
||||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径")
|
|
||||||
|
if not selected_path:
|
||||||
imgui.separator()
|
return ""
|
||||||
|
|
||||||
# 按钮区域
|
selected_path = os.path.normpath(selected_path)
|
||||||
can_import = (self.import_file_path and
|
file_ext = os.path.splitext(selected_path)[1].lower()
|
||||||
os.path.exists(self.import_file_path) and
|
if normalized_exts and file_ext not in normalized_exts:
|
||||||
os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats)
|
self.add_error_message(f"不支持的文件格式: {file_ext}")
|
||||||
|
return ""
|
||||||
# 根据状态设置按钮颜色
|
|
||||||
if can_import:
|
self.add_info_message(f"已选择文件: {selected_path}")
|
||||||
if imgui.button("导入"):
|
return selected_path
|
||||||
self._import_model()
|
except Exception as e:
|
||||||
self.show_import_dialog = False
|
self.add_error_message(f"打开系统文件选择器失败: {e}")
|
||||||
else:
|
return ""
|
||||||
# 禁用状态的按钮(灰色显示)
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def _refresh_path_browser(self):
|
def _refresh_path_browser(self):
|
||||||
@ -410,435 +402,16 @@ class DialogPanels:
|
|||||||
# 导入模型模式:使用选择的文件路径
|
# 导入模型模式:使用选择的文件路径
|
||||||
self.import_file_path = self.path_browser_selected_path
|
self.import_file_path = self.path_browser_selected_path
|
||||||
self.add_info_message(f"已选择文件: {self.import_file_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:
|
except Exception as e:
|
||||||
self.add_error_message(f"应用路径失败: {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):
|
def _draw_spot_light_dialog(self):
|
||||||
"""绘制聚光灯创建对话框"""
|
"""绘制聚光灯创建对话框"""
|
||||||
@ -1457,5 +1030,3 @@ class DialogPanels:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"刷新高度图浏览器时出错: {e}")
|
print(f"刷新高度图浏览器时出错: {e}")
|
||||||
self.heightmap_browser_items = []
|
self.heightmap_browser_items = []
|
||||||
|
|
||||||
# ==================== 导入功能实现 ====================
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class EditorPanels:
|
|||||||
|
|
||||||
def _ensure_web_panel_state(self):
|
def _ensure_web_panel_state(self):
|
||||||
if not hasattr(self.app, "web_panel_url_input") or not self.app.web_panel_url_input:
|
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"):
|
if not hasattr(self.app, "_imgui_webview"):
|
||||||
self.app._imgui_webview = None
|
self.app._imgui_webview = None
|
||||||
if not hasattr(self.app, "_imgui_webview_tex_id"):
|
if not hasattr(self.app, "_imgui_webview_tex_id"):
|
||||||
@ -171,6 +171,8 @@ class EditorPanels:
|
|||||||
self.app._on_save_project()
|
self.app._on_save_project()
|
||||||
if imgui.menu_item("另存为", "", False, True)[1]:
|
if imgui.menu_item("另存为", "", False, True)[1]:
|
||||||
self.app._on_save_as_project()
|
self.app._on_save_as_project()
|
||||||
|
if imgui.menu_item("打包为 WebGL", "", False, True)[1]:
|
||||||
|
self.app._on_build_webgl_package()
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.menu_item("退出", "Alt+F4", False, True)[1]:
|
if imgui.menu_item("退出", "Alt+F4", False, True)[1]:
|
||||||
self.app._on_exit()
|
self.app._on_exit()
|
||||||
@ -217,33 +219,33 @@ class EditorPanels:
|
|||||||
self.app._on_create_plane()
|
self.app._on_create_plane()
|
||||||
|
|
||||||
# 3D GUI子菜单
|
# 3D GUI子菜单
|
||||||
with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
|
# with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
|
||||||
if three_d_gui_menu:
|
# if three_d_gui_menu:
|
||||||
if imgui.menu_item("3D文本", "", False, True)[1]:
|
# if imgui.menu_item("3D文本", "", False, True)[1]:
|
||||||
self.app._on_create_3d_text()
|
# self.app._on_create_3d_text()
|
||||||
if imgui.menu_item("3D图片", "", False, True)[1]:
|
# if imgui.menu_item("3D图片", "", False, True)[1]:
|
||||||
self.app._on_create_3d_image()
|
# self.app._on_create_3d_image()
|
||||||
|
|
||||||
# GUI子菜单
|
# # GUI子菜单
|
||||||
with imgui_ctx.begin_menu("GUI") as gui_menu:
|
# with imgui_ctx.begin_menu("GUI") as gui_menu:
|
||||||
if gui_menu:
|
# if gui_menu:
|
||||||
if imgui.menu_item("创建按钮", "", False, True)[1]:
|
# if imgui.menu_item("创建按钮", "", False, True)[1]:
|
||||||
self.app._on_create_gui_button()
|
# self.app._on_create_gui_button()
|
||||||
if imgui.menu_item("创建标签", "", False, True)[1]:
|
# if imgui.menu_item("创建标签", "", False, True)[1]:
|
||||||
self.app._on_create_gui_label()
|
# self.app._on_create_gui_label()
|
||||||
if imgui.menu_item("创建输入框", "", False, True)[1]:
|
# if imgui.menu_item("创建输入框", "", False, True)[1]:
|
||||||
self.app._on_create_gui_entry()
|
# self.app._on_create_gui_entry()
|
||||||
if imgui.menu_item("创建图片", "", False, True)[1]:
|
# if imgui.menu_item("创建图片", "", False, True)[1]:
|
||||||
self.app._on_create_gui_image()
|
# self.app._on_create_gui_image()
|
||||||
imgui.separator()
|
# imgui.separator()
|
||||||
if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
|
# if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
|
||||||
self.app._on_create_video_screen()
|
# self.app._on_create_video_screen()
|
||||||
if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
|
# if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
|
||||||
self.app._on_create_2d_video_screen()
|
# self.app._on_create_2d_video_screen()
|
||||||
if imgui.menu_item("创建球形视频", "", False, True)[1]:
|
# if imgui.menu_item("创建球形视频", "", False, True)[1]:
|
||||||
self.app._on_create_spherical_video()
|
# self.app._on_create_spherical_video()
|
||||||
if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
|
# if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
|
||||||
self.app._on_create_virtual_screen()
|
# self.app._on_create_virtual_screen()
|
||||||
|
|
||||||
# 光源子菜单
|
# 光源子菜单
|
||||||
with imgui_ctx.begin_menu("光源") as light_menu:
|
with imgui_ctx.begin_menu("光源") as light_menu:
|
||||||
@ -278,10 +280,10 @@ class EditorPanels:
|
|||||||
# 信息面板子菜单
|
# 信息面板子菜单
|
||||||
with imgui_ctx.begin_menu("信息面板") as info_panel_menu:
|
with imgui_ctx.begin_menu("信息面板") as info_panel_menu:
|
||||||
if info_panel_menu:
|
if info_panel_menu:
|
||||||
if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
|
# if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
|
||||||
self.app._on_create_2d_sample_panel()
|
# self.app._on_create_2d_sample_panel()
|
||||||
if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
|
# if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
|
||||||
self.app._on_create_3d_sample_panel()
|
# self.app._on_create_3d_sample_panel()
|
||||||
if imgui.menu_item("Web面板", "", False, True)[1]:
|
if imgui.menu_item("Web面板", "", False, True)[1]:
|
||||||
self.app._on_create_web_panel()
|
self.app._on_create_web_panel()
|
||||||
|
|
||||||
@ -318,10 +320,10 @@ class EditorPanels:
|
|||||||
imgui.separator()
|
imgui.separator()
|
||||||
|
|
||||||
# 编辑工具
|
# 编辑工具
|
||||||
if imgui.menu_item("光照编辑", "", False, True)[1]:
|
# if imgui.menu_item("光照编辑", "", False, True)[1]:
|
||||||
self.app.tool_manager.setCurrentTool("光照编辑")
|
# self.app.tool_manager.setCurrentTool("光照编辑")
|
||||||
if imgui.menu_item("图形编辑", "", False, True)[1]:
|
# if imgui.menu_item("图形编辑", "", False, True)[1]:
|
||||||
self.app.tool_manager.setCurrentTool("图形编辑")
|
# self.app.tool_manager.setCurrentTool("图形编辑")
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
|
|
||||||
@ -610,8 +612,33 @@ class EditorPanels:
|
|||||||
|
|
||||||
# 处理节点选择
|
# 处理节点选择
|
||||||
if imgui.is_item_clicked():
|
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:
|
if hasattr(self.app, 'selection') and self.app.selection:
|
||||||
self.app.selection.updateSelection(node)
|
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
|
# Clear LUI selection when a scene node is selected
|
||||||
if hasattr(self.app, 'lui_manager'):
|
if hasattr(self.app, 'lui_manager'):
|
||||||
self.app.lui_manager.selected_index = -1
|
self.app.lui_manager.selected_index = -1
|
||||||
@ -655,7 +682,7 @@ class EditorPanels:
|
|||||||
imgui.pop_style_color()
|
imgui.pop_style_color()
|
||||||
|
|
||||||
def _draw_ssbo_virtual_children(self, node):
|
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)
|
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||||
if not ssbo_editor:
|
if not ssbo_editor:
|
||||||
return False
|
return False
|
||||||
@ -664,66 +691,59 @@ class EditorPanels:
|
|||||||
if not model or model != node or not controller:
|
if not model or model != node or not controller:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
tree_root = controller.get_virtual_hierarchy() if hasattr(controller, "get_virtual_hierarchy") else None
|
root_key = getattr(controller, "tree_root_key", None)
|
||||||
if not tree_root or not tree_root.get("children"):
|
if not root_key or root_key not in controller.tree_nodes:
|
||||||
imgui.text_disabled("(无可用子节点)")
|
imgui.text_disabled("(无可用子节点)")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for name in sorted(tree_root["children"].keys()):
|
root_node = controller.tree_nodes[root_key]
|
||||||
child = tree_root["children"][name]
|
if not root_node["children"]:
|
||||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, child, "ssbo_root")
|
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
|
return True
|
||||||
|
|
||||||
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, tree_node, unique_id_prefix, depth=0):
|
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
|
||||||
"""Recursively draw virtual SSBO hierarchy in scene tree."""
|
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
|
||||||
if not tree_node:
|
node_data = controller.tree_nodes.get(key)
|
||||||
|
if not node_data:
|
||||||
return
|
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.
|
display = controller.display_names.get(key, key)
|
||||||
if not children and leaf_key:
|
obj_count = len(controller.name_to_ids.get(key, []))
|
||||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_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]:
|
if imgui.selectable(label, is_selected)[0]:
|
||||||
ssbo_editor.select_node(leaf_key)
|
ssbo_editor.select_node(key)
|
||||||
if hasattr(self.app, "lui_manager"):
|
if hasattr(self.app, "lui_manager"):
|
||||||
self.app.lui_manager.selected_index = -1
|
self.app.lui_manager.selected_index = -1
|
||||||
return
|
else:
|
||||||
|
# Branch node: tree node
|
||||||
# Non-leaf: tree node only for hierarchy display.
|
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||||||
opened = imgui.tree_node(label)
|
if is_selected:
|
||||||
# Clicking non-leaf row selects its aggregate group so parent transform affects children.
|
flags |= imgui.TreeNodeFlags_.selected
|
||||||
if group_key and imgui.is_item_clicked(0):
|
label = f"{display} ({obj_count})##{key}"
|
||||||
ssbo_editor.select_node(group_key)
|
opened = imgui.tree_node_ex(label, flags)
|
||||||
if hasattr(self.app, "lui_manager"):
|
if imgui.is_item_clicked(0):
|
||||||
self.app.lui_manager.selected_index = -1
|
ssbo_editor.select_node(key)
|
||||||
if opened:
|
if hasattr(self.app, "lui_manager"):
|
||||||
# If this node is also a selectable leaf, render selectable entry first.
|
self.app.lui_manager.selected_index = -1
|
||||||
if group_key:
|
if opened:
|
||||||
is_group_selected = (getattr(ssbo_editor, "selected_name", None) == group_key)
|
for child_key in children:
|
||||||
if imgui.selectable(f"[整体] {display}##group_{unique_id_prefix}_{path}", is_group_selected)[0]:
|
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||||
ssbo_editor.select_node(group_key)
|
imgui.tree_pop()
|
||||||
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()
|
|
||||||
def _show_node_context_menu(self, node, name, node_type):
|
def _show_node_context_menu(self, node, name, node_type):
|
||||||
"""显示节点右键菜单"""
|
"""显示节点右键菜单"""
|
||||||
self.app._context_menu_node = True
|
self.app._context_menu_node = True
|
||||||
@ -2211,7 +2231,10 @@ class EditorPanels:
|
|||||||
# 删除对象
|
# 删除对象
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("删除"):
|
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()
|
self.selection.deleteSelectedNode()
|
||||||
|
|
||||||
|
|
||||||
@ -2439,4 +2462,3 @@ class EditorPanels:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"绘制着色模型面板失败: {e}")
|
print(f"绘制着色模型面板失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -339,51 +339,6 @@ class ObjectFactory:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ 创建平面失败: {e}")
|
print(f"✗ 创建平面失败: {e}")
|
||||||
return None
|
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"):
|
def createWebPanel(self, url="https://www.example.com"):
|
||||||
"""创建Web面板"""
|
"""创建Web面板"""
|
||||||
|
|||||||
753
ui/panels/panel_delegates.py
Normal file
753
ui/panels/panel_delegates.py
Normal 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)
|
||||||
396
ui/panels/runtime_actions.py
Normal file
396
ui/panels/runtime_actions.py
Normal 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
|
||||||
|
|
||||||
4319
ui/widgets.py
4319
ui/widgets.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user