This commit is contained in:
Rowland 2026-01-20 09:19:55 +08:00
parent 35cecd6008
commit 0860182b11
12 changed files with 1167 additions and 1210 deletions

View File

@ -1,446 +0,0 @@
VR Manager 模块化拆分计划
📊 现状分析
当前状态:
- 文件core/vr_manager.py
- 行数4736行严重超标标准为900行
- 方法数138个方法
- 问题:典型的"上帝类"反模式,承担了过多职责
主要职责识别:
1. 性能监控和调试 (~900行)
2. 测试模式系统 (~800行)
3. 对象池和优化 (~300行)
4. 姿态跟踪系统 (~900行)
5. 设备管理 (~300行)
6. 渲染缓冲管理 (~400行)
7. 相机系统 (~350行)
8. 合成器和提交 (~300行)
9. RenderPipeline集成 (~250行)
10. 核心生命周期 (~500行)
---
🎯 拆分策略
核心原则
1. 渐进迭代:每次拆分一个模块,立即验证
2. 组合模式新模块作为VRManager属性保持接口不变
3. 依赖顺序:先拆分独立模块,后拆分核心模块
4. 向后兼容所有公开API通过委托方法保持可用
拆分顺序(按依赖关系)
阶段1性能监控 → 最独立,零依赖
阶段2测试调试 → 依赖少,可独立测试
阶段3对象池优化 → 小而关键,性能核心
阶段4跟踪系统 → 中等复杂度
阶段5渲染系统 → 核心功能,最复杂
阶段6核心管理器 → 整合所有子系统
---
📋 详细拆分计划
🔷 阶段1拆分性能监控系统 (2-3小时)
创建文件core/vr/performance/monitoring.py (~900行)
迁移方法 (35个)
- 性能报告_print_performance_report, _print_performance_recommendations, _print_brief_performance_report
- 性能监控_init_performance_monitoring, _update_performance_metrics, _update_gpu_metrics, _track_frame_time
- GPU计时_get_gpu_frame_timing, enable_gpu_timing_monitoring, disable_gpu_timing_monitoring
- 管线统计_get_pipeline_stats, test_pipeline_monitoring, _start_timing, _end_timing
- 诊断工具_print_render_callback_diagnostics, _check_rendering_optimizations, _diagnose_opengl_state
- 调试控制enable_debug_output, disable_debug_output, set_debug_mode, toggle_debug_output, get_debug_status
- 配置方法set_performance_check_interval, set_frame_time_history_size, set_performance_report_interval
- 查询方法get_performance_stats, get_current_performance_summary, get_performance_monitoring_config
- 控制方法enable_performance_monitoring, disable_performance_monitoring, force_performance_report, reset_performance_counters
- 状态查询print_performance_monitoring_status, set_prediction_time
迁移属性 (~30个)
# 性能监控开关
performance_monitoring, debug_output_enabled, debug_mode
enable_pipeline_monitoring, enable_gpu_timing
# 性能数据
cpu_usage, memory_usage, gpu_usage, gpu_memory_usage
frame_times, max_frame_time_history
wait_poses_time, left_render_time, right_render_time, submit_time
total_frame_time, vr_sync_wait_time
# GPU计时
gpu_scene_render_ms, gpu_pre_submit_ms, gpu_post_submit_ms
gpu_total_render_ms, gpu_compositor_render_ms
gpu_timing_history, gpu_timing_failure_count
# 历史记录
wait_poses_times, render_times, submit_times, sync_wait_times
pipeline_history_size
类结构:
class VRPerformanceMonitor:
"""VR性能监控系统"""
def __init__(self, vr_manager):
self.vr_manager = vr_manager
# 初始化所有性能监控属性
VRManager集成
# __init__
self.performance_monitor = VRPerformanceMonitor(self)
# 委托方法保持API兼容
def enable_performance_monitoring(self):
return self.performance_monitor.enable_performance_monitoring()
验证步骤:
1. ✅ 编译检查python -m py_compile core/vr/performance/monitoring.py
2. ✅ 导入测试:启动应用,确认无导入错误
3. ✅ 功能测试:调用 vr_manager.enable_performance_monitoring()
4. ✅ 输出验证:检查性能报告正常生成
5. ✅ API测试验证所有委托方法可用
---
🔷 阶段2拆分测试调试系统 (2-3小时)
创建文件core/vr/testing/test_mode.py (~800行)
迁移方法 (17个)
- 测试模式enable_vr_test_mode, disable_vr_test_mode, switch_test_display_mode
- 纹理管理_ensure_test_mode_textures, _create_cached_ovr_textures, _batch_submit_textures
- 显示系统_initialize_test_display, _update_test_display, _create_stereo_display, _cleanup_test_display
- HUD系统_initialize_test_performance_hud, _update_test_performance_hud, _cleanup_test_performance_hud
- 状态查询get_test_mode_status, get_test_mode_features, set_test_mode_features
- 性能测试run_vr_performance_test
迁移属性 (~15个)
vr_test_mode, test_display_mode
test_display_quad, test_right_quad, stereo_display_created
test_performance_hud, test_performance_text
test_mode_initialized
hud_update_counter, hud_update_interval
test_mode_submit_texture, test_mode_wait_poses
类结构:
class VRTestMode:
"""VR测试模式系统"""
def __init__(self, vr_manager):
self.vr_manager = vr_manager
验证步骤:
1. ✅ 启用测试模式vr_manager.enable_vr_test_mode('stereo')
2. ✅ 检查显示验证测试quad显示正确
3. ✅ HUD验证检查性能HUD显示
4. ✅ 切换模式:测试 left/right/stereo 模式切换
5. ✅ 禁用测试vr_manager.disable_vr_test_mode()
---
🔷 阶段3拆分对象池和优化系统 (1-2小时)
创建文件core/vr/performance/optimization.py (~300行)
迁移方法 (19个)
- 对象池_initialize_object_pools, _get_pooled_matrix, _return_pooled_matrix
- GC控制_manual_gc_control, enable_gc_control, disable_gc_control, set_manual_gc_interval, force_manual_gc
- 分辨率set_resolution_scale, set_quality_preset, cycle_quality_preset, _apply_resolution_scale
- 查询方法get_object_pool_status, get_resolution_info, print_resolution_info
- 性能模式enable_performance_mode, disable_performance_mode, set_performance_mode_trigger_frame, get_performance_mode_status
迁移属性 (~20个)
# 对象池
_matrix_pool, _matrix_pool_size
_cached_matrices, _controller_poses_cache
_left_ovr_texture, _right_ovr_texture
# GC控制
_gc_control_enabled, _gc_disabled
_manual_gc_interval, _last_manual_gc_frame
# 分辨率
resolution_scale, base_eye_width, base_eye_height
scaled_eye_width, scaled_eye_height
quality_presets, current_quality_preset
# 性能模式
performance_mode_enabled, performance_mode_trigger_frame
验证步骤:
1. ✅ 对象池检查Mat4对象池正常工作
2. ✅ GC控制验证手动GC按预期触发
3. ✅ 分辨率:测试质量预设切换
4. ✅ 性能测试运行30秒性能测试确认优化生效
---
🔷 阶段4拆分跟踪系统 (3-4小时)
创建文件1core/vr/tracking/poses.py (~600行)
迁移方法 (12个)
- 姿态获取_wait_get_poses, _wait_get_poses_immediate, _wait_get_poses_with_prediction
- 姿态缓存_cache_poses_for_next_frame, _reset_waitgetposes_flag
- 姿态更新_update_tracking_data, update_hmd, _update_camera_poses, _update_camera_poses_with_cache
- 矩阵转换_convert_openvr_matrix_to_panda, _update_matrix_from_openvr, convert_mat
迁移属性:
hmd_pose, controller_poses, tracked_device_poses
poses, game_poses
tracking_space, hmd_anchor, left_eye_anchor, right_eye_anchor
coord_mat, coord_mat_inv
use_prediction_time, poses_updated_in_task
_waitgetposes_called_this_frame
_cached_render_poses, _first_frame
创建文件2core/vr/tracking/devices.py (~300行)
迁移方法 (8个)
- 控制器_initialize_controllers, _detect_controllers, get_controller_by_role
- 设备管理_create_tracked_device_anchor, update_tracked_devices
- 状态查询are_controllers_connected, get_connected_controllers
- 震动trigger_controller_haptic
迁移属性:
left_controller, right_controller
controllers, tracked_device_anchors
创建文件3core/vr/tracking/input_wrapper.py (~200行)
迁移方法 (12个)
- 按钮查询is_trigger_pressed, is_trigger_just_pressed, is_grip_pressed, is_grip_just_pressed, is_menu_pressed
- 触摸板is_trackpad_touched, get_trackpad_position
- 交互查询get_selected_object, get_grabbed_object, is_grabbing_object
- 交互控制force_release_all_grabs, add_interactable_object
类结构:
class VRPoseTracker:
"""VR姿态跟踪系统"""
class VRDeviceManager:
"""VR设备管理系统"""
class VRInputWrapper:
"""VR输入包装层"""
验证步骤:
1. ✅ 姿态跟踪启动VR检查头显跟踪正常
2. ✅ 控制器检测:验证控制器正确识别
3. ✅ 输入测试:测试按钮和触摸板输入
4. ✅ 震动测试:触发控制器震动反馈
---
🔷 阶段5拆分渲染系统 (4-5小时)
创建文件1core/vr/rendering/buffers.py (~400行)
迁移方法 (10个)
- 缓冲区创建_create_vr_buffers, _create_vr_buffer, _create_vr_buffers_with_pipeline
- 纹理管理_create_vr_texture, _prepare_and_cache_textures
- Pipeline效果_apply_pipeline_vr_effects
- 天空盒_check_skybox_status, _create_vr_skybox
- 诊断_diagnose_buffer_performance
- 清理_cleanup_vr_buffers
迁移属性:
vr_left_eye_buffer, vr_right_eye_buffer
vr_left_texture, vr_right_texture
left_texture_id, right_texture_id, textures_prepared
eye_width, eye_height, near_clip, far_clip
scaled_eye_width, scaled_eye_height
创建文件2core/vr/rendering/cameras.py (~350行)
迁移方法 (6个)
- 相机设置_setup_vr_cameras, _get_eye_offset
- 优化_optimize_vr_rendering, _apply_lightweight_rendering, _disable_vr_buffer_extras
- 主相机_disable_main_cam, _enable_main_cam
迁移属性:
vr_left_camera, vr_right_camera
创建文件3core/vr/rendering/compositor.py (~300行)
迁移方法 (9个)
- 渲染回调simple_left_cb, simple_right_cb
- 纹理提交submit_texture
- GPU同步_sync_gpu_if_needed, _smart_gpu_sync
- ATW控制_disable_async_reprojection, enable_async_reprojection_disable, disable_async_reprojection_disable
迁移属性:
vr_compositor, submit_together
openvr_frame_id
left_eye_last_render_frame, right_eye_last_render_frame
disable_async_reprojection
创建文件4core/vr/rendering/pipeline.py (~250行)
迁移方法 (4个)
- Pipeline集成_create_vr_buffers_with_pipeline, _apply_pipeline_vr_effects
- 模式切换set_vr_render_mode, get_vr_render_mode
迁移属性:
vr_render_mode, render_pipeline_enabled
vr_pipeline_left_target, vr_pipeline_right_target
pipeline_resolution_scale, vr_pipeline_controller
pipeline_vr_config
验证步骤:
1. ✅ 缓冲区:检查左右眼缓冲区正确创建
2. ✅ 相机:验证双眼相机位置和视锥
3. ✅ 渲染:测试左右眼渲染回调
4. ✅ 提交确认纹理正确提交到OpenVR
5. ✅ Pipeline测试RenderPipeline模式切换
---
🔷 阶段6重构核心管理器 (2-3小时)
保留在 core/vr_manager.py (~500行)
保留方法 (10个核心方法)
- 生命周期__init__, cleanup
- VR初始化is_vr_available, initialize_vr
- VR控制enable_vr, disable_vr
- 主循环_start_vr_task, _update_vr
- 状态查询get_vr_status
新增子系统属性(组合模式):
def __init__(self, world):
# ... 基础初始化 ...
# 子系统初始化(按依赖顺序)
self.optimization = VROptimization(self)
self.performance_monitor = VRPerformanceMonitor(self)
self.test_mode = VRTestMode(self)
self.pose_tracker = VRPoseTracker(self)
self.device_manager = VRDeviceManager(self)
self.input_wrapper = VRInputWrapper(self)
self.buffer_manager = VRBufferManager(self)
self.camera_manager = VRCameraManager(self)
self.compositor = VRCompositor(self)
self.pipeline_manager = VRPipelineManager(self)
委托方法保持API兼容
# 性能监控委托
def enable_performance_monitoring(self):
return self.performance_monitor.enable_performance_monitoring()
# 测试模式委托
def enable_vr_test_mode(self, display_mode='stereo'):
return self.test_mode.enable_vr_test_mode(display_mode)
# 优化系统委托
def set_resolution_scale(self, scale):
return self.optimization.set_resolution_scale(scale)
# ... 其他委托方法 ...
验证步骤:
1. ✅ 完整启动启动应用测试VR完整流程
2. ✅ API测试验证所有公开API可用
3. ✅ 性能测试:运行性能测试,确认无回退
4. ✅ 集成测试:测试传送、交互、渲染等所有功能
---
✅ 每阶段验证清单
静态验证
- python -m py_compile 检查语法
- python main.py --help 验证模块导入
- 运行 pylint 检查代码质量
功能验证
- 启动应用python main.py
- 检查VR可用性vr_manager.is_vr_available()
- 测试VR启用切换到VR模式
- 测试头显跟踪:移动头显查看跟踪
- 测试控制器:检测和交互
- 测试传送:使用控制器传送
- 测试性能监控:查看性能报告
- 测试VR禁用退出VR模式
性能验证
- 运行性能测试vr_manager.run_vr_performance_test(30)
- 检查帧率稳定在90fps
- 对比拆分前后性能指标
- 确认无性能回退
---
🛡️ 风险控制
备份策略
# 每个阶段开始前
cp core/vr_manager.py core/vr_manager.py.backup.stage{N}
回滚方案
# 恢复备份
cp core/vr_manager.py.backup.stage{N} core/vr_manager.py
循环依赖预防
- ✅ 所有子模块通过 self.vr_manager 访问其他子系统
- ✅ 子模块间不直接引用
- ✅ VRManager 作为中介协调所有子系统
性能保护
- ✅ 热点路径(渲染回调)保持直接调用
- ✅ 非关键路径使用委托模式
- ✅ 对象池和缓存机制保持不变
---
📊 预期成果
代码结构
core/vr_manager.py (500行) ✅
core/vr/
├── performance/
│ ├── monitoring.py (900行) ✅
│ └── optimization.py (300行) ✅
├── testing/
│ └── test_mode.py (800行) ✅
├── tracking/
│ ├── poses.py (600行) ✅
│ ├── devices.py (300行) ✅
│ └── input_wrapper.py (200行) ✅
└── rendering/
├── buffers.py (400行) ✅
├── cameras.py (350行) ✅
├── compositor.py (300行) ✅
└── pipeline.py (250行) ✅
改进指标
- ✅ 文件行数4736行 → 最大900行符合规范
- ✅ 目录文件数:所有目录 ≤ 4个文件远小于8个限制
- ✅ 方法数138个 → 每个类 ≤ 20个方法
- ✅ 职责清晰:每个模块单一职责
- ✅ 可维护性:大幅提升
- ✅ 可测试性:模块化便于单元测试
- ✅ 向后兼容100%保持现有API
时间估算
- 阶段1: 2-3小时
- 阶段2: 2-3小时
- 阶段3: 1-2小时
- 阶段4: 3-4小时
- 阶段5: 4-5小时
- 阶段6: 2-3小时
总计15-20小时分6个阶段逐步完成
---
🚀 开始执行
确认此计划后,将按以下顺序执行:
1. 创建备份
2. 阶段1拆分性能监控系统
3. 验证通过后进入阶段2
4. 依次完成所有6个阶段
5. 最终整体测试和文档更新

