fix(editor): move build stamp to bottom, expand palette list, add hover feedback and instructions
checkpoint / checkpoint (push) Successful in 6m5s

This commit is contained in:
2026-09-16 12:21:39 +00:00
parent 42520b33d5
commit 73172b1e28
6 changed files with 195 additions and 28 deletions
+24 -10
View File
@@ -8,11 +8,11 @@ signal tool_selected(tool_id: String)
signal undo_pressed
signal redo_pressed
const TOOL_LABELS := {
"select": "Select",
"brush": "Brush",
"object_place": "Place",
"erase": "Erase",
const TOOL_CONFIG := {
"brush": {"label": "Brush (B)", "tooltip": "Paint solid ground tiles (drag to paint; 0-9 for tile types)"},
"select": {"label": "Select (V)", "tooltip": "Select and inspect placed objects"},
"object_place": {"label": "Place (P)", "tooltip": "Place objects from the Palette onto the grid"},
"erase": {"label": "Erase (X)", "tooltip": "Erase tiles or objects (drag to erase)"},
}
var _tool_buttons: Dictionary = {}
@@ -25,9 +25,11 @@ func _init() -> void:
func _ready() -> void:
for tool_id in TOOL_LABELS:
for tool_id in TOOL_CONFIG:
var btn := Button.new()
btn.text = TOOL_LABELS[tool_id]
var cfg: Dictionary = TOOL_CONFIG[tool_id]
btn.text = cfg["label"]
btn.tooltip_text = cfg["tooltip"]
btn.toggle_mode = true
btn.pressed.connect(_on_tool_button_pressed.bind(tool_id))
add_child(btn)
@@ -36,12 +38,16 @@ func _ready() -> void:
add_child(VSeparator.new())
_undo_button = Button.new()
_undo_button.text = "Undo"
_undo_button.text = "Undo (Ctrl+Z)"
_undo_button.tooltip_text = "Undo last action"
_undo_button.disabled = true
_undo_button.pressed.connect(func(): undo_pressed.emit())
add_child(_undo_button)
_redo_button = Button.new()
_redo_button.text = "Redo"
_redo_button.text = "Redo (Ctrl+Y)"
_redo_button.tooltip_text = "Redo last undone action"
_redo_button.disabled = true
_redo_button.pressed.connect(func(): redo_pressed.emit())
add_child(_redo_button)
@@ -54,7 +60,15 @@ func _on_tool_button_pressed(tool_id: String) -> void:
## Reflect the active tool in the button states without re-emitting the signal.
func set_active_tool(tool_id: String) -> void:
for id in _tool_buttons:
_tool_buttons[id].button_pressed = (id == tool_id)
var btn: Button = _tool_buttons[id]
var is_active: bool = (str(id) == tool_id)
btn.button_pressed = is_active
if is_active:
btn.add_theme_color_override("font_color", Color(1.0, 0.95, 0.3))
btn.add_theme_color_override("font_pressed_color", Color(1.0, 0.95, 0.3))
else:
btn.remove_theme_color_override("font_color")
btn.remove_theme_color_override("font_pressed_color")
## Reflect whether undo/redo currently have anything to do.
+62 -4
View File
@@ -26,6 +26,12 @@ var pending_catalog_id: String = ""
## Tile id the next "brush" click paints. Cycled with number keys 0-9.
var current_tile_id: int = 1
var _hover_tx: int = -1
var _hover_ty: int = -1
var _mouse_down: bool = false
var _last_drag_tx: int = -1
var _last_drag_ty: int = -1
const TILE_EMPTY_COLOR := Color(0.15, 0.15, 0.17)
const TILE_SOLID_COLOR := Color(0.35, 0.55, 0.35)
const GRID_COLOR := Color(0, 0, 0, 0.25)
@@ -39,6 +45,16 @@ func _init() -> void:
focus_mode = Control.FOCUS_ALL
func _notification(what: int) -> void:
if what == NOTIFICATION_MOUSE_EXIT:
_hover_tx = -1
_hover_ty = -1
_mouse_down = false
_last_drag_tx = -1
_last_drag_ty = -1
queue_redraw()
func setup(p_core: EditorCore, p_layer_idx: int = 0) -> void:
core = p_core
layer_idx = p_layer_idx
@@ -76,11 +92,32 @@ func _draw() -> void:
for y in range(layer.height + 1):
draw_line(Vector2(0, y * cell_size), Vector2(layer.width * cell_size, y * cell_size), GRID_COLOR)
# Hover highlight over current cell
if _hover_tx >= 0 and _hover_tx < layer.width and _hover_ty >= 0 and _hover_ty < layer.height:
var h_rect := Rect2(_hover_tx * cell_size, _hover_ty * cell_size, cell_size, cell_size)
draw_rect(h_rect, Color(1.0, 1.0, 1.0, 0.2), true)
draw_rect(h_rect, Color(1.0, 0.95, 0.3, 0.85), false, 2.0)
if core.tools.get_active_tool_id() == "object_place" and pending_catalog_id != "":
var ghost_pos := Vector2((_hover_tx + 0.5) * cell_size, (_hover_ty + 0.5) * cell_size)
draw_circle(ghost_pos, cell_size * 0.35, Color(1.0, 0.8, 0.2, 0.5))
var default_font := ThemeDB.fallback_font
for obj in core.model.payload.objects:
var center := Vector2(obj.position.x * cell_size, obj.position.y * cell_size)
var is_selected: bool = core.selection.get_selected() == obj.id
var color := OBJECT_SELECTED_COLOR if is_selected else OBJECT_COLOR
draw_circle(center, cell_size * 0.3, color)
var r := cell_size * 0.38
draw_circle(center, r, color)
draw_arc(center, r, 0.0, TAU, 16, Color(0, 0, 0, 0.85), 1.5)
if is_selected:
draw_arc(center, r + 3.0, 0.0, TAU, 16, Color(1, 1, 1, 0.95), 2.0)
var display_name: String = ""
if obj.properties is Dictionary and obj.properties.has("display_name"):
display_name = str(obj.properties["display_name"])
if display_name == "":
display_name = obj.id
draw_string(default_font, center + Vector2(-r * 1.5, -r - 2), display_name.left(6), HORIZONTAL_ALIGNMENT_CENTER, int(r * 3.0), 10, Color(1, 1, 1, 0.9))
func _gui_input(event: InputEvent) -> void:
@@ -88,9 +125,27 @@ func _gui_input(event: InputEvent) -> void:
return
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT:
_handle_click(mb.position)
accept_event()
if mb.button_index == MOUSE_BUTTON_LEFT:
_mouse_down = mb.pressed
if mb.pressed:
_handle_click(mb.position)
accept_event()
else:
_last_drag_tx = -1
_last_drag_ty = -1
elif event is InputEventMouseMotion:
var mm := event as InputEventMouseMotion
var tx := int(floor(mm.position.x / cell_size))
var ty := int(floor(mm.position.y / cell_size))
if tx != _hover_tx or ty != _hover_ty:
_hover_tx = tx
_hover_ty = ty
queue_redraw()
if _mouse_down and (tx != _last_drag_tx or ty != _last_drag_ty):
var active_tool := core.tools.get_active_tool_id()
if active_tool in ["brush", "erase"]:
_handle_click(mm.position)
accept_event()
elif event is InputEventKey:
var key := event as InputEventKey
if key.pressed and not key.echo:
@@ -105,6 +160,8 @@ func _handle_key(key: InputEventKey) -> void:
func _handle_click(local_pos: Vector2) -> void:
var tx := int(floor(local_pos.x / cell_size))
var ty := int(floor(local_pos.y / cell_size))
_last_drag_tx = tx
_last_drag_ty = ty
match core.tools.get_active_tool_id():
"brush":
@@ -125,6 +182,7 @@ func _handle_click(local_pos: Vector2) -> void:
queue_redraw()
"select":
core.select_object(_object_at(tx, ty))
edit_applied.emit()
queue_redraw()
+70 -2
View File
@@ -34,6 +34,7 @@ var _viewport: EditorViewport
var _close_button: Button
var _save_button: Button
var _save_status_label: Label
var _instruction_label: Label
## Save-path state (world_persistence_impl.md §8.1/§8.2), set via
## `setup_persistence()`. `_save_service == null` means there is nothing to
@@ -66,11 +67,16 @@ func setup_persistence(save_service: ChunkSaveService, world_id: String, room_sl
_base_revision = base_revision
if _save_button != null:
_save_button.disabled = false
_save_button.tooltip_text = "Save chunk changes to backend (Ctrl+S)"
func _ready() -> void:
_build_ui()
_refresh_history_buttons()
if core.tools.get_active_tool_id() == "select" and core.model.payload.objects.is_empty():
core.tools.set_active_tool("brush")
_toolbar.set_active_tool("brush")
_update_instruction()
func _build_ui() -> void:
@@ -98,14 +104,21 @@ func _build_ui() -> void:
_save_button = Button.new()
_save_button.text = "Save (Ctrl+S)"
_save_button.disabled = (_save_service == null)
_save_button.tooltip_text = "Save changes to server (Ctrl+S)" if _save_service != null else "Saving not available in scratch mode"
_save_button.pressed.connect(_on_save_pressed)
top_bar.add_child(_save_button)
_close_button = Button.new()
_close_button.text = "Close (F2)"
_close_button.text = "Close (F2 / Esc)"
_close_button.tooltip_text = "Exit build mode and return to game"
_close_button.pressed.connect(_on_close_pressed)
top_bar.add_child(_close_button)
_instruction_label = Label.new()
_instruction_label.add_theme_font_size_override("font_size", 13)
_instruction_label.add_theme_color_override("font_color", Color(0.75, 0.92, 1.0))
root_vbox.add_child(_instruction_label)
_save_status_label = Label.new()
_save_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD
_save_status_label.visible = false
@@ -124,13 +137,15 @@ func _build_ui() -> void:
viewport_scroll.add_child(_viewport)
var sidebar := VBoxContainer.new()
sidebar.custom_minimum_size = Vector2(240, 0)
sidebar.custom_minimum_size = Vector2(260, 0)
body.add_child(sidebar)
_palette = PalettePanel.new()
_palette.size_flags_vertical = Control.SIZE_EXPAND_FILL
sidebar.add_child(_palette)
_inspector = InspectorPanel.new()
_inspector.size_flags_vertical = Control.SIZE_EXPAND_FILL
sidebar.add_child(_inspector)
_viewport.setup(core, 0)
@@ -166,16 +181,34 @@ func _unhandled_key_input(event: InputEvent) -> void:
core.delete_object(core.selection.get_selected())
_on_edit_applied()
get_viewport().set_input_as_handled()
elif key.keycode == KEY_B and not key.ctrl_pressed:
_on_tool_selected("brush")
_toolbar.set_active_tool("brush")
get_viewport().set_input_as_handled()
elif key.keycode == KEY_V and not key.ctrl_pressed:
_on_tool_selected("select")
_toolbar.set_active_tool("select")
get_viewport().set_input_as_handled()
elif key.keycode == KEY_P and not key.ctrl_pressed:
_on_tool_selected("object_place")
_toolbar.set_active_tool("object_place")
get_viewport().set_input_as_handled()
elif key.keycode == KEY_X and not key.ctrl_pressed:
_on_tool_selected("erase")
_toolbar.set_active_tool("erase")
get_viewport().set_input_as_handled()
func _on_tool_selected(tool_id: String) -> void:
core.tools.set_active_tool(tool_id)
_update_instruction()
func _on_palette_object_selected(catalog_id: String) -> void:
_viewport.pending_catalog_id = catalog_id
core.tools.set_active_tool("object_place")
_toolbar.set_active_tool("object_place")
_update_instruction()
func _on_inspector_delete_requested(object_id: String) -> void:
@@ -197,10 +230,45 @@ func _on_edit_applied() -> void:
_viewport.queue_redraw()
_inspector.show_object(core.selection.get_selected())
_refresh_history_buttons()
_update_instruction()
func _on_selection_changed(object_id: String) -> void:
_inspector.show_object(object_id)
_update_instruction()
func _update_instruction() -> void:
if _instruction_label == null:
return
var active_id := core.tools.get_active_tool_id()
match active_id:
"brush":
_instruction_label.text = "Tool: [ Brush ] — Left-click and drag on grid squares to paint ground tiles. (Keys 0-9 pick tile type)"
"object_place":
var name := _viewport.pending_catalog_id
if core.catalog != null:
var e := core.catalog.resolve(name)
if e.has("display_name"):
name = str(e["display_name"])
if name == "":
_instruction_label.text = "Tool: [ Place ] — Select an item in the Palette on the right, then click on the grid squares to place."
else:
_instruction_label.text = "Tool: [ Place: %s ] — Click any grid square to place '%s'." % [name, name]
"select":
if core.selection.has_selection():
var sel_id := core.selection.get_selected()
var obj := core.model.find_object(sel_id)
var dname := sel_id
if obj != null and obj.properties is Dictionary and obj.properties.has("display_name"):
dname = "%s (%s)" % [obj.properties["display_name"], sel_id]
_instruction_label.text = "Tool: [ Select ] — Selected '%s'. Edit in the Inspector on the right, or press Delete to remove." % dname
else:
_instruction_label.text = "Tool: [ Select ] — Click on a placed object on the grid to inspect it, or switch to Brush/Place to build."
"erase":
_instruction_label.text = "Tool: [ Erase ] — Left-click and drag on grid squares to erase painted tiles or delete objects."
_:
_instruction_label.text = "Tool: [ %s ]" % active_id
func _refresh_history_buttons() -> void:
+3
View File
@@ -37,11 +37,14 @@ var _fields_box: VBoxContainer
func _init() -> void:
name = "InspectorPanel"
size_flags_vertical = Control.SIZE_EXPAND_FILL
func _ready() -> void:
var header := Label.new()
header.text = "Inspector"
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)
_empty_label = Label.new()
+11
View File
@@ -14,15 +14,26 @@ var _ids: Array[String] = []
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, 180)
_list.item_selected.connect(_on_item_selected)
add_child(_list)