286 lines
9.3 KiB
GDScript
286 lines
9.3 KiB
GDScript
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.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
|