Files
modbison/scripts/editor/ui/palette_panel.gd
T
2026-09-21 18:11:06 +00:00

288 lines
9.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name PalettePanel
extends VBoxContainer
## Catalog-sourced object palette (docs/architecture/editor.md §7.2
## `PalettePanel`). Lists every id in an `EditorObjectCatalog` and emits
## which one the player picked; `InGameEditor` remembers it as the pending
## `catalog_id` for the next `ObjectPlaceTool` placement.
##
## Creator Studio feature: In-game .glb/.gltf 3D Model Importer.
## Creators can import custom 3D models from disk, validate them via GLTFDocument,
## and place them directly into their worlds.
signal object_selected(catalog_id: String)
signal model_imported(catalog_id: String)
var _list: ItemList
var _catalog: EditorObjectCatalog = null
var _ids: Array[String] = []
var _btn_import_model: Button
var _import_panel: VBoxContainer
var _model_name_edit: LineEdit
var _model_path_edit: LineEdit
var _btn_browse: Button
var _file_dialog: FileDialog
var _import_status_label: Label
func _init() -> void:
name = "PalettePanel"
size_flags_vertical = Control.SIZE_EXPAND_FILL
func _ready() -> void:
var header := Label.new()
header.text = "Palette"
header.add_theme_font_size_override("font_size", 15)
header.add_theme_color_override("font_color", Color(1, 0.95, 0.4))
add_child(header)
var hint := Label.new()
hint.text = "Click an item to place on the grid:"
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint.add_theme_font_size_override("font_size", 11)
hint.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
add_child(hint)
_list = ItemList.new()
_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
_list.custom_minimum_size = Vector2(0, 160)
_list.item_selected.connect(_on_item_selected)
add_child(_list)
_setup_import_ui()
func _setup_import_ui() -> void:
_btn_import_model = Button.new()
_btn_import_model.text = "+ Import .glb Model"
_btn_import_model.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0))
_btn_import_model.pressed.connect(_on_import_button_pressed)
add_child(_btn_import_model)
_import_panel = VBoxContainer.new()
_import_panel.visible = false
add_child(_import_panel)
var title := Label.new()
title.text = "Import Custom Model (.glb)"
title.add_theme_font_size_override("font_size", 12)
title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
_import_panel.add_child(title)
var name_row := HBoxContainer.new()
var name_lbl := Label.new()
name_lbl.text = "Name:"
name_lbl.custom_minimum_size = Vector2(50, 0)
name_row.add_child(name_lbl)
_model_name_edit = LineEdit.new()
_model_name_edit.placeholder_text = "e.g. Vintage Chair"
_model_name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
name_row.add_child(_model_name_edit)
_import_panel.add_child(name_row)
var path_row := HBoxContainer.new()
var path_lbl := Label.new()
path_lbl.text = "Path:"
path_lbl.custom_minimum_size = Vector2(50, 0)
path_row.add_child(path_lbl)
_model_path_edit = LineEdit.new()
_model_path_edit.placeholder_text = "Path to .glb or .gltf"
_model_path_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
path_row.add_child(_model_path_edit)
_btn_browse = Button.new()
_btn_browse.text = "Browse..."
_btn_browse.pressed.connect(_on_browse_pressed)
path_row.add_child(_btn_browse)
_import_panel.add_child(path_row)
var sample_row := HBoxContainer.new()
var sample_lbl := Label.new()
sample_lbl.text = "Sample:"
sample_lbl.custom_minimum_size = Vector2(50, 0)
sample_row.add_child(sample_lbl)
var sample_option := OptionButton.new()
sample_option.add_item("— Pick a sample model —", 0)
sample_option.add_item("Treasure Chest", 1)
sample_option.set_item_metadata(1, {"path": "res://assets/props/chest.glb", "name": "Treasure Chest"})
sample_option.add_item("Pine Tree", 2)
sample_option.set_item_metadata(2, {"path": "res://assets/props/tree_pine_01.glb", "name": "Pine Tree"})
sample_option.add_item("Lamp Post", 3)
sample_option.set_item_metadata(3, {"path": "res://assets/props/lamp_post_01.glb", "name": "Lamp Post"})
sample_option.add_item("Buffalo Chip Coin", 4)
sample_option.set_item_metadata(4, {"path": "res://assets/props/buffalo_chip_coin.glb", "name": "Buffalo Chip Coin"})
sample_option.add_item("Small Crate", 5)
sample_option.set_item_metadata(5, {"path": "res://assets/props/crate_small.glb", "name": "Small Crate"})
sample_option.add_item("Bison", 6)
sample_option.set_item_metadata(6, {"path": "res://assets/props/bison.glb", "name": "Bison"})
sample_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
sample_option.item_selected.connect(func(idx: int):
if idx > 0:
var meta: Dictionary = sample_option.get_item_metadata(idx)
_model_path_edit.text = meta.get("path", "")
if _model_name_edit.text.strip_edges().is_empty():
_model_name_edit.text = meta.get("name", "")
)
sample_row.add_child(sample_option)
_import_panel.add_child(sample_row)
_import_status_label = Label.new()
_import_status_label.text = ""
_import_status_label.add_theme_font_size_override("font_size", 10)
_import_panel.add_child(_import_status_label)
var btn_row := HBoxContainer.new()
var do_import_btn := Button.new()
do_import_btn.text = "Import"
do_import_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
do_import_btn.pressed.connect(_on_do_import_pressed)
btn_row.add_child(do_import_btn)
var cancel_btn := Button.new()
cancel_btn.text = "Cancel"
cancel_btn.pressed.connect(_on_cancel_import_pressed)
btn_row.add_child(cancel_btn)
_import_panel.add_child(btn_row)
_file_dialog = FileDialog.new()
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_file_dialog.filters = ["*.glb ; GLTF Binary Files", "*.gltf ; GLTF Scene Files"]
_file_dialog.file_selected.connect(_on_file_selected)
add_child(_file_dialog)
func set_catalog(catalog: EditorObjectCatalog) -> void:
_catalog = catalog
_refresh()
func _refresh() -> void:
_list.clear()
_ids.clear()
if _catalog == null or not _catalog.is_loaded():
return
for id in _catalog.get_all_ids():
var entry := _catalog.resolve(id)
var label: String = entry.get("display_name", id)
if entry.get("category", "") == "custom":
label = "★ " + label
_list.add_item(label)
_ids.append(id)
func _on_item_selected(index: int) -> void:
if index < 0 or index >= _ids.size():
return
object_selected.emit(_ids[index])
func get_selected_catalog_id() -> String:
var selected: PackedInt32Array = _list.get_selected_items()
if selected.is_empty():
return ""
return _ids[selected[0]]
func _on_import_button_pressed() -> void:
_import_panel.visible = true
_btn_import_model.visible = false
_import_status_label.text = ""
func _on_cancel_import_pressed() -> void:
_import_panel.visible = false
_btn_import_model.visible = true
func _on_browse_pressed() -> void:
_file_dialog.popup_centered(Vector2i(600, 400))
func _on_file_selected(path: String) -> void:
_model_path_edit.text = path
if _model_name_edit.text.strip_edges().is_empty():
_model_name_edit.text = path.get_file().get_basename().capitalize()
func _on_do_import_pressed() -> void:
var path := _model_path_edit.text.strip_edges()
var display_name := _model_name_edit.text.strip_edges()
if path.is_empty():
_import_status_label.text = "Error: Please select a file path."
_import_status_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
return
if display_name.is_empty():
display_name = path.get_file().get_basename().capitalize()
var cat_id := import_model_file(path, display_name)
if cat_id.is_empty():
_import_status_label.text = "Error: Failed to import/validate model."
_import_status_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
return
_import_panel.visible = false
_btn_import_model.visible = true
_model_name_edit.text = ""
_model_path_edit.text = ""
## Import a 3D model (.glb/.gltf), validate it, copy to user custom assets,
## and register it into the active EditorObjectCatalog.
func import_model_file(file_path: String, display_name: String, category: String = "custom") -> String:
if not FileAccess.file_exists(file_path):
push_error("PalettePanel: File does not exist: %s" % file_path)
return ""
# Validate model using GLTFDocument
var gltf := GLTFDocument.new()
var state := GLTFState.new()
var err := gltf.append_from_file(file_path, state)
if err != OK:
push_error("PalettePanel: GLTFDocument failed to parse file (%d): %s" % [err, file_path])
return ""
# Copy to user custom assets directory
var dir := DirAccess.open("user://")
if dir != null:
if not dir.dir_exists("custom_assets"):
dir.make_dir("custom_assets")
var filename := file_path.get_file()
var target_path := "user://custom_assets/%s" % filename
var copy_err := DirAccess.copy_absolute(file_path, target_path)
if copy_err != OK:
# If direct copy failed, use original path
target_path = file_path
# Generate clean unique catalog id
var clean_slug := display_name.to_lower().replace(" ", "_").replace("-", "_")
var regex := RegEx.new()
regex.compile("[^a-z0-9_]")
clean_slug = regex.sub(clean_slug, "", true)
if clean_slug.is_empty():
clean_slug = "model"
var cat_id := "custom_%s" % clean_slug
var catalog_entry: Dictionary = {
"asset_id": target_path,
"type": "prop",
"display_name": display_name,
"category": category
}
if _catalog != null:
_catalog.register_custom_object(cat_id, catalog_entry)
_refresh()
# Select the newly imported model
for i in range(_ids.size()):
if _ids[i] == cat_id:
_list.select(i)
object_selected.emit(cat_id)
break
model_imported.emit(cat_id)
return cat_id