Files
Super-Mario-Bros.-Remastere…/addons/better-terrain/editor/Dock.gd
2025-09-13 16:30:32 +01:00

940 lines
33 KiB
GDScript
Executable File

@tool
extends Control
signal update_overlay
signal force_show_terrains
# The maximum individual tiles the overlay will draw before shortcutting the display
# To prevent editor lag when drawing large rectangles or filling large areas
const MAX_CANVAS_RENDER_TILES = 1500
const TERRAIN_PROPERTIES_SCENE := preload("res://addons/better-terrain/editor/TerrainProperties.tscn")
const TERRAIN_ENTRY_SCENE := preload("res://addons/better-terrain/editor/TerrainEntry.tscn")
const MIN_ZOOM_SETTING := "editor/better_terrain/min_zoom_amount"
const MAX_ZOOM_SETTING := "editor/better_terrain/max_zoom_amount"
# Buttons
@onready var draw_button: Button = $VBox/Toolbar/Draw
@onready var line_button: Button = $VBox/Toolbar/Line
@onready var rectangle_button: Button = $VBox/Toolbar/Rectangle
@onready var fill_button: Button = $VBox/Toolbar/Fill
@onready var replace_button: Button = $VBox/Toolbar/Replace
@onready var paint_type: Button = $VBox/Toolbar/PaintType
@onready var paint_terrain: Button = $VBox/Toolbar/PaintTerrain
@onready var select_tiles: Button = $VBox/Toolbar/SelectTiles
@onready var paint_symmetry: Button = $VBox/Toolbar/PaintSymmetry
@onready var symmetry_options: OptionButton = $VBox/Toolbar/SymmetryOptions
@onready var shuffle_random: Button = $VBox/Toolbar/ShuffleRandom
@onready var zoom_slider_container: VBoxContainer = $VBox/Toolbar/ZoomContainer
@onready var source_selector: MenuBar = $VBox/Toolbar/Sources
@onready var source_selector_popup: PopupMenu = $VBox/Toolbar/Sources/Sources
@onready var clean_button: Button = $VBox/Toolbar/Clean
@onready var layer_up: Button = $VBox/Toolbar/LayerUp
@onready var layer_down: Button = $VBox/Toolbar/LayerDown
@onready var layer_highlight: Button = $VBox/Toolbar/LayerHighlight
@onready var layer_grid: Button = $VBox/Toolbar/LayerGrid
@onready var grid_mode_button: Button = $VBox/HSplit/Terrains/LowerToolbar/GridMode
@onready var quick_mode_button: Button = $VBox/HSplit/Terrains/LowerToolbar/QuickMode
@onready var edit_tool_buttons: HBoxContainer = $VBox/HSplit/Terrains/LowerToolbar/EditTools
@onready var add_terrain_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/AddTerrain
@onready var edit_terrain_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/EditTerrain
@onready var pick_icon_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/PickIcon
@onready var move_up_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/MoveUp
@onready var move_down_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/MoveDown
@onready var remove_terrain_button: Button = $VBox/HSplit/Terrains/LowerToolbar/EditTools/RemoveTerrain
@onready var scroll_container: ScrollContainer = $VBox/HSplit/Terrains/Panel/ScrollContainer
@onready var terrain_list: HFlowContainer = $VBox/HSplit/Terrains/Panel/ScrollContainer/TerrainList
@onready var tile_view: Control = $VBox/HSplit/Panel/ScrollArea/TileView
var selected_entry := -2
var tilemap : TileMapLayer
var tileset : TileSet
var undo_manager : EditorUndoRedoManager
var terrain_undo
var draw_overlay := false
var initial_click : Vector2i
var prev_position : Vector2i
var current_position : Vector2i
var tileset_dirty := false
var zoom_slider : HSlider
enum PaintMode {
NO_PAINT,
PAINT,
ERASE
}
enum PaintAction {
NO_ACTION,
LINE,
RECT
}
enum SourceSelectors {
ALL = 1000000,
NONE = 1000001,
}
var paint_mode := PaintMode.NO_PAINT
var paint_action := PaintAction.NO_ACTION
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
draw_button.icon = get_theme_icon("Edit", "EditorIcons")
line_button.icon = get_theme_icon("Line", "EditorIcons")
rectangle_button.icon = get_theme_icon("Rectangle", "EditorIcons")
fill_button.icon = get_theme_icon("Bucket", "EditorIcons")
select_tiles.icon = get_theme_icon("ToolSelect", "EditorIcons")
add_terrain_button.icon = get_theme_icon("Add", "EditorIcons")
edit_terrain_button.icon = get_theme_icon("Tools", "EditorIcons")
pick_icon_button.icon = get_theme_icon("ColorPick", "EditorIcons")
move_up_button.icon = get_theme_icon("ArrowUp", "EditorIcons")
move_down_button.icon = get_theme_icon("ArrowDown", "EditorIcons")
remove_terrain_button.icon = get_theme_icon("Remove", "EditorIcons")
grid_mode_button.icon = get_theme_icon("FileThumbnail", "EditorIcons")
quick_mode_button.icon = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
layer_up.icon = get_theme_icon("MoveUp", "EditorIcons")
layer_down.icon = get_theme_icon("MoveDown", "EditorIcons")
layer_highlight.icon = get_theme_icon("TileMapHighlightSelected", "EditorIcons")
layer_grid.icon = get_theme_icon("Grid", "EditorIcons")
select_tiles.button_group.pressed.connect(_on_bit_button_pressed)
terrain_undo = load("res://addons/better-terrain/editor/TerrainUndo.gd").new()
add_child(terrain_undo)
tile_view.undo_manager = undo_manager
tile_view.terrain_undo = terrain_undo
tile_view.paste_occurred.connect(_on_paste_occurred)
tile_view.change_zoom_level.connect(_on_change_zoom_level)
tile_view.terrain_updated.connect(_on_terrain_updated)
# Zoom slider is manipulated by settings, make it at runtime
zoom_slider = HSlider.new()
zoom_slider.custom_minimum_size = Vector2(100, 0)
zoom_slider.value_changed.connect(tile_view._on_zoom_value_changed)
zoom_slider_container.add_child(zoom_slider)
# Init settings if needed
if !ProjectSettings.has_setting(MIN_ZOOM_SETTING):
ProjectSettings.set(MIN_ZOOM_SETTING, 1.0)
ProjectSettings.add_property_info({
"name": MIN_ZOOM_SETTING,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0.1,1.0,0.1"
})
ProjectSettings.set_initial_value(MIN_ZOOM_SETTING, 1.0)
ProjectSettings.set_as_basic(MIN_ZOOM_SETTING, true)
if !ProjectSettings.has_setting(MAX_ZOOM_SETTING):
ProjectSettings.set(MAX_ZOOM_SETTING, 8.0)
ProjectSettings.add_property_info({
"name": MAX_ZOOM_SETTING,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "2.0,32.0,1.0"
})
ProjectSettings.set_initial_value(MAX_ZOOM_SETTING, 8.0)
ProjectSettings.set_as_basic(MAX_ZOOM_SETTING, true)
ProjectSettings.set_order(MAX_ZOOM_SETTING, ProjectSettings.get_order(MIN_ZOOM_SETTING) + 1)
ProjectSettings.settings_changed.connect(_on_adjust_settings)
_on_adjust_settings()
zoom_slider.value = 1.0
func _process(delta):
scroll_container.scroll_horizontal = 0
func _on_adjust_settings():
zoom_slider.min_value = ProjectSettings.get_setting(MIN_ZOOM_SETTING, 1.0)
zoom_slider.max_value = ProjectSettings.get_setting(MAX_ZOOM_SETTING, 8.0)
zoom_slider.step = (zoom_slider.max_value - zoom_slider.min_value) / 100.0
func _get_fill_cells(target: Vector2i) -> Array:
var pick := BetterTerrain.get_cell(tilemap, target)
var bounds := tilemap.get_used_rect()
var neighbors = BetterTerrain.data.cells_adjacent_for_fill(tileset)
# No sets yet, so use a dictionary
var checked := {}
var pending := [target]
var goal := []
while !pending.is_empty():
var p = pending.pop_front()
if checked.has(p):
continue
checked[p] = true
if !bounds.has_point(p) or BetterTerrain.get_cell(tilemap, p) != pick:
continue
goal.append(p)
pending.append_array(BetterTerrain.data.neighboring_coords(tilemap, p, neighbors))
return goal
func tiles_about_to_change() -> void:
if tileset and tileset.changed.is_connected(queue_tiles_changed):
tileset.changed.disconnect(queue_tiles_changed)
func tiles_changed() -> void:
# ensure up to date
BetterTerrain._update_terrain_data(tileset)
# clear terrains
for c in terrain_list.get_children():
terrain_list.remove_child(c)
c.queue_free()
# load terrains from tileset
var terrain_count := BetterTerrain.terrain_count(tileset)
var item_count = terrain_count + 1
for i in terrain_count:
var terrain := BetterTerrain.get_terrain(tileset, i)
if i >= terrain_list.get_child_count():
add_terrain_entry(terrain, i)
if item_count > terrain_list.get_child_count():
var terrain := BetterTerrain.get_terrain(tileset, BetterTerrain.TileCategory.EMPTY)
if terrain.valid:
add_terrain_entry(terrain, item_count - 1)
while item_count < terrain_list.get_child_count():
var child = terrain_list.get_child(terrain_list.get_child_count() - 1)
terrain_list.remove_child(child)
child.free()
source_selector_popup.clear()
source_selector_popup.add_item("All", SourceSelectors.ALL)
source_selector_popup.add_item("None", SourceSelectors.NONE)
var source_count = tileset.get_source_count() if tileset else 0
for s in source_count:
var source_id = tileset.get_source_id(s)
var source := tileset.get_source(source_id)
if !(source is TileSetAtlasSource):
continue
var name := source.resource_name
if name.is_empty():
var texture := (source as TileSetAtlasSource).texture
var texture_name := texture.resource_name if texture else ""
if !texture_name.is_empty():
name = texture_name
else:
var texture_path := texture.resource_path if texture else ""
if !texture_path.is_empty():
name = texture_path.get_file()
if !name.is_empty():
name += " "
name += " (ID: %d)" % source_id
source_selector_popup.add_check_item(name, source_id)
source_selector_popup.set_item_checked(source_selector_popup.get_item_index(source_id), true)
source_selector.visible = source_selector_popup.item_count > 3 # All, None and more than one source
update_tile_view_paint()
tile_view.refresh_tileset(tileset)
if tileset and !tileset.changed.is_connected(queue_tiles_changed):
tileset.changed.connect(queue_tiles_changed)
clean_button.visible = BetterTerrain._has_invalid_peering_types(tileset)
tileset_dirty = false
_on_grid_mode_pressed()
_on_quick_mode_pressed()
func about_to_be_visible(visible: bool) -> void:
if !visible:
return
if tileset != tilemap.tile_set:
tiles_about_to_change()
tileset = tilemap.tile_set
tiles_changed()
var settings := EditorInterface.get_editor_settings()
layer_highlight.set_pressed_no_signal(settings.get_setting("editors/tiles_editor/highlight_selected_layer"))
layer_grid.set_pressed_no_signal(settings.get_setting("editors/tiles_editor/display_grid"))
func queue_tiles_changed() -> void:
# Bring terrain data up to date with complex tileset changes
if !tileset or tileset_dirty:
return
tileset_dirty = true
tiles_changed.call_deferred()
func _on_entry_select(index:int):
selected_entry = index
if selected_entry >= BetterTerrain.terrain_count(tileset):
selected_entry = BetterTerrain.TileCategory.EMPTY
for i in range(terrain_list.get_child_count()):
if i != index:
terrain_list.get_child(i).set_selected(false)
update_tile_view_paint()
func _on_clean_pressed() -> void:
var confirmed := [false]
var popup := ConfirmationDialog.new()
popup.dialog_text = tr("Tile set changes have caused terrain to become invalid. Remove invalid terrain data?")
popup.dialog_hide_on_ok = false
popup.confirmed.connect(func():
confirmed[0] = true
popup.hide()
)
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
popup.queue_free()
if confirmed[0]:
undo_manager.create_action("Clean invalid terrain peering data", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(BetterTerrain, &"_clear_invalid_peering_types", tileset)
undo_manager.add_do_method(self, &"tiles_changed")
terrain_undo.create_peering_restore_point(undo_manager, tileset)
undo_manager.add_undo_method(self, &"tiles_changed")
undo_manager.commit_action()
func _on_grid_mode_pressed() -> void:
for c in terrain_list.get_children():
c.grid_mode = grid_mode_button.button_pressed
c.update_style()
func _on_quick_mode_pressed() -> void:
edit_tool_buttons.visible = !quick_mode_button.button_pressed
for c in terrain_list.get_children():
c.visible = !quick_mode_button.button_pressed or c.terrain.type in [BetterTerrain.TerrainType.MATCH_TILES, BetterTerrain.TerrainType.MATCH_VERTICES]
func update_tile_view_paint() -> void:
tile_view.paint = selected_entry
tile_view.queue_redraw()
var editable = tile_view.paint != BetterTerrain.TileCategory.EMPTY
edit_terrain_button.disabled = !editable
move_up_button.disabled = !editable or tile_view.paint == 0
move_down_button.disabled = !editable or tile_view.paint == BetterTerrain.terrain_count(tileset) - 1
remove_terrain_button.disabled = !editable
pick_icon_button.disabled = !editable
func _on_add_terrain_pressed() -> void:
if !tileset:
return
var popup := TERRAIN_PROPERTIES_SCENE.instantiate()
popup.set_category_data(BetterTerrain.get_terrain_categories(tileset))
popup.terrain_name = "New terrain"
popup.terrain_color = Color.from_hsv(randf(), 0.3 + 0.7 * randf(), 0.6 + 0.4 * randf())
popup.terrain_icon = ""
popup.terrain_type = 0
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
if popup.accepted:
undo_manager.create_action("Add terrain type", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_add_terrain", popup.terrain_name, popup.terrain_color, popup.terrain_type, popup.terrain_categories, {path = popup.terrain_icon})
undo_manager.add_undo_method(self, &"perform_remove_terrain", terrain_list.get_child_count() - 1)
undo_manager.commit_action()
popup.queue_free()
func _on_edit_terrain_pressed() -> void:
if !tileset:
return
if selected_entry < 0:
return
var t := BetterTerrain.get_terrain(tileset, selected_entry)
var categories = BetterTerrain.get_terrain_categories(tileset)
categories = categories.filter(func(x): return x.id != selected_entry)
var popup := TERRAIN_PROPERTIES_SCENE.instantiate()
popup.set_category_data(categories)
t.icon = t.icon.duplicate()
popup.terrain_name = t.name
popup.terrain_type = t.type
popup.terrain_color = t.color
if t.has("icon") and t.icon.has("path"):
popup.terrain_icon = t.icon.path
popup.terrain_categories = t.categories
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
if popup.accepted:
undo_manager.create_action("Edit terrain details", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_edit_terrain", selected_entry, popup.terrain_name, popup.terrain_color, popup.terrain_type, popup.terrain_categories, {path = popup.terrain_icon})
undo_manager.add_undo_method(self, &"perform_edit_terrain", selected_entry, t.name, t.color, t.type, t.categories, t.icon)
if t.type != popup.terrain_type:
terrain_undo.create_terrain_type_restore_point(undo_manager, tileset)
terrain_undo.create_peering_restore_point_specific(undo_manager, tileset, selected_entry)
undo_manager.commit_action()
popup.queue_free()
func _on_pick_icon_pressed():
if selected_entry < 0:
return
tile_view.pick_icon_terrain = selected_entry
func _on_pick_icon_focus_exited():
tile_view.pick_icon_terrain_cancel = true
pick_icon_button.button_pressed = false
func _on_move_pressed(down: bool) -> void:
if !tileset:
return
if selected_entry < 0:
return
var index1 = selected_entry
var index2 = index1 + (1 if down else -1)
if index2 < 0 or index2 >= terrain_list.get_child_count():
return
undo_manager.create_action("Reorder terrains", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_swap_terrain", index1, index2)
undo_manager.add_undo_method(self, &"perform_swap_terrain", index1, index2)
undo_manager.commit_action()
func _on_remove_terrain_pressed() -> void:
if !tileset:
return
if selected_entry < 0:
return
# store confirmation in array to pass by ref
var t := BetterTerrain.get_terrain(tileset, selected_entry)
var confirmed := [false]
var popup := ConfirmationDialog.new()
popup.dialog_text = tr("Are you sure you want to remove {0}?").format([t.name])
popup.dialog_hide_on_ok = false
popup.confirmed.connect(func():
confirmed[0] = true
popup.hide()
)
EditorInterface.popup_dialog_centered(popup)
await popup.visibility_changed
popup.queue_free()
if confirmed[0]:
undo_manager.create_action("Remove terrain type", UndoRedo.MERGE_DISABLE, tileset)
undo_manager.add_do_method(self, &"perform_remove_terrain", selected_entry)
undo_manager.add_undo_method(self, &"perform_add_terrain", t.name, t.color, t.type, t.categories, t.icon)
for n in range(terrain_list.get_child_count() - 2, selected_entry, -1):
undo_manager.add_undo_method(self, &"perform_swap_terrain", n, n - 1)
if t.type == BetterTerrain.TerrainType.CATEGORY:
terrain_undo.create_terrain_type_restore_point(undo_manager, tileset)
terrain_undo.create_peering_restore_point_specific(undo_manager, tileset, selected_entry)
undo_manager.commit_action()
func add_terrain_entry(terrain:Dictionary, index:int = -1):
if index < 0:
index = terrain_list.get_child_count()
var entry = TERRAIN_ENTRY_SCENE.instantiate()
entry.tileset = tileset
entry.terrain = terrain
entry.grid_mode = grid_mode_button.button_pressed
entry.select.connect(_on_entry_select)
terrain_list.add_child(entry)
terrain_list.move_child(entry, index)
func remove_terrain_entry(index: int):
terrain_list.get_child(index).free()
for i in range(index, terrain_list.get_child_count()):
var child = terrain_list.get_child(i)
child.terrain = BetterTerrain.get_terrain(tileset, i)
child.update()
func perform_add_terrain(name: String, color: Color, type: int, categories: Array, icon:Dictionary = {}) -> void:
if BetterTerrain.add_terrain(tileset, name, color, type, categories, icon):
var index = BetterTerrain.terrain_count(tileset) - 1
var terrain = BetterTerrain.get_terrain(tileset, index)
add_terrain_entry(terrain, index)
func perform_remove_terrain(index: int) -> void:
if index >= BetterTerrain.terrain_count(tileset):
return
if BetterTerrain.remove_terrain(tileset, index):
remove_terrain_entry(index)
update_tile_view_paint()
func perform_swap_terrain(index1: int, index2: int) -> void:
var lower := min(index1, index2)
var higher := max(index1, index2)
if lower >= terrain_list.get_child_count() or higher >= terrain_list.get_child_count():
return
var item1 = terrain_list.get_child(lower)
var item2 = terrain_list.get_child(higher)
if BetterTerrain.swap_terrains(tileset, lower, higher):
terrain_list.move_child(item1, higher)
item1.terrain = BetterTerrain.get_terrain(tileset, higher)
item1.update()
item2.terrain = BetterTerrain.get_terrain(tileset, lower)
item2.update()
selected_entry = index2
terrain_list.get_child(index2).set_selected(true)
update_tile_view_paint()
func perform_edit_terrain(index: int, name: String, color: Color, type: int, categories: Array, icon: Dictionary = {}) -> void:
if index >= terrain_list.get_child_count():
return
var entry = terrain_list.get_child(index)
# don't overwrite empty icon
var valid_icon = icon
if icon.has("path") and icon.path.is_empty():
var terrain = BetterTerrain.get_terrain(tileset, index)
valid_icon = terrain.icon
if BetterTerrain.set_terrain(tileset, index, name, color, type, categories, valid_icon):
entry.terrain = BetterTerrain.get_terrain(tileset, index)
entry.update()
tile_view.queue_redraw()
func _on_shuffle_random_pressed():
BetterTerrain.use_seed = !shuffle_random.button_pressed
func _on_bit_button_pressed(button: BaseButton) -> void:
match select_tiles.button_group.get_pressed_button():
select_tiles: tile_view.paint_mode = tile_view.PaintMode.SELECT
paint_type: tile_view.paint_mode = tile_view.PaintMode.PAINT_TYPE
paint_terrain: tile_view.paint_mode = tile_view.PaintMode.PAINT_PEERING
paint_symmetry: tile_view.paint_mode = tile_view.PaintMode.PAINT_SYMMETRY
_: tile_view.paint_mode = tile_view.PaintMode.NO_PAINT
tile_view.queue_redraw()
symmetry_options.visible = paint_symmetry.button_pressed
func _on_symmetry_selected(index):
tile_view.paint_symmetry = index
func _on_paste_occurred():
select_tiles.button_pressed = true
func _on_change_zoom_level(value):
zoom_slider.value = value
func _on_terrain_updated(index):
var entry = terrain_list.get_child(index)
entry.terrain = BetterTerrain.get_terrain(tileset, index)
entry.update()
func canvas_tilemap_transform() -> Transform2D:
var transform := tilemap.get_viewport_transform() * tilemap.global_transform
# Handle subviewport
var editor_viewport := EditorInterface.get_editor_viewport_2d()
if tilemap.get_viewport() != editor_viewport:
var container = tilemap.get_viewport().get_parent() as SubViewportContainer
if container:
transform = editor_viewport.global_canvas_transform * container.get_transform() * transform
return transform
func canvas_draw(overlay: Control) -> void:
if !draw_overlay:
return
if selected_entry < 0:
return
var type = selected_entry
var terrain := BetterTerrain.get_terrain(tileset, type)
if !terrain.valid:
return
var tiles := []
var transform := canvas_tilemap_transform()
if paint_action == PaintAction.RECT and paint_mode != PaintMode.NO_PAINT:
var area := Rect2i(initial_click, current_position - initial_click).abs()
# Shortcut fill for large areas
if area.size.x > 1 and area.size.y > 1 and area.size.x * area.size.y > MAX_CANVAS_RENDER_TILES:
var shortcut := PackedVector2Array([
tilemap.map_to_local(area.position),
tilemap.map_to_local(Vector2i(area.end.x, area.position.y)),
tilemap.map_to_local(area.end),
tilemap.map_to_local(Vector2i(area.position.x, area.end.y))
])
overlay.draw_colored_polygon(transform * shortcut, Color(terrain.color, 0.5))
return
for y in range(area.position.y, area.end.y + 1):
for x in range(area.position.x, area.end.x + 1):
tiles.append(Vector2i(x, y))
elif paint_action == PaintAction.LINE and paint_mode != PaintMode.NO_PAINT:
var cells := _get_tileset_line(initial_click, current_position, tileset)
var shape = BetterTerrain.data.cell_polygon(tileset)
for c in cells:
var tile_transform := Transform2D(0.0, tilemap.tile_set.tile_size, 0.0, tilemap.map_to_local(c))
overlay.draw_colored_polygon(transform * tile_transform * shape, Color(terrain.color, 0.5))
elif fill_button.button_pressed:
tiles = _get_fill_cells(current_position)
if tiles.size() > MAX_CANVAS_RENDER_TILES:
tiles.resize(MAX_CANVAS_RENDER_TILES)
else:
tiles.append(current_position)
var shape = BetterTerrain.data.cell_polygon(tileset)
for t in tiles:
var tile_transform := Transform2D(0.0, tilemap.tile_set.tile_size, 0.0, tilemap.map_to_local(t))
overlay.draw_colored_polygon(transform * tile_transform * shape, Color(terrain.color, 0.5))
func canvas_input(event: InputEvent) -> bool:
if selected_entry < 0:
return false
draw_overlay = true
if event is InputEventMouseMotion:
var tr := canvas_tilemap_transform()
var pos := tr.affine_inverse() * Vector2(event.position)
var event_position := tilemap.local_to_map(pos)
prev_position = current_position
if event_position == current_position:
return false
current_position = event_position
update_overlay.emit()
var replace_mode = replace_button.button_pressed
var released : bool = event is InputEventMouseButton and !event.pressed
if released:
terrain_undo.finish_action()
var type = selected_entry
if paint_action == PaintAction.RECT and paint_mode != PaintMode.NO_PAINT:
var area := Rect2i(initial_click, current_position - initial_click).abs()
# Fill from initial_target to target
undo_manager.create_action(tr("Draw terrain rectangle"), UndoRedo.MERGE_DISABLE, tilemap)
for y in range(area.position.y, area.end.y + 1):
for x in range(area.position.x, area.end.x + 1):
var coord := Vector2i(x, y)
if paint_mode == PaintMode.PAINT:
if replace_mode:
undo_manager.add_do_method(BetterTerrain, &"replace_cell", tilemap, coord, type)
else:
undo_manager.add_do_method(BetterTerrain, &"set_cell", tilemap, coord, type)
else:
undo_manager.add_do_method(tilemap, &"erase_cell", coord)
undo_manager.add_do_method(BetterTerrain, &"update_terrain_area", tilemap, area)
terrain_undo.create_tile_restore_point_area(undo_manager, tilemap, area)
undo_manager.commit_action()
update_overlay.emit()
elif paint_action == PaintAction.LINE and paint_mode != PaintMode.NO_PAINT:
undo_manager.create_action(tr("Draw terrain line"), UndoRedo.MERGE_DISABLE, tilemap)
var cells := _get_tileset_line(initial_click, current_position, tileset)
if paint_mode == PaintMode.PAINT:
if replace_mode:
undo_manager.add_do_method(BetterTerrain, &"replace_cells", tilemap, cells, type)
else:
undo_manager.add_do_method(BetterTerrain, &"set_cells", tilemap, cells, type)
elif paint_mode == PaintMode.ERASE:
for c in cells:
undo_manager.add_do_method(tilemap, &"erase_cell", c)
undo_manager.add_do_method(BetterTerrain, &"update_terrain_cells", tilemap, cells)
terrain_undo.create_tile_restore_point(undo_manager, tilemap, cells)
undo_manager.commit_action()
update_overlay.emit()
paint_mode = PaintMode.NO_PAINT
return true
var clicked : bool = event is InputEventMouseButton and event.pressed
if clicked:
paint_mode = PaintMode.NO_PAINT
if (event.is_command_or_control_pressed() and !event.shift_pressed):
var pick = BetterTerrain.get_cell(tilemap, current_position)
if pick >= 0:
terrain_list.get_children()[pick]._on_focus_entered()
#_on_entry_select(pick)
return true
paint_action = PaintAction.NO_ACTION
if rectangle_button.button_pressed:
paint_action = PaintAction.RECT
elif line_button.button_pressed:
paint_action = PaintAction.LINE
elif draw_button.button_pressed:
if event.shift_pressed:
paint_action = PaintAction.LINE
if event.is_command_or_control_pressed():
paint_action = PaintAction.RECT
if event.button_index == MOUSE_BUTTON_LEFT:
paint_mode = PaintMode.PAINT
elif event.button_index == MOUSE_BUTTON_RIGHT:
paint_mode = PaintMode.ERASE
else:
return false
if (clicked or event is InputEventMouseMotion) and paint_mode != PaintMode.NO_PAINT:
if clicked:
initial_click = current_position
terrain_undo.action_index += 1
terrain_undo.action_count = 0
var type = selected_entry
if paint_action == PaintAction.LINE or paint_action == PaintAction.RECT:
# if painting as line, execution happens on release.
# prevent other painting actions from running.
pass
elif draw_button.button_pressed:
undo_manager.create_action(tr("Draw terrain") + str(terrain_undo.action_index), UndoRedo.MERGE_ALL, tilemap, true)
var cells := _get_tileset_line(prev_position, current_position, tileset)
if paint_mode == PaintMode.PAINT:
if replace_mode:
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"replace_cells", [tilemap, cells, type])
else:
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"set_cells", [tilemap, cells, type])
elif paint_mode == PaintMode.ERASE:
for c in cells:
terrain_undo.add_do_method(undo_manager, tilemap, &"erase_cell", [c])
terrain_undo.add_do_method(undo_manager, BetterTerrain, &"update_terrain_cells", [tilemap, cells])
terrain_undo.create_tile_restore_point(undo_manager, tilemap, cells)
undo_manager.commit_action()
terrain_undo.action_count += 1
elif fill_button.button_pressed:
var cells := _get_fill_cells(current_position)
undo_manager.create_action(tr("Fill terrain"), UndoRedo.MERGE_DISABLE, tilemap)
if paint_mode == PaintMode.PAINT:
if replace_mode:
undo_manager.add_do_method(BetterTerrain, &"replace_cells", tilemap, cells, type)
else:
undo_manager.add_do_method(BetterTerrain, &"set_cells", tilemap, cells, type)
elif paint_mode == PaintMode.ERASE:
for c in cells:
undo_manager.add_do_method(tilemap, &"erase_cell", c)
undo_manager.add_do_method(BetterTerrain, &"update_terrain_cells", tilemap, cells)
terrain_undo.create_tile_restore_point(undo_manager, tilemap, cells)
undo_manager.commit_action()
update_overlay.emit()
return true
return false
func canvas_mouse_exit() -> void:
draw_overlay = false
update_overlay.emit()
func _shortcut_input(event) -> void:
if event is InputEventKey:
if event.keycode == KEY_C and (event.is_command_or_control_pressed() and not event.echo):
get_viewport().set_input_as_handled()
tile_view.copy_selection()
if event.keycode == KEY_V and (event.is_command_or_control_pressed() and not event.echo):
get_viewport().set_input_as_handled()
tile_view.paste_selection()
## bresenham alg ported from Geometry2D::bresenham_line()
func _get_line(from:Vector2i, to:Vector2i) -> Array[Vector2i]:
if from == to:
return [to]
var points:Array[Vector2i] = []
var delta := (to - from).abs() * 2
var step := (to - from).sign()
var current := from
if delta.x > delta.y:
var err:int = delta.x / 2
while current.x != to.x:
points.push_back(current);
err -= delta.y
if err < 0:
current.y += step.y
err += delta.x
current.x += step.x
else:
var err:int = delta.y / 2
while current.y != to.y:
points.push_back(current)
err -= delta.x
if err < 0:
current.x += step.x
err += delta.y
current.y += step.y
points.push_back(current);
return points;
## half-offset bresenham alg ported from TileMapEditor::get_line
func _get_tileset_line(from:Vector2i, to:Vector2i, tileset:TileSet) -> Array[Vector2i]:
if tileset.tile_shape == TileSet.TILE_SHAPE_SQUARE:
return _get_line(from, to)
var points:Array[Vector2i] = []
var transposed := tileset.get_tile_offset_axis() == TileSet.TILE_OFFSET_AXIS_VERTICAL
if transposed:
from = Vector2i(from.y, from.x)
to = Vector2i(to.y, to.x)
var delta:Vector2i = to - from
delta = Vector2i(2 * delta.x + abs(posmod(to.y, 2)) - abs(posmod(from.y, 2)), delta.y)
var sign:Vector2i = delta.sign()
var current := from;
points.push_back(Vector2i(current.y, current.x) if transposed else current)
var err := 0
if abs(delta.y) < abs(delta.x):
var err_step:Vector2i = 3 * delta.abs()
while current != to:
err += err_step.y
if err > abs(delta.x):
if sign.x == 0:
current += Vector2i(sign.y, 0)
else:
current += Vector2i(sign.x if bool(current.y % 2) != (sign.x < 0) else 0, sign.y)
err -= err_step.x
else:
current += Vector2i(sign.x, 0)
err += err_step.y
points.push_back(Vector2i(current.y, current.x) if transposed else current)
else:
var err_step:Vector2i = delta.abs()
while current != to:
err += err_step.x
if err > 0:
if sign.x == 0:
current += Vector2i(0, sign.y)
else:
current += Vector2i(sign.x if bool(current.y % 2) != (sign.x < 0) else 0, sign.y)
err -= err_step.y;
else:
if sign.x == 0:
current += Vector2i(0, sign.y)
else:
current += Vector2i(-sign.x if bool(current.y % 2) != (sign.x > 0) else 0, sign.y)
err += err_step.y
points.push_back(Vector2i(current.y, current.x) if transposed else current)
return points
func _on_terrain_enable_id_pressed(id):
if id in [SourceSelectors.ALL, SourceSelectors.NONE]:
for i in source_selector_popup.item_count:
if source_selector_popup.is_item_checkable(i):
source_selector_popup.set_item_checked(i, id == SourceSelectors.ALL)
else:
var index = source_selector_popup.get_item_index(id)
var checked = source_selector_popup.is_item_checked(index)
source_selector_popup.set_item_checked(index, !checked)
var disabled_sources : Array[int]
for i in source_selector_popup.item_count:
if source_selector_popup.is_item_checkable(i) and !source_selector_popup.is_item_checked(i):
disabled_sources.append(source_selector_popup.get_item_id(i))
tile_view.disabled_sources = disabled_sources
func corresponding_tilemap_editor_button(similar: Button) -> Button:
var editors = EditorInterface.get_base_control().find_children("*", "TileMapLayerEditor", true, false)
var tile_map_layer_editor = editors[0]
var buttons = tile_map_layer_editor.find_children("*", "Button", true, false)
for button: Button in buttons:
if button.icon == similar.icon:
return button
return null
func _on_layer_up_or_down_pressed(button: Button) -> void:
var matching_button = corresponding_tilemap_editor_button(button)
if !matching_button:
return
# Major hack, to reduce flicker hide the tileset editor briefly
var editors = EditorInterface.get_base_control().find_children("*", "TileSetEditor", true, false)
var tile_set_editor = editors[0]
matching_button.pressed.emit()
tile_set_editor.modulate = Color.TRANSPARENT
await get_tree().process_frame
await get_tree().process_frame
force_show_terrains.emit()
tile_set_editor.modulate = Color.WHITE
func _on_layer_up_pressed() -> void:
_on_layer_up_or_down_pressed(layer_up)
func _on_layer_down_pressed() -> void:
_on_layer_up_or_down_pressed(layer_down)
func _on_layer_highlight_toggled(toggled: bool) -> void:
var settings = EditorInterface.get_editor_settings()
settings.set_setting("editors/tiles_editor/highlight_selected_layer", toggled)
var highlight = corresponding_tilemap_editor_button(layer_highlight)
if highlight:
highlight.toggled.emit(toggled)
func _on_layer_grid_toggled(toggled: bool) -> void:
var settings = EditorInterface.get_editor_settings()
settings.set_setting("editors/tiles_editor/display_grid", toggled)
var grid = corresponding_tilemap_editor_button(layer_grid)
if grid:
grid.toggled.emit(toggled)