diff --git a/.gitignore b/.gitignore index 3b870ce6..883bec27 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/core/CustomMouseController.py b/core/CustomMouseController.py index b03da669..3d480e18 100644 --- a/core/CustomMouseController.py +++ b/core/CustomMouseController.py @@ -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 \ No newline at end of file + return True diff --git a/project/project_manager.py b/project/project_manager.py index d6f87b18..cfc60f20 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -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)}") diff --git a/project/scene_description.py b/project/scene_description.py index ed1ab69c..62ef93bd 100644 --- a/project/scene_description.py +++ b/project/scene_description.py @@ -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) diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py index 3138caa9..a43c140e 100644 --- a/ssbo_component/ssbo_controller.py +++ b/ssbo_component/ssbo_controller.py @@ -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 拆成可独立编辑的动态对象 diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index 69320120..ba1fe719 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -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() diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index 35d1b653..dc0b1fa9 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -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) diff --git a/ui/panels/editor_panels_left.py b/ui/panels/editor_panels_left.py index 0c481203..52b3b735 100644 --- a/ui/panels/editor_panels_left.py +++ b/ui/panels/editor_panels_left.py @@ -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