模型拖拽

This commit is contained in:
ayuan9957 2026-02-26 11:03:44 +08:00
parent 69d83c6ab5
commit 0fa75b7937
7 changed files with 820 additions and 339 deletions

1
EG Submodule

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

View File

@ -4,6 +4,7 @@
""" """
import os import os
import shutil
import subprocess import subprocess
import platform import platform
from pathlib import Path from pathlib import Path
@ -337,10 +338,137 @@ class ResourceManager:
return str(path.relative_to(self.project_root)) return str(path.relative_to(self.project_root))
except ValueError: except ValueError:
return str(path) 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]): 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): def clear_drag(self):
"""清除拖拽状态""" """清除拖拽状态"""
@ -364,4 +492,4 @@ class ResourceManager:
for file_path in self.dragged_files: for file_path in self.dragged_files:
if file_path.suffix.lower() in supported_extensions: if file_path.suffix.lower() in supported_extensions:
return True return True
return False return False

View File

@ -24,26 +24,26 @@ Size=832,45
Collapsed=0 Collapsed=0
[Window][工具栏] [Window][工具栏]
Pos=273,20 Pos=327,20
Size=1276,32 Size=1862,32
Collapsed=0 Collapsed=0
DockId=0x0000000D,0 DockId=0x0000000D,0
[Window][场景树] [Window][场景树]
Pos=0,20 Pos=0,20
Size=271,634 Size=325,854
Collapsed=0 Collapsed=0
DockId=0x00000007,0 DockId=0x00000007,0
[Window][属性面板] [Window][属性面板]
Pos=1551,20 Pos=2191,20
Size=369,989 Size=369,1331
Collapsed=0 Collapsed=0
DockId=0x00000003,0 DockId=0x00000003,0
[Window][控制台] [Window][控制台]
Pos=0,656 Pos=0,876
Size=271,353 Size=325,475
Collapsed=0 Collapsed=0
DockId=0x00000008,0 DockId=0x00000008,0
@ -60,7 +60,7 @@ Collapsed=0
[Window][WindowOverViewport_11111111] [Window][WindowOverViewport_11111111]
Pos=0,20 Pos=0,20
Size=1920,989 Size=2560,1331
Collapsed=0 Collapsed=0
[Window][测试窗口1] [Window][测试窗口1]
@ -99,8 +99,8 @@ Size=600,500
Collapsed=0 Collapsed=0
[Window][资源管理器] [Window][资源管理器]
Pos=273,817 Pos=327,1013
Size=1276,192 Size=1862,338
Collapsed=0 Collapsed=0
DockId=0x00000006,0 DockId=0x00000006,0
@ -201,16 +201,16 @@ Size=600,400
Collapsed=0 Collapsed=0
[Docking][Data] [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=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=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753 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=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,761 CentralNode=1 DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,957 CentralNode=1
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,192 Selected=0x3A2E05C3 DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,338 Selected=0x3A2E05C3
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=369,989 Split=Y Selected=0x3188AB8D DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=369,989 Split=Y Selected=0x3188AB8D
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37 DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7 DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7

372
main.py
View File

@ -14,8 +14,8 @@ from imgui_bundle import imgui, imgui_ctx
import sys import sys
import os import os
import warnings import warnings
import threading import ctypes
import time from ctypes import wintypes
from pathlib import Path from pathlib import Path
# 导入MyWorld类和必要的模块 # 导入MyWorld类和必要的模块
@ -49,54 +49,203 @@ from TransformGizmo.transform_gizmo import TransformGizmo
# 拖拽监控类 # 拖拽监控类
class DragDropMonitor: class DragDropMonitor:
"""拖拽文件监控器""" """Windows 文件拖拽桥接,收集外部文件拖入事件。"""
WM_DROPFILES = 0x0233
WM_NCDESTROY = 0x0082
GWL_WNDPROC = -4
def __init__(self, world): def __init__(self, world):
self.world = world self.world = world
self.running = False self.enabled = False
self.thread = None self._hwnd = None
self._window_proc = None
# 支持的文件格式 self._old_wndproc = None
self.supported_formats = ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj'] self._set_wndproc = None
self._user32 = None
self._shell32 = None
self._pending_events = []
self._hook_wndproc_ptr = None
def start(self): def start(self):
"""启动监控""" """启动系统文件拖拽捕获。"""
if not self.running: if self.enabled:
self.running = True return
self.thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.thread.start() 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): def stop(self):
"""停止监控""" """恢复窗口过程并停止拖拽捕获。"""
self.running = False if not self.enabled or os.name != "nt":
if self.thread: return
self.thread.join()
try:
def _monitor_loop(self): if self._shell32 and self._hwnd:
"""监控循环""" self._shell32.DragAcceptFiles(wintypes.HWND(self._hwnd), False)
while self.running: 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: try:
# 这里可以实现具体的拖拽检测逻辑 self._shell32.DragFinish(hdrop_handle)
# 由于Panda3D限制我们使用简化的实现 except Exception:
time.sleep(0.1) pass
# 检查是否有新的拖拽文件 return files, drop_pos
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): def add_file_from_external(self, file_path):
"""从外部添加文件路径(用于系统级拖拽集成)""" """手动压入外部文件(保留给其他集成调用)。"""
if self.world: if file_path:
self.add_dragged_file(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: try:
# 尝试导入视频管理器,避免循环导入 # 尝试导入视频管理器,避免循环导入
@ -383,6 +532,14 @@ class MyWorld(CoreWorld):
self.is_dragging = False self.is_dragging = False
self.show_drag_overlay = False self.show_drag_overlay = False
self.drag_drop_monitor = None 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 self.showLUIEditor = not self.use_ssbo_mouse_picking
# 导入功能状态 # 导入功能状态
@ -717,12 +874,25 @@ class MyWorld(CoreWorld):
display_size = imgui.get_io().display_size display_size = imgui.get_io().display_size
window_width = display_size.x window_width = display_size.x
window_height = display_size.y 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_menu_bar()
# 绘制停靠布局 # 绘制停靠布局
self._draw_docked_layout(window_width, window_height) self._draw_docked_layout(window_width, window_height)
# 处理系统层外部拖入
self._process_external_drop_events()
# 绘制纹理测试窗口 # 绘制纹理测试窗口
if self.testTexture: if self.testTexture:
@ -784,12 +954,6 @@ class MyWorld(CoreWorld):
# 更新变换监控 # 更新变换监控
dt = imgui.get_io().delta_time dt = imgui.get_io().delta_time
self.update_transform_monitoring(dt) 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): def _draw_docked_layout(self, window_width, window_height):
"""绘制可停靠的布局(支持拖拽)""" """绘制可停靠的布局(支持拖拽)"""
@ -1342,16 +1506,108 @@ class MyWorld(CoreWorld):
def _refresh_heightmap_browser(self, *args, **kwargs): def _refresh_heightmap_browser(self, *args, **kwargs):
return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs) return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs)
def setup_drag_drop_support(self): def setup_drag_drop_support(self):
"""设置拖拽支持""" """初始化拖拽支持。"""
try: try:
# 启动拖拽监控线程
self.drag_drop_monitor = DragDropMonitor(self) self.drag_drop_monitor = DragDropMonitor(self)
self.drag_drop_monitor.start() self.drag_drop_monitor.start()
print("✓ 拖拽监控已启动") print("拖拽监控已启用")
except Exception as e: 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): def add_dragged_file(self, file_path):
"""添加拖拽的文件""" """添加拖拽的文件"""
if file_path not in self.dragged_files: if file_path not in self.dragged_files:
@ -1400,7 +1656,7 @@ class MyWorld(CoreWorld):
return False 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: if model_node:
# 应用材质确保颜色正常 # 应用材质确保颜色正常
@ -1409,8 +1665,8 @@ class MyWorld(CoreWorld):
# 设置模型位置 # 设置模型位置
model_node.setPos(0, 0, 0) 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)}") self.add_message("success", f"成功导入模型: {os.path.basename(file_path)}")
return True return True

