diff --git a/Resources/models/box.glb b/Resources/models_1_1/box.glb similarity index 100% rename from Resources/models/box.glb rename to Resources/models_1_1/box.glb diff --git a/core/model_drag_drop.py b/core/model_drag_drop.py new file mode 100644 index 00000000..801e0754 --- /dev/null +++ b/core/model_drag_drop.py @@ -0,0 +1,576 @@ +""" +模型拖拽服务 + +集中管理: +1. 系统外部文件拖入窗口(Windows WM_DROPFILES) +2. 资源管理器内部拖拽(移动资源、拖入场景、拖入场景树) +3. 模型导入后的命名冲突处理 +""" + +from __future__ import annotations + +import ctypes +import os +from pathlib import Path +from typing import List, Optional, Tuple + +from imgui_bundle import imgui + +try: + from ctypes import wintypes +except Exception: # pragma: no cover - non-Windows fallback + wintypes = None + + +class NativeFileDropMonitor: + """Windows 文件拖拽桥接,收集外部文件拖入事件。""" + + WM_DROPFILES = 0x0233 + WM_NCDESTROY = 0x0082 + GWL_WNDPROC = -4 + + def __init__(self, world): + self.world = world + 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 self.enabled: + return + + if os.environ.get("EG_DISABLE_NATIVE_DROP") == "1": + print("DragDropMonitor: 已通过 EG_DISABLE_NATIVE_DROP 禁用原生拖拽") + return + + if os.name != "nt" or wintypes is None: + 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): + """恢复窗口过程并停止拖拽捕获。""" + if not self.enabled or os.name != "nt" or wintypes is None: + 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 or wintypes is None: + 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: + self._shell32.DragFinish(hdrop_handle) + except Exception: + pass + + return files, drop_pos + + def add_file_from_external(self, 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 + + +class ModelDragDropService: + """模型拖拽业务入口。""" + + SUPPORTED_MODEL_EXTS = {".gltf", ".glb", ".fbx", ".bam", ".egg", ".obj"} + + def __init__(self, app): + self.app = app + + def setup_drag_drop_support(self): + """初始化系统外部拖拽支持。""" + try: + monitor = NativeFileDropMonitor(self.app) + monitor.start() + self.app.drag_drop_monitor = monitor + except Exception as e: + print(f"拖拽监控启动失败: {e}") + + def is_point_in_resource_manager(self, drop_pos): + """判断投放坐标是否命中资源管理器窗口。""" + if not drop_pos: + return False + rect = getattr(self.app, "_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.app, "resource_manager", None) + default_dir = rm.current_path if rm else None + targets = getattr(self.app, "_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 + + 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): + """处理外部拖入(系统文件拖拽)事件。""" + monitor = getattr(self.app, "drag_drop_monitor", None) + if not monitor: + return + + events = 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.app.resource_manager.import_external_files(paths, target_dir=target_dir) + if imported: + target_label = self.app.resource_manager.get_relative_path(target_dir) if target_dir else "资源管理器" + self.app.add_success_message(f"已导入 {len(imported)} 个外部资源到 {target_label}") + if errors: + self.app.add_warning_message(f"有 {len(errors)} 个资源导入失败,请查看控制台日志") + for err in errors: + print(f"[资源导入] {err}") + return + + imported_models = [] + for path in paths: + if not path.exists() or not self._is_supported_model(path): + continue + try: + model_node = self.app._import_model_for_runtime(str(path), prefer_scene_manager=True) + if not model_node: + continue + self._postprocess_imported_model(model_node) + imported_models.append(model_node) + except Exception as e: + print(f"[外部拖拽] 导入模型失败 {path}: {e}") + + if imported_models: + if getattr(self.app, "selection", None): + try: + self.app.selection.updateSelection(imported_models[-1]) + except Exception as e: + print(f"[外部拖拽] 更新选择失败: {e}") + self.app.add_success_message(f"外部拖拽导入场景成功: {len(imported_models)} 个模型") + else: + self.app.add_info_message("将文件拖放到“资源管理器”窗口可导入到项目资源") + + def draw_drag_drop_interface(self): + """每帧处理资源管理器内部拖拽的释放事件。""" + rm = getattr(self.app, "resource_manager", None) + if not rm: + return + + if rm.is_dragging(): + self.app.is_dragging = True + self.app.dragged_files = rm.get_dragged_files() + + if self.app.is_dragging and self.app.dragged_files and imgui.is_mouse_released(0): + mouse_pos = imgui.get_mouse_pos() + self.handle_internal_drag_drop_completion((float(mouse_pos.x), float(mouse_pos.y))) + + def handle_internal_drag_drop_completion(self, mouse_pos: Optional[Tuple[float, float]] = None): + """处理资源管理器内部拖拽落点。""" + rm = self.app.resource_manager + dragged_files = list(self.app.dragged_files) if self.app.dragged_files else rm.get_dragged_files() + if not dragged_files: + self._reset_drag_state() + return + + drop_kind, target_dir, target_parent = self._resolve_drop_target(mouse_pos) + + if drop_kind == "resource_manager": + moved, errors = rm.move_files_to_directory(dragged_files, target_dir) + if moved: + label = rm.get_relative_path(target_dir) if target_dir else "资源管理器" + self.app.add_success_message(f"已移动 {len(moved)} 个资源到 {label}") + if errors: + self.app.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.app._import_model_for_runtime(str(path), prefer_scene_manager=True) + if not model_node: + continue + + 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._postprocess_imported_model(model_node) + imported_models.append(model_node) + except Exception as e: + print(f"[拖拽导入] 导入失败 {path}: {e}") + + if imported_models and getattr(self.app, "selection", None): + try: + self.app.selection.updateSelection(imported_models[-1]) + except Exception: + pass + + if imported_models: + self.app.add_success_message(f"拖拽导入成功: {len(imported_models)} 个模型") + + self._reset_drag_state() + + def add_dragged_file(self, file_path): + """添加拖拽的文件。""" + if file_path not in self.app.dragged_files: + self.app.dragged_files.append(file_path) + self.app.is_dragging = True + self.app.show_drag_overlay = True + print(f"检测到拖拽文件: {file_path}") + + def clear_dragged_files(self): + """清空拖拽文件列表。""" + self.app.dragged_files.clear() + self.app.is_dragging = False + self.app.show_drag_overlay = False + + def process_dragged_files(self): + """处理拖拽的文件。""" + if not self.app.dragged_files: + return + + imported_count = 0 + for file_path in self.app.dragged_files: + if self.import_model_from_path(file_path): + imported_count += 1 + + if imported_count > 0: + self.app.add_success_message(f"成功导入 {imported_count} 个模型文件") + else: + self.app.add_error_message("没有成功导入任何文件") + + self.clear_dragged_files() + + def import_model_from_path(self, file_path): + """从路径导入模型的内部方法。""" + try: + path = Path(file_path) + if not path.exists(): + self.app.add_error_message(f"文件不存在: {path}") + return False + + if not self._is_supported_model(path): + self.app.add_error_message(f"不支持的文件格式: {path.suffix.lower()}") + return False + + model_node = self.app._import_model_for_runtime(str(path), prefer_scene_manager=True) + if not model_node: + self.app.add_error_message(f"导入模型失败: {path}") + return False + + self._postprocess_imported_model(model_node, set_origin=True) + if getattr(self.app, "selection", None): + self.app.selection.updateSelection(model_node) + + self.app.add_success_message(f"成功导入模型: {path.name}") + return True + except Exception as e: + self.app.add_error_message(f"导入模型时发生错误: {e}") + return False + + @staticmethod + def draw_drag_overlay(): + """兼容接口:Unity 风格不再绘制拖拽遮罩。""" + return + + @staticmethod + def draw_drag_status(): + """兼容接口:Unity 风格不再绘制拖拽状态窗。""" + return + + def _postprocess_imported_model(self, model_node, set_origin=False): + if getattr(self.app, "scene_manager", None): + try: + self.app.scene_manager.processMaterials(model_node) + except Exception: + pass + if set_origin: + try: + model_node.setPos(0, 0, 0) + except Exception: + pass + self._ensure_unique_name(model_node) + + def _resolve_drop_target(self, mouse_pos: Optional[Tuple[float, float]]): + point = mouse_pos + if point is None: + m = imgui.get_mouse_pos() + point = (float(m.x), float(m.y)) + + if self._point_in_rect(point, getattr(self.app, "_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.app, "_scene_tree_window_rect", None)): + return "scene_tree", None, getattr(self.app, "_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.app, 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 _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 + + @classmethod + def _is_supported_model(cls, path: Path): + return path.suffix.lower() in cls.SUPPORTED_MODEL_EXTS + + @staticmethod + def _is_valid_drop_parent(node): + if not node: + return False + try: + return not node.isEmpty() + except Exception: + return False + + @staticmethod + def _ensure_unique_name(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: + candidate = f"{stem}_{index}{suffix}" if suffix else f"{stem}_{index}" + if candidate not in existing_names: + model_node.setName(candidate) + return + index += 1 + + def _reset_drag_state(self): + self.app.is_dragging = False + self.app.dragged_files.clear() + self.app.show_drag_overlay = False + if getattr(self.app, "resource_manager", None): + self.app.resource_manager.clear_drag() + self.app._drag_scene_tree_hover_node = None diff --git a/main.py b/main.py index 9e5e6afc..98298add 100644 --- a/main.py +++ b/main.py @@ -14,8 +14,6 @@ from imgui_bundle import imgui, imgui_ctx import sys import os import warnings -import ctypes -from ctypes import wintypes from pathlib import Path # 导入MyWorld类和必要的模块 @@ -44,209 +42,10 @@ from ui.panels.object_factory import ObjectFactory from ui.panels.animation_tools import AnimationTools from ui.panels.property_helpers import PropertyHelpers from ui.panels.app_actions import AppActions +from core.model_drag_drop import ModelDragDropService from ssbo_component.ssbo_editor import SSBOEditor 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.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 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): - """恢复窗口过程并停止拖拽捕获。""" - 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: - self._shell32.DragFinish(hdrop_handle) - except Exception: - pass - - return files, drop_pos - - def add_file_from_external(self, 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: # 尝试导入视频管理器,避免循环导入 import importlib.util @@ -540,6 +339,7 @@ class MyWorld(CoreWorld): self._console_window_rect = None self._toolbar_window_rect = None self._drag_scene_tree_hover_node = None + self.model_drag_drop = ModelDragDropService(self) self.showLUIEditor = not self.use_ssbo_mouse_picking # 导入功能状态 @@ -1509,174 +1309,39 @@ class MyWorld(CoreWorld): 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}") + return self.model_drag_drop.setup_drag_drop_support() 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) + return self.model_drag_drop.is_point_in_resource_manager(drop_pos) 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 + return self.model_drag_drop.resolve_resource_drop_target_dir(drop_pos) 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) + return self.model_drag_drop.process_external_drop_events() 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("将文件拖放到“资源管理器”窗口可导入到项目资源") + return self.model_drag_drop.handle_external_drop(file_paths, drop_pos) 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}") + return self.model_drag_drop.add_dragged_file(file_path) def clear_dragged_files(self): """清空拖拽文件列表""" - self.dragged_files.clear() - self.is_dragging = False - self.show_drag_overlay = False + return self.model_drag_drop.clear_dragged_files() 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() + return self.model_drag_drop.process_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._import_model_for_runtime(file_path, prefer_scene_manager=True) - - if model_node: - # 应用材质确保颜色正常 - self.scene_manager.processMaterials(model_node) - - # 设置模型位置 - model_node.setPos(0, 0, 0) - - # 更新当前选中模型 - self.selection.updateSelection(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 + return self.model_drag_drop.import_model_from_path(file_path) def _draw_drag_drop_interface(self, *args, **kwargs): return self.interaction_panels._draw_drag_drop_interface(*args, **kwargs) diff --git a/ui/panels/interaction_panels.py b/ui/panels/interaction_panels.py index 1438479e..17c074c0 100644 --- a/ui/panels/interaction_panels.py +++ b/ui/panels/interaction_panels.py @@ -1,5 +1,3 @@ -import os -from pathlib import Path from imgui_bundle import imgui, imgui_ctx @@ -20,173 +18,21 @@ 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() - - if self.is_dragging and self.dragged_files and imgui.is_mouse_released(0): - self._handle_drag_drop_completion() + """委托到核心拖拽服务。""" + return self.model_drag_drop.draw_drag_drop_interface() def _handle_drag_drop_completion(self): - """处理资源管理器内部拖拽落点。""" - 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() - 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.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 + """委托到核心拖拽服务。""" + return self.model_drag_drop.handle_internal_drag_drop_completion() def _draw_drag_overlay(self): - """已弃用:保留兼容接口,不再绘制遮罩。""" - return + return self.model_drag_drop.draw_drag_overlay() def _draw_drag_status(self): - """已弃用:保留兼容接口,不再绘制状态窗。""" - return + return self.model_drag_drop.draw_drag_status() def _draw_context_menus(self):