Compare commits

..

No commits in common. "hu_migrate_geng_new" and "main" have entirely different histories.

148 changed files with 41226 additions and 48476 deletions

View File

@ -1,11 +0,0 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "EG"
[setup]
script = ""
[[actions]]
name = "go"
icon = "tool"
command = "python ./main.py"

View File

@ -1,11 +0,0 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "EG"
[setup]
script = ""
[[actions]]
name = "运行"
icon = "run"
command = "python ./main.py"

18
.gitignore vendored
View File

@ -1,17 +1,7 @@
__pycache__/
*.pyc
imgui.ini
build/
Builds/
Library/
**/Library/
**/Builds/
**/Scenes/*_gui.json
**/Scenes/*_lui.json
**/scenes/*_gui.json
**/scenes/*_lui.json
.idea/
【模板】模型及算法与功能对应清单.docx
__pycache__/
*.pyc
.idea/
【模板】模型及算法与功能对应清单.docx
【模板】GY知识-产品模块架构及功能清单和主要应用场景说明.docx
1.json
2.json

View File

@ -1,4 +1,4 @@
{
"kiroAgent.configureMCP": "Disabled",
"python-envs.defaultEnvManager": "ms-python.python:pyenv"
"python-envs.pythonProjects": [],
"kiroAgent.configureMCP": "Disabled"
}

219
AGENTS.md Normal file
View File

@ -0,0 +1,219 @@
# 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)
---
*该文档由iFlow CLI自动生成最后更新时间: 2026年1月28日*

View File

@ -1,8 +0,0 @@
{
"guid": "2c994aea36c94385a8f9a5274c406a67",
"asset_type": "model",
"source_hash": "fc694905c47a9b0005d77b701cc41852b56ef08c7406829a306e98f3ce158a64",
"importer": "model_importer",
"import_settings": {},
"dependency_guids": []
}

Binary file not shown.

View File

@ -1,8 +0,0 @@
{
"guid": "b547e6c1347a4ec0bb913a9cb1a7ec6b",
"asset_type": "model",
"source_hash": "46fc525a88f6d16eed0bac714a14692b68818d36a00065690b5877d878788948",
"importer": "model_importer",
"import_settings": {},
"dependency_guids": []
}

1
EG Submodule

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

Binary file not shown.

View File

@ -1 +0,0 @@
index-aa08c2.boo

View File

@ -8,7 +8,7 @@ pipeline:
# it will also disable the hotkeys, and give a small performance boost.
# Most likely you also don't want to show it in your own game, so set
# it to false in that case.
display_debugger: false
display_debugger: true
# Affects which debugging information is displayed. If this is set to false,
# only frame time is displayed, otherwise much more information is visible.

View File

@ -9,11 +9,10 @@ enabled:
- color_correction
- forward_shading
- motion_blur
#- pssm
- pssm
- scattering
- skin_shading
- sky_ao
- selection_outline
- smaa
- ssr
# - clouds

View File

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

View File

@ -10,21 +10,17 @@ vertex_shader: |
texcoord = p3d_MultiTexCoord0;
}
fragment_shader: |
#version 330
uniform sampler2D p3d_Texture0;
uniform vec4 material_base_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform float material_opacity = 1.0;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 c = texture(p3d_Texture0, texcoord);
o_color = vec4(
material_base_color.rgb * c.rgb,
material_base_color.a * c.a * material_opacity
);
}
render_states:
TransparencyAttrib: M_alpha
DepthWriteAttrib: 0
fragment_shader: |
#version 330
uniform sampler2D p3d_Texture0;
uniform float material_opacity = 1.0;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 c = texture(p3d_Texture0, texcoord);
o_color = vec4(c.rgb, c.a * material_opacity);
}
render_states:
TransparencyAttrib: M_alpha
DepthWriteAttrib: 0

View File

@ -45,7 +45,6 @@
layout(location = 0) in VertexOutput vOutput;
uniform Panda3DMaterial p3d_Material;
uniform vec4 p3d_ColorScale;
#pragma include "includes/normal_mapping.inc.glsl"
#pragma include "includes/forward_shading.inc.glsl"
@ -142,9 +141,9 @@ void main() {
m.shading_model = mInput.shading_model;
#if DONT_FETCH_DEFAULT_TEXTURES
m.basecolor = mInput.color * p3d_ColorScale.xyz;
m.basecolor = mInput.color;
#else
m.basecolor = mInput.color * sampled_diffuse.xyz * p3d_ColorScale.xyz;
m.basecolor = mInput.color * sampled_diffuse.xyz;
#endif
m.normal = material_nrm;
m.metallic = mInput.metallic;
@ -161,21 +160,14 @@ void main() {
vec3 view_dir = normalize(m_out.position - MainSceneData.camera_pos);
vec3 color = vec3(0);
float alpha = m_out.shading_model_param0 * p3d_ColorScale.w;
float alpha = m_out.shading_model_param0;
AmbientResult ambient = get_full_forward_ambient(m_out, view_dir);
color += ambient.diffuse;
color += ambient.specular;
color += get_sun_shading(m_out, view_dir);
color += get_forward_light_shading(m_out);
// Transparent forward materials end up noticeably darker than the same
// object in the deferred path in this editor build, which makes them look
// "gone" when artists first switch the material type. Apply a small
// preview-space lift so the result stays visually comparable.
if (m_out.shading_model == SHADING_MODEL_TRANSPARENT) {
color *= 1.55;
}
// XXX: Apply shading from lights too
alpha = mix(alpha, 1.0, ambient.fresnel);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,59 +0,0 @@
/**
* 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);
}

0
Resources/a Normal file
View File

0
Resources/c/a Normal file
View File

0
Resources/c/b/a.txt Normal file
View File

0
Resources/c/b/b Normal file
View File

0
Resources/c/b/b.txt Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

@ -1,48 +1,9 @@
import os
import shutil
import subprocess
import sys
def _maybe_relaunch_with_py311():
"""Use Python 3.11 to match ui/lui.pyd ABI on Windows."""
if sys.version_info >= (3, 11):
return
if os.environ.get("EG_RELAUNCHED_PY311") == "1":
return
if os.name != "nt":
return
py_launcher = shutil.which("py")
if not py_launcher:
print(f"⚠ 当前解释器是 Python {sys.version.split()[0]},未找到 py launcher无法自动切换到 3.11。")
return
try:
probe = subprocess.run(
[py_launcher, "-3.11", "-c", "import sys;print(sys.executable)"],
capture_output=True,
text=True,
check=False,
)
if probe.returncode != 0:
print("⚠ 未检测到可用的 Python 3.11LUI 可能不可用。")
return
except Exception as e:
print(f"⚠ 检测 Python 3.11 失败: {e}")
return
os.environ["EG_RELAUNCHED_PY311"] = "1"
relaunch_cmd = [py_launcher, "-3.11", os.path.abspath(__file__), *sys.argv[1:]]
print(f"✓ 检测到 Python {sys.version.split()[0]},自动切换到 Python 3.11: {probe.stdout.strip()}")
os.execv(py_launcher, relaunch_cmd)
# 添加项目根目录到 Python 路径
project_root = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, project_root)
third_party_path = os.path.join(project_root, "third_party")
if os.path.isdir(third_party_path):
sys.path.insert(0, third_party_path)
# 设置工作目录为项目根目录
os.chdir(project_root)
@ -61,7 +22,6 @@ sys.path.insert(0, icons_path)
# 现在可以导入并运行主程序
if __name__ == "__main__":
_maybe_relaunch_with_py311()
args = sys.argv[1:]
# args = "/home/tiger/桌面/Test1"
# args = "C:/Users/29381/Desktop/1"
@ -86,4 +46,4 @@ if __name__ == "__main__":
else:
# print(f"[DEBUG] 无项目路径,正常启动")
app = MyWorld()
app.run()
app.run()

View File

@ -496,7 +496,7 @@ class MoveGizmo(DirectObject):
def undo_last_move(self):
"""
Undo the last committed move.
Default hotkey: Ctrl+Z => 'control-z'.
Default hotkey (from Qt -> Panda3D translation): Ctrl+Z => 'control-z'.
"""
if not self._move_undo_stack:
self._log("undo_last_move: stack empty")
@ -530,7 +530,7 @@ class MoveGizmo(DirectObject):
# --- Mouse helpers -------------------------------------------------
def _get_normalized_mouse(self, extra) -> Optional[Point3]:
"""
Convert external UI pixel coordinates to Panda's
Convert Qt pixel coordinates (from QPanda3DWidget) to Panda's
normalized device coordinates (-1..1), or fall back to
mouseWatcherNode when available.
"""
@ -538,14 +538,14 @@ class MoveGizmo(DirectObject):
mouse = self.world.mouseWatcherNode.getMouse()
return Point3(mouse.x, mouse.y, 0)
# 1) Extra payload from external UI (pixels)
# 1) Extra payload from QPanda3DWidget (pixels)
if isinstance(extra, dict) and "x" in extra and "y" in extra:
parent = getattr(self.world, "parent", None)
if parent is not None:
w = max(parent.width(), 1)
h = max(parent.height(), 1)
nx = (extra["x"] / w) * 2.0 - 1.0
# Pixel origin is top-left; Panda origin is center with +Y up.
# Qt origin is topleft, Panda origin is center with +Y up
ny = 1.0 - (extra["y"] / h) * 2.0
return Point3(nx, ny, 0)

View File

@ -54,10 +54,10 @@ from .events import GizmoEvent
- 控件会根据摄像机距离自动缩放屏幕大小保持近似不变
集成方式示例
from direct.showbase.ShowBase import ShowBase
from TransformGizmo.rotate_gizmo import RotateGizmo
from QPanda3D.Panda3DWorld import Panda3DWorld
from QPanda3DExamples.rotate_gizmo import RotateGizmo
world = ShowBase()
world = Panda3DWorld()
model_np = world.render.attachNewNode("Box")
# ... 在 model_np 下加载模型
@ -72,7 +72,7 @@ from .events import GizmoEvent
鼠标事件要求
- 本类默认监听 Panda3D "mouse1" / "mouse1-up" / "mouse-move" 事件
- 如果从外部 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
"""
@ -955,7 +955,7 @@ class RotateGizmo(DirectObject):
"""
将鼠标转换到 Panda3D 的标准化设备坐标 [-1, 1]
优先使用外部 UI 传入的像素坐标extra 字典
优先使用 Qt / 外部 UI 传入的像素坐标extra 字典
如果没有则回退到 Panda3D mouseWatcherNode
"""
if self.world.mouseWatcherNode.hasMouse():
@ -969,7 +969,7 @@ class RotateGizmo(DirectObject):
w = max(parent.width(), 1)
h = max(parent.height(), 1)
nx = (extra["x"] / w) * 2.0 - 1.0
# 像素坐标原点在左上Panda3D 原点在中心Y 轴向上
# Qt 原点在左上Panda3D 原点在中心Y 轴向上
ny = 1.0 - (extra["y"] / h) * 2.0
return Point3(nx, ny, 0.0)

View File

@ -48,10 +48,10 @@ from .events import GizmoEvent
- 控件会根据摄像机距离自动缩放屏幕大小保持近似不变
集成方式示例
from direct.showbase.ShowBase import ShowBase
from TransformGizmo.scale_gizmo import ScaleGizmo
from QPanda3D.Panda3DWorld import Panda3DWorld
from QPanda3DExamples.scale_gizmo import ScaleGizmo
world = ShowBase()
world = Panda3DWorld()
model_np = world.render.attachNewNode("Box")
# ... 在 model_np 下加载模型
@ -66,7 +66,7 @@ from .events import GizmoEvent
鼠标事件要求
- 本类默认监听 Panda3D "mouse1" / "mouse1-up" / "mouse-move" 事件
- 如果从外部 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
"""

View File

@ -462,189 +462,10 @@ class TransformGizmo(DirectObject):
Callback used by MoveGizmo / RotateGizmo / ScaleGizmo to report that a
transform action (move/rotate/scale) has been committed.
"""
if self._record_action_with_command_manager(action):
return
self._history.append(action)
# New user action invalidates redo chain.
self._redo_history.clear()
def _coerce_vec3(self, value, fallback) -> p3d.Vec3:
if value is None:
return p3d.Vec3(fallback)
try:
return p3d.Vec3(value)
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
return p3d.Vec3(value[0], value[1], value[2])
return p3d.Vec3(fallback)
def _make_transform_mat(self, pos, hpr, scale) -> p3d.LMatrix4f:
try:
state = p3d.TransformState.make_pos_hpr_scale(pos, hpr, scale)
return p3d.LMatrix4f(state.get_mat())
except Exception:
pass
try:
state = p3d.TransformState.makePosHprScale(pos, hpr, scale)
return p3d.LMatrix4f(state.getMat())
except Exception:
temp = NodePath("tg_temp_transform")
temp.setPos(pos)
temp.setHpr(hpr)
temp.setScale(scale)
return p3d.LMatrix4f(temp.getMat())
def _invert_matrix(self, mat) -> Optional[p3d.LMatrix4f]:
inv = p3d.LMatrix4f(mat)
try:
inv.invertInPlace()
return inv
except Exception:
pass
try:
inv.invert_in_place()
return inv
except Exception:
return None
def _build_ssbo_group_snapshot_command(self, action: Dict[str, Any]):
node: NodePath = action.get("node")
if node is None or node.isEmpty() or (not node.hasTag("is_ssbo_proxy")):
return None
ssbo_editor = getattr(self.world, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
if not controller:
return None
selection_key = node.getTag("ssbo_selection_key") if node.hasTag("ssbo_selection_key") else None
selected_ids = list(controller.name_to_ids.get(selection_key, [])) if selection_key else []
if not selected_ids:
selected_ids = list(getattr(ssbo_editor, "selected_ids", []) or [])
targets = []
for gid in selected_ids:
obj_np = controller.id_to_object_np.get(gid)
if obj_np and not obj_np.is_empty():
targets.append(obj_np)
if not targets:
return None
current_pos = node.getPos(self.world.render)
current_hpr = node.getHpr(self.world.render)
current_scale = node.getScale(self.world.render)
old_pos = self._coerce_vec3(action.get("old_pos"), current_pos)
new_pos = self._coerce_vec3(action.get("new_pos"), current_pos)
old_hpr = self._coerce_vec3(action.get("old_hpr"), current_hpr)
new_hpr = self._coerce_vec3(action.get("new_hpr"), current_hpr)
old_scale = self._coerce_vec3(action.get("old_scale"), current_scale)
new_scale = self._coerce_vec3(action.get("new_scale"), current_scale)
old_proxy_mat = self._make_transform_mat(old_pos, old_hpr, old_scale)
new_proxy_mat = self._make_transform_mat(new_pos, new_hpr, new_scale)
new_proxy_inv = self._invert_matrix(new_proxy_mat)
if new_proxy_inv is None:
return None
before_state = []
after_state = []
for target in targets:
try:
current_world_mat = p3d.LMatrix4f(target.get_mat(self.world.render))
except Exception:
try:
current_world_mat = p3d.LMatrix4f(target.getMat(self.world.render))
except Exception:
continue
old_world_mat = p3d.LMatrix4f(current_world_mat * new_proxy_inv * old_proxy_mat)
before_state.append({"node": target, "mat": old_world_mat})
after_state.append({"node": target, "mat": current_world_mat})
if not before_state:
return None
def apply_state(state):
synced_nodes = []
for item in state:
target = item.get("node")
mat = item.get("mat")
if target is None or target.isEmpty() or mat is None:
continue
try:
target.set_mat(self.world.render, mat)
except Exception:
try:
target.setMat(self.world.render, mat)
except Exception:
continue
synced_nodes.append(target)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
try:
ssbo_editor.sync_scene_nodes_to_pick(synced_nodes)
except Exception:
pass
from core.Command_System import SnapshotStateCommand
return SnapshotStateCommand(apply_state, before_state, after_state)
def _record_action_with_command_manager(self, action: Dict[str, Any]) -> bool:
"""Prefer routing transform actions into the global command manager."""
command_manager = getattr(self.world, "command_manager", None)
if not command_manager:
return False
try:
group_command = self._build_ssbo_group_snapshot_command(action)
if group_command is not None:
command_manager.execute_command(group_command)
return True
from core.Command_System import MoveNodeCommand, RotateNodeCommand, ScaleNodeCommand
kind = action.get("kind")
node: NodePath = action.get("node")
if node is None or node.isEmpty():
return False
command = None
if kind == "move":
command = MoveNodeCommand(
node,
action.get("old_pos"),
action.get("new_pos"),
reference_node=self.world.render,
world=self.world,
)
elif kind == "rotate":
command = RotateNodeCommand(
node,
action.get("old_hpr"),
action.get("new_hpr"),
reference_node=self.world.render,
world=self.world,
)
elif kind == "scale":
command = ScaleNodeCommand(
node,
action.get("old_scale"),
action.get("new_scale"),
world=self.world,
)
if command is None:
return False
command_manager.execute_command(command)
return True
except Exception:
return False
def _sync_light_position_if_needed(self, node: Optional[NodePath]) -> None:
"""When target node wraps an RP light, keep RP light position in sync."""
try:

View File

@ -1,53 +0,0 @@
[Window][WindowOverViewport_11111111]
Pos=0,20
Size=2048,1084
Collapsed=0
[Window][工具栏]
Pos=453,20
Size=1326,32
Collapsed=0
DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
Size=451,748
Collapsed=0
DockId=0x00000007,0
[Window][属性面板]
Pos=1781,20
Size=267,390
Collapsed=0
DockId=0x00000003,0
[Window][脚本管理]
Pos=1781,412
Size=267,356
Collapsed=0
DockId=0x00000004,0
[Window][资源管理器]
Pos=0,770
Size=2048,334
Collapsed=0
DockId=0x0000000A,0
[Window][控制台]
Pos=0,770
Size=2048,334
Collapsed=0
DockId=0x0000000A,1
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
DockNode ID=0x00000009 Parent=0x08BD597D SizeRef=2560,748 Split=X
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=451,1084 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=1595,1084 Split=X
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1651,989 Split=Y
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=267,989 Split=Y Selected=0x3188AB8D
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x3188AB8D
DockNode ID=0x0000000A Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3

View File

@ -4,216 +4,6 @@ from typing import List
from panda3d.core import NodePath, Point3
def _is_valid_node(node) -> bool:
return bool(node) and hasattr(node, "isEmpty") and (not node.isEmpty())
def _is_light_node(node: NodePath) -> bool:
return bool(node) and hasattr(node, "hasTag") and node.hasTag("light_type")
def _is_terrain_node(node: NodePath) -> bool:
return bool(node) and hasattr(node, "hasTag") and node.hasTag("tree_item_type") and node.getTag("tree_item_type") == "TERRAIN_NODE"
def _set_light_registration(world, node: NodePath, registered: bool):
if not world or not _is_valid_node(node) or not _is_light_node(node):
return
scene_manager = getattr(world, "scene_manager", None)
light_type = node.getTag("light_type")
light_lists = []
if scene_manager:
if light_type == "spot_light" and hasattr(scene_manager, "Spotlight"):
light_lists.append(scene_manager.Spotlight)
elif light_type == "point_light" and hasattr(scene_manager, "Pointlight"):
light_lists.append(scene_manager.Pointlight)
rp_light = node.getPythonTag("rp_light_object") if hasattr(node, "hasPythonTag") and node.hasPythonTag("rp_light_object") else None
current_registered = bool(node.getPythonTag("engine_light_registered")) if hasattr(node, "hasPythonTag") and node.hasPythonTag("engine_light_registered") else False
if registered:
for light_list in light_lists:
if node not in light_list:
light_list.append(node)
if not current_registered:
try:
if rp_light is not None and getattr(world, "render_pipeline", None):
world.render_pipeline.add_light(rp_light)
elif hasattr(world, "render") and world.render:
world.render.setLight(node)
except Exception:
pass
try:
node.setPythonTag("engine_light_registered", True)
except Exception:
pass
return
for light_list in light_lists:
try:
while node in light_list:
light_list.remove(node)
except Exception:
pass
if current_registered:
try:
if rp_light is not None and getattr(world, "render_pipeline", None):
world.render_pipeline.remove_light(rp_light)
elif hasattr(world, "render") and world.render:
world.render.clearLight(node)
except Exception:
pass
try:
node.setPythonTag("engine_light_registered", False)
except Exception:
pass
def _set_terrain_registration(world, node: NodePath, registered: bool):
if not world or not _is_valid_node(node) or not _is_terrain_node(node):
return
terrain_manager = getattr(world, "terrain_manager", None)
if not terrain_manager or not hasattr(terrain_manager, "terrains"):
return
terrain_info = None
if hasattr(node, "hasPythonTag") and node.hasPythonTag("terrain_info"):
terrain_info = node.getPythonTag("terrain_info")
else:
for info in getattr(terrain_manager, "terrains", []):
if info.get("node") == node:
terrain_info = info
break
if terrain_info is not None:
try:
node.setPythonTag("terrain_info", terrain_info)
except Exception:
pass
if registered:
if terrain_info is not None:
terrain_info["node"] = node
if all(info.get("node") != node for info in terrain_manager.terrains):
terrain_manager.terrains.append(terrain_info)
return
try:
terrain_manager.terrains = [info for info in terrain_manager.terrains if info.get("node") != node]
except Exception:
pass
def _register_scene_node(world, node: NodePath):
if not world or not _is_valid_node(node):
return
scene_manager = getattr(world, "scene_manager", None)
_set_light_registration(world, node, True)
_set_terrain_registration(world, node, True)
if scene_manager and hasattr(scene_manager, "models") and node not in scene_manager.models:
scene_manager.models.append(node)
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _unregister_scene_node(world, node: NodePath):
if not world or not node:
return
scene_manager = getattr(world, "scene_manager", None)
_set_light_registration(world, node, False)
_set_terrain_registration(world, node, False)
if scene_manager and hasattr(scene_manager, "models"):
try:
while node in scene_manager.models:
scene_manager.models.remove(node)
except Exception:
pass
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _refresh_scene_tree(world):
if not world:
return
try:
if hasattr(world, "updateSceneTree"):
world.updateSceneTree()
except Exception:
pass
def _resolve_world(world=None):
if world:
return world
try:
from direct.showbase.ShowBaseGlobal import base
return base
except Exception:
return None
def _sync_scene_node_side_effects(world, nodes):
world = _resolve_world(world)
if not world:
return
ssbo_editor = getattr(world, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
try:
ssbo_editor.sync_scene_nodes_to_pick(nodes or [])
except Exception:
pass
def _sync_transform_side_effects(world, nodes):
_sync_scene_node_side_effects(world, nodes)
def _apply_vec3_method(node, method_name: str, value, reference_node=None):
if not _is_valid_node(node):
return
if reference_node is not None and not _is_valid_node(reference_node):
reference_node = None
# Mark node as dirty for the save system if it's a transform operation
if method_name.startswith("set") and method_name in (
"setPos", "setHpr", "setScale", "setX", "setY", "setZ",
"setH", "setP", "setR", "setSx", "setSy", "setSz", "setMat"
):
try:
node.setTag("scene_transform_dirty", "true")
except Exception:
pass
method = getattr(node, method_name)
if reference_node is not None:
try:
method(reference_node, value)
return
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
method(reference_node, value[0], value[1], value[2])
return
else:
try:
method(value)
return
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
method(value[0], value[1], value[2])
return
method(value)
class Command(ABC):
"""
抽象命令类所有具体命令都需要继承此类
@ -260,22 +50,13 @@ class CommandManager:
"""
try:
command.execute()
self.record_command(command)
self._undo_stack.append(command)
# 清空重做栈,因为执行新命令后就无法重做之前的命令了
self._redo_stack.clear()
except Exception as e:
print(f"执行命令时出错: {e}")
raise
def record_command(self, command: Command):
"""记录一个已经执行完成的命令。"""
self._undo_stack.append(command)
self._redo_stack.clear()
def pop_last_command(self):
"""弹出最后一个已执行命令,供复合操作合并历史使用。"""
if not self._undo_stack:
return None
return self._undo_stack.pop()
def undo(self) -> bool:
"""
撤销上一个命令
@ -349,37 +130,31 @@ class CommandManager:
# 示例命令实现
class MoveNodeCommand(Command):
"""
Move node command.
移动节点命令示例
"""
def __init__(self, node: NodePath, old_pos, new_pos, reference_node=None, world=None):
def __init__(self, node: NodePath, old_pos, new_pos):
self.node = node
self.old_pos = old_pos
self.new_pos = new_pos
self.reference_node = reference_node
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setPos", value, self.reference_node)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
Execute move operation.
执行移动操作
"""
self._apply(self.new_pos)
self.node.setPos(self.new_pos)
def undo(self):
"""
Undo move operation.
撤销移动操作
"""
self._apply(self.old_pos)
self.node.setPos(self.old_pos)
def redo(self):
"""
Redo move operation.
重做移动操作
"""
self._apply(self.new_pos)
self.node.setPos(self.new_pos)
class DeleteNodeCommand(Command):
@ -511,10 +286,8 @@ class DeleteNodeCommand(Command):
for i in reversed(tilesets_to_remove):
del scene_manager.tilesets[i]
_unregister_scene_node(self.world, self.node)
# 从场景图中移除节点,使用 detachNode 而不是 removeNode 以便可以撤销
if _is_valid_node(self.node):
if self.node and not self.node.isEmpty():
self.node.detachNode()
def undo(self):
@ -522,12 +295,9 @@ class DeleteNodeCommand(Command):
撤销删除操作恢复旧节点
"""
try:
if _is_valid_node(self.node):
if self.node and not self.node.isEmpty():
# 直接将节点挂载回原父节点
if _is_valid_node(self.parent_node):
self.node.reparentTo(self.parent_node)
elif self.world and _is_valid_node(getattr(self.world, "render", None)):
self.node.reparentTo(self.world.render)
self.node.reparentTo(self.parent_node)
# 恢复到相应的管理器列表中
if self.world and hasattr(self.world, 'scene_manager'):
@ -549,8 +319,6 @@ class DeleteNodeCommand(Command):
# 简单恢复到 tilesets
if hasattr(scene_manager, 'tilesets'):
scene_manager.tilesets.append({'node': self.node, 'url': self.extra_data.get('tileset_url', '')})
_register_scene_node(self.world, self.node)
print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复")
else:
@ -570,147 +338,60 @@ class DeleteNodeCommand(Command):
class RotateNodeCommand(Command):
"""
Rotate node command.
旋转节点命令
"""
def __init__(self, node: NodePath, old_hpr, new_hpr, reference_node=None, world=None):
def __init__(self, node: NodePath, old_hpr, new_hpr):
self.node = node
self.old_hpr = old_hpr
self.new_hpr = new_hpr
self.reference_node = reference_node
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setHpr", value, self.reference_node)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
Execute move operation.
执行旋转操作
"""
self._apply(self.new_hpr)
self.node.setHpr(self.new_hpr)
def undo(self):
"""
Undo move operation.
撤销旋转操作
"""
self._apply(self.old_hpr)
self.node.setHpr(self.old_hpr)
def redo(self):
"""
Redo move operation.
重做旋转操作
"""
self._apply(self.new_hpr)
self.node.setHpr(self.new_hpr)
class ScaleNodeCommand(Command):
"""
Scale node command.
缩放节点命令
"""
def __init__(self, node: NodePath, old_scale, new_scale, world=None):
def __init__(self, node: NodePath, old_scale, new_scale):
self.node = node
self.old_scale = old_scale
self.new_scale = new_scale
self.world = world
def _apply(self, value):
_apply_vec3_method(self.node, "setScale", value)
_sync_transform_side_effects(self.world, [self.node])
def execute(self):
"""
Execute rotate operation.
执行缩放操作
"""
self._apply(self.new_scale)
self.node.setScale(self.new_scale)
def undo(self):
"""
Undo rotate operation.
撤销缩放操作
"""
self._apply(self.old_scale)
self.node.setScale(self.old_scale)
def redo(self):
"""
Redo rotate operation.
重做缩放操作
"""
self._apply(self.new_scale)
class RenameNodeCommand(Command):
"""Rename a node and refresh scene tree bindings."""
def __init__(self, node: NodePath, old_name: str, new_name: str, world=None):
self.node = node
self.old_name = old_name
self.new_name = new_name
self.world = world
def execute(self):
if _is_valid_node(self.node):
self.node.setName(self.new_name)
_refresh_scene_tree(self.world)
def undo(self):
if _is_valid_node(self.node):
self.node.setName(self.old_name)
_refresh_scene_tree(self.world)
def redo(self):
self.execute()
class VisibilityNodeCommand(Command):
"""Toggle editor visibility state for a node."""
def __init__(self, node: NodePath, old_visible: bool, new_visible: bool, world=None):
self.node = node
self.old_visible = bool(old_visible)
self.new_visible = bool(new_visible)
self.world = world
def _apply(self, visible: bool):
if not _is_valid_node(self.node):
return
self.node.setPythonTag("user_visible", bool(visible))
if visible:
self.node.show()
else:
self.node.hide()
_sync_scene_node_side_effects(self.world, [self.node])
def execute(self):
self._apply(self.new_visible)
def undo(self):
self._apply(self.old_visible)
def redo(self):
self.execute()
class MaterialStateCommand(Command):
"""Replay a captured material snapshot for undo/redo."""
def __init__(self, apply_state_callback, before_state, after_state):
self.apply_state_callback = apply_state_callback
self.before_state = before_state
self.after_state = after_state
def execute(self):
if self.apply_state_callback:
self.apply_state_callback(self.after_state)
def undo(self):
if self.apply_state_callback:
self.apply_state_callback(self.before_state)
def redo(self):
self.execute()
class SnapshotStateCommand(MaterialStateCommand):
"""Generic callback-based snapshot command."""
self.node.setScale(self.new_scale)
class CreateNodeCommand(Command):
@ -718,99 +399,49 @@ class CreateNodeCommand(Command):
创建节点命令
"""
def __init__(self, node_creator_func, parent_node, *args, world=None, **kwargs):
def __init__(self, node_creator_func,parent_node, *args, **kwargs):
self.node_creator_func = node_creator_func
self.parent_node = parent_node
self.args = args
self.kwargs = kwargs
self.world = world
self.created_node = None
def execute(self):
"""
执行创建节点操作
"""
if _is_valid_node(self.created_node):
target_parent = self.parent_node
if (not _is_valid_node(target_parent)) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(target_parent):
self.created_node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return self.created_node
self.created_node = self.node_creator_func(self.parent_node, *self.args, **self.kwargs)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs)
return self.created_node
def undo(self):
"""
撤销创建节点操作
"""
if _is_valid_node(self.created_node):
_unregister_scene_node(self.world, self.created_node)
if self.created_node:
self.created_node.detachNode()
_sync_scene_node_side_effects(self.world, [self.created_node])
def redo(self):
"""
重做创建节点操作
"""
if _is_valid_node(self.created_node):
target_parent = self.parent_node
if (not _is_valid_node(target_parent)) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(target_parent):
self.created_node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.created_node)
_sync_scene_node_side_effects(self.world, [self.created_node])
return
self.execute()
class AttachNodeCommand(Command):
"""Attach an existing detached node into a parent and make it undoable."""
class ReparentNodeCommand(Command):
"""
重新设置节点父子关系命令 - 增强版同时处理Panda3D和Qt树
"""
def __init__(self, node: NodePath, parent_node: NodePath, world=None):
def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath,
old_parent_item=None, new_parent_item=None, is_2d_gui=False, world=None):
self.node = node
self.parent_node = parent_node
self.old_parent = old_parent
self.new_parent = new_parent
self.old_parent_item = old_parent_item # Qt树中的旧父节点项
self.new_parent_item = new_parent_item # Qt树中的新父节点项
self.is_2d_gui = is_2d_gui
self.world = world
def execute(self):
target_parent = self.parent_node
if (not target_parent or target_parent.isEmpty()) and self.world:
target_parent = getattr(self.world, "render", None)
if _is_valid_node(self.node) and _is_valid_node(target_parent):
self.node.wrtReparentTo(target_parent)
_register_scene_node(self.world, self.node)
_sync_scene_node_side_effects(self.world, [self.node])
return self.node
def undo(self):
if _is_valid_node(self.node):
_unregister_scene_node(self.world, self.node)
self.node.detachNode()
_sync_scene_node_side_effects(self.world, [self.node])
def redo(self):
self.execute()
class ReparentNodeCommand(Command):
"""
重新设置节点父子关系命令
"""
def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath,
is_2d_gui=False, world=None):
self.node = node
self.old_parent = old_parent
self.new_parent = new_parent
self.is_2d_gui = is_2d_gui
self.world = world
# 保存节点在操作前的世界坐标和局部坐标,以便在撤销/重做时保持位置不变
self.world_pos = node.getPos(self.world.render if self.world else node.getParent())
self.world_hpr = node.getHpr(self.world.render if self.world else node.getParent())
@ -820,16 +451,34 @@ class ReparentNodeCommand(Command):
self.local_hpr = node.getHpr()
self.local_scale = node.getScale()
def _updateQtTree(self, node_item, new_parent_item):
"""更新Qt树控件中的节点位置"""
if not node_item or not new_parent_item:
return
# 从当前父节点中移除
current_parent = node_item.parent()
if current_parent:
current_parent.removeChild(node_item)
else:
# 如果是顶级项目
tree_widget = node_item.treeWidget()
if tree_widget:
index = tree_widget.indexOfTopLevelItem(node_item)
if index >= 0:
tree_widget.takeTopLevelItem(index)
# 添加到新父节点
new_parent_item.addChild(node_item)
def execute(self):
"""
执行重新父化操作
"""
if not _is_valid_node(self.node):
return
# 更新Panda3D节点父子关系
if self.is_2d_gui and self.world:
# 2D GUI元素需要特殊处理
if _is_valid_node(self.new_parent):
if self.new_parent and not self.new_parent.isEmpty():
if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
self.node.wrtReparentTo(self.new_parent)
@ -843,7 +492,7 @@ class ReparentNodeCommand(Command):
print(f"2D GUI元素重新父化到aspect2d")
else:
# 普通3D节点的处理
if _is_valid_node(self.new_parent):
if self.new_parent and not self.new_parent.isEmpty():
self.node.wrtReparentTo(self.new_parent)
else:
# 如果新父节点为空将其父化到render节点
@ -858,15 +507,13 @@ class ReparentNodeCommand(Command):
"""
撤销重新父化操作
"""
if not _is_valid_node(self.node):
return
# 在改变父节点前保存当前的缩放值
current_scale = self.node.getScale()
# 恢复Panda3D节点父子关系
if self.is_2d_gui and self.world:
# 2D GUI元素需要特殊处理
if _is_valid_node(self.old_parent):
if self.old_parent and not self.old_parent.isEmpty():
if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1":
# 原父节点是GUI元素直接重新父化
self.node.wrtReparentTo(self.old_parent)
@ -880,7 +527,7 @@ class ReparentNodeCommand(Command):
print(f"2D GUI元素恢复到aspect2d")
else:
# 普通3D节点的处理
if _is_valid_node(self.old_parent):
if self.old_parent and not self.old_parent.isEmpty():
self.node.wrtReparentTo(self.old_parent)
else:
# 如果原父节点为空将其父化到render节点
@ -902,15 +549,13 @@ class ReparentNodeCommand(Command):
"""
重做重新父化操作
"""
if not _is_valid_node(self.node):
return
# 在改变父节点前保存当前的缩放值
current_scale = self.node.getScale()
# 重新执行Panda3D节点父子关系更新
if self.is_2d_gui and self.world:
# 2D GUI元素需要特殊处理
if _is_valid_node(self.new_parent):
if self.new_parent and not self.new_parent.isEmpty():
if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1":
# 目标是GUI元素直接重新父化
self.node.wrtReparentTo(self.new_parent)
@ -924,7 +569,7 @@ class ReparentNodeCommand(Command):
print(f"2D GUI元素重新父化到aspect2d")
else:
# 普通3D节点的处理
if _is_valid_node(self.new_parent):
if self.new_parent and not self.new_parent.isEmpty():
self.node.wrtReparentTo(self.new_parent)
else:
# 如果新父节点为空将其父化到render节点
@ -971,40 +616,40 @@ class CompositeCommand(Command):
for command in self.commands:
command.redo()
class MoveLightCommand(Command):
def __init__(self, node, old_pos, new_pos, light_object=None):
self.node = node
self.old_pos = Point3(old_pos)
self.new_pos = Point3(new_pos)
self.light_object = light_object
def _apply_light_position(self, pos):
if not self.light_object:
return
try:
self.light_object.setPos(pos)
return
except Exception:
pass
try:
self.light_object.setPos(pos.x, pos.y, pos.z)
return
except Exception:
pass
try:
self.light_object.pos = Point3(pos)
except Exception:
pass
def execute(self): # 将原来的 do() 改为 execute()
self._apply_light_position(self.new_pos)
if _is_valid_node(self.node):
self.node.setPos(self.new_pos)
def undo(self):
self._apply_light_position(self.old_pos)
if _is_valid_node(self.node):
self.node.setPos(self.old_pos)
class MoveLightCommand(Command):
def __init__(self, node, old_pos, new_pos, light_object=None):
self.node = node
self.old_pos = Point3(old_pos)
self.new_pos = Point3(new_pos)
self.light_object = light_object
def _apply_light_position(self, pos):
if not self.light_object:
return
try:
self.light_object.setPos(pos)
return
except Exception:
pass
try:
self.light_object.setPos(pos.x, pos.y, pos.z)
return
except Exception:
pass
try:
self.light_object.pos = Point3(pos)
except Exception:
pass
def execute(self): # 将原来的 do() 改为 execute()
self._apply_light_position(self.new_pos)
if self.node:
self.node.setPos(self.new_pos)
def undo(self):
self._apply_light_position(self.old_pos)
if self.node:
self.node.setPos(self.old_pos)
def redo(self):
self.execute() # 调用 execute() 而不是 do()

View File

@ -64,12 +64,10 @@ class CustomMouseController:
self.keyMap[key] = value
# 只在右键按下时记录鼠标位置
if key == "mouse3" and value == True:
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if watcher and watcher.hasMouse():
mouse_pos = watcher.getMouse()
if mouse_pos:
self.last_mouse_x = mouse_pos.get_x()
self.last_mouse_y = mouse_pos.get_y()
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
if mouse_pos:
self.last_mouse_x = mouse_pos.get_x()
self.last_mouse_y = mouse_pos.get_y()
def move(self, task):
dt = self.showbase.clock.dt
@ -100,10 +98,7 @@ class CustomMouseController:
try:
# 检查是否应该处理鼠标事件避免与ImGui冲突
if self._should_handle_mouse():
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if not watcher or not watcher.hasMouse():
return task.cont
mouse_pos = watcher.getMouse()
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
if mouse_pos:
current_x = mouse_pos.get_x()
current_y = mouse_pos.get_y()
@ -165,10 +160,7 @@ class CustomMouseController:
pass
# 检查鼠标位置是否在ImGui窗口区域内
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if not watcher or not watcher.hasMouse():
return True
mouse_pos = watcher.getMouse()
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
if not mouse_pos:
return True
@ -193,4 +185,4 @@ class CustomMouseController:
return True
except Exception as e:
print(f"鼠标事件检测失败: {e}")
return True
return True

1693
core/InfoPanelManager.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,26 @@
"""
Core package - 核心功能模块
Keep package imports lazy so lightweight runtime contexts can import
`core.script_system` without eagerly pulling in the editor stack.
"""
from importlib import import_module
__all__ = [
"CoreWorld",
"SelectionSystem",
"EventHandler",
"ToolManager",
"ScriptManager",
"ScriptBase",
"ScriptComponent",
]
_LAZY_IMPORTS = {
"CoreWorld": ("core.world", "CoreWorld"),
"SelectionSystem": ("core.selection", "SelectionSystem"),
"EventHandler": ("core.event_handler", "EventHandler"),
"ToolManager": ("core.tool_manager", "ToolManager"),
"ScriptManager": ("core.script_system", "ScriptManager"),
"ScriptBase": ("core.script_system", "ScriptBase"),
"ScriptComponent": ("core.script_system", "ScriptComponent"),
}
def __getattr__(name):
if name not in _LAZY_IMPORTS:
raise AttributeError(f"module 'core' has no attribute {name!r}")
module_name, attr_name = _LAZY_IMPORTS[name]
module = import_module(module_name)
value = getattr(module, attr_name)
globals()[name] = value
return value
"""
Core package - 核心功能模块
包含引擎的核心功能
- world.py: 基础世界功能相机光照地板等
- selection.py: 选择和变换系统
- event_handler.py: 事件处理系统
- tool_manager.py: 工具管理系统
- script_system.py: 脚本系统
"""
from .world import CoreWorld
from .selection import SelectionSystem
from .event_handler import EventHandler
from .tool_manager import ToolManager
from .script_system import ScriptManager, ScriptBase, ScriptComponent
__all__ = [
'CoreWorld',
'SelectionSystem',
'EventHandler',
'ToolManager',
'ScriptManager',
'ScriptBase',
'ScriptComponent'
]

2339
core/assembly_interaction.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +0,0 @@
"""Editor context adapter for the ImGui editor runtime."""
class EditorContext:
"""Provide a stable access layer for editor-facing services."""
def __init__(self, world):
self.world = world
def get_tree_widget(self):
return None
def get_gui_manager(self):
return getattr(self.world, "gui_manager", None)
def ensure_gui_elements(self):
gui_manager = self.get_gui_manager()
if gui_manager is not None:
gui_elements = getattr(gui_manager, "gui_elements", None)
if gui_elements is None:
gui_elements = []
setattr(gui_manager, "gui_elements", gui_elements)
return gui_elements
gui_elements = getattr(self.world, "gui_elements", None)
if gui_elements is None:
gui_elements = []
setattr(self.world, "gui_elements", gui_elements)
return gui_elements
def append_gui_element(self, element):
gui_elements = self.ensure_gui_elements()
gui_elements.append(element)
return True
def get_editor_context(owner):
"""Get or create editor context from a world/owner object."""
world = getattr(owner, "world", owner)
context = getattr(world, "_editor_context", None)
if context is None or getattr(context, "world", None) is not world:
context = EditorContext(world)
setattr(world, "_editor_context", context)
return context

View File

@ -1,7 +1,6 @@
from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState,
DepthTestAttrib, ColorAttrib)
from core.editor_context import get_editor_context
class EventHandler:
@ -10,29 +9,11 @@ class EventHandler:
def __init__(self, world):
"""初始化事件处理器"""
self.world = world
self._editor_context = get_editor_context(world)
# 射线显示相关
self.showRay = False # 是否显示射线(默认关闭)
self.rayNode = None # 当前显示的射线节点
self.rayLifetime = 2.0 # 射线显示时间(秒)
def _get_editor_context(self):
if not getattr(self, "_editor_context", None):
self._editor_context = get_editor_context(self.world)
return self._editor_context
def _get_tree_widget(self):
"""Qt tree has been removed in the ImGui editor."""
return self._get_editor_context().get_tree_widget()
def _get_gui_manager(self):
"""安全获取 GUI 管理器。"""
return self._get_editor_context().get_gui_manager()
def _sync_tree_selection(self, selected_model):
"""Scene tree selection is rendered directly from editor state in ImGui."""
return False
def showClickRay(self, nearPoint, farPoint, hitPos=None):
"""显示鼠标点击的射线"""
@ -210,10 +191,68 @@ class EventHandler:
pickerNP.removeNode()
return
# 优先检查是否点击了坐标轴
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
if self.world.selection.gizmo:
#print("准备检查坐标轴点击...")
try:
highlighted_axis = self.world.selection.gizmoHighlightAxis
if highlighted_axis:
print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽")
# 直接使用高亮轴开始拖拽
self.world.selection.startGizmoDrag(highlighted_axis, x, y)
pickerNP.removeNode()
return
# 如果没有高亮轴,再尝试检测点击
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
if gizmoAxis:
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# 开始坐标轴拖拽
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
pickerNP.removeNode()
return
else:
print("× 没有点击到坐标轴")
# gizmoAxis = self.world.selection.checkGizmoClick(x, y)
# if gizmoAxis:
# #print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# # 开始坐标轴拖拽
# self.world.selection.startGizmoDrag(gizmoAxis, x, y)
# pickerNP.removeNode()
# return
# else:
# print("× 没有点击到坐标轴")
except Exception as e:
print(f"❌ 坐标轴点击检测出现异常: {str(e)}")
import traceback
traceback.print_exc()
print("继续处理模型选择...")
#print("继续处理碰撞结果...")
if hitPos and hitNode:
#print(f"✓ 检测到碰撞,开始处理点击事件")
#print(f"GUI编辑模式: {self.world.guiEditMode}")
#print(f"当前工具: {self.world.currentTool}")
# 处理GUI编辑模式
if self.world.guiEditMode:
#print("处理GUI编辑模式点击")
# 检查是否点击了GUI元素
clickedGUI = self.world.gui_manager.findClickedGUI(hitNode)
if clickedGUI:
# 选中GUI元素
self.world.selection.updateSelection(clickedGUI)
self.world.gui_manager.selectGUIInTree(clickedGUI)
print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}")
elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool:
# 在点击位置创建新GUI元素
self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool)
pickerNP.removeNode()
return
# 根据当前工具处理点击事件
if self.world.currentTool in ("选择", "移动", "旋转", "缩放"):
print("✓ 使用选择工具处理点击")
@ -228,7 +267,20 @@ class EventHandler:
print(f"当前工具不是'选择',无法处理: {self.world.currentTool}")
else:
print("没有检测到碰撞")
self.world.selection.updateSelection(None)
# 如果不在GUI编辑模式清除选择
if not self.world.guiEditMode:
self.world.selection.updateSelection(None)
# 在GUI编辑模式下即使没有碰撞也可以在空白区域创建GUI
if (self.world.guiEditMode and
hasattr(self.world, 'currentGUITool') and
self.world.currentGUITool):
# 使用默认的地面高度创建GUI
default_height = 0.0
world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换
world_pos.setZ(default_height)
self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool)
# 确保总是清理碰撞检测节点
try:
@ -389,120 +441,89 @@ class EventHandler:
def _handleSelectionClick(self, hitNode):
"""处理选择工具的点击事件"""
print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}")
def _is_valid_node(node):
if not node:
return False
try:
return not node.isEmpty()
except Exception:
return False
def _is_helper_node(node):
if not _is_valid_node(node):
return True
try:
name = node.getName() or ""
except Exception:
name = ""
lowered = name.lower()
return (
isinstance(node.node(), CollisionNode)
or lowered.startswith("modelcollision_")
or lowered.startswith("collision_")
or lowered.startswith("gizmo")
)
def _resolve_model_root(node):
current = node
model_list = self.world.models if hasattr(self.world, "models") else []
while _is_valid_node(current) and current != self.world.render:
try:
if current in model_list or current.hasTag("is_model_root"):
return current
current = current.getParent()
except Exception:
break
return None
# In SSBO mode, animated/legacy models still rely on modelCollision_* for picking,
# so we cannot ignore those hits unconditionally.
# Only treat a hit on the *currently selected same model root* as blank-space clear.
if getattr(self.world, "use_ssbo_mouse_picking", False):
try:
hit_name = hitNode.getName() or ""
except Exception:
hit_name = ""
if hit_name.lower().startswith("modelcollision_"):
current_selected = None
try:
current_selected = self.world.selection.getSelectedNode()
except Exception:
current_selected = getattr(getattr(self, "world", None), "selection", None)
owner_root = _resolve_model_root(hitNode)
try:
same_root = bool(
current_selected and owner_root and current_selected == owner_root
)
except Exception:
same_root = False
if same_root:
print("SSBO 模式下命中当前选中模型的辅助碰撞壳,按空白区域清除选择")
self.world.selection.updateSelection(None)
return
def _is_selectable_scene_node(node):
if not _is_valid_node(node) or node == self.world.render:
return False
if _is_helper_node(node):
return False
try:
if node.hasTag("is_scene_element") or node.hasTag("is_model_root"):
return True
except Exception:
pass
return True
def _resolve_pick_target(node):
current = node
fallback_model_root = None
model_list = self.world.models if hasattr(self.world, "models") else []
while _is_valid_node(current) and current != self.world.render:
try:
if current in model_list or current.hasTag("is_model_root"):
fallback_model_root = current
if not _is_helper_node(current):
return current
if _is_selectable_scene_node(current):
return current
current = current.getParent()
except Exception:
break
return fallback_model_root
selected_node = _resolve_pick_target(hitNode)
if selected_node:
print(f"最终选中节点: {selected_node.getName()}")
self.world.selection.handleMouseClick(selected_node)
# 更新选择状态并显示选择框和坐标轴
self.world.selection.updateSelection(selected_node)
# 在树形控件中查找并选中对应的项
if self._sync_tree_selection(selected_node):
tree_widget = self._get_tree_widget()
if tree_widget and tree_widget.currentItem():
print(f"✓ 在树形控件中找到对应项: {tree_widget.currentItem().text(0)}")
# 查找对应的实际模型节点
selectedModel = None
# 如果点击的是碰撞节点,找到它的父模型
if isinstance(hitNode.node(), CollisionNode):
print(f"点击的是碰撞节点: {hitNode.getName()}")
# 碰撞节点的父节点应该是模型
parent = hitNode.getParent()
if parent in self.world.models:
selectedModel = parent
print(f"找到对应的模型: {selectedModel.getName()}")
else:
print("× 场景树未同步(接口不可用或未找到对应项)")
print(f"碰撞节点的父节点不是模型: {parent.getName()}")
else:
# 查找可选择的节点(模型或其子节点)
current = hitNode
while current != self.world.render:
# 检查是否是模型
if current in self.world.models:
selectedModel = current
print(f"找到模型节点: {selectedModel.getName()}")
break
# 检查是否是模型的子节点
for model in self.world.models:
if current.getParent() == model or current.isAncestorOf(model):
selectedModel = model
print(f"找到父模型: {selectedModel.getName()}")
break
if selectedModel:
break
current = current.getParent()
if selectedModel:
#print(f"✓ 最终选中模型: {selectedModel.getName()}")
self.world.selection.handleMouseClick(selectedModel)
# 更新选择状态并显示选择框和坐标轴
self.world.selection.updateSelection(selectedModel)
# 在树形控件中查找并选中对应的项
if hasattr(self.world, 'interface_manager') and self.world.interface_manager and hasattr(self.world.interface_manager, 'treeWidget') and self.world.interface_manager.treeWidget:
#print("查找树形控件中的对应项...")
root = self.world.interface_manager.treeWidget.invisibleRootItem()
foundItem = None
for i in range(root.childCount()):
sceneItem = root.child(i)
if sceneItem.text(0) == "场景":
#print(f"在场景节点下查找...")
foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem)
if foundItem:
print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}")
try:
self.world.interface_manager.treeWidget.itemClicked.disconnect()
except TypeError:
pass
self.world.interface_manager.treeWidget.setCurrentItem(foundItem)
self.world.interface_manager.treeWidget.itemClicked.connect(
self.world.interface_manager.onTreeItemClicked)
else:
print("× 在树形控件中没有找到对应项")
break
if not foundItem:
print("× 没有找到场景节点或对应的树形项")
else:
print("× 树形控件不存在")
else:
print("× 没有找到可选择的模型节点")
self.world.selection.updateSelection(None)
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""
return
# 处理坐标轴拖拽结束
if self.world.selection.isDraggingGizmo:
self.world.selection.stopGizmoDrag()
return
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进)"""
@ -510,12 +531,11 @@ class EventHandler:
super(type(self.world), self.world).wheelForward(data)
# 更新属性面板
tree_widget = self._get_tree_widget()
current_item = tree_widget.currentItem() if tree_widget else None
if (current_item and
current_item.text(0) == "相机" and
hasattr(self.world, "property_panel")):
self.world.property_panel.updatePropertyPanel(current_item)
if (self.world.interface_manager.treeWidget and
self.world.interface_manager.treeWidget.currentItem() and
self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"):
self.world.property_panel.updatePropertyPanel(
self.world.interface_manager.treeWidget.currentItem())
def wheelBackward(self, data=None):
"""处理滚轮向后滚动(后退)"""
@ -523,12 +543,11 @@ class EventHandler:
super(type(self.world), self.world).wheelBackward(data)
# 更新属性面板
tree_widget = self._get_tree_widget()
current_item = tree_widget.currentItem() if tree_widget else None
if (current_item and
current_item.text(0) == "相机" and
hasattr(self.world, "property_panel")):
self.world.property_panel.updatePropertyPanel(current_item)
if (self.world.interface_manager.treeWidget and
self.world.interface_manager.treeWidget.currentItem() and
self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"):
self.world.property_panel.updatePropertyPanel(
self.world.interface_manager.treeWidget.currentItem())
def mousePressEventMiddle(self, evt):
"""处理鼠标中键按下事件"""
@ -544,6 +563,30 @@ class EventHandler:
"""处理鼠标移动事件"""
if not evt:
return
# 处理坐标轴拖拽
if self.world.selection.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
# 将屏幕坐标转换为世界坐标
mx = 2.0 * x / float(winWidth) - 1.0
my = 1.0 - 2.0 * y / float(winHeight)
# 更新坐标轴拖拽
self.world.selection.updateGizmoDrag(x, y)
return
# 更新坐标轴高亮(鼠标悬停效果)
if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
# 减少高亮调试输出,只在需要时输出
# 已静默处理,避免控制台刷屏
self.world.selection.updateGizmoHighlight(x, y)
# 调用CoreWorld的父类方法处理基础的相机旋转
super(type(self.world), self.world).mouseMoveEvent(evt)

View File

@ -24,8 +24,6 @@ class ImGuiStyleManager:
self.world = world
self.io = imgui_backend.io
self.style = None # 延迟初始化在apply_style中设置
self._icon_cache = {}
self._missing_icons = set()
# 颜色定义 - 与Qt UI保持一致
self.colors = {
@ -341,7 +339,6 @@ class ImGuiStyleManager:
style.scrollbar_rounding = self.sizes['frame_rounding']
style.grab_min_size = 10.0
style.grab_rounding = self.sizes['frame_rounding']
style.window_menu_button_position = imgui.Dir_.none
# 禁用一些ImGui的默认效果使其更像Qt
style.window_border_size = 1.0
@ -380,14 +377,6 @@ class ImGuiStyleManager:
flags = self.get_window_flags(window_type)
return imgui_ctx.begin(name, open, flags)
def prepare_centered_dialog(self, width, height, cond=imgui.Cond_.appearing):
"""Place a modal/dialog in the center of the current main viewport."""
viewport = imgui.get_main_viewport()
center_x = viewport.pos.x + (viewport.size.x - width) / 2
center_y = viewport.pos.y + (viewport.size.y - height) / 2
imgui.set_next_window_size((width, height), cond)
imgui.set_next_window_pos((center_x, center_y), cond)
def styled_button(self, label, size=(0, 0)):
"""绘制带样式的按钮"""
@ -405,40 +394,24 @@ class ImGuiStyleManager:
def load_icon(self, icon_name):
"""加载图标纹理为ImGui可用的格式"""
if icon_name in self._icon_cache:
return self._icon_cache[icon_name]
if icon_name in self._missing_icons:
return None
try:
# 构建图标路径
project_root = Path(__file__).resolve().parent.parent
icon_path = project_root / "icons" / f"{icon_name}.png"
if not icon_path.exists():
if icon_path.exists():
# 使用base.imgui.loadTexture方法
if hasattr(base, 'imgui') and hasattr(base.imgui, 'loadTexture'):
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture不支持 Filename 对象
fn = Filename.fromOsSpecific(str(icon_path))
return base.imgui.loadTexture(fn.getFullpath())
else:
print(f"⚠ ImGui后端未初始化")
return None
else:
print(f"⚠ 图标文件不存在: {icon_path}")
self._missing_icons.add(icon_name)
return None
base_app = getattr(self.world, "base", None) if self.world else None
if base_app is None:
try:
from direct.showbase.ShowBaseGlobal import base as base_app
except Exception:
base_app = None
imgui_runtime = getattr(base_app, "imgui", None) if base_app else None
texture_loader = getattr(imgui_runtime, "loadTexture", None)
if not callable(texture_loader):
print("⚠ ImGui后端未初始化")
return None
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture不支持 Filename 对象
fn = Filename.fromOsSpecific(str(icon_path))
texture = texture_loader(fn.getFullpath())
self._icon_cache[icon_name] = texture
return texture
except Exception as e:
print(f"⚠ 加载图标失败: {e}")
return None
@ -446,72 +419,6 @@ class ImGuiStyleManager:
def image_button(self, texture_id, size=(32, 32), bg_col=(0, 0, 0, 0), tint_col=(1, 1, 1, 1)):
"""绘制图像按钮"""
return imgui.image_button(texture_id, size, bg_col, tint_col)
def draw_stat_chip(self, label, tint=None, text_color=None):
"""绘制不可交互的状态胶囊。"""
if tint is None:
tint = self.colors['button_bg']
if text_color is None:
text_color = self.colors['text']
text_size = imgui.calc_text_size(label)
horizontal_padding = 8.0
vertical_padding = 4.0
chip_width = float(text_size.x) + horizontal_padding * 2.0
chip_height = float(text_size.y) + vertical_padding * 2.0
cursor_pos = imgui.get_cursor_screen_pos()
draw_list = imgui.get_window_draw_list()
bg_color = imgui.color_convert_float4_to_u32(tint)
fg_color = imgui.color_convert_float4_to_u32(text_color)
draw_list.add_rect_filled(
cursor_pos,
(cursor_pos.x + chip_width, cursor_pos.y + chip_height),
bg_color,
12.0,
)
draw_list.add_text(
(cursor_pos.x + horizontal_padding, cursor_pos.y + vertical_padding),
fg_color,
label,
)
imgui.dummy((chip_width, chip_height))
def draw_toolbar_button(self, label, active=False, size=(56, 28), tooltip=None, enabled=True):
"""绘制统一风格的工具栏按钮。"""
if active:
button_color = self.colors['primary']
hovered_color = self.colors['primary_dark']
text_color = (1.0, 1.0, 1.0, 1.0)
border_color = self.colors['primary_dark']
else:
button_color = self.colors['button_bg']
hovered_color = self.colors['panel_bg']
text_color = self.colors['text']
border_color = self.colors['border_secondary']
imgui.push_style_color(imgui.Col_.button, button_color)
imgui.push_style_color(imgui.Col_.button_hovered, hovered_color)
imgui.push_style_color(imgui.Col_.button_active, hovered_color)
imgui.push_style_color(imgui.Col_.text, text_color)
imgui.push_style_color(imgui.Col_.border, border_color)
imgui.push_style_var(imgui.StyleVar_.frame_rounding, 8.0)
imgui.push_style_var(imgui.StyleVar_.frame_border_size, 1.0)
imgui.push_style_var(imgui.StyleVar_.frame_padding, (10.0, 6.0))
if not enabled:
imgui.begin_disabled()
clicked = imgui.button(label, size)
if not enabled:
imgui.end_disabled()
if tooltip and imgui.is_item_hovered():
imgui.set_tooltip(tooltip)
imgui.pop_style_var(3)
imgui.pop_style_color(5)
return clicked
def get_icon_text_button(self, icon_texture, text, size=(0, 0)):
"""绘制带图标的文本按钮"""
@ -521,4 +428,4 @@ class ImGuiStyleManager:
imgui.same_line()
# 再绘制文本按钮
return imgui.button(text, size)
return imgui.button(text, size)

View File

@ -1,225 +0,0 @@
"""
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}")

1094
core/maintenance_gui.py Normal file

File diff suppressed because it is too large Load Diff

495
core/patrol_system.py Normal file
View File

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

View File

@ -1,130 +0,0 @@
"""RenderPipeline tag state helpers for the editor runtime.
The stock native TagStateManager used by RenderPipeline creates pass-specific
tag states from an empty RenderState. That works for static effects, but it
drops material attributes when we edit materials live in the editor. For
transparent forward materials this means the forward pass can no longer see
the current MaterialAttrib.
This module installs a Python-side tag state manager that preserves the
NodePath state when building tag states, and re-registers the already-created
pipeline cameras.
"""
from panda3d.core import BitMask32, ColorWriteAttrib, RenderState, ShaderAttrib
class EditorTagStateManager:
"""Tag-state manager that preserves NodePath state when applying effects."""
class StateContainer:
def __init__(self, tag_name, mask, write_color):
self.cameras = []
self.tag_states = {}
self.tag_name = tag_name
self.mask = BitMask32.bit(mask)
self.write_color = write_color
def __init__(self, main_cam_node):
self._main_cam_node = main_cam_node
self._main_cam_node.node().set_camera_mask(BitMask32.bit(1))
self.containers = {
"shadow": self.StateContainer("Shadows", 2, False),
"voxelize": self.StateContainer("Voxelize", 3, False),
"envmap": self.StateContainer("Envmap", 4, True),
"forward": self.StateContainer("Forward", 5, True),
}
def get_mask(self, container_name):
if container_name == "gbuffer":
return BitMask32.bit(1)
return self.containers[container_name].mask
def apply_state(self, container_name, np, shader, name, sort):
container = self.containers[container_name]
state = np.get_state()
if not container.write_color:
state = state.set_attrib(
ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000
)
state = state.set_attrib(ShaderAttrib.make(shader, sort), sort)
container.tag_states[name] = state
np.set_tag(container.tag_name, name)
for camera in container.cameras:
camera.set_tag_state(name, state)
def cleanup_states(self):
self._main_cam_node.node().clear_tag_states()
for container in self.containers.values():
for camera in container.cameras:
camera.clear_tag_states()
container.tag_states = {}
def register_camera(self, container_name, source):
container = self.containers[container_name]
source.set_tag_state_key(container.tag_name)
source.set_camera_mask(container.mask)
state = RenderState.make_empty()
if not container.write_color:
state = state.set_attrib(
ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000
)
source.set_initial_state(state)
container.cameras.append(source)
def unregister_camera(self, container_name, source):
container = self.containers[container_name]
if source not in container.cameras:
return
container.cameras.remove(source)
source.clear_tag_states()
source.set_initial_state(RenderState.make_empty())
def install_editor_tag_state_manager(render_pipeline, base):
"""Replace the native tag manager with an editor-safe Python variant."""
if not render_pipeline or not base:
return None
if isinstance(render_pipeline.tag_mgr, EditorTagStateManager):
return render_pipeline.tag_mgr
try:
render_pipeline.tag_mgr.cleanup_states()
except Exception:
pass
tag_mgr = EditorTagStateManager(base.cam)
for stage in render_pipeline.stage_mgr.stages:
stage_name = type(stage).__name__
if hasattr(stage, "forward_cam"):
tag_mgr.register_camera("forward", stage.forward_cam)
if stage_name == "EnvironmentCaptureStage" and hasattr(stage, "cameras"):
for camera_np in stage.cameras:
tag_mgr.register_camera("envmap", camera_np.node())
if hasattr(stage, "voxel_cam"):
tag_mgr.register_camera("voxelize", stage.voxel_cam)
if stage_name in (
"PSSMDistShadowStage",
"PSSMSceneShadowStage",
"SkyAOCaptureStage",
) and hasattr(stage, "camera"):
tag_mgr.register_camera("shadow", stage.camera)
pssm_plugin = render_pipeline.plugin_mgr.instances.get("pssm")
if pssm_plugin and hasattr(pssm_plugin, "camera_rig"):
split_count = pssm_plugin.get_setting("split_count")
for index in range(split_count):
camera_np = pssm_plugin.camera_rig.get_camera(index)
tag_mgr.register_camera("shadow", camera_np.node())
render_pipeline.tag_mgr = tag_mgr
return tag_mgr

View File

@ -14,23 +14,15 @@ import time
class ResourceManager:
"""ImGui资源管理器类"""
DEFAULT_ASSET_SUBDIRS = (
"Models",
"Textures",
"Audio",
"Video",
"UI",
"Scripts",
)
def __init__(self, world):
self.world = world
self.project_root = Path(__file__).resolve().parent.parent
self.project_path: Optional[Path] = None
# 当前浏览路径,默认从统一的 Assets 结构开始
self.current_path = self._get_default_root_path()
# 当前浏览路径默认从Resources目录开始
self.current_path = self.project_root / "Resources"
if not self.current_path.exists():
self.current_path = self.project_root
# 历史记录,用于前进后退导航
self.navigation_history: List[Path] = [self.current_path]
@ -63,45 +55,6 @@ class ResourceManager:
# 文件图标映射Unicode Emoji
self._init_icon_map()
def _get_default_root_path(self) -> Path:
assets_root = None
project_manager = getattr(self.world, "project_manager", None)
current_project_path = getattr(project_manager, "current_project_path", None)
if current_project_path:
assets_root = Path(current_project_path) / "Assets"
elif self.project_path:
assets_root = self.project_path / "Assets"
else:
assets_root = self.project_root / "Assets"
if assets_root:
self._ensure_assets_structure(assets_root)
if assets_root.exists():
return assets_root.resolve()
legacy_resources = self.project_root / "Resources"
if legacy_resources.exists():
return legacy_resources.resolve()
return self.project_root.resolve()
def _ensure_assets_structure(self, assets_root: Path):
"""确保资源浏览器默认使用统一的 Assets 目录结构。"""
try:
assets_root.mkdir(parents=True, exist_ok=True)
for subdir in self.DEFAULT_ASSET_SUBDIRS:
(assets_root / subdir).mkdir(parents=True, exist_ok=True)
except OSError:
# 目录创建失败时,保留后续兼容回退逻辑。
pass
def set_project_path(self, project_path: str):
self.project_path = Path(project_path).resolve() if project_path else None
self.current_path = self._get_default_root_path()
self.navigation_history = [self.current_path]
self.history_index = 0
self.selected_files.clear()
self.focused_file = None
def _init_icon_map(self):
"""初始化文件图标映射使用PNG图标文件"""
@ -436,12 +389,6 @@ class ResourceManager:
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
if src.suffix.lower() == '.fbx':
fbm_src = src.with_name(src.stem + '.fbm')
if fbm_src.exists() and fbm_src.is_dir():
fbm_dst = destination_root / fbm_src.name
if not fbm_dst.exists():
shutil.copytree(fbm_src, fbm_dst)
imported.append(dst)
except Exception as e:
errors.append(f"导入失败 {src}: {e}")

View File

@ -72,7 +72,7 @@ class ScriptBase(ABC):
class ScriptComponent:
"""脚本组件 - 挂载到游戏对象上的脚本实例"""
def __init__(self, script_instance: ScriptBase, game_object, script_manager, script_key: Optional[str] = None):
def __init__(self, script_instance: ScriptBase, game_object, script_manager):
self.script_instance = script_instance
self.game_object = game_object
self.script_manager = script_manager
@ -80,7 +80,6 @@ class ScriptComponent:
# 保存脚本名称便于UI显示
self.script_name = script_instance.__class__.__name__
self.script_key = script_key or self.script_name
# 设置脚本实例的引用
script_instance.gameObject = game_object
@ -227,7 +226,7 @@ class ScriptLoader:
break
if script_class is None:
#print(f"脚本文件中没有找到继承自ScriptBase的类: {script_path}")
print(f"脚本文件中没有找到继承自ScriptBase的类: {script_path}")
return None
# 保存模块和类信息
@ -235,7 +234,7 @@ class ScriptLoader:
self.script_classes[script_name] = script_class
self.file_mtimes[script_path] = os.path.getmtime(script_path)
#print(f"✓ 成功加载脚本: {script_name} 从 {script_path}")
print(f"✓ 成功加载脚本: {script_name}{script_path}")
return script_class
except Exception as e:
@ -249,7 +248,7 @@ class ScriptLoader:
# 移除所有使用此脚本的组件
components_to_remove = []
for component in self.script_manager.engine.script_components:
if self.script_manager._script_matches(component, script_name):
if component.script_instance.__class__.__name__ == script_name:
components_to_remove.append(component)
for component in components_to_remove:
@ -266,24 +265,6 @@ class ScriptLoader:
del self.script_classes[script_name]
print(f"✓ 脚本已卸载: {script_name}")
def clear(self, unload_components: bool = False):
"""清空当前加载的脚本缓存。"""
if unload_components:
for script_name in list(self.loaded_modules.keys()):
try:
self.unload_script(script_name)
except Exception as e:
print(f"卸载脚本失败 {script_name}: {e}")
else:
for module in list(self.loaded_modules.values()):
module_name = getattr(module, "__name__", "")
if module_name and module_name in sys.modules:
del sys.modules[module_name]
self.loaded_modules.clear()
self.script_classes.clear()
self.file_mtimes.clear()
def reload_script(self, script_path: str) -> Optional[type]:
"""重新加载脚本(热重载)"""
@ -331,7 +312,7 @@ class ScriptLoader:
scripts_dir = self.script_manager.scripts_directory
if os.path.exists(scripts_dir):
for file_name in os.listdir(scripts_dir):
if file_name.endswith(('.py', '.pyc')):
if file_name.endswith('.py'):
base_name = os.path.splitext(file_name)[0]
if base_name == script_name:
return os.path.join(scripts_dir, file_name)
@ -436,7 +417,7 @@ class ScriptManager:
self.script_templates: Dict[str, type] = {} # 脚本名 -> 脚本类
# 脚本目录
self.scripts_directory = self._normalize_scripts_directory("scripts")
self.scripts_directory = "scripts"
self._ensure_scripts_directory()
# 热重载监控
@ -444,159 +425,6 @@ class ScriptManager:
self.hot_reload_task = None
print("✓ 脚本管理系统初始化完成")
def _normalize_scripts_directory(self, directory: str) -> str:
directory = directory or "scripts"
return os.path.normpath(os.path.abspath(directory))
def get_project_path(self) -> Optional[str]:
project_manager = getattr(self.world, "project_manager", None)
project_path = getattr(project_manager, "current_project_path", None)
if project_path:
return os.path.normpath(project_path)
project_path = getattr(self.world, "project_path", None)
if project_path:
return os.path.normpath(project_path)
return None
def get_project_scripts_directory(self, project_path: Optional[str] = None) -> Optional[str]:
project_path = project_path or self.get_project_path()
if not project_path:
return None
return os.path.normpath(os.path.join(project_path, "Assets", "Scripts"))
def get_script_relative_path(self, script_path: str) -> str:
if not script_path:
return ""
script_path = os.path.normpath(os.path.abspath(script_path))
project_path = self.get_project_path()
if not project_path:
return ""
try:
relative_path = os.path.relpath(script_path, project_path)
except ValueError:
return ""
if relative_path.startswith(".."):
return ""
return relative_path.replace("\\", "/")
def resolve_script_path(self, script_info: Dict[str, Any]) -> str:
if not isinstance(script_info, dict):
return ""
project_path = self.get_project_path()
candidates = []
script_guid = str(script_info.get("script_guid", "") or "").strip()
if script_guid:
try:
project_manager = getattr(self.world, "project_manager", None)
asset_database = project_manager.get_asset_database() if project_manager and hasattr(project_manager, "get_asset_database") else None
asset_record = asset_database.get_asset(script_guid) if asset_database else {}
asset_path = str(asset_record.get("asset_path", "") or "")
if asset_path and project_path:
asset_abs_path = os.path.normpath(os.path.join(project_path, asset_path.replace("/", os.sep)))
candidates.append(asset_abs_path)
except Exception:
pass
for key in ("project_relative_path", "relative_path", "path", "file"):
raw_value = str(script_info.get(key, "") or "").strip()
if not raw_value:
continue
normalized_value = raw_value.replace("/", os.sep)
if os.path.isabs(normalized_value):
candidates.append(os.path.normpath(normalized_value))
continue
if project_path:
candidates.append(os.path.normpath(os.path.join(project_path, normalized_value)))
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, normalized_value)))
lower_value = normalized_value.lower()
if not lower_value.endswith((".py", ".pyc")):
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{normalized_value}.py")))
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{normalized_value}.pyc")))
elif lower_value.endswith(".py"):
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{os.path.splitext(normalized_value)[0]}.pyc")))
elif lower_value.endswith(".pyc"):
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{os.path.splitext(normalized_value)[0]}.py")))
script_name = str(script_info.get("name", "") or "").strip()
if script_name:
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{script_name}.py")))
candidates.append(os.path.normpath(os.path.join(self.scripts_directory, f"{script_name}.pyc")))
seen = set()
for candidate in candidates:
if not candidate or candidate in seen:
continue
seen.add(candidate)
if os.path.exists(candidate):
return candidate
if script_name:
return self.loader.find_script_file(script_name) or ""
return ""
def build_script_reference(self, script_name: str, script_file: str = "") -> Dict[str, Any]:
reference = {"name": script_name}
resolved_script_path = script_file or self.loader.find_script_file(script_name) or ""
if resolved_script_path:
resolved_script_path = os.path.normpath(os.path.abspath(resolved_script_path))
relative_path = self.get_script_relative_path(resolved_script_path)
if relative_path:
reference["project_relative_path"] = relative_path
reference["file"] = relative_path
else:
reference["file"] = resolved_script_path
try:
project_manager = getattr(self.world, "project_manager", None)
if project_manager and hasattr(project_manager, "register_project_asset"):
asset_record = project_manager.register_project_asset(resolved_script_path)
if asset_record and asset_record.get("guid"):
reference["script_guid"] = asset_record["guid"]
except Exception:
pass
return reference
def set_scripts_directory(
self,
directory: str,
*,
create: bool = True,
reload_scripts: bool = True,
) -> str:
normalized_directory = self._normalize_scripts_directory(directory)
if normalized_directory == self.scripts_directory and (
os.path.exists(normalized_directory) or not create
):
return self.scripts_directory
self.scripts_directory = normalized_directory
if create:
self._ensure_scripts_directory()
self.loader.clear(unload_components=False)
self.script_templates.clear()
if reload_scripts and os.path.exists(self.scripts_directory):
self.load_all_scripts_from_directory()
print(f"✓ 当前脚本目录已切换到: {self.scripts_directory}")
return self.scripts_directory
def _ensure_scripts_directory(self):
"""确保脚本目录存在"""
@ -682,19 +510,6 @@ class ExampleScript(ScriptBase):
self.engine.stop_engine()
self.stop_hot_reload()
print("✓ 脚本系统已停止")
def reset_scene_state(self):
"""Clear all mounted script components before loading/replacing a scene."""
try:
for component in list(self.engine.script_components):
try:
self.engine.remove_script_component(component)
except Exception as e:
print(f"移除脚本组件失败: {e}")
self.object_scripts.clear()
print("✓ 脚本场景状态已清空")
except Exception as e:
print(f"清空脚本场景状态失败: {e}")
def start_hot_reload(self):
"""启动热重载监控"""
@ -719,11 +534,7 @@ class ExampleScript(ScriptBase):
def create_script_file(self, script_name: str, template: str = "basic") -> str:
"""创建新的脚本文件"""
script_base_name = os.path.splitext(script_name.strip())[0]
script_path = os.path.join(self.scripts_directory, f"{script_base_name}.py")
if not os.path.exists(self.scripts_directory):
os.makedirs(self.scripts_directory)
script_path = os.path.join(self.scripts_directory, f"{script_name}.py")
if os.path.exists(script_path):
print(f"脚本文件已存在: {script_path}")
@ -731,33 +542,21 @@ class ExampleScript(ScriptBase):
# 根据模板创建脚本
if template == "basic":
script_content = self._get_basic_script_template(script_base_name)
script_content = self._get_basic_script_template(script_name)
elif template == "movement":
script_content = self._get_movement_script_template(script_base_name)
script_content = self._get_movement_script_template(script_name)
else:
script_content = self._get_basic_script_template(script_base_name)
script_content = self._get_basic_script_template(script_name)
with open(script_path, 'w', encoding='utf-8') as f:
f.write(script_content)
print(f"✓ 创建脚本文件: {script_path}")
return script_path
def _build_script_class_name(self, script_name: str) -> str:
normalized_parts = []
for raw_part in script_name.replace('-', '_').split('_'):
part = ''.join(ch for ch in raw_part if ch.isalnum())
if part:
normalized_parts.append(part.capitalize())
class_name = ''.join(normalized_parts) or "GeneratedScript"
if class_name[0].isdigit():
class_name = f"Script{class_name}"
return class_name
def _get_basic_script_template(self, script_name: str) -> str:
"""获取基础脚本模板"""
class_name = self._build_script_class_name(script_name)
class_name = ''.join(word.capitalize() for word in script_name.split('_'))
return f'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@ -791,7 +590,7 @@ class {class_name}(ScriptBase):
def _get_movement_script_template(self, script_name: str) -> str:
"""获取移动脚本模板"""
class_name = self._build_script_class_name(script_name)
class_name = ''.join(word.capitalize() for word in script_name.split('_'))
return f'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@ -844,32 +643,19 @@ class {class_name}(ScriptBase):
"""从目录加载所有脚本"""
if directory is None:
directory = self.scripts_directory
else:
directory = self._normalize_scripts_directory(directory)
if not os.path.exists(directory):
print(f"脚本目录不存在: {directory}")
return []
loaded_scripts = []
seen_script_names = set()
for filename in sorted(os.listdir(directory)):
if filename.startswith('__') or not filename.endswith(('.py', '.pyc')):
continue
script_name = os.path.splitext(filename)[0]
if script_name in seen_script_names:
continue
preferred_path = os.path.join(directory, f"{script_name}.py")
script_path = preferred_path if os.path.exists(preferred_path) else os.path.join(directory, filename)
if not os.path.exists(script_path):
continue
script_class = self.load_script_from_file(script_path)
if script_class:
loaded_scripts.append(script_name)
seen_script_names.add(script_name)
for filename in os.listdir(directory):
if filename.endswith('.py') and not filename.startswith('__'):
script_path = os.path.join(directory, filename)
script_class = self.load_script_from_file(script_path)
if script_class:
script_name = os.path.splitext(filename)[0]
loaded_scripts.append(script_name)
print(f"✓ 从目录 {directory} 加载了 {len(loaded_scripts)} 个脚本")
return loaded_scripts
@ -889,7 +675,7 @@ class {class_name}(ScriptBase):
script_instance = script_class()
# 创建脚本组件
script_component = ScriptComponent(script_instance, game_object, self, script_key=script_name)
script_component = ScriptComponent(script_instance, game_object, self)
# 添加到对象的脚本列表
if game_object not in self.object_scripts:
@ -916,7 +702,7 @@ class {class_name}(ScriptBase):
removed = False
for component in script_components[:]: # 复制列表以避免修改时出错
if self._script_matches(component, script_name):
if component.script_instance.__class__.__name__ == script_name:
# 从引擎移除
self.engine.remove_script_component(component)
# 从对象脚本列表移除
@ -935,13 +721,6 @@ class {class_name}(ScriptBase):
return removed
def _script_matches(self, component: ScriptComponent, script_identifier: str) -> bool:
return script_identifier in {
getattr(component, "script_key", None),
getattr(component, "script_name", None),
component.script_instance.__class__.__name__,
}
def _update_node_script_tags_after_removal(self, game_object, removed_script_name):
"""在移除脚本后更新节点标签"""
try:
@ -960,8 +739,13 @@ class {class_name}(ScriptBase):
script_info_list = []
for script_component in remaining_scripts:
script_name = script_component.script_name
script_class = script_component.script_instance.__class__
script_file = self.loader.find_script_file(script_name) or ""
script_info_list.append(self.build_script_reference(script_name, script_file))
script_info_list.append({
"name": script_name,
"file": script_file
})
import json
game_object.setTag("has_scripts", "true")
@ -979,7 +763,7 @@ class {class_name}(ScriptBase):
"""获取对象上的特定脚本"""
scripts = self.get_scripts_on_object(game_object)
for script in scripts:
if self._script_matches(script, script_name):
if script.script_instance.__class__.__name__ == script_name:
return script
return None
@ -999,7 +783,7 @@ class {class_name}(ScriptBase):
"name": script_name,
"class": script_class,
"doc": script_class.__doc__,
"file": self.loader.find_script_file(script_name) or inspect.getsourcefile(script_class),
"file": inspect.getfile(script_class) if hasattr(script_class, '__file__') else None,
"methods": [method for method in dir(script_class) if not method.startswith('_')]
}
@ -1009,18 +793,6 @@ class {class_name}(ScriptBase):
if script_info and script_info["file"]:
return self.loader.reload_script(script_info["file"]) is not None
return False
def set_hot_reload_enabled(self, enabled: bool):
"""切换热重载并同步后台监控任务。"""
enabled = bool(enabled)
if self.hot_reload_enabled == enabled:
return
self.hot_reload_enabled = enabled
if enabled:
self.start_hot_reload()
else:
self.stop_hot_reload()
# ==================== 调试功能 ====================
@ -1086,4 +858,4 @@ def get_script_api():
__all__ = [
'ScriptBase', 'ScriptComponent', 'ScriptEngine',
'ScriptLoader', 'ScriptAPI', 'ScriptManager'
]
]

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -3,25 +3,29 @@ import time
import urllib
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
from scene import util
from core.editor_context import get_editor_context
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
from scene import util
class TerrainManager:
"""地形管理类"""
def __init__(self, world):
self.world = world
self._editor_context = get_editor_context(world)
self.terrains = []
def __init__(self, world):
self.world = world
self.terrains = []
# core/terrain_manager.py
def _get_tree_widget(self):
"""安全获取树形控件"""
return self._editor_context.get_tree_widget()
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 createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)):
"""从高度图创建地形"""
@ -129,22 +133,30 @@ class TerrainManager:
terrain_node.setPythonTag("selectable", True)
# 保存地形信息(包括高度图的副本)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
'scale': scale,
'name': node_name
}
self.terrains.append(terrain_info)
terrain_node.setPythonTag("terrain_info", terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建高度图地形: {terrain_name}")
}
created_terrains.append((terrain_node, None))
self.terrains.append(terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建高度图地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = None
if tree_widget and parent_item:
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加跳过或失败但Panda3D对象已创建")
# 添加到场景管理器ImGui环境使用
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
@ -167,8 +179,13 @@ class TerrainManager:
print("❌ 没有成功创建任何高度图地形")
return None
if created_terrains:
pass
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形")
@ -265,22 +282,30 @@ class TerrainManager:
terrain_node.setPythonTag("selectable", True)
# 保存地形信息(包括高度图)
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': None,
'heightfield': height_image, # 保存高度图
terrain_info = {
'terrain': terrain,
'node': terrain_node,
'heightmap': None,
'heightfield': height_image, # 保存高度图
'scale': (size[0], size[1], 50),
'name': node_name
}
self.terrains.append(terrain_info)
terrain_node.setPythonTag("terrain_info", terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建平面地形: {terrain_name}")
}
created_terrains.append((terrain_node, None))
self.terrains.append(terrain_info)
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建平面地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = None
if tree_widget and parent_item:
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加跳过或失败但Panda3D对象已创建")
# 添加到场景管理器ImGui环境使用
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
@ -303,8 +328,13 @@ class TerrainManager:
print("❌ 没有成功创建任何平面地形")
return None
if created_terrains:
pass
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形")

View File

@ -9,12 +9,11 @@ class ToolManager:
def setCurrentTool(self, tool):
"""设置当前工具"""
self.currentTool = tool
print(f"\n=== 工具切换 ===")
print(f"当前工具: {tool}")
selected_node = self.world._get_selection_node() if hasattr(self.world, "_get_selection_node") else self.world.selection.selectedNode
print(f"选中节点: {selected_node.getName() if selected_node else ''}")
self.currentTool = tool
print(f"\n=== 工具切换 ===")
print(f"当前工具: {tool}")
print(f"选中节点: {self.world.selection.selectedNode.getName() if self.world.selection.selectedNode else ''}")
# 根据工具类型启用对应的方法
if tool == "选择":

View File

@ -91,10 +91,10 @@ class VRTestMode:
self.vr_manager._disable_main_cam()
# 设置高帧率用于测试
host_widget = getattr(self.vr_manager.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 144))
print("✓ 测试模式:渲染同步器设置为144Hz")
if hasattr(self.vr_manager.world, 'qtWidget') and self.vr_manager.world.qtWidget:
if hasattr(self.vr_manager.world.qtWidget, 'synchronizer'):
self.vr_manager.world.qtWidget.synchronizer.setInterval(int(1000/144))
print("✓ 测试模式:Qt Timer设置为144Hz")
# 初始化测试显示系统
if not self._initialize_test_display():

View File

@ -1893,11 +1893,12 @@ class VRManager(DirectObject):
print(" 注意Submit后立即调用WaitGetPoses是错误实现")
self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值平衡准确性和延迟
# 🚀 动态调整宿主同步器频率以支持VR
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 144))
print("✓ 渲染同步器调整为144Hz让OpenVR控制VR渲染节奏")
# 🚀 动态调整Qt Timer频率以支持VR
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
if hasattr(self.world.qtWidget, 'synchronizer'):
# 设置为144Hz让OpenVR控制实际渲染节奏
self.world.qtWidget.synchronizer.setInterval(int(1000/144))
print("✓ Qt Timer调整为144Hz让OpenVR控制VR渲染节奏")
# 🔧 关键修复检测并重建缺失的手柄visualizer
# 当渲染模式切换时visualizer可能被清理但控制器对象仍存在
@ -1951,11 +1952,11 @@ class VRManager(DirectObject):
# 恢复主相机
self._enable_main_cam()
# 恢复渲染同步器到60FPS
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 60))
print("渲染同步器恢复为60Hz")
# 恢复Qt Timer到60FPS
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
if hasattr(self.world.qtWidget, 'synchronizer'):
self.world.qtWidget.synchronizer.setInterval(int(1000/60))
print("Qt Timer恢复为60Hz")
print("✅ VR模式已禁用手柄模型已隐藏")

View File

@ -9,8 +9,7 @@ from direct.actor.Actor import Actor
warnings.filterwarnings("ignore", category=DeprecationWarning)
from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight,
Point3, WindowProperties, Material, LColor, Shader,
TransparencyAttrib, loadPrcFileData, GraphicsPipeSelection)
Point3, WindowProperties, Material, LColor, loadPrcFileData)
from direct.showbase.ShowBase import ShowBase
from direct.showbase.ShowBaseGlobal import globalClock
from scene.scene_manager import SceneManager
@ -25,7 +24,6 @@ from ssbo_component.ssbo_editor import SSBOEditor
# 从渲染管线工具模块导入全局函数
from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline
from core.render_pipeline_tag_state import install_editor_tag_state_manager
# 尝试导入插件管理器(如果存在)
try:
@ -42,16 +40,11 @@ class CoreWorld(ShowBase):
global _global_render_pipeline
# 初始化基础属性
self.host_widget = None # 外部宿主窗口引用(用于获取准确渲染区域尺寸)
desktop_size = self._get_desktop_window_size()
if not is_fullscreen and (width, height) == (1380, 750) and desktop_size:
width, height = desktop_size
print(f"✓ 使用桌面分辨率启动窗口: {width} x {height}")
self.qtWidget = None # Qt部件引用用于获取准确的渲染区域尺寸
# 设置基本配置
loadPrcFileData("", "show-frame-rate-meter 0")
loadPrcFileData("", "window-type onscreen")
loadPrcFileData("", "window-title MetaCore")
loadPrcFileData("", f"win-size {width} {height}")
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
@ -82,17 +75,8 @@ class CoreWorld(ShowBase):
# 初始化 ShowBase
ShowBase.__init__(self)
try:
props = WindowProperties()
props.setTitle("MetaCore")
self.win.requestProperties(props)
except Exception:
pass
# 创建渲染管线
self.render_pipeline.create(self)
install_editor_tag_state_manager(self.render_pipeline, self)
self._setupForwardTransparencyOverlay()
set_render_pipeline(self.render_pipeline)
# 设置相机
@ -128,50 +112,6 @@ class CoreWorld(ShowBase):
self._setupGround()
self._loadFont()
@staticmethod
def _get_desktop_window_size():
"""Query the current desktop display mode for a better default window size."""
if os.name == "nt":
try:
import ctypes
from ctypes import wintypes
class RECT(ctypes.Structure):
_fields_ = [
("left", wintypes.LONG),
("top", wintypes.LONG),
("right", wintypes.LONG),
("bottom", wintypes.LONG),
]
work_area = RECT()
spi_get_work_area = 0x0030
if ctypes.windll.user32.SystemParametersInfoW(
spi_get_work_area, 0, ctypes.byref(work_area), 0
):
width = int(work_area.right - work_area.left)
height = int(work_area.bottom - work_area.top)
if width > 0 and height > 0:
return width, height
except Exception as e:
print(f"⚠ 获取 Windows 工作区尺寸失败,回退到显示模式尺寸: {e}")
try:
pipe = GraphicsPipeSelection.getGlobalPtr().makeDefaultPipe()
if not pipe:
return None
info = pipe.getDisplayInformation()
current_mode = info.getCurrentDisplayModeIndex()
if current_mode < 0:
return None
width = int(info.getDisplayModeWidth(current_mode))
height = int(info.getDisplayModeHeight(current_mode))
if width > 0 and height > 0:
return width, height
except Exception as e:
print(f"⚠ 获取桌面分辨率失败,继续使用默认窗口尺寸: {e}")
return None
def _handle_transform_error(self):
"""处理TransformState相关的错误"""
try:
@ -182,87 +122,6 @@ class CoreWorld(ShowBase):
except Exception as e:
print(f"清理缓存时出错: {e}")
def _setupForwardTransparencyOverlay(self):
"""Composite the forward transparency result back onto the final frame.
The bundled RP forward merge stage is unreliable in this editor build
during live material edits, but the forward color buffer itself is
correct. We therefore blend that buffer as a fullscreen overlay and use
the scene/forward depth textures to keep transparent objects behind
opaque geometry clipped.
"""
self.forward_transparency_overlay = None
try:
forward_stage = next(
(
stage for stage in self.render_pipeline.stage_mgr.stages
if type(stage).__name__ == "ForwardStage"
),
None,
)
gbuffer_stage = next(
(
stage for stage in self.render_pipeline.stage_mgr.stages
if type(stage).__name__ == "GBufferStage"
),
None,
)
if not forward_stage or not gbuffer_stage:
return
vert = """
#version 330
uniform mat4 p3d_ModelViewProjectionMatrix;
in vec4 p3d_Vertex;
in vec2 p3d_MultiTexCoord0;
out vec2 texcoord;
void main() {
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
texcoord = p3d_MultiTexCoord0;
}
"""
frag = """
#version 330
uniform sampler2D ForwardColor;
uniform sampler2D ForwardDepth;
uniform sampler2D SceneDepth;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 forward_color = texture(ForwardColor, texcoord);
float alpha = clamp(forward_color.a, 0.0, 1.0);
if (alpha <= 1e-4) {
discard;
}
float forward_depth = texture(ForwardDepth, texcoord).x;
float scene_depth = texture(SceneDepth, texcoord).x;
if (scene_depth > 1e-6 && forward_depth > scene_depth + 1e-5) {
discard;
}
o_color = vec4(forward_color.rgb, alpha);
}
"""
cm = CardMaker("ForwardTransparencyOverlay")
cm.setFrameFullscreenQuad()
overlay = self.render2d.attachNewNode(cm.generate())
overlay.setShader(Shader.make(Shader.SL_GLSL, vert, frag))
overlay.setShaderInput("ForwardColor", forward_stage.target.color_tex)
overlay.setShaderInput("ForwardDepth", forward_stage.target.depth_tex)
overlay.setShaderInput("SceneDepth", gbuffer_stage.target.depth_tex)
overlay.setTransparency(TransparencyAttrib.M_alpha)
overlay.setDepthTest(False)
overlay.setDepthWrite(False)
overlay.setBin("fixed", 1)
self.forward_transparency_overlay = overlay
print("✓ 前向透明叠加层初始化完成")
except Exception as e:
print(f"⚠ 初始化前向透明叠加层失败: {e}")
def _setupResourcePaths(self):
"""设置Panda3D资源搜索路径确保能正确找到Resources文件夹中的模型和贴图"""
try:
@ -325,7 +184,6 @@ void main() {
print(f"✓ 创建并添加子目录: {subdir}")
# 设置纹理搜索路径
texture_path = None
try:
from panda3d.core import getTexturePath
texture_path = getTexturePath()
@ -335,13 +193,12 @@ void main() {
# 新版本 Panda3D 中 getTexturePath 可能不可用
print(" 注意: getTexturePath 不可用,使用默认纹理路径")
if texture_path is not None:
for subdir in ['textures', 'materials', 'icons']:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not texture_path.findFile(subdir_filename):
texture_path.appendDirectory(subdir_filename)
for subdir in ['textures', 'materials', 'icons']:
subdir_path = os.path.join(resources_dir, subdir)
if os.path.exists(subdir_path):
subdir_filename = Filename.from_os_specific(subdir_path)
if not texture_path.findFile(subdir_filename):
texture_path.appendDirectory(subdir_filename)
print(f"✓ 资源路径设置完成")
print(f" 项目根目录: {project_root}")
@ -829,17 +686,16 @@ void main() {
print(f"警告: 加载中文字体时发生错误: {e}")
self.chinese_font = None
def setHostWidget(self, widget):
"""设置外部宿主窗口引用。"""
self.host_widget = widget
print(f"✓ 设置宿主窗口引用: {widget}")
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
print(f"✓ 设置Qt部件引用: {widget}")
def getWindowSize(self):
"""获取准确的窗口尺寸"""
widget = self.host_widget
if widget and hasattr(widget, "getActualSize"):
# 优先使用宿主窗口的实际尺寸
width, height = widget.getActualSize()
if self.qtWidget:
# 优先使用Qt部件的实际尺寸
width, height = self.qtWidget.getActualSize()
if width > 0 and height > 0:
return width, height
@ -898,28 +754,30 @@ void main() {
self.lastMouseX = evt['x']
self.lastMouseY = evt['y']
#
# # 通过宿主窗口隐藏光标并捕获鼠标
# # 通过 Qt 窗口隐藏光标并捕获鼠标
# try:
# if hasattr(self, 'host_widget') and self.host_widget:
# self.host_widget.setCursorHidden(True)
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.setCursor(Qt.BlankCursor)
# # 捕获鼠标,使其无法离开窗口
# self.host_widget.grabMouse()
# self.qtWidget.grabMouse()
# except Exception as e:
# print(f"隐藏光标时出错: {e}")
# print(f"通过 Qt 隐藏光标时出错: {e}")
def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件"""
#print("右键释放")
self.mouseRightPressed = False
# # 恢复宿主窗口光标并释放鼠标捕获
# # 恢复 Qt 窗口光标并释放鼠标捕获
# try:
# if hasattr(self, 'host_widget') and self.host_widget:
# self.host_widget.setCursorHidden(False) # 恢复默认光标
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.unsetCursor() # 恢复默认光标
# # 释放鼠标捕获
# self.host_widget.releaseMouse()
# self.qtWidget.releaseMouse()
# except Exception as e:
# print(f"恢复光标时出错: {e}")
# print(f"恢复 Qt 光标时出错: {e}")
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 只处理相机旋转"""
@ -1108,7 +966,7 @@ void main() {
return None
# 创建材质编辑器组件
# 使用纯 Panda3D 实现,不依赖外部 GUI 框架
# 使用纯 Panda3D 实现,不依赖 PyQt5
print("材质编辑器组件已创建(使用 Panda3D 原生实现)")
material_widget.setLayout(layout)

4027
gui/gui_manager.py Normal file

File diff suppressed because it is too large Load Diff

104
imgui.ini
View File

@ -24,33 +24,34 @@ Size=832,45
Collapsed=0
[Window][工具栏]
Pos=255,20
Size=1398,383
Pos=241,20
Size=908,74
Collapsed=0
DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
Size=308,588
Size=239,468
Collapsed=0
DockId=0x00000007,0
[Window][属性面板]
Pos=1151,20
Size=229,730
Collapsed=0
DockId=0x00000003,0
[Window][属性面板]
Pos=1574,20
Size=346,989
Collapsed=0
DockId=0x00000002,0
[Window][控制台]
Pos=0,610
Size=308,399
Pos=0,490
Size=239,260
Collapsed=0
DockId=0x00000004,0
DockId=0x00000008,0
[Window][脚本管理]
Pos=1950,20
Size=610,995
Pos=1540,20
Size=380,390
Collapsed=0
DockId=0x00000003,1
[Window][中文显示测试]
Pos=60,60
@ -59,7 +60,7 @@ Collapsed=0
[Window][WindowOverViewport_11111111]
Pos=0,20
Size=1920,989
Size=1380,730
Collapsed=0
[Window][测试窗口1]
@ -88,18 +89,18 @@ Size=600,500
Collapsed=0
[Window][打开项目]
Pos=702,300
Pos=710,304
Size=500,400
Collapsed=0
[Window][导入模型]
Pos=660,245
Pos=660,254
Size=600,500
Collapsed=0
[Window][资源管理器]
Pos=310,610
Size=1262,399
Pos=241,464
Size=908,286
Collapsed=0
DockId=0x00000006,0
@ -119,8 +120,8 @@ Size=88,226
Collapsed=0
[Window][创建点光源]
Pos=750,324
Size=420,360
Pos=60,60
Size=109,274
Collapsed=0
[Window][创建GUI标签]
@ -129,12 +130,12 @@ Size=93,226
Collapsed=0
[Window][创建聚光灯]
Pos=750,344
Size=420,320
Pos=60,60
Size=89,250
Collapsed=0
[Window][颜色选择器]
Pos=878,354
Pos=810,304
Size=300,400
Collapsed=0
@ -149,10 +150,10 @@ Size=101,226
Collapsed=0
[Window][LUI编辑器]
Pos=2214,20
Size=346,1331
Pos=1628,412
Size=292,597
Collapsed=0
DockId=0x00000002,1
DockId=0x00000004,0
[Window][LUI测试控制面板]
Pos=6,10
@ -200,41 +201,22 @@ Size=600,400
Collapsed=0
[Window][Web面板]
Pos=1438,20
Size=610,748
Collapsed=0
DockId=0x00000005,2
[Window][项目另存为]
Pos=730,384
Size=460,240
Collapsed=0
[Window][打包项目]
Pos=754,392
Size=540,320
Collapsed=0
[Window][关于 EG]
Pos=794,422
Size=460,260
Collapsed=0
[Window][关于 MetaCore]
Pos=790,422
Size=460,260
Pos=373,98
Size=942,580
Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1572,1012 Split=X
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=308,1084 Split=Y Selected=0xE0015051
DockNode ID=0x00000003 Parent=0x00000007 SizeRef=339,588 Selected=0xE0015051
DockNode ID=0x00000004 Parent=0x00000007 SizeRef=339,399 Selected=0x5428E753
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1262,1084 Split=Y
DockNode ID=0x00000005 Parent=0x00000008 SizeRef=2048,683 Split=Y
DockNode ID=0x0000000D Parent=0x00000005 SizeRef=1318,383 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x00000005 SizeRef=1318,363 CentralNode=1 Selected=0xE0015051
DockNode ID=0x00000006 Parent=0x00000008 SizeRef=2048,399 Selected=0x3A2E05C3
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=346,1012 Selected=0x5DB6FF37
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

View File

@ -1 +0,0 @@
index-b2d982.boo

1560
main.py

File diff suppressed because it is too large Load Diff

8
new/project.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "new",
"path": "D:\\PythonProject\\CH_EG\\EG\\new",
"created_at": "2025-09-09 16:47:27",
"last_modified": "2025-09-11 09:28:24",
"version": "1.0.0",
"engine_version": "1.0.0"
}

View File

@ -1,75 +0,0 @@
# EG `.deb` 打包
这套脚本的目标不是重新编译项目,而是把已经生成好的 PyInstaller 发行目录再封装成 Debian 安装包。
默认输入目录是:
```text
dist/EGEditor
```
默认输出目录是:
```text
dist-deb
```
## 最常用流程
1. 先生成 PyInstaller 发行目录:
```bash
python -m PyInstaller --clean packaging/eg_editor.spec
```
2. 再生成 `.deb`
```bash
bash packaging/debian/build_deb.sh 0.1.0
```
生成结果类似:
```text
dist-deb/eg-editor_0.1.0_amd64.deb
```
## 安装与卸载
安装:
```bash
sudo apt install ./dist-deb/eg-editor_0.1.0_amd64.deb
```
卸载:
```bash
sudo apt remove eg-editor
```
## 安装后路径
- 程序主体:`/opt/eg-editor`
- 命令行启动:`/usr/bin/eg-editor`
- 桌面启动器:`/usr/share/applications/eg-editor.desktop`
- 图标:`/usr/share/pixmaps/eg-editor.png`
## 可选环境变量
- `APP_DIR`:指定要封装的 PyInstaller 目录
- `PACKAGE_NAME`Debian 包名,默认 `eg-editor`
- `DISPLAY_NAME`:桌面显示名称
- `ARCH`:包架构,默认自动读取 `dpkg-architecture`
- `MAINTAINER`:维护者字段
- `OUTPUT_DIR``.deb` 输出目录
- `ICON_SOURCE`:图标文件路径
示例:
```bash
APP_DIR=/path/to/dist/EGEditor \
DISPLAY_NAME="EG Editor" \
MAINTAINER="hello <hello@example.com>" \
bash packaging/debian/build_deb.sh 0.1.0
```

View File

@ -1,78 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
APP_DIR="${APP_DIR:-$ROOT_DIR/dist/EGEditor}"
PACKAGE_NAME="${PACKAGE_NAME:-eg-editor}"
DISPLAY_NAME="${DISPLAY_NAME:-EG Editor}"
VERSION="${1:-${VERSION:-0.1.0}}"
ARCH="${ARCH:-$(dpkg-architecture -qDEB_HOST_ARCH 2>/dev/null || echo amd64)}"
MAINTAINER="${MAINTAINER:-EG Team <noreply@example.com>}"
DESCRIPTION="${DESCRIPTION:-EG 3D editor and engine packaged from the PyInstaller distribution directory.}"
INSTALL_DIR="/opt/${PACKAGE_NAME}"
STAGE_BASE="$ROOT_DIR/build/deb/${PACKAGE_NAME}_${VERSION}_${ARCH}"
PKG_ROOT="$STAGE_BASE/${PACKAGE_NAME}"
OUTPUT_DIR="${OUTPUT_DIR:-$ROOT_DIR/dist-deb}"
OUTPUT_FILE="$OUTPUT_DIR/${PACKAGE_NAME}_${VERSION}_${ARCH}.deb"
ICON_SOURCE="${ICON_SOURCE:-$ROOT_DIR/icons/logo.png}"
if [[ ! -x "$APP_DIR/EGEditor" ]]; then
echo "error: executable not found: $APP_DIR/EGEditor" >&2
echo "hint: build the PyInstaller bundle first, or set APP_DIR=/path/to/EGEditor" >&2
exit 1
fi
if [[ ! -f "$ICON_SOURCE" ]]; then
echo "error: icon not found: $ICON_SOURCE" >&2
exit 1
fi
rm -rf "$STAGE_BASE"
mkdir -p \
"$PKG_ROOT/DEBIAN" \
"$PKG_ROOT$INSTALL_DIR" \
"$PKG_ROOT/usr/bin" \
"$PKG_ROOT/usr/share/applications" \
"$PKG_ROOT/usr/share/pixmaps" \
"$OUTPUT_DIR"
cp -a "$APP_DIR/." "$PKG_ROOT$INSTALL_DIR/"
cp "$ICON_SOURCE" "$PKG_ROOT/usr/share/pixmaps/${PACKAGE_NAME}.png"
cat > "$PKG_ROOT/usr/bin/${PACKAGE_NAME}" <<EOF
#!/usr/bin/env bash
exec "${INSTALL_DIR}/EGEditor" "\$@"
EOF
chmod 0755 "$PKG_ROOT/usr/bin/${PACKAGE_NAME}"
cat > "$PKG_ROOT/usr/share/applications/${PACKAGE_NAME}.desktop" <<EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=${DISPLAY_NAME}
Comment=Launch ${DISPLAY_NAME}
Exec=${PACKAGE_NAME}
Icon=${PACKAGE_NAME}
Terminal=false
Categories=Graphics;Development;
StartupNotify=true
EOF
chmod 0644 "$PKG_ROOT/usr/share/applications/${PACKAGE_NAME}.desktop"
cat > "$PKG_ROOT/DEBIAN/control" <<EOF
Package: ${PACKAGE_NAME}
Version: ${VERSION}
Section: graphics
Priority: optional
Architecture: ${ARCH}
Maintainer: ${MAINTAINER}
Depends: libc6 (>= 2.35), libgl1
Description: ${DISPLAY_NAME}
${DESCRIPTION}
EOF
chmod 0644 "$PKG_ROOT/DEBIAN/control"
dpkg-deb --build --root-owner-group "$PKG_ROOT" "$OUTPUT_FILE"
echo "built: $OUTPUT_FILE"

View File

@ -1,180 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
import importlib.machinery
import importlib.util
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_submodules
def _has_module(module_name):
try:
return importlib.util.find_spec(module_name) is not None
except Exception:
return False
def _safe_collect_submodules(package_name):
try:
return collect_submodules(package_name)
except Exception:
return []
def _safe_collect_dynamic_libs(package_name):
try:
return collect_dynamic_libs(package_name)
except Exception:
return []
def _append_data_dir(datas, project_root, relative_path, bundle_target=None):
source = project_root / relative_path
if source.exists():
datas.append((str(source), bundle_target or relative_path.replace("\\", "/")))
def _append_package_data_dir(datas, package_name, relative_path, bundle_target):
spec = importlib.util.find_spec(package_name)
if not spec or not spec.origin:
return
package_root = Path(spec.origin).resolve().parent
source = package_root / relative_path
if source.exists():
datas.append((str(source), bundle_target))
SPEC_DIR = Path(SPEC).resolve().parent
PROJECT_ROOT = SPEC_DIR.parent
THIRD_PARTY_DIR = PROJECT_ROOT / "third_party"
RENDER_PIPELINE_DIR = PROJECT_ROOT / "RenderPipelineFile"
ENTRY_SCRIPT = PROJECT_ROOT / "Start_Run.py"
for candidate in (PROJECT_ROOT, THIRD_PARTY_DIR, RENDER_PIPELINE_DIR):
candidate_str = str(candidate)
if candidate.exists() and candidate_str not in sys.path:
sys.path.insert(0, candidate_str)
pathex = [
str(PROJECT_ROOT),
str(THIRD_PARTY_DIR),
str(RENDER_PIPELINE_DIR),
]
datas = []
for relative_path, bundle_target in (
("Resources", "Resources"),
("RenderPipelineFile", "RenderPipelineFile"),
("config", "config"),
("vr_actions", "vr_actions"),
("icons", "icons"),
("scripts", "scripts"),
("templates", "templates"),
("ui/Skins", "ui/Skins"),
("ssbo_component/shaders", "ssbo_component/shaders"),
("ssbo_component/effects", "ssbo_component/effects"),
):
_append_data_dir(datas, PROJECT_ROOT, relative_path, bundle_target)
imgui_ini = PROJECT_ROOT / "imgui.ini"
if imgui_ini.exists():
datas.append((str(imgui_ini), "."))
demo_dir = PROJECT_ROOT / "demo"
if demo_dir.exists():
datas.append((str(demo_dir), "demo"))
_append_package_data_dir(datas, "panda3d", "etc", "panda3d/etc")
binaries = []
for extension_suffix in importlib.machinery.EXTENSION_SUFFIXES:
local_lui_binary = PROJECT_ROOT / "ui" / f"lui{extension_suffix}"
if local_lui_binary.exists():
binaries.append((str(local_lui_binary), "ui"))
break
for package_name in ("panda3d", "imgui_bundle"):
binaries += _safe_collect_dynamic_libs(package_name)
if _has_module("openvr"):
binaries += _safe_collect_dynamic_libs("openvr")
hiddenimports = sorted(
set(
[
"pyassimp",
"p3dimgui",
"p3dimgui.backend",
"p3dimgui.shaders",
"rpcore",
"rpplugins",
"rplibs",
"RenderPipelineFile.rpcore",
"RenderPipelineFile.rpplugins",
"RenderPipelineFile.rplibs",
]
+ _safe_collect_submodules("core")
+ _safe_collect_submodules("scene")
+ _safe_collect_submodules("project")
+ _safe_collect_submodules("ui")
+ _safe_collect_submodules("ssbo_component")
+ _safe_collect_submodules("TransformGizmo")
+ _safe_collect_submodules("p3dimgui")
+ _safe_collect_submodules("rpcore")
+ _safe_collect_submodules("rpplugins")
+ _safe_collect_submodules("rplibs")
+ _safe_collect_submodules("RenderPipelineFile.rpcore")
+ _safe_collect_submodules("RenderPipelineFile.rpplugins")
+ _safe_collect_submodules("RenderPipelineFile.rplibs")
+ (["openvr"] if _has_module("openvr") else [])
+ (["pyassimp"] if _has_module("pyassimp") else [])
)
)
excludes = [
"PyQt5",
"PyQt6",
"PySide2",
"PySide6",
"tkinter",
]
a = Analysis(
[str(ENTRY_SCRIPT)],
pathex=pathex,
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=excludes,
win_no_prefer_redirects=False,
win_private_assemblies=False,
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="EGEditor",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
upx_exclude=[],
name="EGEditor",
)

View File

@ -1,362 +0,0 @@
"""Asset database and import cache helpers for MetaCore project v2."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
from datetime import datetime
from panda3d.core import Filename
from project.project_schema import (
ASSET_DB_SCHEMA_VERSION,
ProjectLayout,
detect_asset_type,
generate_guid,
get_asset_subdir_for_type,
normalize_path,
relative_project_path,
)
class AssetDatabase:
def __init__(self, project_root: str, world=None):
self.layout = ProjectLayout(project_root)
self.world = world
self.data = {
"schema_version": ASSET_DB_SCHEMA_VERSION,
"assets": {},
"path_to_guid": {},
}
self.ensure_database()
def _get_all_meta_files(self) -> list[str]:
meta_files = []
if not os.path.exists(self.layout.assets_root):
return meta_files
for root, _, files in os.walk(self.layout.assets_root):
for file_name in files:
if file_name.endswith(".meta"):
meta_files.append(os.path.join(root, file_name))
return meta_files
def _rebuild_path_index(self):
rebuilt = {}
for asset_guid, asset_record in (self.data.get("assets", {}) or {}).items():
asset_path = str((asset_record or {}).get("asset_path", "") or "").replace("\\", "/")
if asset_path:
rebuilt[asset_path] = asset_guid
self.data["path_to_guid"] = rebuilt
def _sync_assets_from_meta_scan(self):
assets = self.data.setdefault("assets", {})
changed = False
for meta_path in self._get_all_meta_files():
meta_payload = self._read_meta(meta_path)
asset_guid = str(meta_payload.get("guid", "") or "").strip()
if not asset_guid:
continue
asset_path = meta_path[:-5]
if not os.path.exists(asset_path):
continue
relative_asset_path = relative_project_path(self.layout.project_root, asset_path)
if not relative_asset_path:
continue
asset_type = str(meta_payload.get("asset_type") or detect_asset_type(asset_path))
source_hash = self._hash_file(asset_path)
record = dict(assets.get(asset_guid, {}) or {})
previous_asset_path = str(record.get("asset_path", "") or "")
record.update(
{
"guid": asset_guid,
"asset_path": relative_asset_path,
"asset_type": asset_type,
"meta_path": relative_project_path(self.layout.project_root, meta_path),
"source_hash": source_hash,
"importer": str(meta_payload.get("importer") or f"{asset_type}_importer"),
"import_settings": meta_payload.get("import_settings", {}) or {},
"dependency_guids": meta_payload.get("dependency_guids", []) or [],
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
)
assets[asset_guid] = record
if previous_asset_path != relative_asset_path:
changed = True
self._rebuild_path_index()
return changed
def ensure_database(self):
os.makedirs(self.layout.library_root, exist_ok=True)
if os.path.exists(self.layout.asset_db_path):
try:
with open(self.layout.asset_db_path, "r", encoding="utf-8") as f:
payload = json.load(f)
if isinstance(payload, dict):
self.data["schema_version"] = payload.get("schema_version", ASSET_DB_SCHEMA_VERSION)
self.data["assets"] = payload.get("assets", {}) or {}
self.data["path_to_guid"] = payload.get("path_to_guid", {}) or {}
except Exception as e:
print(f"⚠️ 读取 AssetDB 失败,已重建为空数据库: {e}")
self._sync_assets_from_meta_scan()
self.save()
def save(self):
os.makedirs(os.path.dirname(self.layout.asset_db_path), exist_ok=True)
with open(self.layout.asset_db_path, "w", encoding="utf-8") as f:
json.dump(self.data, f, ensure_ascii=False, indent=4)
def get_asset(self, asset_guid: str) -> dict:
record = dict(self.data.get("assets", {}).get(asset_guid, {}) or {})
if not record:
self._sync_assets_from_meta_scan()
record = dict(self.data.get("assets", {}).get(asset_guid, {}) or {})
if not record:
return {}
asset_path = record.get("asset_path", "")
asset_abs_path = os.path.join(self.layout.project_root, asset_path.replace("/", os.sep)) if asset_path else ""
if asset_abs_path and not os.path.exists(asset_abs_path):
if self._sync_assets_from_meta_scan():
record = dict(self.data.get("assets", {}).get(asset_guid, {}) or {})
return record
def find_by_relative_path(self, relative_path: str) -> dict:
relative_path = str(relative_path or "").replace("\\", "/")
asset_guid = self.data.get("path_to_guid", {}).get(relative_path)
if not asset_guid:
self._sync_assets_from_meta_scan()
asset_guid = self.data.get("path_to_guid", {}).get(relative_path)
if not asset_guid:
return {}
return self.get_asset(asset_guid)
def find_by_absolute_path(self, asset_path: str) -> dict:
relative_path = relative_project_path(self.layout.project_root, asset_path)
if not relative_path:
return {}
return self.find_by_relative_path(relative_path)
def _hash_file(self, file_path: str) -> str:
file_path = normalize_path(file_path)
digest = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _read_meta(self, meta_path: str) -> dict:
if not os.path.exists(meta_path):
return {}
try:
with open(meta_path, "r", encoding="utf-8") as f:
payload = json.load(f)
if isinstance(payload, dict):
return payload
except Exception as e:
print(f"⚠️ 读取资源 meta 失败 {meta_path}: {e}")
return {}
def _write_meta(self, meta_path: str, payload: dict):
os.makedirs(os.path.dirname(meta_path), exist_ok=True)
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=4)
def _copy_into_assets(self, source_path: str, preferred_subdir: str = "") -> str:
source_path = normalize_path(source_path)
asset_type = detect_asset_type(source_path)
subdir = preferred_subdir or get_asset_subdir_for_type(asset_type)
target_dir = os.path.join(self.layout.assets_root, subdir)
os.makedirs(target_dir, exist_ok=True)
file_name = os.path.basename(source_path)
target_path = os.path.join(target_dir, file_name)
name_root, name_ext = os.path.splitext(file_name)
index = 1
while os.path.exists(target_path) and self._hash_file(target_path) != self._hash_file(source_path):
target_path = os.path.join(target_dir, f"{name_root}_{index}{name_ext}")
index += 1
if not os.path.exists(target_path):
shutil.copy2(source_path, target_path)
return target_path
def _build_model_import_cache(self, asset_record: dict):
if not self.world or not getattr(self.world, "loader", None):
return
asset_path = os.path.join(self.layout.project_root, asset_record["asset_path"].replace("/", os.sep))
if not os.path.exists(asset_path):
return
cache_dir = self.layout.imported_asset_dir(asset_record["guid"])
os.makedirs(cache_dir, exist_ok=True)
model_bam_path = os.path.join(cache_dir, "model.bam")
hierarchy_path = os.path.join(cache_dir, "hierarchy.json")
materials_path = os.path.join(cache_dir, "materials.json")
import_info_path = os.path.join(cache_dir, "import_info.json")
try:
model_np = self.world.loader.loadModel(Filename.fromOsSpecific(asset_path))
except Exception as e:
print(f"⚠️ 生成模型导入缓存失败 {asset_path}: {e}")
return
if not model_np or model_np.isEmpty():
return
try:
model_np.writeBamFile(Filename.fromOsSpecific(model_bam_path))
except Exception as e:
print(f"⚠️ 写入模型缓存 BAM 失败 {asset_path}: {e}")
return
hierarchy = []
materials = []
def _walk(node, parent_key=""):
try:
node_name = node.getName()
except Exception:
node_name = "node"
try:
children = list(node.getChildren())
except Exception:
children = []
node_key = parent_key
hierarchy.append(
{
"key": node_key,
"name": node_name,
"child_count": len(children),
}
)
for index, child in enumerate(children):
_walk(child, f"{node_key}/{index}" if node_key else str(index))
def _collect_materials(root_np):
for geom_np in root_np.findAllMatches("**/+GeomNode"):
try:
geom_node = geom_np.node()
except Exception:
continue
for geom_index in range(geom_node.getNumGeoms()):
materials.append(
{
"node_name": geom_np.getName(),
"geom_index": geom_index,
"material_name": "",
}
)
_walk(model_np)
_collect_materials(model_np)
with open(hierarchy_path, "w", encoding="utf-8") as f:
json.dump(hierarchy, f, ensure_ascii=False, indent=4)
with open(materials_path, "w", encoding="utf-8") as f:
json.dump(materials, f, ensure_ascii=False, indent=4)
with open(import_info_path, "w", encoding="utf-8") as f:
json.dump(
{
"asset_guid": asset_record["guid"],
"asset_path": asset_record["asset_path"],
"asset_type": asset_record["asset_type"],
"source_hash": asset_record["source_hash"],
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
},
f,
ensure_ascii=False,
indent=4,
)
asset_record["imported_cache"] = {
"root": relative_project_path(self.layout.project_root, cache_dir),
"model_bam": relative_project_path(self.layout.project_root, model_bam_path),
"hierarchy": relative_project_path(self.layout.project_root, hierarchy_path),
"materials": relative_project_path(self.layout.project_root, materials_path),
"import_info": relative_project_path(self.layout.project_root, import_info_path),
}
def register_asset(
self,
asset_path: str,
preferred_subdir: str = "",
copy_into_assets: bool = False,
build_import_cache: bool = False,
) -> dict:
asset_path = normalize_path(asset_path)
if not os.path.exists(asset_path):
return {}
if copy_into_assets or not relative_project_path(self.layout.project_root, asset_path).startswith("Assets/"):
asset_path = self._copy_into_assets(asset_path, preferred_subdir=preferred_subdir)
relative_asset_path = relative_project_path(self.layout.project_root, asset_path)
if not relative_asset_path:
return {}
meta_path = self.layout.meta_path_for_asset(asset_path)
meta_payload = self._read_meta(meta_path)
asset_guid = str(meta_payload.get("guid") or generate_guid())
asset_type = str(meta_payload.get("asset_type") or detect_asset_type(asset_path))
source_hash = self._hash_file(asset_path)
record = self.data.get("assets", {}).get(asset_guid, {}) or {}
record.update(
{
"guid": asset_guid,
"asset_path": relative_asset_path,
"asset_type": asset_type,
"meta_path": relative_project_path(self.layout.project_root, meta_path),
"source_hash": source_hash,
"importer": f"{asset_type}_importer",
"import_settings": meta_payload.get("import_settings", {}) or {},
"dependency_guids": meta_payload.get("dependency_guids", []) or [],
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
)
if asset_type == "model" and build_import_cache:
self._build_model_import_cache(record)
meta_payload = {
"guid": asset_guid,
"asset_type": asset_type,
"source_hash": source_hash,
"importer": record["importer"],
"import_settings": record["import_settings"],
"dependency_guids": record["dependency_guids"],
}
self._write_meta(meta_path, meta_payload)
self.data.setdefault("assets", {})[asset_guid] = record
self._rebuild_path_index()
self.save()
return dict(record)
def import_asset(self, source_path: str, preferred_subdir: str = "", build_import_cache: bool = False) -> dict:
return self.register_asset(
source_path,
preferred_subdir=preferred_subdir,
copy_into_assets=True,
build_import_cache=build_import_cache,
)
def ensure_project_assets_registered(self):
self._sync_assets_from_meta_scan()
for root, _, files in os.walk(self.layout.assets_root):
for file_name in files:
if file_name.endswith(".meta"):
continue
asset_path = os.path.join(root, file_name)
self.register_asset(asset_path, copy_into_assets=False, build_import_cache=False)

File diff suppressed because it is too large Load Diff

View File

@ -1,219 +0,0 @@
"""Project layout and schema helpers for MetaCore project v2."""
from __future__ import annotations
import os
import uuid
from dataclasses import dataclass
ENGINE_NAME = "MetaCore"
PROJECT_SCHEMA_VERSION = 2
ASSET_DB_SCHEMA_VERSION = 1
METASCENE_SCHEMA_VERSION = 2
RUNTIME_SCENE_SCHEMA_VERSION = 2
MANIFEST_SCHEMA_VERSION = 1
SCENE_DESCRIPTION_EXTENSION = ".metascene"
MODEL_EXTENSIONS = {
".bam",
".egg",
".obj",
".fbx",
".gltf",
".glb",
".ply",
".stl",
}
TEXTURE_EXTENSIONS = {
".png",
".jpg",
".jpeg",
".bmp",
".tga",
".tif",
".tiff",
".dds",
".hdr",
".exr",
}
AUDIO_EXTENSIONS = {".wav", ".mp3", ".ogg", ".flac"}
VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".wmv", ".webm"}
SCRIPT_EXTENSIONS = {".py", ".pyc"}
UI_EXTENSIONS = {".json", ".ttf", ".otf", ".woff", ".woff2"}
def generate_guid() -> str:
return uuid.uuid4().hex
def normalize_path(path: str) -> str:
return os.path.normpath(os.path.abspath(path))
def relative_project_path(project_root: str, target_path: str) -> str:
project_root = normalize_path(project_root)
target_path = normalize_path(target_path)
try:
relative_path = os.path.relpath(target_path, project_root)
except ValueError:
return ""
if relative_path.startswith(".."):
return ""
return relative_path.replace("\\", "/")
def detect_asset_type(file_path: str) -> str:
extension = os.path.splitext(str(file_path or ""))[1].lower()
if extension in MODEL_EXTENSIONS:
return "model"
if extension in TEXTURE_EXTENSIONS:
return "texture"
if extension in AUDIO_EXTENSIONS:
return "audio"
if extension in VIDEO_EXTENSIONS:
return "video"
if extension in SCRIPT_EXTENSIONS:
return "script"
if extension in UI_EXTENSIONS:
return "ui"
return "other"
def get_asset_subdir_for_type(asset_type: str) -> str:
return {
"model": "Models",
"texture": "Textures",
"audio": "Audio",
"video": "Video",
"ui": "UI",
"script": "Scripts",
"other": "Misc",
}.get(asset_type, "Misc")
def default_build_settings() -> dict:
return {
"enabled_scene_guids": [],
"output_format": "directory",
"windows_player": True,
"active_profile": "default",
"profiles": {
"default": {
"name": "Default",
"target_platform": "windows",
"output_format": "directory",
"output_dir": "Builds",
"windows": {
"enabled": True,
"exe_name": "",
"windowed": True,
},
"runtime": {
"startup_scene_guid": "",
"enabled_scene_guids": [],
"script_mode": "pyc",
},
}
},
}
@dataclass(frozen=True)
class ProjectLayout:
project_root: str
def __post_init__(self):
object.__setattr__(self, "project_root", normalize_path(self.project_root))
@property
def project_file(self) -> str:
return os.path.join(self.project_root, "project.json")
@property
def assets_root(self) -> str:
return os.path.join(self.project_root, "Assets")
@property
def scenes_root(self) -> str:
return os.path.join(self.project_root, "Scenes")
@property
def library_root(self) -> str:
return os.path.join(self.project_root, "Library")
@property
def builds_root(self) -> str:
return os.path.join(self.project_root, "Builds")
@property
def asset_db_path(self) -> str:
return os.path.join(self.library_root, "AssetDB.json")
@property
def imported_root(self) -> str:
return os.path.join(self.library_root, "Imported")
@property
def build_cache_root(self) -> str:
return os.path.join(self.library_root, "BuildCache")
@property
def scripts_root(self) -> str:
return os.path.join(self.assets_root, "Scripts")
@property
def models_root(self) -> str:
return os.path.join(self.assets_root, "Models")
@property
def textures_root(self) -> str:
return os.path.join(self.assets_root, "Textures")
@property
def audio_root(self) -> str:
return os.path.join(self.assets_root, "Audio")
@property
def video_root(self) -> str:
return os.path.join(self.assets_root, "Video")
@property
def ui_root(self) -> str:
return os.path.join(self.assets_root, "UI")
@property
def misc_root(self) -> str:
return os.path.join(self.assets_root, "Misc")
def meta_path_for_asset(self, asset_abs_path: str) -> str:
return f"{normalize_path(asset_abs_path)}.meta"
def imported_asset_dir(self, asset_guid: str) -> str:
return os.path.join(self.imported_root, asset_guid)
def build_cache_dir(self, scene_guid: str) -> str:
return os.path.join(self.build_cache_root, scene_guid)
def scene_file(self, scene_name: str) -> str:
return os.path.join(self.scenes_root, f"{scene_name}{SCENE_DESCRIPTION_EXTENSION}")
def ensure_project_directories(layout: ProjectLayout) -> None:
for path in (
layout.assets_root,
layout.models_root,
layout.textures_root,
layout.audio_root,
layout.video_root,
layout.ui_root,
layout.scripts_root,
layout.misc_root,
layout.scenes_root,
layout.library_root,
layout.imported_root,
layout.build_cache_root,
layout.builds_root,
):
os.makedirs(path, exist_ok=True)

