fix(editor): move build stamp to bottom, expand palette list, add hover feedback and instructions
checkpoint / checkpoint (push) Successful in 6m5s
checkpoint / checkpoint (push) Successful in 6m5s
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+25
-12
@@ -85,7 +85,11 @@ func _toggle_build_mode() -> void:
|
||||
|
||||
|
||||
func _open_build_mode() -> void:
|
||||
var model := ChunkEditorModel.create_empty(32, 32, str(Session.world_id), "main")
|
||||
var world_id_str := str(Session.world_id) if Session.world_id != null else "0"
|
||||
var room_slug := "main"
|
||||
var coord := Vector3i.ZERO
|
||||
|
||||
var model := ChunkEditorModel.create_empty(32, 32, world_id_str, room_slug, coord)
|
||||
var catalog := EditorObjectCatalog.new()
|
||||
var err := catalog.load_catalog()
|
||||
if err != OK:
|
||||
@@ -93,6 +97,13 @@ func _open_build_mode() -> void:
|
||||
var core := EditorCore.new(model, catalog)
|
||||
|
||||
_build_editor = InGameEditor.new(core)
|
||||
if int(world_id_str) > 0:
|
||||
var save_service := ChunkSaveService.new()
|
||||
save_service.world_id = world_id_str
|
||||
save_service.room_slug = room_slug
|
||||
add_child(save_service)
|
||||
_build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, 0)
|
||||
|
||||
_build_editor.closed.connect(_close_build_mode)
|
||||
add_child(_build_editor)
|
||||
|
||||
@@ -109,6 +120,9 @@ func _close_build_mode() -> void:
|
||||
if _build_editor != null and is_instance_valid(_build_editor):
|
||||
_build_editor.queue_free()
|
||||
_build_editor = null
|
||||
for child in get_children():
|
||||
if child is ChunkSaveService:
|
||||
child.queue_free()
|
||||
_player.set_physics_process(true)
|
||||
_player.set_process_unhandled_input(true)
|
||||
_player._refresh_mouse_mode()
|
||||
@@ -210,19 +224,18 @@ func _show_build_stamp() -> void:
|
||||
add_child(layer)
|
||||
var lbl := Label.new()
|
||||
lbl.text = "build %s" % _build_label()
|
||||
lbl.add_theme_color_override("font_color", Color(1, 1, 0.35))
|
||||
lbl.add_theme_color_override("font_color", Color(1, 1, 0.35, 0.85))
|
||||
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1))
|
||||
lbl.add_theme_constant_override("outline_size", 6)
|
||||
lbl.add_theme_font_size_override("font_size", 20)
|
||||
# Full-width strip pinned to the TOP-left. (A bare preset + .position on a
|
||||
# zero-size Label collapses the rect to negative width and nothing draws --
|
||||
# which is why this never showed; explicit offsets fix it.)
|
||||
lbl.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
lbl.add_theme_constant_override("outline_size", 4)
|
||||
lbl.add_theme_font_size_override("font_size", 14)
|
||||
# Pinned to the BOTTOM-right so it never blocks top menu or editor toolbars.
|
||||
lbl.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
lbl.offset_left = 12
|
||||
lbl.offset_right = -12
|
||||
lbl.offset_top = 8
|
||||
lbl.offset_bottom = 40
|
||||
lbl.offset_right = -16
|
||||
lbl.offset_top = -32
|
||||
lbl.offset_bottom = -8
|
||||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
layer.add_child(lbl)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user