933 lines
41 KiB
Python
933 lines
41 KiB
Python
from imgui_bundle import imgui, imgui_ctx
|
||
from pathlib import Path
|
||
|
||
class EditorPanelsLeftMixin:
|
||
"""Auto-split mixin from editor_panels.py."""
|
||
|
||
@staticmethod
|
||
def _truncate_panel_text(text, limit=28):
|
||
text = text or ""
|
||
if len(text) <= limit:
|
||
return text
|
||
return text[: max(0, limit - 1)] + "…"
|
||
|
||
def _draw_scene_tree(self):
|
||
"""绘制场景树面板"""
|
||
# 使用更少的限制性标志,允许docking
|
||
flags = (imgui.WindowFlags_.no_collapse)
|
||
|
||
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags) as (_, opened):
|
||
if not opened:
|
||
self.app.showSceneTree = False
|
||
self.app._scene_tree_window_rect = None
|
||
return
|
||
|
||
self.app.showSceneTree = opened
|
||
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),
|
||
)
|
||
|
||
self._draw_scene_tree_header()
|
||
imgui.separator()
|
||
self._build_scene_tree()
|
||
|
||
def _draw_scene_tree_header(self):
|
||
"""绘制场景树头部摘要与检索框。"""
|
||
model_count = self._get_scene_tree_model_display_count()
|
||
selected_node = self.app._get_selection_node()
|
||
selected_name = "未选择"
|
||
if selected_node and not selected_node.isEmpty():
|
||
selected_name = selected_node.getName() or "未命名对象"
|
||
|
||
self.app.style_manager.draw_stat_chip(f"模型 {model_count}", tint=(0.14, 0.17, 0.22, 1.0))
|
||
imgui.same_line()
|
||
self.app.style_manager.draw_stat_chip(
|
||
f"当前 {self._truncate_panel_text(selected_name, 20)}",
|
||
tint=(0.16, 0.20, 0.27, 1.0),
|
||
)
|
||
filter_text = self._get_scene_tree_filter()
|
||
if filter_text:
|
||
imgui.same_line()
|
||
self.app.style_manager.draw_stat_chip(
|
||
f"筛选 {self._truncate_panel_text(filter_text, 16)}",
|
||
tint=(0.19, 0.24, 0.33, 1.0),
|
||
)
|
||
|
||
imgui.spacing()
|
||
imgui.text_disabled("筛选场景")
|
||
imgui.set_next_item_width(-64)
|
||
changed, search_text = imgui.input_text("##scene_tree_search", self.app._scene_tree_search_text, 256)
|
||
if changed:
|
||
self.app._scene_tree_search_text = search_text
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("清空", size=(52, 26), enabled=bool(self.app._scene_tree_search_text)):
|
||
self.app._scene_tree_search_text = ""
|
||
|
||
def _get_scene_tree_filter(self):
|
||
return getattr(self.app, "_scene_tree_search_text", "").strip().lower()
|
||
|
||
def _get_scene_tree_models(self):
|
||
models = []
|
||
# SSBO模式下场景树应以SSBO聚合根为唯一模型入口,避免混入scene_manager残留节点
|
||
# (如 scene.bam / chunk_* 运行时包装节点)导致树结构异常。
|
||
if getattr(self.app, "use_ssbo_mouse_picking", False):
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||
source_model_root = getattr(ssbo_editor, "source_model_root", None) if ssbo_editor else None
|
||
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent():
|
||
return [ssbo_model]
|
||
if source_model_root and not source_model_root.isEmpty():
|
||
return []
|
||
scene_manager = getattr(self.app, "scene_manager", None)
|
||
if scene_manager and hasattr(scene_manager, "models"):
|
||
return [m for m in scene_manager.models if m and not m.isEmpty()]
|
||
return []
|
||
|
||
if hasattr(self.app, "scene_manager") and self.app.scene_manager and hasattr(self.app.scene_manager, "models"):
|
||
models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()])
|
||
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent() and ssbo_model not in models:
|
||
models.append(ssbo_model)
|
||
return models
|
||
|
||
def _get_ssbo_top_level_model_keys(self):
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||
if not ssbo_editor or not controller or not getattr(controller, "tree_root_key", None):
|
||
return self._get_ssbo_virtual_top_level_model_keys(controller)
|
||
|
||
root_node = controller.tree_nodes.get(controller.tree_root_key)
|
||
if not root_node or not root_node.get("children"):
|
||
return self._get_ssbo_virtual_top_level_model_keys(controller)
|
||
|
||
keys = []
|
||
for child_key in root_node.get("children", []):
|
||
if not self._ssbo_key_matches_scene_filter(controller, child_key):
|
||
continue
|
||
keys.append(child_key)
|
||
return keys
|
||
|
||
def _get_ssbo_virtual_top_level_model_keys(self, controller):
|
||
virtual_tree = getattr(controller, "virtual_tree", None) if controller else None
|
||
if not isinstance(virtual_tree, dict):
|
||
return []
|
||
keys = []
|
||
for child in (virtual_tree.get("children", {}) or {}).values():
|
||
key = child.get("group_key") or child.get("leaf_key")
|
||
if not key:
|
||
continue
|
||
if not self._ssbo_key_matches_scene_filter(controller, key):
|
||
continue
|
||
keys.append(key)
|
||
return keys
|
||
|
||
def _find_ssbo_virtual_node_by_key(self, controller, key):
|
||
virtual_tree = getattr(controller, "virtual_tree", None) if controller else None
|
||
if not isinstance(virtual_tree, dict) or not key:
|
||
return None
|
||
|
||
stack = [virtual_tree]
|
||
while stack:
|
||
node = stack.pop()
|
||
if not isinstance(node, dict):
|
||
continue
|
||
if node.get("group_key") == key or node.get("leaf_key") == key:
|
||
return node
|
||
stack.extend(list((node.get("children", {}) or {}).values()))
|
||
return None
|
||
|
||
def _get_scene_tree_model_display_count(self):
|
||
models = self._get_scene_tree_models()
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||
count = 0
|
||
for model in models:
|
||
if ssbo_model and model == ssbo_model:
|
||
count += len(self._get_ssbo_top_level_model_keys())
|
||
else:
|
||
count += 1
|
||
return count
|
||
|
||
def _get_scene_tree_epoch(self):
|
||
try:
|
||
return int(getattr(self.app, "_scene_tree_epoch", 0) or 0)
|
||
except Exception:
|
||
return 0
|
||
|
||
def _ssbo_key_matches_scene_filter(self, controller, key):
|
||
filter_text = self._get_scene_tree_filter()
|
||
if not filter_text:
|
||
return True
|
||
|
||
node_data = controller.tree_nodes.get(key)
|
||
if node_data:
|
||
display_name = controller.display_names.get(key, key)
|
||
if filter_text in display_name.lower() or filter_text in str(key).lower():
|
||
return True
|
||
return any(self._ssbo_key_matches_scene_filter(controller, child_key) for child_key in node_data["children"])
|
||
|
||
virtual_node = self._find_ssbo_virtual_node_by_key(controller, key)
|
||
if not virtual_node:
|
||
return False
|
||
|
||
display_name = str(virtual_node.get("display_name", virtual_node.get("name", key)) or key)
|
||
if filter_text in display_name.lower() or filter_text in str(key).lower():
|
||
return True
|
||
return any(
|
||
self._ssbo_key_matches_scene_filter(controller, child.get("group_key") or child.get("leaf_key"))
|
||
for child in (virtual_node.get("children", {}) or {}).values()
|
||
if child.get("group_key") or child.get("leaf_key")
|
||
)
|
||
|
||
def _node_matches_scene_filter(self, node, name):
|
||
filter_text = self._get_scene_tree_filter()
|
||
if not filter_text:
|
||
return True
|
||
|
||
display_name = (name or "").lower()
|
||
node_name = ""
|
||
try:
|
||
node_name = (node.getName() or "").lower()
|
||
except Exception:
|
||
node_name = ""
|
||
|
||
if filter_text in display_name or filter_text in node_name:
|
||
return True
|
||
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||
if controller and ssbo_model and node == ssbo_model:
|
||
root_key = getattr(controller, "tree_root_key", None)
|
||
if root_key and self._ssbo_key_matches_scene_filter(controller, root_key):
|
||
return True
|
||
|
||
try:
|
||
for child in node.getChildren():
|
||
if not child or child.isEmpty():
|
||
continue
|
||
child_name = child.getName() or ""
|
||
if child_name.startswith("modelCollision_"):
|
||
continue
|
||
if self._node_matches_scene_filter(child, child_name):
|
||
return True
|
||
except Exception:
|
||
pass
|
||
|
||
return False
|
||
|
||
def _build_scene_tree(self):
|
||
"""构建动态场景树"""
|
||
render_entries = []
|
||
if hasattr(self.app, "ambient_light") and self.app.ambient_light:
|
||
render_entries.append((self.app.ambient_light, "环境光", "light"))
|
||
|
||
if hasattr(self.app, "scene_manager") and self.app.scene_manager:
|
||
if hasattr(self.app.scene_manager, "Spotlight") and self.app.scene_manager.Spotlight:
|
||
for i, spotlight in enumerate(self.app.scene_manager.Spotlight):
|
||
render_entries.append((spotlight, f"聚光灯_{i + 1}", "light"))
|
||
if hasattr(self.app.scene_manager, "Pointlight") and self.app.scene_manager.Pointlight:
|
||
for i, pointlight in enumerate(self.app.scene_manager.Pointlight):
|
||
render_entries.append((pointlight, f"点光源_{i + 1}", "light"))
|
||
|
||
if hasattr(self.app, "ground") and self.app.ground:
|
||
render_entries.append((self.app.ground, "地板", "geometry"))
|
||
|
||
render_entries = [entry for entry in render_entries if self._node_matches_scene_filter(entry[0], entry[1])]
|
||
if render_entries and imgui.tree_node(f"渲染 ({len(render_entries)})"):
|
||
for node, name, node_type in render_entries:
|
||
self._draw_scene_node(node, name, node_type)
|
||
imgui.tree_pop()
|
||
|
||
camera_entries = []
|
||
if hasattr(self.app, "camera") and self.app.camera and self._node_matches_scene_filter(self.app.camera, "主相机"):
|
||
camera_entries.append((self.app.camera, "主相机", "camera"))
|
||
if camera_entries and imgui.tree_node(f"相机 ({len(camera_entries)})"):
|
||
for node, name, node_type in camera_entries:
|
||
self._draw_scene_node(node, name, node_type)
|
||
imgui.tree_pop()
|
||
|
||
models = [model for model in self._get_scene_tree_models() if self._node_matches_scene_filter(model, model.getName() or "模型")]
|
||
model_display_count = self._get_scene_tree_model_display_count()
|
||
if model_display_count and imgui.tree_node(f"模型 ({model_display_count})"):
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||
|
||
for i, model in enumerate(models):
|
||
if ssbo_model and controller and model == ssbo_model:
|
||
top_level_keys = self._get_ssbo_top_level_model_keys()
|
||
if top_level_keys:
|
||
for child_key in top_level_keys:
|
||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||
continue
|
||
self._draw_scene_node(model, model.getName() or f"模型_{i + 1}", "model")
|
||
imgui.tree_pop()
|
||
elif not render_entries and not camera_entries and not model_display_count and self._get_scene_tree_filter():
|
||
imgui.text_disabled("没有匹配的 3D 节点")
|
||
|
||
# if imgui.tree_node("GUI元素"):
|
||
# if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'):
|
||
# if self.app.gui_manager.gui_elements:
|
||
# for gui_element in self.app.gui_manager.gui_elements:
|
||
# if gui_element and hasattr(gui_element,'node'):
|
||
# gui_type = getattr(gui_element,'gui_type','GUI_UNKNOWN')
|
||
# display_name = getattr(gui_element,'name',gui_type)
|
||
# self._draw_scene_node(gui_element.node,display_name,"gui",gui_type)
|
||
# else:
|
||
# imgui.text("(空)")
|
||
# else:
|
||
# imgui.text("(空)")
|
||
# imgui.tree_pop()
|
||
|
||
# LUI元素节点
|
||
if imgui.tree_node("GUI元素"):
|
||
if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
|
||
self.app.lui_manager.draw_component_tree()
|
||
imgui.tree_pop()
|
||
# if imgui.tree_node("LUI元素"):
|
||
# if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled:
|
||
# if self.app.lui_manager.components:
|
||
# for comp in self.app.lui_manager.components:
|
||
# if 'node' in comp:
|
||
# self._draw_scene_node(comp['node'], comp['name'], "ui")
|
||
|
||
# if self.app.lui_manager.canvases:
|
||
# for canvas in self.app.lui_manager.canvases:
|
||
# if imgui.tree_node(f"Canvas: {canvas['name']}"):
|
||
# # 实际上组件已经在 node 下了,可以通过 _draw_scene_node 递归显示
|
||
# # 但为了清晰,我们可以手动列出或者依赖递归
|
||
# self._draw_scene_node(canvas['node'], canvas['name'], "geometry")
|
||
# imgui.tree_pop()
|
||
# else:
|
||
# imgui.text("(空)")
|
||
# else:
|
||
# imgui.text("(空)")
|
||
# imgui.tree_pop()
|
||
|
||
def _draw_scene_node(self, node, name, node_type, gui_subtype=None):
|
||
"""绘制单个场景节点"""
|
||
if not node or node.isEmpty():
|
||
return
|
||
if not self._node_matches_scene_filter(node, name):
|
||
return
|
||
|
||
# 检查是否被选中
|
||
is_selected = (self.app._get_selection_node() == node)
|
||
force_ssbo_root_key = None
|
||
ssbo_editor_ref = None
|
||
try:
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||
root_key = getattr(controller, "tree_root_key", None) if controller else None
|
||
if (
|
||
ssbo_editor and controller and ssbo_model and node == ssbo_model
|
||
and root_key and root_key in controller.tree_nodes
|
||
):
|
||
force_ssbo_root_key = root_key
|
||
ssbo_editor_ref = ssbo_editor
|
||
is_selected = (getattr(ssbo_editor, "selected_name", None) == root_key)
|
||
except Exception:
|
||
force_ssbo_root_key = None
|
||
ssbo_editor_ref = None
|
||
|
||
# 节点可见性
|
||
is_visible = node.is_hidden() == False
|
||
|
||
# 设置选择颜色
|
||
if is_selected:
|
||
imgui.push_style_color(imgui.Col_.text, (0.2, 0.6, 1.0, 1.0))
|
||
|
||
node_open = False
|
||
try:
|
||
# 显示节点
|
||
node_open = imgui.tree_node(name)
|
||
|
||
# 处理节点选择
|
||
if imgui.is_item_clicked():
|
||
if force_ssbo_root_key and ssbo_editor_ref:
|
||
try:
|
||
ssbo_editor_ref.select_node(force_ssbo_root_key)
|
||
if hasattr(self.app, 'lui_manager'):
|
||
self.app.lui_manager.selected_index = -1
|
||
except Exception:
|
||
pass
|
||
else:
|
||
if hasattr(self.app, 'selection') and self.app.selection:
|
||
self.app.selection.updateSelection(node)
|
||
# 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):
|
||
self._show_node_context_menu(node, name, node_type)
|
||
|
||
# 显示节点属性
|
||
imgui.same_line()
|
||
if is_visible:
|
||
imgui.text_colored((0.5, 1.0, 0.5, 1.0), "可见")
|
||
else:
|
||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "隐藏")
|
||
|
||
if node_open:
|
||
# SSBO模型使用虚拟层级显示(避免 flatten 后真实子级丢失)
|
||
if self._draw_ssbo_virtual_children(node):
|
||
pass
|
||
elif node.getNumChildren() > 0:
|
||
for i in range(node.getNumChildren()):
|
||
child = node.getChild(i)
|
||
if not child or child.isEmpty():
|
||
continue
|
||
child_name = child.getName() or f"child_{i+1}"
|
||
# 过滤碰撞辅助节点,避免污染场景树
|
||
if child_name.startswith("modelCollision_"):
|
||
continue
|
||
self._draw_scene_node(child, child_name, node_type)
|
||
# tree_pop moved to finally
|
||
except Exception as e:
|
||
print(f"绘制场景节点时出错: {e}")
|
||
finally:
|
||
if node_open:
|
||
imgui.tree_pop()
|
||
# Ensure style stack is balanced.
|
||
if is_selected:
|
||
imgui.pop_style_color()
|
||
|
||
def _draw_ssbo_virtual_children(self, node):
|
||
"""Draw SSBO controller tree_nodes as virtual children for scene tree."""
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
if not ssbo_editor:
|
||
return False
|
||
model = getattr(ssbo_editor, "model", None)
|
||
controller = getattr(ssbo_editor, "controller", None)
|
||
if not model or model != node or not controller:
|
||
return False
|
||
|
||
root_key = getattr(controller, "tree_root_key", None)
|
||
if not root_key or root_key not in controller.tree_nodes:
|
||
imgui.text_disabled("(无可用子节点)")
|
||
return True
|
||
|
||
root_node = controller.tree_nodes[root_key]
|
||
if not root_node["children"]:
|
||
imgui.text_disabled("(无可用子节点)")
|
||
return True
|
||
|
||
for child_key in root_node["children"]:
|
||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||
return True
|
||
|
||
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, controller, key):
|
||
"""Recursively draw SSBO tree_nodes hierarchy in scene tree."""
|
||
if not self._ssbo_key_matches_scene_filter(controller, key):
|
||
return
|
||
|
||
node_data = controller.tree_nodes.get(key)
|
||
if not node_data:
|
||
return self._draw_ssbo_virtual_fallback_node(ssbo_editor, controller, key)
|
||
|
||
# Skip redundant wrapper nodes (e.g. ROOT), show their children instead.
|
||
if controller.should_hide_tree_node(key):
|
||
for child_key in node_data["children"]:
|
||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||
return
|
||
|
||
display = controller.display_names.get(key, key)
|
||
preferred_ids_fn = getattr(controller, "get_preferred_selection_ids", None)
|
||
if callable(preferred_ids_fn):
|
||
obj_count = len(preferred_ids_fn(key))
|
||
else:
|
||
obj_count = len(controller.name_to_ids.get(key, []))
|
||
children = node_data["children"]
|
||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||
tree_epoch = self._get_scene_tree_epoch()
|
||
|
||
if not children:
|
||
# Leaf node: selectable
|
||
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||
if imgui.selectable(label, is_selected)[0]:
|
||
ssbo_editor.select_node(key)
|
||
if hasattr(self.app, "lui_manager"):
|
||
self.app.lui_manager.selected_index = -1
|
||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||
try:
|
||
ssbo_editor.select_node(key)
|
||
except Exception:
|
||
pass
|
||
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
|
||
else:
|
||
# Branch node: tree node
|
||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||
if is_selected:
|
||
flags |= imgui.TreeNodeFlags_.selected
|
||
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||
opened = imgui.tree_node_ex(label, flags)
|
||
if imgui.is_item_clicked(0):
|
||
ssbo_editor.select_node(key)
|
||
if hasattr(self.app, "lui_manager"):
|
||
self.app.lui_manager.selected_index = -1
|
||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||
try:
|
||
ssbo_editor.select_node(key)
|
||
except Exception:
|
||
pass
|
||
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
|
||
if opened:
|
||
for child_key in children:
|
||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||
imgui.tree_pop()
|
||
|
||
def _draw_ssbo_virtual_fallback_node(self, ssbo_editor, controller, key):
|
||
virtual_node = self._find_ssbo_virtual_node_by_key(controller, key)
|
||
if not virtual_node:
|
||
return
|
||
|
||
display = str(virtual_node.get("display_name", virtual_node.get("name", key)) or key)
|
||
preferred_ids_fn = getattr(controller, "get_preferred_selection_ids", None)
|
||
if callable(preferred_ids_fn):
|
||
obj_count = len(preferred_ids_fn(key))
|
||
else:
|
||
obj_count = len(controller.name_to_ids.get(key, []))
|
||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||
children = list((virtual_node.get("children", {}) or {}).values())
|
||
tree_epoch = self._get_scene_tree_epoch()
|
||
|
||
if display.strip().lower() == "root" and children:
|
||
for child in children:
|
||
child_key = child.get("group_key") or child.get("leaf_key")
|
||
if child_key:
|
||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||
return
|
||
|
||
if not children:
|
||
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||
if imgui.selectable(label, is_selected)[0]:
|
||
ssbo_editor.select_node(key)
|
||
if hasattr(self.app, "lui_manager"):
|
||
self.app.lui_manager.selected_index = -1
|
||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||
try:
|
||
ssbo_editor.select_node(key)
|
||
except Exception:
|
||
pass
|
||
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
|
||
return
|
||
|
||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||
if is_selected:
|
||
flags |= imgui.TreeNodeFlags_.selected
|
||
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||
opened = imgui.tree_node_ex(label, flags)
|
||
if imgui.is_item_clicked(0):
|
||
ssbo_editor.select_node(key)
|
||
if hasattr(self.app, "lui_manager"):
|
||
self.app.lui_manager.selected_index = -1
|
||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||
try:
|
||
ssbo_editor.select_node(key)
|
||
except Exception:
|
||
pass
|
||
self._show_node_context_menu(getattr(ssbo_editor, "model", None), display, "model")
|
||
if opened:
|
||
for child in children:
|
||
child_key = child.get("group_key") or child.get("leaf_key")
|
||
if child_key:
|
||
self._draw_ssbo_virtual_tree_node(ssbo_editor, controller, child_key)
|
||
imgui.tree_pop()
|
||
|
||
def _show_node_context_menu(self, node, name, node_type):
|
||
"""显示节点右键菜单"""
|
||
self.app._context_menu_node = True
|
||
self.app._context_menu_target = node
|
||
|
||
def _draw_resource_manager(self):
|
||
"""绘制资源管理器面板"""
|
||
flags = self.app.style_manager.get_window_flags("panel")
|
||
|
||
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags) as (_, opened):
|
||
if not opened:
|
||
self.app.showResourceManager = False
|
||
self.app._resource_manager_window_rect = None
|
||
return
|
||
|
||
self.app.showResourceManager = opened
|
||
rm = self.app.resource_manager
|
||
|
||
self._update_resource_manager_window_rect(rm)
|
||
|
||
rm.refresh_if_needed()
|
||
dirs, files = rm.get_directory_contents(rm.current_path)
|
||
self._draw_resource_manager_header(rm, dirs, files)
|
||
imgui.separator()
|
||
self._draw_resource_toolbar_and_filters(rm)
|
||
imgui.separator()
|
||
self._draw_resource_directory_entries(rm, dirs)
|
||
self._draw_resource_file_entries(rm, files)
|
||
self._draw_resource_context_menu(rm)
|
||
|
||
def _update_resource_manager_window_rect(self, rm):
|
||
"""更新资源管理器窗口矩形与根拖拽目标。"""
|
||
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),
|
||
))
|
||
|
||
def _draw_resource_directory_entries(self, rm, dirs):
|
||
"""绘制当前目录下的文件夹列表。"""
|
||
for dir_path in dirs:
|
||
if not rm.should_show_file(dir_path):
|
||
continue
|
||
self._draw_resource_directory_entry(rm, dir_path)
|
||
|
||
def _draw_resource_directory_entry(self, rm, dir_path: Path):
|
||
"""绘制单个文件夹节点(含一层展开内容)。"""
|
||
icon_name = rm.get_file_icon(dir_path.name, is_folder=True)
|
||
is_selected = dir_path in rm.selected_files
|
||
|
||
if is_selected:
|
||
imgui.push_style_color(imgui.Col_.header, (100 / 255, 150 / 255, 200 / 255, 1.0))
|
||
|
||
icon_texture = self._load_resource_icon(icon_name)
|
||
if icon_texture:
|
||
imgui.image(icon_texture, (16, 16))
|
||
imgui.same_line()
|
||
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}##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():
|
||
self._handle_resource_item_selection(rm, dir_path, is_selected)
|
||
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):
|
||
self._open_resource_item_context_menu(rm, dir_path)
|
||
|
||
if node_open:
|
||
self._draw_resource_directory_children(rm, dir_path)
|
||
imgui.tree_pop()
|
||
|
||
def _draw_resource_directory_children(self, rm, parent_dir: Path):
|
||
"""绘制文件夹展开后的子目录与子文件(保持原有顺序)。"""
|
||
subdirs, subfiles = rm.get_directory_contents(parent_dir)
|
||
|
||
for subdir in subdirs:
|
||
if not rm.should_show_file(subdir):
|
||
continue
|
||
self._draw_resource_subdir_entry(rm, subdir)
|
||
|
||
for subfile in subfiles:
|
||
if not rm.should_show_file(subfile):
|
||
continue
|
||
self._draw_resource_subfile_entry(rm, subfile)
|
||
|
||
def _draw_resource_subdir_entry(self, rm, subdir: Path):
|
||
"""绘制一级子目录节点。"""
|
||
subicon_name = rm.get_file_icon(subdir.name, is_folder=True)
|
||
sub_is_selected = subdir in rm.selected_files
|
||
subicon_texture = self._load_resource_icon(subicon_name)
|
||
|
||
if subicon_texture:
|
||
imgui.image(subicon_texture, (16, 16))
|
||
imgui.same_line()
|
||
sub_node_open = imgui.tree_node(f" {subdir.name}##dir_{subdir}")
|
||
else:
|
||
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():
|
||
self._handle_resource_item_selection(rm, subdir, sub_is_selected)
|
||
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):
|
||
self._open_resource_item_context_menu(rm, subdir)
|
||
|
||
if sub_node_open:
|
||
imgui.tree_pop()
|
||
|
||
def _draw_resource_subfile_entry(self, rm, subfile: Path):
|
||
"""绘制一级子文件项。"""
|
||
subicon_name = rm.get_file_icon(subfile.name)
|
||
sub_is_selected = subfile in rm.selected_files
|
||
subicon_texture = self._load_resource_icon(subicon_name)
|
||
|
||
if subicon_texture:
|
||
imgui.image(subicon_texture, (16, 16))
|
||
imgui.same_line()
|
||
selected = imgui.selectable(f" {subfile.name}##file_{subfile}", sub_is_selected)
|
||
else:
|
||
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:
|
||
self._handle_resource_item_selection(rm, subfile, sub_is_selected)
|
||
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
|
||
self._handle_resource_file_double_click(rm, subfile)
|
||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||
self._open_resource_item_context_menu(rm, subfile)
|
||
|
||
def _draw_resource_file_entries(self, rm, files):
|
||
"""绘制当前目录下的文件列表。"""
|
||
for file_path in files:
|
||
if not rm.should_show_file(file_path):
|
||
continue
|
||
self._draw_resource_file_entry(rm, file_path)
|
||
|
||
def _draw_resource_file_entry(self, rm, file_path: Path):
|
||
"""绘制顶层文件项。"""
|
||
icon_name = rm.get_file_icon(file_path.name)
|
||
is_selected = file_path in rm.selected_files
|
||
icon_texture = self._load_resource_icon(icon_name)
|
||
|
||
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:
|
||
self._handle_resource_item_selection(rm, file_path, is_selected)
|
||
if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
|
||
self._handle_resource_file_double_click(rm, file_path)
|
||
if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
|
||
self._open_resource_item_context_menu(rm, file_path)
|
||
|
||
def _draw_resource_manager_header(self, rm, dirs, files):
|
||
"""绘制资源管理器头部摘要。"""
|
||
total_items = len(dirs) + len(files)
|
||
try:
|
||
location = rm.current_path.relative_to(rm.project_root).as_posix()
|
||
except ValueError:
|
||
location = str(rm.current_path)
|
||
|
||
self.app.style_manager.draw_stat_chip(
|
||
self._truncate_panel_text(location or ".", 30),
|
||
tint=(0.14, 0.17, 0.22, 1.0),
|
||
)
|
||
imgui.same_line()
|
||
self.app.style_manager.draw_stat_chip(f"{total_items} 项", tint=(0.16, 0.19, 0.24, 1.0))
|
||
if rm.selected_files:
|
||
imgui.same_line()
|
||
self.app.style_manager.draw_stat_chip(
|
||
f"已选 {len(rm.selected_files)}",
|
||
tint=(0.18, 0.24, 0.32, 1.0),
|
||
)
|
||
if rm.search_filter:
|
||
imgui.same_line()
|
||
self.app.style_manager.draw_stat_chip(
|
||
f"搜索 {self._truncate_panel_text(rm.search_filter, 18)}",
|
||
tint=(0.19, 0.24, 0.33, 1.0),
|
||
)
|
||
|
||
def _sync_resource_path_input(self, rm):
|
||
current_path_text = str(rm.current_path)
|
||
if getattr(self.app, "_resource_path_source", "") != current_path_text:
|
||
self.app._resource_path_input = current_path_text
|
||
self.app._resource_path_source = current_path_text
|
||
|
||
def _navigate_resource_path_from_input(self, rm):
|
||
raw_path = (getattr(self.app, "_resource_path_input", "") or "").strip().strip('"')
|
||
if not raw_path:
|
||
return
|
||
try:
|
||
rm.navigate_to(Path(raw_path))
|
||
except Exception:
|
||
return
|
||
self.app._resource_path_input = str(rm.current_path)
|
||
self.app._resource_path_source = self.app._resource_path_input
|
||
|
||
def _draw_resource_toolbar_and_filters(self, rm):
|
||
"""绘制资源管理器顶部工具条与筛选输入。"""
|
||
self._sync_resource_path_input(rm)
|
||
|
||
if self.app.style_manager.draw_toolbar_button("←", size=(36, 26), tooltip="后退"):
|
||
rm.navigate_back()
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("→", size=(36, 26), tooltip="前进"):
|
||
rm.navigate_forward()
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("上级", size=(48, 26), tooltip="返回上级目录"):
|
||
rm.navigate_up()
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("资源", size=(48, 26), tooltip="回到 Resources 根目录"):
|
||
rm.navigate_to(rm.project_root / "Resources")
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("刷新", size=(48, 26), tooltip="刷新当前目录"):
|
||
rm.force_refresh()
|
||
|
||
imgui.same_line()
|
||
changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled)
|
||
if changed:
|
||
rm.set_auto_refresh(rm.auto_refresh_enabled)
|
||
|
||
imgui.text_disabled("路径")
|
||
imgui.set_next_item_width(-72)
|
||
changed, new_path = imgui.input_text("##resource_path", self.app._resource_path_input, 512)
|
||
if changed:
|
||
self.app._resource_path_input = new_path
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("前往", size=(52, 26), tooltip="跳转到输入路径"):
|
||
self._navigate_resource_path_from_input(rm)
|
||
|
||
imgui.text_disabled("搜索")
|
||
imgui.set_next_item_width(-72)
|
||
changed, rm.search_filter = imgui.input_text("##resource_search", rm.search_filter, 256)
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("清空", size=(52, 26), tooltip="清空搜索条件", enabled=bool(rm.search_filter)):
|
||
rm.search_filter = ""
|
||
|
||
def _load_resource_icon(self, icon_name: str):
|
||
"""加载资源图标;失败时返回 None。"""
|
||
try:
|
||
return self.app.style_manager.load_icon(f"file_types/{icon_name}")
|
||
except Exception:
|
||
return None
|
||
|
||
def _handle_resource_item_selection(self, rm, target_path: Path, is_selected: bool):
|
||
"""处理资源项选中逻辑(支持 Ctrl 多选)。"""
|
||
if imgui.get_io().key_ctrl:
|
||
if is_selected:
|
||
rm.selected_files.discard(target_path)
|
||
else:
|
||
rm.selected_files.add(target_path)
|
||
else:
|
||
rm.selected_files.clear()
|
||
rm.selected_files.add(target_path)
|
||
rm.focused_file = target_path
|
||
|
||
def _open_resource_item_context_menu(self, rm, target_path: Path):
|
||
"""打开资源项右键菜单。"""
|
||
rm.show_context_menu = True
|
||
rm.context_menu_file = target_path
|
||
rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
|
||
imgui.open_popup("resource_context_menu")
|
||
|
||
def _handle_resource_file_double_click(self, rm, file_path: Path):
|
||
"""处理文件双击:模型导入,其他文件打开。"""
|
||
if self._is_model_file(file_path):
|
||
self.app._import_model_with_menu_logic(
|
||
str(file_path),
|
||
show_info_message=True,
|
||
show_success_message=True,
|
||
)
|
||
else:
|
||
rm.open_file(file_path)
|
||
|
||
def _draw_resource_context_menu(self, rm):
|
||
"""绘制资源管理器右键菜单。保持原有菜单项与顺序。"""
|
||
if rm.context_menu_file:
|
||
imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1]))
|
||
|
||
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._import_model_with_menu_logic(
|
||
str(rm.context_menu_file),
|
||
show_info_message=True,
|
||
show_success_message=True,
|
||
)
|
||
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:
|
||
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
|
||
|