View File

@ -1,674 +0,0 @@
"""Scene description and runtime scene helpers for MetaCore project v2."""
from __future__ import annotations
import json
import os
from datetime import datetime
from project.project_schema import (
ENGINE_NAME,
MANIFEST_SCHEMA_VERSION,
METASCENE_SCHEMA_VERSION,
RUNTIME_SCENE_SCHEMA_VERSION,
relative_project_path,
)
def load_json(file_path: str, default_value):
if not file_path or not os.path.exists(file_path):
return default_value
try:
with open(file_path, "r", encoding="utf-8-sig") as f:
return json.load(f)
except Exception as e:
print(f"⚠️ 读取 JSON 失败 {file_path}: {e}")
return default_value
def save_json(file_path: str, payload):
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=4)
def _migrate_scene_description_v1_to_v2(payload: dict) -> dict:
payload = dict(payload or {})
scene_components = dict(payload.get("scene_components", {}) or {})
camera_component = dict(scene_components.get("camera", {}) or payload.get("camera", {}) or {})
gui_component = dict(scene_components.get("gui", {}) or {})
gui_elements = list(gui_component.get("elements", []) or payload.get("gui", []) or [])
lui_component = dict(scene_components.get("lui", {}) or payload.get("lui", {}) or {})
scene_components["camera"] = camera_component
scene_components["gui"] = {"elements": gui_elements}
scene_components["lui"] = lui_component
payload["scene_components"] = scene_components
payload["camera"] = camera_component
payload["gui"] = gui_elements
payload["lui"] = lui_component
normalized_nodes = []
for raw_node in list(payload.get("nodes", []) or []):
node = dict(raw_node or {})
components = dict(node.get("components", {}) or {})
runtime_interactive = bool(
(components.get("metadata", {}) or {}).get("runtime_interactive", node.get("runtime_interactive", False))
)
scripts = list((components.get("scripts", {}) or {}).get("entries", []) or node.get("scripts", []) or [])
model_component = dict(components.get("model", {}) or {})
if not model_component and (node.get("asset_guid") or node.get("asset_path") or node.get("imported_node_key")):
model_component = {
"asset_guid": node.get("asset_guid", ""),
"asset_path": node.get("asset_path", ""),
"imported_node_key": node.get("imported_node_key", ""),
"is_model_root": bool((node.get("tags", {}) or {}).get("is_model_root")),
}
components["model"] = model_component
components["scripts"] = {"entries": scripts} if scripts else {}
components["metadata"] = dict(components.get("metadata", {}) or {})
components["metadata"]["runtime_interactive"] = runtime_interactive
components["metadata"].setdefault("node_class", node.get("node_class", ""))
node["components"] = components
node["runtime_interactive"] = runtime_interactive
node["scripts"] = scripts
if model_component:
node["asset_guid"] = str(model_component.get("asset_guid", "") or node.get("asset_guid", "") or "")
node["asset_path"] = str(model_component.get("asset_path", "") or node.get("asset_path", "") or "")
node["imported_node_key"] = str(model_component.get("imported_node_key", "") or node.get("imported_node_key", "") or "")
normalized_nodes.append(node)
payload["nodes"] = normalized_nodes
payload["schema_version"] = 2
return payload
def normalize_scene_description(payload: dict) -> dict:
payload = dict(payload or {})
version = int(payload.get("schema_version", 1) or 1)
original_version = version
while version < METASCENE_SCHEMA_VERSION:
if version == 1:
payload = _migrate_scene_description_v1_to_v2(payload)
version = 2
continue
break
payload["schema_version"] = METASCENE_SCHEMA_VERSION
if original_version != METASCENE_SCHEMA_VERSION:
print(f" 场景描述已从 schema v{original_version} 迁移到 v{METASCENE_SCHEMA_VERSION}")
return payload
def _migrate_runtime_scene_v1_to_v2(payload: dict) -> dict:
payload = dict(payload or {})
scene_components = dict(payload.get("scene_components", {}) or {})
camera_component = dict(scene_components.get("camera", {}) or payload.get("camera", {}) or {})
gui_component = dict(scene_components.get("gui", {}) or {})
gui_elements = list(gui_component.get("elements", []) or payload.get("gui", []) or [])
lui_component = dict(scene_components.get("lui", {}) or payload.get("lui", {}) or {})
payload["scene_components"] = {
"camera": camera_component,
"gui": {"elements": gui_elements},
"lui": lui_component,
}
payload["camera"] = camera_component
payload["gui"] = gui_elements
payload["lui"] = lui_component
payload["nodes"] = normalize_scene_description({"nodes": payload.get("nodes", []) or []}).get("nodes", [])
payload["schema_version"] = 2
return payload
def normalize_runtime_scene(payload: dict) -> dict:
payload = dict(payload or {})
version = int(payload.get("schema_version", 1) or 1)
original_version = version
while version < RUNTIME_SCENE_SCHEMA_VERSION:
if version == 1:
payload = _migrate_runtime_scene_v1_to_v2(payload)
version = 2
continue
break
payload["schema_version"] = RUNTIME_SCENE_SCHEMA_VERSION
if original_version != RUNTIME_SCENE_SCHEMA_VERSION:
print(f" 运行时场景已从 schema v{original_version} 迁移到 v{RUNTIME_SCENE_SCHEMA_VERSION}")
return payload
def _node_path_id(parent_id: str, index: int) -> str:
return f"{parent_id}/{index}" if parent_id else str(index)
def _collect_light_component(node):
if not node.hasTag("light_type"):
return {}
component = {"type": node.getTag("light_type")}
for tag_name, output_name in (
("light_energy", "energy"),
("light_radius", "radius"),
("light_fov", "fov"),
("stored_energy", "stored_energy"),
):
if node.hasTag(tag_name):
component[output_name] = node.getTag(tag_name)
return component
def _collect_model_component(node, asset_guid: str, asset_path: str, imported_node_key: str) -> dict:
if not asset_guid and not node.hasTag("is_model_root"):
return {}
return {
"asset_guid": asset_guid,
"asset_path": asset_path,
"imported_node_key": imported_node_key,
"is_model_root": bool(node.hasTag("is_model_root")),
}
def _collect_script_component(scripts: list[dict]) -> dict:
return {"entries": list(scripts or [])} if scripts else {}
def _collect_node_metadata_component(node, runtime_interactive: bool) -> dict:
metadata = {
"node_class": node.node().getClassType().getName() if node.node() else "",
"runtime_interactive": runtime_interactive,
}
for tag_name in (
"has_animations",
"has_animations_checked",
"can_create_actor_from_memory",
"saved_has_animations",
"saved_can_create_actor_from_memory",
):
if node.hasTag(tag_name):
metadata[tag_name] = node.getTag(tag_name)
return metadata
def _collect_material_override_component(node):
material_tags = {}
for tag_name in (
"material_ambient",
"material_diffuse",
"material_specular",
"material_emission",
"material_shininess",
"material_basecolor",
"material_roughness",
"material_metallic",
"material_ior",
"material_effect_metallic_enabled",
"material_effect_default_texture_enabled",
"material_effect_parallax_enabled",
"material_render_effect_signature",
"material_texture_diffuse",
"material_texture_normal",
"material_texture_ior",
"material_texture_roughness",
"material_texture_parallax",
"material_texture_metallic",
"material_texture_emission",
"material_texture_ao",
"material_texture_alpha",
"material_texture_detail",
"material_texture_gloss",
):
if node.hasTag(tag_name):
material_tags[tag_name] = node.getTag(tag_name)
return material_tags
def _resolve_imported_node_key(node, fallback_key: str) -> str:
if node.hasTag("is_model_root"):
for tag_name in ("imported_node_key", "source_model_node_key", "ssbo_tree_key", "tree_item_key"):
if node.hasTag(tag_name):
tag_value = str(node.getTag(tag_name) or "").strip()
if tag_value:
return tag_value
return ""
for tag_name in ("imported_node_key", "source_model_node_key", "ssbo_tree_key", "tree_item_key"):
if node.hasTag(tag_name):
tag_value = str(node.getTag(tag_name) or "").strip()
if tag_value:
return tag_value
return fallback_key
def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], runtime_interactive: bool, light_component: dict, material_component: dict) -> bool:
if not node:
return False
if asset_guid:
return True
if scripts:
return True
if runtime_interactive:
return True
if light_component:
return True
if material_component:
return True
if node.hasTag("scene_transform_dirty") and node.getTag("scene_transform_dirty").lower() == "true":
return True
if node.hasTag("scene_material_dirty") and node.getTag("scene_material_dirty").lower() == "true":
return True
if node.hasTag("user_visible") and node.getTag("user_visible").lower() != "true":
return True
return False
def _build_scene_components(camera_state: dict, gui_elements: list, lui_snapshot: dict) -> dict:
return {
"camera": dict(camera_state or {}),
"gui": {"elements": list(gui_elements or [])},
"lui": dict(lui_snapshot or {}),
}
def _build_scene_description_payload(
*,
asset_database,
project_root: str,
scene_guid: str,
scene_name: str,
scene_file_rel: str,
root_nodes,
cache_bam_path: str = "",
cache_gui_path: str = "",
cache_lui_path: str = "",
gui_elements: list | None = None,
lui_snapshot: dict | None = None,
camera_state: dict | None = None,
):
gui_elements = list(gui_elements or [])
lui_snapshot = dict(lui_snapshot or {})
camera_state = dict(camera_state or {})
node_entries = []
referenced_asset_guids = set()
referenced_script_guids = set()
subtree_serializable_cache = {}
def _node_is_serializable(node) -> bool:
if not node or node.isEmpty():
return False
cache_key = id(node)
if cache_key in subtree_serializable_cache:
return subtree_serializable_cache[cache_key]
runtime_interactive = (
node.hasTag("runtime_interactive")
and node.getTag("runtime_interactive").lower() == "true"
)
scripts = []
if node.hasTag("scripts_info"):
try:
scripts = json.loads(node.getTag("scripts_info"))
except Exception:
scripts = []
asset_guid = ""
if node.hasTag("is_model_root"):
asset_guid = "root"
light_component = _collect_light_component(node)
material_component = _collect_material_override_component(node)
serializable_here = _should_serialize_child_nodes(
node,
asset_guid,
scripts,
runtime_interactive,
light_component,
material_component,
)
if serializable_here:
subtree_serializable_cache[cache_key] = True
return True
try:
children = list(node.getChildren())
except Exception:
children = []
result = any(_node_is_serializable(child) for child in children if child and not child.isEmpty())
subtree_serializable_cache[cache_key] = result
return result
def walk_nodes(children, parent_id="", force_include=False):
for index, child in enumerate(list(children or [])):
if not child or child.isEmpty():
continue
node_id = _node_path_id(parent_id, index)
child_force_include = bool(force_include)
if not child_force_include:
try:
child_force_include = (
child.hasTag("is_model_root")
and child.hasTag("ssbo_managed")
and child.getTag("ssbo_managed").strip().lower() in ("1", "true", "yes", "on")
)
except Exception:
child_force_include = False
# If we are force-including (e.g. ssbo_managed model skeleton),
# we must include this node regardless of its individual "serializable" status.
if not child_force_include and not _node_is_serializable(child):
continue
node_name = child.getName()
imported_node_key = _resolve_imported_node_key(child, node_id)
runtime_interactive = (
child.hasTag("runtime_interactive")
and child.getTag("runtime_interactive").lower() == "true"
)
scripts = []
if child.hasTag("scripts_info"):
try:
scripts = json.loads(child.getTag("scripts_info"))
except Exception:
scripts = []
asset_guid = ""
asset_path = ""
if child.hasTag("is_model_root"):
for tag_name in ("model_path", "saved_model_path", "original_path"):
if child.hasTag(tag_name):
candidate = child.getTag(tag_name)
if candidate:
asset_record = asset_database.register_asset(candidate, copy_into_assets=False)
if asset_record:
asset_guid = asset_record.get("guid", "")
asset_path = asset_record.get("asset_path", "")
referenced_asset_guids.add(asset_guid)
break
for script_info in scripts:
script_guid = str(script_info.get("script_guid", "") or "").strip()
asset_record = asset_database.get_asset(script_guid) if script_guid else {}
if not asset_record:
project_relative_path = str(script_info.get("project_relative_path", "") or "")
if project_relative_path:
script_abs_path = os.path.join(project_root, project_relative_path.replace("/", os.sep))
asset_record = asset_database.register_asset(script_abs_path, copy_into_assets=False)
if asset_record:
resolved_guid = asset_record.get("guid", "")
if resolved_guid:
script_info["script_guid"] = resolved_guid
referenced_script_guids.add(resolved_guid)
asset_path_for_script = str(asset_record.get("asset_path", "") or "")
if asset_path_for_script:
script_info["project_relative_path"] = asset_path_for_script
script_info["file"] = asset_path_for_script
light_component = _collect_light_component(child)
material_component = _collect_material_override_component(child)
node_entries.append(
{
"node_id": node_id,
"parent_id": parent_id or None,
"name": node_name,
"imported_node_key": imported_node_key,
"asset_guid": asset_guid,
"asset_path": asset_path,
"node_class": child.node().getClassType().getName() if child.node() else "",
"transform": {
"position": [child.getX(), child.getY(), child.getZ()],
"rotation": [child.getH(), child.getP(), child.getR()],
"scale": [child.getSx(), child.getSy(), child.getSz()],
},
"visibility": {
"user_visible": (
child.getTag("user_visible").lower() == "true"
if child.hasTag("user_visible")
else True
)
},
"runtime_interactive": runtime_interactive,
"scripts": scripts,
"components": {
"model": _collect_model_component(child, asset_guid, asset_path, imported_node_key),
"scripts": _collect_script_component(scripts),
"light": light_component,
"material_overrides": material_component,
"metadata": _collect_node_metadata_component(child, runtime_interactive),
},
"tags": {
tag_name: child.getTag(tag_name)
for tag_name in child.getTagKeys()
},
}
)
# Recursive step: if force_include is True, we must always recurse to capture the full skeleton.
if (
child_force_include
or _should_serialize_child_nodes(
child,
asset_guid,
scripts,
runtime_interactive,
light_component,
material_component,
)
or any(
_node_is_serializable(grandchild)
for grandchild in list(child.getChildren()) if grandchild and not grandchild.isEmpty()
)
):
try:
child_nodes = list(child.getChildren())
except Exception:
child_nodes = []
walk_nodes(child_nodes, node_id, force_include=child_force_include)
walk_nodes(root_nodes)
description = {
"schema_version": METASCENE_SCHEMA_VERSION,
"scene_guid": scene_guid,
"scene_name": scene_name,
"scene_file": scene_file_rel,
"saved_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"cache": {
"scene_bam": relative_project_path(project_root, cache_bam_path),
"gui_json": relative_project_path(project_root, cache_gui_path),
"lui_json": relative_project_path(project_root, cache_lui_path),
},
"referenced_asset_guids": sorted(guid for guid in referenced_asset_guids if guid),
"referenced_script_guids": sorted(guid for guid in referenced_script_guids if guid),
"scene_components": _build_scene_components(camera_state, gui_elements, lui_snapshot),
"camera": camera_state,
"gui": gui_elements,
"lui": lui_snapshot,
"nodes": node_entries,
}
return description
def build_scene_description_from_world(
*,
asset_database,
project_root: str,
scene_guid: str,
scene_name: str,
scene_file_rel: str,
root_nodes,
cache_bam_path: str = "",
cache_gui_path: str = "",
cache_lui_path: str = "",
gui_elements: list | None = None,
lui_snapshot: dict | None = None,
camera_state: dict | None = None,
):
return _build_scene_description_payload(
asset_database=asset_database,
project_root=project_root,
scene_guid=scene_guid,
scene_name=scene_name,
scene_file_rel=scene_file_rel,
root_nodes=root_nodes,
cache_bam_path=cache_bam_path,
cache_gui_path=cache_gui_path,
cache_lui_path=cache_lui_path,
gui_elements=gui_elements,
lui_snapshot=lui_snapshot,
camera_state=camera_state,
)
def build_runtime_scene(scene_description: dict):
nodes = list(scene_description.get("nodes", []) or [])
scene_components = dict(scene_description.get("scene_components", {}) or {})
camera_component = dict(scene_components.get("camera", {}) or scene_description.get("camera", {}) or {})
gui_component = dict(scene_components.get("gui", {}) or {})
gui_elements = list(gui_component.get("elements", []) or scene_description.get("gui", []) or [])
lui_component = dict(scene_components.get("lui", {}) or scene_description.get("lui", {}) or {})
interactive_model_names = []
interactive_node_ids = []
static_model_names = []
static_node_ids = []
for node in nodes:
if not node.get("asset_guid"):
continue
metadata_component = dict((node.get("components", {}) or {}).get("metadata", {}) or {})
has_animations = str(
metadata_component.get(
"has_animations",
metadata_component.get(
"saved_has_animations",
(node.get("tags", {}) or {}).get(
"has_animations",
(node.get("tags", {}) or {}).get("saved_has_animations", ""),
),
),
)
or ""
).lower() == "true"
can_create_actor = str(
metadata_component.get(
"can_create_actor_from_memory",
metadata_component.get(
"saved_can_create_actor_from_memory",
(node.get("tags", {}) or {}).get(
"can_create_actor_from_memory",
(node.get("tags", {}) or {}).get("saved_can_create_actor_from_memory", ""),
),
),
)
or ""
).lower() == "true"
if has_animations or can_create_actor:
interactive_node_ids.append(node.get("node_id", ""))
interactive_model_names.append(node.get("name", ""))
continue
if node.get("runtime_interactive"):
interactive_node_ids.append(node.get("node_id", ""))
interactive_model_names.append(node.get("name", ""))
continue
if node.get("scripts"):
interactive_node_ids.append(node.get("node_id", ""))
interactive_model_names.append(node.get("name", ""))
continue
static_node_ids.append(node.get("node_id", ""))
static_model_names.append(node.get("name", ""))
return {
"schema_version": RUNTIME_SCENE_SCHEMA_VERSION,
"scene_guid": scene_description.get("scene_guid", ""),
"scene_name": scene_description.get("scene_name", ""),
"cook": {
"interactive_node_ids": sorted({node_id for node_id in interactive_node_ids if node_id}),
"interactive_model_names": sorted({name for name in interactive_model_names if name}),
"static_node_ids": sorted({node_id for node_id in static_node_ids if node_id}),
"static_model_names": sorted({name for name in static_model_names if name}),
},
"interactive_model_names": sorted({name for name in interactive_model_names if name}),
"scene_components": {
"camera": camera_component,
"gui": {"elements": gui_elements},
"lui": lui_component,
},
"camera": camera_component,
"gui": gui_elements,
"lui": lui_component,
"nodes": nodes,
"referenced_asset_guids": scene_description.get("referenced_asset_guids", []) or [],
"referenced_script_guids": scene_description.get("referenced_script_guids", []) or [],
}
def build_scene_cook_manifest(scene_description: dict, runtime_scene: dict):
runtime_cook = dict(runtime_scene.get("cook", {}) or {})
return {
"schema_version": 1,
"scene_guid": scene_description.get("scene_guid", ""),
"scene_name": scene_description.get("scene_name", ""),
"source_scene_file": scene_description.get("scene_file", ""),
"node_count": len(scene_description.get("nodes", []) or []),
"interactive_node_ids": list(runtime_cook.get("interactive_node_ids", []) or []),
"interactive_model_names": list(runtime_cook.get("interactive_model_names", []) or []),
"static_node_ids": list(runtime_cook.get("static_node_ids", []) or []),
"static_model_names": list(runtime_cook.get("static_model_names", []) or []),
"referenced_asset_guids": list(scene_description.get("referenced_asset_guids", []) or []),
"referenced_script_guids": list(scene_description.get("referenced_script_guids", []) or []),
"scene_components": dict(scene_description.get("scene_components", {}) or {}),
"cache": dict(scene_description.get("cache", {}) or {}),
"saved_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
def build_runtime_manifest(project_config: dict, runtime_scenes: list[dict], asset_records: list[dict]):
scene_guids = [scene.get("scene_guid", "") for scene in runtime_scenes]
startup_scene_guid = project_config.get("startup_scene_guid", "")
if startup_scene_guid not in scene_guids:
startup_scene_guid = scene_guids[0] if scene_guids else ""
total_interactive_nodes = 0
total_static_nodes = 0
total_interactive_models = 0
total_static_models = 0
for scene in runtime_scenes:
cook = dict(scene.get("cook", {}) or {})
total_interactive_nodes += len(list(cook.get("interactive_node_ids", []) or []))
total_static_nodes += len(list(cook.get("static_node_ids", []) or []))
total_interactive_models += len(list(cook.get("interactive_model_names", []) or []))
total_static_models += len(list(cook.get("static_model_names", []) or []))
return {
"schema_version": MANIFEST_SCHEMA_VERSION,
"project_name": project_config.get("name", f"{ENGINE_NAME}Project"),
"startup_scene_guid": startup_scene_guid,
"scene_guids": scene_guids,
"scenes": [
{
"guid": scene.get("scene_guid", ""),
"name": scene.get("scene_name", ""),
"runtime_path": f"scenes/{scene.get('scene_guid', '')}.runtime.json",
}
for scene in runtime_scenes
],
"assets": [
{
"guid": asset.get("guid", ""),
"asset_type": asset.get("asset_type", ""),
"asset_path": asset.get("asset_path", ""),
"imported_cache": asset.get("imported_cache", {}) or {},
}
for asset in asset_records
if asset
],
"cook_summary": {
"scene_count": len(runtime_scenes),
"asset_count": len(asset_records),
"interactive_node_count": total_interactive_nodes,
"static_node_count": total_static_nodes,
"interactive_model_count": total_interactive_models,
"static_model_count": total_static_models,
},
"build_settings": project_config.get("build_settings", {}) or {},
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,90 @@
# 项目部署指南
## 🚀 在新电脑上运行此项目
### 方法1使用Conda推荐
```bash
# 1. 安装Miniconda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
# 2. 克隆项目
git clone <your-repo-url>
cd eg
# 3. 创建conda环境
conda env create -f environment.yml
# 4. 激活环境
conda activate eg-project
# 5. 运行项目
python main.py
```
### 方法2使用virtualenv + pip
```bash
# 1. 克隆项目
git clone <your-repo-url>
cd eg
# 2. 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
# 或 venv\Scripts\activate # Windows
# 3. 安装依赖
pip install -r clean-requirements.txt
# 4. 运行项目
python main.py
```
## 📁 需要复制的文件
**必须复制:**
- `main.py` - 主程序
- `gui_preview_window.py` - GUI窗口
- `demo/` - 演示文件夹
- `environment.yml` - Conda环境配置
- `clean-requirements.txt` - 核心依赖
**不要复制:**
- `.conda/` - 虚拟环境文件夹
- `__pycache__/` - Python缓存
- `requirements.txt` - 包含系统包的混乱依赖
## 🔧 Cursor IDE 设置
1. **打开项目**在Cursor中打开项目文件夹
2. **选择解释器**
- `Ctrl+Shift+P` → "Python: Select Interpreter"
- 选择虚拟环境中的Python路径
3. **验证环境**检查状态栏显示正确的Python版本
## 📦 项目依赖说明
- **Panda3D**: 3D图形引擎
- **PyQt5/PySide6**: GUI框架
- **Pillow**: 图像处理
- **python-dotenv**: 环境变量管理
- **pyassimp**: 3D模型加载
## 🌍 跨平台注意事项
- Linux/Mac: 使用 `source venv/bin/activate`
- Windows: 使用 `venv\Scripts\activate`
- 某些依赖可能需要系统级安装如OpenGL库
## 🐛 常见问题
**Q: 无法安装Panda3D**
A: 确保系统有OpenGL支持或使用conda安装
**Q: PyQt5无法运行**
A: 可能需要安装系统级GUI依赖
**Q: 导入错误?**
A: 检查Python版本需要3.10+

View File

@ -1,7 +1,6 @@
Panda3D==1.10.15
imgui-bundle
Pillow>=9.0.1
numpy>=1.24
aiohttp>=3.9
openvr==2.2.0
pyassimp>=5.2.5
Panda3D>=1.10.15
PyQt5>=5.15.9
PySide6>=6.8.1
Pillow>=9.0.1
python-dotenv>=1.0.1
pyassimp>=5.2.5

View File

@ -0,0 +1,34 @@
# This file may be used to create an environment using:
# $ conda create --name <env> --file <this file>
# platform: linux-64
# created-by: conda 25.5.1
@EXPLICIT
https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda
https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.2.25-h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.40-h12ee557_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda
https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda
https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.1-h6a678d5_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.16-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h993c535_1.conda
https://repo.anaconda.com/pkgs/main/linux-64/python-3.10.18-h1a3bd86_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/setuptools-78.1.1-py310h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py310h06a4308_0.conda
https://repo.anaconda.com/pkgs/main/noarch/pip-25.1-pyhc872135_2.conda

View File

@ -0,0 +1,15 @@
name: eg-project
channels:
- conda-forge
- defaults
dependencies:
- python=3.10
- pip
- pip:
- Panda3D>=1.10.15
- PyQt5>=5.15.9
- PySide6>=6.8.1
- Pillow>=9.0.1
- python-dotenv>=1.0.1
- pyassimp>=5.2.5
- six

View File

@ -1,14 +1,102 @@
# Core runtime dependencies for the current ImGui editor stack.
Panda3D==1.10.15
imgui-bundle
Pillow>=9.0.1
numpy>=1.24
aiohttp>=3.9
openvr==2.2.0
# Asset import / conversion helpers.
pyassimp>=5.2.5
# Optional features:
# playwright>=1.40 # Required by core/imgui_webview.py
# bpy # Required only when using Blender-based conversion flows
apturl==0.5.2
bcrypt==3.2.0
beautifulsoup4==4.10.0
blinker==1.4
Brlapi==0.8.3
certifi==2020.6.20
chardet==4.0.0
click==8.0.3
colorama==0.4.4
command-not-found==0.3
cryptography==3.4.8
cupshelpers==1.0
dbus-python==1.2.18
defer==1.0.6
DirectFolderBrowser==22.1
DirectGuiDesigner==22.4.1
DirectGuiExtension==22.9
distro==1.7.0
distro-info==1.1+ubuntu0.2
duplicity==0.8.21
fasteners==0.14.1
future==0.18.2
html5lib==1.1
httplib2==0.20.2
idna==3.3
importlib-metadata==4.6.4
jeepney==0.7.1
keyring==23.5.0
language-selector==0.1
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
lockfile==0.12.2
louis==3.20.0
lxml==4.8.0
macaroonbakery==1.3.1
Mako==1.1.3
MarkupSafe==2.0.1
monotonic==1.6
more-itertools==8.10.0
netifaces==0.11.0
oauthlib==3.2.0
olefile==0.46
Panda3D==1.10.15
panda3d-frame==22.10.post1
Panda3DNodeEditor==22.5
paramiko==2.9.3
pexpect==4.8.0
Pillow==9.0.1
platformdirs==4.3.6
protobuf==3.12.4
ptyprocess==0.7.0
pyassimp==5.2.5
pycairo==1.20.1
pycups==2.0.1
Pygments==2.11.2
PyGObject==3.42.1
PyJWT==2.3.0
pymacaroons==0.13.0
PyNaCl==1.5.0
pyparsing==2.4.7
PyQt5==5.15.9
pyqt5-plugins==5.15.9.2.3
PyQt5-Qt5==5.15.2
pyqt5-tools==5.15.9.3.3
PyQt5_sip==12.16.1
pyRFC3339==1.1
PySide6==6.8.1
PySide6_Addons==6.8.1
PySide6_Essentials==6.8.1
python-apt==2.4.0+ubuntu4
python-dateutil==2.8.1
python-debian==0.1.43+ubuntu1.1
python-dotenv==1.0.1
pytz==2022.1
pyxdg==0.27
PyYAML==5.4.1
QPanda3D==0.2.10
qt5-applications==5.15.2.2.3
qt5-tools==5.15.2.1.3
reportlab==3.6.8
requests==2.25.1
SceneEditor==22.5
screen-resolution-extra==0.0.0
SecretStorage==3.3.1
shiboken6==6.8.1
six==1.16.0
soupsieve==2.3.1
systemd-python==234
ubuntu-drivers-common==0.0.0
ubuntu-pro-client==8001
ufw==0.36.1
unattended-upgrades==0.1
urllib3==1.26.5
usb-creator==0.3.7
wadllib==1.3.6
webencodings==0.5.1
xdg==5
xkit==0.0.0
zipp==1.0.0
openvr==2.2.0
imgui-bundle

View File

@ -1,328 +0,0 @@
"""Helpers for lightweight glTF metadata probing and static BAM caching."""
from __future__ import annotations
import hashlib
import json
import os
import struct
import tempfile
_GLTF_METADATA_CACHE = {}
def is_gltf_path(file_path: str) -> bool:
ext = os.path.splitext(str(file_path or ""))[1].lower()
return ext in {".gltf", ".glb"}
def _to_os_specific_path(file_path: str) -> str:
path_text = os.fspath(file_path or "")
if not path_text:
return path_text
if os.path.exists(path_text):
return os.path.normpath(path_text)
try:
from panda3d.core import Filename
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(path_text)
os_path = panda_filename.to_os_specific()
if os_path and os.path.exists(os_path):
return os.path.normpath(os_path)
except Exception:
continue
converted = Filename(path_text).to_os_specific()
if converted and os.path.exists(converted):
return os.path.normpath(converted)
except Exception:
pass
if len(path_text) >= 3 and path_text[0] in ("/", "\\") and path_text[1].isalpha() and path_text[2] in ("/", "\\"):
drive_path = f"{path_text[1]}:{path_text[2:]}"
drive_path = os.path.normpath(drive_path)
if os.path.exists(drive_path):
return drive_path
return os.path.normpath(path_text)
def _load_gltf_json_payload(file_path: str):
file_path = _to_os_specific_path(file_path)
if not file_path or not os.path.exists(file_path):
return None
ext = os.path.splitext(file_path)[1].lower()
try:
if ext == ".gltf":
with open(file_path, "r", encoding="utf-8") as handle:
return json.load(handle)
if ext != ".glb":
return None
with open(file_path, "rb") as handle:
header = handle.read(12)
if len(header) != 12:
return None
magic, _version, total_length = struct.unpack("<4sII", header)
if magic != b"glTF":
return None
while handle.tell() < total_length:
chunk_header = handle.read(8)
if len(chunk_header) != 8:
break
chunk_length, chunk_type = struct.unpack("<I4s", chunk_header)
chunk_data = handle.read(chunk_length)
if chunk_type == b"JSON":
json_text = chunk_data.decode("utf-8", errors="ignore").rstrip("\x00 \t\r\n")
return json.loads(json_text)
except Exception:
return None
return None
def probe_gltf_metadata(file_path: str) -> dict:
file_path = _to_os_specific_path(file_path)
if not file_path or not os.path.exists(file_path):
return {
"is_gltf": False,
"has_animations": False,
"animation_count": 0,
}
try:
stat_info = os.stat(file_path)
cache_key = (
os.path.abspath(file_path),
int(stat_info.st_mtime_ns),
int(stat_info.st_size),
)
cached = _GLTF_METADATA_CACHE.get(cache_key)
if cached is not None:
return dict(cached)
except Exception:
cache_key = None
payload = _load_gltf_json_payload(file_path)
if not payload:
result = {
"is_gltf": False,
"has_animations": False,
"animation_count": 0,
}
if cache_key is not None:
_GLTF_METADATA_CACHE.clear()
_GLTF_METADATA_CACHE[cache_key] = dict(result)
return result
animations = payload.get("animations") or []
result = {
"is_gltf": True,
"has_animations": bool(animations),
"animation_count": len(animations),
}
if cache_key is not None:
_GLTF_METADATA_CACHE.clear()
_GLTF_METADATA_CACHE[cache_key] = dict(result)
return result
def _resolve_cache_root(project_root: str = "") -> str:
project_root = os.path.normpath(project_root or "")
if project_root and os.path.isdir(project_root):
try:
from project.project_schema import ProjectLayout, ensure_project_directories
layout = ProjectLayout(project_root)
ensure_project_directories(layout)
cache_root = os.path.join(layout.imported_root, "__gltf_visual_cache__")
os.makedirs(cache_root, exist_ok=True)
return cache_root
except Exception:
pass
cache_root = os.path.join(tempfile.gettempdir(), "EG_gltf_visual_cache")
os.makedirs(cache_root, exist_ok=True)
return cache_root
def _choose_visual_cache_builder(file_path: str, *, skip_animations: bool) -> str:
meta = probe_gltf_metadata(file_path)
if meta.get("is_gltf") and not meta.get("has_animations") and bool(skip_animations):
return "fluent"
return "gltf"
def _build_cache_key(file_path: str, *, skip_animations: bool, flatten_nodes: bool, builder: str) -> str:
stat_info = os.stat(file_path)
digest = hashlib.sha1(
"|".join(
[
os.path.abspath(file_path),
str(int(stat_info.st_mtime_ns)),
str(int(stat_info.st_size)),
f"skip_anim={int(bool(skip_animations))}",
f"flatten_nodes={int(bool(flatten_nodes))}",
f"builder={builder}",
"visual_cache_v=3",
]
).encode("utf-8", errors="ignore")
).hexdigest()
return digest
def get_gltf_visual_bam_path(
file_path: str,
*,
project_root: str = "",
skip_animations: bool = True,
flatten_nodes: bool = False,
) -> str:
if not is_gltf_path(file_path):
return file_path
file_path = _to_os_specific_path(file_path)
if not file_path or not os.path.exists(file_path):
return file_path
builder = _choose_visual_cache_builder(file_path, skip_animations=skip_animations)
cache_key = _build_cache_key(
file_path,
skip_animations=skip_animations,
flatten_nodes=flatten_nodes,
builder=builder,
)
cache_root = _resolve_cache_root(project_root)
cache_dir = os.path.join(cache_root, cache_key)
os.makedirs(cache_dir, exist_ok=True)
return os.path.join(cache_dir, "visual_scene.bam")
def ensure_gltf_visual_bam(
file_path: str,
*,
project_root: str = "",
skip_animations: bool = True,
flatten_nodes: bool = False,
) -> str:
if not is_gltf_path(file_path):
return file_path
file_path = _to_os_specific_path(file_path)
if not file_path or not os.path.exists(file_path):
return file_path
cache_bam = get_gltf_visual_bam_path(
file_path,
project_root=project_root,
skip_animations=skip_animations,
flatten_nodes=flatten_nodes,
)
if os.path.exists(cache_bam):
return cache_bam
temp_bam = f"{cache_bam}.tmp"
model_np = None
builder = _choose_visual_cache_builder(file_path, skip_animations=skip_animations)
try:
from panda3d.core import Filename
if builder == "fluent":
try:
from direct.showbase import ShowBaseGlobal
from panda3d.core import LoaderOptions
base = getattr(ShowBaseGlobal, "base", None)
if base and getattr(base, "loader", None):
loader_options = LoaderOptions()
if hasattr(loader_options, "LF_no_cache"):
loader_options.setFlags(loader_options.getFlags() | loader_options.LF_no_cache)
elif hasattr(loader_options, "LFNoCache"):
loader_options.setFlags(loader_options.getFlags() | loader_options.LFNoCache)
model_np = load_model_fluent(
base.loader,
Filename.from_os_specific(file_path),
loader_options,
)
if model_np and not model_np.is_empty():
print(f"[glTF缓存] 静态glTF使用流畅加载器构建可见BAM: {file_path}")
if not model_np or model_np.is_empty():
builder = "gltf"
except Exception as exc:
print(f"[glTF缓存] 流畅模式构建BAM失败回退 gltf: {exc}")
builder = "gltf"
if builder == "gltf":
import gltf
from panda3d.core import NodePath
settings = gltf.GltfSettings(
skip_animations=bool(skip_animations),
flatten_nodes=bool(flatten_nodes),
)
model_root = gltf.load_model(os.path.abspath(file_path), settings)
if not model_root:
return file_path
model_np = NodePath(model_root)
if model_np.is_empty():
return file_path
temp_filename = Filename.from_os_specific(temp_bam)
if not model_np.write_bam_file(temp_filename):
return file_path
os.replace(temp_bam, cache_bam)
return cache_bam
except Exception as exc:
print(f"[glTF缓存] 构建可见BAM缓存失败: {exc}")
return file_path
finally:
try:
if model_np and not model_np.isEmpty():
model_np.removeNode()
except Exception:
pass
try:
if os.path.exists(temp_bam):
os.remove(temp_bam)
except Exception:
pass
def load_model_fluent(loader, file_path, *args, **kwargs):
"""
加载 glTF/glb 模型时临时禁用 panda3d-gltf 插件
强制 Panda3D 使用下一个可用的加载器 Assimp 或自带轻量加载器
这对于静态大场景能显著提高加载后的流畅度
"""
from panda3d.core import ConfigVariableList
cvl = ConfigVariableList("load-file-type")
removed_entries = []
# 扫描并移除所有涉及 panda3d-gltf 的注册项
i = 0
while i < cvl.get_num_values():
val = cvl.get_string_value(i)
if "panda3d-gltf" in val.lower():
removed_entries.append(val)
cvl.remove_value(i)
else:
i += 1
try:
# 调用实际加载器
print(f"[glTF智能加载] 正在以流畅模式加载 (跳过 panda3d-gltf): {file_path}")
return loader.loadModel(file_path, *args, **kwargs)
finally:
# 恢复注册项,以便后续需要动画功能的模型仍能使用它
for entry in removed_entries:
cvl.add_value(entry)

File diff suppressed because it is too large Load Diff

View File

@ -1,523 +0,0 @@
"""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
from core.editor_context import get_editor_context
class SceneManagerConvertTilesMixin:
def _get_tree_widget(self):
"""安全获取树形控件"""
return get_editor_context(self.world).get_tree_widget()
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转换"""
class _ConsoleProgress:
def __init__(self, label):
self._value = 0
self._label = label
def setWindowTitle(self, title):
print(f"[GLB转换] {title}")
def setWindowModality(self, _):
return None
def show(self):
print(f"[GLB转换] {self._label}")
def hide(self):
print("[GLB转换] 完成")
def setValue(self, value):
value = int(value)
if value != self._value:
self._value = value
print(f"[GLB转换] 进度: {self._value}%")
def setLabelText(self, text):
if text != self._label:
self._label = text
print(f"[GLB转换] {self._label}")
progress = _ConsoleProgress("正在转换模型格式以获得更好的动画支持...")
progress.setWindowTitle("模型格式转换")
progress.show()
try:
result = self._convertToGLB(filepath, progress)
progress.hide()
return result
except Exception as e:
progress.hide()
print(f"转换过程出错: {e}")
return self._convertToGLB(filepath)
def _convertToGLB(self, filepath, progress=None):
"""将模型文件转换为GLB格式"""
try:
print(f"[GLB转换] 开始转换: {filepath}")
if progress:
progress.setValue(10)
progress.setLabelText("准备转换...")
# 准备输出路径
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 转换...")
# 方法1: 使用 Blender 进行转换
if self._convertWithBlender(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(60)
progress.setLabelText("尝试 FBX2glTF 转换...")
# 方法2: 使用 FBX2glTF (如果可用)
if self._convertWithFBX2glTF(filepath, glb_path, progress):
return glb_path
if progress:
progress.setValue(80)
progress.setLabelText("尝试 Assimp 转换...")
# 方法3: 使用 Assimp
if self._convertWithAssimp(filepath, glb_path, progress):
return glb_path
#print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换")
return None
except Exception as e:
print(f"[GLB转换] 转换过程出错: {e}")
return None
def _convertWithBlender(self, input_path, output_path, progress=None):
"""使用 Blender 进行转换"""
try:
import subprocess
import tempfile
print(f"[Blender转换] {input_path}{output_path}")
# 创建 Blender 脚本
script_content = f'''
import bpy
import sys
import os
# 清理默认场景
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
print("开始导入文件...")
# 根据文件类型选择导入方法
input_file = "{input_path}"
output_file = "{output_path}"
try:
ext = os.path.splitext(input_file)[1].lower()
if ext == '.fbx':
bpy.ops.import_scene.fbx(filepath=input_file)
elif ext == '.obj':
bpy.ops.import_scene.obj(filepath=input_file)
elif ext == '.dae':
bpy.ops.wm.collada_import(filepath=input_file)
elif ext == '.blend':
bpy.ops.wm.open_mainfile(filepath=input_file)
else:
print(f"不支持的格式: {{ext}}")
sys.exit(1)
print("导入成功开始导出GLB...")
# 导出为 GLB保留动画
bpy.ops.export_scene.gltf(
filepath=output_file,
export_format='GLB',
export_animations=True,
export_force_sampling=True,
export_frame_range=True,
export_current_frame=False,
export_skins=True,
export_morph=True,
export_lights=True,
export_cameras=False
)
print("GLB导出成功")
except Exception as e:
print(f"转换失败: {{e}}")
sys.exit(1)
'''
# 写入临时脚本文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
temp_file.write(script_content)
script_path = temp_file.name
try:
# 执行 Blender 转换
result = subprocess.run([
'blender', '--background', '--python', script_path
], capture_output=True, text=True, timeout=180)
# 清理临时文件
os.unlink(script_path)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[Blender转换] 转换成功")
return True
else:
print(f"[Blender转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[Blender转换] 转换超时")
return False
except FileNotFoundError:
print(f"[Blender转换] Blender 未安装")
return False
except Exception as e:
print(f"[Blender转换] 转换过程出错: {e}")
return False
def _convertWithFBX2glTF(self, input_path, output_path, progress=None):
"""使用 FBX2glTF 进行转换仅支持FBX"""
try:
import subprocess
if not input_path.lower().endswith('.fbx'):
return False
print(f"[FBX2glTF转换] {input_path}{output_path}")
# 使用 FBX2glTF 转换
result = subprocess.run([
'FBX2glTF', input_path, '--output', output_path, '--binary'
], capture_output=True, text=True, timeout=120)
if result.returncode == 0 and os.path.exists(output_path):
print(f"[FBX2glTF转换] 转换成功")
return True
else:
print(f"[FBX2glTF转换] 转换失败: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f"[FBX2glTF转换] 转换超时")
return False
except FileNotFoundError:
print(f"[FBX2glTF转换] FBX2glTF 未安装")
return False
except Exception as e:
print(f"[FBX2glTF转换] 转换过程出错: {e}")
return False
def _convertWithAssimp(self, input_path, output_path, progress=None):
"""使用 PyAssimp 进行转换"""
try:
import pyassimp
print(f"[PyAssimp转换] {input_path}{output_path}")
# 加载模型
scene = pyassimp.load(input_path)
if not scene:
print(f"[PyAssimp转换] 加载模型失败")
return False
if progress:
progress.setValue(30)
# 导出为GLB格式
pyassimp.export(scene, output_path, "glb2")
if progress:
progress.setValue(80)
# 释放资源
pyassimp.release(scene)
if os.path.exists(output_path):
print(f"[PyAssimp转换] 转换成功")
return True
else:
print(f"[PyAssimp转换] 转换失败: 输出文件未生成")
return False
except ImportError:
print(f"[PyAssimp转换] PyAssimp 未安装")
return False
except Exception as e:
print(f"[PyAssimp转换] 转换过程出错: {e}")
return False
def _check_async_task(self, panda3d_task):
# 检查 asyncio 任务是否完成
if hasattr(self, '_current_asyncio_task'):
if self._current_asyncio_task.done():
try:
self._current_asyncio_task.result()
except Exception as e:
print(f"异步任务出错:{e}")
# 返回 Panda3D 任务管理器的完成状态
return panda3d_task.done # 注意是 done 而不是 DONE
# 返回 Panda3D 任务管理器的继续状态
return panda3d_task.cont # 注意是 cont 而不是 CONTINUE
def _parse_tileset(self,tileset_data,tileset_info):
try:
root = tileset_data.get('root',{})
self._parse_tile(root,tileset_info['node'],tileset_info)
print("✓ Tileset 解析完成")
except Exception as e:
print(f"✗ Tileset 解析出错: {e}")
def _parse_tile(self, tile_data, parent_node, tileset_info):
try:
# 获取tileID
tile_id = f"tile_{len(tileset_info['tiles'])}"
print(f"创建tile节点: {tile_id}")
# 创建tile节点
tile_node = parent_node.attachNewNode(tile_id)
tileset_info['tiles'][tile_id] = {
'node': tile_node,
'data': tile_data,
'loaded': False
}
# 如果有内容,创建占位几何体
if 'content' in tile_data:
print(f"为tile {tile_id} 创建几何体")
self._create_tile_geometry(tile_node)
# 递归解析子tiles
children = tile_data.get('children', [])
print(f"Tile {tile_id}{len(children)} 个子节点")
for child_data in children:
self._parse_tile(child_data, tile_node, tileset_info)
except Exception as e:
print(f"✗ Tile 解析出错: {e}")
import traceback
traceback.print_exc()
def _create_tile_geometry(self,parent_node):
"""为 tile 创建占位几何体"""
try:
# 创建一个简单的立方体作为占位符
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('tile_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
vertices = [
(-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5)
]
for vert in vertices:
vertex.addData3f(*vert)
normal.addData3f(0, 0, 1)
color.addData4f(0.2, 0.6, 0.8, 1.0)
# 创建几何体
geom = Geom(vdata)
# 创建面
prim = GeomTriangles(Geom.UHStatic)
# 底面
prim.addVertices(0, 1, 2)
prim.addVertices(0, 2, 3)
# 顶面
prim.addVertices(4, 7, 6)
prim.addVertices(4, 6, 5)
# 前面
prim.addVertices(0, 4, 5)
prim.addVertices(0, 5, 1)
# 后面
prim.addVertices(2, 6, 7)
prim.addVertices(2, 7, 3)
# 左面
prim.addVertices(0, 3, 7)
prim.addVertices(0, 7, 4)
# 右面
prim.addVertices(1, 5, 6)
prim.addVertices(1, 6, 2)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tile_geometry')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(1000) # 放大以便观察
# 添加材质
material = Material()
material.setBaseColor((0.2, 0.6, 0.8, 1.0))
material.setSpecular((0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
cube_node.setMaterial(material)
except Exception as e:
print(f"✗ 创建 tile 几何体出错: {e}")
def _create_placeholder_geometry(self, parent_node):
"""创建一个简单的占位符几何体,让用户能看到节点"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
# 创建简单的立方体作为占位符
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
size = 1.0
vertices = [
# 前面 (Z+)
(-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size),
# 后面 (Z-)
(-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size),
# 左面 (X-)
(-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size),
# 右面 (X+)
(size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size),
# 上面 (Y+)
(-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size),
# 下面 (Y-)
(-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size)
]
normals = [
# 前面法线
(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
# 后面法线
(0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
# 左面法线
(-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0),
# 右面法线
(1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0),
# 上面法线
(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0),
# 下面法线
(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0)
]
# 青色
face_colors = [
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色
(0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色
(0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色
(0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色
(0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色
(0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色
]
for i, vert in enumerate(vertices):
vertex.addData3f(*vert)
normal.addData3f(*normals[i])
color.addData4f(*face_colors[i])
# 创建几何体
geom = Geom(vdata)
# 创建面(每个面两个三角形)
prim = GeomTriangles(Geom.UHStatic)
# 每个面4个顶点生成2个三角形
for face in range(6): # 6个面
base_index = face * 4
# 第一个三角形
prim.addVertices(base_index, base_index + 1, base_index + 2)
# 第二个三角形
prim.addVertices(base_index, base_index + 2, base_index + 3)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tileset_placeholder')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(5) # 设置合适的大小
# 设置双面渲染
cube_node.setTwoSided(True)
# 添加材质
material = Material()
material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
cube_node.setMaterial(material)
# 添加标识标签
cube_node.setTag("element_type", "cesium_placeholder")
print("✓ 占位符几何体创建完成")
return cube_node
except Exception as e:
print(f"✗ 创建占位符几何体出错: {e}")
import traceback
traceback.print_exc()
return None

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,459 +0,0 @@
"""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)
spotlight_node.setPythonTag("engine_light_registered", True)
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.setPythonTag("engine_light_registered", True)
# 设置聚光灯方向(向下照射)
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)
pointlight_node.setPythonTag("engine_light_registered", True)
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")
pointlight_node.setPythonTag("engine_light_registered", True)
# 添加到光源列表
self.Pointlight.append(pointlight_node)
# 启用光源
self.world.render.setLight(pointlight_node)
# 启用阴影
if hasattr(pointlight, 'setShadowCaster'):
pointlight.setShadowCaster(True)
print(f"✓ 标准点光源创建成功,位置: {pos}")
return pointlight_node
except Exception as e:
print(f"✗ 创建点光源失败: {e}")
return None

File diff suppressed because it is too large Load Diff

View File

@ -1,873 +0,0 @@
"""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
from core.editor_context import get_editor_context
class SceneManagerSerializationMixin:
def _get_editor_context(self):
return get_editor_context(self.world)
def _get_tree_widget(self):
return self._get_editor_context().get_tree_widget()
def _get_gui_manager(self):
return self._get_editor_context().get_gui_manager()
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:
tree_widget = self._get_tree_widget()
if tree_widget:
# 查找父节点在场景树中的对应项
parent_item = self._findTreeItemForNode(parent_node)
if parent_item:
# 添加新节点到场景树
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:
tree_widget = self._get_tree_widget()
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
gui_manager = self._get_gui_manager()
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}")
if gui_manager and hasattr(gui_manager, 'createGUI3DImage'):
new_gui_element = gui_manager.createGUI3DImage(pos, image_path, scale)
elif gui_type == "video_screen" and gui_manager and hasattr(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 = gui_manager.createVideoScreen(pos,scale,video_path)
elif gui_type == "2d_video_screen" and gui_manager and hasattr(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 = gui_manager.createGUI2DVideoScreen(pos,scale,video_path)
if new_gui_element:
# 设置名称和变换
if hasattr(new_gui_element, 'setName'):
new_gui_element.setName(name)
# 设置位置、旋转、缩放
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
if hasattr(new_gui_element, 'setPos'):
new_gui_element.setPos(*pos)
if hasattr(new_gui_element, 'setHpr'):
new_gui_element.setHpr(*hpr)
if hasattr(new_gui_element, 'setScale'):
new_gui_element.setScale(*scale)
# 恢复文本内容
if 'text' in gui_data and hasattr(new_gui_element, 'setText'):
new_gui_element.setText(gui_data['text'])
# 恢复其他标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']:
new_gui_element.setTag(tag_key, str(tag_value))
print(f"GUI元素重建成功: {name}")
return new_gui_element
except Exception as e:
print(f"重建GUI元素失败: {e}")
import traceback
traceback.print_exc()
return None
def _recreateModelFromData(self, node_data, parent_node, name):
"""根据数据重建模型,保持材质"""
try:
model_data = node_data.get('model_data', {})
model_path = model_data.get('model_path', model_data.get('file', ''))
if not model_path or not os.path.exists(model_path):
# 如果原始模型文件不存在,创建一个基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
# 导入模型,保持原有参数
model = self.importModel(
model_path,
apply_unit_conversion=False, # 已经处理过的模型不需要再次转换
normalize_scales=False, # 保持原有缩放
auto_convert_to_glb=False # 已经处理过的模型不需要再次转换
)
if model:
# 设置名称
model.setName(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
model.setPos(*pos)
model.setHpr(*hpr)
model.setScale(*scale)
# 恢复材质和纹理信息
try:
self._restoreModelMaterial(model, model_data)
except Exception as e:
print(f"恢复模型材质时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
model.setTag(tag_key, str(tag_value))
# 添加到模型列表
if model not in self.models:
self.models.append(model)
return model
except Exception as e:
print(f"重建模型失败: {e}")
# 出错时创建基本节点
return self._createBasicNodeFromData(node_data, parent_node, name)
def _restoreModelTextures(self, model, texture_data):
"""恢复模型纹理"""
try:
if not texture_data or 'textures' not in texture_data:
return
from panda3d.core import TextureStage, SamplerState
textures_info = texture_data['textures']
# 为每个纹理阶段恢复纹理
for stage_name, texture_info in textures_info.items():
# 创建纹理阶段
stage = TextureStage(stage_name)
stage.setMode(texture_info.get('stage_mode', TextureStage.M_modulate))
# 恢复纹理阶段排序
stage.setSort(texture_info.get('stage_sort', 0)) # 默认为0p3d_Texture0
# 加载纹理
texture_path = texture_info['texture_path']
if texture_path and os.path.exists(texture_path):
texture = self.world.loader.loadTexture(texture_path)
if texture:
# 设置纹理属性
texture.setWrapU(texture_info.get('wrap_u', SamplerState.WM_repeat))
texture.setWrapV(texture_info.get('wrap_v', SamplerState.WM_repeat))
texture.setMinfilter(texture_info.get('minfilter', SamplerState.FT_linear))
texture.setMagfilter(texture_info.get('magfilter', SamplerState.FT_linear))
texture.setAnisotropicDegree(texture_info.get('anisotropic_degree', 1))
# 应用纹理到模型
model.setTexture(stage, texture, 1) # 1 表示强制应用
# 恢复颜色比例和偏移(使用安全的方法)
if 'color_scale' in texture_info:
try:
model.setTextureScale(stage, *texture_info['color_scale'])
except Exception as e:
print(f"恢复纹理比例失败: {e}")
if 'color_offset' in texture_info:
try:
model.setTextureOffset(stage, *texture_info['color_offset'])
except Exception as e:
print(f"恢复纹理偏移失败: {e}")
print(f"恢复纹理: {stage_name} <- {texture_path}")
else:
print(f"纹理文件不存在或路径为空: {texture_path}")
except Exception as e:
print(f"恢复模型纹理时出错: {e}")
def _restoreModelMaterial(self, model, model_data):
"""恢复模型材质和纹理"""
try:
# 恢复基础颜色
if 'base_color' in model_data:
from panda3d.core import ColorAttrib
base_color = model_data['base_color']
color = (base_color[0], base_color[1], base_color[2], base_color[3])
model.setColor(color)
# 恢复复杂材质属性
if any(key.startswith('material_') for key in model_data.keys()):
from panda3d.core import Material
# 创建新材质或获取现有材质
material = Material()
# 恢复基础颜色
if 'material_base_color' in model_data:
base_color = model_data['material_base_color']
material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3]))
# 恢复环境光颜色
if 'material_ambient_color' in model_data:
ambient_color = model_data['material_ambient_color']
material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]))
# 恢复漫反射颜色
if 'material_diffuse_color' in model_data:
diffuse_color = model_data['material_diffuse_color']
material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]))
# 恢复高光颜色
if 'material_specular_color' in model_data:
specular_color = model_data['material_specular_color']
material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3]))
# 恢复自发光颜色
if 'material_emission_color' in model_data:
emission_color = model_data['material_emission_color']
material.setEmission((emission_color[0], emission_color[1], emission_color[2], emission_color[3]))
# 恢复粗糙度和金属度
if 'material_roughness' in model_data:
material.setRoughness(model_data['material_roughness'])
if 'material_metallic' in model_data:
material.setMetallic(model_data['material_metallic'])
# 恢复光泽度
if 'material_shininess' in model_data:
material.setShininess(model_data['material_shininess'])
# 应用材质到模型
model.setMaterial(material)
# 恢复透明度设置
if 'transparency_mode' in model_data:
from panda3d.core import TransparencyAttrib
transparency_mode = model_data['transparency_mode']
model.setTransparency(transparency_mode)
# 恢复纹理信息
if 'texture_data' in model_data:
self._restoreModelTextures(model, model_data['texture_data'])
except Exception as e:
print(f"恢复材质失败: {e}")
def _createBasicNodeFromData(self, node_data, parent_node, name):
"""创建基本节点,保持视觉属性"""
try:
new_node = parent_node.attachNewNode(name)
# 设置变换
pos = node_data.get('pos', (0, 0, 0))
hpr = node_data.get('hpr', (0, 0, 0))
scale = node_data.get('scale', (1, 1, 1))
new_node.setPos(*pos)
new_node.setHpr(*hpr)
new_node.setScale(*scale)
# 恢复视觉属性
try:
# 恢复颜色
if 'color' in node_data:
color_data = node_data['color']
new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3])
# 恢复材质
if 'material' in node_data:
from panda3d.core import Material
material_data = node_data['material']
material = Material()
if 'base_color' in material_data:
bc = material_data['base_color']
material.setBaseColor((bc[0], bc[1], bc[2], bc[3]))
if 'ambient' in material_data:
ac = material_data['ambient']
material.setAmbient((ac[0], ac[1], ac[2], ac[3]))
if 'diffuse' in material_data:
dc = material_data['diffuse']
material.setDiffuse((dc[0], dc[1], dc[2], dc[3]))
if 'specular' in material_data:
sc = material_data['specular']
material.setSpecular((sc[0], sc[1], sc[2], sc[3]))
if 'shininess' in material_data:
material.setShininess(material_data['shininess'])
new_node.setMaterial(material)
except Exception as e:
print(f"恢复视觉属性时出错: {e}")
# 恢复标签
for tag_key, tag_value in node_data.get('tags', {}).items():
if tag_key not in ['name']:
new_node.setTag(tag_key, str(tag_value))
return new_node
except Exception as e:
print(f"创建基本节点失败: {e}")
return None
def _generateUniqueName(self, base_name, parent_node):
"""生成唯一节点名称"""
try:
# 移除可能的数字后缀
import re
import time
name_base = re.sub(r'_\d+$', '', base_name)
# 查找现有同名节点
counter = 1
unique_name = base_name
while True:
# 检查父节点下是否已存在同名子节点
existing_node = parent_node.find(unique_name)
if existing_node.isEmpty():
break
unique_name = f"{name_base}_{counter}"
counter += 1
if counter > 1000: # 防止无限循环
break
return unique_name
except Exception as e:
print(f"生成唯一名称时出错: {e}")
return f"{base_name}_{int(time.time())}"

View File

@ -1,5 +0,0 @@
"""Shared tree data-role constants."""
# Keep this value stable for legacy tree-item data payloads.
TREE_USER_ROLE = 256

View File

@ -22,17 +22,16 @@ class CrossPlatformPathHandler:
print(f"路径处理器初始化 - 系统: {self.system}")
def normalize_model_path(self, filepath):
"""标准化模型文件路径"""
try:
def normalize_model_path(self, filepath):
"""标准化模型文件路径"""
try:
#print(f"\n=== 路径标准化处理 ===")
#print(f"原始路径: {filepath}")
#print(f"当前系统: {self.system}")
# 步骤1: 检查原始路径是否存在
if self._check_file_exists(filepath):
existing_path = self._to_os_specific_existing_path(filepath) or filepath
return self._panda3d_normalize(existing_path)
# 步骤1: 检查原始路径是否存在
if self._check_file_exists(filepath):
return self._panda3d_normalize(filepath)
# 步骤2: 路径修复尝试
fixed_path = self._attempt_path_fixes(filepath)
@ -52,78 +51,21 @@ class CrossPlatformPathHandler:
print(f"❌ 路径标准化失败: {e}")
return filepath
def _check_file_exists(self, filepath):
"""检查文件是否存在"""
try:
if filepath and os.path.exists(filepath):
return True
except Exception:
pass
try:
os_path = self._to_os_specific_existing_path(filepath)
if os_path and os.path.exists(os_path):
return True
except Exception:
pass
return False
def _to_os_specific_existing_path(self, filepath):
"""将 Panda 风格路径转换为当前系统下真实存在的路径。"""
path_text = os.fspath(filepath or "")
if not path_text:
return ""
if os.path.exists(path_text):
return os.path.normpath(path_text)
try:
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(path_text).to_os_specific()
if candidate and os.path.exists(candidate):
return os.path.normpath(candidate)
except Exception:
continue
candidate = Filename(path_text).to_os_specific()
if candidate and os.path.exists(candidate):
return os.path.normpath(candidate)
except Exception:
pass
if len(path_text) >= 3 and path_text[0] in ("/", "\\") and path_text[1].isalpha() and path_text[2] in ("/", "\\"):
drive_path = f"{path_text[1].upper()}:{path_text[2:]}"
drive_path = os.path.normpath(drive_path)
if os.path.exists(drive_path):
return drive_path
return ""
def _check_file_exists(self, filepath):
"""检查文件是否存在"""
exists = os.path.exists(filepath)
return exists
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 _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 _attempt_path_fixes(self, filepath):
"""尝试各种路径修复方法"""
@ -351,4 +293,4 @@ def normalize_model_path(filepath):
def suggest_file_placement(filename):
"""便捷函数:建议文件放置位置"""
return path_handler.suggest_file_placement(filename)
return path_handler.suggest_file_placement(filename)

View File

@ -1,2 +0,0 @@
"""SSBO runtime helpers and editor components."""

View File

@ -1,410 +0,0 @@
import os
from panda3d.core import Filename, GeomNode, Material, MaterialAttrib, NodePath
from .ssbo_controller import ObjectController
class RuntimeSSBOSceneImporter:
"""Minimal runtime-only SSBO scene importer without editor UI dependencies."""
def __init__(self, base_app):
self.base = base_app
self.controller = None
self.model = None
self.source_model = None
self.source_model_root = None
self.static_scene_root = None
self.interactive_scene_root = None
self.interactive_nodes = {}
def load_scene(self, scene_path, interactive_root_names=None):
source_model = self._load_source_model_from_path(scene_path)
self._clear_runtime_state()
source_root = self._ensure_source_model_root()
imported_root = source_model.copyTo(source_root)
self._set_node_name(imported_root, os.path.basename(scene_path) or "scene.bam")
self.source_model = imported_root
interactive_root_names = {str(name).strip() for name in (interactive_root_names or []) if str(name).strip()}
top_level_children = self._get_scene_top_level_children(imported_root)
if interactive_root_names and top_level_children:
self._build_release_split_scene(top_level_children, interactive_root_names)
return self.interactive_scene_root or self.static_scene_root
working_holder = NodePath("ssbo_source_scene_work")
working_root = source_root.copyTo(working_holder)
self.controller = ObjectController()
self.controller.bake_ids_and_collect(working_root)
self.model = self.controller.model
self.model.reparentTo(self.base.render)
try:
if not working_holder.isEmpty():
working_holder.removeNode()
except Exception:
pass
return self.model
def load_split_scene(self, *, static_scene_path="", interactive_scene_path="", interactive_root_names=None):
self._clear_runtime_state()
interactive_root_names = {str(name).strip() for name in (interactive_root_names or []) if str(name).strip()}
runtime_root = NodePath("runtime_split_scene")
loaded_any = False
if static_scene_path and os.path.exists(static_scene_path):
static_source = self._load_source_model_from_path(static_scene_path)
static_holder = NodePath("ssbo_static_scene_work")
static_root = static_source.copyTo(static_holder)
self.controller = ObjectController()
self.controller.bake_ids_and_collect(static_root)
self.model = self.controller.model
if self.model and not self.model.isEmpty():
self.model.reparentTo(runtime_root)
self.static_scene_root = self.model
loaded_any = True
try:
if not static_holder.isEmpty():
static_holder.removeNode()
except Exception:
pass
if interactive_scene_path and os.path.exists(interactive_scene_path):
interactive_source = self._load_source_model_from_path(interactive_scene_path)
interactive_root = interactive_source.copyTo(runtime_root)
self.interactive_scene_root = interactive_root
loaded_any = True
for child in self._get_scene_top_level_children(interactive_root):
child_name = self._get_node_name(child, "")
if not child_name:
continue
if interactive_root_names and child_name not in interactive_root_names:
continue
self.interactive_nodes[child_name] = child
if not loaded_any:
try:
if not runtime_root.isEmpty():
runtime_root.removeNode()
except Exception:
pass
raise RuntimeError("Failed to load split runtime scene")
runtime_root.reparentTo(self.base.render)
print(
f"[RuntimeSSBO] 已加载分层 cooked 场景: 静态={'yes' if self.static_scene_root else 'no'}, "
f"交互对象={len(self.interactive_nodes)}"
)
return runtime_root
def _clear_runtime_state(self):
for node in (self.model, self.static_scene_root, self.interactive_scene_root, self.source_model_root):
try:
if node and not node.isEmpty():
node.removeNode()
except Exception:
continue
self.controller = None
self.model = None
self.source_model = None
self.source_model_root = None
self.static_scene_root = None
self.interactive_scene_root = None
self.interactive_nodes = {}
def get_runtime_node_for_name(self, node_name):
return self.interactive_nodes.get(str(node_name or "").strip())
def _get_scene_top_level_children(self, imported_root):
children = []
try:
for child in imported_root.getChildren():
if child and not child.isEmpty():
children.append(child)
except Exception:
children = []
if children:
return children
return [imported_root] if imported_root and not imported_root.isEmpty() else []
def _build_release_split_scene(self, top_level_children, interactive_root_names):
static_holder = NodePath("runtime_static_scene")
interactive_holder = NodePath("runtime_interactive_scene")
interactive_count = 0
static_count = 0
for child in top_level_children:
child_name = self._get_node_name(child, "")
if child_name in interactive_root_names:
runtime_child = child.copyTo(interactive_holder)
self._set_node_name(runtime_child, child_name)
self.interactive_nodes[child_name] = runtime_child
interactive_count += 1
else:
child.copyTo(static_holder)
static_count += 1
if static_count:
self.static_scene_root = static_holder
self.static_scene_root.reparentTo(self.base.render)
try:
self.static_scene_root.flattenStrong()
except Exception as e:
print(f"静态场景合批失败,继续使用未合批结果: {e}")
else:
try:
if not static_holder.isEmpty():
static_holder.removeNode()
except Exception:
pass
if interactive_count:
self.interactive_scene_root = interactive_holder
self.interactive_scene_root.reparentTo(self.base.render)
else:
try:
if not interactive_holder.isEmpty():
interactive_holder.removeNode()
except Exception:
pass
print(f"[RuntimeSSBO] 运行时分层导入完成: 交互对象 {interactive_count} 个, 静态合批对象 {static_count}")
def _load_source_model_from_path(self, model_path):
source_model = None
last_error = None
for fn in self._build_filename_candidates(model_path):
try:
source_model = self.base.loader.loadModel(fn)
if source_model and not source_model.isEmpty():
break
except Exception as e:
last_error = e
source_model = None
if not source_model or source_model.isEmpty():
if last_error:
raise RuntimeError(f"Failed to load model '{model_path}': {last_error}")
raise RuntimeError(f"Failed to load model '{model_path}'")
self._fix_black_materials(source_model)
self._repair_missing_textures(source_model, model_path)
return source_model
def _build_filename_candidates(self, path_text):
candidates = []
seen = set()
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
ctor = getattr(Filename, ctor_name, None)
if not ctor:
continue
try:
fn = ctor(path_text)
key = fn.getFullpath() if hasattr(fn, "getFullpath") else fn.get_fullpath()
if key in seen:
continue
seen.add(key)
candidates.append(fn)
except Exception:
continue
if not candidates:
try:
candidates.append(Filename(path_text))
except Exception:
pass
return candidates
def _ensure_source_model_root(self):
root = self.source_model_root
if root and not root.isEmpty():
return root
self.source_model_root = NodePath("ssbo_source_scene_root")
return self.source_model_root
def _set_node_name(self, node, name):
if not node:
return
try:
node.set_name(name)
return
except Exception:
pass
try:
node.setName(name)
except Exception:
pass
def _get_node_name(self, node, default_name=None):
if not node:
return default_name
try:
return node.get_name()
except Exception:
pass
try:
return node.getName()
except Exception:
return default_name
def _fix_black_materials(self, model):
try:
for geom_path in model.findAllMatches("**/+GeomNode"):
geom_node = geom_path.node()
node_state = geom_path.getState()
if node_state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType())
mat = mat_attrib.getMaterial()
if mat and self._is_dark_material(mat):
new_mat = Material(mat)
new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0))
new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0))
geom_path.setState(node_state.setAttrib(MaterialAttrib.make(new_mat)))
for i in range(geom_node.getNumGeoms()):
geom_state = geom_node.getGeomState(i)
if geom_state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = geom_state.getAttrib(MaterialAttrib.getClassType())
mat = mat_attrib.getMaterial()
if mat and self._is_dark_material(mat):
new_mat = Material(mat)
new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0))
new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0))
geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(new_mat)))
else:
new_mat = Material()
new_mat.setBaseColor((0.8, 0.8, 0.8, 1.0))
new_mat.setDiffuse((0.8, 0.8, 0.8, 1.0))
new_mat.setSpecular((0.2, 0.2, 0.2, 1.0))
new_mat.setRoughness(0.8)
geom_node.setGeomState(i, geom_state.addAttrib(MaterialAttrib.make(new_mat)))
model.clearColor()
except Exception as e:
print(f"修复黑色模型材质时出错: {e}")
def _is_dark_material(self, material):
try:
if material.hasBaseColor():
c = material.getBaseColor()
elif material.hasDiffuse():
c = material.getDiffuse()
else:
return True
return c.x <= 0.05 and c.y <= 0.05 and c.z <= 0.05
except Exception:
return False
def _repair_missing_textures(self, model_np, model_path):
if not model_np or model_np.isEmpty():
return
search_dirs = self._build_texture_search_dirs(model_path)
texture_index = self._index_texture_files(search_dirs)
for node in [model_np] + list(model_np.findAllMatches("**")):
if not node or node.isEmpty():
continue
try:
stages = node.findAllTextureStages()
stage_count = stages.getNumTextureStages()
stage_at = stages.getTextureStage
except Exception:
continue
for i in range(stage_count):
stage = stage_at(i)
if not stage:
continue
try:
tex = node.getTexture(stage)
except Exception:
tex = None
if not tex or self._texture_is_valid(tex):
continue
basename = self._extract_texture_basename(tex)
if not basename:
continue
replacement = texture_index.get(basename.lower())
if replacement:
new_tex = self._load_texture_from_path(replacement)
if new_tex:
try:
node.setTexture(stage, new_tex, 1)
continue
except Exception:
pass
try:
node.clearTexture(stage)
except Exception:
pass
def _build_texture_search_dirs(self, model_path):
dirs = []
model_dir = os.path.dirname(os.path.abspath(model_path))
project_root = getattr(self.base, "project_path", "") or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def add_dir(path):
if path and os.path.isdir(path):
normalized = os.path.normpath(path)
if normalized not in dirs:
dirs.append(normalized)
add_dir(model_dir)
for sub in ("textures", "texture", "tex", "assets", "materials"):
add_dir(os.path.join(model_dir, sub))
add_dir(os.path.join(project_root, "resources", sub))
add_dir(os.path.join(project_root, "resources"))
return dirs
def _index_texture_files(self, dirs, limit=30000):
texture_exts = {".png", ".jpg", ".jpeg", ".tga", ".bmp", ".dds", ".ktx", ".ktx2", ".webp"}
index = {}
scanned = 0
for root_dir in dirs:
try:
for root, _, files in os.walk(root_dir):
for filename in files:
if os.path.splitext(filename)[1].lower() not in texture_exts:
continue
index.setdefault(filename.lower(), os.path.join(root, filename))
scanned += 1
if scanned >= limit:
return index
except Exception:
continue
return index
def _load_texture_from_path(self, texture_path):
for fn in self._build_filename_candidates(texture_path):
try:
tex = self.base.loader.loadTexture(fn)
if tex:
return tex
except Exception:
continue
return None
def _texture_is_valid(self, texture):
try:
x_size = texture.getXSize()
y_size = texture.getYSize()
return bool(x_size and y_size)
except Exception:
return False
def _extract_texture_basename(self, texture):
try:
path = texture.getFilename().toOsSpecific()
except Exception:
path = ""
if not path:
return ""
return os.path.basename(path)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More