View File

@ -1,30 +0,0 @@
#!/bin/bash
echo "Building EG_Engine with PyInstaller..."
/home/hello/.local/bin/pyinstaller --onedir --windowed --name=EG_Engine \
--exclude-module pyassimp \
--add-data="RenderPipelineFile:RenderPipelineFile" \
--add-data="QMeta3D:QMeta3D" \
--add-data="core:core" \
--add-data="gui:gui" \
--add-data="ui:ui" \
--add-data="scene:scene" \
--add-data="project:project" \
--add-data="demo:demo" \
--add-data="plugins:plugins" \
--add-data="scripts:scripts" \
--hidden-import=PyQt5.sip \
--hidden-import=panda3d.core \
main.py
echo "Creating run.sh script in dist/EG_Engine/"
cat > dist/EG_Engine/run.sh << 'EOF'
cd ..
cd ..
python3.10 main.py "$@"
EOF
echo "Making run.sh executable"
chmod +x dist/EG_Engine/run.sh
echo "Build completed!"

View File

@ -1,30 +0,0 @@
#!/bin/bash
echo "Building EG_Engine with PyInstaller..."
pyinstaller --onedir --windowed --name=EG_Engine \
--exclude-module pyassimp \
--add-data="RenderPipelineFile:RenderPipelineFile" \
--add-data="QMeta3D:QMeta3D" \
--add-data="core:core" \
--add-data="gui:gui" \
--add-data="ui:ui" \
--add-data="scene:scene" \
--add-data="project:project" \
--add-data="demo:demo" \
--add-data="plugins:plugins" \
--add-data="scripts:scripts" \
--hidden-import=PyQt5.sip \
--hidden-import=panda3d.core \
main.py
echo "Creating run.sh script in dist/EG_Engine/"
cat > dist/EG_Engine/run.sh << 'EOF'
cd ..
cd ..
python3 main.py "$@"
EOF
echo "Making run.sh executable"
chmod +x dist/EG_Engine/run.sh
echo "Build completed!"

View File

@ -1,12 +0,0 @@
@echo off
set PYTHONPATH=%~dp0_internal
pyinstaller --onedir --windowed --name=EG_Engine --exclude-module pyassimp --add-data="RenderPipelineFile;RenderPipelineFile" --add-data="QMeta3D;QMeta3D" --add-data="core;core" --add-data="gui;gui" --add-data="ui;ui" --add-data="scene;scene" --add-data="project;project" --add-data="demo;demo" --add-data="plugins;plugins" --add-data="scripts;scripts" --hidden-import=PyQt5.sip --hidden-import=panda3d.core main.py
echo @echo off > dist\EG_Engine\run.bat
echo set PYTHONPATH=%%~dp0_internal >> dist\EG_Engine\run.bat
echo cd .. >> dist\EG_Engine\run.bat
echo cd .. >> dist\EG_Engine\run.bat
echo python main.py >> dist\EG_Engine\run.bat
echo pause >> dist\EG_Engine\run.bat
pause

View File

