From 5336f100fda1f8af21566e29ae49d4d3a0c68ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Oberle?= Date: Mon, 3 Aug 2026 16:54:00 +0200 Subject: [PATCH] (feat): add custom key bindings in settings Add the ability to remap the keys for moving the grid and rotating/tilting/flipping/resetting the tile The keys default to the ones that were hardcoded, and save between reloads --- .../TileMapLayer3D/TileMapLayer3D_plugin.gd | 115 +++++++------ .../core/global/global_const.gd | 12 ++ .../tilemap_layer_settings.gd | 2 + addons/TileMapLayer3D/ui/tileset_panel.gd | 157 +++++++++++++++++- addons/TileMapLayer3D/ui/tileset_panel.tscn | 130 +++++++++++++++ 5 files changed, 363 insertions(+), 53 deletions(-) diff --git a/addons/TileMapLayer3D/TileMapLayer3D_plugin.gd b/addons/TileMapLayer3D/TileMapLayer3D_plugin.gd index ec77ae7..4fdad09 100644 --- a/addons/TileMapLayer3D/TileMapLayer3D_plugin.gd +++ b/addons/TileMapLayer3D/TileMapLayer3D_plugin.gd @@ -450,40 +450,38 @@ func _handle_mesh_rotations(event: InputEventKey, camera: Camera3D) -> int: _on_vertex_delete_requested() return AFTER_GUI_INPUT_STOP return AFTER_GUI_INPUT_PASS + + if _is_key_matching_action(event,"tilemaplayer3d_rotate_left"): + placement_manager.current_mesh_rotation = (placement_manager.current_mesh_rotation - 1) % GlobalConstants.MAX_SPIN_ROTATION_STEPS + if placement_manager.current_mesh_rotation < 0: + placement_manager.current_mesh_rotation += GlobalConstants.MAX_SPIN_ROTATION_STEPS + needs_update = true + + if _is_key_matching_action(event,"tilemaplayer3d_rotate_right"): + placement_manager.current_mesh_rotation = (placement_manager.current_mesh_rotation + 1) % GlobalConstants.MAX_SPIN_ROTATION_STEPS + needs_update = true + + if _is_key_matching_action(event,"tilemaplayer3d_flip"): + placement_manager.is_current_face_flipped = not placement_manager.is_current_face_flipped + needs_update = true + + if _is_key_matching_action(event,"tilemaplayer3d_tilt"): + if event.shift_pressed: + GlobalPlaneDetector.cycle_tilt_backward() + else: + GlobalPlaneDetector.cycle_tilt_forward() + needs_update = true - match event.physical_keycode: - KEY_Q: - placement_manager.current_mesh_rotation = (placement_manager.current_mesh_rotation - 1) % GlobalConstants.MAX_SPIN_ROTATION_STEPS - if placement_manager.current_mesh_rotation < 0: - placement_manager.current_mesh_rotation += GlobalConstants.MAX_SPIN_ROTATION_STEPS - needs_update = true - - KEY_E: - placement_manager.current_mesh_rotation = (placement_manager.current_mesh_rotation + 1) % GlobalConstants.MAX_SPIN_ROTATION_STEPS - needs_update = true - - KEY_F: - placement_manager.is_current_face_flipped = not placement_manager.is_current_face_flipped - needs_update = true - - KEY_R: - if event.shift_pressed: - GlobalPlaneDetector.cycle_tilt_backward() - else: - GlobalPlaneDetector.cycle_tilt_forward() - needs_update = true - - var should_be_flipped: bool = GlobalPlaneDetector.determine_rotation_flip_for_plane(GlobalPlaneDetector.current_plane_6d) - - placement_manager.is_current_face_flipped = should_be_flipped - + var should_be_flipped: bool = GlobalPlaneDetector.determine_rotation_flip_for_plane(GlobalPlaneDetector.current_plane_6d) - KEY_T: - GlobalPlaneDetector.reset_to_flat() - placement_manager.current_mesh_rotation = 0 - needs_update = true - var default_flip: bool = GlobalPlaneDetector.determine_auto_flip_for_plane(GlobalPlaneDetector.current_plane_6d) - placement_manager.is_current_face_flipped = default_flip + placement_manager.is_current_face_flipped = should_be_flipped + + if _is_key_matching_action(event,"tilemaplayer3d_reset"): + GlobalPlaneDetector.reset_to_flat() + placement_manager.current_mesh_rotation = 0 + needs_update = true + var default_flip: bool = GlobalPlaneDetector.determine_auto_flip_for_plane(GlobalPlaneDetector.current_plane_6d) + placement_manager.is_current_face_flipped = default_flip if needs_update: if current_tile_map3d and current_tile_map3d.settings: @@ -513,26 +511,28 @@ func _handle_cursor3d_movement(event: InputEventKey, camera: Camera3D) -> int: var handled: bool = false var move_vector: Vector3 = Vector3.ZERO var basis: Basis = camera.global_transform.basis - - match event.physical_keycode: - KEY_W: - if shift_pressed: - move_vector = GlobalUtil._get_snapped_cardinal_vector(basis.y) - else: - move_vector = GlobalUtil._get_snapped_cardinal_vector(-basis.z) - handled = true - KEY_S: - if shift_pressed: + + + + if _is_key_matching_action(event,"tilemaplayer3d_up"): + if shift_pressed: + move_vector = GlobalUtil._get_snapped_cardinal_vector(basis.y) + else: + move_vector = GlobalUtil._get_snapped_cardinal_vector(-basis.z) + handled = true + + if _is_key_matching_action(event,"tilemaplayer3d_down"): + if shift_pressed: move_vector = GlobalUtil._get_snapped_cardinal_vector(-basis.y) - else: - move_vector = GlobalUtil._get_snapped_cardinal_vector(basis.z) - handled = true - KEY_A: - move_vector = GlobalUtil._get_snapped_cardinal_vector(-basis.x) - handled = true - KEY_D: - move_vector = GlobalUtil._get_snapped_cardinal_vector(basis.x) - handled = true + else: + move_vector = GlobalUtil._get_snapped_cardinal_vector(basis.z) + handled = true + if _is_key_matching_action(event,"tilemaplayer3d_left"): + move_vector = GlobalUtil._get_snapped_cardinal_vector(-basis.x) + handled = true + if _is_key_matching_action(event,"tilemaplayer3d_right"): + move_vector = GlobalUtil._get_snapped_cardinal_vector(basis.x) + handled = true if handled: if move_vector.length_squared() > 0.0: @@ -2538,3 +2538,16 @@ func _delete_selected_tiles() -> void: current_tile_map3d.smart_selected_tiles.clear() current_tile_map3d.clear_highlights() current_tile_map3d.update_gizmos() + +static func _is_key_matching_action(event: InputEventKey, action_name: String) -> bool: + var action_events := InputMap.action_get_events(action_name) + + for action_event in action_events: + if action_event is InputEventKey: + if event.physical_keycode != KEY_NONE and action_event.physical_keycode != KEY_NONE: + if event.physical_keycode == action_event.physical_keycode: + return true + elif event.keycode == action_event.keycode: + return true + + return false diff --git a/addons/TileMapLayer3D/core/global/global_const.gd b/addons/TileMapLayer3D/core/global/global_const.gd index dba590b..7a72e51 100644 --- a/addons/TileMapLayer3D/core/global/global_const.gd +++ b/addons/TileMapLayer3D/core/global/global_const.gd @@ -683,3 +683,15 @@ const VERTEX_HANDLE_SIZE: float = 0.05 const VERTEX_WIREFRAME_COLOR: Color = Color(1.0, 0.4, 0.4, 0.8) const VERTEX_TILE_WARNING_THRESHOLD: int = 100 + +const DEFAULT_KEY_BINDINGS = { + "tilemaplayer3d_up": KEY_W, + "tilemaplayer3d_down": KEY_S, + "tilemaplayer3d_left": KEY_A, + "tilemaplayer3d_right": KEY_D, + "tilemaplayer3d_rotate_right": KEY_E, + "tilemaplayer3d_rotate_left": KEY_Q, + "tilemaplayer3d_flip":KEY_F, + "tilemaplayer3d_tilt":KEY_R, + "tilemaplayer3d_reset":KEY_T +} diff --git a/addons/TileMapLayer3D/core/resource_definition/tilemap_layer_settings.gd b/addons/TileMapLayer3D/core/resource_definition/tilemap_layer_settings.gd index 740fc96..53a28d1 100644 --- a/addons/TileMapLayer3D/core/resource_definition/tilemap_layer_settings.gd +++ b/addons/TileMapLayer3D/core/resource_definition/tilemap_layer_settings.gd @@ -382,6 +382,8 @@ extends Resource arch_radius_ratio = clampf(value, GlobalConstants.ARCH_MIN_RADIUS_RATIO, GlobalConstants.ARCH_MAX_RADIUS_RATIO) emit_changed() +@export var keybindings: Dictionary = {} + static func create_default() -> TileMapLayerSettings: var settings: TileMapLayerSettings = TileMapLayerSettings.new() settings._settings_format_version = 1 diff --git a/addons/TileMapLayer3D/ui/tileset_panel.gd b/addons/TileMapLayer3D/ui/tileset_panel.gd index 72ff679..55ee114 100644 --- a/addons/TileMapLayer3D/ui/tileset_panel.gd +++ b/addons/TileMapLayer3D/ui/tileset_panel.gd @@ -64,6 +64,10 @@ extends PanelContainer @onready var remove_terrain_button: Button = %RemoveTerrainButton @onready var terrain_name_input: LineEdit = %TerrainNameInput +@onready var up_key_button: Button = %UpKeyButton +@onready var down_key_button: Button = %DownKeyButton +@onready var left_key_button: Button = %LeftKeyButton +@onready var right_key_button: Button = %RightKeyButton signal tile_selected(uv_rect: Rect2) signal multi_tile_selected(uv_rects: Array[Rect2], anchor_index: int) @@ -100,13 +104,28 @@ var _current_zoom: float = GlobalConstants.TILESET_DEFAULT_ZOOM var _is_updating_zoom: bool = false var _previous_texture: Texture2D = null +@onready var _keybindings_map: Dictionary = { + "tilemaplayer3d_up": %UpKeyButton, + "tilemaplayer3d_down": %DownKeyButton, + "tilemaplayer3d_left": %LeftKeyButton, + "tilemaplayer3d_right": %RightKeyButton, + "tilemaplayer3d_rotate_right": %RotateRightKeyButton, + "tilemaplayer3d_rotate_left": %RotateLeftKeyButton, + "tilemaplayer3d_flip":%FlipFaceKeyButton, + "tilemaplayer3d_tilt":%TiltFaceKeyButton, + "tilemaplayer3d_reset":%ResetFaceKeyButton +} +var _is_rebinding: bool = false +var _active_action: String = "" +var _rebinding_button: Button = null + var _current_tiling_mode: GlobalConstants.MainAppMode = GlobalConstants.MainAppMode.MANUAL var _selected_tiles: Array[Rect2] = [] func _ready() -> void: - + _ensure_keybinding_actions_exist() _connect_signals() manual_tiling_tab.show() set_tiling_mode_from_external(GlobalConstants.MainAppMode.MANUAL) @@ -241,7 +260,14 @@ func _connect_signals() -> void: if load_tile_set_button and not load_tile_set_button.pressed.is_connected(_on_load_tileset_file_pressed): load_tile_set_button.pressed.connect(_on_load_tileset_file_pressed) - + + for action_name in _keybindings_map: + var button : Button = _keybindings_map[action_name] + + if button and not button.pressed.is_connected(_on_keybinding_button_pressed): + button.pressed.connect(_on_keybinding_button_pressed.bind(action_name)) + _update_keybinding_button_text(action_name) + ## Returns current TileSet tile size (used by AutotileTab for TileSet creation). ## The picker grid uses `_tile_size` directly and is intentionally separate. @@ -444,6 +470,22 @@ func _load_settings_to_ui(settings: TileMapLayerSettings) -> void: grid_size_changed.emit(settings.grid_size) enabled_arched_tiles_checkbox.button_pressed = settings.enable_arched_tiles if _on_enabled_arched_tiles_toggled else false + + if settings and settings.keybindings: + for action_name in _keybindings_map: + var event: InputEventKey + if settings.keybindings.has(action_name): + event = settings.keybindings[action_name] + else: + event = _get_key_event(GlobalConstants.DEFAULT_KEY_BINDINGS[action_name]) + + if event: + if not InputMap.has_action(action_name): + InputMap.add_action(action_name) + + InputMap.action_erase_events(action_name) + InputMap.action_add_event(action_name, event) + _update_keybinding_button_text(action_name) _is_loading_from_node = false @@ -511,6 +553,13 @@ func _save_ui_to_settings() -> void: if _on_enabled_arched_tiles_toggled: current_tilemap3d_node.settings.enable_arched_tiles = enabled_arched_tiles_checkbox.button_pressed + + for action_name in _keybindings_map: + if InputMap.has_action(action_name): + var events := InputMap.action_get_events(action_name) + if events.size() > 0: + current_tilemap3d_node.settings.keybindings[action_name] = events[0] + func _clear_ui() -> void: @@ -1119,3 +1168,107 @@ func _set_scroll_position(scroll_pos: Vector2) -> void: scroll_container.scroll_horizontal = int(scroll_pos.x) scroll_container.scroll_vertical = int(scroll_pos.y) + +func _unhandled_input(event: InputEvent) -> void: + if not _is_rebinding: + return + + if event is InputEventKey and event.pressed: + if event.keycode == KEY_ESCAPE: + _cancel_rebinding() + + if event is InputEventKey and event.pressed and not event.is_echo(): + InputMap.action_erase_events(_active_action) + InputMap.action_add_event(_active_action, event) + + _update_keybinding_button_text(_active_action) + _is_rebinding = false + _active_action = "" + get_viewport().set_input_as_handled() + + _save_ui_to_settings() + else: + _is_rebinding=false + _update_keybinding_button_text(_active_action) + _active_action = "" + get_viewport().set_input_as_handled() + +func _input(event: InputEvent) -> void: + # catch key presses to cancel the rebinding + if not _is_rebinding: + return + + if event is InputEventKey and event.is_pressed() and not event.is_echo(): + _complete_rebinding(event) + get_viewport().set_input_as_handled() + return + + if event is InputEventMouseButton and event.is_pressed(): + if _rebinding_button: + var click_pos: Vector2 = event.position + var button_rect: Rect2 = _rebinding_button.get_global_rect() + + if not button_rect.has_point(click_pos): + _cancel_rebinding() + +static func _get_key_event(keycode): + var ev:InputEventKey=InputEventKey.new() + ev.keycode=keycode + ev.physical_keycode=keycode + return ev + +func _ensure_keybinding_actions_exist() -> void: + for action_name in _keybindings_map.keys(): + if not InputMap.has_action(action_name): + InputMap.add_action(action_name) + var default_event : InputEventKey= _get_key_event(GlobalConstants.DEFAULT_KEY_BINDINGS[action_name]) + InputMap.action_add_event(action_name, default_event) + + +func _on_keybinding_button_pressed(action_name: String) -> void: + if _is_rebinding: + _cancel_rebinding() + return + + _is_rebinding = true + _active_action = action_name + _rebinding_button = _keybindings_map[action_name] + _rebinding_button.text = "New key..." + + +func _update_keybinding_button_text(action_name) -> void: + + var btn: Button = _keybindings_map.get(action_name) + if not btn: + return + + if InputMap.has_action(action_name): + var events := InputMap.action_get_events(action_name) + if events.size() > 0: + btn.text = events[0].as_text().to_upper() + return + btn.text = "NONE" + + +func _complete_rebinding(new_event: InputEventKey) -> void: + if not InputMap.has_action(_active_action): + InputMap.add_action(_active_action) + + InputMap.action_erase_events(_active_action) + InputMap.action_add_event(_active_action, new_event) + + _update_keybinding_button_text(_active_action) + _save_ui_to_settings() + _reset_rebind_state() + + +func _cancel_rebinding() -> void: + if _active_action != "": + _update_keybinding_button_text(_active_action) + _reset_rebind_state() + + +func _reset_rebind_state() -> void: + _is_rebinding = false + _active_action = "" + _rebinding_button = null diff --git a/addons/TileMapLayer3D/ui/tileset_panel.tscn b/addons/TileMapLayer3D/ui/tileset_panel.tscn index fef75ae..00250fb 100644 --- a/addons/TileMapLayer3D/ui/tileset_panel.tscn +++ b/addons/TileMapLayer3D/ui/tileset_panel.tscn @@ -953,6 +953,136 @@ value = 0.5 tick_count = 5 ticks_on_borders = true +[node name="Key Bindings" type="VBoxContainer" parent="TabContainer/Settings" unique_id=376977859] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Key bindings" type="Label" parent="TabContainer/Settings/Key Bindings" unique_id=1340359717] +layout_mode = 2 +text = "Key bindings:" + +[node name="PanelContainer" type="PanelContainer" parent="TabContainer/Settings/Key Bindings" unique_id=897527544] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_i0g27") + +[node name="MarginContainer" type="MarginContainer" parent="TabContainer/Settings/Key Bindings/PanelContainer" unique_id=790636542] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/margin_left = 0 +theme_override_constants/margin_top = 0 +theme_override_constants/margin_right = 0 +theme_override_constants/margin_bottom = 0 + +[node name="ScrollContainer" type="ScrollContainer" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer" unique_id=1784020235] +custom_minimum_size = Vector2(0, 100) +layout_mode = 2 +horizontal_scroll_mode = 0 + +[node name="KeyBindingGrid2" type="GridContainer" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer" unique_id=779118795] +layout_mode = 2 +columns = 2 + +[node name="UpLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=431717185] +layout_mode = 2 +text = "Up key: " + +[node name="UpKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1352163241] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "W" + +[node name="DownLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1061994784] +layout_mode = 2 +text = "Down key: " + +[node name="DownKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=687127383] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "S" + +[node name="LeftLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=24821964] +layout_mode = 2 +text = "Left key: " + +[node name="LeftKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1820346410] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "A" + +[node name="RightLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=424403063] +layout_mode = 2 +text = "Right key: " + +[node name="RightKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=2105462010] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "D" + +[node name="RotateRightLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=614890815] +layout_mode = 2 +text = "Rotate right key: " + +[node name="RotateRightKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=538293353] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "E" + +[node name="RotateLeftLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1713650178] +layout_mode = 2 +text = "Rotate left key: " + +[node name="RotateLeftKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1935979155] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "Q" + +[node name="FlipLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1235837500] +layout_mode = 2 +text = "Flip face key: " + +[node name="FlipFaceKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=511550409] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "F" + +[node name="TiltLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1143181429] +layout_mode = 2 +text = "Tilt face key: " + +[node name="TiltFaceKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=1789733331] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "R" + +[node name="ResetLabel" type="Label" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=176259419] +layout_mode = 2 +text = "Reset face key: " + +[node name="ResetFaceKeyButton" type="Button" parent="TabContainer/Settings/Key Bindings/PanelContainer/MarginContainer/ScrollContainer/KeyBindingGrid2" unique_id=69130202] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 0) +layout_mode = 2 +size_flags_vertical = 0 +text = "T" + [node name="LoadTextureDialog" type="FileDialog" parent="." unique_id=1045463458] unique_name_in_owner = true oversampling_override = 1.0