class_name StudioCollectionsDock extends VBoxContainer ## Model Collections Browser Dock for ModBison Studio. ## Provides collection browsing, theme filtering, item previews, ## and drag-and-drop model placement into the 3D viewport. signal model_selected(model_data: Dictionary) signal model_drag_started(model_data: Dictionary) signal collection_selected(collection_id: int) const AssetCacheManagerClass = preload("res://scripts/studio/asset_cache_manager.gd") var _search_edit: LineEdit var _theme_option: OptionButton var _collections_scroll: ScrollContainer var _collections_vbox: VBoxContainer var _models_grid: GridContainer var _status_label: Label var _collections_data: Array[Dictionary] = [] var _selected_collection_id: int = -1 var _cache_mgr = null var _is_built: bool = false func _init() -> void: name = "StudioCollectionsDock" custom_minimum_size = Vector2(280, 400) size_flags_vertical = Control.SIZE_EXPAND_FILL _cache_mgr = AssetCacheManagerClass.get_instance() if not _is_built: _build_ui() _load_default_preset_collections() func _ready() -> void: if not _is_built: _build_ui() _load_default_preset_collections() func _build_ui() -> void: if _is_built: return _is_built = true var header := Label.new() header.text = "Poly Model Collections" header.add_theme_font_size_override("font_size", 14) header.add_theme_color_override("font_color", Color(1.0, 0.9, 0.35)) add_child(header) var sub := Label.new() sub.text = "Curated themed 3D model packs. Drag onto viewport to place." sub.add_theme_font_size_override("font_size", 10) sub.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) sub.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART add_child(sub) # Search Bar _search_edit = LineEdit.new() _search_edit.placeholder_text = "🔍 Search collections & models..." _search_edit.text_changed.connect(_on_search_changed) add_child(_search_edit) # Theme Filter var filter_row := HBoxContainer.new() var theme_lbl := Label.new() theme_lbl.text = "Theme:" theme_lbl.add_theme_font_size_override("font_size", 11) filter_row.add_child(theme_lbl) _theme_option = OptionButton.new() _theme_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL _theme_option.add_item("All Themes", 0) _theme_option.set_item_metadata(0, "") var themes := ["fantasy", "medieval", "nature", "sci-fi", "urban", "industrial", "props"] for i in range(themes.size()): var t: String = themes[i] _theme_option.add_item(t.capitalize(), i + 1) _theme_option.set_item_metadata(i + 1, t) _theme_option.item_selected.connect(_on_theme_selected) filter_row.add_child(_theme_option) add_child(filter_row) # Scroll area for collections _collections_scroll = ScrollContainer.new() _collections_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL _collections_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(_collections_scroll) _collections_vbox = VBoxContainer.new() _collections_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL _collections_scroll.add_child(_collections_vbox) _status_label = Label.new() _status_label.text = "Ready" _status_label.add_theme_font_size_override("font_size", 10) _status_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5)) add_child(_status_label) func _load_default_preset_collections() -> void: # Built-in collections for offline / studio design var fantasy_pack := { "id": 101, "title": "Fantasy Ruins & Treasure", "theme": "fantasy", "description": "Stone pillars, treasure chests, and fantasy dungeon props", "items": [ {"id": 1, "name": "Treasure Chest", "catalog_id": "chest_01", "asset_id": "props/chest.glb", "theme": "fantasy"}, {"id": 2, "name": "Buffalo Coin", "catalog_id": "coin_01", "asset_id": "props/buffalo_chip_coin.glb", "theme": "fantasy"}, {"id": 3, "name": "Pine Tree", "catalog_id": "tree_pine", "asset_id": "props/tree_pine_01.glb", "theme": "nature"}, ] } var props_pack := { "id": 102, "title": "Town & Village Props", "theme": "medieval", "description": "Street lamps, wooden crates, and boundary rocks", "items": [ {"id": 4, "name": "Wooden Crate", "catalog_id": "crate_small", "asset_id": "props/crate_small.glb", "theme": "props"}, {"id": 5, "name": "Street Lamp", "catalog_id": "lamp_post", "asset_id": "props/lamp_post_01.glb", "theme": "urban"}, {"id": 6, "name": "Garden Rock", "catalog_id": "rock_01", "asset_id": "props/rock_01.glb", "theme": "nature"}, ] } var bison_pack := { "id": 103, "title": "Wildlife & Creatures", "theme": "nature", "description": "Wild Bison and terrain fauna", "items": [ {"id": 7, "name": "ModBison", "catalog_id": "bison", "asset_id": "props/bison.glb", "theme": "nature"}, ] } _collections_data = [fantasy_pack, props_pack, bison_pack] _refresh_collections_display() func add_collection(col_dict: Dictionary) -> void: _collections_data.append(col_dict) _refresh_collections_display() func set_collections(cols: Array[Dictionary]) -> void: _collections_data = cols _refresh_collections_display() func _on_search_changed(_new_text: String) -> void: _refresh_collections_display() func _on_theme_selected(_idx: int) -> void: _refresh_collections_display() func _refresh_collections_display() -> void: for c in _collections_vbox.get_children(): c.queue_free() var search_text := _search_edit.text.strip_edges().to_lower() var selected_theme := "" if _theme_option != null: var meta = _theme_option.get_item_metadata(_theme_option.selected) if meta != null: selected_theme = str(meta).to_lower() var matched_count := 0 for col in _collections_data: var col_theme: String = str(col.get("theme", "")).to_lower() var col_title: String = str(col.get("title", "")) var col_desc: String = str(col.get("description", "")) if selected_theme != "" and col_theme != selected_theme: continue if search_text != "": var matches_title := col_title.to_lower().contains(search_text) var matches_desc := col_desc.to_lower().contains(search_text) var matches_item := false for item in col.get("items", []): if str(item.get("name", "")).to_lower().contains(search_text): matches_item = true break if not matches_title and not matches_desc and not matches_item: continue var card := _create_collection_card(col) _collections_vbox.add_child(card) matched_count += 1 if matched_count == 0: var empty_lbl := Label.new() empty_lbl.text = "No collections found matching filter." empty_lbl.add_theme_font_size_override("font_size", 11) empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) _collections_vbox.add_child(empty_lbl) _status_label.text = "Showing %d collections" % matched_count func _create_collection_card(col: Dictionary) -> Control: var panel := PanelContainer.new() var vbox := VBoxContainer.new() panel.add_child(vbox) var title_lbl := Label.new() title_lbl.text = "📦 " + str(col.get("title", "Collection")) title_lbl.add_theme_font_size_override("font_size", 12) title_lbl.add_theme_color_override("font_color", Color(0.3, 0.85, 1.0)) vbox.add_child(title_lbl) var theme_lbl := Label.new() theme_lbl.text = "Theme: %s | %d models" % [str(col.get("theme", "general")).capitalize(), (col.get("items", []) as Array).size()] theme_lbl.add_theme_font_size_override("font_size", 10) theme_lbl.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) vbox.add_child(theme_lbl) # Grid of models in this collection var items: Array = col.get("items", []) var grid := GridContainer.new() grid.columns = 2 grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL for item in items: if item is Dictionary: var btn := _create_model_item_button(item) grid.add_child(btn) vbox.add_child(grid) return panel func _create_model_item_button(item: Dictionary) -> Button: var btn := ModelDragButton.new() btn.model_data = item btn.text = "🔹 " + str(item.get("name", "Model")) btn.custom_minimum_size = Vector2(120, 28) btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL btn.tooltip_text = "Click to select or drag directly into 3D viewport" btn.pressed.connect(func(): model_selected.emit(item) _status_label.text = "Selected: %s" % item.get("name") ) btn.drag_started.connect(func(data): model_drag_started.emit(data) ) return btn class ModelDragButton extends Button: signal drag_started(data: Dictionary) var model_data: Dictionary = {} func _get_drag_data(_at_position: Vector2) -> Variant: var preview := Label.new() preview.text = "📦 " + str(model_data.get("name", "Model")) preview.add_theme_color_override("font_color", Color(1.0, 0.95, 0.2)) set_drag_preview(preview) drag_started.emit(model_data) return { "type": "model_placement", "catalog_id": model_data.get("catalog_id", ""), "asset_id": model_data.get("asset_id", ""), "model_name": model_data.get("name", ""), "model_data": model_data }