diff --git a/EG b/EG new file mode 160000 index 00000000..69e2bda4 --- /dev/null +++ b/EG @@ -0,0 +1 @@ +Subproject commit 69e2bda47e9713705ad5c45a08b6fc643a2b51f6 diff --git a/core/resource_manager.py b/core/resource_manager.py index f6c4c32b..257f5fb5 100644 --- a/core/resource_manager.py +++ b/core/resource_manager.py @@ -4,6 +4,7 @@ """ import os +import shutil import subprocess import platform from pathlib import Path @@ -337,10 +338,137 @@ class ResourceManager: return str(path.relative_to(self.project_root)) except ValueError: return str(path) + + def _normalize_target_dir(self, target_dir: Optional[Path]) -> Path: + """归一化目标目录,不合法时回退到当前目录。""" + candidate = Path(target_dir) if target_dir else self.current_path + if candidate.exists() and candidate.is_dir(): + return candidate.resolve() + if self.current_path.exists() and self.current_path.is_dir(): + return self.current_path.resolve() + return self.project_root.resolve() + + def _get_unique_destination_path(self, target_dir: Path, source_name: str) -> Path: + """生成不冲突的目标路径。""" + source_path = Path(source_name) + stem = source_path.stem or source_path.name or "item" + suffix = source_path.suffix + + candidate = target_dir / source_path.name + if not candidate.exists(): + return candidate + + index = 1 + while True: + if suffix: + unique_name = f"{stem}_{index}{suffix}" + else: + unique_name = f"{stem}_{index}" + candidate = target_dir / unique_name + if not candidate.exists(): + return candidate + index += 1 + + def import_external_files(self, file_paths: List[Path], target_dir: Optional[Path] = None): + """将外部文件导入资源目录(复制),返回(成功列表, 错误列表)。""" + destination_root = self._normalize_target_dir(target_dir) + destination_root.mkdir(parents=True, exist_ok=True) + + imported = [] + errors = [] + + for path in file_paths: + src = Path(path) + if not src.exists(): + errors.append(f"文件不存在: {src}") + continue + + dst = self._get_unique_destination_path(destination_root, src.name) + try: + if src.is_dir(): + shutil.copytree(src, dst) + else: + shutil.copy2(src, dst) + imported.append(dst) + except Exception as e: + errors.append(f"导入失败 {src}: {e}") + + if imported: + self.force_refresh() + + return imported, errors + + def move_files_to_directory(self, file_paths: List[Path], target_dir: Optional[Path]): + """资源管理器内部移动文件/文件夹,返回(成功列表, 错误列表)。""" + destination_root = self._normalize_target_dir(target_dir) + destination_root.mkdir(parents=True, exist_ok=True) + + moved = [] + errors = [] + moved_map = {} + + destination_root_resolved = destination_root.resolve() + + for path in file_paths: + src = Path(path) + if not src.exists(): + errors.append(f"源文件不存在: {src}") + continue + + try: + src_resolved = src.resolve() + except Exception: + src_resolved = src + + if src_resolved == destination_root_resolved: + continue + if src_resolved.parent == destination_root_resolved: + continue + + if src.is_dir(): + try: + if destination_root_resolved.is_relative_to(src_resolved): + errors.append(f"不能将目录移动到其子目录中: {src}") + continue + except Exception: + # Python 兼容保护:is_relative_to 不可用时跳过该校验 + pass + + dst = self._get_unique_destination_path(destination_root, src.name) + try: + shutil.move(str(src), str(dst)) + moved.append(dst) + moved_map[src_resolved] = dst.resolve() + except Exception as e: + errors.append(f"移动失败 {src}: {e}") + + if moved: + updated_selection = set() + for selected in self.selected_files: + try: + selected_resolved = selected.resolve() + except Exception: + selected_resolved = selected + if selected_resolved in moved_map: + updated_selection.add(moved_map[selected_resolved]) + elif selected.exists(): + updated_selection.add(selected) + self.selected_files = updated_selection + if self.focused_file: + try: + focused_resolved = self.focused_file.resolve() + except Exception: + focused_resolved = self.focused_file + if focused_resolved in moved_map: + self.focused_file = moved_map[focused_resolved] + + self.force_refresh() + + return moved, errors def start_drag(self, files: List[Path]): """开始拖拽操作""" - self.dragged_files = files.copy() + self.dragged_files = [Path(f) for f in files if Path(f).exists()] def clear_drag(self): """清除拖拽状态""" @@ -364,4 +492,4 @@ class ResourceManager: for file_path in self.dragged_files: if file_path.suffix.lower() in supported_extensions: return True - return False \ No newline at end of file + return False diff --git a/imgui.ini b/imgui.ini index a75a3c70..64ff275b 100644 --- a/imgui.ini +++ b/imgui.ini @@ -24,26 +24,26 @@ Size=832,45 Collapsed=0 [Window][工具栏] -Pos=273,20 -Size=1276,32 +Pos=327,20 +Size=1862,32 Collapsed=0 DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=271,634 +Size=325,854 Collapsed=0 DockId=0x00000007,0 [Window][属性面板] -Pos=1551,20 -Size=369,989 +Pos=2191,20 +Size=369,1331 Collapsed=0 DockId=0x00000003,0 [Window][控制台] -Pos=0,656 -Size=271,353 +Pos=0,876 +Size=325,475 Collapsed=0 DockId=0x00000008,0 @@ -60,7 +60,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=1920,989 +Size=2560,1331 Collapsed=0 [Window][测试窗口1] @@ -99,8 +99,8 @@ Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=273,817 -Size=1276,192 +Pos=327,1013 +Size=1862,338 Collapsed=0 DockId=0x00000006,0 @@ -201,16 +201,16 @@ Size=600,400 Collapsed=0 [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1331 Split=X DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1549,989 Split=X - DockNode ID=0x00000009 Parent=0x00000001 SizeRef=271,989 Split=Y Selected=0xE0015051 + DockNode ID=0x00000009 Parent=0x00000001 SizeRef=325,989 Split=Y Selected=0xE0015051 DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051 DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753 - DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1276,989 Split=Y + DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1862,989 Split=Y DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006 DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y - DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,761 CentralNode=1 - DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,192 Selected=0x3A2E05C3 + DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,957 CentralNode=1 + DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,338 Selected=0x3A2E05C3 DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=369,989 Split=Y Selected=0x3188AB8D DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37 DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7 diff --git a/main.py b/main.py index e15559bd..9e5e6afc 100644 --- a/main.py +++ b/main.py @@ -14,8 +14,8 @@ from imgui_bundle import imgui, imgui_ctx import sys import os import warnings -import threading -import time +import ctypes +from ctypes import wintypes from pathlib import Path # 导入MyWorld类和必要的模块 @@ -49,54 +49,203 @@ from TransformGizmo.transform_gizmo import TransformGizmo # 拖拽监控类 class DragDropMonitor: - """拖拽文件监控器""" - + """Windows 文件拖拽桥接,收集外部文件拖入事件。""" + + WM_DROPFILES = 0x0233 + WM_NCDESTROY = 0x0082 + GWL_WNDPROC = -4 + def __init__(self, world): self.world = world - self.running = False - self.thread = None - - # 支持的文件格式 - self.supported_formats = ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'] - + self.enabled = False + self._hwnd = None + self._window_proc = None + self._old_wndproc = None + self._set_wndproc = None + self._user32 = None + self._shell32 = None + self._pending_events = [] + self._hook_wndproc_ptr = None + def start(self): - """启动监控""" - if not self.running: - self.running = True - self.thread = threading.Thread(target=self._monitor_loop, daemon=True) - self.thread.start() - + """启动系统文件拖拽捕获。""" + if self.enabled: + return + + if os.environ.get("EG_DISABLE_NATIVE_DROP") == "1": + print("DragDropMonitor: 已通过 EG_DISABLE_NATIVE_DROP 禁用原生拖拽") + return + + if os.name != "nt": + print("DragDropMonitor: 非 Windows 平台,未启用系统拖拽桥接") + return + + try: + win_handle = self.world.win.getWindowHandle() if self.world and self.world.win else None + if not win_handle: + return + + self._hwnd = int(win_handle.getIntHandle()) + if not self._hwnd: + return + + self._user32 = ctypes.windll.user32 + self._shell32 = ctypes.windll.shell32 + + long_ptr_t = ctypes.c_ssize_t + wparam_t = wintypes.WPARAM + lparam_t = wintypes.LPARAM + wndproc_t = ctypes.WINFUNCTYPE( + long_ptr_t, wintypes.HWND, wintypes.UINT, wparam_t, lparam_t + ) + + if ctypes.sizeof(ctypes.c_void_p) == 8: + self._set_wndproc = self._user32.SetWindowLongPtrW + get_wndproc = self._user32.GetWindowLongPtrW + else: + self._set_wndproc = self._user32.SetWindowLongW + get_wndproc = self._user32.GetWindowLongW + + self._set_wndproc.argtypes = [wintypes.HWND, ctypes.c_int, long_ptr_t] + self._set_wndproc.restype = long_ptr_t + get_wndproc.argtypes = [wintypes.HWND, ctypes.c_int] + get_wndproc.restype = long_ptr_t + + self._user32.CallWindowProcW.argtypes = [ + ctypes.c_void_p, wintypes.HWND, wintypes.UINT, wparam_t, lparam_t + ] + self._user32.CallWindowProcW.restype = long_ptr_t + self._user32.IsWindow.argtypes = [wintypes.HWND] + self._user32.IsWindow.restype = wintypes.BOOL + + self._shell32.DragQueryFileW.argtypes = [wintypes.HANDLE, wintypes.UINT, wintypes.LPWSTR, wintypes.UINT] + self._shell32.DragQueryFileW.restype = wintypes.UINT + self._shell32.DragQueryPoint.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.POINT)] + self._shell32.DragQueryPoint.restype = wintypes.BOOL + self._shell32.DragFinish.argtypes = [wintypes.HANDLE] + self._shell32.DragFinish.restype = None + self._shell32.DragAcceptFiles.argtypes = [wintypes.HWND, wintypes.BOOL] + self._shell32.DragAcceptFiles.restype = None + + self._old_wndproc = get_wndproc(wintypes.HWND(self._hwnd), self.GWL_WNDPROC) + if not self._old_wndproc: + raise RuntimeError("GetWindowLongPtrW returned null WndProc") + + def _window_proc(hwnd, msg, wparam, lparam): + try: + if msg == self.WM_DROPFILES: + files, drop_pos = self._extract_drop_files(wparam) + if files: + self._pending_events.append((files, drop_pos)) + return 0 + if msg == self.WM_NCDESTROY: + result = self._user32.CallWindowProcW( + ctypes.c_void_p(self._old_wndproc), hwnd, msg, wparam, lparam + ) + self.enabled = False + self._hwnd = None + self._old_wndproc = None + self._hook_wndproc_ptr = None + return result + except Exception as cb_err: + print(f"[DragDropMonitor] 回调异常: {cb_err}") + + if not self._old_wndproc: + return 0 + return self._user32.CallWindowProcW( + ctypes.c_void_p(self._old_wndproc), hwnd, msg, wparam, lparam + ) + + self._window_proc = wndproc_t(_window_proc) + self._hook_wndproc_ptr = ctypes.cast(self._window_proc, ctypes.c_void_p).value + if not self._hook_wndproc_ptr: + raise RuntimeError("无法获取 WndProc 回调指针") + + self._set_wndproc( + wintypes.HWND(self._hwnd), + self.GWL_WNDPROC, + self._hook_wndproc_ptr, + ) + installed_wndproc = get_wndproc(wintypes.HWND(self._hwnd), self.GWL_WNDPROC) + if int(installed_wndproc) != int(self._hook_wndproc_ptr): + raise RuntimeError("WndProc 安装校验失败") + + self._shell32.DragAcceptFiles(wintypes.HWND(self._hwnd), True) + self.enabled = True + print("拖拽监控已启用") + except Exception as e: + print(f"拖拽监控启动失败: {e}") + self.enabled = False + def stop(self): - """停止监控""" - self.running = False - if self.thread: - self.thread.join() - - def _monitor_loop(self): - """监控循环""" - while self.running: + """恢复窗口过程并停止拖拽捕获。""" + if not self.enabled or os.name != "nt": + return + + try: + if self._shell32 and self._hwnd: + self._shell32.DragAcceptFiles(wintypes.HWND(self._hwnd), False) + if ( + self._set_wndproc + and self._hwnd + and self._old_wndproc + and self._user32 + and self._user32.IsWindow(wintypes.HWND(self._hwnd)) + ): + self._set_wndproc( + wintypes.HWND(self._hwnd), + self.GWL_WNDPROC, + self._old_wndproc, + ) + except Exception: + pass + finally: + self.enabled = False + self._window_proc = None + self._hook_wndproc_ptr = None + + def _extract_drop_files(self, hdrop): + """从 Win32 HDROP 中提取文件路径和投放点。""" + files = [] + drop_pos = None + if not self._shell32: + return files, drop_pos + + hdrop_handle = wintypes.HANDLE(int(hdrop)) + if not hdrop_handle: + return files, drop_pos + + try: + file_count = self._shell32.DragQueryFileW(hdrop_handle, 0xFFFFFFFF, None, 0) + for i in range(file_count): + path_len = self._shell32.DragQueryFileW(hdrop_handle, i, None, 0) + buffer = ctypes.create_unicode_buffer(path_len + 1) + self._shell32.DragQueryFileW(hdrop_handle, i, buffer, path_len + 1) + files.append(buffer.value) + + point = wintypes.POINT() + if self._shell32.DragQueryPoint(hdrop_handle, ctypes.byref(point)): + drop_pos = (int(point.x), int(point.y)) + except Exception as e: + print(f"[DragDropMonitor] 解析拖拽数据失败: {e}") + finally: 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 - + self._shell32.DragFinish(hdrop_handle) + except Exception: + pass + + return files, drop_pos + def add_file_from_external(self, file_path): - """从外部添加文件路径(用于系统级拖拽集成)""" - if self.world: - self.add_dragged_file(file_path) + """手动压入外部文件(保留给其他集成调用)。""" + if file_path: + self._pending_events.append(([str(file_path)], None)) + + def pop_pending_events(self): + """弹出并清空待处理的外部拖拽事件。""" + events = list(self._pending_events) + self._pending_events.clear() + return events try: # 尝试导入视频管理器,避免循环导入 @@ -383,6 +532,14 @@ class MyWorld(CoreWorld): self.is_dragging = False self.show_drag_overlay = False self.drag_drop_monitor = None + self._resource_manager_window_rect = None + self._resource_drop_targets = [] + self._scene_tree_window_rect = None + self._property_panel_window_rect = None + self._script_panel_window_rect = None + self._console_window_rect = None + self._toolbar_window_rect = None + self._drag_scene_tree_hover_node = None self.showLUIEditor = not self.use_ssbo_mouse_picking # 导入功能状态 @@ -717,12 +874,25 @@ class MyWorld(CoreWorld): display_size = imgui.get_io().display_size window_width = display_size.x window_height = display_size.y + + # 每帧重置窗口命中区域,由各面板绘制时重新填充 + self._resource_manager_window_rect = None + self._resource_drop_targets = [] + self._scene_tree_window_rect = None + self._property_panel_window_rect = None + self._script_panel_window_rect = None + self._console_window_rect = None + self._toolbar_window_rect = None + self._drag_scene_tree_hover_node = None # 绘制菜单栏 self._draw_menu_bar() # 绘制停靠布局 self._draw_docked_layout(window_width, window_height) + + # 处理系统层外部拖入 + self._process_external_drop_events() # 绘制纹理测试窗口 if self.testTexture: @@ -784,12 +954,6 @@ class MyWorld(CoreWorld): # 更新变换监控 dt = imgui.get_io().delta_time self.update_transform_monitoring(dt) - - # 检查鼠标释放事件(用于处理拖拽结束) - if imgui.is_mouse_released(0) and self.is_dragging: - if not imgui.is_any_window_hovered(): - # 在3D视图中释放,处理模型导入 - self._handle_drag_drop_completion() def _draw_docked_layout(self, window_width, window_height): """绘制可停靠的布局(支持拖拽)""" @@ -1342,16 +1506,108 @@ class MyWorld(CoreWorld): def _refresh_heightmap_browser(self, *args, **kwargs): return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs) + def setup_drag_drop_support(self): - """设置拖拽支持""" + """初始化拖拽支持。""" try: - # 启动拖拽监控线程 self.drag_drop_monitor = DragDropMonitor(self) self.drag_drop_monitor.start() - print("✓ 拖拽监控已启动") + print("拖拽监控已启用") except Exception as e: - print(f"⚠ 拖拽监控启动失败: {e}") - + print(f"拖拽监控启动失败: {e}") + + def _is_point_in_resource_manager(self, drop_pos): + """判断投放坐标是否命中资源管理器窗口。""" + if not drop_pos: + return False + rect = getattr(self, "_resource_manager_window_rect", None) + if not rect: + return False + + x, y = drop_pos + rx, ry, rw, rh = rect + return (rx <= x <= rx + rw) and (ry <= y <= ry + rh) + + def _resolve_resource_drop_target_dir(self, drop_pos): + """根据投放坐标解析资源管理器中的目标目录。""" + rm = getattr(self, "resource_manager", None) + default_dir = rm.current_path if rm else None + targets = getattr(self, "_resource_drop_targets", None) or [] + if not drop_pos or not targets: + return default_dir + + x, y = drop_pos + hits = [] + for tx, ty, tw, th, path in targets: + if tx <= x <= tx + tw and ty <= y <= ty + th: + hits.append((tw * th, path)) + + if not hits: + return default_dir + + # Pick the smallest hit-area target to prefer deeper folder rows. + hits.sort(key=lambda item: item[0]) + target_path = Path(hits[0][1]) + if target_path.exists() and target_path.is_dir(): + return target_path + return default_dir + + def _process_external_drop_events(self): + """处理外部拖入(系统文件拖拽)事件。""" + if not self.drag_drop_monitor: + return + + events = self.drag_drop_monitor.pop_pending_events() + if not events: + return + + for file_paths, drop_pos in events: + self._handle_external_drop(file_paths, drop_pos) + + def _handle_external_drop(self, file_paths, drop_pos=None): + """把外部拖入文件分发到资源管理器或场景。""" + if not file_paths: + return + + paths = [Path(p) for p in file_paths if p] + if not paths: + return + + if self._is_point_in_resource_manager(drop_pos): + target_dir = self._resolve_resource_drop_target_dir(drop_pos) + imported, errors = self.resource_manager.import_external_files(paths, target_dir=target_dir) + if imported: + target_label = self.resource_manager.get_relative_path(target_dir) if target_dir else "资源管理器" + self.add_success_message(f"已导入 {len(imported)} 个外部资源到 {target_label}") + if errors: + self.add_warning_message(f"有 {len(errors)} 个资源导入失败,请查看控制台日志") + for err in errors: + print(f"[资源导入] {err}") + return + + supported_formats = {'.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'} + imported_count = 0 + last_imported_model = None + for path in paths: + if path.exists() and path.suffix.lower() in supported_formats: + try: + model_node = self._import_model_for_runtime(str(path), prefer_scene_manager=True) + if model_node: + imported_count += 1 + last_imported_model = model_node + except Exception as e: + print(f"[外部拖拽] 导入模型失败 {path}: {e}") + + if imported_count > 0: + if self.selection and last_imported_model: + try: + self.selection.updateSelection(last_imported_model) + except Exception as e: + print(f"[外部拖拽] 更新选择失败: {e}") + self.add_success_message(f"外部拖拽导入场景成功: {imported_count} 个模型") + else: + self.add_info_message("将文件拖放到“资源管理器”窗口可导入到项目资源") + def add_dragged_file(self, file_path): """添加拖拽的文件""" if file_path not in self.dragged_files: @@ -1400,7 +1656,7 @@ class MyWorld(CoreWorld): return False # 导入模型 - model_node = self._import_model_for_runtime(file_path) + model_node = self._import_model_for_runtime(file_path, prefer_scene_manager=True) if model_node: # 应用材质确保颜色正常 @@ -1409,8 +1665,8 @@ class MyWorld(CoreWorld): # 设置模型位置 model_node.setPos(0, 0, 0) - # 添加到选择系统 - self.selection.select_node(model_node) + # 更新当前选中模型 + self.selection.updateSelection(model_node) self.add_message("success", f"成功导入模型: {os.path.basename(file_path)}") return True diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index 8c6143af..8a3170c3 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -1025,13 +1025,28 @@ class AppActions: # ==================== 路径浏览器辅助方法 ==================== - def _import_model_for_runtime(self, file_path): + def _import_model_for_runtime(self, file_path, prefer_scene_manager=False): """Import model through the active runtime path. SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model). Legacy mode: load via SceneManager. """ + if prefer_scene_manager: + if hasattr(self, 'scene_manager') and self.scene_manager: + return self.scene_manager.importModel(file_path) + return None + if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None): try: + # Clear selection/gizmo first to avoid dangling references to soon-to-be removed nodes. + if hasattr(self, 'selection') and self.selection: + try: + self.selection.clearSelection() + except Exception: + try: + self.selection.updateSelection(None) + except Exception: + pass + # Remove legacy scene-manager models to avoid duplicate rendering if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'): for m in list(self.scene_manager.models): diff --git a/ui/panels/editor_panels.py b/ui/panels/editor_panels.py index cd944023..689316e4 100644 --- a/ui/panels/editor_panels.py +++ b/ui/panels/editor_panels.py @@ -340,6 +340,14 @@ class EditorPanels: with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags): self.app.showSceneTree = True # 确保窗口保持打开 + window_pos = imgui.get_window_pos() + window_size = imgui.get_window_size() + self.app._scene_tree_window_rect = ( + float(window_pos.x), + float(window_pos.y), + float(window_size.x), + float(window_size.y), + ) imgui.text("场景层级") imgui.separator() @@ -465,6 +473,9 @@ class EditorPanels: # Clear LUI selection when a scene node is selected if hasattr(self.app, 'lui_manager'): self.app.lui_manager.selected_index = -1 + + if self.app.is_dragging and imgui.is_item_hovered(): + self.app._drag_scene_tree_hover_node = node # 右键菜单 if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): @@ -579,20 +590,31 @@ class EditorPanels: def _draw_resource_manager(self): """绘制资源管理器面板""" - # 使用面板类型的窗口标志,支持docking flags = self.app.style_manager.get_window_flags("panel") - + with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags): - self.app.showResourceManager = True # 确保窗口保持打开 - - # 获取资源管理器实例 + self.app.showResourceManager = True rm = self.app.resource_manager - - # 工具栏 + + window_pos = imgui.get_window_pos() + window_size = imgui.get_window_size() + self.app._resource_manager_window_rect = ( + float(window_pos.x), + float(window_pos.y), + float(window_size.x), + float(window_size.y), + ) + self.app._resource_drop_targets.append(( + float(window_pos.x), + float(window_pos.y), + float(window_size.x), + float(window_size.y), + str(rm.current_path), + )) + imgui.text("文件浏览器") imgui.separator() - - # 导航按钮 + if imgui.button("◀"): rm.navigate_back() imgui.same_line() @@ -628,115 +650,89 @@ class EditorPanels: # 搜索框 changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) - + imgui.separator() - - # 检查自动刷新 + if rm.refresh_if_needed(): - # 目录内容发生变化,可以在这里添加通知逻辑 pass - - # 获取目录内容 + dirs, files = rm.get_directory_contents(rm.current_path) - - # 显示目录 + for dir_path in dirs: if not rm.should_show_file(dir_path): continue - - # 目录图标和名称 + icon_name = rm.get_file_icon(dir_path.name, is_folder=True) - node_open = False - - # 检查是否被选中 is_selected = dir_path in rm.selected_files - - # 使用TreeNode来显示目录 + if is_selected: imgui.push_style_color(imgui.Col_.header, (100/255, 150/255, 200/255, 1.0)) - - # 尝试加载PNG图标 + icon_texture = None try: - # 直接使用图标名称,load_icon会自动添加.png icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}") except: pass - + if icon_texture: - # 使用PNG图标 imgui.image(icon_texture, (16, 16)) imgui.same_line() - node_open = imgui.tree_node(f"{dir_path.name}") + node_open = imgui.tree_node(f"{dir_path.name}##dir_{dir_path}") else: - # 回退到文本标识符 - node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}") - + node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}##dir_{dir_path}") + if is_selected: imgui.pop_style_color() - - # 处理选择 + + self._append_drop_target_from_last_item(dir_path) + self._start_resource_drag_if_needed(rm, dir_path) + if imgui.is_item_clicked(): if imgui.get_io().key_ctrl: - # 多选模式 if is_selected: rm.selected_files.discard(dir_path) else: rm.selected_files.add(dir_path) else: - # 单选模式 rm.selected_files.clear() rm.selected_files.add(dir_path) rm.focused_file = dir_path - - # 双击导航 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): rm.navigate_to(dir_path) - - # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): rm.show_context_menu = True rm.context_menu_file = dir_path rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - - # 如果节点展开,显示子内容 + imgui.open_popup("resource_context_menu") + if node_open: - # 获取子目录内容 subdirs, subfiles = rm.get_directory_contents(dir_path) - - # 显示子目录 + for subdir in subdirs: if not rm.should_show_file(subdir): continue - - # 初始化变量 - subicon_name = "folder" - sub_is_selected = False - - # 获取子目录图标名称 + subicon_name = rm.get_file_icon(subdir.name, is_folder=True) sub_is_selected = subdir in rm.selected_files - - # 尝试加载PNG图标 + subicon_texture = None try: subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}") except: pass - + if subicon_texture: - # 使用PNG图标 imgui.image(subicon_texture, (16, 16)) imgui.same_line() - sub_node_open = imgui.tree_node(f" {subdir.name}") + sub_node_open = imgui.tree_node(f" {subdir.name}##dir_{subdir}") else: - # 回退到文本标识符 - sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}") - - if sub_is_selected: - imgui.pop_style_color() - - # 处理子目录的选择 + sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}##dir_{subdir}") + + self._append_drop_target_from_last_item(subdir) + self._start_resource_drag_if_needed(rm, subdir) + if imgui.is_item_clicked(): if imgui.get_io().key_ctrl: if sub_is_selected: @@ -747,46 +743,43 @@ class EditorPanels: rm.selected_files.clear() rm.selected_files.add(subdir) rm.focused_file = subdir - - # 双击子目录导航 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): rm.navigate_to(subdir) - - # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): rm.show_context_menu = True rm.context_menu_file = subdir rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - + imgui.open_popup("resource_context_menu") + if sub_node_open: imgui.tree_pop() - - # 显示子文件 + for subfile in subfiles: if not rm.should_show_file(subfile): continue - + subicon_name = rm.get_file_icon(subfile.name) sub_is_selected = subfile in rm.selected_files - - # 尝试加载PNG图标 + subicon_texture = None try: subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}") except: pass - + if subicon_texture: - # 使用PNG图标 imgui.image(subicon_texture, (16, 16)) imgui.same_line() - selected = imgui.selectable(f" {subfile.name}", sub_is_selected) + selected = imgui.selectable(f" {subfile.name}##file_{subfile}", sub_is_selected) else: - # 回退到文本标识符 - selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}", sub_is_selected) - - # 处理子文件的选择 - if selected: + selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}##file_{subfile}", sub_is_selected) + + selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected) + self._start_resource_drag_if_needed(rm, subfile) + + if selected_clicked: if imgui.get_io().key_ctrl: if sub_is_selected: rm.selected_files.discard(subfile) @@ -796,99 +789,142 @@ class EditorPanels: rm.selected_files.clear() rm.selected_files.add(subfile) rm.focused_file = subfile - - # 双击子文件操作 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - if subfile.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + if self._is_model_file(subfile): self.app._import_model_for_runtime(str(subfile)) self.app.add_info_message(f"正在导入模型: {subfile.name}") else: rm.open_file(subfile) - - # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): rm.show_context_menu = True rm.context_menu_file = subfile rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - - # 只有在节点展开时才调用tree_pop - if node_open: - imgui.tree_pop() - - - - # 处理拖拽开始 - if imgui.is_item_active() and imgui.is_item_hovered(): - if imgui.is_mouse_dragging(0): - # 开始拖拽 - drag_files = list(rm.selected_files) if rm.selected_files else [file_path] - rm.start_drag(drag_files) - self.app.is_dragging = True - self.app.show_drag_overlay = True - - # 双击打开文件 + imgui.open_popup("resource_context_menu") + + imgui.tree_pop() + + for file_path in files: + if not rm.should_show_file(file_path): + continue + + icon_name = rm.get_file_icon(file_path.name) + is_selected = file_path in rm.selected_files + + icon_texture = None + try: + icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}") + except: + pass + + if icon_texture: + imgui.image(icon_texture, (16, 16)) + imgui.same_line() + selected = imgui.selectable(f"{file_path.name}##file_{file_path}", is_selected) + else: + selected = imgui.selectable(f"[{icon_name.upper()}] {file_path.name}##file_{file_path}", is_selected) + + selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected) + self._start_resource_drag_if_needed(rm, file_path) + + if selected_clicked: + if imgui.get_io().key_ctrl: + if is_selected: + rm.selected_files.discard(file_path) + else: + rm.selected_files.add(file_path) + else: + rm.selected_files.clear() + rm.selected_files.add(file_path) + rm.focused_file = file_path + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - # 检查是否是支持的3D模型格式 - if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: - # 导入3D模型 + if self._is_model_file(file_path): self.app.add_info_message(f"正在导入模型: {file_path.name}") self.app._import_model_for_runtime(str(file_path)) else: - # 使用系统默认程序打开 rm.open_file(file_path) - - # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): rm.show_context_menu = True rm.context_menu_file = file_path rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - - # 右键菜单 - if rm.show_context_menu and rm.context_menu_file: + imgui.open_popup("resource_context_menu") + + if rm.context_menu_file: imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1])) - with imgui_ctx.begin_popup("context_menu", imgui.WindowFlags_.no_title_bar | - imgui.WindowFlags_.no_resize | imgui.WindowFlags_.always_auto_resize) as popup: - if popup: - if rm.context_menu_file.is_dir(): - if imgui.menu_item("打开"): - rm.navigate_to(rm.context_menu_file) - imgui.separator() - if imgui.menu_item("重命名"): - print(f"重命名文件夹: {rm.context_menu_file.name}") - if imgui.menu_item("删除"): - print(f"删除文件夹: {rm.context_menu_file.name}") + + if imgui.begin_popup("resource_context_menu"): + if rm.context_menu_file and rm.context_menu_file.is_dir(): + if imgui.menu_item("打开")[1]: + rm.navigate_to(rm.context_menu_file) + imgui.separator() + if imgui.menu_item("重命名")[1]: + print(f"重命名文件夹: {rm.context_menu_file.name}") + if imgui.menu_item("删除")[1]: + print(f"删除文件夹: {rm.context_menu_file.name}") + elif rm.context_menu_file: + if imgui.menu_item("打开")[1]: + rm.open_file(rm.context_menu_file) + imgui.separator() + if imgui.menu_item("导入到场景")[1]: + if self._is_model_file(rm.context_menu_file): + self.app.add_info_message(f"正在导入模型: {rm.context_menu_file.name}") + self.app._import_model_for_runtime(str(rm.context_menu_file)) + if imgui.menu_item("重命名")[1]: + print(f"重命名文件: {rm.context_menu_file.name}") + if imgui.menu_item("删除")[1]: + print(f"删除文件: {rm.context_menu_file.name}") + + if rm.context_menu_file: + imgui.separator() + if imgui.menu_item("复制路径")[1]: + imgui.set_clipboard_text(str(rm.context_menu_file)) + self.app.add_info_message("路径已复制到剪贴板") + if imgui.menu_item("在文件管理器中显示")[1]: + import platform + import subprocess + if platform.system() == "Windows": + subprocess.run(["explorer", "/select,", str(rm.context_menu_file)]) + elif platform.system() == "Darwin": + subprocess.run(["open", "-R", str(rm.context_menu_file)]) else: - if imgui.menu_item("打开"): - rm.open_file(rm.context_menu_file) - imgui.separator() - if imgui.menu_item("导入到场景"): - if rm.context_menu_file.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: - self.app.add_info_message(f"正在导入模型: {rm.context_menu_file.name}") - self.app._import_model_for_runtime(str(rm.context_menu_file)) - if imgui.menu_item("重命名"): - print(f"重命名文件: {rm.context_menu_file.name}") - if imgui.menu_item("删除"): - print(f"删除文件: {rm.context_menu_file.name}") - - imgui.separator() - if imgui.menu_item("复制路径"): - imgui.set_clipboard_text(str(rm.context_menu_file)) - self.app.add_info_message("路径已复制到剪贴板") - if imgui.menu_item("在文件管理器中显示"): - import platform - import subprocess - if platform.system() == "Windows": - subprocess.run(["explorer", "/select,", str(rm.context_menu_file)]) - elif platform.system() == "Darwin": - subprocess.run(["open", "-R", str(rm.context_menu_file)]) - else: - subprocess.run(["xdg-open", str(rm.context_menu_file.parent)]) - - # 如果点击其他地方,关闭菜单 - if imgui.is_mouse_clicked(0) or imgui.is_mouse_clicked(1): - if not imgui.is_window_hovered(): - rm.show_context_menu = False - rm.context_menu_file = None + subprocess.run(["xdg-open", str(rm.context_menu_file.parent)]) + + if imgui.is_mouse_clicked(0) and not imgui.is_window_hovered(): + rm.context_menu_file = None + rm.show_context_menu = False + imgui.close_current_popup() + + imgui.end_popup() + + @staticmethod + def _is_model_file(path: Path) -> bool: + return path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'] + + def _append_drop_target_from_last_item(self, target_path: Path): + item_min = imgui.get_item_rect_min() + item_max = imgui.get_item_rect_max() + width = float(item_max.x - item_min.x) + height = float(item_max.y - item_min.y) + if width <= 0 or height <= 0: + return + self.app._resource_drop_targets.append(( + float(item_min.x), + float(item_min.y), + width, + height, + str(target_path), + )) + + def _start_resource_drag_if_needed(self, rm, fallback_path: Path): + if not imgui.is_item_active() or not imgui.is_mouse_dragging(0): + return + drag_files = list(rm.selected_files) if rm.selected_files else [fallback_path] + rm.start_drag(drag_files) + self.app.is_dragging = True + self.app.show_drag_overlay = True def _draw_property_panel(self): diff --git a/ui/panels/interaction_panels.py b/ui/panels/interaction_panels.py index 7e12dca8..1438479e 100644 --- a/ui/panels/interaction_panels.py +++ b/ui/panels/interaction_panels.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from imgui_bundle import imgui, imgui_ctx @@ -19,129 +20,173 @@ class InteractionPanels: def _draw_drag_drop_interface(self): - """绘制拖拽界面""" - # 检查资源管理器的拖拽状态 + """处理资源管理器到场景/树的拖拽完成。""" if self.resource_manager.is_dragging(): self.is_dragging = True self.dragged_files = self.resource_manager.get_dragged_files() - self.show_drag_overlay = True - - # 绘制拖拽覆盖层 - if self.show_drag_overlay: - self._draw_drag_overlay() - - # 检查是否有拖拽的文件需要处理 - if self.is_dragging and self.dragged_files: - # 显示拖拽状态 - self._draw_drag_status() - - # 检查是否释放鼠标(结束拖拽) - if imgui.is_mouse_released(0): - self._handle_drag_drop_completion() - + + if self.is_dragging and self.dragged_files and imgui.is_mouse_released(0): + self._handle_drag_drop_completion() + def _handle_drag_drop_completion(self): - """处理拖拽完成""" - # 检查是否在3D视图中释放 + """处理资源管理器内部拖拽落点。""" + dragged_files = list(self.dragged_files) if self.dragged_files else self.resource_manager.get_dragged_files() + if not dragged_files: + self._reset_drag_state() + return + mouse_pos = imgui.get_mouse_pos() - viewport = imgui.get_main_viewport() - - # 简单检查:如果不在任何ImGui窗口上,则认为是在3D视图中 - if not imgui.is_any_window_hovered(): - # 导入支持的3D模型文件 - imported_count = 0 - for file_path in self.dragged_files: - if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + drop_kind, target_dir, target_parent = self._resolve_drop_target(mouse_pos) + + if drop_kind == "resource_manager": + moved, errors = self.resource_manager.move_files_to_directory(dragged_files, target_dir) + if moved: + label = self.resource_manager.get_relative_path(target_dir) if target_dir else "资源管理器" + self.add_success_message(f"已移动 {len(moved)} 个资源到 {label}") + if errors: + self.add_warning_message(f"有 {len(errors)} 个资源移动失败,请查看控制台") + for err in errors: + print(f"[资源移动] {err}") + self._reset_drag_state() + return + + if drop_kind not in ("scene_view", "scene_tree"): + self._reset_drag_state() + return + + imported_models = [] + for file_path in dragged_files: + path = Path(file_path) + if not self._is_supported_model(path): + continue + try: + model_node = self._import_model_for_runtime(str(path), prefer_scene_manager=True) + if not model_node: + continue + if getattr(self, "scene_manager", None): try: - self._import_model_for_runtime(str(file_path)) - self.add_success_message(f"成功导入模型: {file_path.name}") - imported_count += 1 - except Exception as e: - self.add_error_message(f"导入模型失败 {file_path.name}: {e}") - - if imported_count > 0: - self.add_success_message(f"共导入 {imported_count} 个模型") - - # 清除拖拽状态 + self.scene_manager.processMaterials(model_node) + except Exception: + pass + if drop_kind == "scene_tree" and self._is_valid_drop_parent(target_parent): + try: + model_node.wrtReparentTo(target_parent) + except Exception: + model_node.reparentTo(target_parent) + self._ensure_unique_name(model_node) + imported_models.append(model_node) + except Exception as e: + print(f"[拖拽导入] 导入失败 {path}: {e}") + + if imported_models: + if self.selection: + try: + self.selection.updateSelection(imported_models[-1]) + except Exception: + pass + self.add_success_message(f"拖拽导入成功: {len(imported_models)} 个模型") + + self._reset_drag_state() + + + @staticmethod + def _point_in_rect(point, rect): + if not point or not rect: + return False + x, y = point + rx, ry, rw, rh = rect + return rx <= x <= rx + rw and ry <= y <= ry + rh + + + def _resolve_drop_target(self, mouse_pos): + point = (float(mouse_pos.x), float(mouse_pos.y)) + if self._point_in_rect(point, getattr(self, "_resource_manager_window_rect", None)): + target_dir = self._resolve_resource_drop_target_dir(point) + return "resource_manager", target_dir, None + + if self._point_in_rect(point, getattr(self, "_scene_tree_window_rect", None)): + return "scene_tree", None, getattr(self, "_drag_scene_tree_hover_node", None) + + for rect_name in ("_property_panel_window_rect", "_script_panel_window_rect", "_console_window_rect", "_toolbar_window_rect"): + if self._point_in_rect(point, getattr(self, rect_name, None)): + return "ui_panel", None, None + + if imgui.get_io().want_capture_mouse: + return "ui_panel", None, None + + return "scene_view", None, None + + + @staticmethod + def _is_supported_model(path): + return path.suffix.lower() in {'.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'} + + + @staticmethod + def _is_valid_drop_parent(node): + if not node: + return False + try: + return not node.isEmpty() + except Exception: + return False + + + def _ensure_unique_name(self, model_node): + if not model_node or model_node.isEmpty(): + return + parent = model_node.getParent() + if not parent or parent.isEmpty(): + return + + current_name = model_node.getName() or "model" + stem, suffix = os.path.splitext(current_name) + if not stem: + stem = current_name + + existing_names = set() + try: + siblings = parent.getChildren() + for i in range(siblings.getNumPaths()): + sibling = siblings.getPath(i) + if sibling == model_node: + continue + existing_names.add(sibling.getName()) + except Exception: + return + + if current_name not in existing_names: + return + + index = 1 + while True: + if suffix: + candidate = f"{stem}_{index}{suffix}" + else: + candidate = f"{stem}_{index}" + if candidate not in existing_names: + model_node.setName(candidate) + return + index += 1 + + + def _reset_drag_state(self): self.is_dragging = False self.dragged_files.clear() self.show_drag_overlay = False self.resource_manager.clear_drag() - + self._drag_scene_tree_hover_node = None + 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() - + """已弃用:保留兼容接口,不再绘制遮罩。""" + return + 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() + """已弃用:保留兼容接口,不再绘制状态窗。""" + return def _draw_context_menus(self):