@ -1,95 +1,145 @@
from direct.task.TaskManagerGlobal import taskMgr
class CustomMouseController:
def __init__(self, showbase):
self.showbase = showbase
# This is used to store which keys are currently pressed.
self.keyMap = {
"mouse1": 0,
"cam-forward": 0,
"cam-backward": 0,
"cam-left": 0,
"cam-right": 0,
"cam-up": 0,
"cam-down": 0
}
# 添加鼠标控制
# self.showbase.accept("mouse1", self.setKey, ["mouse1", True])
# self.showbase.accept("mouse1-up", self.setKey, ["mouse1", False])
self.showbase.accept("w", self.setKey, ["cam-forward", True])
self.showbase.accept("a", self.setKey, ["cam-left", True])
self.showbase.accept("s", self.setKey, ["cam-backward", True])
self.showbase.accept("d", self.setKey, ["cam-right", True])
self.showbase.accept("e", self.setKey, ["cam-up", True])
self.showbase.accept("q", self.setKey, ["cam-down", True])
self.showbase.accept("w-up", self.setKey, ["cam-forward", False])
self.showbase.accept("a-up", self.setKey, ["cam-left", False])
self.showbase.accept("s-up", self.setKey, ["cam-backward", False])
self.showbase.accept("d-up", self.setKey, ["cam-right", False])
self.showbase.accept("e-up", self.setKey, ["cam-up", False])
self.showbase.accept("q-up", self.setKey, ["cam-down", False])
self.last_mouse_x = 0
self.last_mouse_y = 0
self.camera_speed = 25
self.move_speed = 20
def setUp(self, mouse_speed: float = 25, move_speed: float = 20):
taskMgr.add(self.move, "moveTask")
self.camera_speed = mouse_speed
self.move_speed = move_speed
def setKey(self, key, value, arg: str = None):
self.keyMap[key] = value
if key == "mouse1" and value == True and self.keyMap[key]:
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
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
if self.keyMap["cam-left"]:
self.showbase.camera.setX(self.showbase.camera, -self.move_speed * dt)
if self.keyMap["cam-right"]:
self.showbase.camera.setX(self.showbase.camera, +self.move_speed * dt)
if self.keyMap["cam-backward"]:
self.showbase.camera.setY(self.showbase.camera, -self.move_speed * dt)
if self.keyMap["cam-forward"]:
self.showbase.camera.setY(self.showbase.camera, +self.move_speed * dt)
if self.keyMap["cam-up"]:
self.showbase.camera.setZ(self.showbase.camera, +self.move_speed * dt)
if self.keyMap["cam-down"]:
self.showbase.camera.setZ(self.showbase.camera, -self.move_speed * dt)
if self.keyMap["mouse1"]:
try:
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
current_x = mouse_pos.get_x()
current_y = mouse_pos.get_y()
# 计算鼠标移动差值
dx = current_x - self.last_mouse_x
dy = current_y - self.last_mouse_y
dy *= -1
# 根据鼠标移动调整摄像机
if abs(dx) > 0.01 or abs(dy) > 0.01:
cam_hpr = self.showbase.camera.get_hpr()
self.showbase.camera.set_hpr(
cam_hpr.x - dx * self.camera_speed,
max(-90, min(90, cam_hpr.y - dy * self.camera_speed)),
0
)
# 更新鼠标位置
self.last_mouse_x = current_x
self.last_mouse_y = current_y
except Exception as e:
print(f"旋转相机失败:{e}")
return task.cont
from direct.task.TaskManagerGlobal import taskMgr
class CustomMouseController:
def __init__(self, showbase):
self.showbase = showbase
# This is used to store which keys are currently pressed.
self.keyMap = {
"mouse1": 0,
"mouse3": 0, # 右键
"cam-forward": 0,
"cam-backward": 0,
"cam-left": 0,
"cam-right": 0,
"cam-up": 0,
"cam-down": 0
}
# 添加鼠标控制
self.showbase.accept("mouse1", self.setKey, ["mouse1", True])
self.showbase.accept("mouse1-up", self.setKey, ["mouse1", False])
self.showbase.accept("mouse3", self.setKey, ["mouse3", True]) # 右键
self.showbase.accept("mouse3-up", self.setKey, ["mouse3", False]) # 右键释放
self.showbase.accept("w", self.setKey, ["cam-forward", True])
self.showbase.accept("a", self.setKey, ["cam-left", True])
self.showbase.accept("s", self.setKey, ["cam-backward", True])
self.showbase.accept("d", self.setKey, ["cam-right", True])
self.showbase.accept("e", self.setKey, ["cam-up", True])
self.showbase.accept("q", self.setKey, ["cam-down", True])
self.showbase.accept("w-up", self.setKey, ["cam-forward", False])
self.showbase.accept("a-up", self.setKey, ["cam-left", False])
self.showbase.accept("s-up", self.setKey, ["cam-backward", False])
self.showbase.accept("d-up", self.setKey, ["cam-right", False])
self.showbase.accept("e-up", self.setKey, ["cam-up", False])
self.showbase.accept("q-up", self.setKey, ["cam-down", False])
self.last_mouse_x = 0
self.last_mouse_y = 0
self.camera_speed = 25
self.move_speed = 20
def setUp(self, mouse_speed: float = 25, move_speed: float = 20):
taskMgr.add(self.move, "moveTask")
self.camera_speed = mouse_speed
self.move_speed = move_speed
def setKey(self, key, value, arg: str = None):
self.keyMap[key] = value
if (key == "mouse1" or key == "mouse3") and value == True:
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
if self.keyMap["cam-left"]:
self.showbase.camera.setX(self.showbase.camera, -self.move_speed * dt)
if self.keyMap["cam-right"]:
self.showbase.camera.setX(self.showbase.camera, +self.move_speed * dt)
if self.keyMap["cam-backward"]:
self.showbase.camera.setY(self.showbase.camera, -self.move_speed * dt)
if self.keyMap["cam-forward"]:
self.showbase.camera.setY(self.showbase.camera, +self.move_speed * dt)
if self.keyMap["cam-up"]:
self.showbase.camera.setZ(self.showbase.camera, +self.move_speed * dt)
if self.keyMap["cam-down"]:
self.showbase.camera.setZ(self.showbase.camera, -self.move_speed * dt)
if self.keyMap["mouse1"] or self.keyMap["mouse3"]: # 左键或右键按下
try:
# 检查是否应该处理鼠标事件避免与ImGui冲突
if self._should_handle_mouse():
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
if mouse_pos:
current_x = mouse_pos.get_x()
current_y = mouse_pos.get_y()
# 计算鼠标移动差值
dx = current_x - self.last_mouse_x
dy = current_y - self.last_mouse_y
dy *= -1
# 根据鼠标移动调整摄像机
if abs(dx) > 0.01 or abs(dy) > 0.01:
cam_hpr = self.showbase.camera.get_hpr()
self.showbase.camera.set_hpr(
cam_hpr.x - dx * self.camera_speed,
max(-90, min(90, cam_hpr.y - dy * self.camera_speed)),
0
)
# 更新鼠标位置
self.last_mouse_x = current_x
self.last_mouse_y = current_y
except Exception as e:
print(f"旋转相机失败:{e}")
return task.cont
def _should_handle_mouse(self):
"""检查是否应该处理鼠标事件避免与ImGui冲突"""
try:
# 如果是右键,优先处理视角控制
if self.keyMap["mouse3"]:
return True
# 检查ImGui是否想要捕获鼠标
try:
from imgui_bundle import imgui
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
return False
except ImportError:
pass
# 检查鼠标位置是否在ImGui窗口区域内
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
if not mouse_pos:
return True
# 获取显示尺寸
try:
from imgui_bundle import imgui
display_size = imgui.get_io().display_size
mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2
mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)
# 检查是否在常见的ImGui界面区域内
# 这里可以根据实际的ImGui窗口位置进行更精确的检测
if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏)
return False
if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏)
return False
if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域
return False
except:
pass
return True
except Exception as e:
print(f"鼠标事件检测失败: {e}")
return True

788
demo.py
View File

