diff --git a/VR_Manager 模块化拆分计划.md b/VR_Manager 模块化拆分计划.md deleted file mode 100644 index 22bb9c89..00000000 --- a/VR_Manager 模块化拆分计划.md +++ /dev/null @@ -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小时) - - 创建文件1:core/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 - - 创建文件2:core/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 - - 创建文件3:core/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小时) - - 创建文件1:core/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 - - 创建文件2:core/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 - - 创建文件3:core/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 - - 创建文件4:core/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. 最终整体测试和文档更新 \ No newline at end of file diff --git a/build_kylin.sh b/build_kylin.sh deleted file mode 100755 index 12fd0a07..00000000 --- a/build_kylin.sh +++ /dev/null @@ -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!" \ No newline at end of file diff --git a/build_linux.sh b/build_linux.sh deleted file mode 100755 index 92e21de6..00000000 --- a/build_linux.sh +++ /dev/null @@ -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!" \ No newline at end of file diff --git a/build_windows.bat b/build_windows.bat deleted file mode 100755 index c323ff85..00000000 --- a/build_windows.bat +++ /dev/null @@ -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 \ No newline at end of file diff --git a/core/CustomMouseController.py b/core/CustomMouseController.py index 6ac00945..f19754ca 100644 --- a/core/CustomMouseController.py +++ b/core/CustomMouseController.py @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/demo.py b/demo.py index 4314659b..1a2498cc 100644 --- a/demo.py +++ b/demo.py @@ -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() diff --git a/imgui.ini b/imgui.ini index d33cec4c..0de698f2 100644 --- a/imgui.ini +++ b/imgui.ini @@ -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 diff --git a/run_complete_analysis.py b/run_complete_analysis.py deleted file mode 100644 index 2c5b5a15..00000000 --- a/run_complete_analysis.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/simple_drag_drop.py b/simple_drag_drop.py new file mode 100644 index 00000000..848279eb --- /dev/null +++ b/simple_drag_drop.py @@ -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('<>', self.on_drop) + self.drop_area.dnd_bind('<>', self.on_drag_enter) + self.drop_area.dnd_bind('<>', 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() \ No newline at end of file diff --git a/test_chinese_font.py b/test_chinese_font.py deleted file mode 100644 index 6ed783b6..00000000 --- a/test_chinese_font.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/test_docking.py b/test_docking.py deleted file mode 100644 index 3efc54ba..00000000 --- a/test_docking.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/testgui.py b/testgui.py deleted file mode 100644 index c7735dbf..00000000 --- a/testgui.py +++ /dev/null @@ -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() \ No newline at end of file