182 lines
5.0 KiB
GDScript
182 lines
5.0 KiB
GDScript
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)
|
|
_tree.scroll_to_item(_items_by_id[obj_id])
|
|
_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)
|