@ -13,6 +13,8 @@ from imgui_bundle import imgui, imgui_ctx
import sys
import os
import warnings
import threading
import time
# 导入MyWorld类和必要的模块
from core.world import CoreWorld
@ -28,6 +30,59 @@ from scene.scene_manager import SceneManager
from project.project_manager import ProjectManager
from core.InfoPanelManager import InfoPanelManager
from core.collision_manager import CollisionManager
from core.CustomMouseController import CustomMouseController
# 拖拽监控类
class DragDropMonitor:
"""拖拽文件监控器"""
def __init__(self, world):
self.world = world
self.running = False
self.thread = None
# 支持的文件格式
self.supported_formats = ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']
def start(self):
"""启动监控"""
if not self.running:
self.running = True
self.thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.thread.start()
def stop(self):
"""停止监控"""
self.running = False
if self.thread:
self.thread.join()
def _monitor_loop(self):
"""监控循环"""
while self.running:
try:
# 这里可以实现具体的拖拽检测逻辑
# 由于Panda3D限制我们使用简化的实现
time.sleep(0.1)
# 检查是否有新的拖拽文件
self._check_for_dropped_files()
except Exception as e:
print(f"拖拽监控错误: {e}")
time.sleep(1)
def _check_for_dropped_files(self):
"""检查是否有拖拽的文件"""
# 这里可以实现具体的文件检测逻辑
# 由于系统限制,我们提供一个占位符实现
pass
def add_file_from_external(self, file_path):
"""从外部添加文件路径(用于系统级拖拽集成)"""
if self.world:
self.world.add_dragged_file(file_path)
try:
# 尝试导入视频管理器,避免循环导入
import importlib.util
@ -95,6 +150,11 @@ class MyWorld(CoreWorld):
# 初始化碰撞管理器
self.collision_manager = CollisionManager(self)
# 初始化自定义鼠标控制器(视角移动)
self.mouse_controller = CustomMouseController(self)
self.mouse_controller.setUp(mouse_speed=25, move_speed=20)
print("✓ 自定义鼠标控制器初始化完成")
# 初始化VR管理器
try:
from core.vr import VRManager
@ -230,6 +290,25 @@ class MyWorld(CoreWorld):
# 消息系统
self.messages = []
self.max_messages = 5 # 最多显示5条消息
# 剪切板系统
self.clipboard = []
self.clipboard_mode = "" # "copy" 或 "cut"
# 视角控制状态
self.camera_control_enabled = True
self.show_camera_info = False
# 拖拽导入状态
self.dragged_files = []
self.is_dragging = False
self.show_drag_overlay = False
self.drag_drop_monitor = None
# 导入功能状态
self.show_import_dialog = False
self.import_file_path = ""
self.supported_formats = [".gltf", ".glb", ".fbx", ".bam", ".egg", ".obj"]
self.accept('imgui-new-frame', self.__newFrame)
self.accept('`', self.__toggleImgui)
@ -243,6 +322,18 @@ class MyWorld(CoreWorld):
self.accept('o', self._on_o_pressed)
self.accept('s', self._on_s_pressed)
self.accept('f4', self._on_f4_pressed)
# 编辑功能快捷键
self.accept('z', self._on_z_pressed)
self.accept('y', self._on_y_pressed)
self.accept('x', self._on_x_pressed)
self.accept('c', self._on_c_pressed)
self.accept('v', self._on_v_pressed)
self.accept('delete', self._on_delete_pressed)
# 滚轮事件
self.accept('wheel_up', self._on_wheel_up)
self.accept('wheel_down', self._on_wheel_down)
self.testTexture = None
self.icons = {} # 初始化图标字典
@ -251,6 +342,10 @@ class MyWorld(CoreWorld):
self.add_success_message("MyWorld 初始化完成")
self.add_info_message("ImGui菜单系统已就绪")
self.add_info_message("快捷键已启用 (Ctrl+N, Ctrl+O, Ctrl+S, Alt+F4)")
# 启用拖拽导入功能
self.setup_drag_drop_support()
self.add_info_message("拖拽导入功能已启用 - 可将3D文件拖拽到窗口中导入")
print("✓ MyWorld 初始化完成")
@ -443,6 +538,10 @@ class MyWorld(CoreWorld):
self._draw_new_project_dialog()
self._draw_open_project_dialog()
self._draw_path_browser()
self._draw_import_dialog()
# 绘制拖拽界面
self._draw_drag_drop_interface()
def _draw_docked_layout(self, window_width, window_height):
"""绘制可停靠的布局(支持拖拽)"""
@ -491,12 +590,20 @@ class MyWorld(CoreWorld):
# 编辑菜单
with imgui_ctx.begin_menu("编辑") as edit_menu:
if edit_menu:
imgui.menu_item("撤销", "Ctrl+Z", False, True)
imgui.menu_item("重做", "Ctrl+Y", False, True)
if imgui.menu_item("撤销", "Ctrl+Z", False, True)[1]:
self._on_undo()
if imgui.menu_item("重做", "Ctrl+Y", False, True)[1]:
self._on_redo()
imgui.separator()
imgui.menu_item("复制", "Ctrl+C", False, True)
imgui.menu_item("粘贴", "Ctrl+V", False, True)
imgui.menu_item("删除", "Del", False, True)
if imgui.menu_item("剪切", "Ctrl+X", False, True)[1]:
self._on_cut()
if imgui.menu_item("复制", "Ctrl+C", False, True)[1]:
self._on_copy()
if imgui.menu_item("粘贴", "Ctrl+V", False, True)[1]:
self._on_paste()
imgui.separator()
if imgui.menu_item("删除", "Del", False, True)[1]:
self._on_delete()
# 视图菜单
with imgui_ctx.begin_menu("视图") as view_menu:
@ -510,7 +617,8 @@ class MyWorld(CoreWorld):
# 工具菜单
with imgui_ctx.begin_menu("工具") as tools_menu:
if tools_menu:
imgui.menu_item("导入模型", "", False, True)
if imgui.menu_item("导入模型", "", False, True)[1]:
self._on_import_model()
imgui.menu_item("地形编辑器", "", False, True)
imgui.menu_item("材质编辑器", "", False, True)
imgui.menu_item("脚本编辑器", "", False, True)
@ -708,6 +816,30 @@ class MyWorld(CoreWorld):
if changed and command:
self.add_info_message(f"执行命令: {command}")
# TODO: 实现命令执行逻辑
imgui.separator()
# 视角控制信息
imgui.text("视角控制:")
imgui.text(" WASD - 移动")
imgui.text(" Q/E - 上下")
imgui.text(" 右键拖拽 - 旋转视角")
imgui.text(" 滚轮 - 前进/后退")
# 相机位置信息
cam_pos = self.camera.getPos()
cam_hpr = self.camera.getHpr()
imgui.text(f"位置: X={cam_pos.x:.1f}, Y={cam_pos.y:.1f}, Z={cam_pos.z:.1f}")
imgui.text(f"旋转: H={cam_hpr.x:.1f}, P={cam_hpr.y:.1f}, R={cam_hpr.z:.1f}")
# 控制状态
imgui.checkbox("启用视角控制", self.camera_control_enabled)
# 重置按钮
if imgui.button("重置相机"):
self.camera.setPos(0, -20, 5)
self.camera.setHpr(0, 0, 0)
self.add_info_message("相机位置已重置")
def _draw_script_panel(self):
"""绘制脚本管理面板"""
@ -830,6 +962,101 @@ class MyWorld(CoreWorld):
if self.alt_pressed:
self._on_exit()
def _on_z_pressed(self):
"""Z键按下 - 检查Ctrl+Z组合键撤销"""
if self.ctrl_pressed:
self._on_undo()
def _on_y_pressed(self):
"""Y键按下 - 检查Ctrl+Y组合键重做"""
if self.ctrl_pressed:
self._on_redo()
def _on_x_pressed(self):
"""X键按下 - 检查Ctrl+X组合键剪切"""
if self.ctrl_pressed:
self._on_cut()
def _on_c_pressed(self):
"""C键按下 - 检查Ctrl+C组合键复制"""
if self.ctrl_pressed:
self._on_copy()
def _on_v_pressed(self):
"""V键按下 - 检查Ctrl+V组合键粘贴"""
if self.ctrl_pressed:
self._on_paste()
def _on_delete_pressed(self):
"""Delete键按下 - 删除选中节点"""
self._on_delete()
def _on_wheel_up(self):
"""滚轮向上滚动 - 相机前进"""
try:
if not self.camera_control_enabled:
return
# 检查鼠标是否在ImGui窗口上
if self._is_mouse_over_imgui():
return
# 沿相机前向向量移动
forward = self.camera.getMat().getRow3(1)
distance = 20.0 * globalClock.getDt()
currentPos = self.camera.getPos()
newPos = currentPos + forward * distance
self.camera.setPos(newPos)
except Exception as e:
print(f"滚轮前进失败: {e}")
def _on_wheel_down(self):
"""滚轮向下滚动 - 相机后退"""
try:
# 检查鼠标是否在ImGui窗口上
if self._is_mouse_over_imgui():
return
# 沿相机前向向量移动
forward = self.camera.getMat().getRow3(1)
distance = -20.0 * globalClock.getDt()
currentPos = self.camera.getPos()
newPos = currentPos + forward * distance
self.camera.setPos(newPos)
except Exception as e:
print(f"滚轮后退失败: {e}")
def _is_mouse_over_imgui(self):
"""检测鼠标是否在ImGui窗口上"""
try:
# 检查是否有任何ImGui窗口想要捕获鼠标
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
return True
# 检查鼠标是否在任何ImGui窗口内
mouse_pos = self.mouseWatcherNode.getMouse()
if not mouse_pos:
return False
# 简单的边界检查(可以根据需要扩展)
display_size = imgui.get_io().display_size
mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2
mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)
# 检查是否在常见的ImGui界面区域内
# 这里可以根据实际的ImGui窗口位置进行更精确的检测
if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏)
return True
if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏)
return True
if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域
return True
return False
except Exception as e:
print(f"ImGui界面检测失败: {e}")
return False
# ==================== 消息系统 ====================
def add_message(self, text, color=(1.0, 1.0, 1.0, 1.0)):
@ -862,6 +1089,178 @@ class MyWorld(CoreWorld):
"""添加信息消息"""
self.add_message(f" {text}", (0.157, 0.620, 1.0, 1.0))
# ==================== 编辑菜单功能实现 ====================
def _on_undo(self):
"""处理撤销操作"""
try:
if hasattr(self, 'command_manager') and self.command_manager:
if self.command_manager.can_undo():
success = self.command_manager.undo()
if success:
self.add_success_message("撤销操作成功")
else:
self.add_error_message("撤销操作失败")
else:
self.add_warning_message("没有可撤销的操作")
else:
self.add_error_message("命令管理器未初始化")
except Exception as e:
self.add_error_message(f"撤销操作失败: {e}")
def _on_redo(self):
"""处理重做操作"""
try:
if hasattr(self, 'command_manager') and self.command_manager:
if self.command_manager.can_redo():
success = self.command_manager.redo()
if success:
self.add_success_message("重做操作成功")
else:
self.add_error_message("重做操作失败")
else:
self.add_warning_message("没有可重做的操作")
else:
self.add_error_message("命令管理器未初始化")
except Exception as e:
self.add_error_message(f"重做操作失败: {e}")
def _on_copy(self):
"""处理复制操作"""
try:
if not hasattr(self, 'selection') or not self.selection:
self.add_error_message("选择系统未初始化")
return
# 获取当前选中的节点
selected_node = self.selection.selectedNode
if not selected_node:
self.add_warning_message("没有选中的节点")
return
# 检查节点有效性(不能复制根节点)
if selected_node.getName() == "render":
self.add_warning_message("不能复制根节点")
return
# 序列化节点
if hasattr(self, 'scene_manager') and self.scene_manager:
node_data = self.scene_manager.serializeNodeForCopy(selected_node)
if node_data:
self.clipboard = [node_data]
self.clipboard_mode = "copy"
self.add_success_message(f"已复制节点: {selected_node.getName()}")
else:
self.add_error_message("节点序列化失败")
else:
self.add_error_message("场景管理器未初始化")
except Exception as e:
self.add_error_message(f"复制操作失败: {e}")
def _on_cut(self):
"""处理剪切操作"""
try:
if not hasattr(self, 'selection') or not self.selection:
self.add_error_message("选择系统未初始化")
return
# 获取当前选中的节点
selected_node = self.selection.selectedNode
if not selected_node:
self.add_warning_message("没有选中的节点")
return
# 检查节点有效性(不能剪切根节点和系统节点)
node_name = selected_node.getName()
if node_name == "render":
self.add_warning_message("不能剪切根节点")
return
# 序列化节点
if hasattr(self, 'scene_manager') and self.scene_manager:
node_data = self.scene_manager.serializeNodeForCopy(selected_node)
if node_data:
self.clipboard = [node_data]
self.clipboard_mode = "cut"
# 删除原节点
self.scene_manager.deleteNode(selected_node)
self.selection.clearSelection()
self.add_success_message(f"已剪切节点: {node_name}")
else:
self.add_error_message("节点序列化失败")
else:
self.add_error_message("场景管理器未初始化")
except Exception as e:
self.add_error_message(f"剪切操作失败: {e}")
def _on_paste(self):
"""处理粘贴操作"""
try:
if not self.clipboard:
self.add_warning_message("剪切板为空")
return
if not hasattr(self, 'scene_manager') or not self.scene_manager:
self.add_error_message("场景管理器未初始化")
return
# 确定粘贴目标父节点
parent_node = None
if hasattr(self, 'selection') and self.selection:
selected_node = self.selection.selectedNode
if selected_node:
parent_node = selected_node
# 如果没有选中节点,使用渲染根节点
if not parent_node:
parent_node = self.render
# 反序列化并添加节点
for node_data in self.clipboard:
new_node = self.scene_manager.deserializeNode(node_data, parent_node)
if new_node:
self.add_success_message(f"已粘贴节点: {new_node.getName()}")
# 如果是剪切模式,清空剪切板
if self.clipboard_mode == "cut":
self.clipboard = []
self.clipboard_mode = ""
else:
self.add_error_message("节点反序列化失败")
except Exception as e:
self.add_error_message(f"粘贴操作失败: {e}")
def _on_delete(self):
"""处理删除操作"""
try:
if not hasattr(self, 'selection') or not self.selection:
self.add_error_message("选择系统未初始化")
return
# 获取当前选中的节点
selected_node = self.selection.selectedNode
if not selected_node:
self.add_warning_message("没有选中的节点")
return
# 检查节点有效性(不能删除根节点)
node_name = selected_node.getName()
if node_name == "render":
self.add_warning_message("不能删除根节点")
return
# 删除节点
if hasattr(self, 'scene_manager') and self.scene_manager:
self.scene_manager.deleteNode(selected_node)
self.selection.clearSelection()
self.add_success_message(f"已删除节点: {node_name}")
else:
self.add_error_message("场景管理器未初始化")
except Exception as e:
self.add_error_message(f"删除操作失败: {e}")
# ==================== 对话框绘制函数 ====================
def _draw_new_project_dialog(self):
@ -1054,24 +1453,41 @@ class MyWorld(CoreWorld):
self.path_browser_current_path = item['path']
self._refresh_path_browser()
# 显示文件(仅在打开项目模式下显示.json文件)
# 显示文件(根据模式显示不同类型的文件)
if self.path_browser_mode == "open_project":
for item in self.path_browser_items:
if not item['is_dir'] and item['name'].endswith('.json'):
# 尝试使用图标或文本标识文件
if self.icons.get('success_icon'): # 使用成功图标作为文件图标
imgui.image(self.icons['success_icon'], (16, 16))
imgui.same_line()
else:
imgui.text_colored((1.0, 0.8, 0.4, 1.0), "-")
imgui.same_line()
imgui.text_colored((1.0, 1.0, 0.7, 1.0), "[FILE]")
imgui.same_line()
if imgui.selectable(item['name'], False)[0]:
self.path_browser_selected_path = item['path']
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
# 选择包含project.json的目录
self.path_browser_current_path = os.path.dirname(item['path'])
self._apply_selected_path()
elif self.path_browser_mode == "import_model":
for item in self.path_browser_items:
if not item['is_dir']:
file_ext = os.path.splitext(item['name'])[1].lower()
# 根据文件类型显示不同颜色
if file_ext in ['.gltf', '.glb']:
color = (0.7, 1.0, 0.7, 1.0) # 绿色 - glTF
elif file_ext == '.fbx':
color = (1.0, 0.7, 0.7, 1.0) # 红色 - FBX
elif file_ext in ['.bam', '.egg']:
color = (0.7, 0.7, 1.0, 1.0) # 蓝色 - Panda3D
elif file_ext == '.obj':
color = (1.0, 1.0, 0.7, 1.0) # 黄色 - OBJ
else:
color = (0.8, 0.8, 0.8, 1.0) # 灰色 - 其他
imgui.text_colored(color, f"[{file_ext[1:].upper()}]")
imgui.same_line()
if imgui.selectable(item['name'], False)[0]:
self.path_browser_selected_path = item['path']
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
self.path_browser_selected_path = item['path']
self._apply_selected_path()
imgui.separator()
@ -1090,6 +1506,90 @@ class MyWorld(CoreWorld):
if imgui.button("取消"):
self.show_path_browser = False
def _draw_import_dialog(self):
"""绘制导入模型对话框"""
if not self.show_import_dialog:
return
# 设置对话框标志
flags = (imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_collapse |
imgui.WindowFlags_.modal)
# 获取屏幕尺寸,居中显示对话框
display_size = imgui.get_io().display_size
dialog_width = 600
dialog_height = 500
imgui.set_next_window_size((dialog_width, dialog_height))
imgui.set_next_window_pos(
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
)
with imgui_ctx.begin("导入模型", True, flags) as window:
if not window.opened:
self.show_import_dialog = False
return
imgui.text("选择要导入的模型文件")
imgui.separator()
# 文件路径输入
imgui.text("文件路径:")
changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512)
if changed:
self.import_file_path = file_path
imgui.same_line()
if imgui.button("浏览..."):
self.path_browser_mode = "import_model"
self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd()
self.show_path_browser = True
self._refresh_path_browser()
imgui.separator()
# 支持的格式说明
imgui.text("支持的文件格式:")
formats_text = ", ".join(self.supported_formats)
imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text)
imgui.separator()
# 文件预览信息
if self.import_file_path and os.path.exists(self.import_file_path):
file_size = os.path.getsize(self.import_file_path)
imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
file_ext = os.path.splitext(self.import_file_path)[1].lower()
if file_ext in self.supported_formats:
imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持")
else:
imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式")
else:
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径")
imgui.separator()
# 按钮区域
can_import = (self.import_file_path and
os.path.exists(self.import_file_path) and
os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats)
# 根据状态设置按钮颜色
if can_import:
if imgui.button("导入"):
self._import_model()
self.show_import_dialog = False
else:
# 禁用状态的按钮(灰色显示)
imgui.push_style_color(imgui.Col_.button, (0.3, 0.3, 0.3, 1.0))
imgui.button("导入")
imgui.pop_style_color()
imgui.same_line()
if imgui.button("取消"):
self.show_import_dialog = False
def _create_new_project(self, name, path):
"""创建新项目的实际实现"""
if not hasattr(self, 'project_manager') or not self.project_manager:
@ -1287,6 +1787,19 @@ class MyWorld(CoreWorld):
self.add_error_message(f"无法访问路径: {self.path_browser_current_path}")
return
# 根据模式过滤文件
if self.path_browser_mode == "import_model":
# 只显示支持的模型文件
filtered_items = []
for item in items:
if item['is_dir']:
filtered_items.append(item)
else:
file_ext = os.path.splitext(item['name'])[1].lower()
if file_ext in self.supported_formats:
filtered_items.append(item)
items = filtered_items
# 排序:目录在前,文件在后,按名称排序
items.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
self.path_browser_items = items
@ -1305,8 +1818,253 @@ class MyWorld(CoreWorld):
# 打开项目模式:使用当前路径
self.open_project_path = self.path_browser_current_path
self.add_info_message(f"已选择项目路径: {self.open_project_path}")
elif self.path_browser_mode == "import_model":
# 导入模型模式:使用选择的文件路径
self.import_file_path = self.path_browser_selected_path
self.add_info_message(f"已选择文件: {self.import_file_path}")
except Exception as e:
self.add_error_message(f"应用路径失败: {e}")
# ==================== 导入功能实现 ====================
def _on_import_model(self):
"""处理导入模型菜单项"""
self.add_info_message("打开导入模型对话框")
self.show_import_dialog = True
def _import_model(self):
"""导入模型的具体实现"""
try:
if not self.import_file_path:
self.add_error_message("请选择要导入的文件")
return
if not os.path.exists(self.import_file_path):
self.add_error_message(f"文件不存在: {self.import_file_path}")
return
# 检查文件格式
file_ext = os.path.splitext(self.import_file_path)[1].lower()
if file_ext not in self.supported_formats:
self.add_error_message(f"不支持的文件格式: {file_ext}")
return
# 调用场景管理器导入模型
if hasattr(self, 'scene_manager') and self.scene_manager:
self.add_info_message(f"正在导入模型: {os.path.basename(self.import_file_path)}")
# 导入模型
model_node = self.scene_manager.importModel(self.import_file_path)
if model_node:
# 添加材质处理确保颜色正常
if hasattr(self.scene_manager, 'processMaterials'):
self.scene_manager.processMaterials(model_node)
self.add_info_message("已应用默认材质")
# 额外的材质处理,确保颜色正确显示
try:
# 强制刷新模型显示
model_node.clearMaterial()
model_node.clearTexture()
# 重新应用材质
if hasattr(self.scene_manager, 'processMaterials'):
self.scene_manager.processMaterials(model_node)
# 设置默认的基础颜色(如果模型没有颜色)
if model_node.getColor() == (1, 1, 1, 1): # 默认白色
model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰
except Exception as e:
self.add_warning_message(f"材质处理警告: {e}")
# 设置模型位置
model_node.setPos(0, 0, 0)
# 添加到场景管理器的模型列表
if hasattr(self.scene_manager, 'models'):
self.scene_manager.models.append(model_node)
# 选中新导入的模型
if hasattr(self, 'selection') and self.selection:
self.selection.selectNode(model_node)
self.add_success_message(f"模型导入成功: {os.path.basename(self.import_file_path)}")
else:
self.add_error_message("模型导入失败")
else:
self.add_error_message("场景管理器未初始化")
except Exception as e:
self.add_error_message(f"导入模型失败: {e}")
# 清空导入路径
self.import_file_path = ""
def setup_drag_drop_support(self):
"""设置拖拽支持"""
try:
# 启动拖拽监控线程
self.drag_drop_monitor = DragDropMonitor(self)
self.drag_drop_monitor.start()
print("✓ 拖拽监控已启动")
except Exception as e:
print(f"⚠ 拖拽监控启动失败: {e}")
def add_dragged_file(self, file_path):
"""添加拖拽的文件"""
if file_path not in self.dragged_files:
self.dragged_files.append(file_path)
self.is_dragging = True
self.show_drag_overlay = True
print(f"检测到拖拽文件: {file_path}")
def clear_dragged_files(self):
"""清空拖拽文件列表"""
self.dragged_files.clear()
self.is_dragging = False
self.show_drag_overlay = False
def process_dragged_files(self):
"""处理拖拽的文件"""
if not self.dragged_files:
return
imported_count = 0
for file_path in self.dragged_files:
if self._import_model_from_path(file_path):
imported_count += 1
if imported_count > 0:
self.add_message("success", f"成功导入 {imported_count} 个模型文件")
else:
self.add_message("error", "没有成功导入任何文件")
self.clear_dragged_files()
def _import_model_from_path(self, file_path):
"""从路径导入模型的内部方法"""
try:
# 检查文件是否存在
if not os.path.exists(file_path):
self.add_message("error", f"文件不存在: {file_path}")
return False
# 检查文件格式
supported_formats = ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext not in supported_formats:
self.add_message("error", f"不支持的文件格式: {file_ext}")
return False
# 导入模型
model_node = self.scene_manager.importModel(file_path)
if model_node:
# 应用材质确保颜色正常
self.scene_manager.processMaterials(model_node)
# 设置模型位置
model_node.setPos(0, 0, 0)
# 添加到选择系统
self.selection.select_node(model_node)
self.add_message("success", f"成功导入模型: {os.path.basename(file_path)}")
return True
else:
self.add_message("error", f"导入模型失败: {file_path}")
return False
except Exception as e:
self.add_message("error", f"导入模型时发生错误: {str(e)}")
return False
def _draw_drag_drop_interface(self):
"""绘制拖拽界面"""
# 绘制拖拽覆盖层
if self.show_drag_overlay:
self._draw_drag_overlay()
# 检查是否有拖拽的文件需要处理
if self.is_dragging and self.dragged_files:
# 显示拖拽状态
self._draw_drag_status()
def _draw_drag_overlay(self):
"""绘制拖拽覆盖层"""
viewport = imgui.get_main_viewport()
imgui.set_next_window_pos((0, 0))
imgui.set_next_window_size(viewport.work_size)
flags = (
imgui.WindowFlags_.no_title_bar |
imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_move |
imgui.WindowFlags_.no_scrollbar |
imgui.WindowFlags_.no_saved_settings |
imgui.WindowFlags_.no_background |
imgui.WindowFlags_.no_focus_on_appearing
)
imgui.begin("##DragOverlay", True, flags)
# 绘制半透明背景
draw_list = imgui.get_window_draw_list()
draw_list.add_rect_filled(
(0, 0), viewport.work_size,
imgui.get_color_u32((0, 0, 0, 0.1))
)
# 绘制提示文本
text_size = imgui.calc_text_size("释放以导入文件")
text_pos = (
(viewport.work_size.x - text_size.x) / 2,
(viewport.work_size.y - text_size.y) / 2
)
draw_list.add_text(
text_pos,
imgui.get_color_u32((1, 1, 1, 1)),
"释放以导入文件"
)
imgui.end()
def _draw_drag_status(self):
"""绘制拖拽状态"""
viewport = imgui.get_main_viewport()
# 在右下角显示拖拽状态
imgui.set_next_window_pos(
(viewport.work_size.x - 300, viewport.work_size.y - 150),
imgui.Cond_.first_use_ever
)
flags = (
imgui.WindowFlags_.no_title_bar |
imgui.WindowFlags_.no_resize |
imgui.WindowFlags_.no_move |
imgui.WindowFlags_.no_scrollbar |
imgui.WindowFlags_.no_saved_settings
)
with imgui_ctx.begin("拖拽状态", True, flags):
imgui.text("拖拽的文件:")
for file_path in self.dragged_files:
filename = os.path.basename(file_path)
imgui.text(f"{filename}")
imgui.separator()
if imgui.button("导入所有文件"):
self.process_dragged_files()
imgui.same_line()
if imgui.button("取消"):
self.clear_dragged_files()
demo = MyWorld()
demo.run()

