Refine MetaCore scene workflow and SSBO loading

This commit is contained in:
ayuan9957 2026-03-19 16:25:32 +08:00
parent b9053b6a28
commit c14c2b8796
8 changed files with 549 additions and 46 deletions

18
.gitignore vendored
View File

@ -1,7 +1,17 @@
__pycache__/
*.pyc
.idea/
【模板】模型及算法与功能对应清单.docx
__pycache__/
*.pyc
imgui.ini
build/
Builds/
Library/
**/Library/
**/Builds/
**/Scenes/*_gui.json
**/Scenes/*_lui.json
**/scenes/*_gui.json
**/scenes/*_lui.json
.idea/
【模板】模型及算法与功能对应清单.docx
【模板】GY知识-产品模块架构及功能清单和主要应用场景说明.docx
1.json
2.json

View File

@ -64,10 +64,12 @@ class CustomMouseController:
self.keyMap[key] = value
# 只在右键按下时记录鼠标位置
if key == "mouse3" and value == True:
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
if mouse_pos:
self.last_mouse_x = mouse_pos.get_x()
self.last_mouse_y = mouse_pos.get_y()
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if watcher and watcher.hasMouse():
mouse_pos = watcher.getMouse()
if mouse_pos:
self.last_mouse_x = mouse_pos.get_x()
self.last_mouse_y = mouse_pos.get_y()
def move(self, task):
dt = self.showbase.clock.dt
@ -98,7 +100,10 @@ class CustomMouseController:
try:
# 检查是否应该处理鼠标事件避免与ImGui冲突
if self._should_handle_mouse():
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if not watcher or not watcher.hasMouse():
return task.cont
mouse_pos = watcher.getMouse()
if mouse_pos:
current_x = mouse_pos.get_x()
current_y = mouse_pos.get_y()
@ -160,7 +165,10 @@ class CustomMouseController:
pass
# 检查鼠标位置是否在ImGui窗口区域内
mouse_pos = self.showbase.mouseWatcherNode.getMouse()
watcher = getattr(self.showbase, "mouseWatcherNode", None)
if not watcher or not watcher.hasMouse():
return True
mouse_pos = watcher.getMouse()
if not mouse_pos:
return True
@ -185,4 +193,4 @@ class CustomMouseController:
return True
except Exception as e:
print(f"鼠标事件检测失败: {e}")
return True
return True

View File

@ -810,20 +810,13 @@ class ProjectManager:
def _cleanup_legacy_scene_artifacts(self, scene_entry, project_path):
scene_paths = self._scene_paths(scene_entry, project_path=project_path)
scene_file = scene_paths.get("scene_file", "")
scene_root, _ = os.path.splitext(scene_file)
legacy_candidates = [
os.path.join(project_path, "scenes", "scene.bam"),
os.path.join(project_path, "scenes", "scene_gui.json"),
os.path.join(project_path, "scenes", "scene_lui.json"),
]
if scene_root:
legacy_candidates.extend(
[
f"{scene_root}.bam",
f"{scene_root}_gui.json",
f"{scene_root}_lui.json",
]
)
if scene_file:
legacy_candidates.append(f"{os.path.splitext(scene_file)[0]}.bam")
removed_files = []
for legacy_path in legacy_candidates:
@ -923,6 +916,37 @@ class ProjectManager:
return False
self._clearCurrentScene()
if self._load_scene_description_via_ssbo(scene_description, project_path, asset_database):
scene_manager = getattr(self.world, "scene_manager", None)
scene_components = dict(scene_description.get("scene_components", {}) or {})
camera_state = dict(scene_components.get("camera", {}) or scene_description.get("camera", {}) or {})
camera = getattr(self.world, "camera", None) or getattr(self.world, "cam", None)
if camera and not camera.isEmpty():
try:
position = list(camera_state.get("position", []) or [])
rotation = list(camera_state.get("rotation", []) or [])
if len(position) >= 3:
camera.setPos(float(position[0]), float(position[1]), float(position[2]))
if len(rotation) >= 3:
camera.setHpr(float(rotation[0]), float(rotation[1]), float(rotation[2]))
if "camera_control_enabled" in camera_state:
self.world.camera_control_enabled = bool(camera_state.get("camera_control_enabled", True))
except Exception:
pass
gui_snapshot = list((scene_components.get("gui", {}) or {}).get("elements", []) or scene_description.get("gui", []) or [])
lui_snapshot = dict(scene_components.get("lui", {}) or scene_description.get("lui", {}) or {})
scene_paths = self._scene_paths(scene_entry or {}, project_path=project_path) if scene_entry else {}
self._write_scene_sidecars(scene_paths, gui_snapshot, lui_snapshot)
try:
load_lui_fn = getattr(scene_manager, "_load_lui_snapshot_file", None) if scene_manager else None
if callable(load_lui_fn) and scene_paths.get("scene_file"):
temp_stub = os.path.splitext(scene_paths["scene_file"])[0] + ".bam"
load_lui_fn(temp_stub)
except Exception:
pass
return True
scene_root, keep_nodes = self._build_scene_root_from_description(
scene_description,
project_path,
@ -966,6 +990,12 @@ class ProjectManager:
scene_manager.models = built_model_nodes
scene_manager.Spotlight = built_spot_lights
scene_manager.Pointlight = built_point_lights
update_tree_fn = getattr(scene_manager, "updateSceneTree", None)
if callable(update_tree_fn):
try:
update_tree_fn()
except Exception:
pass
try:
if not scene_root.isEmpty():
@ -974,6 +1004,20 @@ class ProjectManager:
pass
scene_components = dict(scene_description.get("scene_components", {}) or {})
camera_state = dict(scene_components.get("camera", {}) or scene_description.get("camera", {}) or {})
camera = getattr(self.world, "camera", None) or getattr(self.world, "cam", None)
if camera and not camera.isEmpty():
try:
position = list(camera_state.get("position", []) or [])
rotation = list(camera_state.get("rotation", []) or [])
if len(position) >= 3:
camera.setPos(float(position[0]), float(position[1]), float(position[2]))
if len(rotation) >= 3:
camera.setHpr(float(rotation[0]), float(rotation[1]), float(rotation[2]))
if "camera_control_enabled" in camera_state:
self.world.camera_control_enabled = bool(camera_state.get("camera_control_enabled", True))
except Exception:
pass
gui_snapshot = list((scene_components.get("gui", {}) or {}).get("elements", []) or scene_description.get("gui", []) or [])
lui_snapshot = dict(scene_components.get("lui", {}) or scene_description.get("lui", {}) or {})
scene_paths = self._scene_paths(scene_entry or {}, project_path=project_path) if scene_entry else {}
@ -981,13 +1025,179 @@ class ProjectManager:
try:
load_lui_fn = getattr(scene_manager, "_load_lui_snapshot_file", None) if scene_manager else None
if callable(load_lui_fn) and scene_paths.get("scene_lui_file"):
temp_stub = os.path.splitext(scene_paths["scene_lui_file"])[0] + ".bam"
if callable(load_lui_fn) and scene_paths.get("scene_file"):
temp_stub = os.path.splitext(scene_paths["scene_file"])[0] + ".bam"
load_lui_fn(temp_stub)
except Exception:
pass
return True
def _iter_top_level_scene_asset_nodes(self, scene_description):
nodes = list(scene_description.get("nodes", []) or [])
node_lookup = {
str(node.get("node_id", "") or ""): dict(node)
for node in nodes
if node.get("node_id") is not None
}
top_level_nodes = []
for node_id, node in node_lookup.items():
asset_guid = str(node.get("asset_guid", "") or (node.get("components", {}) or {}).get("model", {}).get("asset_guid", "") or "").strip()
if not asset_guid:
continue
parent_id = node.get("parent_id")
has_asset_parent = False
while parent_id:
parent_node = node_lookup.get(parent_id)
if not parent_node:
break
parent_asset_guid = str(parent_node.get("asset_guid", "") or (parent_node.get("components", {}) or {}).get("model", {}).get("asset_guid", "") or "").strip()
if parent_asset_guid:
has_asset_parent = True
break
parent_id = parent_node.get("parent_id")
if not has_asset_parent:
top_level_nodes.append(node)
return top_level_nodes, node_lookup
def _apply_scene_description_state_to_node(self, target_np, node, project_path):
if not target_np or target_np.isEmpty() or not isinstance(node, dict):
return
node_name = str(node.get("name", "") or target_np.getName() or "Node")
target_np.setName(node_name)
transform = dict(node.get("transform", {}) or {})
position = list(transform.get("position", [0, 0, 0]) or [0, 0, 0])
rotation = list(transform.get("rotation", [0, 0, 0]) or [0, 0, 0])
scale = list(transform.get("scale", [1, 1, 1]) or [1, 1, 1])
if len(position) >= 3:
target_np.setPos(float(position[0]), float(position[1]), float(position[2]))
if len(rotation) >= 3:
target_np.setHpr(float(rotation[0]), float(rotation[1]), float(rotation[2]))
if len(scale) >= 3:
target_np.setScale(float(scale[0]), float(scale[1]), float(scale[2]))
visibility = dict(node.get("visibility", {}) or {})
user_visible = bool(visibility.get("user_visible", True))
target_np.setPythonTag("user_visible", user_visible)
target_np.setTag("user_visible", "true" if user_visible else "false")
if user_visible:
target_np.show()
else:
target_np.hide()
for tag_name, tag_value in dict(node.get("tags", {}) or {}).items():
if tag_value is None:
continue
target_np.setTag(str(tag_name), str(tag_value))
components = dict(node.get("components", {}) or {})
model_component = dict(components.get("model", {}) or {})
scripts_component = dict(components.get("scripts", {}) or {})
metadata_component = dict(components.get("metadata", {}) or {})
asset_guid = str(model_component.get("asset_guid", "") or node.get("asset_guid", "") or "").strip()
asset_path = str(model_component.get("asset_path", "") or node.get("asset_path", "") or "").strip()
imported_node_key = str(model_component.get("imported_node_key", "") or node.get("imported_node_key", "") or "").strip()
if asset_guid:
target_np.setTag("asset_guid", asset_guid)
if asset_path:
asset_abs_path = os.path.join(project_path, asset_path.replace("/", os.sep))
target_np.setTag("asset_path", asset_path)
target_np.setTag("model_path", asset_abs_path)
target_np.setTag("saved_model_path", asset_abs_path)
target_np.setTag("original_path", asset_abs_path)
target_np.setTag("file", os.path.basename(asset_abs_path))
if imported_node_key:
target_np.setTag("imported_node_key", imported_node_key)
target_np.setTag("is_model_root", "1")
target_np.setTag("is_scene_element", "1")
runtime_interactive = bool(metadata_component.get("runtime_interactive", node.get("runtime_interactive", False)))
if runtime_interactive:
target_np.setTag("runtime_interactive", "true")
scripts = list(scripts_component.get("entries", []) or node.get("scripts", []) or [])
if scripts:
target_np.setTag("has_scripts", "true")
target_np.setTag("scripts_info", json.dumps(scripts, ensure_ascii=False))
def _load_scene_description_via_ssbo(self, scene_description, project_path, asset_database):
if not getattr(self.world, "use_ssbo_mouse_picking", False):
return False
ssbo_editor = getattr(self.world, "ssbo_editor", None)
if not ssbo_editor:
return False
top_level_asset_nodes, node_lookup = self._iter_top_level_scene_asset_nodes(scene_description)
if not top_level_asset_nodes:
return False
loaded_any = False
for node in top_level_asset_nodes:
components = dict(node.get("components", {}) or {})
model_component = dict(components.get("model", {}) or {})
asset_guid = str(model_component.get("asset_guid", "") or node.get("asset_guid", "") or "").strip()
if not asset_guid:
continue
asset_record = asset_database.get_asset(asset_guid)
if not asset_record:
continue
asset_rel = str(asset_record.get("asset_path", "") or model_component.get("asset_path", "") or node.get("asset_path", "") or "").strip()
if not asset_rel:
continue
asset_abs = os.path.join(project_path, asset_rel.replace("/", os.sep))
if not os.path.exists(asset_abs):
continue
try:
imported_root = ssbo_editor.load_model(
asset_abs,
keep_source_model=False,
append=loaded_any,
scene_package_import=False,
)
except Exception as e:
print(f"⚠️ SSBO 恢复场景模型失败 {asset_abs}: {e}")
continue
target_root = imported_root if imported_root and not imported_root.isEmpty() else None
if target_root is None:
continue
self._apply_scene_description_state_to_node(target_root, node, project_path)
loaded_any = True
if not loaded_any:
return False
rebuild_fn = getattr(ssbo_editor, "_rebuild_runtime_from_source_root", None)
if callable(rebuild_fn):
try:
rebuild_fn(highlight_root_name=None)
except Exception:
pass
scene_manager = getattr(self.world, "scene_manager", None)
runtime_model = getattr(ssbo_editor, "model", None)
if scene_manager:
scene_manager.models = [runtime_model] if runtime_model and not runtime_model.isEmpty() else []
scene_manager.Spotlight = []
scene_manager.Pointlight = []
update_tree_fn = getattr(scene_manager, "updateSceneTree", None)
if callable(update_tree_fn):
try:
update_tree_fn()
except Exception:
pass
return True
def _should_keep_scene_description_node(self, node_id, node, node_lookup):
if not node_id:
@ -1972,9 +2182,95 @@ class ProjectManager:
def _clearCurrentScene(self):
"""清空当前场景"""
try:
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
self.world.scene_manager.clearScene()
print("✓ 当前场景已清空")
selection = getattr(self.world, "selection", None)
if selection and hasattr(selection, "clearSelection"):
try:
selection.clearSelection()
except Exception:
pass
script_manager = getattr(self.world, "script_manager", None)
if script_manager and hasattr(script_manager, "reset_scene_state"):
try:
script_manager.reset_scene_state()
except Exception:
pass
ssbo_editor = getattr(self.world, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "reset_scene_state"):
try:
ssbo_editor.reset_scene_state()
except Exception:
pass
scene_manager = getattr(self.world, "scene_manager", None)
if scene_manager:
tree_widget = scene_manager._get_tree_widget() if hasattr(scene_manager, "_get_tree_widget") else None
for model in list(getattr(scene_manager, "models", []) or []):
try:
if tree_widget:
tree_widget.delete_item(model)
elif model and not model.isEmpty():
model.removeNode()
except Exception:
pass
scene_manager.models = []
for collection_name in ("Spotlight", "Pointlight"):
collection = list(getattr(scene_manager, collection_name, []) or [])
for light_node in collection:
try:
if tree_widget:
tree_widget.delete_item(light_node)
elif light_node and not light_node.isEmpty():
light_node.removeNode()
except Exception:
pass
setattr(scene_manager, collection_name, [])
terrain_manager = getattr(self.world, "terrain_manager", None)
terrains = list(getattr(terrain_manager, "terrains", []) or []) if terrain_manager else []
for terrain in terrains:
try:
if tree_widget:
tree_widget.delete_item(terrain)
elif terrain and not terrain.isEmpty():
terrain.removeNode()
except Exception:
pass
cleanup_aux_fn = getattr(scene_manager, "_cleanupAuxiliaryNodes", None)
if callable(cleanup_aux_fn):
try:
cleanup_aux_fn()
except Exception:
pass
cleanup_render_fn = getattr(scene_manager, "_cleanup_untracked_render_children", None)
if callable(cleanup_render_fn):
try:
cleanup_render_fn()
except Exception:
pass
update_tree_fn = getattr(scene_manager, "updateSceneTree", None)
if callable(update_tree_fn):
try:
update_tree_fn()
except Exception:
pass
lui_manager = getattr(self.world, "lui_manager", None)
if lui_manager:
clear_lui_fn = getattr(lui_manager, "_clear_lui_structure_runtime", None)
if callable(clear_lui_fn):
try:
clear_lui_fn()
except Exception:
pass
print("✓ 当前场景已清空")
except Exception as e:
print(f"清空场景失败: {str(e)}")

View File

@ -214,6 +214,13 @@ def _collect_material_override_component(node):
def _resolve_imported_node_key(node, fallback_key: str) -> str:
if node.hasTag("is_model_root"):
for tag_name in ("imported_node_key", "source_model_node_key", "ssbo_tree_key", "tree_item_key"):
if node.hasTag(tag_name):
tag_value = str(node.getTag(tag_name) or "").strip()
if tag_value:
return tag_value
return ""
for tag_name in ("imported_node_key", "source_model_node_key", "ssbo_tree_key", "tree_item_key"):
if node.hasTag(tag_name):
tag_value = str(node.getTag(tag_name) or "").strip()
@ -222,6 +229,22 @@ def _resolve_imported_node_key(node, fallback_key: str) -> str:
return fallback_key
def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], runtime_interactive: bool, light_component: dict, material_component: dict) -> bool:
if not node:
return False
if not asset_guid:
return True
if scripts:
return True
if runtime_interactive:
return True
if light_component:
return True
if material_component:
return True
return False
def _build_scene_components(camera_state: dict, gui_elements: list, lui_snapshot: dict) -> dict:
return {
"camera": dict(camera_state or {}),
@ -304,6 +327,9 @@ def _build_scene_description_payload(
script_info["project_relative_path"] = asset_path_for_script
script_info["file"] = asset_path_for_script
light_component = _collect_light_component(child)
material_component = _collect_material_override_component(child)
node_entries.append(
{
"node_id": node_id,
@ -330,8 +356,8 @@ def _build_scene_description_payload(
"components": {
"model": _collect_model_component(child, asset_guid, asset_path, imported_node_key),
"scripts": _collect_script_component(scripts),
"light": _collect_light_component(child),
"material_overrides": _collect_material_override_component(child),
"light": light_component,
"material_overrides": material_component,
"metadata": _collect_node_metadata_component(child, runtime_interactive),
},
"tags": {
@ -340,11 +366,19 @@ def _build_scene_description_payload(
},
}
)
try:
child_nodes = list(child.getChildren())
except Exception:
child_nodes = []
walk_nodes(child_nodes, node_id)
if _should_serialize_child_nodes(
child,
asset_guid,
scripts,
runtime_interactive,
light_component,
material_component,
):
try:
child_nodes = list(child.getChildren())
except Exception:
child_nodes = []
walk_nodes(child_nodes, node_id)
walk_nodes(root_nodes)

View File

@ -807,7 +807,7 @@ class ObjectController:
for chunk_id in target_chunks:
self._set_chunk_dynamic(chunk_id, True)
def bake_ids_and_collect(self, model):
def bake_ids_and_collect_hybrid(self, model):
"""
构建混合架构:
1) 把每个 geom 拆成可独立编辑的动态对象

View File

@ -972,7 +972,15 @@ class SSBOEditor:
working_root = root.copy_to(working_holder)
self.controller = ObjectController()
count = self.controller.bake_ids_and_collect(working_root)
try:
geom_count = len(list(working_root.find_all_matches("**/+GeomNode")))
except Exception:
geom_count = 0
use_hybrid_mode = geom_count > 0 and geom_count <= 256
if use_hybrid_mode:
count = self.controller.bake_ids_and_collect_hybrid(working_root)
else:
count = self.controller.bake_ids_and_collect(working_root)
self.model = self.controller.model
self.model.reparent_to(self.base.render)
@ -1000,7 +1008,8 @@ class SSBOEditor:
except Exception:
pass
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
mode_name = "hybrid" if use_hybrid_mode else "flat"
print(f"[SSBOEditor] Model loaded. Total objects: {count}, mode={mode_name}, geoms={geom_count}")
def load_model(
self,
@ -2126,9 +2135,14 @@ class SSBOEditor:
if not self.controller or self.selected_name is None:
return None
has_dynamic_objects = bool(getattr(self.controller, "id_to_object_np", {}) or {})
if self._is_root_selection():
return self.model if self._node_is_valid(self.model) else None
if not has_dynamic_objects:
return self.model if self._node_is_valid(self.model) else None
if len(self.selected_ids) > 1:
proxy = getattr(self, "_group_proxy", None)
if proxy and self._node_is_valid(proxy):
@ -2265,17 +2279,27 @@ class SSBOEditor:
self.selected_name = key
self.selected_ids = self.controller.name_to_ids.get(key, [])
has_dynamic_objects = bool(getattr(self.controller, "id_to_object_np", {}) or {})
is_root_selection = (
self.controller and
key == getattr(self.controller, "tree_root_key", None)
)
is_top_level_heavy_selection = False
if self.controller and not is_root_selection:
try:
root_key = getattr(self.controller, "tree_root_key", None)
root_node = self.controller.tree_nodes.get(root_key, {}) if root_key else {}
top_level_children = set(root_node.get("children", []) or [])
is_top_level_heavy_selection = key in top_level_children and len(self.selected_ids) >= 256
except Exception:
is_top_level_heavy_selection = False
if sync_world_selection:
self._clear_editor_selection_visuals()
self._sync_editor_selection_reference(self.get_selection_scene_node())
# Root selection should stay lightweight:
# keep static chunks active and transform the model root directly.
if is_root_selection:
if is_root_selection or is_top_level_heavy_selection or not has_dynamic_objects:
self.controller.set_active_ids([])
if self._outline_manager:
self._outline_manager.clear()

View File

@ -1321,6 +1321,9 @@ class AppActions:
except Exception:
normalized_model_path = file_path
asset_guid = ""
asset_path = ""
if normalized_model_path:
model_np.setTag("model_path", normalized_model_path)
model_np.setTag("saved_model_path", normalized_model_path)
@ -1333,9 +1336,11 @@ class AppActions:
asset_record = project_manager.register_project_asset(file_path)
if asset_record:
if asset_record.get("guid"):
model_np.setTag("asset_guid", asset_record["guid"])
asset_guid = asset_record["guid"]
model_np.setTag("asset_guid", asset_guid)
if asset_record.get("asset_path"):
model_np.setTag("asset_path", asset_record["asset_path"])
asset_path = asset_record["asset_path"]
model_np.setTag("asset_path", asset_path)
except Exception as e:
print(f"[SSBO] register_project_asset failed: {e}")
@ -1372,6 +1377,37 @@ class AppActions:
except Exception:
pass
target_source_child = None
last_import_root_name = getattr(ssbo_editor, "last_import_root_name", None) if ssbo_editor else None
if source_children:
if last_import_root_name:
for source_child in source_children:
try:
if source_child.getName() == last_import_root_name:
target_source_child = source_child
break
except Exception:
continue
if target_source_child is None:
target_source_child = source_children[-1]
if target_source_child is not None:
try:
if normalized_model_path:
target_source_child.setTag("model_path", normalized_model_path)
target_source_child.setTag("saved_model_path", normalized_model_path)
if file_path:
target_source_child.setTag("original_path", file_path)
target_source_child.setTag("file", os.path.basename(file_path))
if asset_guid:
target_source_child.setTag("asset_guid", asset_guid)
if asset_path:
target_source_child.setTag("asset_path", asset_path)
target_source_child.setTag("is_model_root", "1")
target_source_child.setTag("is_scene_element", "1")
except Exception as e:
print(f"[SSBO] sync source child tags failed: {e}")
if scene_manager:
try:
scene_manager.setupCollision(model_np)

View File

@ -80,6 +80,9 @@ class EditorPanelsLeftMixin:
ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
if ssbo_model and not ssbo_model.isEmpty() and ssbo_model.hasParent():
return [ssbo_model]
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"):
@ -95,11 +98,11 @@ class EditorPanelsLeftMixin:
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 []
return self._get_ssbo_virtual_top_level_model_keys(controller)
root_node = controller.tree_nodes.get(controller.tree_root_key)
if not root_node:
return []
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", []):
@ -108,6 +111,35 @@ class EditorPanelsLeftMixin:
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)
@ -126,14 +158,24 @@ class EditorPanelsLeftMixin:
return True
node_data = controller.tree_nodes.get(key)
if not node_data:
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 = controller.display_names.get(key, key)
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_key) for child_key in node_data["children"])
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()
@ -385,7 +427,7 @@ class EditorPanelsLeftMixin:
node_data = controller.tree_nodes.get(key)
if not node_data:
return
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):
@ -433,6 +475,59 @@ class EditorPanelsLeftMixin:
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)
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())
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})##{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})##{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