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/
|
||||
/engine/
|
||||
/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 编辑器和游戏引擎,集成了高级渲染管线、VR 支持、物理模拟、脚本系统等功能。该项目主要用于创建 3D 场景、游戏开发和交互式应用程序。
|
||||
|
||||
## 核心技术栈
|
||||
|
||||
- **渲染引擎**: Panda3D 1.10.15 + RenderPipeline (延迟渲染、PBR材质)
|
||||
- **GUI框架**: PyQt5 + imgui_bundle (用于编辑器界面)
|
||||
- **VR支持**: OpenVR 2.2.0
|
||||
- **脚本系统**: Python 3.10.12
|
||||
- **其他依赖**: PyQt5-WebEngine, assimp, pillow 等
|
||||
|
||||
## 项目架构
|
||||
|
||||
### 核心模块
|
||||
|
||||
- **main.py** - 应用程序入口点,初始化所有系统
|
||||
- **Start_Run.py** - 启动脚本,设置环境变量和路径
|
||||
- **core/** - 核心功能模块
|
||||
- `world.py` - 世界管理器,处理场景渲染和更新
|
||||
- `selection.py` - 选择系统,处理对象选择
|
||||
- `event_handler.py` - 事件处理系统
|
||||
- `tool_manager.py` - 工具管理器
|
||||
- `script_system.py` - 脚本系统
|
||||
- `patrol_system.py` - 巡逻系统
|
||||
- `Command_System.py` - 命令系统
|
||||
- `terrain_manager.py` - 地形管理
|
||||
- `vr_manager.py` - VR 功能管理
|
||||
- `collision_manager.py` - 碰撞检测管理
|
||||
- `resource_manager.py` - 资源管理
|
||||
- `assembly_interaction.py` - 装配交互
|
||||
- `maintenance_gui.py` - 维护界面
|
||||
- `render_pipeline_utils.py` - 渲染管线工具
|
||||
- `InfoPanelManager.py` - 信息面板管理
|
||||
- `CustomMouseController.py` - 自定义鼠标控制器
|
||||
|
||||
### GUI 系统
|
||||
|
||||
- **gui/gui_manager.py** - GUI管理器,处理2D/3D界面元素
|
||||
- **ui/icon_manager.py** - 图标管理器
|
||||
|
||||
### 场景管理
|
||||
|
||||
- **scene/scene_manager.py** - 场景管理器,处理模型导入、场景树构建
|
||||
- **scene/util.py** - 场景工具函数
|
||||
|
||||
### 项目管理
|
||||
|
||||
- **project/project_manager.py** - 项目管理器,处理项目创建、保存、加载
|
||||
|
||||
### 脚本系统
|
||||
|
||||
- **scripts/** - 包含各种预定义脚本
|
||||
- `MoverScript.py` - 移动脚本
|
||||
- `RotatorScript.py` - 旋转脚本
|
||||
- `ScalerScript.py` - 缩放脚本
|
||||
- `ColorChangerScript.py` - 颜色变化脚本
|
||||
- `FollowerScript.py` - 跟随脚本
|
||||
- `BouncerScript.py` - 弹跳脚本
|
||||
- `ComboAnimatorScript.py` - 组合动画脚本
|
||||
|
||||
### VR 系统
|
||||
|
||||
- **vr_actions/** - VR动作配置
|
||||
- `actions.json` - VR动作定义
|
||||
- `bindings_*.json` - 不同VR设备的绑定配置
|
||||
|
||||
### 资源管理
|
||||
|
||||
- **Resources/** - 资源目录
|
||||
- `models/` - 3D模型
|
||||
- `textures/` - 纹理贴图
|
||||
- `materials/` - 材质文件
|
||||
- `animations/` - 动画文件
|
||||
- `icons/` - 图标资源
|
||||
|
||||
### 渲染管线
|
||||
|
||||
- **RenderPipelineFile/** - 高级渲染管线
|
||||
- `rpcore/` - 渲染管线核心
|
||||
- `rpplugins/` - 渲染插件
|
||||
- `effects/` - 后处理效果
|
||||
- `config/` - 渲染配置
|
||||
|
||||
## 启动和运行
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Python 3.10.12
|
||||
- Panda3D 1.10.15
|
||||
- PyQt5 5.15.9
|
||||
- OpenVR 2.2.0 (VR功能)
|
||||
|
||||
### 运行方式
|
||||
|
||||
1. **直接运行主程序**:
|
||||
```bash
|
||||
python Start_Run.py
|
||||
```
|
||||
|
||||
2. **带项目路径运行**:
|
||||
```bash
|
||||
python Start_Run.py /path/to/project
|
||||
```
|
||||
|
||||
3. **从main.py运行**:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
- **config/vr_settings.json** - VR渲染配置
|
||||
- **imgui.ini** - ImGui界面配置
|
||||
- **.gitignore** - Git忽略文件配置
|
||||
|
||||
## 开发约定
|
||||
|
||||
### 代码风格
|
||||
|
||||
- 使用UTF-8编码
|
||||
- 遵循PEP 8代码规范
|
||||
- 类名使用驼峰命名法
|
||||
- 函数和变量使用下划线命名法
|
||||
- 文件头部包含模块说明和导入信息
|
||||
|
||||
### 脚本开发
|
||||
|
||||
- 所有用户脚本应继承 `ScriptBase` 类
|
||||
- 脚本文件放在 `scripts/` 目录下
|
||||
- 使用 `ScriptManager` 管理脚本生命周期
|
||||
- 脚本API通过 `world` 对象提供
|
||||
|
||||
### 插件开发
|
||||
|
||||
- 插件系统支持动态加载
|
||||
- 插件配置文件使用JSON格式
|
||||
- 插件应实现标准接口
|
||||
|
||||
### VR开发
|
||||
|
||||
- VR动作配置在 `vr_actions/actions.json` 中定义
|
||||
- 支持多种VR设备(Vive、Oculus、Index)
|
||||
- VR渲染配置在 `config/vr_settings.json` 中
|
||||
|
||||
## 构建和部署
|
||||
|
||||
### 依赖安装
|
||||
|
||||
```bash
|
||||
pip install -r requirements/requirements.txt
|
||||
```
|
||||
|
||||
### 测试
|
||||
|
||||
项目包含多个测试脚本:
|
||||
- `test_quick_script.py` - 快速测试脚本
|
||||
- `TestMover.py` - 移动测试
|
||||
- `TestRotator.py` - 旋转测试
|
||||
- `TestScaler.py` - 缩放测试
|
||||
|
||||
### 项目文件
|
||||
|
||||
- 项目配置使用JSON格式
|
||||
- 项目文件包含场景、资源、脚本等信息
|
||||
- 使用 `ProjectManager` 管理项目生命周期
|
||||
|
||||
## 常见问题
|
||||
|
||||
### VR相关问题
|
||||
|
||||
1. 确保VR设备已正确连接
|
||||
2. 检查OpenVR运行时是否安装
|
||||
3. 验证VR动作配置是否正确
|
||||
|
||||
### 渲染问题
|
||||
|
||||
1. 检查显卡驱动是否最新
|
||||
2. 确认RenderPipeline配置正确
|
||||
3. 验证材质和纹理路径
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. 使用合适的LOD设置
|
||||
2. 优化场景复杂度
|
||||
3. 调整渲染质量设置
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 添加新脚本
|
||||
|
||||
1. 在 `scripts/` 目录创建新脚本文件
|
||||
2. 继承 `ScriptBase` 类
|
||||
3. 实现必要的方法
|
||||
4. 通过 `ScriptManager` 注册脚本
|
||||
|
||||
### 添加新工具
|
||||
|
||||
1. 在 `core/tool_manager.py` 中注册新工具
|
||||
2. 实现工具逻辑
|
||||
3. 添加GUI界面元素
|
||||
|
||||
### 添加新渲染效果
|
||||
|
||||
1. 在 `RenderPipelineFile/rpplugins/` 目录创建插件
|
||||
2. 实现渲染逻辑
|
||||
3. 添加配置选项
|
||||
|
||||
## 联系和支持
|
||||
|
||||
- 项目Git仓库: http://10.0.0.99:4000/Rowland/EG.git
|
||||
- 当前分支: imgui
|
||||
- 最新提交: 移除qt依赖 (33e62bd1e4c2c8d3aac15e045b419edb8992d7ff)
|
||||
元泰 EG 是基于 Panda3D 的 3D 编辑器和游戏引擎。
|
||||
|
||||
---
|
||||
|
||||
*该文档由iFlow CLI自动生成,最后更新时间: 2026年1月28日*
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **Python 3.11.x**(必须)
|
||||
- Windows 10/11 64位
|
||||
- Visual Studio Build Tools(用于构建)
|
||||
|
||||
### 安装步骤
|
||||
|
||||
```bash
|
||||
# 1. 检查 Python 版本(必须是 3.11)
|
||||
python --version
|
||||
|
||||
# 2. 验证版本
|
||||
python build_scripts/check_python_version.py
|
||||
|
||||
# 3. 安装依赖
|
||||
pip install -r requirements/requirements-minimal.txt
|
||||
|
||||
# 4. 运行程序
|
||||
python Start_Run.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 构建安装程序
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
# 在 VS Dev Shell 中执行
|
||||
.\build_scripts\build_windows.ps1 -Version "1.0.0"
|
||||
```
|
||||
|
||||
详细步骤请参考 `build_scripts/BUILD_GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 项目结构
|
||||
|
||||
```
|
||||
元泰 EG/
|
||||
├── AGENTS.md # 本文件
|
||||
├── main.py # 主程序入口
|
||||
├── Start_Run.py # 启动脚本
|
||||
├── requirements/ # 依赖配置
|
||||
│ ├── requirements-minimal.txt # 最小依赖(推荐)
|
||||
│ └── requirements.txt # 完整依赖(旧)
|
||||
├── build_scripts/ # 构建脚本和文档
|
||||
│ ├── build_windows.ps1 # Windows 构建脚本
|
||||
│ ├── build_linux.sh # Linux 构建脚本
|
||||
│ ├── BUILD_GUIDE.md # 构建指南 ⭐
|
||||
│ ├── BUILD_README.md # 构建说明
|
||||
│ ├── INSTALLER_GUIDE.md # 完整指南
|
||||
│ ├── QUICK_REFERENCE.md # 快速参考
|
||||
│ ├── ICON_GUIDE.md # 图标准备
|
||||
│ ├── PYTHON_VERSION_REQUIREMENT.md # Python版本要求
|
||||
│ └── PACKAGING_CHECKLIST.md # 打包检查清单
|
||||
├── core/ # 核心模块
|
||||
├── RenderPipelineFile/ # 渲染管线
|
||||
├── Resources/ # 资源文件
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **Python**: 3.11.x(必须)
|
||||
- **渲染引擎**: Panda3D 1.10.16
|
||||
- **UI框架**: imgui_bundle + p3dimgui
|
||||
- **VR支持**: OpenVR 2.2.0
|
||||
- **图像处理**: Pillow
|
||||
|
||||
### 已移除的依赖
|
||||
|
||||
为减少体积,已移除以下未使用的依赖:
|
||||
|
||||
- ❌ PyQt5 (~50MB)
|
||||
- ❌ PySide6 (~100MB)
|
||||
- ❌ 相关 Qt 配套库 (~20MB)
|
||||
|
||||
**总计节省 ~170MB**
|
||||
|
||||
---
|
||||
|
||||
## 📚 重要文档
|
||||
|
||||
| 文档 | 位置 | 用途 |
|
||||
|------|------|------|
|
||||
| 构建指南 | `build_scripts/BUILD_GUIDE.md` | 从零开始构建 ⭐ |
|
||||
| Python版本要求 | `build_scripts/PYTHON_VERSION_REQUIREMENT.md` | Python 3.11 安装指南 |
|
||||
| 快速参考 | `build_scripts/QUICK_REFERENCE.md` | 命令速查 |
|
||||
| 打包检查清单 | `build_scripts/PACKAGING_CHECKLIST.md` | 构建前检查 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 重要提示
|
||||
|
||||
1. **Python 版本**: 必须使用 3.11,构建脚本会检查
|
||||
2. **构建环境**: Windows 必须在 VS Dev Shell 中构建
|
||||
3. **清理旧依赖**: 如果之前有安装 PyQt5/PySide6,建议卸载以节省空间
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- Python 3.11: https://www.python.org/downloads/release/python-3119/
|
||||
- Visual Studio Build Tools: https://visualstudio.microsoft.com/visual-cpp-build-tools/
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024年
|
||||
|
||||
1
EG
1
EG
@ -1 +0,0 @@
|
||||
Subproject commit 69e2bda47e9713705ad5c45a08b6fc643a2b51f6
|
||||
@ -13,6 +13,7 @@ enabled:
|
||||
- scattering
|
||||
- skin_shading
|
||||
- sky_ao
|
||||
- selection_outline
|
||||
- smaa
|
||||
- ssr
|
||||
# - clouds
|
||||
|
||||
@ -63,5 +63,6 @@ global_stage_order:
|
||||
|
||||
# Finishing stages, do not insert anything below
|
||||
- UpscaleStage
|
||||
- SelectionOutlineStage
|
||||
- FinalStage
|
||||
- UpdatePreviousPipesStage
|
||||
|
||||
@ -40,7 +40,22 @@ from rpcore.rpobject import RPObject
|
||||
NATIVE_CXX_LOADED = False
|
||||
|
||||
# Read the configuration from the flag-file
|
||||
# Fix for Nuitka/PyInstaller: try multiple methods to find the flag file
|
||||
import os
|
||||
current_path = dirname(realpath(__file__))
|
||||
|
||||
# Nuitka standalone mode fix: if __file__ is empty or invalid, use sys.argv[0]
|
||||
if not current_path or not isfile(join(current_path, "use_cxx.flag")):
|
||||
# Try relative to executable
|
||||
exe_dir = dirname(realpath(sys.argv[0]))
|
||||
current_path = join(exe_dir, "RenderPipelineFile", "rpcore", "native")
|
||||
|
||||
# Try one more fallback: check environment variable
|
||||
if not isfile(join(current_path, "use_cxx.flag")):
|
||||
rp_path = os.environ.get("RENDER_PIPELINE_PATH", "")
|
||||
if rp_path:
|
||||
current_path = join(rp_path, "rpcore", "native")
|
||||
|
||||
cxx_flag_path = join(current_path, "use_cxx.flag")
|
||||
if not isfile(cxx_flag_path):
|
||||
RPObject.global_error("CORE", "Could not find cxx flag, please run the setup.py!")
|
||||
|
||||
@ -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 sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||
# 添加项目根目录到 Python 路径。打包后可通过环境变量覆盖,
|
||||
# 以适配 Windows dist / Linux AppDir 等不同目录布局。
|
||||
project_root = os.environ.get("EG_PROJECT_ROOT") or os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 设置工作目录为项目根目录
|
||||
@ -20,30 +23,34 @@ sys.path.insert(0, render_pipeline_file_path)
|
||||
icons_path = os.path.join(project_root, "icons")
|
||||
sys.path.insert(0, icons_path)
|
||||
|
||||
|
||||
def _write_startup_log(message: str) -> None:
|
||||
"""Write startup diagnostics next to the executable/source root."""
|
||||
log_path = os.path.join(project_root, "eg_startup.log")
|
||||
with open(log_path, "a", encoding="utf-8") as handle:
|
||||
handle.write(f"\n[{datetime.now().isoformat(timespec='seconds')}] {message}\n")
|
||||
|
||||
# 现在可以导入并运行主程序
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
# args = "/home/tiger/桌面/Test1"
|
||||
# args = "C:/Users/29381/Desktop/1"
|
||||
print(f'Path is {args}')
|
||||
# 将整个列表转换为字符串(包括方括号)
|
||||
args_str = ''.join(args)
|
||||
try:
|
||||
args = sys.argv[1:]
|
||||
_write_startup_log(f"Launch args: {args}")
|
||||
_write_startup_log(f"Working directory: {os.getcwd()}")
|
||||
|
||||
from main import MyWorld
|
||||
if args:
|
||||
# print(f"[DEBUG] 启动时传入项目路径: {args_str}")
|
||||
# 创建应用实例
|
||||
app = MyWorld()
|
||||
# 如果传入了项目路径,尝试打开项目
|
||||
if hasattr(app, 'project_manager'):
|
||||
# print(f"[DEBUG] 尝试打开项目: {args_str}")
|
||||
success = app.project_manager.openProject(args_str)
|
||||
# print(f"[DEBUG] 项目打开结果: {success}")
|
||||
# 将整个列表转换为字符串(包括方括号)
|
||||
args_str = ''.join(args)
|
||||
|
||||
from main import MyWorld
|
||||
|
||||
if args:
|
||||
app = MyWorld()
|
||||
if hasattr(app, 'project_manager'):
|
||||
app.project_manager.openProject(args_str)
|
||||
app.run()
|
||||
else:
|
||||
# print(f"[DEBUG] 项目管理器未初始化")
|
||||
pass
|
||||
app.run()
|
||||
else:
|
||||
# print(f"[DEBUG] 无项目路径,正常启动")
|
||||
app = MyWorld()
|
||||
app.run()
|
||||
app = MyWorld()
|
||||
app.run()
|
||||
except Exception:
|
||||
_write_startup_log("Unhandled exception during startup:")
|
||||
_write_startup_log(traceback.format_exc())
|
||||
raise
|
||||
|
||||
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()}")
|
||||
# 碰撞节点的父节点应该是模型
|
||||
parent = hitNode.getParent()
|
||||
if parent in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if parent in model_list or parent.hasTag('is_model_root'):
|
||||
selectedModel = parent
|
||||
print(f"找到对应的模型: {selectedModel.getName()}")
|
||||
else:
|
||||
@ -460,13 +461,15 @@ class EventHandler:
|
||||
current = hitNode
|
||||
while current != self.world.render:
|
||||
# 检查是否是模型
|
||||
if current in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if current in model_list or current.hasTag('is_model_root'):
|
||||
selectedModel = current
|
||||
print(f"找到模型节点: {selectedModel.getName()}")
|
||||
break
|
||||
|
||||
# 检查是否是模型的子节点
|
||||
for model in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
for model in model_list:
|
||||
if current.getParent() == model or current.isAncestorOf(model):
|
||||
selectedModel = model
|
||||
print(f"找到父模型: {selectedModel.getName()}")
|
||||
|
||||
225
core/imgui_webview.py
Normal file
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)
|
||||
else:
|
||||
shutil.copy2(src, dst)
|
||||
if src.suffix.lower() == '.fbx':
|
||||
fbm_src = src.with_name(src.stem + '.fbm')
|
||||
if fbm_src.exists() and fbm_src.is_dir():
|
||||
fbm_dst = destination_root / fbm_src.name
|
||||
if not fbm_dst.exists():
|
||||
shutil.copytree(fbm_src, fbm_dst)
|
||||
imported.append(dst)
|
||||
except Exception as e:
|
||||
errors.append(f"导入失败 {src}: {e}")
|
||||
|
||||
@ -14,6 +14,7 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta
|
||||
TransparencyAttrib, Vec4, CollisionCapsule)
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
import math
|
||||
from core.selection_outline import SelectionOutlineManager
|
||||
|
||||
class SelectionSystem:
|
||||
"""选择和变换系统类"""
|
||||
@ -30,6 +31,17 @@ class SelectionSystem:
|
||||
self.selectedNode = None
|
||||
self.selectionBox = None # 选择框
|
||||
self.selectionBoxTarget = None # 选择框跟踪的目标节点
|
||||
self.show_selection_box = False
|
||||
self.enable_unity_outline = True
|
||||
self.outline_manager = getattr(self.world, "_selection_outline_manager", None)
|
||||
if not self.outline_manager:
|
||||
self.outline_manager = SelectionOutlineManager(
|
||||
self.world,
|
||||
enabled=self.enable_unity_outline,
|
||||
)
|
||||
setattr(self.world, "_selection_outline_manager", self.outline_manager)
|
||||
else:
|
||||
self.outline_manager.set_enabled(self.enable_unity_outline)
|
||||
|
||||
# 坐标轴工具(Gizmo)相关
|
||||
self.gizmo = None # 坐标轴
|
||||
@ -207,6 +219,21 @@ class SelectionSystem:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _updateSelectionOutline(self, nodePath):
|
||||
"""Update Unity-like selection outline visuals."""
|
||||
try:
|
||||
if not getattr(self, "outline_manager", None):
|
||||
return
|
||||
if not self.enable_unity_outline:
|
||||
self.outline_manager.clear()
|
||||
return
|
||||
if nodePath and not nodePath.isEmpty():
|
||||
self.outline_manager.set_targets([nodePath])
|
||||
else:
|
||||
self.outline_manager.clear()
|
||||
except Exception as e:
|
||||
print(f"update selection outline failed: {e}")
|
||||
|
||||
def updateSelectionBoxGeometry(self):
|
||||
"""更新选择框的几何形状和位置"""
|
||||
try:
|
||||
@ -1963,10 +1990,30 @@ class SelectionSystem:
|
||||
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
|
||||
self.world.command_manager.execute_command(command)
|
||||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建撤销命令时出错: {e}")
|
||||
|
||||
# 同步碰撞体
|
||||
try:
|
||||
target = self.gizmoTarget
|
||||
if target:
|
||||
# 寻找它的所属模型根节点
|
||||
root_model = target
|
||||
while root_model and root_model != self.world.render:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if root_model in model_list or root_model.hasTag('is_model_root'):
|
||||
break
|
||||
root_model = root_model.getParent()
|
||||
|
||||
# 如果这个节点属于某个模型,或者是模型自己,更新该模型的碰撞边界
|
||||
if root_model and hasattr(self.world, 'models') and root_model in self.world.models:
|
||||
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'refreshCollisionBounds'):
|
||||
self.world.scene_manager.refreshCollisionBounds(root_model)
|
||||
except Exception as e:
|
||||
print(f"同步模型碰撞体失败: {e}")
|
||||
|
||||
# 恢复所有轴的颜色
|
||||
for axis_name in ["x", "y", "z"]:
|
||||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||||
@ -2045,8 +2092,12 @@ class SelectionSystem:
|
||||
|
||||
# 创建选择框
|
||||
#print("创建选择框...")
|
||||
self.createSelectionBox(nodePath)
|
||||
if self.selectionBox:
|
||||
if self.show_selection_box:
|
||||
self.createSelectionBox(nodePath)
|
||||
else:
|
||||
self.clearSelectionBox()
|
||||
|
||||
if (not self.show_selection_box) or self.selectionBox:
|
||||
box_name = "Unknown"
|
||||
if self.selectionBox and not self.selectionBox.isEmpty():
|
||||
box_name = self.selectionBox.getName()
|
||||
@ -2056,6 +2107,7 @@ class SelectionSystem:
|
||||
|
||||
# 创建坐标轴
|
||||
#print("创建坐标轴...")
|
||||
self._updateSelectionOutline(nodePath)
|
||||
self.createGizmo(nodePath)
|
||||
if self.gizmo:
|
||||
gizmo_name = "Unknown"
|
||||
@ -2068,6 +2120,7 @@ class SelectionSystem:
|
||||
else:
|
||||
print("清除选择...")
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
print("✓ 取消选择")
|
||||
|
||||
@ -2145,6 +2198,40 @@ class SelectionSystem:
|
||||
"""获取当前选中的节点"""
|
||||
return self.selectedNode
|
||||
|
||||
def deleteSelectedNode(self):
|
||||
"""兼容旧接口:删除当前选中节点。"""
|
||||
node = self.selectedNode
|
||||
if not node or node.isEmpty():
|
||||
return False
|
||||
|
||||
try:
|
||||
if node.getName() == "render":
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
self._deleting_node = True
|
||||
try:
|
||||
# 优先走应用层统一删除链路(命令系统/SSBO清理/消息等)。
|
||||
if hasattr(self.world, "_delete_node") and callable(self.world._delete_node):
|
||||
deleted = bool(self.world._delete_node(node))
|
||||
else:
|
||||
deleted = False
|
||||
scene_manager = getattr(self.world, "scene_manager", None)
|
||||
if scene_manager and hasattr(scene_manager, "models") and node in scene_manager.models:
|
||||
scene_manager.models.remove(node)
|
||||
try:
|
||||
node.removeNode()
|
||||
deleted = True
|
||||
except Exception:
|
||||
deleted = False
|
||||
|
||||
if deleted:
|
||||
self.clearSelection()
|
||||
return deleted
|
||||
finally:
|
||||
self._deleting_node = False
|
||||
|
||||
def hasSelection(self):
|
||||
"""检查是否有选中的节点"""
|
||||
return self.selectedNode is not None
|
||||
@ -2156,6 +2243,9 @@ class SelectionSystem:
|
||||
if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()):
|
||||
self.clearSelectionBox()
|
||||
|
||||
if self.selectedNode and self.selectedNode.isEmpty():
|
||||
self._updateSelectionOutline(None)
|
||||
|
||||
def setupGizmoCollision(self):
|
||||
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
|
||||
return
|
||||
@ -2875,6 +2965,7 @@ class SelectionSystem:
|
||||
|
||||
# 清理其他资源
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
def clearSelection(self):
|
||||
"""清除当前选择"""
|
||||
@ -2882,6 +2973,7 @@ class SelectionSystem:
|
||||
self.selectedNode = None
|
||||
self.selectedObject = None
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
|
||||
# 清除树形控件中的选择
|
||||
|
||||
303
core/selection_outline.py
Normal file
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 scene.scene_manager import SceneManager
|
||||
|
||||
# 设置 RenderPipelineFile 路径
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
# 设置 RenderPipelineFile 路径。打包后可通过环境变量显式指定项目根目录,
|
||||
# 避免 AppImage / 独立发行目录的布局差异导致资源定位错误。
|
||||
project_root = os.environ.get("EG_PROJECT_ROOT") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
render_pipeline_path = os.path.join(project_root, "RenderPipelineFile")
|
||||
sys.path.insert(0, render_pipeline_path)
|
||||
|
||||
@ -65,8 +66,16 @@ class CoreWorld(ShowBase):
|
||||
if is_fullscreen:
|
||||
loadPrcFileData("", "fullscreen #t")
|
||||
|
||||
# 创建渲染管线
|
||||
# 创建渲染管线。打包后 Nuitka 会把 rpcore/rplibs 平铺到发行目录,
|
||||
# 自动探测到的 base_path 可能变成 dist 根目录,导致找不到
|
||||
# RenderPipelineFile/config/pipeline.yaml,这里显式指回真实管线目录。
|
||||
self.render_pipeline = RenderPipeline()
|
||||
rp_base_dir = os.path.join(project_root, "RenderPipelineFile")
|
||||
rp_config_dir = os.path.join(rp_base_dir, "config")
|
||||
if os.path.isdir(rp_base_dir):
|
||||
self.render_pipeline.mount_mgr.base_path = rp_base_dir
|
||||
if os.path.isdir(rp_config_dir):
|
||||
self.render_pipeline.mount_mgr.config_dir = rp_config_dir
|
||||
self.render_pipeline.pre_showbase_init()
|
||||
|
||||
# 强制开启多线程支持 (Video播放必需)
|
||||
|
||||
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 subprocess
|
||||
import shutil
|
||||
from project.webgl_packager import WebGLPackager
|
||||
|
||||
class ProjectManager:
|
||||
|
||||
@ -11,6 +12,7 @@ class ProjectManager:
|
||||
self.world = world
|
||||
self.current_project_path = None
|
||||
self.project_config = None
|
||||
self.last_webgl_export_report = None
|
||||
|
||||
print("✓ 项目管理系统初始化完成")
|
||||
|
||||
@ -300,6 +302,52 @@ class ProjectManager:
|
||||
return False
|
||||
|
||||
# ==================== 项目打包功能 ====================
|
||||
|
||||
def buildWebGLPackage(self, output_dir):
|
||||
"""将当前项目打包为 WebGL 静态站点目录。
|
||||
|
||||
Args:
|
||||
output_dir (str): 用户选择的输出根目录
|
||||
|
||||
Returns:
|
||||
bool: success/partial 返回 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):
|
||||
"""打包项目为可执行文件 - 按照Panda3D官方标准方法
|
||||
@ -478,7 +526,6 @@ class ProjectManager:
|
||||
def _copyScriptSystemToBuild(self,build_dir):
|
||||
core_files = [
|
||||
"script_system.py",
|
||||
"InfoPanelManager.py",
|
||||
"CustomMouseController.py"
|
||||
]
|
||||
|
||||
@ -840,4 +887,4 @@ setup(
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行打包命令失败: {str(e)}")
|
||||
return False
|
||||
return False
|
||||
|
||||
995
project/webgl_packager.py
Normal file
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)
|
||||
return exists
|
||||
|
||||
def _panda3d_normalize(self, filepath):
|
||||
"""使用Panda3D标准化路径"""
|
||||
try:
|
||||
panda_filename = Filename.from_os_specific(filepath)
|
||||
normalized_path = panda_filename.get_fullpath()
|
||||
print(f"✓ Panda3D标准化: {normalized_path}")
|
||||
return normalized_path
|
||||
except Exception as e:
|
||||
print(f"⚠️ Panda3D标准化失败: {e}")
|
||||
return filepath
|
||||
def _panda3d_normalize(self, filepath):
|
||||
"""使用Panda3D标准化路径"""
|
||||
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||||
ctor = getattr(Filename, ctor_name, None)
|
||||
if not ctor:
|
||||
continue
|
||||
try:
|
||||
panda_filename = ctor(filepath)
|
||||
normalized_path = panda_filename.get_fullpath()
|
||||
if normalized_path:
|
||||
print(f"✓ Panda3D标准化: {normalized_path}")
|
||||
return normalized_path
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
panda_filename = Filename(filepath)
|
||||
normalized_path = panda_filename.get_fullpath()
|
||||
if normalized_path:
|
||||
print(f"✓ Panda3D标准化: {normalized_path}")
|
||||
return normalized_path
|
||||
except Exception as e:
|
||||
print(f"⚠️ Panda3D标准化失败: {e}")
|
||||
return filepath
|
||||
|
||||
def _attempt_path_fixes(self, filepath):
|
||||
"""尝试各种路径修复方法"""
|
||||
@ -293,4 +306,4 @@ def normalize_model_path(filepath):
|
||||
|
||||
def suggest_file_placement(filename):
|
||||
"""便捷函数:建议文件放置位置"""
|
||||
return path_handler.suggest_file_placement(filename)
|
||||
return path_handler.suggest_file_placement(filename)
|
||||
|
||||
@ -1,51 +1,193 @@
|
||||
|
||||
import math
|
||||
from panda3d.core import (
|
||||
GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter,
|
||||
InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums,
|
||||
BoundingSphere, NodePath, GeomNode, Texture, SamplerState,
|
||||
Point3, BoundingBox, Quat
|
||||
)
|
||||
import struct
|
||||
import time
|
||||
|
||||
class ObjectController:
|
||||
"""
|
||||
物体控制器 (No Custom Shader Mode)
|
||||
====================================
|
||||
Uses RP's default rendering (no rp.set_effect) for maximum FPS.
|
||||
Vertex colors baked for picking. Movement modifies vertex data directly.
|
||||
Stores original vertex positions per object for rotation/translation.
|
||||
混合架构控制器 (Chunked Static + Dynamic Editing)
|
||||
================================================
|
||||
- 默认: 每个 chunk 使用 flatten 后的静态表示
|
||||
- 编辑: 被选中对象所属 chunk 切换为动态表示,直接改 NodePath 变换
|
||||
- 提交: 离开 chunk 时仅重建该 chunk 的静态表示
|
||||
"""
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, chunk_size=64, chunk_world_size=40.0):
|
||||
self.chunk_size = max(8, int(chunk_size))
|
||||
self.chunk_world_size = max(8.0, float(chunk_world_size))
|
||||
self._reset_state()
|
||||
|
||||
def _reset_state(self):
|
||||
self.name_to_ids = {}
|
||||
self.id_to_name = {}
|
||||
self.key_to_node = {}
|
||||
self.node_list = []
|
||||
self.display_names = {}
|
||||
self.global_transforms = [] # Original transforms (for center/position)
|
||||
|
||||
self.id_to_chunk = {} # global_id -> (chunk_key, local_idx)
|
||||
self.chunks = {} # chunk_key -> dict with 'node' key
|
||||
|
||||
# Vertex index: local_id -> list of (geom_node_np, geom_idx, [row_indices])
|
||||
self.vertex_index = {}
|
||||
|
||||
# Original vertex positions: local_id -> list of (Vec3,) matching row order
|
||||
self.original_positions = {}
|
||||
|
||||
# Current position offsets: local_id -> Vec3 delta
|
||||
self.global_transforms = []
|
||||
self.position_offsets = {}
|
||||
self.vertex_index = {}
|
||||
self.original_positions = {}
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.pick_vertex_index = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
|
||||
|
||||
self.model = None
|
||||
self.pick_model = None
|
||||
self.chunk_node = None # Single chunk node
|
||||
self._source_model_name = ""
|
||||
self._source_model_stem = ""
|
||||
self.id_to_chunk = {} # global_id -> chunk_id
|
||||
self.id_to_object_np = {} # global_id -> dynamic object nodepath
|
||||
self.id_to_pick_np = {} # global_id -> pick-scene nodepath
|
||||
|
||||
# chunk_id -> {
|
||||
# "dynamic_np": NodePath,
|
||||
# "static_np": NodePath or None,
|
||||
# "members": [global_id],
|
||||
# "dirty": bool,
|
||||
# "dynamic_enabled": bool
|
||||
# }
|
||||
self.chunks = {}
|
||||
self.active_chunks = set()
|
||||
self._next_chunk_id = 0
|
||||
# spatial cell key -> [chunk_id, ...]
|
||||
self._cell_to_chunks = {}
|
||||
|
||||
# UI hierarchy metadata (matches source model parent/child structure)
|
||||
self.tree_root_key = None
|
||||
self.tree_nodes = {}
|
||||
self._path_to_tree_key = {}
|
||||
|
||||
def _register_tree_node(self, key, display_name, parent_key):
|
||||
self.tree_nodes[key] = {
|
||||
"name": display_name,
|
||||
"parent": parent_key,
|
||||
"children": [],
|
||||
"local_ids": [],
|
||||
}
|
||||
self.display_names[key] = display_name
|
||||
self.name_to_ids[key] = []
|
||||
if parent_key is not None and parent_key in self.tree_nodes:
|
||||
self.tree_nodes[parent_key]["children"].append(key)
|
||||
|
||||
def _build_scene_tree(self, root_np):
|
||||
"""Capture source model hierarchy for UI (independent from render batching)."""
|
||||
self.tree_root_key = "0"
|
||||
|
||||
def walk(np, parent_key, key):
|
||||
display_name = np.get_name() or "Unnamed"
|
||||
self._register_tree_node(key, display_name, parent_key)
|
||||
self._path_to_tree_key[str(np)] = key
|
||||
|
||||
children = list(np.get_children())
|
||||
for i, child in enumerate(children):
|
||||
walk(child, key, f"{key}/{i}")
|
||||
|
||||
walk(root_np, None, self.tree_root_key)
|
||||
|
||||
def _get_model_world_mat(self):
|
||||
"""Return current model net transform matrix (to top/root)."""
|
||||
if not self.model:
|
||||
return LMatrix4f.ident_mat()
|
||||
try:
|
||||
if self.model.isEmpty():
|
||||
return LMatrix4f.ident_mat()
|
||||
except Exception:
|
||||
try:
|
||||
if self.model.is_empty():
|
||||
return LMatrix4f.ident_mat()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return LMatrix4f(self.model.getNetTransform().getMat())
|
||||
except Exception:
|
||||
try:
|
||||
# snake_case fallback in newer Panda3D bindings
|
||||
return LMatrix4f(self.model.get_net_transform().get_mat())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
top = self.model.getTop()
|
||||
if top and not top.isEmpty():
|
||||
return LMatrix4f(self.model.getMat(top))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return LMatrix4f(self.model.getMat())
|
||||
except Exception:
|
||||
return LMatrix4f.ident_mat()
|
||||
|
||||
def get_model_world_mat(self):
|
||||
"""Public accessor for current model net transform matrix."""
|
||||
return self._get_model_world_mat()
|
||||
|
||||
def _local_point_to_world(self, local_pos):
|
||||
"""Convert a local-space point to world-space based on model net transform."""
|
||||
mat = self._get_model_world_mat()
|
||||
p = Point3(float(local_pos.x), float(local_pos.y), float(local_pos.z))
|
||||
wp = mat.xformPoint(p)
|
||||
return Vec3(wp.x, wp.y, wp.z)
|
||||
|
||||
def _world_vec_to_local(self, world_vec):
|
||||
"""Convert a world-space vector to model-local space."""
|
||||
mat = self._get_model_world_mat()
|
||||
inv = LMatrix4f(mat)
|
||||
try:
|
||||
inv.invertInPlace()
|
||||
except Exception:
|
||||
try:
|
||||
inv.invert_in_place()
|
||||
except Exception:
|
||||
return Vec3(world_vec)
|
||||
v = Vec3(world_vec)
|
||||
lv = inv.xformVec(v)
|
||||
return Vec3(lv.x, lv.y, lv.z)
|
||||
|
||||
def world_vector_to_model_local(self, world_vec):
|
||||
"""Public converter from world delta vector to model-local delta vector."""
|
||||
return self._world_vec_to_local(world_vec)
|
||||
|
||||
def get_model_world_quat(self):
|
||||
"""Return current model world quaternion."""
|
||||
if not self.model:
|
||||
return Quat.identQuat()
|
||||
try:
|
||||
if self.model.isEmpty():
|
||||
return Quat.identQuat()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
top = self.model.getTop()
|
||||
if top and not top.isEmpty():
|
||||
return Quat(self.model.getQuat(top))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return Quat(self.model.getQuat())
|
||||
except Exception:
|
||||
return Quat.identQuat()
|
||||
|
||||
def world_quat_delta_to_model_local(self, delta_quat_world):
|
||||
"""
|
||||
Convert world-space delta quaternion to model-local delta quaternion.
|
||||
local = inv(model_world_rot) * world_delta * model_world_rot
|
||||
"""
|
||||
if delta_quat_world is None:
|
||||
return Quat.identQuat()
|
||||
model_q = self.get_model_world_quat()
|
||||
inv_model_q = Quat(model_q)
|
||||
inv_model_q.invertInPlace()
|
||||
local_q = inv_model_q * Quat(delta_quat_world) * model_q
|
||||
local_q.normalize()
|
||||
return local_q
|
||||
|
||||
def _build_original_hierarchy_key(self, np, model_root):
|
||||
"""Capture hierarchy path before flatten/reparent."""
|
||||
@ -60,6 +202,71 @@ class ObjectController:
|
||||
if not parts:
|
||||
return np.get_name() or "Unnamed"
|
||||
return "/".join(parts)
|
||||
def _aggregate_tree_ids(self, key):
|
||||
node = self.tree_nodes[key]
|
||||
agg_ids = list(node["local_ids"])
|
||||
for child_key in node["children"]:
|
||||
agg_ids.extend(self._aggregate_tree_ids(child_key))
|
||||
self.name_to_ids[key] = agg_ids
|
||||
return agg_ids
|
||||
|
||||
def _build_tree_preorder(self, key, out):
|
||||
out.append(key)
|
||||
for child_key in self.tree_nodes[key]["children"]:
|
||||
self._build_tree_preorder(child_key, out)
|
||||
|
||||
def should_hide_tree_node(self, key):
|
||||
"""
|
||||
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
|
||||
This keeps `model.glb` as the visible root in the UI.
|
||||
"""
|
||||
node = self.tree_nodes.get(key)
|
||||
if not node:
|
||||
return False
|
||||
if node["parent"] != self.tree_root_key:
|
||||
return False
|
||||
|
||||
name = (node["name"] or "").strip().lower()
|
||||
if name != "root":
|
||||
return False
|
||||
|
||||
# Keep node visible if it actually carries direct geoms.
|
||||
if node["local_ids"]:
|
||||
return False
|
||||
|
||||
return len(node["children"]) > 0
|
||||
|
||||
def _encode_id_color(self, vdata, object_id):
|
||||
if not vdata.has_column("color"):
|
||||
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
vdata.set_format(new_fmt)
|
||||
|
||||
low = object_id & 0xFF
|
||||
high = (object_id >> 8) & 0xFF
|
||||
r = low / 255.0
|
||||
g = high / 255.0
|
||||
|
||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||
for row in range(vdata.get_num_rows()):
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, 0.0, 1.0)
|
||||
|
||||
def _ensure_chunk(self, root_np, chunk_id):
|
||||
if chunk_id in self.chunks:
|
||||
return self.chunks[chunk_id]
|
||||
|
||||
dynamic_np = root_np.attach_new_node(f"chunk_{chunk_id:04d}_dynamic")
|
||||
dynamic_np.stash()
|
||||
|
||||
chunk_data = {
|
||||
"dynamic_np": dynamic_np,
|
||||
"static_np": None,
|
||||
"members": [],
|
||||
"dirty": False,
|
||||
"dynamic_enabled": False,
|
||||
}
|
||||
self.chunks[chunk_id] = chunk_data
|
||||
return chunk_data
|
||||
|
||||
def _is_wrapper_segment(self, segment):
|
||||
s = (segment or "").strip().lower()
|
||||
@ -99,20 +306,21 @@ class ObjectController:
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.pick_vertex_index = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
self.pick_model = None
|
||||
model_name = (model.get_name() or "").strip()
|
||||
self._source_model_name = model_name.lower()
|
||||
self._source_model_stem = model_name.rsplit(".", 1)[0].lower() if "." in model_name else model_name.lower()
|
||||
|
||||
|
||||
global_id_counter = 0
|
||||
chunk_key = model.get_name() or "default"
|
||||
|
||||
|
||||
# No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py)
|
||||
self.chunk_node = model
|
||||
self.chunks[chunk_key] = {'node': model, 'base_id': 0}
|
||||
|
||||
|
||||
# Cache original hierarchy path BEFORE flatten/reparent.
|
||||
original_keys = {}
|
||||
for np in geom_nodes:
|
||||
@ -121,12 +329,12 @@ class ObjectController:
|
||||
# Flatten hierarchy
|
||||
for np in geom_nodes:
|
||||
np.wrt_reparent_to(model)
|
||||
|
||||
|
||||
local_idx = 0
|
||||
|
||||
|
||||
for np in geom_nodes:
|
||||
gnode = np.node()
|
||||
|
||||
|
||||
if gnode.get_num_parents() > 1:
|
||||
parent = np.get_parent()
|
||||
if not parent.is_empty():
|
||||
@ -134,57 +342,57 @@ class ObjectController:
|
||||
np.detach_node()
|
||||
np = new_np
|
||||
gnode = np.node()
|
||||
|
||||
|
||||
unique_key = original_keys.get(id(np), str(np))
|
||||
display_name = np.get_name() or f"Object_{global_id_counter}"
|
||||
|
||||
|
||||
if unique_key not in self.name_to_ids:
|
||||
self.name_to_ids[unique_key] = []
|
||||
self.key_to_node[unique_key] = np
|
||||
self.node_list.append(unique_key)
|
||||
self.display_names[unique_key] = display_name
|
||||
|
||||
|
||||
# Save original transform
|
||||
mat_double = np.get_mat()
|
||||
original_transform = LMatrix4f(mat_double)
|
||||
|
||||
|
||||
for i in range(gnode.get_num_geoms()):
|
||||
geom = gnode.modify_geom(i)
|
||||
vdata = geom.modify_vertex_data()
|
||||
|
||||
|
||||
if not vdata.has_column("color"):
|
||||
new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
vdata.set_format(new_format)
|
||||
|
||||
|
||||
# Encode Local ID in R/G
|
||||
low = local_idx % 256
|
||||
high = local_idx // 256
|
||||
r = low / 255.0
|
||||
g = high / 255.0
|
||||
|
||||
|
||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||
for row in range(vdata.get_num_rows()):
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, 0.0, 1.0)
|
||||
|
||||
|
||||
self.global_transforms.append(original_transform)
|
||||
self.id_to_chunk[global_id_counter] = (chunk_key, local_idx)
|
||||
self.name_to_ids[unique_key].append(global_id_counter)
|
||||
self.id_to_name[global_id_counter] = unique_key
|
||||
self.local_to_global_id[local_idx] = global_id_counter
|
||||
self.position_offsets[local_idx] = Vec3(0, 0, 0)
|
||||
|
||||
|
||||
global_id_counter += 1
|
||||
local_idx += 1
|
||||
|
||||
|
||||
# DO NOT reset transform — keep world-space positions
|
||||
|
||||
|
||||
# Flatten directly on model — NO set_final, allows per-geom frustum culling
|
||||
model.flatten_strong()
|
||||
|
||||
|
||||
t1 = time.time()
|
||||
print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms")
|
||||
|
||||
|
||||
# Build vertex index AFTER flatten
|
||||
self._build_vertex_index(model)
|
||||
self._init_local_transform_state()
|
||||
@ -192,6 +400,7 @@ class ObjectController:
|
||||
|
||||
# Keep ID colors only in picking clone to avoid affecting visible shading.
|
||||
self.pick_model = model.copy_to(NodePath("ssbo_pick_root"))
|
||||
self._build_pick_vertex_index(self.pick_model)
|
||||
self._set_uniform_vertex_color(model, 1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
t2 = time.time()
|
||||
@ -219,6 +428,114 @@ class ObjectController:
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, b, a)
|
||||
|
||||
def _build_tree_preorder(self, key, out):
|
||||
out.append(key)
|
||||
for child_key in self.tree_nodes[key]["children"]:
|
||||
self._build_tree_preorder(child_key, out)
|
||||
|
||||
def should_hide_tree_node(self, key):
|
||||
"""
|
||||
Hide a redundant wrapper node directly below the file root, e.g. ROOT.
|
||||
This keeps `model.glb` as the visible root in the UI.
|
||||
"""
|
||||
node = self.tree_nodes.get(key)
|
||||
if not node:
|
||||
return False
|
||||
if node["parent"] != self.tree_root_key:
|
||||
return False
|
||||
|
||||
name = (node["name"] or "").strip().lower()
|
||||
if name != "root":
|
||||
return False
|
||||
|
||||
# Keep node visible if it actually carries direct geoms.
|
||||
if node["local_ids"]:
|
||||
return False
|
||||
|
||||
return len(node["children"]) > 0
|
||||
|
||||
def _encode_id_color(self, vdata, object_id):
|
||||
if not vdata.has_column("color"):
|
||||
new_fmt = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
vdata.set_format(new_fmt)
|
||||
|
||||
low = object_id & 0xFF
|
||||
high = (object_id >> 8) & 0xFF
|
||||
r = low / 255.0
|
||||
g = high / 255.0
|
||||
|
||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||
for row in range(vdata.get_num_rows()):
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, 0.0, 1.0)
|
||||
|
||||
def _ensure_chunk(self, root_np, chunk_id):
|
||||
if chunk_id in self.chunks:
|
||||
return self.chunks[chunk_id]
|
||||
|
||||
dynamic_np = root_np.attach_new_node(f"chunk_{chunk_id:04d}_dynamic")
|
||||
dynamic_np.stash()
|
||||
|
||||
chunk_data = {
|
||||
"dynamic_np": dynamic_np,
|
||||
"static_np": None,
|
||||
"members": [],
|
||||
"dirty": False,
|
||||
"dynamic_enabled": False,
|
||||
}
|
||||
self.chunks[chunk_id] = chunk_data
|
||||
return chunk_data
|
||||
|
||||
def _get_cell_key_from_pos(self, pos):
|
||||
inv = 1.0 / self.chunk_world_size
|
||||
return (
|
||||
int(math.floor(pos.x * inv)),
|
||||
int(math.floor(pos.y * inv)),
|
||||
int(math.floor(pos.z * inv)),
|
||||
)
|
||||
|
||||
def _allocate_spatial_chunk(self, root_np, world_pos):
|
||||
"""
|
||||
Allocate object into a spatially-local chunk for better frustum culling.
|
||||
Objects in the same world cell are grouped together, and overflow creates
|
||||
another chunk for that same cell.
|
||||
"""
|
||||
cell_key = self._get_cell_key_from_pos(world_pos)
|
||||
chunk_ids = self._cell_to_chunks.setdefault(cell_key, [])
|
||||
|
||||
for chunk_id in chunk_ids:
|
||||
chunk = self.chunks.get(chunk_id)
|
||||
if chunk and len(chunk["members"]) < self.chunk_size:
|
||||
return chunk_id, chunk
|
||||
|
||||
chunk_id = self._next_chunk_id
|
||||
self._next_chunk_id += 1
|
||||
chunk_ids.append(chunk_id)
|
||||
return chunk_id, self._ensure_chunk(root_np, chunk_id)
|
||||
|
||||
def _rebuild_static_chunk(self, chunk_id):
|
||||
chunk = self.chunks.get(chunk_id)
|
||||
if not chunk:
|
||||
return
|
||||
|
||||
old_static = chunk.get("static_np")
|
||||
if old_static and not old_static.is_empty():
|
||||
old_static.remove_node()
|
||||
|
||||
static_np = chunk["dynamic_np"].copy_to(self.model)
|
||||
static_np.set_name(f"chunk_{chunk_id:04d}_static")
|
||||
static_np.unstash()
|
||||
static_np.flatten_strong()
|
||||
|
||||
chunk["static_np"] = static_np
|
||||
chunk["dirty"] = False
|
||||
|
||||
# Keep visibility coherent with current mode after rebuild.
|
||||
if chunk["dynamic_enabled"]:
|
||||
static_np.stash()
|
||||
else:
|
||||
static_np.unstash()
|
||||
|
||||
def build_virtual_hierarchy(self):
|
||||
"""Build a readonly virtual tree from node_list path keys."""
|
||||
root = {
|
||||
@ -403,6 +720,228 @@ class ObjectController:
|
||||
self.vertex_index[uid].append((gn_np, gi, rows))
|
||||
self.original_positions[uid].append(pos.copy())
|
||||
|
||||
def _set_chunk_dynamic(self, chunk_id, enabled):
|
||||
chunk = self.chunks.get(chunk_id)
|
||||
if not chunk:
|
||||
return
|
||||
|
||||
if enabled:
|
||||
if chunk["dynamic_enabled"]:
|
||||
return
|
||||
chunk["dynamic_np"].unstash()
|
||||
if chunk["static_np"] and not chunk["static_np"].is_empty():
|
||||
chunk["static_np"].stash()
|
||||
chunk["dynamic_enabled"] = True
|
||||
self.active_chunks.add(chunk_id)
|
||||
return
|
||||
|
||||
if not chunk["dynamic_enabled"]:
|
||||
return
|
||||
if chunk["static_np"] and not chunk["static_np"].is_empty():
|
||||
chunk["static_np"].unstash()
|
||||
chunk["dynamic_np"].stash()
|
||||
chunk["dynamic_enabled"] = False
|
||||
self.active_chunks.discard(chunk_id)
|
||||
|
||||
def _resolve_chunk_and_local_idx(self, global_id):
|
||||
"""
|
||||
Compatibility helper for merged branches:
|
||||
- legacy: id_to_chunk[gid] -> (chunk_id, local_idx)
|
||||
- current: id_to_chunk[gid] -> chunk_id (local_idx defaults to gid)
|
||||
"""
|
||||
mapping = self.id_to_chunk.get(global_id)
|
||||
if mapping is None:
|
||||
return None, None
|
||||
if isinstance(mapping, (tuple, list)):
|
||||
if not mapping:
|
||||
return None, None
|
||||
chunk_id = mapping[0]
|
||||
local_idx = mapping[1] if len(mapping) > 1 else global_id
|
||||
return chunk_id, local_idx
|
||||
return mapping, global_id
|
||||
|
||||
def set_active_ids(self, active_ids):
|
||||
"""切换编辑激活集合,仅保留 active_ids 对应 chunk 为动态模式。"""
|
||||
target_chunks = set()
|
||||
for obj_id in active_ids:
|
||||
chunk_id, _ = self._resolve_chunk_and_local_idx(obj_id)
|
||||
if chunk_id is not None:
|
||||
target_chunks.add(chunk_id)
|
||||
|
||||
# Demote no-longer-active chunks. Dirty chunks are re-baked before demotion.
|
||||
for chunk_id in list(self.active_chunks):
|
||||
if chunk_id in target_chunks:
|
||||
continue
|
||||
if self.chunks[chunk_id]["dirty"]:
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
self._set_chunk_dynamic(chunk_id, False)
|
||||
|
||||
# Promote target chunks.
|
||||
for chunk_id in target_chunks:
|
||||
self._set_chunk_dynamic(chunk_id, True)
|
||||
|
||||
def bake_ids_and_collect(self, model):
|
||||
"""
|
||||
构建混合架构:
|
||||
1) 把每个 geom 拆成可独立编辑的动态对象
|
||||
2) 按 chunk 生成 flatten 后的静态副本
|
||||
"""
|
||||
t0 = time.time()
|
||||
self._reset_state()
|
||||
|
||||
geom_nodes = list(model.find_all_matches("**/+GeomNode"))
|
||||
print(f"[控制器] 找到 {len(geom_nodes)} 个 GeomNode")
|
||||
|
||||
# Build hierarchy metadata first so UI can mirror source model tree.
|
||||
self._build_scene_tree(model)
|
||||
|
||||
root_name = model.get_name() or "scene"
|
||||
scene_root = NodePath(root_name)
|
||||
pick_root = NodePath(root_name + "_pick")
|
||||
self.model = scene_root
|
||||
self.pick_model = pick_root
|
||||
|
||||
global_id = 0
|
||||
for np in geom_nodes:
|
||||
gnode = np.node()
|
||||
owner_key = self._path_to_tree_key.get(str(np), self.tree_root_key)
|
||||
|
||||
world_mat = LMatrix4f(np.get_mat(model))
|
||||
|
||||
for gi in range(gnode.get_num_geoms()):
|
||||
# Render geometry stays untouched (keep original material/color behavior).
|
||||
render_geom = gnode.get_geom(gi).make_copy()
|
||||
render_gnode = GeomNode(f"obj_{global_id}")
|
||||
render_gnode.add_geom(render_geom, gnode.get_geom_state(gi))
|
||||
|
||||
# Picking geometry gets encoded ID in vertex color.
|
||||
pick_geom = gnode.get_geom(gi).make_copy()
|
||||
pick_vdata = pick_geom.modify_vertex_data()
|
||||
self._encode_id_color(pick_vdata, global_id)
|
||||
pick_gnode = GeomNode(f"pick_{global_id}")
|
||||
pick_gnode.add_geom(pick_geom, gnode.get_geom_state(gi))
|
||||
|
||||
world_pos = world_mat.get_row3(3)
|
||||
chunk_id, chunk = self._allocate_spatial_chunk(scene_root, world_pos)
|
||||
|
||||
obj_np = chunk["dynamic_np"].attach_new_node(render_gnode)
|
||||
obj_np.set_mat(world_mat)
|
||||
pick_np = pick_root.attach_new_node(pick_gnode)
|
||||
pick_np.set_mat(world_mat)
|
||||
|
||||
chunk["members"].append(global_id)
|
||||
self.id_to_chunk[global_id] = chunk_id
|
||||
self.id_to_object_np[global_id] = obj_np
|
||||
self.id_to_pick_np[global_id] = pick_np
|
||||
self.tree_nodes[owner_key]["local_ids"].append(global_id)
|
||||
self.id_to_name[global_id] = owner_key
|
||||
self.global_transforms.append(LMatrix4f(world_mat))
|
||||
self.position_offsets[global_id] = Vec3(0, 0, 0)
|
||||
global_id += 1
|
||||
|
||||
t1 = time.time()
|
||||
print(f"[控制器] Dynamic object build took {(t1 - t0) * 1000:.0f}ms")
|
||||
|
||||
for chunk_id in sorted(self.chunks):
|
||||
self._rebuild_static_chunk(chunk_id)
|
||||
self._set_chunk_dynamic(chunk_id, False)
|
||||
|
||||
t2 = time.time()
|
||||
print(f"[控制器] Static chunk flatten took {(t2 - t1) * 1000:.0f}ms")
|
||||
print(f"[控制器] Built {len(self.chunks)} chunks, {global_id} objects")
|
||||
print(f"[控制器] Spatial chunking: cell={self.chunk_world_size:.1f}, max_members={self.chunk_size}")
|
||||
|
||||
# Fill per-node aggregate IDs and build deterministic preorder list for UI.
|
||||
self._aggregate_tree_ids(self.tree_root_key)
|
||||
self.node_list = []
|
||||
self._build_tree_preorder(self.tree_root_key, self.node_list)
|
||||
|
||||
model.remove_node()
|
||||
return global_id
|
||||
|
||||
def _build_pick_vertex_index(self, pick_root):
|
||||
"""
|
||||
Build local_id -> [(geom_node_np, geom_idx, row_indices_array)] for pick model.
|
||||
This keeps GPU-picking geometry writable in sync with visible geometry edits.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
self.pick_vertex_index = {}
|
||||
if not pick_root:
|
||||
return
|
||||
|
||||
for gn_np in pick_root.find_all_matches("**/+GeomNode"):
|
||||
gnode = gn_np.node()
|
||||
for gi in range(gnode.get_num_geoms()):
|
||||
geom = gnode.get_geom(gi)
|
||||
vdata = geom.get_vertex_data()
|
||||
num_rows = vdata.get_num_rows()
|
||||
if num_rows == 0:
|
||||
continue
|
||||
|
||||
fmt = vdata.get_format()
|
||||
color_col = fmt.get_column(InternalName.make("color"))
|
||||
if color_col is None:
|
||||
continue
|
||||
|
||||
color_array_idx = fmt.get_array_with(InternalName.make("color"))
|
||||
color_start = color_col.get_start()
|
||||
color_array_format = fmt.get_array(color_array_idx)
|
||||
color_stride = color_array_format.get_stride()
|
||||
|
||||
color_handle = vdata.get_array(color_array_idx).get_handle()
|
||||
color_raw = bytes(color_handle.get_data())
|
||||
color_buf = np.frombuffer(color_raw, dtype=np.uint8).reshape(num_rows, color_stride)
|
||||
|
||||
num_components = color_col.get_num_components()
|
||||
component_bytes = color_col.get_component_bytes()
|
||||
|
||||
if component_bytes == 4:
|
||||
color_data = np.ndarray(
|
||||
(num_rows, num_components),
|
||||
dtype=np.float32,
|
||||
buffer=color_buf[:, color_start:color_start + num_components * 4].tobytes()
|
||||
)
|
||||
r_vals = (color_data[:, 0] * 255.0 + 0.5).astype(np.int32)
|
||||
g_vals = (color_data[:, 1] * 255.0 + 0.5).astype(np.int32)
|
||||
elif component_bytes == 1:
|
||||
color_bytes = color_buf[:, color_start:color_start + num_components].copy()
|
||||
r_vals = color_bytes[:, 0].astype(np.int32)
|
||||
g_vals = color_bytes[:, 1].astype(np.int32)
|
||||
else:
|
||||
continue
|
||||
|
||||
local_ids = r_vals + (g_vals << 8)
|
||||
sort_idx = np.argsort(local_ids)
|
||||
sorted_ids = local_ids[sort_idx]
|
||||
boundaries = np.where(np.diff(sorted_ids) != 0)[0] + 1
|
||||
id_groups = np.split(sort_idx, boundaries)
|
||||
group_ids = sorted_ids[np.concatenate([[0], boundaries])]
|
||||
|
||||
for k in range(len(group_ids)):
|
||||
uid = int(group_ids[k])
|
||||
rows = id_groups[k]
|
||||
if uid not in self.pick_vertex_index:
|
||||
self.pick_vertex_index[uid] = []
|
||||
self.pick_vertex_index[uid].append((gn_np, gi, rows))
|
||||
|
||||
def _apply_vertices_to_pick(self, local_idx, entry_idx, new_pos):
|
||||
"""Mirror one transformed vertex group to pick-model geometry."""
|
||||
pick_entries = self.pick_vertex_index.get(local_idx)
|
||||
if not pick_entries or entry_idx >= len(pick_entries):
|
||||
return
|
||||
|
||||
pick_gn_np, pick_gi, pick_rows = pick_entries[entry_idx]
|
||||
gnode = pick_gn_np.node()
|
||||
geom = gnode.modify_geom(pick_gi)
|
||||
vdata = geom.modify_vertex_data()
|
||||
writer = GeomVertexWriter(vdata, "vertex")
|
||||
|
||||
max_rows = min(len(pick_rows), len(new_pos))
|
||||
for j in range(max_rows):
|
||||
writer.set_row(int(pick_rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
|
||||
def _init_local_transform_state(self):
|
||||
"""Initialize transform state for each local_idx after vertex index is ready."""
|
||||
self.local_transform_state = {}
|
||||
@ -424,10 +963,9 @@ class ObjectController:
|
||||
return local_indices
|
||||
seen = set()
|
||||
for global_id in global_ids:
|
||||
mapping = self.id_to_chunk.get(global_id)
|
||||
if not mapping:
|
||||
_, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||
if local_idx is None:
|
||||
continue
|
||||
_, local_idx = mapping
|
||||
if local_idx in seen:
|
||||
continue
|
||||
if local_idx not in self.vertex_index:
|
||||
@ -437,7 +975,7 @@ class ObjectController:
|
||||
return local_indices
|
||||
|
||||
def get_local_pivot(self, local_idx):
|
||||
"""Get pivot for one local object (world-space center)."""
|
||||
"""Get pivot for one local object (model-local center)."""
|
||||
global_id = self.local_to_global_id.get(local_idx)
|
||||
if global_id is None:
|
||||
return Vec3(0, 0, 0)
|
||||
@ -457,7 +995,8 @@ class ObjectController:
|
||||
valid += 1
|
||||
if valid == 0:
|
||||
return Vec3(0, 0, 0)
|
||||
return acc / float(valid)
|
||||
center_local = acc / float(valid)
|
||||
return self._local_point_to_world(center_local)
|
||||
|
||||
def begin_transform_session(self, local_indices):
|
||||
"""Create immutable baseline snapshot for one gizmo drag session."""
|
||||
@ -543,6 +1082,7 @@ class ObjectController:
|
||||
for j in range(len(rows)):
|
||||
writer.set_row(int(rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
self._apply_vertices_to_pick(local_idx, i, new_pos)
|
||||
|
||||
def _quat_to_np_mat3(self, quat):
|
||||
"""Convert Panda3D Quat to 3x3 numpy rotation matrix."""
|
||||
@ -571,7 +1111,7 @@ class ObjectController:
|
||||
], dtype=np.float32)
|
||||
|
||||
def create_ssbo(self):
|
||||
"""No SSBO needed — using RP default rendering."""
|
||||
"""No SSBO needed in hybrid mode."""
|
||||
return None
|
||||
|
||||
def move_object(self, global_id, delta):
|
||||
@ -584,10 +1124,28 @@ class ObjectController:
|
||||
|
||||
if global_id not in self.id_to_chunk:
|
||||
return
|
||||
|
||||
_, local_idx = self.id_to_chunk[global_id]
|
||||
|
||||
if local_idx not in self.vertex_index:
|
||||
|
||||
chunk_id, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||
if local_idx is None:
|
||||
return
|
||||
|
||||
# Hybrid chunk mode (current) may move NodePaths directly without
|
||||
# vertex_index/original_positions populated.
|
||||
if local_idx not in self.vertex_index or local_idx not in self.original_positions:
|
||||
obj_np = self.id_to_object_np.get(global_id)
|
||||
if not obj_np or obj_np.is_empty():
|
||||
return
|
||||
next_pos = obj_np.get_pos() + delta
|
||||
if hasattr(obj_np, "set_fluid_pos"):
|
||||
obj_np.set_fluid_pos(next_pos)
|
||||
else:
|
||||
obj_np.set_pos(next_pos)
|
||||
pick_np = self.id_to_pick_np.get(global_id)
|
||||
if pick_np and not pick_np.is_empty():
|
||||
pick_np.set_mat(self.model, obj_np.get_mat(self.model))
|
||||
if chunk_id is not None and chunk_id in self.chunks:
|
||||
self.chunks[chunk_id]["dirty"] = True
|
||||
self.position_offsets[local_idx] = self.position_offsets.get(local_idx, Vec3(0)) + delta
|
||||
return
|
||||
|
||||
# Accumulate offset
|
||||
@ -611,28 +1169,35 @@ class ObjectController:
|
||||
for j in range(len(rows)):
|
||||
writer.set_row(int(rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
self._apply_vertices_to_pick(local_idx, i, new_pos)
|
||||
|
||||
def get_world_pos(self, global_id):
|
||||
"""Get current world position of an object."""
|
||||
if global_id not in self.id_to_chunk:
|
||||
if not self.model:
|
||||
return Vec3(0, 0, 0)
|
||||
|
||||
obj_np = self.id_to_object_np.get(global_id)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
p = obj_np.get_pos(self.model)
|
||||
return self._local_point_to_world(Vec3(p))
|
||||
|
||||
_, local_idx = self._resolve_chunk_and_local_idx(global_id)
|
||||
if local_idx is None:
|
||||
return Vec3(0, 0, 0)
|
||||
_, local_idx = self.id_to_chunk[global_id]
|
||||
|
||||
original_mat = self.global_transforms[global_id]
|
||||
original_pos = original_mat.get_row3(3)
|
||||
offset = self.position_offsets.get(local_idx, Vec3(0))
|
||||
|
||||
return Vec3(original_pos) + offset
|
||||
|
||||
local_pos = Vec3(original_pos) + offset
|
||||
return self._local_point_to_world(local_pos)
|
||||
|
||||
def get_object_center(self, global_id):
|
||||
"""Get the original center position of an object (for rotation pivot)."""
|
||||
if global_id >= len(self.global_transforms):
|
||||
return Vec3(0, 0, 0)
|
||||
mat = self.global_transforms[global_id]
|
||||
return Vec3(mat.get_row3(3))
|
||||
|
||||
def get_transform(self, global_id):
|
||||
"""Get original transform."""
|
||||
if global_id >= len(self.global_transforms):
|
||||
return LMatrix4f.ident_mat()
|
||||
return self.global_transforms[global_id]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
测试动画模型 - Panda3D应用程序
|
||||
使用Panda3D引擎编辑器创建
|
||||
打包
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
@ -14,7 +13,6 @@ from direct.actor.Actor import Actor
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \
|
||||
Point3
|
||||
from core.InfoPanelManager import InfoPanelManager
|
||||
# 获取渲染管线路径
|
||||
# 在文件开头添加sys导入(如果还没有的话)
|
||||
import sys
|
||||
@ -98,8 +96,6 @@ class MainApp(ShowBase):
|
||||
# 加载所有脚本e
|
||||
self.script_manager.load_all_scripts_from_directory()
|
||||
|
||||
self.info_panel_manager = InfoPanelManager(self)
|
||||
|
||||
try:
|
||||
# 再导入controller模块
|
||||
from rpcore.util.movement_controller import MovementController
|
||||
@ -539,8 +535,6 @@ class MainApp(ShowBase):
|
||||
video_path=video_path,
|
||||
size=absolute_scale
|
||||
)
|
||||
elif gui_type == "info_panel":
|
||||
new_element = self.info_panel_manager.onCreateSampleInfoPanel()
|
||||
|
||||
if "scripts" in gui_info and new_element:
|
||||
self.processScripts(new_element,gui_info["scripts"])
|
||||
|
||||
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.task.TaskManagerGlobal import taskMgr
|
||||
from panda3d.core import NodePath, PartSubset
|
||||
from panda3d.core import NodePath, PartSubset, Filename
|
||||
|
||||
|
||||
class _BoundAnimationProxy:
|
||||
@ -704,7 +704,22 @@ class AnimationTools:
|
||||
|
||||
def _try_create_actor_from_source(source, source_desc):
|
||||
try:
|
||||
actor = Actor(source)
|
||||
resolved_source = source
|
||||
if isinstance(source, (str, os.PathLike)):
|
||||
src_text = os.fspath(source)
|
||||
resolved_source = src_text
|
||||
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||||
ctor = getattr(Filename, ctor_name, None)
|
||||
if not ctor:
|
||||
continue
|
||||
try:
|
||||
candidate = ctor(src_text).get_fullpath()
|
||||
if candidate:
|
||||
resolved_source = candidate
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
actor = Actor(resolved_source)
|
||||
# 无论是否已检测到动画名,都显式绑定一次,避免“有名字但无可播放控制”
|
||||
try:
|
||||
actor.bindAllAnims(allowAsyncBind=False)
|
||||
@ -1139,7 +1154,10 @@ class AnimationTools:
|
||||
|
||||
# 路径 Actor 失败后,再尝试把文件作为普通模型加载并 autoBind
|
||||
try:
|
||||
loaded_model = self.loader.loadModel(p)
|
||||
model_source = p
|
||||
if isinstance(p, (str, os.PathLike)):
|
||||
model_source = Filename.from_os_specific(os.fspath(p))
|
||||
loaded_model = self.loader.loadModel(model_source)
|
||||
if loaded_model and not loaded_model.isEmpty():
|
||||
proxy = _try_create_autobind_proxy(loaded_model, f"文件路径({p})", owns_node=True)
|
||||
if proxy:
|
||||
|
||||
@ -256,12 +256,105 @@ class AppActions:
|
||||
self.add_info_message("另存为项目(功能待实现)")
|
||||
# TODO: 实现另存为对话框
|
||||
# self.show_save_as_dialog = True
|
||||
|
||||
def _on_build_webgl_package(self):
|
||||
"""处理“打包为 WebGL”菜单项。"""
|
||||
if not hasattr(self, "project_manager") or not self.project_manager:
|
||||
self.add_error_message("项目管理器未初始化")
|
||||
return
|
||||
|
||||
if not self.project_manager.current_project_path:
|
||||
self.add_warning_message("请先创建或打开项目,再进行WebGL打包")
|
||||
return
|
||||
|
||||
# 导出前先保存当前项目
|
||||
if not self._save_project_impl():
|
||||
self.add_error_message("打包前自动保存失败,已取消WebGL打包")
|
||||
return
|
||||
|
||||
current_project_path = self.project_manager.current_project_path
|
||||
initial_dir = os.path.dirname(current_project_path) if current_project_path else os.getcwd()
|
||||
selected_dir = self._select_directory_system_dialog("选择 WebGL 打包输出目录", initial_dir)
|
||||
if not selected_dir:
|
||||
dialog_error = getattr(self, "_last_directory_dialog_error", "")
|
||||
if dialog_error:
|
||||
# Fallback: when system dialog is unavailable (e.g. missing tkinter),
|
||||
# reuse the built-in path browser so user can still choose directory.
|
||||
self.add_warning_message(f"系统目录选择器不可用: {dialog_error}")
|
||||
self.path_browser_mode = "webgl_build"
|
||||
self.path_browser_current_path = initial_dir if os.path.isdir(initial_dir) else os.getcwd()
|
||||
self.path_browser_selected_path = self.path_browser_current_path
|
||||
self.show_path_browser = True
|
||||
self._pending_webgl_package = True
|
||||
self._refresh_path_browser()
|
||||
self.add_info_message("已切换到内置路径浏览器,请选择输出目录并点击确定")
|
||||
else:
|
||||
self.add_info_message("已取消WebGL打包")
|
||||
return
|
||||
|
||||
self._execute_webgl_package(selected_dir)
|
||||
|
||||
def _execute_webgl_package(self, selected_dir):
|
||||
"""执行WebGL打包并输出消息。"""
|
||||
ok = self.project_manager.buildWebGLPackage(selected_dir)
|
||||
report = getattr(self.project_manager, "last_webgl_export_report", None) or {}
|
||||
status = report.get("status", "failed")
|
||||
out_dir = report.get("output_dir", "")
|
||||
report_path = os.path.join(out_dir, "reports", "export_report.json") if out_dir else ""
|
||||
|
||||
if ok:
|
||||
missing_count = len(report.get("missing_assets", []))
|
||||
unsupported_count = len(report.get("unsupported_assets", []))
|
||||
if status == "partial":
|
||||
self.add_warning_message(
|
||||
f"WebGL打包部分成功: 缺失资源 {missing_count},不支持资源 {unsupported_count}"
|
||||
)
|
||||
else:
|
||||
self.add_success_message("WebGL打包成功")
|
||||
|
||||
if out_dir:
|
||||
self.add_info_message(f"输出目录: {out_dir}")
|
||||
if report_path:
|
||||
self.add_info_message(f"报告: {report_path}")
|
||||
else:
|
||||
self.add_error_message("WebGL打包失败")
|
||||
if report_path:
|
||||
self.add_warning_message(f"请检查报告: {report_path}")
|
||||
|
||||
|
||||
def _on_exit(self):
|
||||
"""处理退出菜单项"""
|
||||
self.add_info_message("退出应用程序")
|
||||
self.userExit()
|
||||
|
||||
def _select_directory_system_dialog(self, title, initial_dir=""):
|
||||
"""打开系统目录选择器并返回目录路径。"""
|
||||
self._last_directory_dialog_error = ""
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
|
||||
if not initial_dir or (not os.path.isdir(initial_dir)):
|
||||
initial_dir = os.getcwd()
|
||||
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
try:
|
||||
root.attributes("-topmost", True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
selected = filedialog.askdirectory(
|
||||
title=title or "选择目录",
|
||||
initialdir=initial_dir,
|
||||
mustexist=True,
|
||||
)
|
||||
root.destroy()
|
||||
return os.path.normpath(selected) if selected else ""
|
||||
except Exception as e:
|
||||
print(f"目录选择器打开失败: {e}")
|
||||
self._last_directory_dialog_error = str(e)
|
||||
return ""
|
||||
|
||||
# ==================== 键盘事件处理函数 ====================
|
||||
|
||||
@ -735,6 +828,9 @@ class AppActions:
|
||||
|
||||
node_name = node.getName() or "未命名节点"
|
||||
parent = node.getParent()
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||
deleting_ssbo_root = bool(ssbo_model and (node == ssbo_model))
|
||||
|
||||
# 创建删除命令
|
||||
if hasattr(self, 'command_manager') and self.command_manager:
|
||||
@ -747,6 +843,12 @@ class AppActions:
|
||||
print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}")
|
||||
self._perform_node_cleanup(node)
|
||||
node.removeNode()
|
||||
|
||||
if deleting_ssbo_root and ssbo_editor:
|
||||
try:
|
||||
ssbo_editor.on_model_deleted(node)
|
||||
except Exception as e:
|
||||
print(f"[SSBO] 删除模型后清理失败: {e}")
|
||||
|
||||
print(f"[删除] 成功删除节点: {node_name}")
|
||||
return True
|
||||
@ -921,6 +1023,8 @@ class AppActions:
|
||||
# 检查场景文件
|
||||
scene_file = os.path.join(project_path, "scenes", "scene.bam")
|
||||
if os.path.exists(scene_file):
|
||||
if getattr(self, "use_ssbo_mouse_picking", False) and callable(getattr(self, "_import_model_for_runtime", None)):
|
||||
self.use_ssbo_scene_import = True
|
||||
# 加载场景
|
||||
try:
|
||||
if self.scene_manager.loadScene(scene_file):
|
||||
@ -1027,17 +1131,12 @@ class AppActions:
|
||||
|
||||
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False):
|
||||
"""Import model through the active runtime path.
|
||||
SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model).
|
||||
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
||||
Legacy mode: load via SceneManager.
|
||||
"""
|
||||
if prefer_scene_manager:
|
||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||
return self.scene_manager.importModel(file_path)
|
||||
return None
|
||||
|
||||
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
|
||||
try:
|
||||
# Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes.
|
||||
# Clear selection/gizmo first, then import as an additional scene model.
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
try:
|
||||
self.selection.clearSelection()
|
||||
@ -1047,32 +1146,24 @@ class AppActions:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Remove legacy scene-manager models to avoid duplicate rendering
|
||||
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
|
||||
for m in list(self.scene_manager.models):
|
||||
try:
|
||||
if m and not m.isEmpty():
|
||||
m.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
self.scene_manager.models = []
|
||||
|
||||
# Replace previous SSBO model
|
||||
old_model = getattr(self.ssbo_editor, 'model', None)
|
||||
if old_model is not None:
|
||||
try:
|
||||
if not old_model.isEmpty():
|
||||
old_model.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.ssbo_editor.load_model(file_path)
|
||||
model_np = getattr(self.ssbo_editor, 'model', None)
|
||||
normalized_import_path = str(file_path).replace("\\", "/").lower()
|
||||
is_project_scene_bam = (
|
||||
normalized_import_path.endswith(".bam")
|
||||
and "/scenes/" in normalized_import_path
|
||||
)
|
||||
# Keep legacy ray-pick fallback usable by adding a collision body.
|
||||
if model_np:
|
||||
try:
|
||||
from scene import util as scene_util
|
||||
normalized_model_path = scene_util.normalize_model_path(file_path)
|
||||
except Exception:
|
||||
normalized_model_path = file_path
|
||||
# Apply vital tags manually since SSBO overrides SceneManager loader
|
||||
model_np.setTag("model_path", file_path)
|
||||
model_np.setTag("model_path", normalized_model_path)
|
||||
model_np.setTag("original_path", file_path)
|
||||
model_np.setTag("saved_model_path", normalized_model_path)
|
||||
model_np.setTag("is_model_root", "1")
|
||||
model_np.setTag("is_scene_element", "1")
|
||||
model_np.setTag("file", os.path.basename(file_path))
|
||||
@ -1080,8 +1171,14 @@ class AppActions:
|
||||
|
||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||
try:
|
||||
self.scene_manager.setupCollision(model_np)
|
||||
self.scene_manager._processModelAnimations(model_np)
|
||||
if not is_project_scene_bam:
|
||||
self.scene_manager.setupCollision(model_np)
|
||||
self.scene_manager._processModelAnimations(model_np)
|
||||
else:
|
||||
model_np.setTag("has_animations", "false")
|
||||
model_np.setTag("has_animations_checked", "true")
|
||||
if hasattr(self.scene_manager, "models") and model_np not in self.scene_manager.models:
|
||||
self.scene_manager.models.append(model_np)
|
||||
except Exception as e:
|
||||
print(f"[SSBO] setup components failed: {e}")
|
||||
return model_np
|
||||
@ -1131,33 +1228,35 @@ class AppActions:
|
||||
self.add_error_message("模型导入失败")
|
||||
return None
|
||||
|
||||
if hasattr(self.scene_manager, 'processMaterials'):
|
||||
self.scene_manager.processMaterials(model_node)
|
||||
if show_info_message:
|
||||
self.add_info_message("已应用默认材质")
|
||||
|
||||
try:
|
||||
model_node.clearMaterial()
|
||||
model_node.clearTexture()
|
||||
|
||||
# SSBO 模式下保留原始材质/纹理,避免模型发黑。
|
||||
if not getattr(self, "use_ssbo_mouse_picking", False):
|
||||
if hasattr(self.scene_manager, 'processMaterials'):
|
||||
self.scene_manager.processMaterials(model_node)
|
||||
if show_info_message:
|
||||
self.add_info_message("已应用默认材质")
|
||||
|
||||
try:
|
||||
color = model_node.getColor()
|
||||
if color and len(color) >= 4 and color == (1, 1, 1, 1):
|
||||
model_node.clearMaterial()
|
||||
model_node.clearTexture()
|
||||
|
||||
if hasattr(self.scene_manager, 'processMaterials'):
|
||||
self.scene_manager.processMaterials(model_node)
|
||||
|
||||
try:
|
||||
color = model_node.getColor()
|
||||
if color and len(color) >= 4 and color == (1, 1, 1, 1):
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
elif not color:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
except Exception:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
elif not color:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
except Exception:
|
||||
model_node.setColor(0.8, 0.8, 0.8, 1.0)
|
||||
except Exception as e:
|
||||
self.add_warning_message(f"材质处理警告: {e}")
|
||||
except Exception as e:
|
||||
self.add_warning_message(f"材质处理警告: {e}")
|
||||
|
||||
if set_origin:
|
||||
model_node.setPos(0, 0, 0)
|
||||
|
||||
if hasattr(self.scene_manager, 'models'):
|
||||
if hasattr(self.scene_manager, 'models') and model_node not in self.scene_manager.models:
|
||||
self.scene_manager.models.append(model_node)
|
||||
|
||||
if select_model and hasattr(self, 'selection') and self.selection:
|
||||
@ -1172,7 +1271,7 @@ class AppActions:
|
||||
|
||||
def _on_import_model(self):
|
||||
"""处理导入模型菜单项"""
|
||||
self.add_info_message("打开导入模型对话框")
|
||||
self.add_info_message("打开系统文件选择器")
|
||||
self.show_import_dialog = True
|
||||
|
||||
|
||||
|
||||
@ -78,57 +78,6 @@ class CreateActions:
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建平面失败: {str(e)}")
|
||||
|
||||
|
||||
def _on_create_3d_text(self):
|
||||
"""创建3D文本"""
|
||||
self.show_3d_text_dialog = True
|
||||
|
||||
|
||||
def _on_create_3d_image(self):
|
||||
"""创建3D图片"""
|
||||
self.show_3d_image_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_button(self):
|
||||
"""创建GUI按钮"""
|
||||
self.show_gui_button_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_label(self):
|
||||
"""创建GUI标签"""
|
||||
self.show_gui_label_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_entry(self):
|
||||
"""创建GUI输入框"""
|
||||
self.show_gui_entry_dialog = True
|
||||
|
||||
|
||||
def _on_create_gui_image(self):
|
||||
"""创建GUI图片"""
|
||||
self.show_gui_image_dialog = True
|
||||
|
||||
|
||||
def _on_create_video_screen(self):
|
||||
"""创建视频屏幕"""
|
||||
self.show_video_screen_dialog = True
|
||||
|
||||
|
||||
def _on_create_2d_video_screen(self):
|
||||
"""创建2D视频屏幕"""
|
||||
self.show_2d_video_screen_dialog = True
|
||||
|
||||
|
||||
def _on_create_spherical_video(self):
|
||||
"""创建球形视频"""
|
||||
self.show_spherical_video_dialog = True
|
||||
|
||||
|
||||
def _on_create_virtual_screen(self):
|
||||
"""创建虚拟屏幕"""
|
||||
self.show_virtual_screen_dialog = True
|
||||
|
||||
|
||||
def _on_create_spot_light(self):
|
||||
"""创建聚光灯"""
|
||||
self.show_spot_light_dialog = True
|
||||
|
||||
@ -265,88 +265,80 @@ class DialogPanels:
|
||||
|
||||
|
||||
def _draw_import_dialog(self):
|
||||
"""绘制导入模型对话框"""
|
||||
"""使用系统文件选择器导入模型。"""
|
||||
if not self.show_import_dialog:
|
||||
return
|
||||
|
||||
# 设置对话框标志
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
# 获取屏幕尺寸,居中显示对话框
|
||||
display_size = imgui.get_io().display_size
|
||||
dialog_width = 600
|
||||
dialog_height = 500
|
||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
||||
imgui.set_next_window_pos(
|
||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
||||
)
|
||||
|
||||
with imgui_ctx.begin("导入模型", True, flags) as window:
|
||||
if not window.opened:
|
||||
self.show_import_dialog = False
|
||||
return
|
||||
|
||||
imgui.text("选择要导入的模型文件")
|
||||
imgui.separator()
|
||||
|
||||
# 文件路径输入
|
||||
imgui.text("文件路径:")
|
||||
changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512)
|
||||
if changed:
|
||||
self.import_file_path = file_path
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("浏览..."):
|
||||
self.path_browser_mode = "import_model"
|
||||
self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd()
|
||||
self.show_path_browser = True
|
||||
self._refresh_path_browser()
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 支持的格式说明
|
||||
imgui.text("支持的文件格式:")
|
||||
formats_text = ", ".join(self.supported_formats)
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 文件预览信息
|
||||
if self.import_file_path and os.path.exists(self.import_file_path):
|
||||
file_size = os.path.getsize(self.import_file_path)
|
||||
imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
|
||||
|
||||
file_ext = os.path.splitext(self.import_file_path)[1].lower()
|
||||
if file_ext in self.supported_formats:
|
||||
imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持")
|
||||
else:
|
||||
imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式")
|
||||
else:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径")
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮区域
|
||||
can_import = (self.import_file_path and
|
||||
os.path.exists(self.import_file_path) and
|
||||
os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats)
|
||||
|
||||
# 根据状态设置按钮颜色
|
||||
if can_import:
|
||||
if imgui.button("导入"):
|
||||
self._import_model()
|
||||
self.show_import_dialog = False
|
||||
else:
|
||||
# 禁用状态的按钮(灰色显示)
|
||||
imgui.push_style_color(imgui.Col_.button, (0.3, 0.3, 0.3, 1.0))
|
||||
imgui.button("导入")
|
||||
imgui.pop_style_color()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_import_dialog = False
|
||||
|
||||
# 立即关闭标记,防止每帧重复弹窗
|
||||
self.show_import_dialog = False
|
||||
|
||||
selected_path = self._select_model_file_system_dialog()
|
||||
if not selected_path:
|
||||
self.add_info_message("已取消导入模型")
|
||||
return
|
||||
|
||||
self.import_file_path = selected_path
|
||||
self._import_model()
|
||||
|
||||
def _select_model_file_system_dialog(self):
|
||||
"""弹出系统文件选择器并返回模型路径。"""
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
|
||||
initial_dir = os.path.dirname(self.import_file_path) if self.import_file_path else ""
|
||||
if not initial_dir or (not os.path.isdir(initial_dir)):
|
||||
candidates = [
|
||||
os.path.join(os.getcwd(), "Resources", "models"),
|
||||
os.path.join(os.getcwd(), "Resources"),
|
||||
os.getcwd(),
|
||||
]
|
||||
initial_dir = next((p for p in candidates if os.path.isdir(p)), os.getcwd())
|
||||
|
||||
normalized_exts = []
|
||||
for ext in getattr(self, "supported_formats", []):
|
||||
ext = str(ext).strip().lower()
|
||||
if not ext:
|
||||
continue
|
||||
if not ext.startswith("."):
|
||||
ext = f".{ext}"
|
||||
if ext not in normalized_exts:
|
||||
normalized_exts.append(ext)
|
||||
|
||||
model_patterns = " ".join(f"*{ext}" for ext in normalized_exts) or "*.*"
|
||||
filetypes = [
|
||||
("模型文件", model_patterns),
|
||||
("All Files", "*.*"),
|
||||
]
|
||||
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
try:
|
||||
root.attributes("-topmost", True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
selected_path = filedialog.askopenfilename(
|
||||
title="选择要导入的模型文件",
|
||||
initialdir=initial_dir,
|
||||
filetypes=filetypes,
|
||||
)
|
||||
root.destroy()
|
||||
|
||||
if not selected_path:
|
||||
return ""
|
||||
|
||||
selected_path = os.path.normpath(selected_path)
|
||||
file_ext = os.path.splitext(selected_path)[1].lower()
|
||||
if normalized_exts and file_ext not in normalized_exts:
|
||||
self.add_error_message(f"不支持的文件格式: {file_ext}")
|
||||
return ""
|
||||
|
||||
self.add_info_message(f"已选择文件: {selected_path}")
|
||||
return selected_path
|
||||
except Exception as e:
|
||||
self.add_error_message(f"打开系统文件选择器失败: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def _refresh_path_browser(self):
|
||||
@ -410,435 +402,16 @@ class DialogPanels:
|
||||
# 导入模型模式:使用选择的文件路径
|
||||
self.import_file_path = self.path_browser_selected_path
|
||||
self.add_info_message(f"已选择文件: {self.import_file_path}")
|
||||
elif self.path_browser_mode == "webgl_build":
|
||||
output_dir = self.path_browser_current_path
|
||||
self.add_info_message(f"已选择WebGL输出目录: {output_dir}")
|
||||
if getattr(self, "_pending_webgl_package", False):
|
||||
self._pending_webgl_package = False
|
||||
if hasattr(self, "_execute_webgl_package"):
|
||||
self._execute_webgl_package(output_dir)
|
||||
except Exception as e:
|
||||
self.add_error_message(f"应用路径失败: {e}")
|
||||
|
||||
# ==================== 创建功能对话框实现 ====================
|
||||
|
||||
|
||||
def _draw_gui_button_dialog(self):
|
||||
"""绘制GUI按钮创建对话框"""
|
||||
if not self.show_gui_button_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI按钮", self.show_gui_button_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_button_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'button_text' not in self.dialog_params:
|
||||
self.dialog_params['button_text'] = "按钮"
|
||||
if 'button_pos' not in self.dialog_params:
|
||||
self.dialog_params['button_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'button_size' not in self.dialog_params:
|
||||
self.dialog_params['button_size'] = [0.1, 0.1, 0.1]
|
||||
|
||||
imgui.text("GUI按钮参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 文本输入
|
||||
changed, self.dialog_params['button_text'] = imgui.input_text("按钮文本", self.dialog_params['button_text'], 256)
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['button_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['button_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['button_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['button_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['button_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['button_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['button_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['button_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['button_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['button_size'][1] = height
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['button_pos'])
|
||||
text = self.dialog_params['button_text']
|
||||
size = tuple(self.dialog_params['button_size'][:2])
|
||||
|
||||
result = self.createGUIButton(pos, text, size)
|
||||
self.add_success_message(f"GUI按钮创建成功: {text}")
|
||||
self.show_gui_button_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI按钮失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_button_dialog = False
|
||||
|
||||
|
||||
def _draw_gui_label_dialog(self):
|
||||
"""绘制GUI标签创建对话框"""
|
||||
if not self.show_gui_label_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI标签", self.show_gui_label_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_label_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'label_text' not in self.dialog_params:
|
||||
self.dialog_params['label_text'] = "标签"
|
||||
if 'label_pos' not in self.dialog_params:
|
||||
self.dialog_params['label_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'label_size' not in self.dialog_params:
|
||||
self.dialog_params['label_size'] = [0.1, 0.1, 0.1]
|
||||
|
||||
imgui.text("GUI标签参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 文本输入
|
||||
changed, self.dialog_params['label_text'] = imgui.input_text("标签文本", self.dialog_params['label_text'], 256)
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['label_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['label_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['label_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['label_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['label_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['label_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['label_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['label_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['label_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['label_size'][1] = height
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['label_pos'])
|
||||
text = self.dialog_params['label_text']
|
||||
size = tuple(self.dialog_params['label_size'][:2])
|
||||
|
||||
result = self.createGUILabel(pos, text, size)
|
||||
self.add_success_message(f"GUI标签创建成功: {text}")
|
||||
self.show_gui_label_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI标签失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_label_dialog = False
|
||||
|
||||
|
||||
def _draw_gui_entry_dialog(self):
|
||||
"""绘制GUI输入框创建对话框"""
|
||||
if not self.show_gui_entry_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI输入框", self.show_gui_entry_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_entry_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'entry_pos' not in self.dialog_params:
|
||||
self.dialog_params['entry_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'entry_size' not in self.dialog_params:
|
||||
self.dialog_params['entry_size'] = [0.2, 0.05, 0.1]
|
||||
|
||||
imgui.text("GUI输入框参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['entry_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['entry_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['entry_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['entry_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['entry_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['entry_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['entry_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['entry_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['entry_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['entry_size'][1] = height
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['entry_pos'])
|
||||
size = tuple(self.dialog_params['entry_size'][:2])
|
||||
|
||||
result = self.createGUIEntry(pos, size)
|
||||
self.add_success_message("GUI输入框创建成功")
|
||||
self.show_gui_entry_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI输入框失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_entry_dialog = False
|
||||
|
||||
|
||||
def _draw_gui_image_dialog(self):
|
||||
"""绘制GUI图片创建对话框"""
|
||||
if not self.show_gui_image_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建GUI图片", self.show_gui_image_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_gui_image_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'image_pos' not in self.dialog_params:
|
||||
self.dialog_params['image_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'image_size' not in self.dialog_params:
|
||||
self.dialog_params['image_size'] = [0.2, 0.2, 0.1]
|
||||
if 'image_path' not in self.dialog_params:
|
||||
self.dialog_params['image_path'] = ""
|
||||
|
||||
imgui.text("GUI图片参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['image_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['image_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['image_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['image_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['image_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['image_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['image_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['image_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['image_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['image_size'][1] = height
|
||||
|
||||
# 图片路径
|
||||
changed, self.dialog_params['image_path'] = imgui.input_text("图片路径", self.dialog_params['image_path'], 512)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['image_pos'])
|
||||
size = tuple(self.dialog_params['image_size'][:2])
|
||||
image_path = self.dialog_params['image_path']
|
||||
|
||||
result = self.createGUIImage(pos, image_path, size)
|
||||
self.add_success_message("GUI图片创建成功")
|
||||
self.show_gui_image_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建GUI图片失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_gui_image_dialog = False
|
||||
|
||||
|
||||
def _draw_3d_text_dialog(self):
|
||||
"""绘制3D文本创建对话框"""
|
||||
if not self.show_3d_text_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建3D文本", self.show_3d_text_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_3d_text_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'text3d_text' not in self.dialog_params:
|
||||
self.dialog_params['text3d_text'] = "3D文本"
|
||||
if 'text3d_pos' not in self.dialog_params:
|
||||
self.dialog_params['text3d_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'text3d_size' not in self.dialog_params:
|
||||
self.dialog_params['text3d_size'] = 1.0
|
||||
|
||||
imgui.text("3D文本参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 文本输入
|
||||
changed, self.dialog_params['text3d_text'] = imgui.input_text("文本内容", self.dialog_params['text3d_text'], 256)
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['text3d_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['text3d_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['text3d_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['text3d_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['text3d_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['text3d_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, self.dialog_params['text3d_size'] = imgui.input_float("文本大小", self.dialog_params['text3d_size'])
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['text3d_pos'])
|
||||
text = self.dialog_params['text3d_text']
|
||||
size = self.dialog_params['text3d_size']
|
||||
|
||||
result = self.create3DText(pos, text, size)
|
||||
self.add_success_message(f"3D文本创建成功: {text}")
|
||||
self.show_3d_text_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建3D文本失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_3d_text_dialog = False
|
||||
|
||||
|
||||
def _draw_3d_image_dialog(self):
|
||||
"""绘制3D图片创建对话框"""
|
||||
if not self.show_3d_image_dialog:
|
||||
return
|
||||
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
with imgui_ctx.begin("创建3D图片", self.show_3d_image_dialog, flags) as window:
|
||||
if not window:
|
||||
self.show_3d_image_dialog = False
|
||||
return
|
||||
|
||||
# 初始化参数
|
||||
if 'image3d_pos' not in self.dialog_params:
|
||||
self.dialog_params['image3d_pos'] = [0.0, 0.0, 0.0]
|
||||
if 'image3d_size' not in self.dialog_params:
|
||||
self.dialog_params['image3d_size'] = [1.0, 1.0, 1.0]
|
||||
if 'image3d_path' not in self.dialog_params:
|
||||
self.dialog_params['image3d_path'] = ""
|
||||
|
||||
imgui.text("3D图片参数设置")
|
||||
imgui.separator()
|
||||
|
||||
# 位置输入
|
||||
changed, x = imgui.input_float("X坐标", self.dialog_params['image3d_pos'][0])
|
||||
if changed:
|
||||
self.dialog_params['image3d_pos'][0] = x
|
||||
changed, y = imgui.input_float("Y坐标", self.dialog_params['image3d_pos'][1])
|
||||
if changed:
|
||||
self.dialog_params['image3d_pos'][1] = y
|
||||
changed, z = imgui.input_float("Z坐标", self.dialog_params['image3d_pos'][2])
|
||||
if changed:
|
||||
self.dialog_params['image3d_pos'][2] = z
|
||||
|
||||
# 大小输入
|
||||
changed, width = imgui.input_float("宽度", self.dialog_params['image3d_size'][0])
|
||||
if changed:
|
||||
self.dialog_params['image3d_size'][0] = width
|
||||
changed, height = imgui.input_float("高度", self.dialog_params['image3d_size'][1])
|
||||
if changed:
|
||||
self.dialog_params['image3d_size'][1] = height
|
||||
|
||||
# 图片路径
|
||||
changed, self.dialog_params['image3d_path'] = imgui.input_text("图片路径", self.dialog_params['image3d_path'], 512)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 按钮
|
||||
if imgui.button("创建"):
|
||||
try:
|
||||
pos = tuple(self.dialog_params['image3d_pos'])
|
||||
size = tuple(self.dialog_params['image3d_size'][:2])
|
||||
image_path = self.dialog_params['image3d_path']
|
||||
|
||||
result = self.create3DImage(pos, image_path, size)
|
||||
self.add_success_message("3D图片创建成功")
|
||||
self.show_3d_image_dialog = False
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建3D图片失败: {str(e)}")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_3d_image_dialog = False
|
||||
|
||||
# 添加其他创建对话框的占位符方法
|
||||
|
||||
def _draw_video_screen_dialog(self):
|
||||
"""绘制视频屏幕创建对话框"""
|
||||
if not self.show_video_screen_dialog:
|
||||
return
|
||||
self.show_video_screen_dialog = False
|
||||
self.add_info_message("视频屏幕创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_2d_video_screen_dialog(self):
|
||||
"""绘制2D视频屏幕创建对话框"""
|
||||
if not self.show_2d_video_screen_dialog:
|
||||
return
|
||||
self.show_2d_video_screen_dialog = False
|
||||
self.add_info_message("2D视频屏幕创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_spherical_video_dialog(self):
|
||||
"""绘制球形视频创建对话框"""
|
||||
if not self.show_spherical_video_dialog:
|
||||
return
|
||||
self.show_spherical_video_dialog = False
|
||||
self.add_info_message("球形视频创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_virtual_screen_dialog(self):
|
||||
"""绘制虚拟屏幕创建对话框"""
|
||||
if not self.show_virtual_screen_dialog:
|
||||
return
|
||||
self.show_virtual_screen_dialog = False
|
||||
self.add_info_message("虚拟屏幕创建功能开发中...")
|
||||
|
||||
|
||||
def _draw_spot_light_dialog(self):
|
||||
"""绘制聚光灯创建对话框"""
|
||||
@ -1457,5 +1030,3 @@ class DialogPanels:
|
||||
except Exception as e:
|
||||
print(f"刷新高度图浏览器时出错: {e}")
|
||||
self.heightmap_browser_items = []
|
||||
|
||||
# ==================== 导入功能实现 ====================
|
||||
|
||||
@ -40,7 +40,7 @@ class EditorPanels:
|
||||
|
||||
def _ensure_web_panel_state(self):
|
||||
if not hasattr(self.app, "web_panel_url_input") or not self.app.web_panel_url_input:
|
||||
self.app.web_panel_url_input = "https://www.example.com"
|
||||
self.app.web_panel_url_input = "https://www.baidu.com"
|
||||
if not hasattr(self.app, "_imgui_webview"):
|
||||
self.app._imgui_webview = None
|
||||
if not hasattr(self.app, "_imgui_webview_tex_id"):
|
||||
@ -171,6 +171,8 @@ class EditorPanels:
|
||||
self.app._on_save_project()
|
||||
if imgui.menu_item("另存为", "", False, True)[1]:
|
||||
self.app._on_save_as_project()
|
||||
if imgui.menu_item("打包为 WebGL", "", False, True)[1]:
|
||||
self.app._on_build_webgl_package()
|
||||
imgui.separator()
|
||||
if imgui.menu_item("退出", "Alt+F4", False, True)[1]:
|
||||
self.app._on_exit()
|
||||
@ -217,33 +219,33 @@ class EditorPanels:
|
||||
self.app._on_create_plane()
|
||||
|
||||
# 3D GUI子菜单
|
||||
with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
|
||||
if three_d_gui_menu:
|
||||
if imgui.menu_item("3D文本", "", False, True)[1]:
|
||||
self.app._on_create_3d_text()
|
||||
if imgui.menu_item("3D图片", "", False, True)[1]:
|
||||
self.app._on_create_3d_image()
|
||||
# with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
|
||||
# if three_d_gui_menu:
|
||||
# if imgui.menu_item("3D文本", "", False, True)[1]:
|
||||
# self.app._on_create_3d_text()
|
||||
# if imgui.menu_item("3D图片", "", False, True)[1]:
|
||||
# self.app._on_create_3d_image()
|
||||
|
||||
# GUI子菜单
|
||||
with imgui_ctx.begin_menu("GUI") as gui_menu:
|
||||
if gui_menu:
|
||||
if imgui.menu_item("创建按钮", "", False, True)[1]:
|
||||
self.app._on_create_gui_button()
|
||||
if imgui.menu_item("创建标签", "", False, True)[1]:
|
||||
self.app._on_create_gui_label()
|
||||
if imgui.menu_item("创建输入框", "", False, True)[1]:
|
||||
self.app._on_create_gui_entry()
|
||||
if imgui.menu_item("创建图片", "", False, True)[1]:
|
||||
self.app._on_create_gui_image()
|
||||
imgui.separator()
|
||||
if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
|
||||
self.app._on_create_video_screen()
|
||||
if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
|
||||
self.app._on_create_2d_video_screen()
|
||||
if imgui.menu_item("创建球形视频", "", False, True)[1]:
|
||||
self.app._on_create_spherical_video()
|
||||
if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
|
||||
self.app._on_create_virtual_screen()
|
||||
# # GUI子菜单
|
||||
# with imgui_ctx.begin_menu("GUI") as gui_menu:
|
||||
# if gui_menu:
|
||||
# if imgui.menu_item("创建按钮", "", False, True)[1]:
|
||||
# self.app._on_create_gui_button()
|
||||
# if imgui.menu_item("创建标签", "", False, True)[1]:
|
||||
# self.app._on_create_gui_label()
|
||||
# if imgui.menu_item("创建输入框", "", False, True)[1]:
|
||||
# self.app._on_create_gui_entry()
|
||||
# if imgui.menu_item("创建图片", "", False, True)[1]:
|
||||
# self.app._on_create_gui_image()
|
||||
# imgui.separator()
|
||||
# if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
|
||||
# self.app._on_create_video_screen()
|
||||
# if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
|
||||
# self.app._on_create_2d_video_screen()
|
||||
# if imgui.menu_item("创建球形视频", "", False, True)[1]:
|
||||
# self.app._on_create_spherical_video()
|
||||
# if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
|
||||
# self.app._on_create_virtual_screen()
|
||||
|
||||
# 光源子菜单
|
||||
with imgui_ctx.begin_menu("光源") as light_menu:
|
||||
@ -278,10 +280,10 @@ class EditorPanels:
|
||||
# 信息面板子菜单
|
||||
with imgui_ctx.begin_menu("信息面板") as info_panel_menu:
|
||||
if info_panel_menu:
|
||||
if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
|
||||
self.app._on_create_2d_sample_panel()
|
||||
if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
|
||||
self.app._on_create_3d_sample_panel()
|
||||
# if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
|
||||
# self.app._on_create_2d_sample_panel()
|
||||
# if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
|
||||
# self.app._on_create_3d_sample_panel()
|
||||
if imgui.menu_item("Web面板", "", False, True)[1]:
|
||||
self.app._on_create_web_panel()
|
||||
|
||||
@ -318,10 +320,10 @@ class EditorPanels:
|
||||
imgui.separator()
|
||||
|
||||
# 编辑工具
|
||||
if imgui.menu_item("光照编辑", "", False, True)[1]:
|
||||
self.app.tool_manager.setCurrentTool("光照编辑")
|
||||
if imgui.menu_item("图形编辑", "", False, True)[1]:
|
||||
self.app.tool_manager.setCurrentTool("图形编辑")
|
||||
# if imgui.menu_item("光照编辑", "", False, True)[1]:
|
||||
# self.app.tool_manager.setCurrentTool("光照编辑")
|
||||
# if imgui.menu_item("图形编辑", "", False, True)[1]:
|
||||
# self.app.tool_manager.setCurrentTool("图形编辑")
|
||||
|
||||
imgui.separator()
|
||||
|
||||
@ -610,8 +612,33 @@ class EditorPanels:
|
||||
|
||||
# 处理节点选择
|
||||
if imgui.is_item_clicked():
|
||||
# In SSBO mode, clicking model root should finally bind gizmo to
|
||||
# SSBO group proxy (not legacy model root). So run legacy
|
||||
# selection first, then force SSBO root selection at the end.
|
||||
force_ssbo_root_key = None
|
||||
ssbo_editor_ref = None
|
||||
try:
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||
root_key = getattr(controller, "tree_root_key", None) if controller else None
|
||||
if (
|
||||
ssbo_editor and controller and ssbo_model and node == ssbo_model
|
||||
and root_key and root_key in controller.tree_nodes
|
||||
):
|
||||
force_ssbo_root_key = root_key
|
||||
ssbo_editor_ref = ssbo_editor
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(self.app, 'selection') and self.app.selection:
|
||||
self.app.selection.updateSelection(node)
|
||||
|
||||
if force_ssbo_root_key and ssbo_editor_ref:
|
||||
try:
|
||||
ssbo_editor_ref.select_node(force_ssbo_root_key)
|
||||
except Exception:
|
||||
pass
|
||||
# Clear LUI selection when a scene node is selected
|
||||
if hasattr(self.app, 'lui_manager'):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
@ -655,7 +682,7 @@ class EditorPanels:
|
||||
imgui.pop_style_color()
|
||||
|
||||
def _draw_ssbo_virtual_children(self, node):
|
||||
"""Draw SSBO controller nodes as virtual children for scene tree."""
|
||||
"""Draw SSBO controller tree_nodes as virtual children for scene tree."""
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
if not ssbo_editor:
|
||||
return False
|
||||
@ -664,66 +691,59 @@ class EditorPanels:
|
||||
if not model or model != node or not controller:
|
||||
return False
|
||||
|
||||
tree_root = controller.get_virtual_hierarchy() if hasattr(controller, "get_virtual_hierarchy") else None
|
||||
if not tree_root or not tree_root.get("children"):
|
||||
root_key = getattr(controller, "tree_root_key", None)
|
||||
if not root_key or root_key not in controller.tree_nodes:
|
||||
imgui.text_disabled("(无可用子节点)")
|
||||
return True
|
||||
|
||||
for name in sorted(tree_root["children"].keys()):
|
||||
child = tree_root["children"][name]
|
||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, child, "ssbo_root")
|
||||
root_node = controller.tree_nodes[root_key]
|
||||
if not root_node["children"]:
|
||||
imgui.text_disabled("(无可用子节点)")
|
||||
return True
|
||||
|
||||
for child_key in root_node["children"]:
|
||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||
return True
|
||||
|
||||
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, tree_node, unique_id_prefix, depth=0):
|
||||
"""Recursively draw virtual SSBO hierarchy in scene tree."""
|
||||
if not tree_node:
|
||||
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
|
||||
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
|
||||
node_data = controller.tree_nodes.get(key)
|
||||
if not node_data:
|
||||
return
|
||||
path = tree_node.get("path", "")
|
||||
display = tree_node.get("display_name") or tree_node.get("name") or path
|
||||
leaf_key = tree_node.get("leaf_key")
|
||||
group_key = tree_node.get("group_key")
|
||||
children = tree_node.get("children", {}) or {}
|
||||
|
||||
label = f"{display}##{unique_id_prefix}_{path}"
|
||||
# Skip redundant wrapper nodes (e.g. ROOT), show their children instead.
|
||||
if controller.should_hide_tree_node(key):
|
||||
for child_key in node_data["children"]:
|
||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||
return
|
||||
|
||||
# Leaf: selectable to trigger SSBO selection.
|
||||
if not children and leaf_key:
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
|
||||
display = controller.display_names.get(key, key)
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
children = node_data["children"]
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||
|
||||
if not children:
|
||||
# Leaf node: selectable
|
||||
label = f"{display} ({obj_count})##{key}"
|
||||
if imgui.selectable(label, is_selected)[0]:
|
||||
ssbo_editor.select_node(leaf_key)
|
||||
ssbo_editor.select_node(key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
return
|
||||
|
||||
# Non-leaf: tree node only for hierarchy display.
|
||||
opened = imgui.tree_node(label)
|
||||
# Clicking non-leaf row selects its aggregate group so parent transform affects children.
|
||||
if group_key and imgui.is_item_clicked(0):
|
||||
ssbo_editor.select_node(group_key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
if opened:
|
||||
# If this node is also a selectable leaf, render selectable entry first.
|
||||
if group_key:
|
||||
is_group_selected = (getattr(ssbo_editor, "selected_name", None) == group_key)
|
||||
if imgui.selectable(f"[整体] {display}##group_{unique_id_prefix}_{path}", is_group_selected)[0]:
|
||||
ssbo_editor.select_node(group_key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
if leaf_key:
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
|
||||
if imgui.selectable(f"[节点] {display}##leaf_{unique_id_prefix}_{path}", is_selected)[0]:
|
||||
ssbo_editor.select_node(leaf_key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
for child_name in sorted(children.keys()):
|
||||
self._draw_ssbo_virtual_tree_node(
|
||||
ssbo_editor,
|
||||
children[child_name],
|
||||
unique_id_prefix,
|
||||
depth + 1,
|
||||
)
|
||||
imgui.tree_pop()
|
||||
else:
|
||||
# Branch node: tree node
|
||||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||||
if is_selected:
|
||||
flags |= imgui.TreeNodeFlags_.selected
|
||||
label = f"{display} ({obj_count})##{key}"
|
||||
opened = imgui.tree_node_ex(label, flags)
|
||||
if imgui.is_item_clicked(0):
|
||||
ssbo_editor.select_node(key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.selected_index = -1
|
||||
if opened:
|
||||
for child_key in children:
|
||||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||||
imgui.tree_pop()
|
||||
def _show_node_context_menu(self, node, name, node_type):
|
||||
"""显示节点右键菜单"""
|
||||
self.app._context_menu_node = True
|
||||
@ -2211,7 +2231,10 @@ class EditorPanels:
|
||||
# 删除对象
|
||||
imgui.same_line()
|
||||
if imgui.button("删除"):
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
if hasattr(self.app, "_on_delete") and callable(self.app._on_delete):
|
||||
self.app._on_delete()
|
||||
elif hasattr(self, 'selection') and self.selection and hasattr(self.selection, "deleteSelectedNode"):
|
||||
# 兼容旧选择系统接口
|
||||
self.selection.deleteSelectedNode()
|
||||
|
||||
|
||||
@ -2439,4 +2462,3 @@ class EditorPanels:
|
||||
except Exception as e:
|
||||
print(f"绘制着色模型面板失败: {e}")
|
||||
|
||||
|
||||
|
||||
@ -339,51 +339,6 @@ class ObjectFactory:
|
||||
except Exception as e:
|
||||
print(f"✗ 创建平面失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def create2DSamplePanel(self):
|
||||
"""创建2D示例面板"""
|
||||
try:
|
||||
from core.InfoPanelManager import createSampleInfoPanel
|
||||
result = createSampleInfoPanel(self.render)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"创建2D示例面板失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def create3DSamplePanel(self):
|
||||
"""创建3D实例面板"""
|
||||
try:
|
||||
if hasattr(self, 'info_panel_manager') and self.info_panel_manager:
|
||||
# 创建3D信息面板
|
||||
panel_id = f"3d_sample_{int(time.time())}"
|
||||
result = self.info_panel_manager.create3DInfoPanel(
|
||||
panel_id=panel_id,
|
||||
position=(0, 0, 2),
|
||||
size=(1.0, 0.6),
|
||||
bg_color=(0.15, 0.25, 0.35, 0.95),
|
||||
border_color=(0.3, 0.5, 0.7, 1.0),
|
||||
title_color=(0.7, 0.9, 1.0, 1.0),
|
||||
content_color=(0.95, 0.95, 0.95, 1.0)
|
||||
)
|
||||
|
||||
# 添加示例内容
|
||||
if result:
|
||||
sample_data = {
|
||||
"标题": "3D信息面板",
|
||||
"状态": "运行中",
|
||||
"创建时间": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"位置": f"X:0, Y:0, Z:2"
|
||||
}
|
||||
self.info_panel_manager.updatePanelContent(panel_id, content=sample_data)
|
||||
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建3D示例面板失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def createWebPanel(self, url="https://www.example.com"):
|
||||
"""创建Web面板"""
|
||||
|
||||
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