View File

@ -31,7 +31,7 @@ DockId=0x00000007,0
[Window][场景树]
Pos=0,20
Size=285,861
Size=285,883
Collapsed=0
DockId=0x00000001,0
@ -42,8 +42,8 @@ Collapsed=0
DockId=0x00000005,0
[Window][控制台]
Pos=0,883
Size=1524,133
Pos=0,905
Size=1524,111
Collapsed=0
DockId=0x0000000A,0
@ -93,15 +93,20 @@ Pos=675,308
Size=500,400
Collapsed=0
[Window][导入模型]
Pos=625,258
Size=600,500
Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X
DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=1524,996 Split=Y
DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1380,861 Split=X
DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1380,883 Split=X
DockNode ID=0x00000001 Parent=0x00000009 SizeRef=285,730 HiddenTabBar=1 Selected=0xE0015051
DockNode ID=0x00000002 Parent=0x00000009 SizeRef=1237,730 Split=Y
DockNode ID=0x00000007 Parent=0x00000002 SizeRef=1380,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x00000008 Parent=0x00000002 SizeRef=1380,827 CentralNode=1 Selected=0x5E5F7166
DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1380,133 HiddenTabBar=1 Selected=0x5428E753
DockNode ID=0x00000008 Parent=0x00000002 SizeRef=1380,849 CentralNode=1 Selected=0x5E5F7166
DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1380,111 HiddenTabBar=1 Selected=0x5428E753
DockNode ID=0x00000004 Parent=0x08BD597D SizeRef=324,996 Split=Y Selected=0x5DB6FF37
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=304,498 HiddenTabBar=1 Selected=0x5DB6FF37
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=304,496 HiddenTabBar=1 Selected=0x3188AB8D