View File

@ -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. """Import model through the active runtime path.
SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model). SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model).
Legacy mode: load via SceneManager. 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): if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
try: 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 # Remove legacy scene-manager models to avoid duplicate rendering
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'): if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
for m in list(self.scene_manager.models): for m in list(self.scene_manager.models):

View File

@ -340,6 +340,14 @@ class EditorPanels:
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags): with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags):
self.app.showSceneTree = True # 确保窗口保持打开 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.text("场景层级")
imgui.separator() imgui.separator()
@ -465,6 +473,9 @@ class EditorPanels:
# Clear LUI selection when a scene node is selected # Clear LUI selection when a scene node is selected
if hasattr(self.app, 'lui_manager'): if hasattr(self.app, 'lui_manager'):
self.app.lui_manager.selected_index = -1 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): if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
@ -579,20 +590,31 @@ class EditorPanels:
def _draw_resource_manager(self): def _draw_resource_manager(self):
"""绘制资源管理器面板""" """绘制资源管理器面板"""
# 使用面板类型的窗口标志支持docking
flags = self.app.style_manager.get_window_flags("panel") flags = self.app.style_manager.get_window_flags("panel")
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags): 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 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.text("文件浏览器")
imgui.separator() imgui.separator()
# 导航按钮
if imgui.button(""): if imgui.button(""):
rm.navigate_back() rm.navigate_back()
imgui.same_line() imgui.same_line()
@ -628,115 +650,89 @@ class EditorPanels:
# 搜索框 # 搜索框
changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256)
imgui.separator() imgui.separator()
# 检查自动刷新
if rm.refresh_if_needed(): if rm.refresh_if_needed():
# 目录内容发生变化,可以在这里添加通知逻辑
pass pass
# 获取目录内容
dirs, files = rm.get_directory_contents(rm.current_path) dirs, files = rm.get_directory_contents(rm.current_path)
# 显示目录
for dir_path in dirs: for dir_path in dirs:
if not rm.should_show_file(dir_path): if not rm.should_show_file(dir_path):
continue continue
# 目录图标和名称
icon_name = rm.get_file_icon(dir_path.name, is_folder=True) icon_name = rm.get_file_icon(dir_path.name, is_folder=True)
node_open = False
# 检查是否被选中
is_selected = dir_path in rm.selected_files is_selected = dir_path in rm.selected_files
# 使用TreeNode来显示目录
if is_selected: if is_selected:
imgui.push_style_color(imgui.Col_.header, (100/255, 150/255, 200/255, 1.0)) imgui.push_style_color(imgui.Col_.header, (100/255, 150/255, 200/255, 1.0))
# 尝试加载PNG图标
icon_texture = None icon_texture = None
try: try:
# 直接使用图标名称load_icon会自动添加.png
icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}") icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}")
except: except:
pass pass
if icon_texture: if icon_texture:
# 使用PNG图标
imgui.image(icon_texture, (16, 16)) imgui.image(icon_texture, (16, 16))
imgui.same_line() 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: else:
# 回退到文本标识符 node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}##dir_{dir_path}")
node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}")
if is_selected: if is_selected:
imgui.pop_style_color() 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.is_item_clicked():
if imgui.get_io().key_ctrl: if imgui.get_io().key_ctrl:
# 多选模式
if is_selected: if is_selected:
rm.selected_files.discard(dir_path) rm.selected_files.discard(dir_path)
else: else:
rm.selected_files.add(dir_path) rm.selected_files.add(dir_path)
else: else:
# 单选模式
rm.selected_files.clear() rm.selected_files.clear()
rm.selected_files.add(dir_path) rm.selected_files.add(dir_path)
rm.focused_file = dir_path rm.focused_file = dir_path
# 双击导航
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
rm.navigate_to(dir_path) rm.navigate_to(dir_path)
# 右键菜单
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True rm.show_context_menu = True
rm.context_menu_file = dir_path rm.context_menu_file = dir_path
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
# 如果节点展开,显示子内容
if node_open: if node_open:
# 获取子目录内容
subdirs, subfiles = rm.get_directory_contents(dir_path) subdirs, subfiles = rm.get_directory_contents(dir_path)
# 显示子目录
for subdir in subdirs: for subdir in subdirs:
if not rm.should_show_file(subdir): if not rm.should_show_file(subdir):
continue continue
# 初始化变量
subicon_name = "folder"
sub_is_selected = False
# 获取子目录图标名称
subicon_name = rm.get_file_icon(subdir.name, is_folder=True) subicon_name = rm.get_file_icon(subdir.name, is_folder=True)
sub_is_selected = subdir in rm.selected_files sub_is_selected = subdir in rm.selected_files
# 尝试加载PNG图标
subicon_texture = None subicon_texture = None
try: try:
subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}") subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}")
except: except:
pass pass
if subicon_texture: if subicon_texture:
# 使用PNG图标
imgui.image(subicon_texture, (16, 16)) imgui.image(subicon_texture, (16, 16))
imgui.same_line() imgui.same_line()
sub_node_open = imgui.tree_node(f" {subdir.name}") sub_node_open = imgui.tree_node(f" {subdir.name}##dir_{subdir}")
else: else:
# 回退到文本标识符 sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}##dir_{subdir}")
sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}")
self._append_drop_target_from_last_item(subdir)
if sub_is_selected: self._start_resource_drag_if_needed(rm, subdir)
imgui.pop_style_color()
# 处理子目录的选择
if imgui.is_item_clicked(): if imgui.is_item_clicked():
if imgui.get_io().key_ctrl: if imgui.get_io().key_ctrl:
if sub_is_selected: if sub_is_selected:
@ -747,46 +743,43 @@ class EditorPanels:
rm.selected_files.clear() rm.selected_files.clear()
rm.selected_files.add(subdir) rm.selected_files.add(subdir)
rm.focused_file = subdir rm.focused_file = subdir
# 双击子目录导航
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
rm.navigate_to(subdir) rm.navigate_to(subdir)
# 右键菜单
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True rm.show_context_menu = True
rm.context_menu_file = subdir rm.context_menu_file = subdir
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
if sub_node_open: if sub_node_open:
imgui.tree_pop() imgui.tree_pop()
# 显示子文件
for subfile in subfiles: for subfile in subfiles:
if not rm.should_show_file(subfile): if not rm.should_show_file(subfile):
continue continue
subicon_name = rm.get_file_icon(subfile.name) subicon_name = rm.get_file_icon(subfile.name)
sub_is_selected = subfile in rm.selected_files sub_is_selected = subfile in rm.selected_files
# 尝试加载PNG图标
subicon_texture = None subicon_texture = None
try: try:
subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}") subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}")
except: except:
pass pass
if subicon_texture: if subicon_texture:
# 使用PNG图标
imgui.image(subicon_texture, (16, 16)) imgui.image(subicon_texture, (16, 16))
imgui.same_line() imgui.same_line()
selected = imgui.selectable(f" {subfile.name}", sub_is_selected) selected = imgui.selectable(f" {subfile.name}##file_{subfile}", sub_is_selected)
else: else:
# 回退到文本标识符 selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}##file_{subfile}", sub_is_selected)
selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}", sub_is_selected)
selected_clicked = selected[0] if isinstance(selected, tuple) else bool(selected)
# 处理子文件的选择 self._start_resource_drag_if_needed(rm, subfile)
if selected:
if selected_clicked:
if imgui.get_io().key_ctrl: if imgui.get_io().key_ctrl:
if sub_is_selected: if sub_is_selected:
rm.selected_files.discard(subfile) rm.selected_files.discard(subfile)
@ -796,99 +789,142 @@ class EditorPanels:
rm.selected_files.clear() rm.selected_files.clear()
rm.selected_files.add(subfile) rm.selected_files.add(subfile)
rm.focused_file = subfile rm.focused_file = subfile
# 双击子文件操作
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): 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._import_model_for_runtime(str(subfile))
self.app.add_info_message(f"正在导入模型: {subfile.name}") self.app.add_info_message(f"正在导入模型: {subfile.name}")
else: else:
rm.open_file(subfile) rm.open_file(subfile)
# 右键菜单
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True rm.show_context_menu = True
rm.context_menu_file = subfile rm.context_menu_file = subfile
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
# 只有在节点展开时才调用tree_pop
if node_open: imgui.tree_pop()
imgui.tree_pop()
for file_path in files:
if not rm.should_show_file(file_path):
continue
# 处理拖拽开始
if imgui.is_item_active() and imgui.is_item_hovered(): icon_name = rm.get_file_icon(file_path.name)
if imgui.is_mouse_dragging(0): is_selected = file_path in rm.selected_files
# 开始拖拽
drag_files = list(rm.selected_files) if rm.selected_files else [file_path] icon_texture = None
rm.start_drag(drag_files) try:
self.app.is_dragging = True icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}")
self.app.show_drag_overlay = True 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): if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
# 检查是否是支持的3D模型格式 if self._is_model_file(file_path):
if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']:
# 导入3D模型
self.app.add_info_message(f"正在导入模型: {file_path.name}") self.app.add_info_message(f"正在导入模型: {file_path.name}")
self.app._import_model_for_runtime(str(file_path)) self.app._import_model_for_runtime(str(file_path))
else: else:
# 使用系统默认程序打开
rm.open_file(file_path) rm.open_file(file_path)
# 右键菜单
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
rm.show_context_menu = True rm.show_context_menu = True
rm.context_menu_file = file_path rm.context_menu_file = file_path
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
imgui.open_popup("resource_context_menu")
# 右键菜单
if rm.show_context_menu and rm.context_menu_file: if rm.context_menu_file:
imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1])) 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 imgui.begin_popup("resource_context_menu"):
if popup: if rm.context_menu_file and rm.context_menu_file.is_dir():
if rm.context_menu_file.is_dir(): if imgui.menu_item("打开")[1]:
if imgui.menu_item("打开"): rm.navigate_to(rm.context_menu_file)
rm.navigate_to(rm.context_menu_file) imgui.separator()
imgui.separator() if imgui.menu_item("重命名")[1]:
if imgui.menu_item("重命名"): print(f"重命名文件夹: {rm.context_menu_file.name}")
print(f"重命名文件夹: {rm.context_menu_file.name}") if imgui.menu_item("删除")[1]:
if imgui.menu_item("删除"): print(f"删除文件夹: {rm.context_menu_file.name}")
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: else:
if imgui.menu_item("打开"): subprocess.run(["xdg-open", str(rm.context_menu_file.parent)])
rm.open_file(rm.context_menu_file)
imgui.separator() if imgui.is_mouse_clicked(0) and not imgui.is_window_hovered():
if imgui.menu_item("导入到场景"): rm.context_menu_file = None
if rm.context_menu_file.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: rm.show_context_menu = False
self.app.add_info_message(f"正在导入模型: {rm.context_menu_file.name}") imgui.close_current_popup()
self.app._import_model_for_runtime(str(rm.context_menu_file))
if imgui.menu_item("重命名"): imgui.end_popup()
print(f"重命名文件: {rm.context_menu_file.name}")
if imgui.menu_item("删除"): @staticmethod
print(f"删除文件: {rm.context_menu_file.name}") def _is_model_file(path: Path) -> bool:
return path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']
imgui.separator()
if imgui.menu_item("复制路径"): def _append_drop_target_from_last_item(self, target_path: Path):
imgui.set_clipboard_text(str(rm.context_menu_file)) item_min = imgui.get_item_rect_min()
self.app.add_info_message("路径已复制到剪贴板") item_max = imgui.get_item_rect_max()
if imgui.menu_item("在文件管理器中显示"): width = float(item_max.x - item_min.x)
import platform height = float(item_max.y - item_min.y)
import subprocess if width <= 0 or height <= 0:
if platform.system() == "Windows": return
subprocess.run(["explorer", "/select,", str(rm.context_menu_file)]) self.app._resource_drop_targets.append((
elif platform.system() == "Darwin": float(item_min.x),
subprocess.run(["open", "-R", str(rm.context_menu_file)]) float(item_min.y),
else: width,
subprocess.run(["xdg-open", str(rm.context_menu_file.parent)]) height,
str(target_path),
# 如果点击其他地方,关闭菜单 ))
if imgui.is_mouse_clicked(0) or imgui.is_mouse_clicked(1):
if not imgui.is_window_hovered(): def _start_resource_drag_if_needed(self, rm, fallback_path: Path):
rm.show_context_menu = False if not imgui.is_item_active() or not imgui.is_mouse_dragging(0):
rm.context_menu_file = None 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): def _draw_property_panel(self):

View File

@ -1,4 +1,5 @@
import os import os
from pathlib import Path
from imgui_bundle import imgui, imgui_ctx from imgui_bundle import imgui, imgui_ctx
@ -19,129 +20,173 @@ class InteractionPanels:
def _draw_drag_drop_interface(self): def _draw_drag_drop_interface(self):
"""绘制拖拽界面""" """处理资源管理器到场景/树的拖拽完成。"""
# 检查资源管理器的拖拽状态
if self.resource_manager.is_dragging(): if self.resource_manager.is_dragging():
self.is_dragging = True self.is_dragging = True
self.dragged_files = self.resource_manager.get_dragged_files() self.dragged_files = self.resource_manager.get_dragged_files()
self.show_drag_overlay = True
if self.is_dragging and self.dragged_files and imgui.is_mouse_released(0):
# 绘制拖拽覆盖层 self._handle_drag_drop_completion()
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()
def _handle_drag_drop_completion(self): 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() mouse_pos = imgui.get_mouse_pos()
viewport = imgui.get_main_viewport() drop_kind, target_dir, target_parent = self._resolve_drop_target(mouse_pos)
# 简单检查如果不在任何ImGui窗口上则认为是在3D视图中 if drop_kind == "resource_manager":
if not imgui.is_any_window_hovered(): moved, errors = self.resource_manager.move_files_to_directory(dragged_files, target_dir)
# 导入支持的3D模型文件 if moved:
imported_count = 0 label = self.resource_manager.get_relative_path(target_dir) if target_dir else "资源管理器"
for file_path in self.dragged_files: self.add_success_message(f"已移动 {len(moved)} 个资源到 {label}")
if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: 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: try:
self._import_model_for_runtime(str(file_path)) self.scene_manager.processMaterials(model_node)
self.add_success_message(f"成功导入模型: {file_path.name}") except Exception:
imported_count += 1 pass
except Exception as e: if drop_kind == "scene_tree" and self._is_valid_drop_parent(target_parent):
self.add_error_message(f"导入模型失败 {file_path.name}: {e}") try:
model_node.wrtReparentTo(target_parent)
if imported_count > 0: except Exception:
self.add_success_message(f"共导入 {imported_count} 个模型") 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.is_dragging = False
self.dragged_files.clear() self.dragged_files.clear()
self.show_drag_overlay = False self.show_drag_overlay = False
self.resource_manager.clear_drag() self.resource_manager.clear_drag()
self._drag_scene_tree_hover_node = None
def _draw_drag_overlay(self): def _draw_drag_overlay(self):
"""绘制拖拽覆盖层""" """已弃用:保留兼容接口,不再绘制遮罩。"""
viewport = imgui.get_main_viewport() return
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): def _draw_drag_status(self):
"""绘制拖拽状态""" """已弃用:保留兼容接口,不再绘制状态窗。"""
viewport = imgui.get_main_viewport() return
# 在右下角显示拖拽状态
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()
def _draw_context_menus(self): def _draw_context_menus(self):