class_name StudioHierarchyDock extends VBoxContainer ## Scene Hierarchy Tree Dock for ModBison Studio. ## Displays all world objects, supports search filtering, selection sync, ## parent-child hierarchies, duplicate, and deletion. signal object_selected(object_id: String) signal delete_requested(object_id: String) signal duplicate_requested(object_id: String) signal group_created(object_ids: Array[String]) var _search_edit: LineEdit var _tree: Tree var _tree_root: TreeItem var _btn_delete: Button var _btn_duplicate: Button var _btn_group: Button var _items_by_id: Dictionary = {} var _updating: bool = false var _core: EditorCore = null func _init() -> void: name = "StudioHierarchyDock" custom_minimum_size = Vector2(240, 300) size_flags_vertical = Control.SIZE_EXPAND_FILL _build_ui() func setup(core: EditorCore) -> void: _core = core if _core != null and _core.selection != null: _core.selection.selection_changed.connect(_on_core_selection_changed) refresh_tree() func _build_ui() -> void: var header := Label.new() header.text = "Scene Hierarchy" header.add_theme_font_size_override("font_size", 13) header.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4)) add_child(header) _search_edit = LineEdit.new() _search_edit.placeholder_text = "🔍 Search hierarchy..." _search_edit.text_changed.connect(_on_search_changed) add_child(_search_edit) _tree = Tree.new() _tree.size_flags_vertical = Control.SIZE_EXPAND_FILL _tree.select_mode = Tree.SELECT_MULTI _tree.item_selected.connect(_on_tree_item_selected) add_child(_tree) # Action buttons row var btn_row := HBoxContainer.new() _btn_duplicate = Button.new() _btn_duplicate.text = "Duplicate" _btn_duplicate.size_flags_horizontal = Control.SIZE_EXPAND_FILL _btn_duplicate.pressed.connect(_on_duplicate_pressed) btn_row.add_child(_btn_duplicate) _btn_group = Button.new() _btn_group.text = "Group" _btn_group.size_flags_horizontal = Control.SIZE_EXPAND_FILL _btn_group.pressed.connect(_on_group_pressed) btn_row.add_child(_btn_group) _btn_delete = Button.new() _btn_delete.text = "Delete" _btn_delete.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) _btn_delete.size_flags_horizontal = Control.SIZE_EXPAND_FILL _btn_delete.pressed.connect(_on_delete_pressed) btn_row.add_child(_btn_delete) add_child(btn_row) func refresh_tree() -> void: _tree.clear() _items_by_id.clear() _tree_root = _tree.create_item() _tree_root.set_text(0, "World Scene") if _core == null or _core.model == null: return var filter := _search_edit.text.strip_edges().to_lower() var objects = _core.model.payload.objects # First pass: identify parents and root objects for obj in objects: var parent_id := "" if obj.properties is Dictionary: parent_id = str(obj.properties.get("parent_id", "")) var dname: String = str(obj.id) if obj.properties is Dictionary and obj.properties.has("display_name"): dname = str(obj.properties["display_name"]) if filter != "" and not dname.to_lower().contains(filter) and not obj.id.to_lower().contains(filter): continue var item := _tree.create_item(_tree_root) item.set_text(0, "🔹 " + dname) item.set_metadata(0, obj.id) _items_by_id[obj.id] = item # Highlight current selection if _core.selection != null: var sel := _core.selection.get_selected() if sel != "" and _items_by_id.has(sel): _items_by_id[sel].select(0) func _on_search_changed(_text: String) -> void: refresh_tree() func _on_tree_item_selected() -> void: if _updating: return var selected_item := _tree.get_selected() if selected_item != null: var obj_id: String = str(selected_item.get_metadata(0)) if obj_id != "": object_selected.emit(obj_id) if _core != null: _updating = true _core.select_object(obj_id) _updating = false func _on_core_selection_changed(obj_id: String) -> void: if _updating: return _updating = true _tree.deselect_all() if obj_id != "" and _items_by_id.has(obj_id): _items_by_id[obj_id].select(0) _items_by_id[obj_id].scroll_to_visible() _updating = false func _on_delete_pressed() -> void: if _core == null: return var sel := _core.selection.get_selected() if sel != "": delete_requested.emit(sel) _core.delete_object(sel) refresh_tree() func _on_duplicate_pressed() -> void: if _core == null: return var sel := _core.selection.get_selected() if sel != "": var obj := _core.model.find_object(sel) if obj != null: var new_pos := obj.position + Vector3(1.0, 1.0, 0.0) var cat_id := str(obj.properties.get("catalog_id", "crate_small")) var new_id := _core.place_object(cat_id, new_pos, obj.rotation, obj.scale, obj.properties.duplicate(true)) if new_id != "": duplicate_requested.emit(new_id) _core.select_object(new_id) refresh_tree() func _on_group_pressed() -> void: var selected_ids: Array[String] = [] var item := _tree.get_next_selected(null) while item != null: var meta = item.get_metadata(0) if meta != null and str(meta) != "": selected_ids.append(str(meta)) item = _tree.get_next_selected(item) if selected_ids.size() >= 2: group_created.emit(selected_ids)