View File

@ -1,354 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
完整的开源率分析脚本
该脚本将执行完整的测试流程并生成准确报告
"""
import json
import os
import subprocess
import sys
def run_command(command, ignore_failure=False):
"""运行命令并返回结果"""
try:
print(f"执行命令: {command}")
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if result.returncode != 0 and not ignore_failure:
print(f"命令执行失败: {result.stderr}")
return False
print("命令执行成功")
return True
except Exception as e:
print(f"执行命令时出错: {e}")
return False
def load_cloc_data(path="cloc.json"):
"""加载并解析cloc统计数据"""
if not os.path.exists(path):
print(f"❌ 未找到 {path} 文件")
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def load_scancode_data(path="summary.json"):
"""加载并解析ScanCode统计数据"""
if not os.path.exists(path):
print(f"❌ 未找到 {path} 文件")
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def get_licensed_files_details(scancode_data):
"""获取含许可证文件的详细信息"""
if not scancode_data:
return []
files = scancode_data.get("files", [])
licensed_files = []
# 定义需要排除的目录和文件模式
exclude_patterns = [
"/venv/",
"/.git/",
"/__pycache__/",
"/.idea/",
"/.vscode/",
"/build/",
"/dist/",
".egg-info",
"/Resources/",
"/icons/",
"/tex/",
"cloc.json",
"detailed_cloc.txt",
"summary.json",
"完整开源率分析报告.txt",
"run_complete_analysis.py",
]
for file in files:
# 获取文件路径
file_path = file.get("path", "")
# 只处理类型为"file"的条目
if file.get("type") != "file":
continue
# 检查是否应该排除该文件
should_exclude = False
for pattern in exclude_patterns:
if pattern in file_path:
should_exclude = True
break
# 如果应该排除,则跳过该文件
if should_exclude:
continue
# 检查是否有许可证信息
if file.get("detected_license_expression") or file.get("license_detections"):
licensed_files.append({
"path": file_path,
"license": file.get("detected_license_expression", "Unknown"),
"detections": file.get("license_detections", [])
})
return licensed_files
def get_file_code_lines():
"""从detailed_cloc.txt获取文件代码行数"""
file_code_lines = {}
if not os.path.exists("detailed_cloc.txt"):
print("❌ 未找到 detailed_cloc.txt 文件")
return file_code_lines
with open("detailed_cloc.txt", "r") as f:
cloc_lines = f.readlines()
# 解析cloc输出创建文件路径到代码行数的映射
for line in cloc_lines[3:]: # 跳过标题行
parts = line.strip().split()
if len(parts) >= 4:
try:
file_path = parts[0]
code_lines = int(parts[-1])
# 标准化路径格式
if file_path.startswith('./'):
file_path = file_path[2:]
file_code_lines[file_path] = code_lines
except ValueError:
continue
return file_code_lines
def calculate_accurate_open_source_lines(licensed_files_details, file_code_lines):
"""计算准确的开源代码行数"""
total_licensed_code_lines = 0
found_files = 0
detailed_files = []
for file_info in licensed_files_details:
file_path = file_info["path"]
# 将文件路径标准化
normalized_path = file_path
if normalized_path.startswith('EG/'):
normalized_path = normalized_path[3:] # 去掉开头的EG/
if normalized_path in file_code_lines:
code_lines = file_code_lines[normalized_path]
total_licensed_code_lines += code_lines
found_files += 1
detailed_files.append({
"path": file_path,
"code_lines": code_lines,
"license": file_info["license"]
})
else:
# 尝试其他可能的路径格式
alt_path1 = './' + normalized_path
alt_path2 = 'EG/' + normalized_path
if alt_path1 in file_code_lines:
code_lines = file_code_lines[alt_path1]
total_licensed_code_lines += code_lines
found_files += 1
detailed_files.append({
"path": file_path,
"code_lines": code_lines,
"license": file_info["license"]
})
elif alt_path2 in file_code_lines:
code_lines = file_code_lines[alt_path2]
total_licensed_code_lines += code_lines
found_files += 1
detailed_files.append({
"path": file_path,
"code_lines": code_lines,
"license": file_info["license"]
})
return total_licensed_code_lines, detailed_files
def generate_detailed_report():
"""生成详细报告"""
# 加载数据
cloc_data = load_cloc_data("cloc.json")
scancode_data = load_scancode_data("summary.json")
file_code_lines = get_file_code_lines()
if not cloc_data or not scancode_data:
print("无法加载必要数据文件")
return False
# 获取含许可证文件详情
licensed_files_details = get_licensed_files_details(scancode_data)
# 计算准确的开源代码行数
accurate_open_source_lines, detailed_files = calculate_accurate_open_source_lines(
licensed_files_details, file_code_lines)
# 获取统计数据
total_code_lines = cloc_data.get("SUM", {}).get("code", 0)
total_files = 1075 # 根据脚本分析得出的实际文件数
licensed_files = len(licensed_files_details)
# 计算开源率
open_source_rate = (accurate_open_source_lines / total_code_lines) * 100 if total_code_lines > 0 else 0
# 创建报告内容
report_content = []
report_content.append("项目开源率分析完整报告")
report_content.append("=" * 50)
report_content.append("")
report_content.append("1. 报告概览")
report_content.append("-" * 20)
report_content.append(f"项目总文件数: {total_files}")
report_content.append(f"含许可证文件数: {licensed_files}")
report_content.append(f"项目总代码行数: {total_code_lines}")
report_content.append(f"准确开源代码行数: {accurate_open_source_lines}")
report_content.append(f"代码开源率: {open_source_rate:.2f}%")
report_content.append("")
report_content.append("2. 各语言代码行数分布(包含文件路径)")
report_content.append("-" * 40)
# 按语言分组显示文件
lang_files = {}
with open("detailed_cloc.txt", "r") as f:
cloc_lines = f.readlines()
for line in cloc_lines[3:]: # 跳过标题行
parts = line.strip().split()
if len(parts) >= 4:
try:
file_path = parts[0]
# 从文件路径推断语言(简化处理)
if file_path.endswith('.py'):
lang = 'Python'
elif file_path.endswith('.js'):
lang = 'JavaScript'
elif file_path.endswith('.cpp') or file_path.endswith('.cc'):
lang = 'C++'
elif file_path.endswith('.h'):
lang = 'C/C++ Header'
elif file_path.endswith('.glsl'):
lang = 'GLSL'
elif file_path.endswith('.qml'):
lang = 'QML'
elif file_path.endswith('.xml'):
lang = 'XML'
elif file_path.endswith('.json'):
lang = 'JSON'
elif file_path.endswith('.md'):
lang = 'Markdown'
elif file_path.endswith('.html'):
lang = 'HTML'
elif file_path.endswith('.css'):
lang = 'CSS'
elif file_path.endswith('.sh'):
lang = 'Shell'
elif file_path.endswith('.yml') or file_path.endswith('.yaml'):
lang = 'YAML'
else:
lang = 'Other'
if lang not in lang_files:
lang_files[lang] = []
lang_files[lang].append((file_path, int(parts[-1])))
except ValueError:
continue
for lang, files in lang_files.items():
report_content.append(f"\n{lang}语言文件:")
report_content.append(f" 文件总数: {len(files)}")
total_lines = sum([f[1] for f in files])
report_content.append(f" 代码行数: {total_lines}")
report_content.append(" 文件列表:")
for file_path, code_lines in files[:10]: # 只显示前10个文件
report_content.append(f" {file_path}: {code_lines}")
if len(files) > 10:
report_content.append(f" ... 还有 {len(files) - 10} 个文件")
report_content.append("")
report_content.append("3. 含许可证的开源文件详情")
report_content.append("-" * 30)
# 按许可证类型分组显示文件
files_by_license = {}
for file_info in detailed_files:
license_type = file_info.get("license", "Unknown")
if license_type not in files_by_license:
files_by_license[license_type] = []
files_by_license[license_type].append(file_info)
for license_type, files in files_by_license.items():
report_content.append(f"\n许可证类型: {license_type}")
report_content.append(f" 文件数量: {len(files)}")
total_lines = sum([f["code_lines"] for f in files])
report_content.append(f" 代码行数: {total_lines}")
report_content.append(" 文件列表:")
for file_info in files:
report_content.append(f" {file_info['path']}: {file_info['code_lines']}")
# 保存报告
with open("完整开源率分析报告.txt", "w", encoding="utf-8") as f:
f.write("\n".join(report_content))
print("完整报告已生成:完整开源率分析报告.txt")
return True
def main():
"""主函数"""
print("开始执行完整的开源率分析流程...")
# 步骤1: 执行cloc统计代码行数
print("\n步骤1: 执行cloc统计代码行数")
cloc_cmd = "cloc --json --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . > cloc.json"
if not run_command(cloc_cmd):
print("❌ cloc统计失败")
return False
# 步骤2: 生成详细文件列表
print("\n步骤2: 生成详细文件列表")
detailed_cloc_cmd = "cloc --by-file --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . | grep -v \"^\\s*$\" | grep -E \"(\\.py|\\.js|\\.cpp|\\.h|\\.glsl|\\.qml|\\.xml|\\.html|\\.css|\\.java|\\.cs|\\.php)\" > detailed_cloc.txt"
if not run_command(detailed_cloc_cmd):
print("❌ 生成详细文件列表失败")
return False
# 步骤3: 执行ScanCode扫描许可证
print("\n步骤3: 执行ScanCode扫描许可证")
scancode_cmd = "scancode --license --classify --summary --json-pp summary.json . --ignore \"venv\" --ignore \".git\" --ignore \"__pycache__\" --ignore \".idea\" --ignore \".vscode\" --ignore \"build\" --ignore \"dist\" --ignore \"*.egg-info\" --ignore \"Resources\" --ignore \"icons\" --ignore \"tex\" --ignore \"cloc.json\" --ignore \"detailed_cloc.txt\" --ignore \"完整开源率分析报告.txt\" --ignore \"run_complete_analysis.py\""
# 忽略失败因为ScanCode会尝试扫描自己生成的summary.json文件导致"失败"
run_command(scancode_cmd, ignore_failure=True)
# 检查summary.json是否生成
if not os.path.exists("summary.json"):
print("❌ ScanCode未生成summary.json文件")
return False
# 步骤4: 生成详细报告
print("\n步骤4: 生成详细报告")
if not generate_detailed_report():
print("❌ 生成报告失败")
return False
print("\n✅ 完整分析流程执行完成!")
print("生成的文件:")
print(" - cloc.json: 代码行数统计")
print(" - detailed_cloc.txt: 详细文件列表")
print(" - summary.json: 许可证扫描结果")
print(" - 完整开源率分析报告.txt: 最终报告")
return True
if __name__ == "__main__":
success = main()
if success:
print("\n🎉 所有步骤执行成功!")
sys.exit(0)
else:
print("\n❌ 执行过程中出现错误!")
sys.exit(1)

238
simple_drag_drop.py Normal file
View File

@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
简单的拖拽导入工具
直接运行此工具然后将3D文件拖拽到窗口中
工具会生成可在demo.py中使用的导入命令
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import os
import sys
class SimpleDragDrop:
def __init__(self):
self.root = tk.Tk()
self.root.title("3D模型拖拽导入工具")
self.root.geometry("600x400")
# 设置窗口始终在最前面
self.root.attributes('-topmost', True)
# 支持的文件格式
self.supported_formats = ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']
self.setup_ui()
self.setup_drag_drop()
def setup_ui(self):
"""设置用户界面"""
# 主框架
main_frame = tk.Frame(self.root)
main_frame.pack(fill='both', expand=True, padx=10, pady=10)
# 标题
title_label = tk.Label(
main_frame,
text="将3D模型文件拖拽到此处",
font=('Arial', 16, 'bold')
)
title_label.pack(pady=(0, 10))
# 拖拽区域
self.drop_area = tk.Frame(
main_frame,
bg='lightgray',
relief='sunken',
bd=2,
height=150
)
self.drop_area.pack(fill='both', expand=True, pady=(0, 10))
# 提示文本
hint_label = tk.Label(
self.drop_area,
text="支持格式: .gltf, .glb, .fbx, .bam, .egg, .obj\n\n拖拽文件到此处或点击下方按钮选择文件",
bg='lightgray',
font=('Arial', 11)
)
hint_label.pack(expand=True)
# 按钮框架
button_frame = tk.Frame(main_frame)
button_frame.pack(fill='x', pady=(0, 10))
# 选择文件按钮
select_btn = tk.Button(
button_frame,
text="选择文件",
command=self.select_files,
width=15
)
select_btn.pack(side='left', padx=5)
# 清空按钮
clear_btn = tk.Button(
button_frame,
text="清空",
command=self.clear_files,
width=15
)
clear_btn.pack(side='left', padx=5)
# 生成命令按钮
generate_btn = tk.Button(
button_frame,
text="生成导入命令",
command=self.generate_commands,
bg='lightblue',
font=('Arial', 10, 'bold'),
width=15
)
generate_btn.pack(side='right', padx=5)
# 文件列表
list_label = tk.Label(
main_frame,
text="文件列表:",
font=('Arial', 10, 'bold')
)
list_label.pack(anchor='w', pady=(0, 5))
# 文件列表框
self.file_listbox = tk.Listbox(
main_frame,
height=8,
selectmode=tk.MULTIPLE
)
self.file_listbox.pack(fill='both', expand=True, pady=(0, 10))
# 命令输出区域
output_label = tk.Label(
main_frame,
text="导入命令:",
font=('Arial', 10, 'bold')
)
output_label.pack(anchor='w', pady=(0, 5))
self.command_text = scrolledtext.ScrolledText(
main_frame,
height=6,
width=70
)
self.command_text.pack(fill='both', expand=True)
def setup_drag_drop(self):
"""设置拖拽功能"""
try:
# 设置拖拽目标
self.drop_area.drop_target_register('DND_Files')
self.drop_area.dnd_bind('<<Drop>>', self.on_drop)
self.drop_area.dnd_bind('<<DragEnter>>', self.on_drag_enter)
self.drop_area.dnd_bind('<<DragLeave>>', self.on_drag_leave)
except:
# 如果拖拽不支持,只使用文件选择器
print("拖拽功能不可用,请使用文件选择器")
def on_drag_enter(self, event):
"""拖拽进入事件"""
self.drop_area.configure(bg='lightblue')
def on_drag_leave(self, event):
"""拖拽离开事件"""
self.drop_area.configure(bg='lightgray')
def on_drop(self, event):
"""拖拽释放事件"""
try:
self.drop_area.configure(bg='lightgray')
# 获取拖拽的文件
files = self.root.tk.splitlist(event.data)
for file_path in files:
self.add_file(file_path)
except Exception as e:
messagebox.showerror("错误", f"拖拽处理失败: {e}")
def add_file(self, file_path):
"""添加文件到列表"""
if os.path.exists(file_path):
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext in self.supported_formats:
if file_path not in self.file_listbox.get(0, tk.END):
self.file_listbox.insert(tk.END, file_path)
else:
messagebox.showwarning("格式错误", f"不支持的文件格式: {file_ext}")
else:
messagebox.showerror("文件错误", f"文件不存在: {file_path}")
def select_files(self):
"""选择文件"""
files = filedialog.askopenfilenames(
title="选择3D模型文件",
filetypes=[
("所有支持的格式", "*.gltf *.glb *.fbx *.bam *.egg *.obj"),
("glTF文件", "*.gltf *.glb"),
("FBX文件", "*.fbx"),
("BAM文件", "*.bam"),
("EGG文件", "*.egg"),
("OBJ文件", "*.obj"),
("所有文件", "*.*")
]
)
for file_path in files:
self.add_file(file_path)
def clear_files(self):
"""清空文件列表"""
self.file_listbox.delete(0, tk.END)
self.command_text.delete(1.0, tk.END)
def generate_commands(self):
"""生成导入命令"""
files = list(self.file_listbox.get(0, tk.END))
if not files:
messagebox.showwarning("没有文件", "请先选择或拖拽文件")
return
# 生成Python导入代码
commands = "# 在demo.py的控制台中执行以下命令来导入文件:\n\n"
for i, file_path in enumerate(files, 1):
filename = os.path.basename(file_path)
commands += f"# 导入文件 {i}: {filename}\n"
commands += f"world._import_model_from_path('{file_path}')\n\n"
commands += "# 或者使用批量导入:\n"
commands += "files = [\n"
for file_path in files:
commands += f" '{file_path}',\n"
commands += "]\n"
commands += "for file_path in files:\n"
commands += " world._import_model_from_path(file_path)\n"
# 显示命令
self.command_text.delete(1.0, tk.END)
self.command_text.insert(1.0, commands)
messagebox.showinfo("完成", "导入命令已生成请复制到demo.py控制台中执行")
def run(self):
"""运行应用"""
self.root.mainloop()
def main():
"""主函数"""
try:
app = SimpleDragDrop()
app.run()
except Exception as e:
print(f"启动拖拽工具失败: {e}")
input("按回车键退出...")
if __name__ == "__main__":
main()

View File

@ -1,149 +0,0 @@
#!/usr/bin/env python3
"""
测试中文字体显示的调试脚本
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '.'))
from panda3d.core import loadPrcFileData
from direct.showbase.ShowBase import ShowBase
import p3dimgui
from imgui_bundle import imgui
import platform
from pathlib import Path
class ChineseFontTest(ShowBase):
def __init__(self):
# 设置窗口
loadPrcFileData('', 'win-size 800 600')
ShowBase.__init__(self)
# 初始化ImGui
p3dimgui.init()
# 测试字体加载
self.test_font_loading()
# 接受ImGui新帧事件
self.accept('imgui-new-frame', self._new_frame)
self.accept('`', self._toggle_imgui)
self.show_test_window = True
def test_font_loading(self):
"""测试字体加载"""
print("=== 开始字体测试 ===")
# 检查系统
system = platform.system().lower()
print(f"系统: {system}")
# 获取字体路径
font_paths = []
if system == "linux":
font_paths = [
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
elif system == "windows":
font_paths = [
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
]
elif system == "darwin":
font_paths = [
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STHeiti.ttc",
]
# 测试每个字体
for font_path in font_paths:
if Path(font_path).exists():
print(f"✓ 字体文件存在: {font_path}")
try:
# 尝试加载字体
font = self.imgui.io.fonts.add_font_from_file_ttf(font_path, 16.0)
print(f" ✓ 字体加载成功")
# 检查字体信息
font_info = self.imgui.io.fonts.fonts
print(f" 字体数量: {len(font_info)}")
# 尝试获取字符集
try:
# 方法1尝试获取中文字符集
chinese_ranges = imgui.get_io().fonts.get_glyph_ranges_chinese_full()
print(f" ✓ 中文字符集获取成功,范围: {len(chinese_ranges)} 个字符")
except:
try:
# 方法2尝试获取中文字符集简化版
chinese_ranges = imgui.get_io().fonts.get_glyph_ranges_chinese()
print(f" ✓ 中文字符集获取成功(简化版),范围: {len(chinese_ranges)} 个字符")
except:
try:
# 方法3尝试获取默认字符集
default_ranges = imgui.get_io().fonts.get_glyph_ranges_default()
print(f" ✓ 默认字符集获取成功,范围: {len(default_ranges)} 个字符")
print(f" ⚠ 使用默认字符集,可能不支持中文")
except Exception as e:
print(f" ⚠ 字符集获取失败: {e}")
print(f" ⚠ 将使用默认字符集")
break
except Exception as e:
print(f" ✗ 字体加载失败: {e}")
else:
print(f"✗ 字体文件不存在: {font_path}")
print("=== 字体测试完成 ===")
def _toggle_imgui(self):
if not self.imgui.isKeyboardCaptured():
self.imgui.toggle()
def _new_frame(self):
# 绘制测试窗口
if self.show_test_window:
imgui.show_demo_window()
# 中文测试窗口
expanded, opened = imgui.begin("中文显示测试", True)
if expanded:
imgui.text("基础中文测试:")
imgui.text("你好,世界!")
imgui.text("这是一个中文测试")
imgui.text("引擎初始化完成")
imgui.text("选择和变换系统")
imgui.text("脚本管理系统")
imgui.separator()
imgui.text("特殊字符测试:")
imgui.text("标点符号:,。!?;:""''()【】")
imgui.separator()
imgui.text("数字测试:")
imgui.text("一二三四五六七八九十")
imgui.separator()
if imgui.button("测试按钮"):
print("中文按钮被点击")
imgui.same_line()
if imgui.button("另一个按钮"):
print("另一个中文按钮")
imgui.separator()
changed, text = imgui.input_text("输入中文", "在这里输入中文")
if changed:
print(f"输入的中文: {text}")
imgui.end()
if __name__ == "__main__":
test = ChineseFontTest()
test.run()

View File

@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""
测试ImGui docking功能
"""
from direct.showbase.ShowBase import ShowBase
import p3dimgui
from imgui_bundle import imgui, imgui_ctx
class DockingTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# Install Dear ImGui
p3dimgui.init()
# 启用ImGui Docking功能
imgui.get_io().config_flags |= imgui.ConfigFlags_.docking_enable
print("✓ ImGui Docking功能已启用")
# 添加任务
self.taskMgr.add(self.__newFrame, "imgui-new-frame")
def __newFrame(self, task):
# 创建全屏DockSpace在第一帧之后创建
if imgui.get_frame_count() > 0:
viewport = imgui.get_main_viewport()
imgui.dock_space_over_viewport(0, viewport, imgui.DockNodeFlags_.passthru_central_node)
# 测试窗口
if imgui.get_frame_count() > 10: # 等待几帧后显示
# 窗口1
with imgui_ctx.begin("测试窗口1", True):
imgui.text("这是第一个测试窗口")
imgui.text("你可以拖拽标题栏来移动窗口")
imgui.text("也可以拖拽到其他窗口边缘来停靠")
# 窗口2
with imgui_ctx.begin("测试窗口2", True):
imgui.text("这是第二个测试窗口")
imgui.text("尝试将这个窗口停靠到第一个窗口")
imgui.text("或者创建标签页")
# 窗口3
with imgui_ctx.begin("测试窗口3", True):
imgui.text("这是第三个测试窗口")
imgui.text("自由调整窗口大小和位置")
return task.cont
if __name__ == "__main__":
test = DockingTest()
test.run()

View File

@ -1,21 +0,0 @@
from direct.showbase.ShowBase import ShowBase
from imgui_bundle import imgui
import p3dimgui
class MyApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# Install Dear ImGui
p3dimgui.init()
self.accept('imgui-new-frame', self.draw)
def draw(self):
# Show the demo window.
imgui.show_demo_window()
app = MyApp()
app.run()