diff options
Diffstat (limited to 'editor/plugins/tiles')
19 files changed, 14268 insertions, 0 deletions
diff --git a/editor/plugins/tiles/SCsub b/editor/plugins/tiles/SCsub new file mode 100644 index 0000000000..359d04e5df --- /dev/null +++ b/editor/plugins/tiles/SCsub @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +Import("env") + +env.add_source_files(env.editor_sources, "*.cpp") diff --git a/editor/plugins/tiles/atlas_merging_dialog.cpp b/editor/plugins/tiles/atlas_merging_dialog.cpp new file mode 100644 index 0000000000..efccac7b74 --- /dev/null +++ b/editor/plugins/tiles/atlas_merging_dialog.cpp @@ -0,0 +1,323 @@ +/*************************************************************************/ +/* atlas_merging_dialog.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "atlas_merging_dialog.h" + +#include "editor/editor_scale.h" + +#include "scene/gui/control.h" +#include "scene/gui/split_container.h" + +void AtlasMergingDialog::_property_changed(const StringName &p_property, const Variant &p_value, const String &p_field, bool p_changing) { + _set(p_property, p_value); +} + +void AtlasMergingDialog::_generate_merged(Vector<Ref<TileSetAtlasSource>> p_atlas_sources, int p_max_columns) { + merged.instantiate(); + merged_mapping.clear(); + + if (p_atlas_sources.size() >= 2) { + Ref<Image> output_image; + output_image.instantiate(); + output_image->create(1, 1, false, Image::FORMAT_RGBA8); + + // Compute the new texture region size. + Vector2i new_texture_region_size; + for (int source_index = 0; source_index < p_atlas_sources.size(); source_index++) { + Ref<TileSetAtlasSource> atlas_source = p_atlas_sources[source_index]; + new_texture_region_size = new_texture_region_size.max(atlas_source->get_texture_region_size()); + } + + // Generate the merged TileSetAtlasSource. + Vector2i atlas_offset; + int line_height = 0; + for (int source_index = 0; source_index < p_atlas_sources.size(); source_index++) { + Ref<TileSetAtlasSource> atlas_source = p_atlas_sources[source_index]; + merged_mapping.push_back(Map<Vector2i, Vector2i>()); + + // Layout the tiles. + Vector2i atlas_size; + + for (int tile_index = 0; tile_index < atlas_source->get_tiles_count(); tile_index++) { + Vector2i tile_id = atlas_source->get_tile_id(tile_index); + atlas_size = atlas_size.max(tile_id + atlas_source->get_tile_size_in_atlas(tile_id)); + + Rect2i new_tile_rect_in_altas = Rect2i(atlas_offset + tile_id, atlas_source->get_tile_size_in_atlas(tile_id)); + + // Create tiles and alternatives, then copy their properties. + for (int alternative_index = 0; alternative_index < atlas_source->get_alternative_tiles_count(tile_id); alternative_index++) { + int alternative_id = atlas_source->get_alternative_tile_id(tile_id, alternative_index); + if (alternative_id == 0) { + merged->create_tile(new_tile_rect_in_altas.position, new_tile_rect_in_altas.size); + } else { + merged->create_alternative_tile(new_tile_rect_in_altas.position, alternative_index); + } + + // Copy the properties. + TileData *original_tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(tile_id, alternative_id)); + List<PropertyInfo> properties; + original_tile_data->get_property_list(&properties); + for (List<PropertyInfo>::Element *E = properties.front(); E; E = E->next()) { + const StringName &property_name = E->get().name; + merged->set(property_name, original_tile_data->get(property_name)); + } + + // Add to the mapping. + merged_mapping[source_index][tile_id] = new_tile_rect_in_altas.position; + } + + // Copy the texture. + for (int frame = 0; frame < atlas_source->get_tile_animation_frames_count(tile_id); frame++) { + Rect2i src_rect = atlas_source->get_tile_texture_region(tile_id, frame); + Rect2 dst_rect_wide = Rect2i(new_tile_rect_in_altas.position * new_texture_region_size, new_tile_rect_in_altas.size * new_texture_region_size); + if (dst_rect_wide.get_end().x > output_image->get_width() || dst_rect_wide.get_end().y > output_image->get_height()) { + output_image->crop(MAX(dst_rect_wide.get_end().x, output_image->get_width()), MAX(dst_rect_wide.get_end().y, output_image->get_height())); + } + output_image->blit_rect(atlas_source->get_texture()->get_image(), src_rect, dst_rect_wide.get_center() - src_rect.size / 2); + } + } + + // Compute the atlas offset. + line_height = MAX(atlas_size.y, line_height); + atlas_offset.x += atlas_size.x; + if (atlas_offset.x >= p_max_columns) { + atlas_offset.x = 0; + atlas_offset.y += line_height; + line_height = 0; + } + } + + Ref<ImageTexture> output_image_texture; + output_image_texture.instantiate(); + output_image_texture->create_from_image(output_image); + + merged->set_name(p_atlas_sources[0]->get_name()); + merged->set_texture(output_image_texture); + merged->set_texture_region_size(new_texture_region_size); + } +} + +void AtlasMergingDialog::_update_texture() { + Vector<int> selected = atlas_merging_atlases_list->get_selected_items(); + if (selected.size() >= 2) { + Vector<Ref<TileSetAtlasSource>> to_merge; + for (int i = 0; i < selected.size(); i++) { + int source_id = atlas_merging_atlases_list->get_item_metadata(selected[i]); + to_merge.push_back(tile_set->get_source(source_id)); + } + _generate_merged(to_merge, next_line_after_column); + preview->set_texture(merged->get_texture()); + preview->show(); + select_2_atlases_label->hide(); + get_ok_button()->set_disabled(false); + merge_button->set_disabled(false); + } else { + _generate_merged(Vector<Ref<TileSetAtlasSource>>(), next_line_after_column); + preview->set_texture(Ref<Texture2D>()); + preview->hide(); + select_2_atlases_label->show(); + get_ok_button()->set_disabled(true); + merge_button->set_disabled(true); + } +} + +void AtlasMergingDialog::_merge_confirmed(String p_path) { + ERR_FAIL_COND(!merged.is_valid()); + + Ref<ImageTexture> output_image_texture = merged->get_texture(); + output_image_texture->get_image()->save_png(p_path); + + Ref<Texture2D> new_texture_resource = ResourceLoader::load(p_path, "Texture2D"); + merged->set_texture(new_texture_resource); + + undo_redo->create_action(TTR("Merge TileSetAtlasSource")); + int next_id = tile_set->get_next_source_id(); + undo_redo->add_do_method(*tile_set, "add_source", merged, next_id); + undo_redo->add_undo_method(*tile_set, "remove_source", next_id); + + if (delete_original_atlases) { + // Delete originals if needed. + Vector<int> selected = atlas_merging_atlases_list->get_selected_items(); + for (int i = 0; i < selected.size(); i++) { + int source_id = atlas_merging_atlases_list->get_item_metadata(selected[i]); + Ref<TileSetAtlasSource> tas = tile_set->get_source(source_id); + undo_redo->add_do_method(*tile_set, "remove_source", source_id); + undo_redo->add_undo_method(*tile_set, "add_source", tas, source_id); + + // Add the tile proxies. + for (int tile_index = 0; tile_index < tas->get_tiles_count(); tile_index++) { + Vector2i tile_id = tas->get_tile_id(tile_index); + undo_redo->add_do_method(*tile_set, "set_coords_level_tile_proxy", source_id, tile_id, next_id, merged_mapping[i][tile_id]); + if (tile_set->has_coords_level_tile_proxy(source_id, tile_id)) { + Array a = tile_set->get_coords_level_tile_proxy(source_id, tile_id); + undo_redo->add_undo_method(*tile_set, "set_coords_level_tile_proxy", a[0], a[1]); + } else { + undo_redo->add_undo_method(*tile_set, "remove_coords_level_tile_proxy", source_id, tile_id); + } + } + } + } + undo_redo->commit_action(); + commited_actions_count++; + + hide(); +} + +void AtlasMergingDialog::ok_pressed() { + delete_original_atlases = false; + editor_file_dialog->popup_file_dialog(); +} + +void AtlasMergingDialog::cancel_pressed() { + for (int i = 0; i < commited_actions_count; i++) { + undo_redo->undo(); + } + commited_actions_count = 0; +} + +void AtlasMergingDialog::custom_action(const String &p_action) { + if (p_action == "merge") { + delete_original_atlases = true; + editor_file_dialog->popup_file_dialog(); + } +} + +bool AtlasMergingDialog::_set(const StringName &p_name, const Variant &p_value) { + if (p_name == "next_line_after_column" && p_value.get_type() == Variant::INT) { + next_line_after_column = p_value; + _update_texture(); + return true; + } + return false; +} + +bool AtlasMergingDialog::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "next_line_after_column") { + r_ret = next_line_after_column; + return true; + } + return false; +} + +void AtlasMergingDialog::update_tile_set(Ref<TileSet> p_tile_set) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + tile_set = p_tile_set; + + atlas_merging_atlases_list->clear(); + for (int i = 0; i < p_tile_set->get_source_count(); i++) { + int source_id = p_tile_set->get_source_id(i); + Ref<TileSetAtlasSource> atlas_source = p_tile_set->get_source(source_id); + if (atlas_source.is_valid()) { + Ref<Texture2D> texture = atlas_source->get_texture(); + if (texture.is_valid()) { + String item_text = vformat("%s (id:%d)", texture->get_path().get_file(), source_id); + atlas_merging_atlases_list->add_item(item_text, texture); + atlas_merging_atlases_list->set_item_metadata(atlas_merging_atlases_list->get_item_count() - 1, source_id); + } + } + } + + get_ok_button()->set_disabled(true); + merge_button->set_disabled(true); + + commited_actions_count = 0; +} + +AtlasMergingDialog::AtlasMergingDialog() { + // Atlas merging window. + set_title(TTR("Atlas Merging")); + set_hide_on_ok(false); + + // Ok buttons + get_ok_button()->set_text(TTR("Merge (Keep original Atlases)")); + get_ok_button()->set_disabled(true); + merge_button = add_button(TTR("Merge"), true, "merge"); + merge_button->set_disabled(true); + + HSplitContainer *atlas_merging_h_split_container = memnew(HSplitContainer); + atlas_merging_h_split_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + atlas_merging_h_split_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + add_child(atlas_merging_h_split_container); + + // Atlas sources item list. + atlas_merging_atlases_list = memnew(ItemList); + atlas_merging_atlases_list->set_fixed_icon_size(Size2i(60, 60) * EDSCALE); + atlas_merging_atlases_list->set_h_size_flags(Control::SIZE_EXPAND_FILL); + atlas_merging_atlases_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + atlas_merging_atlases_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + atlas_merging_atlases_list->set_custom_minimum_size(Size2(100, 200)); + atlas_merging_atlases_list->set_select_mode(ItemList::SELECT_MULTI); + atlas_merging_atlases_list->connect("multi_selected", callable_mp(this, &AtlasMergingDialog::_update_texture).unbind(2)); + atlas_merging_h_split_container->add_child(atlas_merging_atlases_list); + + VBoxContainer *atlas_merging_right_panel = memnew(VBoxContainer); + atlas_merging_right_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL); + atlas_merging_h_split_container->add_child(atlas_merging_right_panel); + + // Settings. + Label *settings_label = memnew(Label); + settings_label->set_text(TTR("Settings:")); + atlas_merging_right_panel->add_child(settings_label); + + columns_editor_property = memnew(EditorPropertyInteger); + columns_editor_property->set_label(TTR("Next Line After Column")); + columns_editor_property->set_object_and_property(this, "next_line_after_column"); + columns_editor_property->update_property(); + columns_editor_property->connect("property_changed", callable_mp(this, &AtlasMergingDialog::_property_changed)); + atlas_merging_right_panel->add_child(columns_editor_property); + + // Preview. + Label *preview_label = memnew(Label); + preview_label->set_text(TTR("Preview:")); + atlas_merging_right_panel->add_child(preview_label); + + preview = memnew(TextureRect); + preview->set_h_size_flags(Control::SIZE_EXPAND_FILL); + preview->set_v_size_flags(Control::SIZE_EXPAND_FILL); + preview->set_expand(true); + preview->hide(); + preview->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED); + atlas_merging_right_panel->add_child(preview); + + select_2_atlases_label = memnew(Label); + select_2_atlases_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + select_2_atlases_label->set_v_size_flags(Control::SIZE_EXPAND_FILL); + select_2_atlases_label->set_align(Label::ALIGN_CENTER); + select_2_atlases_label->set_valign(Label::VALIGN_CENTER); + select_2_atlases_label->set_text(TTR("Please select two atlases or more.")); + atlas_merging_right_panel->add_child(select_2_atlases_label); + + // The file dialog to choose the texture path. + editor_file_dialog = memnew(EditorFileDialog); + editor_file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); + editor_file_dialog->add_filter("*.png"); + editor_file_dialog->connect("file_selected", callable_mp(this, &AtlasMergingDialog::_merge_confirmed)); + add_child(editor_file_dialog); +} diff --git a/editor/plugins/tiles/atlas_merging_dialog.h b/editor/plugins/tiles/atlas_merging_dialog.h new file mode 100644 index 0000000000..7cb54bc17e --- /dev/null +++ b/editor/plugins/tiles/atlas_merging_dialog.h @@ -0,0 +1,86 @@ +/*************************************************************************/ +/* atlas_merging_dialog.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef ATLAS_MERGING_DIALOG_H +#define ATLAS_MERGING_DIALOG_H + +#include "editor/editor_node.h" +#include "editor/editor_properties.h" + +#include "scene/gui/dialogs.h" +#include "scene/gui/item_list.h" +#include "scene/gui/texture_rect.h" +#include "scene/resources/tile_set.h" + +class AtlasMergingDialog : public ConfirmationDialog { + GDCLASS(AtlasMergingDialog, ConfirmationDialog); + +private: + int commited_actions_count = 0; + bool delete_original_atlases = true; + Ref<TileSetAtlasSource> merged; + LocalVector<Map<Vector2i, Vector2i>> merged_mapping; + Ref<TileSet> tile_set; + + UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo(); + + // Settings. + int next_line_after_column = 30; + + // GUI. + ItemList *atlas_merging_atlases_list; + EditorPropertyVector2i *texture_region_size_editor_property; + EditorPropertyInteger *columns_editor_property; + TextureRect *preview; + Label *select_2_atlases_label; + EditorFileDialog *editor_file_dialog; + Button *merge_button; + + void _property_changed(const StringName &p_property, const Variant &p_value, const String &p_field, bool p_changing); + + void _generate_merged(Vector<Ref<TileSetAtlasSource>> p_atlas_sources, int p_max_columns); + void _update_texture(); + void _merge_confirmed(String p_path); + +protected: + virtual void ok_pressed() override; + virtual void cancel_pressed() override; + virtual void custom_action(const String &) override; + + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + +public: + void update_tile_set(Ref<TileSet> p_tile_set); + + AtlasMergingDialog(); +}; + +#endif // ATLAS_MERGING_DIALOG_H diff --git a/editor/plugins/tiles/tile_atlas_view.cpp b/editor/plugins/tiles/tile_atlas_view.cpp new file mode 100644 index 0000000000..604143ef93 --- /dev/null +++ b/editor/plugins/tiles/tile_atlas_view.cpp @@ -0,0 +1,687 @@ +/*************************************************************************/ +/* tile_atlas_view.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tile_atlas_view.h" + +#include "core/input/input.h" +#include "core/os/keyboard.h" +#include "scene/2d/tile_map.h" +#include "scene/gui/box_container.h" +#include "scene/gui/label.h" +#include "scene/gui/panel.h" +#include "scene/gui/texture_rect.h" + +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +void TileAtlasView::gui_input(const Ref<InputEvent> &p_event) { + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + drag_type = DRAG_TYPE_NONE; + + Vector2i scroll_vec = Vector2((mb->get_button_index() == MouseButton::WHEEL_LEFT) - (mb->get_button_index() == MouseButton::WHEEL_RIGHT), (mb->get_button_index() == MouseButton::WHEEL_UP) - (mb->get_button_index() == MouseButton::WHEEL_DOWN)); + if (scroll_vec != Vector2()) { + if (mb->is_ctrl_pressed()) { + if (mb->is_shift_pressed()) { + panning.x += 32 * mb->get_factor() * scroll_vec.y; + panning.y += 32 * mb->get_factor() * scroll_vec.x; + } else { + panning.y += 32 * mb->get_factor() * scroll_vec.y; + panning.x += 32 * mb->get_factor() * scroll_vec.x; + } + + emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning); + _update_zoom_and_panning(true); + accept_event(); + + } else if (!mb->is_shift_pressed()) { + zoom_widget->set_zoom_by_increments(scroll_vec.y * 2); + emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning); + _update_zoom_and_panning(true); + accept_event(); + } + } + + if (mb->get_button_index() == MouseButton::MIDDLE || mb->get_button_index() == MouseButton::RIGHT) { + if (mb->is_pressed()) { + drag_type = DRAG_TYPE_PAN; + } else { + drag_type = DRAG_TYPE_NONE; + } + accept_event(); + } + } + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + if (drag_type == DRAG_TYPE_PAN) { + panning += mm->get_relative(); + _update_zoom_and_panning(); + emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning); + accept_event(); + } + } +} + +Size2i TileAtlasView::_compute_base_tiles_control_size() { + // Update the texture. + Vector2i size; + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + if (texture.is_valid()) { + size = texture->get_size(); + } + return size; +} + +Size2i TileAtlasView::_compute_alternative_tiles_control_size() { + Vector2i size; + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i tile_id = tile_set_atlas_source->get_tile_id(i); + int alternatives_count = tile_set_atlas_source->get_alternative_tiles_count(tile_id); + Vector2i line_size; + Size2i texture_region_size = tile_set_atlas_source->get_tile_texture_region(tile_id).size; + for (int j = 1; j < alternatives_count; j++) { + int alternative_id = tile_set_atlas_source->get_alternative_tile_id(tile_id, j); + bool transposed = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(tile_id, alternative_id))->get_transpose(); + line_size.x += transposed ? texture_region_size.y : texture_region_size.x; + line_size.y = MAX(line_size.y, transposed ? texture_region_size.x : texture_region_size.y); + } + size.x = MAX(size.x, line_size.x); + size.y += line_size.y; + } + + return size; +} + +void TileAtlasView::_update_zoom_and_panning(bool p_zoom_on_mouse_pos) { + float zoom = zoom_widget->get_zoom(); + + // Compute the minimum sizes. + Size2i base_tiles_control_size = _compute_base_tiles_control_size(); + base_tiles_root_control->set_custom_minimum_size(Vector2(base_tiles_control_size) * zoom); + + Size2i alternative_tiles_control_size = _compute_alternative_tiles_control_size(); + alternative_tiles_root_control->set_custom_minimum_size(Vector2(alternative_tiles_control_size) * zoom); + + // Set the texture for the base tiles. + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + + // Set the scales. + if (base_tiles_control_size.x > 0 && base_tiles_control_size.y > 0) { + base_tiles_drawing_root->set_scale(Vector2(zoom, zoom)); + } else { + base_tiles_drawing_root->set_scale(Vector2(1, 1)); + } + if (alternative_tiles_control_size.x > 0 && alternative_tiles_control_size.y > 0) { + alternative_tiles_drawing_root->set_scale(Vector2(zoom, zoom)); + } else { + alternative_tiles_drawing_root->set_scale(Vector2(1, 1)); + } + + // Update the margin container's margins. + const char *constants[] = { "margin_left", "margin_top", "margin_right", "margin_bottom" }; + for (int i = 0; i < 4; i++) { + margin_container->add_theme_constant_override(constants[i], margin_container_paddings[i] * zoom); + } + + // Update the backgrounds. + background_left->update(); + background_right->update(); + + // Zoom on the position. + if (p_zoom_on_mouse_pos) { + // Offset the panning relative to the center of panel. + Vector2 relative_mpos = get_local_mouse_position() - get_size() / 2; + panning = (panning - relative_mpos) * zoom / previous_zoom + relative_mpos; + } else { + // Center of panel. + panning = panning * zoom / previous_zoom; + } + button_center_view->set_disabled(panning.is_equal_approx(Vector2())); + + previous_zoom = zoom; + + center_container->set_begin(panning - center_container->get_minimum_size() / 2); + center_container->set_size(center_container->get_minimum_size()); +} + +void TileAtlasView::_zoom_widget_changed() { + _update_zoom_and_panning(); + emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning); +} + +void TileAtlasView::_center_view() { + panning = Vector2(); + button_center_view->set_disabled(true); + _update_zoom_and_panning(); + emit_signal(SNAME("transform_changed"), zoom_widget->get_zoom(), panning); +} + +void TileAtlasView::_base_tiles_root_control_gui_input(const Ref<InputEvent> &p_event) { + base_tiles_root_control->set_tooltip(""); + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + Transform2D xform = base_tiles_drawing_root->get_transform().affine_inverse(); + Vector2i coords = get_atlas_tile_coords_at_pos(xform.xform(mm->get_position())); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + coords = tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + base_tiles_root_control->set_tooltip(vformat(TTR("Source: %d\nAtlas coordinates: %s\nAlternative: 0"), source_id, coords)); + } + } + } +} + +void TileAtlasView::_draw_base_tiles() { + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + if (texture.is_valid()) { + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i texture_region_size = tile_set_atlas_source->get_texture_region_size(); + Size2i grid_size = tile_set_atlas_source->get_atlas_grid_size(); + + // Draw the texture where there is no tile. + for (int x = 0; x < grid_size.x; x++) { + for (int y = 0; y < grid_size.y; y++) { + Vector2i coords = Vector2i(x, y); + if (tile_set_atlas_source->get_tile_at_coords(coords) == TileSetSource::INVALID_ATLAS_COORDS) { + Rect2i rect = Rect2i((texture_region_size + separation) * coords + margins, texture_region_size + separation); + rect = rect.intersection(Rect2i(Vector2(), texture->get_size())); + if (rect.size.x > 0 && rect.size.y > 0) { + base_tiles_draw->draw_texture_rect_region(texture, rect, rect); + base_tiles_draw->draw_rect(rect, Color(0.0, 0.0, 0.0, 0.5)); + } + } + } + } + + // Draw the texture around the grid. + Rect2i rect; + + // Top. + rect.position = Vector2i(); + rect.set_end(Vector2i(texture->get_size().x, margins.y)); + base_tiles_draw->draw_texture_rect_region(texture, rect, rect); + base_tiles_draw->draw_rect(rect, Color(0.0, 0.0, 0.0, 0.5)); + + // Bottom + int bottom_border = margins.y + (grid_size.y * (texture_region_size.y + separation.y)); + if (bottom_border < texture->get_size().y) { + rect.position = Vector2i(0, bottom_border); + rect.set_end(texture->get_size()); + base_tiles_draw->draw_texture_rect_region(texture, rect, rect); + base_tiles_draw->draw_rect(rect, Color(0.0, 0.0, 0.0, 0.5)); + } + + // Left + rect.position = Vector2i(0, margins.y); + rect.set_end(Vector2i(margins.x, margins.y + (grid_size.y * (texture_region_size.y + separation.y)))); + base_tiles_draw->draw_texture_rect_region(texture, rect, rect); + base_tiles_draw->draw_rect(rect, Color(0.0, 0.0, 0.0, 0.5)); + + // Right. + int right_border = margins.x + (grid_size.x * (texture_region_size.x + separation.x)); + if (right_border < texture->get_size().x) { + rect.position = Vector2i(right_border, margins.y); + rect.set_end(Vector2i(texture->get_size().x, margins.y + (grid_size.y * (texture_region_size.y + separation.y)))); + base_tiles_draw->draw_texture_rect_region(texture, rect, rect); + base_tiles_draw->draw_rect(rect, Color(0.0, 0.0, 0.0, 0.5)); + } + + // Draw actual tiles, using their properties (modulation, etc...) + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i atlas_coords = tile_set_atlas_source->get_tile_id(i); + + for (int frame = 0; frame < tile_set_atlas_source->get_tile_animation_frames_count(atlas_coords); frame++) { + // Update the y to max value. + Rect2i base_frame_rect = tile_set_atlas_source->get_tile_texture_region(atlas_coords, frame); + Vector2i offset_pos = base_frame_rect.get_center() + tile_set_atlas_source->get_tile_effective_texture_offset(atlas_coords, 0); + + // Draw the tile. + TileMap::draw_tile(base_tiles_draw->get_canvas_item(), offset_pos, tile_set, source_id, atlas_coords, 0, frame); + + // Draw, the texture in the separation areas + if (separation.x > 0) { + Rect2i right_sep_rect = Rect2i(base_frame_rect.get_position() + Vector2i(base_frame_rect.size.x, 0), Vector2i(separation.x, base_frame_rect.size.y)); + right_sep_rect = right_sep_rect.intersection(Rect2i(Vector2(), texture->get_size())); + if (right_sep_rect.size.x > 0 && right_sep_rect.size.y > 0) { + base_tiles_draw->draw_texture_rect_region(texture, right_sep_rect, right_sep_rect); + base_tiles_draw->draw_rect(right_sep_rect, Color(0.0, 0.0, 0.0, 0.5)); + } + } + + if (separation.y > 0) { + Rect2i bottom_sep_rect = Rect2i(base_frame_rect.get_position() + Vector2i(0, base_frame_rect.size.y), Vector2i(base_frame_rect.size.x + separation.x, separation.y)); + bottom_sep_rect = bottom_sep_rect.intersection(Rect2i(Vector2(), texture->get_size())); + if (bottom_sep_rect.size.x > 0 && bottom_sep_rect.size.y > 0) { + base_tiles_draw->draw_texture_rect_region(texture, bottom_sep_rect, bottom_sep_rect); + base_tiles_draw->draw_rect(bottom_sep_rect, Color(0.0, 0.0, 0.0, 0.5)); + } + } + } + } + } +} + +void TileAtlasView::_draw_base_tiles_texture_grid() { + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + if (texture.is_valid()) { + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i texture_region_size = tile_set_atlas_source->get_texture_region_size(); + + Size2i grid_size = tile_set_atlas_source->get_atlas_grid_size(); + + // Draw each tile texture region. + for (int x = 0; x < grid_size.x; x++) { + for (int y = 0; y < grid_size.y; y++) { + Vector2i origin = margins + (Vector2i(x, y) * (texture_region_size + separation)); + Vector2i base_tile_coords = tile_set_atlas_source->get_tile_at_coords(Vector2i(x, y)); + if (base_tile_coords != TileSetSource::INVALID_ATLAS_COORDS) { + if (base_tile_coords == Vector2i(x, y)) { + // Draw existing tile. + Vector2i size_in_atlas = tile_set_atlas_source->get_tile_size_in_atlas(base_tile_coords); + Vector2 region_size = texture_region_size * size_in_atlas + separation * (size_in_atlas - Vector2i(1, 1)); + base_tiles_texture_grid->draw_rect(Rect2i(origin, region_size), Color(1.0, 1.0, 1.0, 0.8), false); + } + } else { + // Draw the grid. + base_tiles_texture_grid->draw_rect(Rect2i(origin, texture_region_size), Color(0.7, 0.7, 0.7, 0.1), false); + } + } + } + } +} + +void TileAtlasView::_draw_base_tiles_shape_grid() { + // Draw the shapes. + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Vector2i tile_shape_size = tile_set->get_tile_size(); + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i tile_id = tile_set_atlas_source->get_tile_id(i); + Vector2 in_tile_base_offset = tile_set_atlas_source->get_tile_effective_texture_offset(tile_id, 0); + + for (int frame = 0; frame < tile_set_atlas_source->get_tile_animation_frames_count(tile_id); frame++) { + Color color = grid_color; + if (frame > 0) { + color.a *= 0.3; + } + Rect2i texture_region = tile_set_atlas_source->get_tile_texture_region(tile_id); + Transform2D tile_xform; + tile_xform.set_origin(texture_region.get_center() + in_tile_base_offset); + tile_xform.set_scale(tile_shape_size); + tile_set->draw_tile_shape(base_tiles_shape_grid, tile_xform, color); + } + } +} + +void TileAtlasView::_alternative_tiles_root_control_gui_input(const Ref<InputEvent> &p_event) { + alternative_tiles_root_control->set_tooltip(""); + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + Transform2D xform = alternative_tiles_drawing_root->get_transform().affine_inverse(); + Vector3i coords3 = get_alternative_tile_at_pos(xform.xform(mm->get_position())); + Vector2i coords = Vector2i(coords3.x, coords3.y); + int alternative_id = coords3.z; + if (coords != TileSetSource::INVALID_ATLAS_COORDS && alternative_id != TileSetSource::INVALID_TILE_ALTERNATIVE) { + alternative_tiles_root_control->set_tooltip(vformat(TTR("Source: %d\nAtlas coordinates: %s\nAlternative: %d"), source_id, coords, alternative_id)); + } + } +} + +void TileAtlasView::_draw_alternatives() { + // Draw the alternative tiles. + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + if (texture.is_valid()) { + Vector2 current_pos; + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i atlas_coords = tile_set_atlas_source->get_tile_id(i); + current_pos.x = 0; + int y_increment = 0; + Size2i texture_region_size = tile_set_atlas_source->get_tile_texture_region(atlas_coords).size; + int alternatives_count = tile_set_atlas_source->get_alternative_tiles_count(atlas_coords); + for (int j = 1; j < alternatives_count; j++) { + int alternative_id = tile_set_atlas_source->get_alternative_tile_id(atlas_coords, j); + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(atlas_coords, alternative_id)); + bool transposed = tile_data->get_transpose(); + + // Update the y to max value. + Vector2i offset_pos = current_pos; + if (transposed) { + offset_pos = (current_pos + Vector2(texture_region_size.y, texture_region_size.x) / 2 + tile_set_atlas_source->get_tile_effective_texture_offset(atlas_coords, alternative_id)); + y_increment = MAX(y_increment, texture_region_size.x); + } else { + offset_pos = (current_pos + texture_region_size / 2 + tile_set_atlas_source->get_tile_effective_texture_offset(atlas_coords, alternative_id)); + y_increment = MAX(y_increment, texture_region_size.y); + } + + // Draw the tile. + TileMap::draw_tile(alternatives_draw->get_canvas_item(), offset_pos, tile_set, source_id, atlas_coords, alternative_id); + + // Increment the x position. + current_pos.x += transposed ? texture_region_size.y : texture_region_size.x; + } + if (alternatives_count > 1) { + current_pos.y += y_increment; + } + } + } +} + +void TileAtlasView::_draw_background_left() { + Ref<Texture2D> texture = get_theme_icon(SNAME("Checkerboard"), SNAME("EditorIcons")); + background_left->set_size(base_tiles_root_control->get_custom_minimum_size()); + background_left->draw_texture_rect(texture, Rect2(Vector2(), background_left->get_size()), true); +} + +void TileAtlasView::_draw_background_right() { + Ref<Texture2D> texture = get_theme_icon(SNAME("Checkerboard"), SNAME("EditorIcons")); + background_right->set_size(alternative_tiles_root_control->get_custom_minimum_size()); + background_right->draw_texture_rect(texture, Rect2(Vector2(), background_right->get_size()), true); +} + +void TileAtlasView::set_atlas_source(TileSet *p_tile_set, TileSetAtlasSource *p_tile_set_atlas_source, int p_source_id) { + ERR_FAIL_COND(!p_tile_set); + ERR_FAIL_COND(!p_tile_set_atlas_source); + ERR_FAIL_COND(p_source_id < 0); + ERR_FAIL_COND(p_tile_set->get_source(p_source_id) != p_tile_set_atlas_source); + + tile_set = p_tile_set; + tile_set_atlas_source = p_tile_set_atlas_source; + source_id = p_source_id; + + // Show or hide the view. + bool valid = tile_set_atlas_source->get_texture().is_valid(); + hbox->set_visible(valid); + missing_source_label->set_visible(!valid); + + // Update the rect cache. + _update_alternative_tiles_rect_cache(); + + // Update everything. + _update_zoom_and_panning(); + + // Change children control size. + Size2i base_tiles_control_size = _compute_base_tiles_control_size(); + for (int i = 0; i < base_tiles_drawing_root->get_child_count(); i++) { + Control *control = Object::cast_to<Control>(base_tiles_drawing_root->get_child(i)); + if (control) { + control->set_size(base_tiles_control_size); + } + } + + Size2i alternative_control_size = _compute_alternative_tiles_control_size(); + for (int i = 0; i < alternative_tiles_drawing_root->get_child_count(); i++) { + Control *control = Object::cast_to<Control>(alternative_tiles_drawing_root->get_child(i)); + if (control) { + control->set_size(alternative_control_size); + } + } + + // Update. + base_tiles_draw->update(); + base_tiles_texture_grid->update(); + base_tiles_shape_grid->update(); + alternatives_draw->update(); + background_left->update(); + background_right->update(); +} + +float TileAtlasView::get_zoom() const { + return zoom_widget->get_zoom(); +}; + +void TileAtlasView::set_transform(float p_zoom, Vector2i p_panning) { + zoom_widget->set_zoom(p_zoom); + panning = p_panning; + _update_zoom_and_panning(); +}; + +void TileAtlasView::set_padding(Side p_side, int p_padding) { + ERR_FAIL_COND(p_padding < 0); + margin_container_paddings[p_side] = p_padding; +} + +Vector2i TileAtlasView::get_atlas_tile_coords_at_pos(const Vector2 p_pos) const { + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + if (texture.is_valid()) { + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i texture_region_size = tile_set_atlas_source->get_texture_region_size(); + + // Compute index in atlas + Vector2 pos = p_pos - margins; + Vector2i ret = (pos / (texture_region_size + separation)).floor(); + + return ret; + } + + return TileSetSource::INVALID_ATLAS_COORDS; +} + +void TileAtlasView::_update_alternative_tiles_rect_cache() { + alternative_tiles_rect_cache.clear(); + + Rect2i current; + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i tile_id = tile_set_atlas_source->get_tile_id(i); + int alternatives_count = tile_set_atlas_source->get_alternative_tiles_count(tile_id); + Size2i texture_region_size = tile_set_atlas_source->get_tile_texture_region(tile_id).size; + int line_height = 0; + for (int j = 1; j < alternatives_count; j++) { + int alternative_id = tile_set_atlas_source->get_alternative_tile_id(tile_id, j); + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(tile_id, alternative_id)); + bool transposed = tile_data->get_transpose(); + current.size = transposed ? Vector2i(texture_region_size.y, texture_region_size.x) : texture_region_size; + + // Update the rect. + if (!alternative_tiles_rect_cache.has(tile_id)) { + alternative_tiles_rect_cache[tile_id] = Map<int, Rect2i>(); + } + alternative_tiles_rect_cache[tile_id][alternative_id] = current; + + current.position.x += transposed ? texture_region_size.y : texture_region_size.x; + line_height = MAX(line_height, transposed ? texture_region_size.x : texture_region_size.y); + } + + current.position.x = 0; + current.position.y += line_height; + } +} + +Vector3i TileAtlasView::get_alternative_tile_at_pos(const Vector2 p_pos) const { + for (const KeyValue<Vector2, Map<int, Rect2i>> &E_coords : alternative_tiles_rect_cache) { + for (const KeyValue<int, Rect2i> &E_alternative : E_coords.value) { + if (E_alternative.value.has_point(p_pos)) { + return Vector3i(E_coords.key.x, E_coords.key.y, E_alternative.key); + } + } + } + + return Vector3i(TileSetSource::INVALID_ATLAS_COORDS.x, TileSetSource::INVALID_ATLAS_COORDS.y, TileSetSource::INVALID_TILE_ALTERNATIVE); +} + +Rect2i TileAtlasView::get_alternative_tile_rect(const Vector2i p_coords, int p_alternative_tile) { + ERR_FAIL_COND_V_MSG(!alternative_tiles_rect_cache.has(p_coords), Rect2i(), vformat("No cached rect for tile coords:%s", p_coords)); + ERR_FAIL_COND_V_MSG(!alternative_tiles_rect_cache[p_coords].has(p_alternative_tile), Rect2i(), vformat("No cached rect for tile coords:%s alternative_id:%d", p_coords, p_alternative_tile)); + + return alternative_tiles_rect_cache[p_coords][p_alternative_tile]; +} + +void TileAtlasView::update() { + base_tiles_draw->update(); + base_tiles_texture_grid->update(); + base_tiles_shape_grid->update(); + alternatives_draw->update(); + background_left->update(); + background_right->update(); +} + +void TileAtlasView::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_READY: + button_center_view->set_icon(get_theme_icon(SNAME("CenterView"), SNAME("EditorIcons"))); + break; + } +} + +void TileAtlasView::_bind_methods() { + ADD_SIGNAL(MethodInfo("transform_changed", PropertyInfo(Variant::FLOAT, "zoom"), PropertyInfo(Variant::VECTOR2, "scroll"))); +} + +TileAtlasView::TileAtlasView() { + set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + + Panel *panel = memnew(Panel); + panel->set_clip_contents(true); + panel->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + panel->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + panel->set_h_size_flags(SIZE_EXPAND_FILL); + panel->set_v_size_flags(SIZE_EXPAND_FILL); + add_child(panel); + + // Scrollingsc + zoom_widget = memnew(EditorZoomWidget); + add_child(zoom_widget); + zoom_widget->set_anchors_and_offsets_preset(Control::PRESET_TOP_LEFT, Control::PRESET_MODE_MINSIZE, 2 * EDSCALE); + zoom_widget->connect("zoom_changed", callable_mp(this, &TileAtlasView::_zoom_widget_changed).unbind(1)); + + button_center_view = memnew(Button); + button_center_view->set_icon(get_theme_icon(SNAME("CenterView"), SNAME("EditorIcons"))); + button_center_view->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT, Control::PRESET_MODE_MINSIZE, 5); + button_center_view->connect("pressed", callable_mp(this, &TileAtlasView::_center_view)); + button_center_view->set_flat(true); + button_center_view->set_disabled(true); + button_center_view->set_tooltip(TTR("Center View")); + add_child(button_center_view); + + center_container = memnew(CenterContainer); + center_container->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + center_container->set_anchors_preset(Control::PRESET_CENTER); + center_container->connect("gui_input", callable_mp(this, &TileAtlasView::gui_input)); + panel->add_child(center_container); + + missing_source_label = memnew(Label); + missing_source_label->set_text(TTR("No atlas source with a valid texture selected.")); + center_container->add_child(missing_source_label); + + margin_container = memnew(MarginContainer); + margin_container->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + center_container->add_child(margin_container); + + hbox = memnew(HBoxContainer); + hbox->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + hbox->add_theme_constant_override("separation", 10); + hbox->hide(); + margin_container->add_child(hbox); + + VBoxContainer *left_vbox = memnew(VBoxContainer); + left_vbox->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + hbox->add_child(left_vbox); + + VBoxContainer *right_vbox = memnew(VBoxContainer); + right_vbox->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + hbox->add_child(right_vbox); + + // Base tiles. + Label *base_tile_label = memnew(Label); + base_tile_label->set_mouse_filter(Control::MOUSE_FILTER_PASS); + base_tile_label->set_text(TTR("Base Tiles")); + base_tile_label->set_align(Label::ALIGN_CENTER); + left_vbox->add_child(base_tile_label); + + base_tiles_root_control = memnew(Control); + base_tiles_root_control->set_mouse_filter(Control::MOUSE_FILTER_PASS); + base_tiles_root_control->set_v_size_flags(Control::SIZE_EXPAND_FILL); + base_tiles_root_control->connect("gui_input", callable_mp(this, &TileAtlasView::_base_tiles_root_control_gui_input)); + left_vbox->add_child(base_tiles_root_control); + + background_left = memnew(Control); + background_left->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + background_left->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + background_left->set_texture_repeat(TextureRepeat::TEXTURE_REPEAT_ENABLED); + background_left->connect("draw", callable_mp(this, &TileAtlasView::_draw_background_left)); + base_tiles_root_control->add_child(background_left); + + base_tiles_drawing_root = memnew(Control); + base_tiles_drawing_root->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + base_tiles_drawing_root->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + base_tiles_drawing_root->set_texture_filter(TEXTURE_FILTER_NEAREST); + base_tiles_root_control->add_child(base_tiles_drawing_root); + + base_tiles_draw = memnew(Control); + base_tiles_draw->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + base_tiles_draw->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + base_tiles_draw->connect("draw", callable_mp(this, &TileAtlasView::_draw_base_tiles)); + base_tiles_drawing_root->add_child(base_tiles_draw); + + base_tiles_texture_grid = memnew(Control); + base_tiles_texture_grid->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + base_tiles_texture_grid->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + base_tiles_texture_grid->connect("draw", callable_mp(this, &TileAtlasView::_draw_base_tiles_texture_grid)); + base_tiles_drawing_root->add_child(base_tiles_texture_grid); + + base_tiles_shape_grid = memnew(Control); + base_tiles_shape_grid->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + base_tiles_shape_grid->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + base_tiles_shape_grid->connect("draw", callable_mp(this, &TileAtlasView::_draw_base_tiles_shape_grid)); + base_tiles_drawing_root->add_child(base_tiles_shape_grid); + + // Alternative tiles. + Label *alternative_tiles_label = memnew(Label); + alternative_tiles_label->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + alternative_tiles_label->set_text(TTR("Alternative Tiles")); + alternative_tiles_label->set_align(Label::ALIGN_CENTER); + right_vbox->add_child(alternative_tiles_label); + + alternative_tiles_root_control = memnew(Control); + alternative_tiles_root_control->set_mouse_filter(Control::MOUSE_FILTER_PASS); + alternative_tiles_root_control->connect("gui_input", callable_mp(this, &TileAtlasView::_alternative_tiles_root_control_gui_input)); + right_vbox->add_child(alternative_tiles_root_control); + + background_right = memnew(Control); + background_right->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + background_right->set_texture_repeat(TextureRepeat::TEXTURE_REPEAT_ENABLED); + background_right->connect("draw", callable_mp(this, &TileAtlasView::_draw_background_right)); + + alternative_tiles_root_control->add_child(background_right); + + alternative_tiles_drawing_root = memnew(Control); + alternative_tiles_drawing_root->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + alternative_tiles_drawing_root->set_texture_filter(TEXTURE_FILTER_NEAREST); + alternative_tiles_root_control->add_child(alternative_tiles_drawing_root); + + alternatives_draw = memnew(Control); + alternatives_draw->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + alternatives_draw->connect("draw", callable_mp(this, &TileAtlasView::_draw_alternatives)); + alternative_tiles_drawing_root->add_child(alternatives_draw); +} diff --git a/editor/plugins/tiles/tile_atlas_view.h b/editor/plugins/tiles/tile_atlas_view.h new file mode 100644 index 0000000000..e1ca3eebee --- /dev/null +++ b/editor/plugins/tiles/tile_atlas_view.h @@ -0,0 +1,156 @@ +/*************************************************************************/ +/* tile_atlas_view.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILE_ATLAS_VIEW_H +#define TILE_ATLAS_VIEW_H + +#include "editor/editor_zoom_widget.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/center_container.h" +#include "scene/gui/label.h" +#include "scene/gui/margin_container.h" +#include "scene/gui/scroll_container.h" +#include "scene/gui/texture_rect.h" +#include "scene/resources/tile_set.h" + +class TileAtlasView : public Control { + GDCLASS(TileAtlasView, Control); + +private: + TileSet *tile_set; + TileSetAtlasSource *tile_set_atlas_source; + int source_id = TileSet::INVALID_SOURCE; + + enum DragType { + DRAG_TYPE_NONE, + DRAG_TYPE_PAN, + }; + DragType drag_type = DRAG_TYPE_NONE; + float previous_zoom = 1.0; + EditorZoomWidget *zoom_widget; + Button *button_center_view; + CenterContainer *center_container; + Vector2 panning; + void _update_zoom_and_panning(bool p_zoom_on_mouse_pos = false); + void _zoom_widget_changed(); + void _center_view(); + virtual void gui_input(const Ref<InputEvent> &p_event) override; + + Map<Vector2, Map<int, Rect2i>> alternative_tiles_rect_cache; + void _update_alternative_tiles_rect_cache(); + + MarginContainer *margin_container; + int margin_container_paddings[4] = { 0, 0, 0, 0 }; + HBoxContainer *hbox; + Label *missing_source_label; + + // Background + Control *background_left; + void _draw_background_left(); + Control *background_right; + void _draw_background_right(); + + // Left side. + Control *base_tiles_root_control; + void _base_tiles_root_control_gui_input(const Ref<InputEvent> &p_event); + + Control *base_tiles_drawing_root; + + Control *base_tiles_draw; + void _draw_base_tiles(); + + Control *base_tiles_texture_grid; + void _draw_base_tiles_texture_grid(); + + Control *base_tiles_shape_grid; + void _draw_base_tiles_shape_grid(); + + Size2i _compute_base_tiles_control_size(); + + // Right side. + Control *alternative_tiles_root_control; + void _alternative_tiles_root_control_gui_input(const Ref<InputEvent> &p_event); + + Control *alternative_tiles_drawing_root; + + Control *alternatives_draw; + void _draw_alternatives(); + + Size2i _compute_alternative_tiles_control_size(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + // Global. + void set_atlas_source(TileSet *p_tile_set, TileSetAtlasSource *p_tile_set_atlas_source, int p_source_id); + + float get_zoom() const; + void set_transform(float p_zoom, Vector2i p_panning); + + void set_padding(Side p_side, int p_padding); + + // Left side. + void set_texture_grid_visible(bool p_visible) { base_tiles_texture_grid->set_visible(p_visible); }; + void set_tile_shape_grid_visible(bool p_visible) { base_tiles_shape_grid->set_visible(p_visible); }; + + Vector2i get_atlas_tile_coords_at_pos(const Vector2 p_pos) const; + + void add_control_over_atlas_tiles(Control *p_control, bool scaled = true) { + if (scaled) { + base_tiles_drawing_root->add_child(p_control); + } else { + base_tiles_root_control->add_child(p_control); + } + p_control->set_mouse_filter(Control::MOUSE_FILTER_PASS); + }; + + // Right side. + Vector3i get_alternative_tile_at_pos(const Vector2 p_pos) const; + Rect2i get_alternative_tile_rect(const Vector2i p_coords, int p_alternative_tile); + + void add_control_over_alternative_tiles(Control *p_control, bool scaled = true) { + if (scaled) { + alternative_tiles_drawing_root->add_child(p_control); + } else { + alternative_tiles_root_control->add_child(p_control); + } + p_control->set_mouse_filter(Control::MOUSE_FILTER_PASS); + }; + + // Update everything. + void update(); + + TileAtlasView(); +}; + +#endif // TILE_ATLAS_VIEW diff --git a/editor/plugins/tiles/tile_data_editors.cpp b/editor/plugins/tiles/tile_data_editors.cpp new file mode 100644 index 0000000000..d165f44334 --- /dev/null +++ b/editor/plugins/tiles/tile_data_editors.cpp @@ -0,0 +1,2624 @@ +/*************************************************************************/ +/* tile_data_editors.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tile_data_editors.h" + +#include "tile_set_editor.h" + +#include "core/math/geometry_2d.h" +#include "core/os/keyboard.h" + +#include "editor/editor_properties.h" +#include "editor/editor_scale.h" + +void TileDataEditor::_tile_set_changed_plan_update() { + _tile_set_changed_update_needed = true; + call_deferred("_tile_set_changed_deferred_update"); +} + +void TileDataEditor::_tile_set_changed_deferred_update() { + if (_tile_set_changed_update_needed) { + _tile_set_changed(); + _tile_set_changed_update_needed = false; + } +} + +TileData *TileDataEditor::_get_tile_data(TileMapCell p_cell) { + ERR_FAIL_COND_V(!tile_set.is_valid(), nullptr); + ERR_FAIL_COND_V(!tile_set->has_source(p_cell.source_id), nullptr); + + TileData *td = nullptr; + TileSetSource *source = *tile_set->get_source(p_cell.source_id); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + if (atlas_source) { + ERR_FAIL_COND_V(!atlas_source->has_tile(p_cell.get_atlas_coords()), nullptr); + ERR_FAIL_COND_V(!atlas_source->has_alternative_tile(p_cell.get_atlas_coords(), p_cell.alternative_tile), nullptr); + td = Object::cast_to<TileData>(atlas_source->get_tile_data(p_cell.get_atlas_coords(), p_cell.alternative_tile)); + } + + return td; +} + +void TileDataEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_tile_set_changed_deferred_update"), &TileDataEditor::_tile_set_changed_deferred_update); + + ADD_SIGNAL(MethodInfo("needs_redraw")); +} + +void TileDataEditor::set_tile_set(Ref<TileSet> p_tile_set) { + if (tile_set.is_valid()) { + tile_set->disconnect("changed", callable_mp(this, &TileDataEditor::_tile_set_changed_plan_update)); + } + tile_set = p_tile_set; + if (tile_set.is_valid()) { + tile_set->connect("changed", callable_mp(this, &TileDataEditor::_tile_set_changed_plan_update)); + } + _tile_set_changed_plan_update(); +} + +bool DummyObject::_set(const StringName &p_name, const Variant &p_value) { + if (properties.has(p_name)) { + properties[p_name] = p_value; + return true; + } + return false; +} + +bool DummyObject::_get(const StringName &p_name, Variant &r_ret) const { + if (properties.has(p_name)) { + r_ret = properties[p_name]; + return true; + } + return false; +} + +bool DummyObject::has_dummy_property(StringName p_name) { + return properties.has(p_name); +} + +void DummyObject::add_dummy_property(StringName p_name) { + ERR_FAIL_COND(properties.has(p_name)); + properties[p_name] = Variant(); +} + +void DummyObject::remove_dummy_property(StringName p_name) { + ERR_FAIL_COND(!properties.has(p_name)); + properties.erase(p_name); +} + +void DummyObject::clear_dummy_properties() { + properties.clear(); +} + +void GenericTilePolygonEditor::_base_control_draw() { + ERR_FAIL_COND(!tile_set.is_valid()); + + real_t grab_threshold = EDITOR_GET("editors/polygon_editor/point_grab_radius"); + + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + const Ref<Texture2D> handle = get_theme_icon(SNAME("EditorPathSharpHandle"), SNAME("EditorIcons")); + const Ref<Texture2D> add_handle = get_theme_icon(SNAME("EditorHandleAdd"), SNAME("EditorIcons")); + const Ref<StyleBox> focus_stylebox = get_theme_stylebox(SNAME("Focus"), SNAME("EditorStyles")); + + // Draw the focus rectangle. + if (base_control->has_focus()) { + base_control->draw_style_box(focus_stylebox, Rect2(Vector2(), base_control->get_size())); + } + + // Draw tile-related things. + Size2 tile_size = tile_set->get_tile_size(); + + Transform2D xform; + xform.set_origin(base_control->get_size() / 2 + panning); + xform.set_scale(Vector2(editor_zoom_widget->get_zoom(), editor_zoom_widget->get_zoom())); + base_control->draw_set_transform_matrix(xform); + + // Draw the tile shape filled. + Transform2D tile_xform; + tile_xform.set_scale(tile_size); + tile_set->draw_tile_shape(base_control, tile_xform, Color(1.0, 1.0, 1.0, 0.3), true); + + // Draw the background. + if (background_texture.is_valid()) { + base_control->draw_texture_rect_region(background_texture, Rect2(-background_region.size / 2 - background_offset, background_region.size), background_region, background_modulate, background_transpose); + } + + // Draw the polygons. + for (unsigned int i = 0; i < polygons.size(); i++) { + const Vector<Vector2> &polygon = polygons[i]; + Color color = polygon_color; + if (!in_creation_polygon.is_empty()) { + color = color.darkened(0.3); + } + color.a = 0.5; + Vector<Color> v_color; + v_color.push_back(color); + base_control->draw_polygon(polygon, v_color); + + color.a = 0.7; + for (int j = 0; j < polygon.size(); j++) { + base_control->draw_line(polygon[j], polygon[(j + 1) % polygon.size()], color); + } + } + + // Draw the polygon in creation. + if (!in_creation_polygon.is_empty()) { + for (int i = 0; i < in_creation_polygon.size() - 1; i++) { + base_control->draw_line(in_creation_polygon[i], in_creation_polygon[i + 1], Color(1.0, 1.0, 1.0)); + } + } + + Point2 in_creation_point = xform.affine_inverse().xform(base_control->get_local_mouse_position()); + float in_creation_distance = grab_threshold * 2.0; + _snap_to_tile_shape(in_creation_point, in_creation_distance, grab_threshold / editor_zoom_widget->get_zoom()); + if (button_pixel_snap->is_pressed()) { + _snap_to_half_pixel(in_creation_point); + } + + if (drag_type == DRAG_TYPE_CREATE_POINT && !in_creation_polygon.is_empty()) { + base_control->draw_line(in_creation_polygon[in_creation_polygon.size() - 1], in_creation_point, Color(1.0, 1.0, 1.0)); + } + + // Draw the handles. + int tinted_polygon_index = -1; + int tinted_point_index = -1; + if (drag_type == DRAG_TYPE_DRAG_POINT) { + tinted_polygon_index = drag_polygon_index; + tinted_point_index = drag_point_index; + } else if (hovered_point_index >= 0) { + tinted_polygon_index = hovered_polygon_index; + tinted_point_index = hovered_point_index; + } + + base_control->draw_set_transform_matrix(Transform2D()); + if (!in_creation_polygon.is_empty()) { + for (int i = 0; i < in_creation_polygon.size(); i++) { + base_control->draw_texture(handle, xform.xform(in_creation_polygon[i]) - handle->get_size() / 2); + } + } else { + for (int i = 0; i < (int)polygons.size(); i++) { + const Vector<Vector2> &polygon = polygons[i]; + for (int j = 0; j < polygon.size(); j++) { + const Color modulate = (tinted_polygon_index == i && tinted_point_index == j) ? Color(0.5, 1, 2) : Color(1, 1, 1); + base_control->draw_texture(handle, xform.xform(polygon[j]) - handle->get_size() / 2, modulate); + } + } + } + + // Draw the text on top of the selected point. + if (tinted_polygon_index >= 0) { + Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label")); + int font_size = get_theme_font_size(SNAME("font_size"), SNAME("Label")); + String text = multiple_polygon_mode ? vformat("%d:%d", tinted_polygon_index, tinted_point_index) : vformat("%d", tinted_point_index); + Size2 text_size = font->get_string_size(text, font_size); + base_control->draw_string(font, xform.xform(polygons[tinted_polygon_index][tinted_point_index]) - text_size * 0.5, text, HALIGN_LEFT, -1, font_size, Color(1.0, 1.0, 1.0, 0.5)); + } + + if (drag_type == DRAG_TYPE_CREATE_POINT) { + base_control->draw_texture(handle, xform.xform(in_creation_point) - handle->get_size() / 2, Color(0.5, 1, 2)); + } + + // Draw the point creation preview in edit mode. + if (hovered_segment_index >= 0) { + base_control->draw_texture(add_handle, xform.xform(hovered_segment_point) - add_handle->get_size() / 2); + } + + // Draw the tile shape line. + base_control->draw_set_transform_matrix(xform); + tile_set->draw_tile_shape(base_control, tile_xform, grid_color, false); + base_control->draw_set_transform_matrix(Transform2D()); +} + +void GenericTilePolygonEditor::_center_view() { + panning = Vector2(); + base_control->update(); + button_center_view->set_disabled(true); +} + +void GenericTilePolygonEditor::_zoom_changed() { + base_control->update(); +} + +void GenericTilePolygonEditor::_advanced_menu_item_pressed(int p_item_pressed) { + UndoRedo *undo_redo = use_undo_redo ? editor_undo_redo : memnew(UndoRedo); + switch (p_item_pressed) { + case RESET_TO_DEFAULT_TILE: { + undo_redo->create_action(TTR("Reset Polygons")); + undo_redo->add_do_method(this, "clear_polygons"); + Vector<Vector2> polygon = tile_set->get_tile_shape_polygon(); + for (int i = 0; i < polygon.size(); i++) { + polygon.write[i] = polygon[i] * tile_set->get_tile_size(); + } + undo_redo->add_do_method(this, "add_polygon", polygon); + undo_redo->add_do_method(base_control, "update"); + undo_redo->add_do_method(this, "emit_signal", "polygons_changed"); + undo_redo->add_undo_method(this, "clear_polygons"); + for (unsigned int i = 0; i < polygons.size(); i++) { + undo_redo->add_undo_method(this, "add_polygon", polygons[i]); + } + undo_redo->add_undo_method(base_control, "update"); + undo_redo->add_undo_method(this, "emit_signal", "polygons_changed"); + undo_redo->commit_action(true); + } break; + case CLEAR_TILE: { + undo_redo->create_action(TTR("Clear Polygons")); + undo_redo->add_do_method(this, "clear_polygons"); + undo_redo->add_do_method(base_control, "update"); + undo_redo->add_do_method(this, "emit_signal", "polygons_changed"); + undo_redo->add_undo_method(this, "clear_polygons"); + for (unsigned int i = 0; i < polygons.size(); i++) { + undo_redo->add_undo_method(this, "add_polygon", polygons[i]); + } + undo_redo->add_undo_method(base_control, "update"); + undo_redo->add_undo_method(this, "emit_signal", "polygons_changed"); + undo_redo->commit_action(true); + } break; + case ROTATE_RIGHT: + case ROTATE_LEFT: + case FLIP_HORIZONTALLY: + case FLIP_VERTICALLY: { + undo_redo->create_action(TTR("Rotate Polygons Left")); + for (unsigned int i = 0; i < polygons.size(); i++) { + Vector<Point2> new_polygon; + for (int point_index = 0; point_index < polygons[i].size(); point_index++) { + Vector2 point = polygons[i][point_index]; + switch (p_item_pressed) { + case ROTATE_RIGHT: { + point = Vector2(-point.y, point.x); + } break; + case ROTATE_LEFT: { + point = Vector2(point.y, -point.x); + } break; + case FLIP_HORIZONTALLY: { + point = Vector2(-point.x, point.y); + } break; + case FLIP_VERTICALLY: { + point = Vector2(point.x, -point.y); + } break; + default: + break; + } + new_polygon.push_back(point); + } + undo_redo->add_do_method(this, "set_polygon", i, new_polygon); + } + undo_redo->add_do_method(base_control, "update"); + undo_redo->add_do_method(this, "emit_signal", "polygons_changed"); + for (unsigned int i = 0; i < polygons.size(); i++) { + undo_redo->add_undo_method(this, "set_polygon", polygons[i]); + } + undo_redo->add_undo_method(base_control, "update"); + undo_redo->add_undo_method(this, "emit_signal", "polygons_changed"); + undo_redo->commit_action(true); + } break; + default: + break; + } + if (!use_undo_redo) { + memdelete(undo_redo); + } +} + +void GenericTilePolygonEditor::_grab_polygon_point(Vector2 p_pos, const Transform2D &p_polygon_xform, int &r_polygon_index, int &r_point_index) { + const real_t grab_threshold = EDITOR_GET("editors/polygon_editor/point_grab_radius"); + r_polygon_index = -1; + r_point_index = -1; + float closest_distance = grab_threshold + 1.0; + for (unsigned int i = 0; i < polygons.size(); i++) { + const Vector<Vector2> &polygon = polygons[i]; + for (int j = 0; j < polygon.size(); j++) { + float distance = p_pos.distance_to(p_polygon_xform.xform(polygon[j])); + if (distance < grab_threshold && distance < closest_distance) { + r_polygon_index = i; + r_point_index = j; + closest_distance = distance; + } + } + } +} + +void GenericTilePolygonEditor::_grab_polygon_segment_point(Vector2 p_pos, const Transform2D &p_polygon_xform, int &r_polygon_index, int &r_segment_index, Vector2 &r_point) { + const real_t grab_threshold = EDITOR_GET("editors/polygon_editor/point_grab_radius"); + + Point2 point = p_polygon_xform.affine_inverse().xform(p_pos); + r_polygon_index = -1; + r_segment_index = -1; + float closest_distance = grab_threshold * 2.0; + for (unsigned int i = 0; i < polygons.size(); i++) { + const Vector<Vector2> &polygon = polygons[i]; + for (int j = 0; j < polygon.size(); j++) { + Vector2 segment[2] = { polygon[j], polygon[(j + 1) % polygon.size()] }; + Vector2 closest_point = Geometry2D::get_closest_point_to_segment(point, segment); + float distance = closest_point.distance_to(point); + if (distance < grab_threshold / editor_zoom_widget->get_zoom() && distance < closest_distance) { + r_polygon_index = i; + r_segment_index = j; + r_point = closest_point; + closest_distance = distance; + } + } + } +} + +void GenericTilePolygonEditor::_snap_to_tile_shape(Point2 &r_point, float &r_current_snapped_dist, float p_snap_dist) { + ERR_FAIL_COND(!tile_set.is_valid()); + + Vector<Point2> polygon = tile_set->get_tile_shape_polygon(); + for (int i = 0; i < polygon.size(); i++) { + polygon.write[i] = polygon[i] * tile_set->get_tile_size(); + } + Point2 snapped_point = r_point; + + // Snap to polygon vertices. + bool snapped = false; + for (int i = 0; i < polygon.size(); i++) { + float distance = r_point.distance_to(polygon[i]); + if (distance < p_snap_dist && distance < r_current_snapped_dist) { + snapped_point = polygon[i]; + r_current_snapped_dist = distance; + snapped = true; + } + } + + // Snap to edges if we did not snap to vertices. + if (!snapped) { + for (int i = 0; i < polygon.size(); i++) { + Point2 segment[2] = { polygon[i], polygon[(i + 1) % polygon.size()] }; + Point2 point = Geometry2D::get_closest_point_to_segment(r_point, segment); + float distance = r_point.distance_to(point); + if (distance < p_snap_dist && distance < r_current_snapped_dist) { + snapped_point = point; + r_current_snapped_dist = distance; + } + } + } + + r_point = snapped_point; +} + +void GenericTilePolygonEditor::_snap_to_half_pixel(Point2 &r_point) { + r_point = (r_point * 2).round() / 2.0; +} + +void GenericTilePolygonEditor::_base_control_gui_input(Ref<InputEvent> p_event) { + UndoRedo *undo_redo = use_undo_redo ? editor_undo_redo : memnew(UndoRedo); + real_t grab_threshold = EDITOR_GET("editors/polygon_editor/point_grab_radius"); + + hovered_polygon_index = -1; + hovered_point_index = -1; + hovered_segment_index = -1; + hovered_segment_point = Vector2(); + + Transform2D xform; + xform.set_origin(base_control->get_size() / 2 + panning); + xform.set_scale(Vector2(editor_zoom_widget->get_zoom(), editor_zoom_widget->get_zoom())); + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + if (drag_type == DRAG_TYPE_DRAG_POINT) { + ERR_FAIL_INDEX(drag_polygon_index, (int)polygons.size()); + ERR_FAIL_INDEX(drag_point_index, polygons[drag_polygon_index].size()); + Point2 point = xform.affine_inverse().xform(mm->get_position()); + float distance = grab_threshold * 2.0; + _snap_to_tile_shape(point, distance, grab_threshold / editor_zoom_widget->get_zoom()); + if (button_pixel_snap->is_pressed()) { + _snap_to_half_pixel(point); + } + polygons[drag_polygon_index].write[drag_point_index] = point; + } else if (drag_type == DRAG_TYPE_PAN) { + panning += mm->get_position() - drag_last_pos; + drag_last_pos = mm->get_position(); + button_center_view->set_disabled(panning.is_equal_approx(Vector2())); + } else { + // Update hovered point. + _grab_polygon_point(mm->get_position(), xform, hovered_polygon_index, hovered_point_index); + + // If we have no hovered point, check if we hover a segment. + if (hovered_point_index == -1) { + _grab_polygon_segment_point(mm->get_position(), xform, hovered_polygon_index, hovered_segment_index, hovered_segment_point); + } + } + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + if (mb->get_button_index() == MouseButton::WHEEL_UP && mb->is_ctrl_pressed()) { + editor_zoom_widget->set_zoom_by_increments(1); + _zoom_changed(); + accept_event(); + } else if (mb->get_button_index() == MouseButton::WHEEL_DOWN && mb->is_ctrl_pressed()) { + editor_zoom_widget->set_zoom_by_increments(-1); + _zoom_changed(); + accept_event(); + } else if (mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { + if (tools_button_group->get_pressed_button() != button_create) { + in_creation_polygon.clear(); + } + if (tools_button_group->get_pressed_button() == button_create) { + // Create points. + if (in_creation_polygon.size() >= 3 && mb->get_position().distance_to(xform.xform(in_creation_polygon[0])) < grab_threshold) { + // Closes and create polygon. + if (!multiple_polygon_mode) { + clear_polygons(); + } + int added = add_polygon(in_creation_polygon); + + in_creation_polygon.clear(); + button_edit->set_pressed(true); + undo_redo->create_action(TTR("Edit Polygons")); + if (!multiple_polygon_mode) { + undo_redo->add_do_method(this, "clear_polygons"); + } + undo_redo->add_do_method(this, "add_polygon", in_creation_polygon); + undo_redo->add_do_method(base_control, "update"); + undo_redo->add_undo_method(this, "remove_polygon", added); + undo_redo->add_undo_method(base_control, "update"); + undo_redo->commit_action(false); + emit_signal(SNAME("polygons_changed")); + } else { + // Create a new point. + drag_type = DRAG_TYPE_CREATE_POINT; + } + } else if (tools_button_group->get_pressed_button() == button_edit) { + // Edit points. + int closest_polygon; + int closest_point; + _grab_polygon_point(mb->get_position(), xform, closest_polygon, closest_point); + if (closest_polygon >= 0) { + drag_type = DRAG_TYPE_DRAG_POINT; + drag_polygon_index = closest_polygon; + drag_point_index = closest_point; + drag_old_polygon = polygons[drag_polygon_index]; + } else { + // Create a point. + Vector2 point_to_create; + _grab_polygon_segment_point(mb->get_position(), xform, closest_polygon, closest_point, point_to_create); + if (closest_polygon >= 0) { + polygons[closest_polygon].insert(closest_point + 1, point_to_create); + drag_type = DRAG_TYPE_DRAG_POINT; + drag_polygon_index = closest_polygon; + drag_point_index = closest_point + 1; + drag_old_polygon = polygons[closest_polygon]; + } + } + } else if (tools_button_group->get_pressed_button() == button_delete) { + // Remove point. + int closest_polygon; + int closest_point; + _grab_polygon_point(mb->get_position(), xform, closest_polygon, closest_point); + if (closest_polygon >= 0) { + PackedVector2Array old_polygon = polygons[closest_polygon]; + polygons[closest_polygon].remove(closest_point); + undo_redo->create_action(TTR("Edit Polygons")); + if (polygons[closest_polygon].size() < 3) { + remove_polygon(closest_polygon); + undo_redo->add_do_method(this, "remove_polygon", closest_polygon); + undo_redo->add_undo_method(this, "add_polygon", old_polygon, closest_polygon); + } else { + undo_redo->add_do_method(this, "set_polygon", closest_polygon, polygons[closest_polygon]); + undo_redo->add_undo_method(this, "set_polygon", closest_polygon, old_polygon); + } + undo_redo->add_do_method(base_control, "update"); + undo_redo->add_undo_method(base_control, "update"); + undo_redo->commit_action(false); + emit_signal(SNAME("polygons_changed")); + } + } + } else { + if (drag_type == DRAG_TYPE_DRAG_POINT) { + undo_redo->create_action(TTR("Edit Polygons")); + undo_redo->add_do_method(this, "set_polygon", drag_polygon_index, polygons[drag_polygon_index]); + undo_redo->add_do_method(base_control, "update"); + undo_redo->add_undo_method(this, "set_polygon", drag_polygon_index, drag_old_polygon); + undo_redo->add_undo_method(base_control, "update"); + undo_redo->commit_action(false); + emit_signal(SNAME("polygons_changed")); + } else if (drag_type == DRAG_TYPE_CREATE_POINT) { + Point2 point = xform.affine_inverse().xform(mb->get_position()); + float distance = grab_threshold * 2; + _snap_to_tile_shape(point, distance, grab_threshold / editor_zoom_widget->get_zoom()); + if (button_pixel_snap->is_pressed()) { + _snap_to_half_pixel(point); + } + in_creation_polygon.push_back(point); + } + drag_type = DRAG_TYPE_NONE; + drag_point_index = -1; + } + + } else if (mb->get_button_index() == MouseButton::RIGHT) { + if (mb->is_pressed()) { + if (tools_button_group->get_pressed_button() == button_edit) { + // Remove point or pan. + int closest_polygon; + int closest_point; + _grab_polygon_point(mb->get_position(), xform, closest_polygon, closest_point); + if (closest_polygon >= 0) { + PackedVector2Array old_polygon = polygons[closest_polygon]; + polygons[closest_polygon].remove(closest_point); + undo_redo->create_action(TTR("Edit Polygons")); + if (polygons[closest_polygon].size() < 3) { + remove_polygon(closest_polygon); + undo_redo->add_do_method(this, "remove_polygon", closest_polygon); + undo_redo->add_undo_method(this, "add_polygon", old_polygon, closest_polygon); + } else { + undo_redo->add_do_method(this, "set_polygon", closest_polygon, polygons[closest_polygon]); + undo_redo->add_undo_method(this, "set_polygon", closest_polygon, old_polygon); + } + undo_redo->add_do_method(base_control, "update"); + undo_redo->add_undo_method(base_control, "update"); + undo_redo->commit_action(false); + emit_signal(SNAME("polygons_changed")); + } else { + drag_type = DRAG_TYPE_PAN; + drag_last_pos = mb->get_position(); + } + } else { + drag_type = DRAG_TYPE_PAN; + drag_last_pos = mb->get_position(); + } + } else { + drag_type = DRAG_TYPE_NONE; + } + } else if (mb->get_button_index() == MouseButton::MIDDLE) { + if (mb->is_pressed()) { + drag_type = DRAG_TYPE_PAN; + drag_last_pos = mb->get_position(); + } else { + drag_type = DRAG_TYPE_NONE; + } + } + } + + base_control->update(); + + if (!use_undo_redo) { + memdelete(undo_redo); + } +} + +void GenericTilePolygonEditor::set_use_undo_redo(bool p_use_undo_redo) { + use_undo_redo = p_use_undo_redo; +} + +void GenericTilePolygonEditor::set_tile_set(Ref<TileSet> p_tile_set) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + if (tile_set == p_tile_set) { + return; + } + + // Set the default tile shape + clear_polygons(); + if (p_tile_set.is_valid()) { + Vector<Vector2> polygon = p_tile_set->get_tile_shape_polygon(); + for (int i = 0; i < polygon.size(); i++) { + polygon.write[i] = polygon[i] * p_tile_set->get_tile_size(); + } + add_polygon(polygon); + } + + tile_set = p_tile_set; + + // Set the default zoom value. + int default_control_y_size = 200 * EDSCALE; + Vector2 zoomed_tile = editor_zoom_widget->get_zoom() * tile_set->get_tile_size(); + while (zoomed_tile.y < default_control_y_size) { + editor_zoom_widget->set_zoom_by_increments(6, false); + zoomed_tile = editor_zoom_widget->get_zoom() * tile_set->get_tile_size(); + } + while (zoomed_tile.y > default_control_y_size) { + editor_zoom_widget->set_zoom_by_increments(-6, false); + zoomed_tile = editor_zoom_widget->get_zoom() * tile_set->get_tile_size(); + } + editor_zoom_widget->set_zoom_by_increments(-6, false); + _zoom_changed(); +} + +void GenericTilePolygonEditor::set_background(Ref<Texture2D> p_texture, Rect2 p_region, Vector2 p_offset, bool p_flip_h, bool p_flip_v, bool p_transpose, Color p_modulate) { + background_texture = p_texture; + background_region = p_region; + background_offset = p_offset; + background_h_flip = p_flip_h; + background_v_flip = p_flip_v; + background_transpose = p_transpose; + background_modulate = p_modulate; + base_control->update(); +} + +int GenericTilePolygonEditor::get_polygon_count() { + return polygons.size(); +} + +int GenericTilePolygonEditor::add_polygon(Vector<Point2> p_polygon, int p_index) { + ERR_FAIL_COND_V(p_polygon.size() < 3, -1); + ERR_FAIL_COND_V(!multiple_polygon_mode && polygons.size() >= 1, -1); + + if (p_index < 0) { + polygons.push_back(p_polygon); + base_control->update(); + button_edit->set_pressed(true); + return polygons.size() - 1; + } else { + polygons.insert(p_index, p_polygon); + button_edit->set_pressed(true); + base_control->update(); + return p_index; + } +} + +void GenericTilePolygonEditor::remove_polygon(int p_index) { + ERR_FAIL_INDEX(p_index, (int)polygons.size()); + polygons.remove(p_index); + + if (polygons.size() == 0) { + button_create->set_pressed(true); + } + base_control->update(); +} + +void GenericTilePolygonEditor::clear_polygons() { + polygons.clear(); + base_control->update(); +} + +void GenericTilePolygonEditor::set_polygon(int p_polygon_index, Vector<Point2> p_polygon) { + ERR_FAIL_INDEX(p_polygon_index, (int)polygons.size()); + ERR_FAIL_COND(p_polygon.size() < 3); + polygons[p_polygon_index] = p_polygon; + button_edit->set_pressed(true); + base_control->update(); +} + +Vector<Point2> GenericTilePolygonEditor::get_polygon(int p_polygon_index) { + ERR_FAIL_INDEX_V(p_polygon_index, (int)polygons.size(), Vector<Point2>()); + return polygons[p_polygon_index]; +} + +void GenericTilePolygonEditor::set_polygons_color(Color p_color) { + polygon_color = p_color; + base_control->update(); +} + +void GenericTilePolygonEditor::set_multiple_polygon_mode(bool p_multiple_polygon_mode) { + multiple_polygon_mode = p_multiple_polygon_mode; +} + +void GenericTilePolygonEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_READY: + button_create->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("CurveCreate"), SNAME("EditorIcons"))); + button_edit->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("CurveEdit"), SNAME("EditorIcons"))); + button_delete->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("CurveDelete"), SNAME("EditorIcons"))); + button_center_view->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("CenterView"), SNAME("EditorIcons"))); + button_pixel_snap->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Snap"), SNAME("EditorIcons"))); + button_advanced_menu->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("GuiTabMenuHl"), SNAME("EditorIcons"))); + + PopupMenu *p = button_advanced_menu->get_popup(); + p->set_item_icon(p->get_item_index(ROTATE_RIGHT), get_theme_icon(SNAME("RotateRight"), SNAME("EditorIcons"))); + p->set_item_icon(p->get_item_index(ROTATE_LEFT), get_theme_icon(SNAME("RotateLeft"), SNAME("EditorIcons"))); + p->set_item_icon(p->get_item_index(FLIP_HORIZONTALLY), get_theme_icon(SNAME("MirrorX"), SNAME("EditorIcons"))); + p->set_item_icon(p->get_item_index(FLIP_VERTICALLY), get_theme_icon(SNAME("MirrorY"), SNAME("EditorIcons"))); + break; + } +} + +void GenericTilePolygonEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_polygon_count"), &GenericTilePolygonEditor::get_polygon_count); + ClassDB::bind_method(D_METHOD("add_polygon", "polygon", "index"), &GenericTilePolygonEditor::add_polygon, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("remove_polygon", "index"), &GenericTilePolygonEditor::remove_polygon); + ClassDB::bind_method(D_METHOD("clear_polygons"), &GenericTilePolygonEditor::clear_polygons); + ClassDB::bind_method(D_METHOD("set_polygon", "index", "polygon"), &GenericTilePolygonEditor::set_polygon); + ClassDB::bind_method(D_METHOD("get_polygon", "index"), &GenericTilePolygonEditor::set_polygon); + + ADD_SIGNAL(MethodInfo("polygons_changed")); +} + +GenericTilePolygonEditor::GenericTilePolygonEditor() { + toolbar = memnew(HBoxContainer); + add_child(toolbar); + + tools_button_group.instantiate(); + + button_create = memnew(Button); + button_create->set_flat(true); + button_create->set_toggle_mode(true); + button_create->set_button_group(tools_button_group); + button_create->set_pressed(true); + button_create->set_tooltip(TTR("Add polygon tool")); + toolbar->add_child(button_create); + + button_edit = memnew(Button); + button_edit->set_flat(true); + button_edit->set_toggle_mode(true); + button_edit->set_button_group(tools_button_group); + button_edit->set_tooltip(TTR("Edit points tool")); + toolbar->add_child(button_edit); + + button_delete = memnew(Button); + button_delete->set_flat(true); + button_delete->set_toggle_mode(true); + button_delete->set_button_group(tools_button_group); + button_delete->set_tooltip(TTR("Delete points tool")); + toolbar->add_child(button_delete); + + button_advanced_menu = memnew(MenuButton); + button_advanced_menu->set_flat(true); + button_advanced_menu->set_toggle_mode(true); + button_advanced_menu->get_popup()->add_item(TTR("Reset to default tile shape"), RESET_TO_DEFAULT_TILE); + button_advanced_menu->get_popup()->add_item(TTR("Clear"), CLEAR_TILE); + button_advanced_menu->get_popup()->add_separator(); + button_advanced_menu->get_popup()->add_icon_item(get_theme_icon(SNAME("RotateRight"), SNAME("EditorIcons")), TTR("Rotate Right"), ROTATE_RIGHT); + button_advanced_menu->get_popup()->add_icon_item(get_theme_icon(SNAME("RotateLeft"), SNAME("EditorIcons")), TTR("Rotate Left"), ROTATE_LEFT); + button_advanced_menu->get_popup()->add_icon_item(get_theme_icon(SNAME("MirrorX"), SNAME("EditorIcons")), TTR("Flip Horizontally"), FLIP_HORIZONTALLY); + button_advanced_menu->get_popup()->add_icon_item(get_theme_icon(SNAME("MirrorY"), SNAME("EditorIcons")), TTR("Flip Vertically"), FLIP_VERTICALLY); + button_advanced_menu->get_popup()->connect("id_pressed", callable_mp(this, &GenericTilePolygonEditor::_advanced_menu_item_pressed)); + button_advanced_menu->set_focus_mode(FOCUS_ALL); + toolbar->add_child(button_advanced_menu); + + toolbar->add_child(memnew(VSeparator)); + + button_pixel_snap = memnew(Button); + button_pixel_snap->set_flat(true); + button_pixel_snap->set_toggle_mode(true); + button_pixel_snap->set_pressed(true); + button_pixel_snap->set_tooltip(TTR("Snap to half-pixel")); + toolbar->add_child(button_pixel_snap); + + Control *root = memnew(Control); + root->set_h_size_flags(Control::SIZE_EXPAND_FILL); + root->set_custom_minimum_size(Size2(0, 200 * EDSCALE)); + root->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + add_child(root); + + panel = memnew(Panel); + panel->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + panel->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + root->add_child(panel); + + base_control = memnew(Control); + base_control->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + base_control->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + base_control->connect("draw", callable_mp(this, &GenericTilePolygonEditor::_base_control_draw)); + base_control->connect("gui_input", callable_mp(this, &GenericTilePolygonEditor::_base_control_gui_input)); + base_control->set_clip_contents(true); + base_control->set_focus_mode(Control::FOCUS_CLICK); + root->add_child(base_control); + + editor_zoom_widget = memnew(EditorZoomWidget); + editor_zoom_widget->set_position(Vector2(5, 5)); + editor_zoom_widget->connect("zoom_changed", callable_mp(this, &GenericTilePolygonEditor::_zoom_changed).unbind(1)); + root->add_child(editor_zoom_widget); + + button_center_view = memnew(Button); + button_center_view->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("CenterView"), SNAME("EditorIcons"))); + button_center_view->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT, Control::PRESET_MODE_MINSIZE, 5); + button_center_view->connect("pressed", callable_mp(this, &GenericTilePolygonEditor::_center_view)); + button_center_view->set_flat(true); + button_center_view->set_disabled(true); + root->add_child(button_center_view); +} + +void TileDataDefaultEditor::_property_value_changed(StringName p_property, Variant p_value, StringName p_field) { + ERR_FAIL_COND(!dummy_object); + dummy_object->set(p_property, p_value); +} + +Variant TileDataDefaultEditor::_get_painted_value() { + ERR_FAIL_COND_V(!dummy_object, Variant()); + return dummy_object->get(property); +} + +void TileDataDefaultEditor::_set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + Variant value = tile_data->get(property); + dummy_object->set(property, value); + if (property_editor) { + property_editor->update_property(); + } +} + +void TileDataDefaultEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + tile_data->set(property, p_value); +} + +Variant TileDataDefaultEditor::_get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND_V(!tile_data, Variant()); + return tile_data->get(property); +} + +void TileDataDefaultEditor::_setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value) { + for (const KeyValue<TileMapCell, Variant> &E : p_previous_values) { + Vector2i coords = E.key.get_atlas_coords(); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/%s", coords.x, coords.y, E.key.alternative_tile, property), E.value); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/%s", coords.x, coords.y, E.key.alternative_tile, property), p_new_value); + } +} + +void TileDataDefaultEditor::forward_draw_over_atlas(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform) { + if (drag_type == DRAG_TYPE_PAINT_RECT) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + + p_canvas_item->draw_set_transform_matrix(p_transform); + + Rect2i rect; + rect.set_position(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_pos)); + rect.set_end(p_tile_atlas_view->get_atlas_tile_coords_at_pos(p_transform.affine_inverse().xform(p_canvas_item->get_local_mouse_position()))); + rect = rect.abs(); + + Set<TileMapCell> edited; + for (int x = rect.get_position().x; x <= rect.get_end().x; x++) { + for (int y = rect.get_position().y; y <= rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + edited.insert(cell); + } + } + } + + for (Set<TileMapCell>::Element *E = edited.front(); E; E = E->next()) { + Vector2i coords = E->get().get_atlas_coords(); + p_canvas_item->draw_rect(p_tile_set_atlas_source->get_tile_texture_region(coords), selection_color, false); + } + p_canvas_item->draw_set_transform_matrix(Transform2D()); + } +}; + +void TileDataDefaultEditor::forward_draw_over_alternatives(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform){ + +}; + +void TileDataDefaultEditor::forward_painting_atlas_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, const Ref<InputEvent> &p_event) { + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + if (drag_type == DRAG_TYPE_PAINT) { + Vector<Vector2i> line = Geometry2D::bresenham_line(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_last_pos), p_tile_atlas_view->get_atlas_tile_coords_at_pos(mm->get_position())); + for (int i = 0; i < line.size(); i++) { + Vector2i coords = p_tile_set_atlas_source->get_tile_at_coords(line[i]); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + if (!drag_modified.has(cell)) { + drag_modified[cell] = _get_value(p_tile_set_atlas_source, coords, 0); + } + _set_value(p_tile_set_atlas_source, coords, 0, drag_painted_value); + } + } + drag_last_pos = mm->get_position(); + } + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + if (mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { + if (picker_button->is_pressed()) { + Vector2i coords = p_tile_atlas_view->get_atlas_tile_coords_at_pos(mb->get_position()); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + _set_painted_value(p_tile_set_atlas_source, coords, 0); + picker_button->set_pressed(false); + } + } else if (mb->is_ctrl_pressed()) { + drag_type = DRAG_TYPE_PAINT_RECT; + drag_modified.clear(); + drag_painted_value = _get_painted_value(); + drag_start_pos = mb->get_position(); + } else { + drag_type = DRAG_TYPE_PAINT; + drag_modified.clear(); + drag_painted_value = _get_painted_value(); + Vector2i coords = p_tile_atlas_view->get_atlas_tile_coords_at_pos(mb->get_position()); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + drag_modified[cell] = _get_value(p_tile_set_atlas_source, coords, 0); + _set_value(p_tile_set_atlas_source, coords, 0, drag_painted_value); + } + drag_last_pos = mb->get_position(); + } + } else { + if (drag_type == DRAG_TYPE_PAINT_RECT) { + Rect2i rect; + rect.set_position(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_pos)); + rect.set_end(p_tile_atlas_view->get_atlas_tile_coords_at_pos(mb->get_position())); + rect = rect.abs(); + + drag_modified.clear(); + for (int x = rect.get_position().x; x <= rect.get_end().x; x++) { + for (int y = rect.get_position().y; y <= rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + drag_modified[cell] = _get_value(p_tile_set_atlas_source, coords, 0); + } + } + } + undo_redo->create_action(TTR("Painting Tiles Property")); + _setup_undo_redo_action(p_tile_set_atlas_source, drag_modified, drag_painted_value); + undo_redo->commit_action(true); + drag_type = DRAG_TYPE_NONE; + } else if (drag_type == DRAG_TYPE_PAINT) { + undo_redo->create_action(TTR("Painting Tiles Property")); + _setup_undo_redo_action(p_tile_set_atlas_source, drag_modified, drag_painted_value); + undo_redo->commit_action(false); + drag_type = DRAG_TYPE_NONE; + } + } + } + } +} + +void TileDataDefaultEditor::forward_painting_alternatives_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, const Ref<InputEvent> &p_event) { + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + if (drag_type == DRAG_TYPE_PAINT) { + Vector3i tile = p_tile_atlas_view->get_alternative_tile_at_pos(mm->get_position()); + Vector2i coords = Vector2i(tile.x, tile.y); + int alternative_tile = tile.z; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = alternative_tile; + if (!drag_modified.has(cell)) { + drag_modified[cell] = _get_value(p_tile_set_atlas_source, coords, alternative_tile); + } + _set_value(p_tile_set_atlas_source, coords, alternative_tile, drag_painted_value); + } + + drag_last_pos = mm->get_position(); + } + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + if (mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { + if (picker_button->is_pressed()) { + Vector3i tile = p_tile_atlas_view->get_alternative_tile_at_pos(mb->get_position()); + Vector2i coords = Vector2i(tile.x, tile.y); + int alternative_tile = tile.z; + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + _set_painted_value(p_tile_set_atlas_source, coords, alternative_tile); + picker_button->set_pressed(false); + } + } else { + drag_type = DRAG_TYPE_PAINT; + drag_modified.clear(); + drag_painted_value = _get_painted_value(); + + Vector3i tile = p_tile_atlas_view->get_alternative_tile_at_pos(mb->get_position()); + Vector2i coords = Vector2i(tile.x, tile.y); + int alternative_tile = tile.z; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = alternative_tile; + drag_modified[cell] = _get_value(p_tile_set_atlas_source, coords, alternative_tile); + _set_value(p_tile_set_atlas_source, coords, alternative_tile, drag_painted_value); + } + drag_last_pos = mb->get_position(); + } + } else { + undo_redo->create_action(TTR("Painting Tiles Property")); + _setup_undo_redo_action(p_tile_set_atlas_source, drag_modified, drag_painted_value); + undo_redo->commit_action(false); + drag_type = DRAG_TYPE_NONE; + } + } + } +} + +void TileDataDefaultEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + bool valid; + Variant value = tile_data->get(property, &valid); + if (!valid) { + return; + } + + if (value.get_type() == Variant::BOOL) { + Ref<Texture2D> texture = (bool)value ? tile_bool_checked : tile_bool_unchecked; + int size = MIN(tile_set->get_tile_size().x, tile_set->get_tile_size().y) / 3; + Rect2 rect = p_transform.xform(Rect2(Vector2(-size / 2, -size / 2), Vector2(size, size))); + p_canvas_item->draw_texture_rect(texture, rect); + } else if (value.get_type() == Variant::COLOR) { + int size = MIN(tile_set->get_tile_size().x, tile_set->get_tile_size().y) / 3; + Rect2 rect = p_transform.xform(Rect2(Vector2(-size / 2, -size / 2), Vector2(size, size))); + p_canvas_item->draw_rect(rect, value); + } else { + Ref<Font> font = TileSetEditor::get_singleton()->get_theme_font(SNAME("bold"), SNAME("EditorFonts")); + int font_size = TileSetEditor::get_singleton()->get_theme_font_size(SNAME("bold_size"), SNAME("EditorFonts")); + String text; + switch (value.get_type()) { + case Variant::INT: + text = vformat("%d", value); + break; + case Variant::FLOAT: + text = vformat("%.2f", value); + break; + case Variant::STRING: + case Variant::STRING_NAME: + text = value; + break; + default: + return; + break; + } + + Color color = Color(1, 1, 1); + if (p_selected) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + selection_color.set_v(0.9); + color = selection_color; + } else if (is_visible_in_tree()) { + Variant painted_value = _get_painted_value(); + bool equal = (painted_value.get_type() == Variant::FLOAT && value.get_type() == Variant::FLOAT) ? Math::is_equal_approx(float(painted_value), float(value)) : painted_value == value; + if (equal) { + color = Color(0.7, 0.7, 0.7); + } + } + + Vector2 string_size = font->get_string_size(text, font_size); + p_canvas_item->draw_string(font, p_transform.get_origin() + Vector2i(-string_size.x / 2, string_size.y / 2), text, HALIGN_CENTER, string_size.x, font_size, color, 1, Color(0, 0, 0, 1)); + } +} + +void TileDataDefaultEditor::setup_property_editor(Variant::Type p_type, String p_property, String p_label, Variant p_default_value) { + ERR_FAIL_COND_MSG(!property.is_empty(), "Cannot setup TileDataDefaultEditor twice"); + property = p_property; + + // Update everything. + if (property_editor) { + property_editor->queue_delete(); + } + + // Update the dummy object. + dummy_object->add_dummy_property(p_property); + + // Get the default value for the type. + if (p_default_value == Variant()) { + Callable::CallError error; + Variant painted_value; + Variant::construct(p_type, painted_value, nullptr, 0, error); + dummy_object->set(p_property, painted_value); + } else { + dummy_object->set(p_property, p_default_value); + } + + // Create and setup the property editor. + property_editor = EditorInspectorDefaultPlugin::get_editor_for_property(dummy_object, p_type, p_property, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT); + property_editor->set_object_and_property(dummy_object, p_property); + if (p_label.is_empty()) { + property_editor->set_label(p_property); + } else { + property_editor->set_label(p_label); + } + property_editor->connect("property_changed", callable_mp(this, &TileDataDefaultEditor::_property_value_changed).unbind(1)); + property_editor->update_property(); + add_child(property_editor); +} + +void TileDataDefaultEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: + picker_button->set_icon(get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); + tile_bool_checked = get_theme_icon(SNAME("TileChecked"), SNAME("EditorIcons")); + tile_bool_unchecked = get_theme_icon(SNAME("TileUnchecked"), SNAME("EditorIcons")); + break; + default: + break; + } +} + +TileDataDefaultEditor::TileDataDefaultEditor() { + label = memnew(Label); + label->set_text(TTR("Painting:")); + add_child(label); + + toolbar->add_child(memnew(VSeparator)); + + picker_button = memnew(Button); + picker_button->set_flat(true); + picker_button->set_toggle_mode(true); + picker_button->set_shortcut(ED_SHORTCUT("tiles_editor/picker", "Picker", Key::P)); + toolbar->add_child(picker_button); +} + +TileDataDefaultEditor::~TileDataDefaultEditor() { + toolbar->queue_delete(); + memdelete(dummy_object); +} + +void TileDataTextureOffsetEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + Vector2i tile_set_tile_size = tile_set->get_tile_size(); + Color color = Color(1.0, 0.0, 0.0); + if (p_selected) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + color = selection_color; + } + Transform2D tile_xform; + tile_xform.set_scale(tile_set_tile_size); + tile_set->draw_tile_shape(p_canvas_item, p_transform * tile_xform, color); +} + +void TileDataPositionEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + bool valid; + Variant value = tile_data->get(property, &valid); + if (!valid) { + return; + } + ERR_FAIL_COND(value.get_type() != Variant::VECTOR2I && value.get_type() != Variant::VECTOR2); + + Color color = Color(1.0, 1.0, 1.0); + if (p_selected) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + color = selection_color; + } + Ref<Texture2D> position_icon = TileSetEditor::get_singleton()->get_theme_icon(SNAME("EditorPosition"), SNAME("EditorIcons")); + p_canvas_item->draw_texture(position_icon, p_transform.xform(Vector2(value)) - position_icon->get_size() / 2, color); +} + +void TileDataYSortEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + Color color = Color(1.0, 1.0, 1.0); + if (p_selected) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + color = selection_color; + } + Ref<Texture2D> position_icon = TileSetEditor::get_singleton()->get_theme_icon(SNAME("EditorPosition"), SNAME("EditorIcons")); + p_canvas_item->draw_texture(position_icon, p_transform.xform(Vector2(0, tile_data->get_y_sort_origin())) - position_icon->get_size() / 2, color); +} + +void TileDataOcclusionShapeEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + Color color = grid_color.darkened(0.2); + if (p_selected) { + color = selection_color.darkened(0.2); + } + color.a *= 0.5; + + Vector<Color> debug_occlusion_color; + debug_occlusion_color.push_back(color); + + RenderingServer::get_singleton()->canvas_item_add_set_transform(p_canvas_item->get_canvas_item(), p_transform); + Ref<OccluderPolygon2D> occluder = tile_data->get_occluder(occlusion_layer); + if (occluder.is_valid() && occluder->get_polygon().size() >= 3) { + p_canvas_item->draw_polygon(Variant(occluder->get_polygon()), debug_occlusion_color); + } + RenderingServer::get_singleton()->canvas_item_add_set_transform(p_canvas_item->get_canvas_item(), Transform2D()); +} + +Variant TileDataOcclusionShapeEditor::_get_painted_value() { + Ref<OccluderPolygon2D> occluder_polygon; + occluder_polygon.instantiate(); + if (polygon_editor->get_polygon_count() >= 1) { + occluder_polygon->set_polygon(polygon_editor->get_polygon(0)); + } + return occluder_polygon; +} + +void TileDataOcclusionShapeEditor::_set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + + Ref<OccluderPolygon2D> occluder_polygon = tile_data->get_occluder(occlusion_layer); + polygon_editor->clear_polygons(); + if (occluder_polygon.is_valid()) { + polygon_editor->add_polygon(occluder_polygon->get_polygon()); + } + polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), p_tile_set_atlas_source->get_tile_effective_texture_offset(p_coords, p_alternative_tile), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); +} + +void TileDataOcclusionShapeEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + Ref<OccluderPolygon2D> occluder_polygon = p_value; + tile_data->set_occluder(occlusion_layer, occluder_polygon); + + polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), p_tile_set_atlas_source->get_tile_effective_texture_offset(p_coords, p_alternative_tile), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); +} + +Variant TileDataOcclusionShapeEditor::_get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND_V(!tile_data, Variant()); + return tile_data->get_occluder(occlusion_layer); +} + +void TileDataOcclusionShapeEditor::_setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value) { + for (const KeyValue<TileMapCell, Variant> &E : p_previous_values) { + Vector2i coords = E.key.get_atlas_coords(); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/occlusion_layer_%d/polygon", coords.x, coords.y, E.key.alternative_tile, occlusion_layer), E.value); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/occlusion_layer_%d/polygon", coords.x, coords.y, E.key.alternative_tile, occlusion_layer), p_new_value); + } +} + +void TileDataOcclusionShapeEditor::_tile_set_changed() { + polygon_editor->set_tile_set(tile_set); +} + +void TileDataOcclusionShapeEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + polygon_editor->set_polygons_color(get_tree()->get_debug_collisions_color()); + break; + default: + break; + } +} + +TileDataOcclusionShapeEditor::TileDataOcclusionShapeEditor() { + polygon_editor = memnew(GenericTilePolygonEditor); + add_child(polygon_editor); +} + +void TileDataCollisionEditor::_property_value_changed(StringName p_property, Variant p_value, StringName p_field) { + dummy_object->set(p_property, p_value); +} + +void TileDataCollisionEditor::_polygons_changed() { + // Update the dummy object properties and their editors. + for (int i = 0; i < polygon_editor->get_polygon_count(); i++) { + StringName one_way_property = vformat("polygon_%d_one_way", i); + StringName one_way_margin_property = vformat("polygon_%d_one_way_margin", i); + + if (!dummy_object->has_dummy_property(one_way_property)) { + dummy_object->add_dummy_property(one_way_property); + dummy_object->set(one_way_property, false); + } + + if (!dummy_object->has_dummy_property(one_way_margin_property)) { + dummy_object->add_dummy_property(one_way_margin_property); + dummy_object->set(one_way_margin_property, 1.0); + } + + if (!property_editors.has(one_way_property)) { + EditorProperty *one_way_property_editor = EditorInspectorDefaultPlugin::get_editor_for_property(dummy_object, Variant::BOOL, one_way_property, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT); + one_way_property_editor->set_object_and_property(dummy_object, one_way_property); + one_way_property_editor->set_label(one_way_property); + one_way_property_editor->connect("property_changed", callable_mp(this, &TileDataCollisionEditor::_property_value_changed).unbind(1)); + one_way_property_editor->update_property(); + add_child(one_way_property_editor); + property_editors[one_way_property] = one_way_property_editor; + } + + if (!property_editors.has(one_way_margin_property)) { + EditorProperty *one_way_margin_property_editor = EditorInspectorDefaultPlugin::get_editor_for_property(dummy_object, Variant::FLOAT, one_way_margin_property, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT); + one_way_margin_property_editor->set_object_and_property(dummy_object, one_way_margin_property); + one_way_margin_property_editor->set_label(one_way_margin_property); + one_way_margin_property_editor->connect("property_changed", callable_mp(this, &TileDataCollisionEditor::_property_value_changed).unbind(1)); + one_way_margin_property_editor->update_property(); + add_child(one_way_margin_property_editor); + property_editors[one_way_margin_property] = one_way_margin_property_editor; + } + } + + // Remove unneeded properties and their editors. + for (int i = polygon_editor->get_polygon_count(); dummy_object->has_dummy_property(vformat("polygon_%d_one_way", i)); i++) { + dummy_object->remove_dummy_property(vformat("polygon_%d_one_way", i)); + } + for (int i = polygon_editor->get_polygon_count(); dummy_object->has_dummy_property(vformat("polygon_%d_one_way_margin", i)); i++) { + dummy_object->remove_dummy_property(vformat("polygon_%d_one_way_margin", i)); + } + for (int i = polygon_editor->get_polygon_count(); property_editors.has(vformat("polygon_%d_one_way", i)); i++) { + property_editors[vformat("polygon_%d_one_way", i)]->queue_delete(); + property_editors.erase(vformat("polygon_%d_one_way", i)); + } + for (int i = polygon_editor->get_polygon_count(); property_editors.has(vformat("polygon_%d_one_way_margin", i)); i++) { + property_editors[vformat("polygon_%d_one_way_margin", i)]->queue_delete(); + property_editors.erase(vformat("polygon_%d_one_way_margin", i)); + } +} + +Variant TileDataCollisionEditor::_get_painted_value() { + Dictionary dict; + dict["linear_velocity"] = dummy_object->get("linear_velocity"); + dict["angular_velocity"] = dummy_object->get("angular_velocity"); + Array array; + for (int i = 0; i < polygon_editor->get_polygon_count(); i++) { + ERR_FAIL_COND_V(polygon_editor->get_polygon(i).size() < 3, Variant()); + Dictionary polygon_dict; + polygon_dict["points"] = polygon_editor->get_polygon(i); + polygon_dict["one_way"] = dummy_object->get(vformat("polygon_%d_one_way", i)); + polygon_dict["one_way_margin"] = dummy_object->get(vformat("polygon_%d_one_way_margin", i)); + array.push_back(polygon_dict); + } + dict["polygons"] = array; + + return dict; +} + +void TileDataCollisionEditor::_set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + + polygon_editor->clear_polygons(); + for (int i = 0; i < tile_data->get_collision_polygons_count(physics_layer); i++) { + Vector<Vector2> polygon = tile_data->get_collision_polygon_points(physics_layer, i); + if (polygon.size() >= 3) { + polygon_editor->add_polygon(polygon); + } + } + + _polygons_changed(); + dummy_object->set("linear_velocity", tile_data->get_constant_linear_velocity(physics_layer)); + dummy_object->set("angular_velocity", tile_data->get_constant_angular_velocity(physics_layer)); + for (int i = 0; i < tile_data->get_collision_polygons_count(physics_layer); i++) { + dummy_object->set(vformat("polygon_%d_one_way", i), tile_data->is_collision_polygon_one_way(physics_layer, i)); + dummy_object->set(vformat("polygon_%d_one_way_margin", i), tile_data->get_collision_polygon_one_way_margin(physics_layer, i)); + } + for (const KeyValue<StringName, EditorProperty *> &E : property_editors) { + E.value->update_property(); + } + + polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), p_tile_set_atlas_source->get_tile_effective_texture_offset(p_coords, p_alternative_tile), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); +} + +void TileDataCollisionEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + + Dictionary dict = p_value; + tile_data->set_constant_linear_velocity(physics_layer, dict["linear_velocity"]); + tile_data->set_constant_angular_velocity(physics_layer, dict["angular_velocity"]); + Array array = dict["polygons"]; + tile_data->set_collision_polygons_count(physics_layer, array.size()); + for (int i = 0; i < array.size(); i++) { + Dictionary polygon_dict = array[i]; + tile_data->set_collision_polygon_points(physics_layer, i, polygon_dict["points"]); + tile_data->set_collision_polygon_one_way(physics_layer, i, polygon_dict["one_way"]); + tile_data->set_collision_polygon_one_way_margin(physics_layer, i, polygon_dict["one_way_margin"]); + } + + polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), p_tile_set_atlas_source->get_tile_effective_texture_offset(p_coords, p_alternative_tile), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); +} + +Variant TileDataCollisionEditor::_get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND_V(!tile_data, Variant()); + + Dictionary dict; + dict["linear_velocity"] = tile_data->get_constant_linear_velocity(physics_layer); + dict["angular_velocity"] = tile_data->get_constant_angular_velocity(physics_layer); + Array array; + for (int i = 0; i < tile_data->get_collision_polygons_count(physics_layer); i++) { + Dictionary polygon_dict; + polygon_dict["points"] = tile_data->get_collision_polygon_points(physics_layer, i); + polygon_dict["one_way"] = tile_data->is_collision_polygon_one_way(physics_layer, i); + polygon_dict["one_way_margin"] = tile_data->get_collision_polygon_one_way_margin(physics_layer, i); + array.push_back(polygon_dict); + } + dict["polygons"] = array; + return dict; +} + +void TileDataCollisionEditor::_setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value) { + Array new_array = p_new_value; + for (KeyValue<TileMapCell, Variant> &E : p_previous_values) { + Array old_array = E.value; + + Vector2i coords = E.key.get_atlas_coords(); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygons_count", coords.x, coords.y, E.key.alternative_tile, physics_layer), old_array.size()); + for (int i = 0; i < old_array.size(); i++) { + Dictionary dict = old_array[i]; + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygon_%d/points", coords.x, coords.y, E.key.alternative_tile, physics_layer, i), dict["points"]); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygon_%d/one_way", coords.x, coords.y, E.key.alternative_tile, physics_layer, i), dict["one_way"]); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygon_%d/one_way_margin", coords.x, coords.y, E.key.alternative_tile, physics_layer, i), dict["one_way_margin"]); + } + + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygons_count", coords.x, coords.y, E.key.alternative_tile, physics_layer), new_array.size()); + for (int i = 0; i < new_array.size(); i++) { + Dictionary dict = new_array[i]; + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygon_%d/points", coords.x, coords.y, E.key.alternative_tile, physics_layer, i), dict["points"]); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygon_%d/one_way", coords.x, coords.y, E.key.alternative_tile, physics_layer, i), dict["one_way"]); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/physics_layer_%d/polygon_%d/one_way_margin", coords.x, coords.y, E.key.alternative_tile, physics_layer, i), dict["one_way_margin"]); + } + } +} + +void TileDataCollisionEditor::_tile_set_changed() { + polygon_editor->set_tile_set(tile_set); + _polygons_changed(); +} + +void TileDataCollisionEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + polygon_editor->set_polygons_color(get_tree()->get_debug_collisions_color()); + break; + default: + break; + } +} + +TileDataCollisionEditor::TileDataCollisionEditor() { + polygon_editor = memnew(GenericTilePolygonEditor); + polygon_editor->set_multiple_polygon_mode(true); + polygon_editor->connect("polygons_changed", callable_mp(this, &TileDataCollisionEditor::_polygons_changed)); + add_child(polygon_editor); + + dummy_object->add_dummy_property("linear_velocity"); + dummy_object->set("linear_velocity", Vector2()); + dummy_object->add_dummy_property("angular_velocity"); + dummy_object->set("angular_velocity", 0.0); + + EditorProperty *linear_velocity_editor = EditorInspectorDefaultPlugin::get_editor_for_property(dummy_object, Variant::VECTOR2, "linear_velocity", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT); + linear_velocity_editor->set_object_and_property(dummy_object, "linear_velocity"); + linear_velocity_editor->set_label("linear_velocity"); + linear_velocity_editor->connect("property_changed", callable_mp(this, &TileDataCollisionEditor::_property_value_changed).unbind(1)); + linear_velocity_editor->update_property(); + add_child(linear_velocity_editor); + property_editors["linear_velocity"] = linear_velocity_editor; + + EditorProperty *angular_velocity_editor = EditorInspectorDefaultPlugin::get_editor_for_property(dummy_object, Variant::FLOAT, "angular_velocity", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT); + angular_velocity_editor->set_object_and_property(dummy_object, "angular_velocity"); + angular_velocity_editor->set_label("angular_velocity"); + angular_velocity_editor->connect("property_changed", callable_mp(this, &TileDataCollisionEditor::_property_value_changed).unbind(1)); + angular_velocity_editor->update_property(); + add_child(angular_velocity_editor); + property_editors["angular_velocity"] = angular_velocity_editor; + + _polygons_changed(); +} + +TileDataCollisionEditor::~TileDataCollisionEditor() { + memdelete(dummy_object); +} + +void TileDataCollisionEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + // Draw all shapes. + Vector<Color> color; + if (p_selected) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + selection_color.a = 0.7; + color.push_back(selection_color); + } else { + Color debug_collision_color = p_canvas_item->get_tree()->get_debug_collisions_color(); + color.push_back(debug_collision_color); + } + + RenderingServer::get_singleton()->canvas_item_add_set_transform(p_canvas_item->get_canvas_item(), p_transform); + for (int i = 0; i < tile_data->get_collision_polygons_count(physics_layer); i++) { + Vector<Vector2> polygon = tile_data->get_collision_polygon_points(physics_layer, i); + if (polygon.size() >= 3) { + p_canvas_item->draw_polygon(polygon, color); + } + } + RenderingServer::get_singleton()->canvas_item_add_set_transform(p_canvas_item->get_canvas_item(), Transform2D()); +} + +void TileDataTerrainsEditor::_update_terrain_selector() { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Update the terrain set selector. + Vector<String> options; + options.push_back(String(TTR("No terrains")) + String(":-1")); + for (int i = 0; i < tile_set->get_terrain_sets_count(); i++) { + options.push_back(vformat("Terrain Set %d", i)); + } + terrain_set_property_editor->setup(options); + terrain_set_property_editor->update_property(); + + // Update the terrain selector. + int terrain_set = int(dummy_object->get("terrain_set")); + if (terrain_set == -1) { + terrain_property_editor->hide(); + } else { + options.clear(); + Vector<Vector<Ref<Texture2D>>> icons = tile_set->generate_terrains_icons(Size2(16, 16) * EDSCALE); + options.push_back(String(TTR("No terrain")) + String(":-1")); + for (int i = 0; i < tile_set->get_terrains_count(terrain_set); i++) { + String name = tile_set->get_terrain_name(terrain_set, i); + if (name.is_empty()) { + options.push_back(vformat("Terrain %d", i)); + } else { + options.push_back(name); + } + } + terrain_property_editor->setup(options); + terrain_property_editor->update_property(); + + // Kind of a hack to set icons. + // We could provide a way to modify that in the EditorProperty. + OptionButton *option_button = Object::cast_to<OptionButton>(terrain_property_editor->get_child(0)); + for (int terrain = 0; terrain < tile_set->get_terrains_count(terrain_set); terrain++) { + option_button->set_item_icon(terrain + 1, icons[terrain_set][terrain]); + } + terrain_property_editor->show(); + } +} + +void TileDataTerrainsEditor::_property_value_changed(StringName p_property, Variant p_value, StringName p_field) { + Variant old_value = dummy_object->get(p_property); + dummy_object->set(p_property, p_value); + if (p_property == "terrain_set") { + if (p_value != old_value) { + dummy_object->set("terrain", -1); + } + _update_terrain_selector(); + } + emit_signal(SNAME("needs_redraw")); +} + +void TileDataTerrainsEditor::_tile_set_changed() { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Fix if wrong values are selected. + int terrain_set = int(dummy_object->get("terrain_set")); + if (terrain_set >= tile_set->get_terrain_sets_count()) { + terrain_set = -1; + dummy_object->set("terrain_set", -1); + } + if (terrain_set >= 0) { + if (int(dummy_object->get("terrain")) >= tile_set->get_terrains_count(terrain_set)) { + dummy_object->set("terrain", -1); + } + } + + _update_terrain_selector(); +} + +void TileDataTerrainsEditor::forward_draw_over_atlas(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform) { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Draw the hovered terrain bit, or the whole tile if it has the wrong terrain set. + Vector2i hovered_coords = TileSetSource::INVALID_ATLAS_COORDS; + if (drag_type == DRAG_TYPE_NONE) { + Vector2i mouse_pos = p_transform.affine_inverse().xform(p_canvas_item->get_local_mouse_position()); + hovered_coords = p_tile_atlas_view->get_atlas_tile_coords_at_pos(mouse_pos); + hovered_coords = p_tile_set_atlas_source->get_tile_at_coords(hovered_coords); + if (hovered_coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(hovered_coords, 0)); + int terrain_set = tile_data->get_terrain_set(); + Rect2i texture_region = p_tile_set_atlas_source->get_tile_texture_region(hovered_coords); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(hovered_coords, 0); + + if (terrain_set >= 0 && terrain_set == int(dummy_object->get("terrain_set"))) { + // Draw hovered bit. + Transform2D xform; + xform.set_origin(position); + + Vector<Color> color; + color.push_back(Color(1.0, 1.0, 1.0, 0.5)); + + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + if (Geometry2D::is_point_in_polygon(xform.affine_inverse().xform(mouse_pos), polygon)) { + p_canvas_item->draw_set_transform_matrix(p_transform * xform); + p_canvas_item->draw_polygon(polygon, color); + } + } + } + } else { + // Draw hovered tile. + Transform2D tile_xform; + tile_xform.set_origin(position); + tile_xform.set_scale(tile_set->get_tile_size()); + tile_set->draw_tile_shape(p_canvas_item, p_transform * tile_xform, Color(1.0, 1.0, 1.0, 0.5), true); + } + } + } + + // Dim terrains with wrong terrain set. + Ref<Font> font = TileSetEditor::get_singleton()->get_theme_font(SNAME("bold"), SNAME("EditorFonts")); + int font_size = TileSetEditor::get_singleton()->get_theme_font_size(SNAME("bold_size"), SNAME("EditorFonts")); + for (int i = 0; i < p_tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i coords = p_tile_set_atlas_source->get_tile_id(i); + if (coords != hovered_coords) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + if (tile_data->get_terrain_set() != int(dummy_object->get("terrain_set"))) { + // Dimming + p_canvas_item->draw_set_transform_matrix(p_transform); + Rect2i rect = p_tile_set_atlas_source->get_tile_texture_region(coords); + p_canvas_item->draw_rect(rect, Color(0.0, 0.0, 0.0, 0.3)); + + // Text + p_canvas_item->draw_set_transform_matrix(Transform2D()); + Rect2i texture_region = p_tile_set_atlas_source->get_tile_texture_region(coords); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + + Color color = Color(1, 1, 1); + String text; + if (tile_data->get_terrain_set() >= 0) { + text = vformat("%d", tile_data->get_terrain_set()); + } else { + text = "-"; + } + Vector2 string_size = font->get_string_size(text, font_size); + p_canvas_item->draw_string(font, p_transform.xform(position) + Vector2i(-string_size.x / 2, string_size.y / 2), text, HALIGN_CENTER, string_size.x, font_size, color, 1, Color(0, 0, 0, 1)); + } + } + } + p_canvas_item->draw_set_transform_matrix(Transform2D()); + + if (drag_type == DRAG_TYPE_PAINT_TERRAIN_SET_RECT) { + // Draw selection rectangle. + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + + p_canvas_item->draw_set_transform_matrix(p_transform); + + Rect2i rect; + rect.set_position(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_pos)); + rect.set_end(p_tile_atlas_view->get_atlas_tile_coords_at_pos(p_transform.affine_inverse().xform(p_canvas_item->get_local_mouse_position()))); + rect = rect.abs(); + + Set<TileMapCell> edited; + for (int x = rect.get_position().x; x <= rect.get_end().x; x++) { + for (int y = rect.get_position().y; y <= rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + edited.insert(cell); + } + } + } + + for (Set<TileMapCell>::Element *E = edited.front(); E; E = E->next()) { + Vector2i coords = E->get().get_atlas_coords(); + p_canvas_item->draw_rect(p_tile_set_atlas_source->get_tile_texture_region(coords), selection_color, false); + } + p_canvas_item->draw_set_transform_matrix(Transform2D()); + } else if (drag_type == DRAG_TYPE_PAINT_TERRAIN_BITS_RECT) { + // Highlight selected peering bits. + Dictionary painted = Dictionary(drag_painted_value); + int terrain_set = int(painted["terrain_set"]); + + Rect2i rect; + rect.set_position(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_pos)); + rect.set_end(p_tile_atlas_view->get_atlas_tile_coords_at_pos(p_transform.affine_inverse().xform(p_canvas_item->get_local_mouse_position()))); + rect = rect.abs(); + + Set<TileMapCell> edited; + for (int x = rect.get_position().x; x <= rect.get_end().x; x++) { + for (int y = rect.get_position().y; y <= rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + if (tile_data->get_terrain_set() == terrain_set) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + edited.insert(cell); + } + } + } + } + + Vector2 end = p_transform.affine_inverse().xform(p_canvas_item->get_local_mouse_position()); + Vector<Point2> mouse_pos_rect_polygon; + mouse_pos_rect_polygon.push_back(drag_start_pos); + mouse_pos_rect_polygon.push_back(Vector2(end.x, drag_start_pos.y)); + mouse_pos_rect_polygon.push_back(end); + mouse_pos_rect_polygon.push_back(Vector2(drag_start_pos.x, end.y)); + + Vector<Color> color; + color.push_back(Color(1.0, 1.0, 1.0, 0.5)); + + p_canvas_item->draw_set_transform_matrix(p_transform); + + for (Set<TileMapCell>::Element *E = edited.front(); E; E = E->next()) { + Vector2i coords = E->get().get_atlas_coords(); + + Rect2i texture_region = p_tile_set_atlas_source->get_tile_texture_region(coords); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + for (int j = 0; j < polygon.size(); j++) { + polygon.write[j] += position; + } + if (!Geometry2D::intersect_polygons(polygon, mouse_pos_rect_polygon).is_empty()) { + // Draw bit. + p_canvas_item->draw_polygon(polygon, color); + } + } + } + } + + p_canvas_item->draw_set_transform_matrix(Transform2D()); + } +} + +void TileDataTerrainsEditor::forward_draw_over_alternatives(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform) { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Draw the hovered terrain bit, or the whole tile if it has the wrong terrain set. + Vector2i hovered_coords = TileSetSource::INVALID_ATLAS_COORDS; + int hovered_alternative = TileSetSource::INVALID_TILE_ALTERNATIVE; + if (drag_type == DRAG_TYPE_NONE) { + Vector2i mouse_pos = p_transform.affine_inverse().xform(p_canvas_item->get_local_mouse_position()); + Vector3i hovered = p_tile_atlas_view->get_alternative_tile_at_pos(mouse_pos); + hovered_coords = Vector2i(hovered.x, hovered.y); + hovered_alternative = hovered.z; + if (hovered_coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(hovered_coords, hovered_alternative)); + int terrain_set = tile_data->get_terrain_set(); + Rect2i texture_region = p_tile_atlas_view->get_alternative_tile_rect(hovered_coords, hovered_alternative); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(hovered_coords, hovered_alternative); + + if (terrain_set == int(dummy_object->get("terrain_set"))) { + // Draw hovered bit. + Transform2D xform; + xform.set_origin(position); + + Vector<Color> color; + color.push_back(Color(1.0, 1.0, 1.0, 0.5)); + + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + if (Geometry2D::is_point_in_polygon(xform.affine_inverse().xform(mouse_pos), polygon)) { + p_canvas_item->draw_set_transform_matrix(p_transform * xform); + p_canvas_item->draw_polygon(polygon, color); + } + } + } + } else { + // Draw hovered tile. + Transform2D tile_xform; + tile_xform.set_origin(position); + tile_xform.set_scale(tile_set->get_tile_size()); + tile_set->draw_tile_shape(p_canvas_item, p_transform * tile_xform, Color(1.0, 1.0, 1.0, 0.5), true); + } + } + } + + // Dim terrains with wrong terrain set. + Ref<Font> font = TileSetEditor::get_singleton()->get_theme_font(SNAME("bold"), SNAME("EditorFonts")); + int font_size = TileSetEditor::get_singleton()->get_theme_font_size(SNAME("bold_size"), SNAME("EditorFonts")); + for (int i = 0; i < p_tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i coords = p_tile_set_atlas_source->get_tile_id(i); + for (int j = 1; j < p_tile_set_atlas_source->get_alternative_tiles_count(coords); j++) { + int alternative_tile = p_tile_set_atlas_source->get_alternative_tile_id(coords, j); + if (coords != hovered_coords || alternative_tile != hovered_alternative) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, alternative_tile)); + if (tile_data->get_terrain_set() != int(dummy_object->get("terrain_set"))) { + // Dimming + p_canvas_item->draw_set_transform_matrix(p_transform); + Rect2i rect = p_tile_atlas_view->get_alternative_tile_rect(coords, alternative_tile); + p_canvas_item->draw_rect(rect, Color(0.0, 0.0, 0.0, 0.3)); + + // Text + p_canvas_item->draw_set_transform_matrix(Transform2D()); + Rect2i texture_region = p_tile_atlas_view->get_alternative_tile_rect(coords, alternative_tile); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + + Color color = Color(1, 1, 1); + String text; + if (tile_data->get_terrain_set() >= 0) { + text = vformat("%d", tile_data->get_terrain_set()); + } else { + text = "-"; + } + Vector2 string_size = font->get_string_size(text, font_size); + p_canvas_item->draw_string(font, p_transform.xform(position) + Vector2i(-string_size.x / 2, string_size.y / 2), text, HALIGN_CENTER, string_size.x, font_size, color, 1, Color(0, 0, 0, 1)); + } + } + } + } + + p_canvas_item->draw_set_transform_matrix(Transform2D()); +} + +void TileDataTerrainsEditor::forward_painting_atlas_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, const Ref<InputEvent> &p_event) { + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + if (drag_type == DRAG_TYPE_PAINT_TERRAIN_SET) { + Vector<Vector2i> line = Geometry2D::bresenham_line(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_last_pos), p_tile_atlas_view->get_atlas_tile_coords_at_pos(mm->get_position())); + for (int i = 0; i < line.size(); i++) { + Vector2i coords = p_tile_set_atlas_source->get_tile_at_coords(line[i]); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + int terrain_set = drag_painted_value; + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + + // Save the old terrain_set and terrains bits. + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + if (!drag_modified.has(cell)) { + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(j); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + } + + // Set the terrain_set. + tile_data->set_terrain_set(terrain_set); + } + } + drag_last_pos = mm->get_position(); + } else if (drag_type == DRAG_TYPE_PAINT_TERRAIN_BITS) { + int terrain_set = Dictionary(drag_painted_value)["terrain_set"]; + int terrain = Dictionary(drag_painted_value)["terrain"]; + Vector<Vector2i> line = Geometry2D::bresenham_line(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_last_pos), p_tile_atlas_view->get_atlas_tile_coords_at_pos(mm->get_position())); + for (int i = 0; i < line.size(); i++) { + Vector2i coords = p_tile_set_atlas_source->get_tile_at_coords(line[i]); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + if (tile_data->get_terrain_set() == terrain_set) { + // Save the old terrain_set and terrains bits. + if (!drag_modified.has(cell)) { + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(j); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + } + + // Set the terrains bits. + Rect2i texture_region = p_tile_set_atlas_source->get_tile_texture_region(coords); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(j); + if (tile_data->is_valid_peering_bit_terrain(bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(tile_data->get_terrain_set(), bit); + if (Geometry2D::is_segment_intersecting_polygon(mm->get_position() - position, drag_last_pos - position, polygon)) { + tile_data->set_peering_bit_terrain(bit, terrain); + } + } + } + } + } + } + drag_last_pos = mm->get_position(); + } + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + if (mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { + if (picker_button->is_pressed()) { + Vector2i coords = p_tile_atlas_view->get_atlas_tile_coords_at_pos(mb->get_position()); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + int terrain_set = tile_data->get_terrain_set(); + Rect2i texture_region = p_tile_set_atlas_source->get_tile_texture_region(coords); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + dummy_object->set("terrain_set", terrain_set); + dummy_object->set("terrain", -1); + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + if (Geometry2D::is_point_in_polygon(mb->get_position() - position, polygon)) { + dummy_object->set("terrain", tile_data->get_peering_bit_terrain(bit)); + } + } + } + terrain_set_property_editor->update_property(); + _update_terrain_selector(); + picker_button->set_pressed(false); + } + } else { + Vector2i coords = p_tile_atlas_view->get_atlas_tile_coords_at_pos(mb->get_position()); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + TileData *tile_data = nullptr; + if (coords != TileSetAtlasSource::INVALID_ATLAS_COORDS) { + tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + } + int terrain_set = int(dummy_object->get("terrain_set")); + int terrain = int(dummy_object->get("terrain")); + if (terrain_set == -1 || !tile_data || tile_data->get_terrain_set() != terrain_set) { + if (mb->is_ctrl_pressed()) { + // Paint terrain set with rect. + drag_type = DRAG_TYPE_PAINT_TERRAIN_SET_RECT; + drag_modified.clear(); + drag_painted_value = terrain_set; + drag_start_pos = mb->get_position(); + } else { + // Paint terrain set. + drag_type = DRAG_TYPE_PAINT_TERRAIN_SET; + drag_modified.clear(); + drag_painted_value = terrain_set; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + + // Save the old terrain_set and terrains bits. + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + + // Set the terrain_set. + tile_data->set_terrain_set(terrain_set); + } + drag_last_pos = mb->get_position(); + } + } else if (tile_data && tile_data->get_terrain_set() == terrain_set) { + if (mb->is_ctrl_pressed()) { + // Paint terrain set with rect. + drag_type = DRAG_TYPE_PAINT_TERRAIN_BITS_RECT; + drag_modified.clear(); + Dictionary painted_dict; + painted_dict["terrain_set"] = terrain_set; + painted_dict["terrain"] = terrain; + drag_painted_value = painted_dict; + drag_start_pos = mb->get_position(); + } else { + // Paint terrain bits. + drag_type = DRAG_TYPE_PAINT_TERRAIN_BITS; + drag_modified.clear(); + Dictionary painted_dict; + painted_dict["terrain_set"] = terrain_set; + painted_dict["terrain"] = terrain; + drag_painted_value = painted_dict; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + + // Save the old terrain_set and terrains bits. + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + + // Set the terrain bit. + Rect2i texture_region = p_tile_set_atlas_source->get_tile_texture_region(coords); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + if (Geometry2D::is_point_in_polygon(mb->get_position() - position, polygon)) { + tile_data->set_peering_bit_terrain(bit, terrain); + } + } + } + } + drag_last_pos = mb->get_position(); + } + } + } + } else { + if (drag_type == DRAG_TYPE_PAINT_TERRAIN_SET_RECT) { + Rect2i rect; + rect.set_position(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_pos)); + rect.set_end(p_tile_atlas_view->get_atlas_tile_coords_at_pos(mb->get_position())); + rect = rect.abs(); + + Set<TileMapCell> edited; + for (int x = rect.get_position().x; x <= rect.get_end().x; x++) { + for (int y = rect.get_position().y; y <= rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + edited.insert(cell); + } + } + } + undo_redo->create_action(TTR("Painting Terrain Set")); + for (Set<TileMapCell>::Element *E = edited.front(); E; E = E->next()) { + Vector2i coords = E->get().get_atlas_coords(); + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrain_set", coords.x, coords.y, E->get().alternative_tile), tile_data->get_terrain_set()); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrain_set", coords.x, coords.y, E->get().alternative_tile), drag_painted_value); + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_data->is_valid_peering_bit_terrain(bit)) { + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E->get().alternative_tile), tile_data->get_peering_bit_terrain(bit)); + } + } + } + undo_redo->commit_action(true); + drag_type = DRAG_TYPE_NONE; + } else if (drag_type == DRAG_TYPE_PAINT_TERRAIN_SET) { + undo_redo->create_action(TTR("Painting Terrain Set")); + for (KeyValue<TileMapCell, Variant> &E : drag_modified) { + Dictionary dict = E.value; + Vector2i coords = E.key.get_atlas_coords(); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrain_set", coords.x, coords.y, E.key.alternative_tile), drag_painted_value); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrain_set", coords.x, coords.y, E.key.alternative_tile), dict["terrain_set"]); + Array array = dict["terrain_peering_bits"]; + for (int i = 0; i < array.size(); i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(dict["terrain_set"], bit)) { + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E.key.alternative_tile), array[i]); + } + } + } + undo_redo->commit_action(false); + drag_type = DRAG_TYPE_NONE; + } else if (drag_type == DRAG_TYPE_PAINT_TERRAIN_BITS) { + Dictionary painted = Dictionary(drag_painted_value); + int terrain_set = int(painted["terrain_set"]); + int terrain = int(painted["terrain"]); + undo_redo->create_action(TTR("Painting Terrain")); + for (KeyValue<TileMapCell, Variant> &E : drag_modified) { + Dictionary dict = E.value; + Vector2i coords = E.key.get_atlas_coords(); + Array array = dict["terrain_peering_bits"]; + for (int i = 0; i < array.size(); i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E.key.alternative_tile), terrain); + } + if (tile_set->is_valid_peering_bit_terrain(dict["terrain_set"], bit)) { + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E.key.alternative_tile), array[i]); + } + } + } + undo_redo->commit_action(false); + drag_type = DRAG_TYPE_NONE; + } else if (drag_type == DRAG_TYPE_PAINT_TERRAIN_BITS_RECT) { + Dictionary painted = Dictionary(drag_painted_value); + int terrain_set = int(painted["terrain_set"]); + int terrain = int(painted["terrain"]); + + Rect2i rect; + rect.set_position(p_tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_pos)); + rect.set_end(p_tile_atlas_view->get_atlas_tile_coords_at_pos(mb->get_position())); + rect = rect.abs(); + + Set<TileMapCell> edited; + for (int x = rect.get_position().x; x <= rect.get_end().x; x++) { + for (int y = rect.get_position().y; y <= rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + coords = p_tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + if (tile_data->get_terrain_set() == terrain_set) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + edited.insert(cell); + } + } + } + } + + Vector<Point2> mouse_pos_rect_polygon; + mouse_pos_rect_polygon.push_back(drag_start_pos); + mouse_pos_rect_polygon.push_back(Vector2(mb->get_position().x, drag_start_pos.y)); + mouse_pos_rect_polygon.push_back(mb->get_position()); + mouse_pos_rect_polygon.push_back(Vector2(drag_start_pos.x, mb->get_position().y)); + + undo_redo->create_action(TTR("Painting Terrain")); + for (Set<TileMapCell>::Element *E = edited.front(); E; E = E->next()) { + Vector2i coords = E->get().get_atlas_coords(); + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, 0)); + + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Rect2i texture_region = p_tile_set_atlas_source->get_tile_texture_region(coords); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + for (int j = 0; j < polygon.size(); j++) { + polygon.write[j] += position; + } + if (!Geometry2D::intersect_polygons(polygon, mouse_pos_rect_polygon).is_empty()) { + // Draw bit. + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E->get().alternative_tile), terrain); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E->get().alternative_tile), tile_data->get_peering_bit_terrain(bit)); + } + } + } + } + undo_redo->commit_action(true); + drag_type = DRAG_TYPE_NONE; + } + } + } + } +} + +void TileDataTerrainsEditor::forward_painting_alternatives_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_set_atlas_source, const Ref<InputEvent> &p_event) { + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + if (drag_type == DRAG_TYPE_PAINT_TERRAIN_SET) { + Vector3i tile = p_tile_atlas_view->get_alternative_tile_at_pos(mm->get_position()); + Vector2i coords = Vector2i(tile.x, tile.y); + int alternative_tile = tile.z; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = alternative_tile; + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, alternative_tile)); + if (!drag_modified.has(cell)) { + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(j); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + } + tile_data->set_terrain_set(drag_painted_value); + } + + drag_last_pos = mm->get_position(); + } else if (drag_type == DRAG_TYPE_PAINT_TERRAIN_BITS) { + Dictionary painted = Dictionary(drag_painted_value); + int terrain_set = int(painted["terrain_set"]); + int terrain = int(painted["terrain"]); + + Vector3i tile = p_tile_atlas_view->get_alternative_tile_at_pos(mm->get_position()); + Vector2i coords = Vector2i(tile.x, tile.y); + int alternative_tile = tile.z; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = alternative_tile; + + // Save the old terrain_set and terrains bits. + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, alternative_tile)); + if (tile_data->get_terrain_set() == terrain_set) { + if (!drag_modified.has(cell)) { + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(j); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + } + + // Set the terrains bits. + Rect2i texture_region = p_tile_atlas_view->get_alternative_tile_rect(coords, alternative_tile); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, alternative_tile); + for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(j); + if (tile_data->is_valid_peering_bit_terrain(bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(tile_data->get_terrain_set(), bit); + if (Geometry2D::is_segment_intersecting_polygon(mm->get_position() - position, drag_last_pos - position, polygon)) { + tile_data->set_peering_bit_terrain(bit, terrain); + } + } + } + } + } + drag_last_pos = mm->get_position(); + } + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + if (mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { + if (picker_button->is_pressed()) { + Vector3i tile = p_tile_atlas_view->get_alternative_tile_at_pos(mb->get_position()); + Vector2i coords = Vector2i(tile.x, tile.y); + int alternative_tile = tile.z; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, alternative_tile)); + int terrain_set = tile_data->get_terrain_set(); + Rect2i texture_region = p_tile_atlas_view->get_alternative_tile_rect(coords, alternative_tile); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, alternative_tile); + dummy_object->set("terrain_set", terrain_set); + dummy_object->set("terrain", -1); + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + if (Geometry2D::is_point_in_polygon(mb->get_position() - position, polygon)) { + dummy_object->set("terrain", tile_data->get_peering_bit_terrain(bit)); + } + } + } + terrain_set_property_editor->update_property(); + _update_terrain_selector(); + picker_button->set_pressed(false); + } + } else { + int terrain_set = int(dummy_object->get("terrain_set")); + int terrain = int(dummy_object->get("terrain")); + + Vector3i tile = p_tile_atlas_view->get_alternative_tile_at_pos(mb->get_position()); + Vector2i coords = Vector2i(tile.x, tile.y); + int alternative_tile = tile.z; + + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(coords, alternative_tile)); + + if (terrain_set == -1 || !tile_data || tile_data->get_terrain_set() != terrain_set) { + drag_type = DRAG_TYPE_PAINT_TERRAIN_SET; + drag_modified.clear(); + drag_painted_value = int(dummy_object->get("terrain_set")); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = alternative_tile; + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + tile_data->set_terrain_set(drag_painted_value); + } + drag_last_pos = mb->get_position(); + } else if (tile_data && tile_data->get_terrain_set() == terrain_set) { + // Paint terrain bits. + drag_type = DRAG_TYPE_PAINT_TERRAIN_BITS; + drag_modified.clear(); + Dictionary painted_dict; + painted_dict["terrain_set"] = terrain_set; + painted_dict["terrain"] = terrain; + drag_painted_value = painted_dict; + + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + TileMapCell cell; + cell.source_id = 0; + cell.set_atlas_coords(coords); + cell.alternative_tile = alternative_tile; + + // Save the old terrain_set and terrains bits. + Dictionary dict; + dict["terrain_set"] = tile_data->get_terrain_set(); + Array array; + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + array.push_back(tile_data->is_valid_peering_bit_terrain(bit) ? tile_data->get_peering_bit_terrain(bit) : -1); + } + dict["terrain_peering_bits"] = array; + drag_modified[cell] = dict; + + // Set the terrain bit. + Rect2i texture_region = p_tile_atlas_view->get_alternative_tile_rect(coords, alternative_tile); + Vector2i position = texture_region.get_center() + p_tile_set_atlas_source->get_tile_effective_texture_offset(coords, alternative_tile); + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + Vector<Vector2> polygon = tile_set->get_terrain_bit_polygon(terrain_set, bit); + if (Geometry2D::is_point_in_polygon(mb->get_position() - position, polygon)) { + tile_data->set_peering_bit_terrain(bit, terrain); + } + } + } + } + drag_last_pos = mb->get_position(); + } + } + } else { + if (drag_type == DRAG_TYPE_PAINT_TERRAIN_SET) { + undo_redo->create_action(TTR("Painting Tiles Property")); + for (KeyValue<TileMapCell, Variant> &E : drag_modified) { + Dictionary dict = E.value; + Vector2i coords = E.key.get_atlas_coords(); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrain_set", coords.x, coords.y, E.key.alternative_tile), dict["terrain_set"]); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrain_set", coords.x, coords.y, E.key.alternative_tile), drag_painted_value); + Array array = dict["terrain_peering_bits"]; + for (int i = 0; i < array.size(); i++) { + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E.key.alternative_tile), array[i]); + } + } + undo_redo->commit_action(false); + drag_type = DRAG_TYPE_NONE; + } else if (drag_type == DRAG_TYPE_PAINT_TERRAIN_BITS) { + Dictionary painted = Dictionary(drag_painted_value); + int terrain_set = int(painted["terrain_set"]); + int terrain = int(painted["terrain"]); + undo_redo->create_action(TTR("Painting Terrain")); + for (KeyValue<TileMapCell, Variant> &E : drag_modified) { + Dictionary dict = E.value; + Vector2i coords = E.key.get_atlas_coords(); + Array array = dict["terrain_peering_bits"]; + for (int i = 0; i < array.size(); i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E.key.alternative_tile), terrain); + } + if (tile_set->is_valid_peering_bit_terrain(dict["terrain_set"], bit)) { + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i]), coords.x, coords.y, E.key.alternative_tile), array[i]); + } + } + } + undo_redo->commit_action(false); + drag_type = DRAG_TYPE_NONE; + } + } + } + } +} + +void TileDataTerrainsEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + tile_set->draw_terrains(p_canvas_item, p_transform, tile_data); +} + +void TileDataTerrainsEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: + picker_button->set_icon(get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); + break; + default: + break; + } +} + +TileDataTerrainsEditor::TileDataTerrainsEditor() { + label = memnew(Label); + label->set_text("Painting:"); + add_child(label); + + // Toolbar + toolbar->add_child(memnew(VSeparator)); + + picker_button = memnew(Button); + picker_button->set_flat(true); + picker_button->set_toggle_mode(true); + picker_button->set_shortcut(ED_SHORTCUT("tiles_editor/picker", "Picker", Key::P)); + toolbar->add_child(picker_button); + + // Setup + dummy_object->add_dummy_property("terrain_set"); + dummy_object->set("terrain_set", -1); + dummy_object->add_dummy_property("terrain"); + dummy_object->set("terrain", -1); + + // Get the default value for the type. + terrain_set_property_editor = memnew(EditorPropertyEnum); + terrain_set_property_editor->set_object_and_property(dummy_object, "terrain_set"); + terrain_set_property_editor->set_label("Terrain Set"); + terrain_set_property_editor->connect("property_changed", callable_mp(this, &TileDataTerrainsEditor::_property_value_changed).unbind(1)); + add_child(terrain_set_property_editor); + + terrain_property_editor = memnew(EditorPropertyEnum); + terrain_property_editor->set_object_and_property(dummy_object, "terrain"); + terrain_property_editor->set_label("Terrain"); + terrain_property_editor->connect("property_changed", callable_mp(this, &TileDataTerrainsEditor::_property_value_changed).unbind(1)); + add_child(terrain_property_editor); +} + +TileDataTerrainsEditor::~TileDataTerrainsEditor() { + toolbar->queue_delete(); + memdelete(dummy_object); +} + +Variant TileDataNavigationEditor::_get_painted_value() { + Ref<NavigationPolygon> navigation_polygon; + navigation_polygon.instantiate(); + + for (int i = 0; i < polygon_editor->get_polygon_count(); i++) { + Vector<Vector2> polygon = polygon_editor->get_polygon(i); + navigation_polygon->add_outline(polygon); + } + + navigation_polygon->make_polygons_from_outlines(); + return navigation_polygon; +} + +void TileDataNavigationEditor::_set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + + Ref<NavigationPolygon> navigation_polygon = tile_data->get_navigation_polygon(navigation_layer); + polygon_editor->clear_polygons(); + if (navigation_polygon.is_valid()) { + for (int i = 0; i < navigation_polygon->get_outline_count(); i++) { + polygon_editor->add_polygon(navigation_polygon->get_outline(i)); + } + } + polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), p_tile_set_atlas_source->get_tile_effective_texture_offset(p_coords, p_alternative_tile), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); +} + +void TileDataNavigationEditor::_set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND(!tile_data); + Ref<NavigationPolygon> navigation_polygon = p_value; + tile_data->set_navigation_polygon(navigation_layer, navigation_polygon); + + polygon_editor->set_background(p_tile_set_atlas_source->get_texture(), p_tile_set_atlas_source->get_tile_texture_region(p_coords), p_tile_set_atlas_source->get_tile_effective_texture_offset(p_coords, p_alternative_tile), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); +} + +Variant TileDataNavigationEditor::_get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) { + TileData *tile_data = Object::cast_to<TileData>(p_tile_set_atlas_source->get_tile_data(p_coords, p_alternative_tile)); + ERR_FAIL_COND_V(!tile_data, Variant()); + return tile_data->get_navigation_polygon(navigation_layer); +} + +void TileDataNavigationEditor::_setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value) { + for (const KeyValue<TileMapCell, Variant> &E : p_previous_values) { + Vector2i coords = E.key.get_atlas_coords(); + undo_redo->add_undo_property(p_tile_set_atlas_source, vformat("%d:%d/%d/navigation_layer_%d/polygon", coords.x, coords.y, E.key.alternative_tile, navigation_layer), E.value); + undo_redo->add_do_property(p_tile_set_atlas_source, vformat("%d:%d/%d/navigation_layer_%d/polygon", coords.x, coords.y, E.key.alternative_tile, navigation_layer), p_new_value); + } +} + +void TileDataNavigationEditor::_tile_set_changed() { + polygon_editor->set_tile_set(tile_set); +} + +void TileDataNavigationEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + polygon_editor->set_polygons_color(get_tree()->get_debug_navigation_color()); + break; + default: + break; + } +} + +TileDataNavigationEditor::TileDataNavigationEditor() { + polygon_editor = memnew(GenericTilePolygonEditor); + polygon_editor->set_multiple_polygon_mode(true); + add_child(polygon_editor); +} + +void TileDataNavigationEditor::draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected) { + TileData *tile_data = _get_tile_data(p_cell); + ERR_FAIL_COND(!tile_data); + + // Draw all shapes. + RenderingServer::get_singleton()->canvas_item_add_set_transform(p_canvas_item->get_canvas_item(), p_transform); + + Ref<NavigationPolygon> navigation_polygon = tile_data->get_navigation_polygon(navigation_layer); + if (navigation_polygon.is_valid()) { + Vector<Vector2> verts = navigation_polygon->get_vertices(); + if (verts.size() < 3) { + return; + } + + Color color = p_canvas_item->get_tree()->get_debug_navigation_color(); + if (p_selected) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + selection_color.a = 0.7; + color = selection_color; + } + + RandomPCG rand; + for (int i = 0; i < navigation_polygon->get_polygon_count(); i++) { + // An array of vertices for this polygon. + Vector<int> polygon = navigation_polygon->get_polygon(i); + Vector<Vector2> vertices; + vertices.resize(polygon.size()); + for (int j = 0; j < polygon.size(); j++) { + ERR_FAIL_INDEX(polygon[j], verts.size()); + vertices.write[j] = verts[polygon[j]]; + } + + // Generate the polygon color, slightly randomly modified from the settings one. + Color random_variation_color; + random_variation_color.set_hsv(color.get_h() + rand.random(-1.0, 1.0) * 0.05, color.get_s(), color.get_v() + rand.random(-1.0, 1.0) * 0.1); + random_variation_color.a = color.a; + Vector<Color> colors; + colors.push_back(random_variation_color); + + RenderingServer::get_singleton()->canvas_item_add_polygon(p_canvas_item->get_canvas_item(), vertices, colors); + } + } + + RenderingServer::get_singleton()->canvas_item_add_set_transform(p_canvas_item->get_canvas_item(), Transform2D()); +} diff --git a/editor/plugins/tiles/tile_data_editors.h b/editor/plugins/tiles/tile_data_editors.h new file mode 100644 index 0000000000..3fc5e738bb --- /dev/null +++ b/editor/plugins/tiles/tile_data_editors.h @@ -0,0 +1,417 @@ +/*************************************************************************/ +/* tile_data_editors.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILE_DATA_EDITORS_H +#define TILE_DATA_EDITORS_H + +#include "tile_atlas_view.h" + +#include "editor/editor_node.h" +#include "editor/editor_properties.h" + +#include "scene/2d/tile_map.h" +#include "scene/gui/box_container.h" +#include "scene/gui/control.h" +#include "scene/gui/label.h" + +class TileDataEditor : public VBoxContainer { + GDCLASS(TileDataEditor, VBoxContainer); + +private: + bool _tile_set_changed_update_needed = false; + void _tile_set_changed_plan_update(); + void _tile_set_changed_deferred_update(); + +protected: + Ref<TileSet> tile_set; + TileData *_get_tile_data(TileMapCell p_cell); + virtual void _tile_set_changed(){}; + + static void _bind_methods(); + +public: + void set_tile_set(Ref<TileSet> p_tile_set); + + // Input to handle painting. + virtual Control *get_toolbar() { return nullptr; }; + virtual void forward_draw_over_atlas(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform){}; + virtual void forward_draw_over_alternatives(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform){}; + virtual void forward_painting_atlas_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, const Ref<InputEvent> &p_event){}; + virtual void forward_painting_alternatives_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, const Ref<InputEvent> &p_event){}; + + // Used to draw the tile data property value over a tile. + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false){}; +}; + +class DummyObject : public Object { + GDCLASS(DummyObject, Object) +private: + Map<String, Variant> properties; + +protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + +public: + bool has_dummy_property(StringName p_name); + void add_dummy_property(StringName p_name); + void remove_dummy_property(StringName p_name); + void clear_dummy_properties(); +}; + +class GenericTilePolygonEditor : public VBoxContainer { + GDCLASS(GenericTilePolygonEditor, VBoxContainer); + +private: + Ref<TileSet> tile_set; + LocalVector<Vector<Point2>> polygons; + bool multiple_polygon_mode = false; + + bool use_undo_redo = true; + UndoRedo *editor_undo_redo = EditorNode::get_undo_redo(); + + // UI + int hovered_polygon_index = -1; + int hovered_point_index = -1; + int hovered_segment_index = -1; + Vector2 hovered_segment_point; + + enum DragType { + DRAG_TYPE_NONE, + DRAG_TYPE_DRAG_POINT, + DRAG_TYPE_CREATE_POINT, + DRAG_TYPE_PAN, + }; + DragType drag_type = DRAG_TYPE_NONE; + int drag_polygon_index; + int drag_point_index; + Vector2 drag_last_pos; + PackedVector2Array drag_old_polygon; + + HBoxContainer *toolbar; + Ref<ButtonGroup> tools_button_group; + Button *button_create; + Button *button_edit; + Button *button_delete; + Button *button_pixel_snap; + MenuButton *button_advanced_menu; + + Vector<Point2> in_creation_polygon; + + Panel *panel; + Control *base_control; + EditorZoomWidget *editor_zoom_widget; + Button *button_center_view; + Vector2 panning; + + Ref<Texture2D> background_texture; + Rect2 background_region; + Vector2 background_offset; + bool background_h_flip; + bool background_v_flip; + bool background_transpose; + Color background_modulate; + + Color polygon_color = Color(1.0, 0.0, 0.0); + + enum AdvancedMenuOption { + RESET_TO_DEFAULT_TILE, + CLEAR_TILE, + ROTATE_RIGHT, + ROTATE_LEFT, + FLIP_HORIZONTALLY, + FLIP_VERTICALLY, + }; + + void _base_control_draw(); + void _zoom_changed(); + void _advanced_menu_item_pressed(int p_item_pressed); + void _center_view(); + void _base_control_gui_input(Ref<InputEvent> p_event); + + void _snap_to_tile_shape(Point2 &r_point, float &r_current_snapped_dist, float p_snap_dist); + void _snap_to_half_pixel(Point2 &r_point); + void _grab_polygon_point(Vector2 p_pos, const Transform2D &p_polygon_xform, int &r_polygon_index, int &r_point_index); + void _grab_polygon_segment_point(Vector2 p_pos, const Transform2D &p_polygon_xform, int &r_polygon_index, int &r_segment_index, Vector2 &r_point); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void set_use_undo_redo(bool p_use_undo_redo); + + void set_tile_set(Ref<TileSet> p_tile_set); + void set_background(Ref<Texture2D> p_texture, Rect2 p_region = Rect2(), Vector2 p_offset = Vector2(), bool p_flip_h = false, bool p_flip_v = false, bool p_transpose = false, Color p_modulate = Color(1.0, 1.0, 1.0, 0.0)); + + int get_polygon_count(); + int add_polygon(Vector<Point2> p_polygon, int p_index = -1); + void remove_polygon(int p_index); + void clear_polygons(); + void set_polygon(int p_polygon_index, Vector<Point2> p_polygon); + Vector<Point2> get_polygon(int p_polygon_index); + + void set_polygons_color(Color p_color); + void set_multiple_polygon_mode(bool p_multiple_polygon_mode); + + GenericTilePolygonEditor(); +}; + +class TileDataDefaultEditor : public TileDataEditor { + GDCLASS(TileDataDefaultEditor, TileDataEditor); + +private: + // Toolbar + HBoxContainer *toolbar = memnew(HBoxContainer); + Button *picker_button; + + // UI + Ref<Texture2D> tile_bool_checked; + Ref<Texture2D> tile_bool_unchecked; + Label *label; + + EditorProperty *property_editor = nullptr; + + // Painting state. + enum DragType { + DRAG_TYPE_NONE = 0, + DRAG_TYPE_PAINT, + DRAG_TYPE_PAINT_RECT, + }; + DragType drag_type = DRAG_TYPE_NONE; + Vector2 drag_start_pos; + Vector2 drag_last_pos; + Map<TileMapCell, Variant> drag_modified; + Variant drag_painted_value; + + void _property_value_changed(StringName p_property, Variant p_value, StringName p_field); + +protected: + DummyObject *dummy_object = memnew(DummyObject); + + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + + StringName type; + String property; + void _notification(int p_what); + + virtual Variant _get_painted_value(); + virtual void _set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile); + virtual void _set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value); + virtual Variant _get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile); + virtual void _setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value); + +public: + virtual Control *get_toolbar() override { return toolbar; }; + virtual void forward_draw_over_atlas(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform) override; + virtual void forward_draw_over_alternatives(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform) override; + virtual void forward_painting_atlas_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, const Ref<InputEvent> &p_event) override; + virtual void forward_painting_alternatives_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, const Ref<InputEvent> &p_event) override; + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; + + void setup_property_editor(Variant::Type p_type, String p_property, String p_label = "", Variant p_default_value = Variant()); + + TileDataDefaultEditor(); + ~TileDataDefaultEditor(); +}; + +class TileDataTextureOffsetEditor : public TileDataDefaultEditor { + GDCLASS(TileDataTextureOffsetEditor, TileDataDefaultEditor); + +public: + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; +}; + +class TileDataPositionEditor : public TileDataDefaultEditor { + GDCLASS(TileDataPositionEditor, TileDataDefaultEditor); + +public: + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; +}; + +class TileDataYSortEditor : public TileDataDefaultEditor { + GDCLASS(TileDataYSortEditor, TileDataDefaultEditor); + +public: + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; +}; + +class TileDataOcclusionShapeEditor : public TileDataDefaultEditor { + GDCLASS(TileDataOcclusionShapeEditor, TileDataDefaultEditor); + +private: + int occlusion_layer = -1; + + // UI + GenericTilePolygonEditor *polygon_editor; + + void _polygon_changed(PackedVector2Array p_polygon); + + virtual Variant _get_painted_value() override; + virtual void _set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) override; + virtual void _set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value) override; + virtual Variant _get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) override; + virtual void _setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value) override; + +protected: + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + + virtual void _tile_set_changed() override; + + void _notification(int p_what); + +public: + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; + + void set_occlusion_layer(int p_occlusion_layer) { occlusion_layer = p_occlusion_layer; } + + TileDataOcclusionShapeEditor(); +}; + +class TileDataCollisionEditor : public TileDataDefaultEditor { + GDCLASS(TileDataCollisionEditor, TileDataDefaultEditor); + + int physics_layer = -1; + + // UI + GenericTilePolygonEditor *polygon_editor; + DummyObject *dummy_object = memnew(DummyObject); + Map<StringName, EditorProperty *> property_editors; + + void _property_value_changed(StringName p_property, Variant p_value, StringName p_field); + void _polygons_changed(); + + virtual Variant _get_painted_value() override; + virtual void _set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) override; + virtual void _set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value) override; + virtual Variant _get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) override; + virtual void _setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value) override; + +protected: + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + + virtual void _tile_set_changed() override; + + void _notification(int p_what); + +public: + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; + + void set_physics_layer(int p_physics_layer) { physics_layer = p_physics_layer; } + + TileDataCollisionEditor(); + ~TileDataCollisionEditor(); +}; + +class TileDataTerrainsEditor : public TileDataEditor { + GDCLASS(TileDataTerrainsEditor, TileDataEditor); + +private: + // Toolbar + HBoxContainer *toolbar = memnew(HBoxContainer); + Button *picker_button; + + // Painting state. + enum DragType { + DRAG_TYPE_NONE = 0, + DRAG_TYPE_PAINT_TERRAIN_SET, + DRAG_TYPE_PAINT_TERRAIN_SET_RECT, + DRAG_TYPE_PAINT_TERRAIN_BITS, + DRAG_TYPE_PAINT_TERRAIN_BITS_RECT, + }; + DragType drag_type = DRAG_TYPE_NONE; + Vector2 drag_start_pos; + Vector2 drag_last_pos; + Map<TileMapCell, Variant> drag_modified; + Variant drag_painted_value; + + // UI + Label *label; + DummyObject *dummy_object = memnew(DummyObject); + EditorPropertyEnum *terrain_set_property_editor = nullptr; + EditorPropertyEnum *terrain_property_editor = nullptr; + + void _property_value_changed(StringName p_property, Variant p_value, StringName p_field); + + void _update_terrain_selector(); + +protected: + virtual void _tile_set_changed() override; + + void _notification(int p_what); + + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + +public: + virtual Control *get_toolbar() override { return toolbar; }; + virtual void forward_draw_over_atlas(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform) override; + virtual void forward_draw_over_alternatives(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, CanvasItem *p_canvas_item, Transform2D p_transform) override; + virtual void forward_painting_atlas_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, const Ref<InputEvent> &p_event) override; + virtual void forward_painting_alternatives_gui_input(TileAtlasView *p_tile_atlas_view, TileSetAtlasSource *p_tile_atlas_source, const Ref<InputEvent> &p_event) override; + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; + + TileDataTerrainsEditor(); + ~TileDataTerrainsEditor(); +}; + +class TileDataNavigationEditor : public TileDataDefaultEditor { + GDCLASS(TileDataNavigationEditor, TileDataDefaultEditor); + +private: + int navigation_layer = -1; + PackedVector2Array navigation_polygon; + + // UI + GenericTilePolygonEditor *polygon_editor; + + void _polygon_changed(PackedVector2Array p_polygon); + + virtual Variant _get_painted_value() override; + virtual void _set_painted_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) override; + virtual void _set_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile, Variant p_value) override; + virtual Variant _get_value(TileSetAtlasSource *p_tile_set_atlas_source, Vector2 p_coords, int p_alternative_tile) override; + virtual void _setup_undo_redo_action(TileSetAtlasSource *p_tile_set_atlas_source, Map<TileMapCell, Variant> p_previous_values, Variant p_new_value) override; + +protected: + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + + virtual void _tile_set_changed() override; + + void _notification(int p_what); + +public: + virtual void draw_over_tile(CanvasItem *p_canvas_item, Transform2D p_transform, TileMapCell p_cell, bool p_selected = false) override; + + void set_navigation_layer(int p_navigation_layer) { navigation_layer = p_navigation_layer; } + + TileDataNavigationEditor(); +}; + +#endif // TILE_DATA_EDITORS_H diff --git a/editor/plugins/tiles/tile_map_editor.cpp b/editor/plugins/tiles/tile_map_editor.cpp new file mode 100644 index 0000000000..73b1fc7c67 --- /dev/null +++ b/editor/plugins/tiles/tile_map_editor.cpp @@ -0,0 +1,4006 @@ +/*************************************************************************/ +/* tile_map_editor.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tile_map_editor.h" + +#include "tiles_editor_plugin.h" + +#include "editor/editor_resource_preview.h" +#include "editor/editor_scale.h" +#include "editor/plugins/canvas_item_editor_plugin.h" + +#include "scene/2d/camera_2d.h" +#include "scene/gui/center_container.h" +#include "scene/gui/split_container.h" + +#include "core/input/input.h" +#include "core/math/geometry_2d.h" +#include "core/os/keyboard.h" + +void TileMapEditorTilesPlugin::tile_set_changed() { + _update_fix_selected_and_hovered(); + _update_tile_set_sources_list(); + _update_source_display(); + _update_patterns_list(); +} + +void TileMapEditorTilesPlugin::_on_random_tile_checkbox_toggled(bool p_pressed) { + scatter_spinbox->set_editable(p_pressed); +} + +void TileMapEditorTilesPlugin::_on_scattering_spinbox_changed(double p_value) { + scattering = p_value; +} + +void TileMapEditorTilesPlugin::_update_toolbar() { + // Stop draggig if needed. + _stop_dragging(); + + // Hide all settings. + for (int i = 0; i < tools_settings->get_child_count(); i++) { + Object::cast_to<CanvasItem>(tools_settings->get_child(i))->hide(); + } + + // Show only the correct settings. + if (tool_buttons_group->get_pressed_button() == select_tool_button) { + } else if (tool_buttons_group->get_pressed_button() == paint_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->show(); + random_tile_checkbox->show(); + scatter_label->show(); + scatter_spinbox->show(); + } else if (tool_buttons_group->get_pressed_button() == line_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->show(); + random_tile_checkbox->show(); + scatter_label->show(); + scatter_spinbox->show(); + } else if (tool_buttons_group->get_pressed_button() == rect_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->show(); + random_tile_checkbox->show(); + scatter_label->show(); + scatter_spinbox->show(); + } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->show(); + bucket_contiguous_checkbox->show(); + random_tile_checkbox->show(); + scatter_label->show(); + scatter_spinbox->show(); + } +} + +Vector<TileMapEditorPlugin::TabData> TileMapEditorTilesPlugin::get_tabs() const { + Vector<TileMapEditorPlugin::TabData> tabs; + tabs.push_back({ toolbar, tiles_bottom_panel }); + tabs.push_back({ toolbar, patterns_bottom_panel }); + return tabs; +} + +void TileMapEditorTilesPlugin::_tab_changed() { + if (tiles_bottom_panel->is_visible_in_tree()) { + _update_selection_pattern_from_tileset_tiles_selection(); + } else if (patterns_bottom_panel->is_visible_in_tree()) { + _update_selection_pattern_from_tileset_pattern_selection(); + } +} + +void TileMapEditorTilesPlugin::_update_tile_set_sources_list() { + // Update the sources. + int old_current = sources_list->get_current(); + sources_list->clear(); + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + for (int i = 0; i < tile_set->get_source_count(); i++) { + int source_id = tile_set->get_source_id(i); + + TileSetSource *source = *tile_set->get_source(source_id); + + Ref<Texture2D> texture; + String item_text; + + // Common to all type of sources. + if (!source->get_name().is_empty()) { + item_text = vformat(TTR("%s (id:%d)"), source->get_name(), source_id); + } + + // Atlas source. + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + if (atlas_source) { + texture = atlas_source->get_texture(); + if (item_text.is_empty()) { + if (texture.is_valid()) { + item_text = vformat("%s (ID: %d)", texture->get_path().get_file(), source_id); + } else { + item_text = vformat("No Texture Atlas Source (ID: %d)", source_id); + } + } + } + + // Scene collection source. + TileSetScenesCollectionSource *scene_collection_source = Object::cast_to<TileSetScenesCollectionSource>(source); + if (scene_collection_source) { + texture = tiles_bottom_panel->get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons")); + if (item_text.is_empty()) { + item_text = vformat(TTR("Scene Collection Source (ID: %d)"), source_id); + } + } + + // Use default if not valid. + if (item_text.is_empty()) { + item_text = vformat(TTR("Unknown Type Source (ID: %d)"), source_id); + } + if (!texture.is_valid()) { + texture = missing_atlas_texture_icon; + } + + sources_list->add_item(item_text, texture); + sources_list->set_item_metadata(i, source_id); + } + + if (sources_list->get_item_count() > 0) { + if (old_current > 0) { + // Keep the current selected item if needed. + sources_list->set_current(CLAMP(old_current, 0, sources_list->get_item_count() - 1)); + } else { + sources_list->set_current(0); + } + sources_list->emit_signal(SNAME("item_selected"), sources_list->get_current()); + } + + // Synchronize + TilesEditorPlugin::get_singleton()->set_sources_lists_current(sources_list->get_current()); +} + +void TileMapEditorTilesPlugin::_update_source_display() { + // Update the atlas display. + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + int source_index = sources_list->get_current(); + if (source_index >= 0 && source_index < sources_list->get_item_count()) { + atlas_sources_split_container->show(); + missing_source_label->hide(); + + int source_id = sources_list->get_item_metadata(source_index); + TileSetSource *source = *tile_set->get_source(source_id); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + TileSetScenesCollectionSource *scenes_collection_source = Object::cast_to<TileSetScenesCollectionSource>(source); + + if (atlas_source) { + tile_atlas_view->show(); + scene_tiles_list->hide(); + invalid_source_label->hide(); + _update_atlas_view(); + } else if (scenes_collection_source) { + tile_atlas_view->hide(); + scene_tiles_list->show(); + invalid_source_label->hide(); + _update_scenes_collection_view(); + } else { + tile_atlas_view->hide(); + scene_tiles_list->hide(); + invalid_source_label->show(); + } + } else { + atlas_sources_split_container->hide(); + missing_source_label->show(); + + tile_atlas_view->hide(); + scene_tiles_list->hide(); + invalid_source_label->hide(); + } +} + +void TileMapEditorTilesPlugin::_patterns_item_list_gui_input(const Ref<InputEvent> &p_event) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + if (ED_IS_SHORTCUT("tiles_editor/paste", p_event) && p_event->is_pressed() && !p_event->is_echo()) { + select_last_pattern = true; + int new_pattern_index = tile_set->get_patterns_count(); + undo_redo->create_action(TTR("Add TileSet pattern")); + undo_redo->add_do_method(*tile_set, "add_pattern", tile_map_clipboard, new_pattern_index); + undo_redo->add_undo_method(*tile_set, "remove_pattern", new_pattern_index); + undo_redo->commit_action(); + patterns_item_list->accept_event(); + } + + if (ED_IS_SHORTCUT("tiles_editor/delete", p_event) && p_event->is_pressed() && !p_event->is_echo()) { + Vector<int> selected = patterns_item_list->get_selected_items(); + undo_redo->create_action(TTR("Remove TileSet patterns")); + for (int i = 0; i < selected.size(); i++) { + int pattern_index = selected[i]; + undo_redo->add_do_method(*tile_set, "remove_pattern", pattern_index); + undo_redo->add_undo_method(*tile_set, "add_pattern", tile_set->get_pattern(pattern_index), pattern_index); + } + undo_redo->commit_action(); + patterns_item_list->accept_event(); + } +} + +void TileMapEditorTilesPlugin::_pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture) { + // TODO optimize ? + for (int i = 0; i < patterns_item_list->get_item_count(); i++) { + if (patterns_item_list->get_item_metadata(i) == p_pattern) { + patterns_item_list->set_item_icon(i, p_texture); + break; + } + } +} + +void TileMapEditorTilesPlugin::_update_patterns_list() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Recreate the items. + patterns_item_list->clear(); + for (int i = 0; i < tile_set->get_patterns_count(); i++) { + int id = patterns_item_list->add_item(""); + patterns_item_list->set_item_metadata(id, tile_set->get_pattern(i)); + TilesEditorPlugin::get_singleton()->queue_pattern_preview(tile_set, tile_set->get_pattern(i), callable_mp(this, &TileMapEditorTilesPlugin::_pattern_preview_done)); + } + + // Update the label visibility. + patterns_help_label->set_visible(patterns_item_list->get_item_count() == 0); + + // Added a new pattern, thus select the last one. + if (select_last_pattern) { + patterns_item_list->select(tile_set->get_patterns_count() - 1); + patterns_item_list->grab_focus(); + _update_selection_pattern_from_tileset_pattern_selection(); + } + select_last_pattern = false; +} + +void TileMapEditorTilesPlugin::_update_atlas_view() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + int source_id = sources_list->get_item_metadata(sources_list->get_current()); + TileSetSource *source = *tile_set->get_source(source_id); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + ERR_FAIL_COND(!atlas_source); + + tile_atlas_view->set_atlas_source(*tile_map->get_tileset(), atlas_source, source_id); + TilesEditorPlugin::get_singleton()->synchronize_atlas_view(tile_atlas_view); + tile_atlas_control->update(); +} + +void TileMapEditorTilesPlugin::_update_scenes_collection_view() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + int source_id = sources_list->get_item_metadata(sources_list->get_current()); + TileSetSource *source = *tile_set->get_source(source_id); + TileSetScenesCollectionSource *scenes_collection_source = Object::cast_to<TileSetScenesCollectionSource>(source); + ERR_FAIL_COND(!scenes_collection_source); + + // Clear the list. + scene_tiles_list->clear(); + + // Rebuild the list. + for (int i = 0; i < scenes_collection_source->get_scene_tiles_count(); i++) { + int scene_id = scenes_collection_source->get_scene_tile_id(i); + + Ref<PackedScene> scene = scenes_collection_source->get_scene_tile_scene(scene_id); + + int item_index = 0; + if (scene.is_valid()) { + item_index = scene_tiles_list->add_item(vformat("%s (Path: %s, ID: %d)", scene->get_path().get_file().get_basename(), scene->get_path(), scene_id)); + Variant udata = i; + EditorResourcePreview::get_singleton()->queue_edited_resource_preview(scene, this, "_scene_thumbnail_done", udata); + } else { + item_index = scene_tiles_list->add_item(TTR("Tile with Invalid Scene"), tiles_bottom_panel->get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons"))); + } + scene_tiles_list->set_item_metadata(item_index, scene_id); + + // Check if in selection. + if (tile_set_selection.has(TileMapCell(source_id, Vector2i(), scene_id))) { + scene_tiles_list->select(item_index, false); + } + } + + // Icon size update. + int int_size = int(EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size")) * EDSCALE; + scene_tiles_list->set_fixed_icon_size(Vector2(int_size, int_size)); +} + +void TileMapEditorTilesPlugin::_scene_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, Variant p_ud) { + int index = p_ud; + + if (index >= 0 && index < scene_tiles_list->get_item_count()) { + scene_tiles_list->set_item_icon(index, p_preview); + } +} + +void TileMapEditorTilesPlugin::_scenes_list_multi_selected(int p_index, bool p_selected) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Add or remove the Tile form the selection. + int scene_id = scene_tiles_list->get_item_metadata(p_index); + int source_id = sources_list->get_item_metadata(sources_list->get_current()); + TileSetSource *source = *tile_set->get_source(source_id); + TileSetScenesCollectionSource *scenes_collection_source = Object::cast_to<TileSetScenesCollectionSource>(source); + ERR_FAIL_COND(!scenes_collection_source); + + TileMapCell selected = TileMapCell(source_id, Vector2i(), scene_id); + + // Clear the selection if shift is not pressed. + if (!Input::get_singleton()->is_key_pressed(Key::SHIFT)) { + tile_set_selection.clear(); + } + + if (p_selected) { + tile_set_selection.insert(selected); + } else { + if (tile_set_selection.has(selected)) { + tile_set_selection.erase(selected); + } + } + + _update_selection_pattern_from_tileset_tiles_selection(); +} + +void TileMapEditorTilesPlugin::_scenes_list_nothing_selected() { + scene_tiles_list->deselect_all(); + tile_set_selection.clear(); + tile_map_selection.clear(); + selection_pattern.instantiate(); + _update_selection_pattern_from_tileset_tiles_selection(); +} + +void TileMapEditorTilesPlugin::_update_theme() { + select_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("ToolSelect"), SNAME("EditorIcons"))); + paint_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Edit"), SNAME("EditorIcons"))); + line_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("CurveLinear"), SNAME("EditorIcons"))); + rect_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Rectangle"), SNAME("EditorIcons"))); + bucket_tool_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Bucket"), SNAME("EditorIcons"))); + + picker_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); + erase_button->set_icon(tiles_bottom_panel->get_theme_icon(SNAME("Eraser"), SNAME("EditorIcons"))); + + missing_atlas_texture_icon = tiles_bottom_panel->get_theme_icon(SNAME("TileSet"), SNAME("EditorIcons")); +} + +bool TileMapEditorTilesPlugin::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { + if (!(tiles_bottom_panel->is_visible_in_tree() || patterns_bottom_panel->is_visible_in_tree())) { + // If the bottom editor is not visible, we ignore inputs. + return false; + } + + if (CanvasItemEditor::get_singleton()->get_current_tool() != CanvasItemEditor::TOOL_SELECT) { + return false; + } + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return false; + } + + if (tile_map_layer < 0) { + return false; + } + ERR_FAIL_INDEX_V(tile_map_layer, tile_map->get_layers_count(), false); + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return false; + } + + // Shortcuts + if (ED_IS_SHORTCUT("tiles_editor/cut", p_event) || ED_IS_SHORTCUT("tiles_editor/copy", p_event)) { + // Fill in the clipboard. + if (!tile_map_selection.is_empty()) { + tile_map_clipboard.instantiate(); + TypedArray<Vector2i> coords_array; + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + coords_array.push_back(E->get()); + } + tile_map_clipboard = tile_map->get_pattern(tile_map_layer, coords_array); + } + + if (ED_IS_SHORTCUT("tiles_editor/cut", p_event)) { + // Delete selected tiles. + if (!tile_map_selection.is_empty()) { + undo_redo->create_action(TTR("Delete tiles")); + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E->get(), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E->get(), tile_map->get_cell_source_id(tile_map_layer, E->get()), tile_map->get_cell_atlas_coords(tile_map_layer, E->get()), tile_map->get_cell_alternative_tile(tile_map_layer, E->get())); + } + undo_redo->add_undo_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + tile_map_selection.clear(); + undo_redo->add_do_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + undo_redo->commit_action(); + } + } + + return true; + } + if (ED_IS_SHORTCUT("tiles_editor/paste", p_event)) { + if (drag_type == DRAG_TYPE_NONE) { + drag_type = DRAG_TYPE_CLIPBOARD_PASTE; + } + CanvasItemEditor::get_singleton()->update_viewport(); + return true; + } + if (ED_IS_SHORTCUT("tiles_editor/cancel", p_event)) { + if (drag_type == DRAG_TYPE_CLIPBOARD_PASTE) { + drag_type = DRAG_TYPE_NONE; + CanvasItemEditor::get_singleton()->update_viewport(); + return true; + } + } + if (ED_IS_SHORTCUT("tiles_editor/delete", p_event)) { + // Delete selected tiles. + if (!tile_map_selection.is_empty()) { + undo_redo->create_action(TTR("Delete tiles")); + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E->get(), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E->get(), tile_map->get_cell_source_id(tile_map_layer, E->get()), tile_map->get_cell_atlas_coords(tile_map_layer, E->get()), tile_map->get_cell_alternative_tile(tile_map_layer, E->get())); + } + undo_redo->add_undo_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + tile_map_selection.clear(); + undo_redo->add_do_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + undo_redo->commit_action(); + } + return true; + } + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + has_mouse = true; + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2 mpos = xform.affine_inverse().xform(mm->get_position()); + + switch (drag_type) { + case DRAG_TYPE_PAINT: { + Map<Vector2i, TileMapCell> to_draw = _draw_line(drag_start_mouse_pos, drag_last_mouse_pos, mpos, drag_erasing); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + Vector2i coords = E.key; + if (!drag_modified.has(coords)) { + drag_modified.insert(coords, tile_map->get_cell(tile_map_layer, coords)); + } + tile_map->set_cell(tile_map_layer, coords, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + _fix_invalid_tiles_in_tile_map_selection(); + } break; + case DRAG_TYPE_BUCKET: { + Vector<Vector2i> line = TileMapEditor::get_line(tile_map, tile_map->world_to_map(drag_last_mouse_pos), tile_map->world_to_map(mpos)); + for (int i = 0; i < line.size(); i++) { + if (!drag_modified.has(line[i])) { + Map<Vector2i, TileMapCell> to_draw = _draw_bucket_fill(line[i], bucket_contiguous_checkbox->is_pressed(), drag_erasing); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + Vector2i coords = E.key; + if (!drag_modified.has(coords)) { + drag_modified.insert(coords, tile_map->get_cell(tile_map_layer, coords)); + } + tile_map->set_cell(tile_map_layer, coords, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + } + } + _fix_invalid_tiles_in_tile_map_selection(); + } break; + default: + break; + } + drag_last_mouse_pos = mpos; + CanvasItemEditor::get_singleton()->update_viewport(); + + return true; + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + has_mouse = true; + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2 mpos = xform.affine_inverse().xform(mb->get_position()); + + if (mb->get_button_index() == MouseButton::LEFT || mb->get_button_index() == MouseButton::RIGHT) { + if (mb->is_pressed()) { + // Pressed + if (erase_button->is_pressed() || mb->get_button_index() == MouseButton::RIGHT) { + drag_erasing = true; + } + + if (drag_type == DRAG_TYPE_CLIPBOARD_PASTE) { + // Do nothing. + } else if (tool_buttons_group->get_pressed_button() == select_tool_button) { + drag_start_mouse_pos = mpos; + if (tile_map_selection.has(tile_map->world_to_map(drag_start_mouse_pos)) && !mb->is_shift_pressed()) { + // Move the selection + _update_selection_pattern_from_tilemap_selection(); // Make sure the pattern is up to date before moving. + drag_type = DRAG_TYPE_MOVE; + drag_modified.clear(); + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + Vector2i coords = E->get(); + drag_modified.insert(coords, tile_map->get_cell(tile_map_layer, coords)); + tile_map->set_cell(tile_map_layer, coords, TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + } + } else { + // Select tiles + drag_type = DRAG_TYPE_SELECT; + } + } else { + // Check if we are picking a tile. + if (picker_button->is_pressed() || (Input::get_singleton()->is_key_pressed(Key::CTRL) && !Input::get_singleton()->is_key_pressed(Key::SHIFT))) { + drag_type = DRAG_TYPE_PICK; + drag_start_mouse_pos = mpos; + } else { + // Paint otherwise. + if (tool_buttons_group->get_pressed_button() == paint_tool_button && !Input::get_singleton()->is_key_pressed(Key::CTRL) && !Input::get_singleton()->is_key_pressed(Key::SHIFT)) { + drag_type = DRAG_TYPE_PAINT; + drag_start_mouse_pos = mpos; + drag_modified.clear(); + Map<Vector2i, TileMapCell> to_draw = _draw_line(drag_start_mouse_pos, mpos, mpos, drag_erasing); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + Vector2i coords = E.key; + if (!drag_modified.has(coords)) { + drag_modified.insert(coords, tile_map->get_cell(tile_map_layer, coords)); + } + tile_map->set_cell(tile_map_layer, coords, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + _fix_invalid_tiles_in_tile_map_selection(); + } else if (tool_buttons_group->get_pressed_button() == line_tool_button || (tool_buttons_group->get_pressed_button() == paint_tool_button && Input::get_singleton()->is_key_pressed(Key::SHIFT) && !Input::get_singleton()->is_key_pressed(Key::CTRL))) { + drag_type = DRAG_TYPE_LINE; + drag_start_mouse_pos = mpos; + drag_modified.clear(); + } else if (tool_buttons_group->get_pressed_button() == rect_tool_button || (tool_buttons_group->get_pressed_button() == paint_tool_button && Input::get_singleton()->is_key_pressed(Key::SHIFT) && Input::get_singleton()->is_key_pressed(Key::CTRL))) { + drag_type = DRAG_TYPE_RECT; + drag_start_mouse_pos = mpos; + drag_modified.clear(); + } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button) { + drag_type = DRAG_TYPE_BUCKET; + drag_start_mouse_pos = mpos; + drag_modified.clear(); + Vector<Vector2i> line = TileMapEditor::get_line(tile_map, tile_map->world_to_map(drag_last_mouse_pos), tile_map->world_to_map(mpos)); + for (int i = 0; i < line.size(); i++) { + if (!drag_modified.has(line[i])) { + Map<Vector2i, TileMapCell> to_draw = _draw_bucket_fill(line[i], bucket_contiguous_checkbox->is_pressed(), drag_erasing); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + Vector2i coords = E.key; + if (!drag_modified.has(coords)) { + drag_modified.insert(coords, tile_map->get_cell(tile_map_layer, coords)); + } + tile_map->set_cell(tile_map_layer, coords, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + } + } + _fix_invalid_tiles_in_tile_map_selection(); + } + } + } + + } else { + // Released + _stop_dragging(); + drag_erasing = false; + } + + CanvasItemEditor::get_singleton()->update_viewport(); + + return true; + } + drag_last_mouse_pos = mpos; + } + + return false; +} + +void TileMapEditorTilesPlugin::forward_canvas_draw_over_viewport(Control *p_overlay) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + if (tile_map_layer < 0) { + return; + } + ERR_FAIL_INDEX(tile_map_layer, tile_map->get_layers_count()); + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + if (!tile_map->is_visible_in_tree()) { + return; + } + + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2i tile_shape_size = tile_set->get_tile_size(); + + // Draw the selection. + if ((tiles_bottom_panel->is_visible_in_tree() || patterns_bottom_panel->is_visible_in_tree()) && tool_buttons_group->get_pressed_button() == select_tool_button) { + // In select mode, we only draw the current selection if we are modifying it (pressing control or shift). + if (drag_type == DRAG_TYPE_MOVE || (drag_type == DRAG_TYPE_SELECT && !Input::get_singleton()->is_key_pressed(Key::CTRL) && !Input::get_singleton()->is_key_pressed(Key::SHIFT))) { + // Do nothing + } else { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + tile_map->draw_cells_outline(p_overlay, tile_map_selection, selection_color, xform); + } + } + + // Handle the preview of the tiles to be placed. + if ((tiles_bottom_panel->is_visible_in_tree() || patterns_bottom_panel->is_visible_in_tree()) && has_mouse) { // Only if the tilemap editor is opened and the viewport is hovered. + Map<Vector2i, TileMapCell> preview; + Rect2i drawn_grid_rect; + + if (drag_type == DRAG_TYPE_PICK) { + // Draw the area being picked. + Rect2i rect = Rect2i(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(drag_last_mouse_pos) - tile_map->world_to_map(drag_start_mouse_pos)).abs(); + rect.size += Vector2i(1, 1); + for (int x = rect.position.x; x < rect.get_end().x; x++) { + for (int y = rect.position.y; y < rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + if (tile_map->get_cell_source_id(tile_map_layer, coords) != TileSet::INVALID_SOURCE) { + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(coords)); + tile_xform.set_scale(tile_shape_size); + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, Color(1.0, 1.0, 1.0), false); + } + } + } + } else if (drag_type == DRAG_TYPE_SELECT) { + // Draw the area being selected. + Rect2i rect = Rect2i(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(drag_last_mouse_pos) - tile_map->world_to_map(drag_start_mouse_pos)).abs(); + rect.size += Vector2i(1, 1); + Set<Vector2i> to_draw; + for (int x = rect.position.x; x < rect.get_end().x; x++) { + for (int y = rect.position.y; y < rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + if (tile_map->get_cell_source_id(tile_map_layer, coords) != TileSet::INVALID_SOURCE) { + to_draw.insert(coords); + } + } + } + tile_map->draw_cells_outline(p_overlay, to_draw, Color(1.0, 1.0, 1.0), xform); + } else if (drag_type == DRAG_TYPE_MOVE) { + if (!(patterns_item_list->is_visible_in_tree() && patterns_item_list->has_point(patterns_item_list->get_local_mouse_position()))) { + // Preview when moving. + Vector2i top_left; + if (!tile_map_selection.is_empty()) { + top_left = tile_map_selection.front()->get(); + } + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + top_left = top_left.min(E->get()); + } + Vector2i offset = drag_start_mouse_pos - tile_map->map_to_world(top_left); + offset = tile_map->world_to_map(drag_last_mouse_pos - offset) - tile_map->world_to_map(drag_start_mouse_pos - offset); + + TypedArray<Vector2i> selection_used_cells = selection_pattern->get_used_cells(); + for (int i = 0; i < selection_used_cells.size(); i++) { + Vector2i coords = tile_map->map_pattern(offset + top_left, selection_used_cells[i], selection_pattern); + preview[coords] = TileMapCell(selection_pattern->get_cell_source_id(selection_used_cells[i]), selection_pattern->get_cell_atlas_coords(selection_used_cells[i]), selection_pattern->get_cell_alternative_tile(selection_used_cells[i])); + } + } + } else if (drag_type == DRAG_TYPE_CLIPBOARD_PASTE) { + // Preview when pasting. + Vector2 mouse_offset = (Vector2(tile_map_clipboard->get_size()) / 2.0 - Vector2(0.5, 0.5)) * tile_set->get_tile_size(); + TypedArray<Vector2i> clipboard_used_cells = tile_map_clipboard->get_used_cells(); + for (int i = 0; i < clipboard_used_cells.size(); i++) { + Vector2i coords = tile_map->map_pattern(tile_map->world_to_map(drag_last_mouse_pos - mouse_offset), clipboard_used_cells[i], tile_map_clipboard); + preview[coords] = TileMapCell(tile_map_clipboard->get_cell_source_id(clipboard_used_cells[i]), tile_map_clipboard->get_cell_atlas_coords(clipboard_used_cells[i]), tile_map_clipboard->get_cell_alternative_tile(clipboard_used_cells[i])); + } + } else if (!picker_button->is_pressed() && !(drag_type == DRAG_TYPE_NONE && Input::get_singleton()->is_key_pressed(Key::CTRL) && !Input::get_singleton()->is_key_pressed(Key::SHIFT))) { + bool expand_grid = false; + if (tool_buttons_group->get_pressed_button() == paint_tool_button && drag_type == DRAG_TYPE_NONE) { + // Preview for a single pattern. + preview = _draw_line(drag_last_mouse_pos, drag_last_mouse_pos, drag_last_mouse_pos, erase_button->is_pressed()); + expand_grid = true; + } else if (tool_buttons_group->get_pressed_button() == line_tool_button || drag_type == DRAG_TYPE_LINE) { + if (drag_type == DRAG_TYPE_NONE) { + // Preview for a single pattern. + preview = _draw_line(drag_last_mouse_pos, drag_last_mouse_pos, drag_last_mouse_pos, erase_button->is_pressed()); + expand_grid = true; + } else if (drag_type == DRAG_TYPE_LINE) { + // Preview for a line pattern. + preview = _draw_line(drag_start_mouse_pos, drag_start_mouse_pos, drag_last_mouse_pos, drag_erasing); + expand_grid = true; + } + } else if (drag_type == DRAG_TYPE_RECT) { + // Preview for a rect pattern. + preview = _draw_rect(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(drag_last_mouse_pos), drag_erasing); + expand_grid = true; + } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button && drag_type == DRAG_TYPE_NONE) { + // Preview for a fill pattern. + preview = _draw_bucket_fill(tile_map->world_to_map(drag_last_mouse_pos), bucket_contiguous_checkbox->is_pressed(), erase_button->is_pressed()); + } + + // Expand the grid if needed + if (expand_grid && !preview.is_empty()) { + drawn_grid_rect = Rect2i(preview.front()->key(), Vector2i(1, 1)); + for (const KeyValue<Vector2i, TileMapCell> &E : preview) { + drawn_grid_rect.expand_to(E.key); + } + } + } + + if (!preview.is_empty()) { + const int fading = 5; + + // Draw the lines of the grid behind the preview. + bool display_grid = EditorSettings::get_singleton()->get("editors/tiles_editor/display_grid"); + if (display_grid) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + if (drawn_grid_rect.size.x > 0 && drawn_grid_rect.size.y > 0) { + drawn_grid_rect = drawn_grid_rect.grow(fading); + for (int x = drawn_grid_rect.position.x; x < (drawn_grid_rect.position.x + drawn_grid_rect.size.x); x++) { + for (int y = drawn_grid_rect.position.y; y < (drawn_grid_rect.position.y + drawn_grid_rect.size.y); y++) { + Vector2i pos_in_rect = Vector2i(x, y) - drawn_grid_rect.position; + + // Fade out the border of the grid. + float left_opacity = CLAMP(Math::inverse_lerp(0.0f, (float)fading, (float)pos_in_rect.x), 0.0f, 1.0f); + float right_opacity = CLAMP(Math::inverse_lerp((float)drawn_grid_rect.size.x, (float)(drawn_grid_rect.size.x - fading), (float)pos_in_rect.x), 0.0f, 1.0f); + float top_opacity = CLAMP(Math::inverse_lerp(0.0f, (float)fading, (float)pos_in_rect.y), 0.0f, 1.0f); + float bottom_opacity = CLAMP(Math::inverse_lerp((float)drawn_grid_rect.size.y, (float)(drawn_grid_rect.size.y - fading), (float)pos_in_rect.y), 0.0f, 1.0f); + float opacity = CLAMP(MIN(left_opacity, MIN(right_opacity, MIN(top_opacity, bottom_opacity))) + 0.1, 0.0f, 1.0f); + + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(Vector2(x, y))); + tile_xform.set_scale(tile_shape_size); + Color color = grid_color; + color.a = color.a * opacity; + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, color, false); + } + } + } + } + + // Draw the preview. + for (const KeyValue<Vector2i, TileMapCell> &E : preview) { + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(E.key)); + tile_xform.set_scale(tile_set->get_tile_size()); + if (!(drag_erasing || erase_button->is_pressed()) && random_tile_checkbox->is_pressed()) { + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, Color(1.0, 1.0, 1.0, 0.5), true); + } else { + if (tile_set->has_source(E.value.source_id)) { + TileSetSource *source = *tile_set->get_source(E.value.source_id); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + if (atlas_source) { + // Get tile data. + TileData *tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(E.value.get_atlas_coords(), E.value.alternative_tile)); + + // Compute the offset + Rect2i source_rect = atlas_source->get_tile_texture_region(E.value.get_atlas_coords()); + Vector2i tile_offset = atlas_source->get_tile_effective_texture_offset(E.value.get_atlas_coords(), E.value.alternative_tile); + + // Compute the destination rectangle in the CanvasItem. + Rect2 dest_rect; + dest_rect.size = source_rect.size; + + bool transpose = tile_data->get_transpose(); + if (transpose) { + dest_rect.position = (tile_map->map_to_world(E.key) - Vector2(dest_rect.size.y, dest_rect.size.x) / 2 - tile_offset); + } else { + dest_rect.position = (tile_map->map_to_world(E.key) - dest_rect.size / 2 - tile_offset); + } + + dest_rect = xform.xform(dest_rect); + + if (tile_data->get_flip_h()) { + dest_rect.size.x = -dest_rect.size.x; + } + + if (tile_data->get_flip_v()) { + dest_rect.size.y = -dest_rect.size.y; + } + + // Get the tile modulation. + Color modulate = tile_data->get_modulate(); + Color self_modulate = tile_map->get_self_modulate(); + modulate *= self_modulate; + + // Draw the tile. + p_overlay->draw_texture_rect_region(atlas_source->get_texture(), dest_rect, source_rect, modulate * Color(1.0, 1.0, 1.0, 0.5), transpose, tile_set->is_uv_clipping()); + } else { + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, Color(1.0, 1.0, 1.0, 0.5), true); + } + } else { + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, Color(0.0, 0.0, 0.0, 0.5), true); + } + } + } + } + } +} + +void TileMapEditorTilesPlugin::_mouse_exited_viewport() { + has_mouse = false; + CanvasItemEditor::get_singleton()->update_viewport(); +} + +TileMapCell TileMapEditorTilesPlugin::_pick_random_tile(Ref<TileMapPattern> p_pattern) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return TileMapCell(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return TileMapCell(); + } + + TypedArray<Vector2i> used_cells = p_pattern->get_used_cells(); + double sum = 0.0; + for (int i = 0; i < used_cells.size(); i++) { + int source_id = p_pattern->get_cell_source_id(used_cells[i]); + Vector2i atlas_coords = p_pattern->get_cell_atlas_coords(used_cells[i]); + int alternative_tile = p_pattern->get_cell_alternative_tile(used_cells[i]); + + TileSetSource *source = *tile_set->get_source(source_id); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + if (atlas_source) { + TileData *tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(atlas_coords, alternative_tile)); + ERR_FAIL_COND_V(!tile_data, TileMapCell()); + sum += tile_data->get_probability(); + } else { + sum += 1.0; + } + } + + double empty_probability = sum * scattering; + double current = 0.0; + double rand = Math::random(0.0, sum + empty_probability); + for (int i = 0; i < used_cells.size(); i++) { + int source_id = p_pattern->get_cell_source_id(used_cells[i]); + Vector2i atlas_coords = p_pattern->get_cell_atlas_coords(used_cells[i]); + int alternative_tile = p_pattern->get_cell_alternative_tile(used_cells[i]); + + TileSetSource *source = *tile_set->get_source(source_id); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + if (atlas_source) { + current += Object::cast_to<TileData>(atlas_source->get_tile_data(atlas_coords, alternative_tile))->get_probability(); + } else { + current += 1.0; + } + + if (current >= rand) { + return TileMapCell(source_id, atlas_coords, alternative_tile); + } + } + return TileMapCell(); +} + +Map<Vector2i, TileMapCell> TileMapEditorTilesPlugin::_draw_line(Vector2 p_start_drag_mouse_pos, Vector2 p_from_mouse_pos, Vector2 p_to_mouse_pos, bool p_erase) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Map<Vector2i, TileMapCell>(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Map<Vector2i, TileMapCell>(); + } + + // Get or create the pattern. + Ref<TileMapPattern> erase_pattern; + erase_pattern.instantiate(); + erase_pattern->set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + Ref<TileMapPattern> pattern = p_erase ? erase_pattern : selection_pattern; + + Map<Vector2i, TileMapCell> output; + if (!pattern->is_empty()) { + // Paint the tiles on the tile map. + if (!p_erase && random_tile_checkbox->is_pressed()) { + // Paint a random tile. + Vector<Vector2i> line = TileMapEditor::get_line(tile_map, tile_map->world_to_map(p_from_mouse_pos), tile_map->world_to_map(p_to_mouse_pos)); + for (int i = 0; i < line.size(); i++) { + output.insert(line[i], _pick_random_tile(pattern)); + } + } else { + // Paint the pattern. + // If we paint several tiles, we virtually move the mouse as if it was in the center of the "brush" + Vector2 mouse_offset = (Vector2(pattern->get_size()) / 2.0 - Vector2(0.5, 0.5)) * tile_set->get_tile_size(); + Vector2i last_hovered_cell = tile_map->world_to_map(p_from_mouse_pos - mouse_offset); + Vector2i new_hovered_cell = tile_map->world_to_map(p_to_mouse_pos - mouse_offset); + Vector2i drag_start_cell = tile_map->world_to_map(p_start_drag_mouse_pos - mouse_offset); + + TypedArray<Vector2i> used_cells = pattern->get_used_cells(); + Vector2i offset = Vector2i(Math::posmod(drag_start_cell.x, pattern->get_size().x), Math::posmod(drag_start_cell.y, pattern->get_size().y)); // Note: no posmodv for Vector2i for now. Meh.s + Vector<Vector2i> line = TileMapEditor::get_line(tile_map, (last_hovered_cell - offset) / pattern->get_size(), (new_hovered_cell - offset) / pattern->get_size()); + for (int i = 0; i < line.size(); i++) { + Vector2i top_left = line[i] * pattern->get_size() + offset; + for (int j = 0; j < used_cells.size(); j++) { + Vector2i coords = tile_map->map_pattern(top_left, used_cells[j], pattern); + output.insert(coords, TileMapCell(pattern->get_cell_source_id(used_cells[j]), pattern->get_cell_atlas_coords(used_cells[j]), pattern->get_cell_alternative_tile(used_cells[j]))); + } + } + } + } + return output; +} + +Map<Vector2i, TileMapCell> TileMapEditorTilesPlugin::_draw_rect(Vector2i p_start_cell, Vector2i p_end_cell, bool p_erase) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Map<Vector2i, TileMapCell>(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Map<Vector2i, TileMapCell>(); + } + + // Create the rect to draw. + Rect2i rect = Rect2i(p_start_cell, p_end_cell - p_start_cell).abs(); + rect.size += Vector2i(1, 1); + + // Get or create the pattern. + Ref<TileMapPattern> erase_pattern; + erase_pattern.instantiate(); + erase_pattern->set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + Ref<TileMapPattern> pattern = p_erase ? erase_pattern : selection_pattern; + + Map<Vector2i, TileMapCell> err_output; + ERR_FAIL_COND_V(pattern->is_empty(), err_output); + + // Compute the offset to align things to the bottom or right. + bool aligned_right = p_end_cell.x < p_start_cell.x; + bool valigned_bottom = p_end_cell.y < p_start_cell.y; + Vector2i offset = Vector2i(aligned_right ? -(pattern->get_size().x - (rect.get_size().x % pattern->get_size().x)) : 0, valigned_bottom ? -(pattern->get_size().y - (rect.get_size().y % pattern->get_size().y)) : 0); + + Map<Vector2i, TileMapCell> output; + if (!pattern->is_empty()) { + if (!p_erase && random_tile_checkbox->is_pressed()) { + // Paint a random tile. + for (int x = 0; x < rect.size.x; x++) { + for (int y = 0; y < rect.size.y; y++) { + Vector2i coords = rect.position + Vector2i(x, y); + output.insert(coords, _pick_random_tile(pattern)); + } + } + } else { + // Paint the pattern. + TypedArray<Vector2i> used_cells = pattern->get_used_cells(); + for (int x = 0; x <= rect.size.x / pattern->get_size().x; x++) { + for (int y = 0; y <= rect.size.y / pattern->get_size().y; y++) { + Vector2i pattern_coords = rect.position + Vector2i(x, y) * pattern->get_size() + offset; + for (int j = 0; j < used_cells.size(); j++) { + Vector2i coords = pattern_coords + used_cells[j]; + if (rect.has_point(coords)) { + output.insert(coords, TileMapCell(pattern->get_cell_source_id(used_cells[j]), pattern->get_cell_atlas_coords(used_cells[j]), pattern->get_cell_alternative_tile(used_cells[j]))); + } + } + } + } + } + } + + return output; +} + +Map<Vector2i, TileMapCell> TileMapEditorTilesPlugin::_draw_bucket_fill(Vector2i p_coords, bool p_contiguous, bool p_erase) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Map<Vector2i, TileMapCell>(); + } + + if (tile_map_layer < 0) { + return Map<Vector2i, TileMapCell>(); + } + Map<Vector2i, TileMapCell> output; + ERR_FAIL_INDEX_V(tile_map_layer, tile_map->get_layers_count(), output); + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Map<Vector2i, TileMapCell>(); + } + + // Get or create the pattern. + Ref<TileMapPattern> erase_pattern; + erase_pattern.instantiate(); + erase_pattern->set_cell(Vector2i(0, 0), TileSet::INVALID_SOURCE, TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE); + Ref<TileMapPattern> pattern = p_erase ? erase_pattern : selection_pattern; + + if (!pattern->is_empty()) { + TileMapCell source_cell = tile_map->get_cell(tile_map_layer, p_coords); + + // If we are filling empty tiles, compute the tilemap boundaries. + Rect2i boundaries; + if (source_cell.source_id == TileSet::INVALID_SOURCE) { + boundaries = tile_map->get_used_rect(); + } + + if (p_contiguous) { + // Replace continuous tiles like the source. + Set<Vector2i> already_checked; + List<Vector2i> to_check; + to_check.push_back(p_coords); + while (!to_check.is_empty()) { + Vector2i coords = to_check.back()->get(); + to_check.pop_back(); + if (!already_checked.has(coords)) { + if (source_cell.source_id == tile_map->get_cell_source_id(tile_map_layer, coords) && + source_cell.get_atlas_coords() == tile_map->get_cell_atlas_coords(tile_map_layer, coords) && + source_cell.alternative_tile == tile_map->get_cell_alternative_tile(tile_map_layer, coords) && + (source_cell.source_id != TileSet::INVALID_SOURCE || boundaries.has_point(coords))) { + if (!p_erase && random_tile_checkbox->is_pressed()) { + // Paint a random tile. + output.insert(coords, _pick_random_tile(pattern)); + } else { + // Paint the pattern. + Vector2i pattern_coords = (coords - p_coords) % pattern->get_size(); // Note: it would be good to have posmodv for Vector2i. + pattern_coords.x = pattern_coords.x < 0 ? pattern_coords.x + pattern->get_size().x : pattern_coords.x; + pattern_coords.y = pattern_coords.y < 0 ? pattern_coords.y + pattern->get_size().y : pattern_coords.y; + if (pattern->has_cell(pattern_coords)) { + output.insert(coords, TileMapCell(pattern->get_cell_source_id(pattern_coords), pattern->get_cell_atlas_coords(pattern_coords), pattern->get_cell_alternative_tile(pattern_coords))); + } else { + output.insert(coords, TileMapCell()); + } + } + + // Get surrounding tiles (handles different tile shapes). + TypedArray<Vector2i> around = tile_map->get_surrounding_tiles(coords); + for (int i = 0; i < around.size(); i++) { + to_check.push_back(around[i]); + } + } + already_checked.insert(coords); + } + } + } else { + // Replace all tiles like the source. + TypedArray<Vector2i> to_check; + if (source_cell.source_id == TileSet::INVALID_SOURCE) { + Rect2i rect = tile_map->get_used_rect(); + if (rect.has_no_area()) { + rect = Rect2i(p_coords, Vector2i(1, 1)); + } + for (int x = boundaries.position.x; x < boundaries.get_end().x; x++) { + for (int y = boundaries.position.y; y < boundaries.get_end().y; y++) { + to_check.append(Vector2i(x, y)); + } + } + } else { + to_check = tile_map->get_used_cells(tile_map_layer); + } + for (int i = 0; i < to_check.size(); i++) { + Vector2i coords = to_check[i]; + if (source_cell.source_id == tile_map->get_cell_source_id(tile_map_layer, coords) && + source_cell.get_atlas_coords() == tile_map->get_cell_atlas_coords(tile_map_layer, coords) && + source_cell.alternative_tile == tile_map->get_cell_alternative_tile(tile_map_layer, coords) && + (source_cell.source_id != TileSet::INVALID_SOURCE || boundaries.has_point(coords))) { + if (!p_erase && random_tile_checkbox->is_pressed()) { + // Paint a random tile. + output.insert(coords, _pick_random_tile(pattern)); + } else { + // Paint the pattern. + Vector2i pattern_coords = (coords - p_coords) % pattern->get_size(); // Note: it would be good to have posmodv for Vector2i. + pattern_coords.x = pattern_coords.x < 0 ? pattern_coords.x + pattern->get_size().x : pattern_coords.x; + pattern_coords.y = pattern_coords.y < 0 ? pattern_coords.y + pattern->get_size().y : pattern_coords.y; + if (pattern->has_cell(pattern_coords)) { + output.insert(coords, TileMapCell(pattern->get_cell_source_id(pattern_coords), pattern->get_cell_atlas_coords(pattern_coords), pattern->get_cell_alternative_tile(pattern_coords))); + } else { + output.insert(coords, TileMapCell()); + } + } + } + } + } + } + return output; +} + +void TileMapEditorTilesPlugin::_stop_dragging() { + if (drag_type == DRAG_TYPE_NONE) { + return; + } + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + if (tile_map_layer < 0) { + return; + } + ERR_FAIL_INDEX(tile_map_layer, tile_map->get_layers_count()); + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2 mpos = xform.affine_inverse().xform(CanvasItemEditor::get_singleton()->get_viewport_control()->get_local_mouse_position()); + + switch (drag_type) { + case DRAG_TYPE_SELECT: { + undo_redo->create_action(TTR("Change selection")); + undo_redo->add_undo_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + + if (!Input::get_singleton()->is_key_pressed(Key::SHIFT) && !Input::get_singleton()->is_key_pressed(Key::CTRL)) { + tile_map_selection.clear(); + } + Rect2i rect = Rect2i(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(mpos) - tile_map->world_to_map(drag_start_mouse_pos)).abs(); + for (int x = rect.position.x; x <= rect.get_end().x; x++) { + for (int y = rect.position.y; y <= rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + if (Input::get_singleton()->is_key_pressed(Key::CTRL)) { + if (tile_map_selection.has(coords)) { + tile_map_selection.erase(coords); + } + } else { + if (tile_map->get_cell_source_id(tile_map_layer, coords) != TileSet::INVALID_SOURCE) { + tile_map_selection.insert(coords); + } + } + } + } + undo_redo->add_do_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + undo_redo->commit_action(false); + + _update_selection_pattern_from_tilemap_selection(); + _update_tileset_selection_from_selection_pattern(); + } break; + case DRAG_TYPE_MOVE: { + if (patterns_item_list->is_visible_in_tree() && patterns_item_list->has_point(patterns_item_list->get_local_mouse_position())) { + // Restore the cells. + for (KeyValue<Vector2i, TileMapCell> kv : drag_modified) { + tile_map->set_cell(tile_map_layer, kv.key, kv.value.source_id, kv.value.get_atlas_coords(), kv.value.alternative_tile); + } + + // Creating a pattern in the pattern list. + select_last_pattern = true; + int new_pattern_index = tile_set->get_patterns_count(); + undo_redo->create_action(TTR("Add TileSet pattern")); + undo_redo->add_do_method(*tile_set, "add_pattern", selection_pattern, new_pattern_index); + undo_redo->add_undo_method(*tile_set, "remove_pattern", new_pattern_index); + undo_redo->commit_action(); + } else { + // Get the top-left cell. + Vector2i top_left; + if (!tile_map_selection.is_empty()) { + top_left = tile_map_selection.front()->get(); + } + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + top_left = top_left.min(E->get()); + } + + // Get the offset from the mouse. + Vector2i offset = drag_start_mouse_pos - tile_map->map_to_world(top_left); + offset = tile_map->world_to_map(mpos - offset) - tile_map->world_to_map(drag_start_mouse_pos - offset); + + TypedArray<Vector2i> selection_used_cells = selection_pattern->get_used_cells(); + + // Build the list of cells to undo. + Vector2i coords; + Map<Vector2i, TileMapCell> cells_undo; + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left, selection_used_cells[i], selection_pattern); + cells_undo[coords] = TileMapCell(drag_modified[coords].source_id, drag_modified[coords].get_atlas_coords(), drag_modified[coords].alternative_tile); + coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); + cells_undo[coords] = TileMapCell(tile_map->get_cell_source_id(tile_map_layer, coords), tile_map->get_cell_atlas_coords(tile_map_layer, coords), tile_map->get_cell_alternative_tile(tile_map_layer, coords)); + } + + // Build the list of cells to do. + Map<Vector2i, TileMapCell> cells_do; + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left, selection_used_cells[i], selection_pattern); + cells_do[coords] = TileMapCell(); + } + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); + cells_do[coords] = TileMapCell(selection_pattern->get_cell_source_id(selection_used_cells[i]), selection_pattern->get_cell_atlas_coords(selection_used_cells[i]), selection_pattern->get_cell_alternative_tile(selection_used_cells[i])); + } + + // Move the tiles. + undo_redo->create_action(TTR("Move tiles")); + for (Map<Vector2i, TileMapCell>::Element *E = cells_do.front(); E; E = E->next()) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E->key(), E->get().source_id, E->get().get_atlas_coords(), E->get().alternative_tile); + } + for (Map<Vector2i, TileMapCell>::Element *E = cells_undo.front(); E; E = E->next()) { + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E->key(), E->get().source_id, E->get().get_atlas_coords(), E->get().alternative_tile); + } + + // Update the selection. + undo_redo->add_undo_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + tile_map_selection.clear(); + for (int i = 0; i < selection_used_cells.size(); i++) { + coords = tile_map->map_pattern(top_left + offset, selection_used_cells[i], selection_pattern); + tile_map_selection.insert(coords); + } + undo_redo->add_do_method(this, "_set_tile_map_selection", _get_tile_map_selection()); + undo_redo->commit_action(); + } + } break; + case DRAG_TYPE_PICK: { + Rect2i rect = Rect2i(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(mpos) - tile_map->world_to_map(drag_start_mouse_pos)).abs(); + rect.size += Vector2i(1, 1); + + TypedArray<Vector2i> coords_array; + for (int x = rect.position.x; x < rect.get_end().x; x++) { + for (int y = rect.position.y; y < rect.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + if (tile_map->get_cell_source_id(tile_map_layer, coords) != TileSet::INVALID_SOURCE) { + coords_array.push_back(coords); + } + } + } + Ref<TileMapPattern> new_selection_pattern = tile_map->get_pattern(tile_map_layer, coords_array); + if (!new_selection_pattern->is_empty()) { + selection_pattern = new_selection_pattern; + _update_tileset_selection_from_selection_pattern(); + } + picker_button->set_pressed(false); + } break; + case DRAG_TYPE_PAINT: { + undo_redo->create_action(TTR("Paint tiles")); + for (const KeyValue<Vector2i, TileMapCell> &E : drag_modified) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + undo_redo->commit_action(false); + } break; + case DRAG_TYPE_LINE: { + Map<Vector2i, TileMapCell> to_draw = _draw_line(drag_start_mouse_pos, drag_start_mouse_pos, mpos, drag_erasing); + undo_redo->create_action(TTR("Paint tiles")); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + } + undo_redo->commit_action(); + } break; + case DRAG_TYPE_RECT: { + Map<Vector2i, TileMapCell> to_draw = _draw_rect(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(mpos), drag_erasing); + undo_redo->create_action(TTR("Paint tiles")); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + } + undo_redo->commit_action(); + } break; + case DRAG_TYPE_BUCKET: { + undo_redo->create_action(TTR("Paint tiles")); + for (const KeyValue<Vector2i, TileMapCell> &E : drag_modified) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + undo_redo->commit_action(false); + } break; + case DRAG_TYPE_CLIPBOARD_PASTE: { + Vector2 mouse_offset = (Vector2(tile_map_clipboard->get_size()) / 2.0 - Vector2(0.5, 0.5)) * tile_set->get_tile_size(); + undo_redo->create_action(TTR("Paste tiles")); + TypedArray<Vector2i> used_cells = tile_map_clipboard->get_used_cells(); + for (int i = 0; i < used_cells.size(); i++) { + Vector2i coords = tile_map->map_pattern(tile_map->world_to_map(mpos - mouse_offset), used_cells[i], tile_map_clipboard); + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, coords, tile_map_clipboard->get_cell_source_id(used_cells[i]), tile_map_clipboard->get_cell_atlas_coords(used_cells[i]), tile_map_clipboard->get_cell_alternative_tile(used_cells[i])); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, coords, tile_map->get_cell_source_id(tile_map_layer, coords), tile_map->get_cell_atlas_coords(tile_map_layer, coords), tile_map->get_cell_alternative_tile(tile_map_layer, coords)); + } + undo_redo->commit_action(); + } break; + default: + break; + } + drag_type = DRAG_TYPE_NONE; +} + +void TileMapEditorTilesPlugin::_update_fix_selected_and_hovered() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + hovered_tile.source_id = TileSet::INVALID_SOURCE; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + tile_set_selection.clear(); + patterns_item_list->deselect_all(); + tile_map_selection.clear(); + selection_pattern.instantiate(); + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + hovered_tile.source_id = TileSet::INVALID_SOURCE; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + tile_set_selection.clear(); + patterns_item_list->deselect_all(); + tile_map_selection.clear(); + selection_pattern.instantiate(); + return; + } + + int source_index = sources_list->get_current(); + if (source_index < 0 || source_index >= sources_list->get_item_count()) { + hovered_tile.source_id = TileSet::INVALID_SOURCE; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + tile_set_selection.clear(); + patterns_item_list->deselect_all(); + tile_map_selection.clear(); + selection_pattern.instantiate(); + return; + } + + int source_id = sources_list->get_item_metadata(source_index); + + // Clear hovered if needed. + if (source_id != hovered_tile.source_id || + !tile_set->has_source(hovered_tile.source_id) || + !tile_set->get_source(hovered_tile.source_id)->has_tile(hovered_tile.get_atlas_coords()) || + !tile_set->get_source(hovered_tile.source_id)->has_alternative_tile(hovered_tile.get_atlas_coords(), hovered_tile.alternative_tile)) { + hovered_tile.source_id = TileSet::INVALID_SOURCE; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + } + + // Selection if needed. + for (Set<TileMapCell>::Element *E = tile_set_selection.front(); E; E = E->next()) { + const TileMapCell *selected = &(E->get()); + if (!tile_set->has_source(selected->source_id) || + !tile_set->get_source(selected->source_id)->has_tile(selected->get_atlas_coords()) || + !tile_set->get_source(selected->source_id)->has_alternative_tile(selected->get_atlas_coords(), selected->alternative_tile)) { + tile_set_selection.erase(E); + } + } + + if (!tile_map_selection.is_empty()) { + _update_selection_pattern_from_tilemap_selection(); + } else if (tiles_bottom_panel->is_visible_in_tree()) { + _update_selection_pattern_from_tileset_tiles_selection(); + } else { + _update_selection_pattern_from_tileset_pattern_selection(); + } +} + +void TileMapEditorTilesPlugin::_fix_invalid_tiles_in_tile_map_selection() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Set<Vector2i> to_remove; + for (Vector2i selected : tile_map_selection) { + TileMapCell cell = tile_map->get_cell(tile_map_layer, selected); + if (cell.source_id == TileSet::INVALID_SOURCE && cell.get_atlas_coords() == TileSetSource::INVALID_ATLAS_COORDS && cell.alternative_tile == TileSetAtlasSource::INVALID_TILE_ALTERNATIVE) { + to_remove.insert(selected); + } + } + + for (Vector2i cell : to_remove) { + tile_map_selection.erase(cell); + } +} + +void TileMapEditorTilesPlugin::_update_selection_pattern_from_tilemap_selection() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + ERR_FAIL_INDEX(tile_map_layer, tile_map->get_layers_count()); + + selection_pattern.instantiate(); + + TypedArray<Vector2i> coords_array; + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + coords_array.push_back(E->get()); + } + selection_pattern = tile_map->get_pattern(tile_map_layer, coords_array); +} + +void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_tiles_selection() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Clear the tilemap selection. + tile_map_selection.clear(); + + // Clear the selected pattern. + selection_pattern.instantiate(); + + // Group per source. + Map<int, List<const TileMapCell *>> per_source; + for (Set<TileMapCell>::Element *E = tile_set_selection.front(); E; E = E->next()) { + per_source[E->get().source_id].push_back(&(E->get())); + } + + int vertical_offset = 0; + for (const KeyValue<int, List<const TileMapCell *>> &E_source : per_source) { + // Per source. + List<const TileMapCell *> unorganized; + Rect2i encompassing_rect_coords; + Map<Vector2i, const TileMapCell *> organized_pattern; + + TileSetSource *source = *tile_set->get_source(E_source.key); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + if (atlas_source) { + // Organize using coordinates. + for (const TileMapCell *current : E_source.value) { + if (current->alternative_tile == 0) { + organized_pattern[current->get_atlas_coords()] = current; + } else { + unorganized.push_back(current); + } + } + + // Compute the encompassing rect for the organized pattern. + Map<Vector2i, const TileMapCell *>::Element *E_cell = organized_pattern.front(); + if (E_cell) { + encompassing_rect_coords = Rect2i(E_cell->key(), Vector2i(1, 1)); + for (; E_cell; E_cell = E_cell->next()) { + encompassing_rect_coords.expand_to(E_cell->key() + Vector2i(1, 1)); + encompassing_rect_coords.expand_to(E_cell->key()); + } + } + } else { + // Add everything unorganized. + for (const TileMapCell *cell : E_source.value) { + unorganized.push_back(cell); + } + } + + // Now add everything to the output pattern. + for (const KeyValue<Vector2i, const TileMapCell *> &E_cell : organized_pattern) { + selection_pattern->set_cell(E_cell.key - encompassing_rect_coords.position + Vector2i(0, vertical_offset), E_cell.value->source_id, E_cell.value->get_atlas_coords(), E_cell.value->alternative_tile); + } + Vector2i organized_size = selection_pattern->get_size(); + int unorganized_index = 0; + for (const TileMapCell *cell : unorganized) { + selection_pattern->set_cell(Vector2(organized_size.x + unorganized_index, vertical_offset), cell->source_id, cell->get_atlas_coords(), cell->alternative_tile); + unorganized_index++; + } + vertical_offset += MAX(organized_size.y, 1); + } + CanvasItemEditor::get_singleton()->update_viewport(); +} + +void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Clear the tilemap selection. + tile_map_selection.clear(); + + // Clear the selected pattern. + selection_pattern.instantiate(); + + if (patterns_item_list->get_selected_items().size() >= 1) { + selection_pattern = patterns_item_list->get_item_metadata(patterns_item_list->get_selected_items()[0]); + } + + CanvasItemEditor::get_singleton()->update_viewport(); +} + +void TileMapEditorTilesPlugin::_update_tileset_selection_from_selection_pattern() { + tile_set_selection.clear(); + TypedArray<Vector2i> used_cells = selection_pattern->get_used_cells(); + for (int i = 0; i < used_cells.size(); i++) { + Vector2i coords = used_cells[i]; + if (selection_pattern->get_cell_source_id(coords) != TileSet::INVALID_SOURCE) { + tile_set_selection.insert(TileMapCell(selection_pattern->get_cell_source_id(coords), selection_pattern->get_cell_atlas_coords(coords), selection_pattern->get_cell_alternative_tile(coords))); + } + } + _update_source_display(); + tile_atlas_control->update(); + alternative_tiles_control->update(); +} + +void TileMapEditorTilesPlugin::_tile_atlas_control_draw() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + int source_index = sources_list->get_current(); + if (source_index < 0 || source_index >= sources_list->get_item_count()) { + return; + } + + int source_id = sources_list->get_item_metadata(source_index); + if (!tile_set->has_source(source_id)) { + return; + } + + TileSetAtlasSource *atlas = Object::cast_to<TileSetAtlasSource>(*tile_set->get_source(source_id)); + if (!atlas) { + return; + } + + // Draw the selection. + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + for (Set<TileMapCell>::Element *E = tile_set_selection.front(); E; E = E->next()) { + if (E->get().source_id == source_id && E->get().alternative_tile == 0) { + for (int frame = 0; frame < atlas->get_tile_animation_frames_count(E->get().get_atlas_coords()); frame++) { + Color color = selection_color; + if (frame > 0) { + color.a *= 0.3; + } + tile_atlas_control->draw_rect(atlas->get_tile_texture_region(E->get().get_atlas_coords(), frame), color, false); + } + } + } + + // Draw the hovered tile. + if (hovered_tile.get_atlas_coords() != TileSetSource::INVALID_ATLAS_COORDS && hovered_tile.alternative_tile == 0 && !tile_set_dragging_selection) { + for (int frame = 0; frame < atlas->get_tile_animation_frames_count(hovered_tile.get_atlas_coords()); frame++) { + Color color = Color(1.0, 1.0, 1.0); + if (frame > 0) { + color.a *= 0.3; + } + tile_atlas_control->draw_rect(atlas->get_tile_texture_region(hovered_tile.get_atlas_coords(), frame), color, false); + } + } + + // Draw the selection rect. + if (tile_set_dragging_selection) { + Vector2i start_tile = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_set_drag_start_mouse_pos); + Vector2i end_tile = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + + Rect2i region = Rect2i(start_tile, end_tile - start_tile).abs(); + region.size += Vector2i(1, 1); + + Set<Vector2i> to_draw; + for (int x = region.position.x; x < region.get_end().x; x++) { + for (int y = region.position.y; y < region.get_end().y; y++) { + Vector2i tile = atlas->get_tile_at_coords(Vector2i(x, y)); + if (tile != TileSetSource::INVALID_ATLAS_COORDS) { + to_draw.insert(tile); + } + } + } + Color selection_rect_color = selection_color.lightened(0.2); + for (Set<Vector2i>::Element *E = to_draw.front(); E; E = E->next()) { + tile_atlas_control->draw_rect(atlas->get_tile_texture_region(E->get()), selection_rect_color, false); + } + } +} + +void TileMapEditorTilesPlugin::_tile_atlas_control_mouse_exited() { + hovered_tile.source_id = TileSet::INVALID_SOURCE; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + tile_set_dragging_selection = false; + tile_atlas_control->update(); +} + +void TileMapEditorTilesPlugin::_tile_atlas_control_gui_input(const Ref<InputEvent> &p_event) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + int source_index = sources_list->get_current(); + if (source_index < 0 || source_index >= sources_list->get_item_count()) { + return; + } + + int source_id = sources_list->get_item_metadata(source_index); + if (!tile_set->has_source(source_id)) { + return; + } + + TileSetAtlasSource *atlas = Object::cast_to<TileSetAtlasSource>(*tile_set->get_source(source_id)); + if (!atlas) { + return; + } + + // Update the hovered tile + hovered_tile.source_id = source_id; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + Vector2i coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + coords = atlas->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + hovered_tile.set_atlas_coords(coords); + hovered_tile.alternative_tile = 0; + } + } + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + tile_atlas_control->update(); + alternative_tiles_control->update(); + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { // Pressed + tile_set_dragging_selection = true; + tile_set_drag_start_mouse_pos = tile_atlas_control->get_local_mouse_position(); + if (!mb->is_shift_pressed()) { + tile_set_selection.clear(); + } + + if (hovered_tile.get_atlas_coords() != TileSetSource::INVALID_ATLAS_COORDS && hovered_tile.alternative_tile == 0) { + if (mb->is_shift_pressed() && tile_set_selection.has(TileMapCell(source_id, hovered_tile.get_atlas_coords(), 0))) { + tile_set_selection.erase(TileMapCell(source_id, hovered_tile.get_atlas_coords(), 0)); + } else { + tile_set_selection.insert(TileMapCell(source_id, hovered_tile.get_atlas_coords(), 0)); + } + } + _update_selection_pattern_from_tileset_tiles_selection(); + } else { // Released + if (tile_set_dragging_selection) { + if (!mb->is_shift_pressed()) { + tile_set_selection.clear(); + } + // Compute the covered area. + Vector2i start_tile = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_set_drag_start_mouse_pos); + Vector2i end_tile = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + if (start_tile != TileSetSource::INVALID_ATLAS_COORDS && end_tile != TileSetSource::INVALID_ATLAS_COORDS) { + Rect2i region = Rect2i(start_tile, end_tile - start_tile).abs(); + region.size += Vector2i(1, 1); + + // To update the selection, we copy the selected/not selected status of the tiles we drag from. + Vector2i start_coords = atlas->get_tile_at_coords(start_tile); + if (mb->is_shift_pressed() && start_coords != TileSetSource::INVALID_ATLAS_COORDS && !tile_set_selection.has(TileMapCell(source_id, start_coords, 0))) { + // Remove from the selection. + for (int x = region.position.x; x < region.get_end().x; x++) { + for (int y = region.position.y; y < region.get_end().y; y++) { + Vector2i tile_coords = atlas->get_tile_at_coords(Vector2i(x, y)); + if (tile_coords != TileSetSource::INVALID_ATLAS_COORDS && tile_set_selection.has(TileMapCell(source_id, tile_coords, 0))) { + tile_set_selection.erase(TileMapCell(source_id, tile_coords, 0)); + } + } + } + } else { + // Insert in the selection. + for (int x = region.position.x; x < region.get_end().x; x++) { + for (int y = region.position.y; y < region.get_end().y; y++) { + Vector2i tile_coords = atlas->get_tile_at_coords(Vector2i(x, y)); + if (tile_coords != TileSetSource::INVALID_ATLAS_COORDS) { + tile_set_selection.insert(TileMapCell(source_id, tile_coords, 0)); + } + } + } + } + } + _update_selection_pattern_from_tileset_tiles_selection(); + } + tile_set_dragging_selection = false; + } + tile_atlas_control->update(); + } +} + +void TileMapEditorTilesPlugin::_tile_alternatives_control_draw() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + int source_index = sources_list->get_current(); + if (source_index < 0 || source_index >= sources_list->get_item_count()) { + return; + } + + int source_id = sources_list->get_item_metadata(source_index); + if (!tile_set->has_source(source_id)) { + return; + } + + TileSetAtlasSource *atlas = Object::cast_to<TileSetAtlasSource>(*tile_set->get_source(source_id)); + if (!atlas) { + return; + } + + // Draw the selection. + for (Set<TileMapCell>::Element *E = tile_set_selection.front(); E; E = E->next()) { + if (E->get().source_id == source_id && E->get().get_atlas_coords() != TileSetSource::INVALID_ATLAS_COORDS && E->get().alternative_tile > 0) { + Rect2i rect = tile_atlas_view->get_alternative_tile_rect(E->get().get_atlas_coords(), E->get().alternative_tile); + if (rect != Rect2i()) { + alternative_tiles_control->draw_rect(rect, Color(0.2, 0.2, 1.0), false); + } + } + } + + // Draw hovered tile. + if (hovered_tile.get_atlas_coords() != TileSetSource::INVALID_ATLAS_COORDS && hovered_tile.alternative_tile > 0) { + Rect2i rect = tile_atlas_view->get_alternative_tile_rect(hovered_tile.get_atlas_coords(), hovered_tile.alternative_tile); + if (rect != Rect2i()) { + alternative_tiles_control->draw_rect(rect, Color(1.0, 1.0, 1.0), false); + } + } +} + +void TileMapEditorTilesPlugin::_tile_alternatives_control_mouse_exited() { + hovered_tile.source_id = TileSet::INVALID_SOURCE; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + tile_set_dragging_selection = false; + alternative_tiles_control->update(); +} + +void TileMapEditorTilesPlugin::_tile_alternatives_control_gui_input(const Ref<InputEvent> &p_event) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + int source_index = sources_list->get_current(); + if (source_index < 0 || source_index >= sources_list->get_item_count()) { + return; + } + + int source_id = sources_list->get_item_metadata(source_index); + if (!tile_set->has_source(source_id)) { + return; + } + + TileSetAtlasSource *atlas = Object::cast_to<TileSetAtlasSource>(*tile_set->get_source(source_id)); + if (!atlas) { + return; + } + + // Update the hovered tile + hovered_tile.source_id = source_id; + hovered_tile.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + hovered_tile.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + Vector3i alternative_coords = tile_atlas_view->get_alternative_tile_at_pos(alternative_tiles_control->get_local_mouse_position()); + Vector2i coords = Vector2i(alternative_coords.x, alternative_coords.y); + int alternative = alternative_coords.z; + if (coords != TileSetSource::INVALID_ATLAS_COORDS && alternative != TileSetSource::INVALID_TILE_ALTERNATIVE) { + hovered_tile.set_atlas_coords(coords); + hovered_tile.alternative_tile = alternative; + } + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + tile_atlas_control->update(); + alternative_tiles_control->update(); + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { // Pressed + // Left click pressed. + if (!mb->is_shift_pressed()) { + tile_set_selection.clear(); + } + + if (coords != TileSetSource::INVALID_ATLAS_COORDS && alternative != TileSetAtlasSource::INVALID_TILE_ALTERNATIVE) { + if (mb->is_shift_pressed() && tile_set_selection.has(TileMapCell(source_id, hovered_tile.get_atlas_coords(), hovered_tile.alternative_tile))) { + tile_set_selection.erase(TileMapCell(source_id, hovered_tile.get_atlas_coords(), hovered_tile.alternative_tile)); + } else { + tile_set_selection.insert(TileMapCell(source_id, hovered_tile.get_atlas_coords(), hovered_tile.alternative_tile)); + } + } + _update_selection_pattern_from_tileset_tiles_selection(); + } + tile_atlas_control->update(); + alternative_tiles_control->update(); + } +} + +void TileMapEditorTilesPlugin::_set_tile_map_selection(const TypedArray<Vector2i> &p_selection) { + tile_map_selection.clear(); + for (int i = 0; i < p_selection.size(); i++) { + tile_map_selection.insert(p_selection[i]); + } + _update_selection_pattern_from_tilemap_selection(); + _update_tileset_selection_from_selection_pattern(); + CanvasItemEditor::get_singleton()->update_viewport(); +} + +TypedArray<Vector2i> TileMapEditorTilesPlugin::_get_tile_map_selection() const { + TypedArray<Vector2i> output; + for (Set<Vector2i>::Element *E = tile_map_selection.front(); E; E = E->next()) { + output.push_back(E->get()); + } + return output; +} + +void TileMapEditorTilesPlugin::edit(ObjectID p_tile_map_id, int p_tile_map_layer) { + _stop_dragging(); // Avoids staying in a wrong drag state. + + if (tile_map_id != p_tile_map_id) { + tile_map_id = p_tile_map_id; + + // Clear the selection. + tile_set_selection.clear(); + patterns_item_list->deselect_all(); + tile_map_selection.clear(); + selection_pattern.instantiate(); + } + + tile_map_layer = p_tile_map_layer; +} + +void TileMapEditorTilesPlugin::_bind_methods() { + ClassDB::bind_method(D_METHOD("_scene_thumbnail_done"), &TileMapEditorTilesPlugin::_scene_thumbnail_done); + ClassDB::bind_method(D_METHOD("_set_tile_map_selection", "selection"), &TileMapEditorTilesPlugin::_set_tile_map_selection); + ClassDB::bind_method(D_METHOD("_get_tile_map_selection"), &TileMapEditorTilesPlugin::_get_tile_map_selection); +} + +TileMapEditorTilesPlugin::TileMapEditorTilesPlugin() { + CanvasItemEditor::get_singleton()->get_viewport_control()->connect("mouse_exited", callable_mp(this, &TileMapEditorTilesPlugin::_mouse_exited_viewport)); + + // --- Shortcuts --- + ED_SHORTCUT("tiles_editor/cut", TTR("Cut"), KeyModifierMask::CMD | Key::X); + ED_SHORTCUT("tiles_editor/copy", TTR("Copy"), KeyModifierMask::CMD | Key::C); + ED_SHORTCUT("tiles_editor/paste", TTR("Paste"), KeyModifierMask::CMD | Key::V); + ED_SHORTCUT("tiles_editor/cancel", TTR("Cancel"), Key::ESCAPE); + ED_SHORTCUT("tiles_editor/delete", TTR("Delete"), Key::KEY_DELETE); + + // --- Initialize references --- + tile_map_clipboard.instantiate(); + selection_pattern.instantiate(); + + // --- Toolbar --- + toolbar = memnew(HBoxContainer); + toolbar->set_h_size_flags(Control::SIZE_EXPAND_FILL); + + HBoxContainer *tilemap_tiles_tools_buttons = memnew(HBoxContainer); + + tool_buttons_group.instantiate(); + + select_tool_button = memnew(Button); + select_tool_button->set_flat(true); + select_tool_button->set_toggle_mode(true); + select_tool_button->set_button_group(tool_buttons_group); + select_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/selection_tool", "Selection", Key::S)); + select_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(select_tool_button); + + paint_tool_button = memnew(Button); + paint_tool_button->set_flat(true); + paint_tool_button->set_toggle_mode(true); + paint_tool_button->set_button_group(tool_buttons_group); + paint_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/paint_tool", "Paint", Key::D)); + paint_tool_button->set_tooltip(TTR("Shift: Draw line.") + "\n" + TTR("Shift+Ctrl: Draw rectangle.")); + paint_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(paint_tool_button); + + line_tool_button = memnew(Button); + line_tool_button->set_flat(true); + line_tool_button->set_toggle_mode(true); + line_tool_button->set_button_group(tool_buttons_group); + line_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/line_tool", "Line", Key::L)); + line_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(line_tool_button); + + rect_tool_button = memnew(Button); + rect_tool_button->set_flat(true); + rect_tool_button->set_toggle_mode(true); + rect_tool_button->set_button_group(tool_buttons_group); + rect_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/rect_tool", "Rect", Key::R)); + rect_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(rect_tool_button); + + bucket_tool_button = memnew(Button); + bucket_tool_button->set_flat(true); + bucket_tool_button->set_toggle_mode(true); + bucket_tool_button->set_button_group(tool_buttons_group); + bucket_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/bucket_tool", "Bucket", Key::B)); + bucket_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(bucket_tool_button); + toolbar->add_child(tilemap_tiles_tools_buttons); + + // -- TileMap tool settings -- + tools_settings = memnew(HBoxContainer); + toolbar->add_child(tools_settings); + + tools_settings_vsep = memnew(VSeparator); + tools_settings->add_child(tools_settings_vsep); + + // Picker + picker_button = memnew(Button); + picker_button->set_flat(true); + picker_button->set_toggle_mode(true); + picker_button->set_shortcut(ED_SHORTCUT("tiles_editor/picker", "Picker", Key::P)); + picker_button->set_tooltip(TTR("Alternatively hold Ctrl with other tools to pick tile.")); + picker_button->connect("pressed", callable_mp(CanvasItemEditor::get_singleton(), &CanvasItemEditor::update_viewport)); + tools_settings->add_child(picker_button); + + // Erase button. + erase_button = memnew(Button); + erase_button->set_flat(true); + erase_button->set_toggle_mode(true); + erase_button->set_shortcut(ED_SHORTCUT("tiles_editor/eraser", "Eraser", Key::E)); + erase_button->set_tooltip(TTR("Alternatively use RMB to erase tiles.")); + erase_button->connect("pressed", callable_mp(CanvasItemEditor::get_singleton(), &CanvasItemEditor::update_viewport)); + tools_settings->add_child(erase_button); + + // Separator 2. + tools_settings_vsep_2 = memnew(VSeparator); + tools_settings->add_child(tools_settings_vsep_2); + + // Continuous checkbox. + bucket_contiguous_checkbox = memnew(CheckBox); + bucket_contiguous_checkbox->set_flat(true); + bucket_contiguous_checkbox->set_text(TTR("Contiguous")); + bucket_contiguous_checkbox->set_pressed(true); + tools_settings->add_child(bucket_contiguous_checkbox); + + // Random tile checkbox. + random_tile_checkbox = memnew(CheckBox); + random_tile_checkbox->set_flat(true); + random_tile_checkbox->set_text(TTR("Place Random Tile")); + random_tile_checkbox->connect("toggled", callable_mp(this, &TileMapEditorTilesPlugin::_on_random_tile_checkbox_toggled)); + tools_settings->add_child(random_tile_checkbox); + + // Random tile scattering. + scatter_label = memnew(Label); + scatter_label->set_tooltip(TTR("Defines the probability of painting nothing instead of a randomly selected tile.")); + scatter_label->set_text(TTR("Scattering:")); + tools_settings->add_child(scatter_label); + + scatter_spinbox = memnew(SpinBox); + scatter_spinbox->set_min(0.0); + scatter_spinbox->set_max(1000); + scatter_spinbox->set_step(0.001); + scatter_spinbox->set_tooltip(TTR("Defines the probability of painting nothing instead of a randomly selected tile.")); + scatter_spinbox->get_line_edit()->add_theme_constant_override("minimum_character_width", 4); + scatter_spinbox->connect("value_changed", callable_mp(this, &TileMapEditorTilesPlugin::_on_scattering_spinbox_changed)); + tools_settings->add_child(scatter_spinbox); + + _on_random_tile_checkbox_toggled(false); + + // Default tool. + paint_tool_button->set_pressed(true); + _update_toolbar(); + + // --- Bottom panel tiles --- + tiles_bottom_panel = memnew(VBoxContainer); + tiles_bottom_panel->connect("tree_entered", callable_mp(this, &TileMapEditorTilesPlugin::_update_theme)); + tiles_bottom_panel->connect("theme_changed", callable_mp(this, &TileMapEditorTilesPlugin::_update_theme)); + tiles_bottom_panel->connect("visibility_changed", callable_mp(this, &TileMapEditorTilesPlugin::_stop_dragging)); + tiles_bottom_panel->connect("visibility_changed", callable_mp(this, &TileMapEditorTilesPlugin::_tab_changed)); + tiles_bottom_panel->set_name(TTR("Tiles")); + + missing_source_label = memnew(Label); + missing_source_label->set_text(TTR("This TileMap's TileSet has no source configured. Edit the TileSet resource to add one.")); + missing_source_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + missing_source_label->set_v_size_flags(Control::SIZE_EXPAND_FILL); + missing_source_label->set_align(Label::ALIGN_CENTER); + missing_source_label->set_valign(Label::VALIGN_CENTER); + missing_source_label->hide(); + tiles_bottom_panel->add_child(missing_source_label); + + atlas_sources_split_container = memnew(HSplitContainer); + atlas_sources_split_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + atlas_sources_split_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + tiles_bottom_panel->add_child(atlas_sources_split_container); + + sources_list = memnew(ItemList); + sources_list->set_fixed_icon_size(Size2i(60, 60) * EDSCALE); + sources_list->set_h_size_flags(Control::SIZE_EXPAND_FILL); + sources_list->set_stretch_ratio(0.25); + sources_list->set_custom_minimum_size(Size2i(70, 0) * EDSCALE); + sources_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + sources_list->connect("item_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_fix_selected_and_hovered).unbind(1)); + sources_list->connect("item_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_source_display).unbind(1)); + sources_list->connect("item_selected", callable_mp(TilesEditorPlugin::get_singleton(), &TilesEditorPlugin::set_sources_lists_current)); + sources_list->connect("visibility_changed", callable_mp(TilesEditorPlugin::get_singleton(), &TilesEditorPlugin::synchronize_sources_list), varray(sources_list)); + atlas_sources_split_container->add_child(sources_list); + + // Tile atlas source. + tile_atlas_view = memnew(TileAtlasView); + tile_atlas_view->set_h_size_flags(Control::SIZE_EXPAND_FILL); + tile_atlas_view->set_v_size_flags(Control::SIZE_EXPAND_FILL); + tile_atlas_view->set_texture_grid_visible(false); + tile_atlas_view->set_tile_shape_grid_visible(false); + tile_atlas_view->connect("transform_changed", callable_mp(TilesEditorPlugin::get_singleton(), &TilesEditorPlugin::set_atlas_view_transform)); + atlas_sources_split_container->add_child(tile_atlas_view); + + tile_atlas_control = memnew(Control); + tile_atlas_control->connect("draw", callable_mp(this, &TileMapEditorTilesPlugin::_tile_atlas_control_draw)); + tile_atlas_control->connect("mouse_exited", callable_mp(this, &TileMapEditorTilesPlugin::_tile_atlas_control_mouse_exited)); + tile_atlas_control->connect("gui_input", callable_mp(this, &TileMapEditorTilesPlugin::_tile_atlas_control_gui_input)); + tile_atlas_view->add_control_over_atlas_tiles(tile_atlas_control); + + alternative_tiles_control = memnew(Control); + alternative_tiles_control->connect("draw", callable_mp(this, &TileMapEditorTilesPlugin::_tile_alternatives_control_draw)); + alternative_tiles_control->connect("mouse_exited", callable_mp(this, &TileMapEditorTilesPlugin::_tile_alternatives_control_mouse_exited)); + alternative_tiles_control->connect("gui_input", callable_mp(this, &TileMapEditorTilesPlugin::_tile_alternatives_control_gui_input)); + tile_atlas_view->add_control_over_alternative_tiles(alternative_tiles_control); + + // Scenes collection source. + scene_tiles_list = memnew(ItemList); + scene_tiles_list->set_h_size_flags(Control::SIZE_EXPAND_FILL); + scene_tiles_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + scene_tiles_list->set_drag_forwarding(this); + scene_tiles_list->set_select_mode(ItemList::SELECT_MULTI); + scene_tiles_list->connect("multi_selected", callable_mp(this, &TileMapEditorTilesPlugin::_scenes_list_multi_selected)); + scene_tiles_list->connect("nothing_selected", callable_mp(this, &TileMapEditorTilesPlugin::_scenes_list_nothing_selected)); + scene_tiles_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + atlas_sources_split_container->add_child(scene_tiles_list); + + // Invalid source label. + invalid_source_label = memnew(Label); + invalid_source_label->set_text(TTR("Invalid source selected.")); + invalid_source_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + invalid_source_label->set_v_size_flags(Control::SIZE_EXPAND_FILL); + invalid_source_label->set_align(Label::ALIGN_CENTER); + invalid_source_label->set_valign(Label::VALIGN_CENTER); + invalid_source_label->hide(); + atlas_sources_split_container->add_child(invalid_source_label); + + // --- Bottom panel patterns --- + patterns_bottom_panel = memnew(VBoxContainer); + patterns_bottom_panel->set_name(TTR("Patterns")); + patterns_bottom_panel->connect("visibility_changed", callable_mp(this, &TileMapEditorTilesPlugin::_tab_changed)); + + int thumbnail_size = 64; + patterns_item_list = memnew(ItemList); + patterns_item_list->set_max_columns(0); + patterns_item_list->set_icon_mode(ItemList::ICON_MODE_TOP); + patterns_item_list->set_fixed_column_width(thumbnail_size * 3 / 2); + patterns_item_list->set_max_text_lines(2); + patterns_item_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); + patterns_item_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + patterns_item_list->connect("gui_input", callable_mp(this, &TileMapEditorTilesPlugin::_patterns_item_list_gui_input)); + patterns_item_list->connect("item_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection).unbind(1)); + patterns_item_list->connect("item_activated", callable_mp(this, &TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection)); + patterns_item_list->connect("nothing_selected", callable_mp(this, &TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection)); + patterns_bottom_panel->add_child(patterns_item_list); + + patterns_help_label = memnew(Label); + patterns_help_label->set_text(TTR("Drag and drop or paste a TileMap selection here to store a pattern.")); + patterns_help_label->set_anchors_and_offsets_preset(Control::PRESET_CENTER); + patterns_item_list->add_child(patterns_help_label); + + // Update. + _update_source_display(); +} + +TileMapEditorTilesPlugin::~TileMapEditorTilesPlugin() { +} + +void TileMapEditorTerrainsPlugin::tile_set_changed() { + _update_terrains_cache(); + _update_terrains_tree(); + _update_tiles_list(); +} + +void TileMapEditorTerrainsPlugin::_update_toolbar() { + // Hide all settings. + for (int i = 0; i < tools_settings->get_child_count(); i++) { + Object::cast_to<CanvasItem>(tools_settings->get_child(i))->hide(); + } + + // Show only the correct settings. + if (tool_buttons_group->get_pressed_button() == paint_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->hide(); + bucket_contiguous_checkbox->hide(); + } else if (tool_buttons_group->get_pressed_button() == line_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->hide(); + bucket_contiguous_checkbox->hide(); + } else if (tool_buttons_group->get_pressed_button() == rect_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->hide(); + bucket_contiguous_checkbox->hide(); + } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button) { + tools_settings_vsep->show(); + picker_button->show(); + erase_button->show(); + tools_settings_vsep_2->show(); + bucket_contiguous_checkbox->show(); + } +} + +Vector<TileMapEditorPlugin::TabData> TileMapEditorTerrainsPlugin::get_tabs() const { + Vector<TileMapEditorPlugin::TabData> tabs; + tabs.push_back({ toolbar, main_vbox_container }); + return tabs; +} + +Map<Vector2i, TileMapCell> TileMapEditorTerrainsPlugin::_draw_terrains(const Map<Vector2i, TileSet::TerrainsPattern> &p_to_paint, int p_terrain_set) const { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Map<Vector2i, TileMapCell>(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Map<Vector2i, TileMapCell>(); + } + + Map<Vector2i, TileMapCell> output; + + // Add the constraints from the added tiles. + Set<TileMap::TerrainConstraint> added_tiles_constraints_set; + for (const KeyValue<Vector2i, TileSet::TerrainsPattern> &E_to_paint : p_to_paint) { + Vector2i coords = E_to_paint.key; + TileSet::TerrainsPattern terrains_pattern = E_to_paint.value; + + Set<TileMap::TerrainConstraint> cell_constraints = tile_map->get_terrain_constraints_from_added_tile(coords, p_terrain_set, terrains_pattern); + for (Set<TileMap::TerrainConstraint>::Element *E = cell_constraints.front(); E; E = E->next()) { + added_tiles_constraints_set.insert(E->get()); + } + } + + // Build the list of potential tiles to replace. + Set<Vector2i> potential_to_replace; + for (const KeyValue<Vector2i, TileSet::TerrainsPattern> &E_to_paint : p_to_paint) { + Vector2i coords = E_to_paint.key; + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + if (tile_map->is_existing_neighbor(TileSet::CellNeighbor(i))) { + Vector2i neighbor = tile_map->get_neighbor_cell(coords, TileSet::CellNeighbor(i)); + if (!p_to_paint.has(neighbor)) { + potential_to_replace.insert(neighbor); + } + } + } + } + + // Set of tiles to replace + Set<Vector2i> to_replace; + + // Add the central tiles to the one to replace. + for (const KeyValue<Vector2i, TileSet::TerrainsPattern> &E_to_paint : p_to_paint) { + to_replace.insert(E_to_paint.key); + } + + // Add the constraints from the surroundings of the modified areas. + Set<TileMap::TerrainConstraint> removed_cells_constraints_set; + bool to_replace_modified = true; + while (to_replace_modified) { + // Get the constraints from the removed cells. + removed_cells_constraints_set = tile_map->get_terrain_constraints_from_removed_cells_list(tile_map_layer, to_replace, p_terrain_set); + + // Filter the sources to make sure they are in the potential_to_replace. + Map<TileMap::TerrainConstraint, Set<Vector2i>> per_constraint_tiles; + for (Set<TileMap::TerrainConstraint>::Element *E = removed_cells_constraints_set.front(); E; E = E->next()) { + Map<Vector2i, TileSet::CellNeighbor> sources_of_constraint = E->get().get_overlapping_coords_and_peering_bits(); + for (const KeyValue<Vector2i, TileSet::CellNeighbor> &E_source_tile_of_constraint : sources_of_constraint) { + if (potential_to_replace.has(E_source_tile_of_constraint.key)) { + per_constraint_tiles[E->get()].insert(E_source_tile_of_constraint.key); + } + } + } + + to_replace_modified = false; + for (Set<TileMap::TerrainConstraint>::Element *E = added_tiles_constraints_set.front(); E; E = E->next()) { + TileMap::TerrainConstraint c = E->get(); + // Check if we have a conflict in constraints. + if (removed_cells_constraints_set.has(c) && removed_cells_constraints_set.find(c)->get().get_terrain() != c.get_terrain()) { + // If we do, we search for a neighbor to remove. + if (per_constraint_tiles.has(c) && !per_constraint_tiles[c].is_empty()) { + // Remove it. + Vector2i to_add_to_remove = per_constraint_tiles[c].front()->get(); + potential_to_replace.erase(to_add_to_remove); + to_replace.insert(to_add_to_remove); + to_replace_modified = true; + for (KeyValue<TileMap::TerrainConstraint, Set<Vector2i>> &E_source_tiles_of_constraint : per_constraint_tiles) { + E_source_tiles_of_constraint.value.erase(to_add_to_remove); + } + break; + } + } + } + } + + // Combine all constraints together. + Set<TileMap::TerrainConstraint> constraints = removed_cells_constraints_set; + for (Set<TileMap::TerrainConstraint>::Element *E = added_tiles_constraints_set.front(); E; E = E->next()) { + constraints.insert(E->get()); + } + + // Remove the central tiles from the ones to replace. + for (const KeyValue<Vector2i, TileSet::TerrainsPattern> &E_to_paint : p_to_paint) { + to_replace.erase(E_to_paint.key); + } + + // Run WFC to fill the holes with the constraints. + Map<Vector2i, TileSet::TerrainsPattern> wfc_output = tile_map->terrain_wave_function_collapse(to_replace, p_terrain_set, constraints); + + // Actually paint the tiles. + for (const KeyValue<Vector2i, TileSet::TerrainsPattern> &E_to_paint : p_to_paint) { + output[E_to_paint.key] = tile_set->get_random_tile_from_terrains_pattern(p_terrain_set, E_to_paint.value); + } + + // Use the WFC run for the output. + for (const KeyValue<Vector2i, TileSet::TerrainsPattern> &E : wfc_output) { + output[E.key] = tile_set->get_random_tile_from_terrains_pattern(p_terrain_set, E.value); + } + + return output; +} + +Map<Vector2i, TileMapCell> TileMapEditorTerrainsPlugin::_draw_line(Vector2i p_start_cell, Vector2i p_end_cell, bool p_erase) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Map<Vector2i, TileMapCell>(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Map<Vector2i, TileMapCell>(); + } + + TileSet::TerrainsPattern terrains_pattern; + if (p_erase) { + terrains_pattern = TileSet::TerrainsPattern(*tile_set, selected_terrain_set); + } else { + terrains_pattern = selected_terrains_pattern; + } + + Vector<Vector2i> line = TileMapEditor::get_line(tile_map, p_start_cell, p_end_cell); + Map<Vector2i, TileSet::TerrainsPattern> to_draw; + for (int i = 0; i < line.size(); i++) { + to_draw[line[i]] = terrains_pattern; + } + return _draw_terrains(to_draw, selected_terrain_set); +} + +Map<Vector2i, TileMapCell> TileMapEditorTerrainsPlugin::_draw_rect(Vector2i p_start_cell, Vector2i p_end_cell, bool p_erase) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Map<Vector2i, TileMapCell>(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Map<Vector2i, TileMapCell>(); + } + + TileSet::TerrainsPattern terrains_pattern; + if (p_erase) { + terrains_pattern = TileSet::TerrainsPattern(*tile_set, selected_terrain_set); + } else { + terrains_pattern = selected_terrains_pattern; + } + + Rect2i rect; + rect.set_position(p_start_cell); + rect.set_end(p_end_cell); + rect = rect.abs(); + + Map<Vector2i, TileSet::TerrainsPattern> to_draw; + for (int x = rect.position.x; x <= rect.get_end().x; x++) { + for (int y = rect.position.y; y <= rect.get_end().y; y++) { + to_draw[Vector2i(x, y)] = terrains_pattern; + } + } + return _draw_terrains(to_draw, selected_terrain_set); +} + +Set<Vector2i> TileMapEditorTerrainsPlugin::_get_cells_for_bucket_fill(Vector2i p_coords, bool p_contiguous) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Set<Vector2i>(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Set<Vector2i>(); + } + + TileMapCell source_cell = tile_map->get_cell(tile_map_layer, p_coords); + + TileSet::TerrainsPattern source_pattern(*tile_set, selected_terrain_set); + if (source_cell.source_id != TileSet::INVALID_SOURCE) { + TileData *tile_data = nullptr; + Ref<TileSetSource> source = tile_set->get_source(source_cell.source_id); + Ref<TileSetAtlasSource> atlas_source = source; + if (atlas_source.is_valid()) { + tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(source_cell.get_atlas_coords(), source_cell.alternative_tile)); + } + if (!tile_data) { + return Set<Vector2i>(); + } + source_pattern = tile_data->get_terrains_pattern(); + } + + // If we are filling empty tiles, compute the tilemap boundaries. + Rect2i boundaries; + if (source_cell.source_id == TileSet::INVALID_SOURCE) { + boundaries = tile_map->get_used_rect(); + } + + Set<Vector2i> output; + if (p_contiguous) { + // Replace continuous tiles like the source. + Set<Vector2i> already_checked; + List<Vector2i> to_check; + to_check.push_back(p_coords); + while (!to_check.is_empty()) { + Vector2i coords = to_check.back()->get(); + to_check.pop_back(); + if (!already_checked.has(coords)) { + // Get the candidate cell pattern. + TileSet::TerrainsPattern candidate_pattern(*tile_set, selected_terrain_set); + if (tile_map->get_cell_source_id(tile_map_layer, coords) != TileSet::INVALID_SOURCE) { + TileData *tile_data = nullptr; + Ref<TileSetSource> source = tile_set->get_source(tile_map->get_cell_source_id(tile_map_layer, coords)); + Ref<TileSetAtlasSource> atlas_source = source; + if (atlas_source.is_valid()) { + tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(tile_map->get_cell_atlas_coords(tile_map_layer, coords), tile_map->get_cell_alternative_tile(tile_map_layer, coords))); + } + if (tile_data) { + candidate_pattern = tile_data->get_terrains_pattern(); + } + } + + // Draw. + if (candidate_pattern == source_pattern && (!source_pattern.is_erase_pattern() || boundaries.has_point(coords))) { + output.insert(coords); + + // Get surrounding tiles (handles different tile shapes). + TypedArray<Vector2i> around = tile_map->get_surrounding_tiles(coords); + for (int i = 0; i < around.size(); i++) { + to_check.push_back(around[i]); + } + } + already_checked.insert(coords); + } + } + } else { + // Replace all tiles like the source. + TypedArray<Vector2i> to_check; + if (source_cell.source_id == TileSet::INVALID_SOURCE) { + Rect2i rect = tile_map->get_used_rect(); + if (rect.has_no_area()) { + rect = Rect2i(p_coords, Vector2i(1, 1)); + } + for (int x = boundaries.position.x; x < boundaries.get_end().x; x++) { + for (int y = boundaries.position.y; y < boundaries.get_end().y; y++) { + to_check.append(Vector2i(x, y)); + } + } + } else { + to_check = tile_map->get_used_cells(tile_map_layer); + } + for (int i = 0; i < to_check.size(); i++) { + Vector2i coords = to_check[i]; + // Get the candidate cell pattern. + TileSet::TerrainsPattern candidate_pattern; + if (tile_map->get_cell_source_id(tile_map_layer, coords) != TileSet::INVALID_SOURCE) { + TileData *tile_data = nullptr; + Ref<TileSetSource> source = tile_set->get_source(tile_map->get_cell_source_id(tile_map_layer, coords)); + Ref<TileSetAtlasSource> atlas_source = source; + if (atlas_source.is_valid()) { + tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(tile_map->get_cell_atlas_coords(tile_map_layer, coords), tile_map->get_cell_alternative_tile(tile_map_layer, coords))); + } + if (tile_data) { + candidate_pattern = tile_data->get_terrains_pattern(); + } + } + + // Draw. + if (candidate_pattern == source_pattern && (!source_pattern.is_erase_pattern() || boundaries.has_point(coords))) { + output.insert(coords); + } + } + } + return output; +} + +Map<Vector2i, TileMapCell> TileMapEditorTerrainsPlugin::_draw_bucket_fill(Vector2i p_coords, bool p_contiguous, bool p_erase) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return Map<Vector2i, TileMapCell>(); + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return Map<Vector2i, TileMapCell>(); + } + + TileSet::TerrainsPattern terrains_pattern; + if (p_erase) { + terrains_pattern = TileSet::TerrainsPattern(*tile_set, selected_terrain_set); + } else { + terrains_pattern = selected_terrains_pattern; + } + + Set<Vector2i> cells_to_draw = _get_cells_for_bucket_fill(p_coords, p_contiguous); + Map<Vector2i, TileSet::TerrainsPattern> to_draw; + for (const Vector2i &coords : cells_to_draw) { + to_draw[coords] = terrains_pattern; + } + + return _draw_terrains(to_draw, selected_terrain_set); +} + +void TileMapEditorTerrainsPlugin::_stop_dragging() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2 mpos = xform.affine_inverse().xform(CanvasItemEditor::get_singleton()->get_viewport_control()->get_local_mouse_position()); + + switch (drag_type) { + case DRAG_TYPE_PICK: { + Vector2i coords = tile_map->world_to_map(mpos); + TileMapCell cell = tile_map->get_cell(tile_map_layer, coords); + TileData *tile_data = nullptr; + + Ref<TileSetSource> source = tile_set->get_source(cell.source_id); + Ref<TileSetAtlasSource> atlas_source = source; + if (atlas_source.is_valid()) { + tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(cell.get_atlas_coords(), cell.alternative_tile)); + } + + if (tile_data) { + TileSet::TerrainsPattern terrains_pattern = tile_data->get_terrains_pattern(); + + // Find the tree item for the right terrain set. + bool need_tree_item_switch = true; + TreeItem *tree_item = terrains_tree->get_selected(); + int new_terrain_set = -1; + if (tree_item) { + Dictionary metadata_dict = tree_item->get_metadata(0); + if (metadata_dict.has("terrain_set") && metadata_dict.has("terrain_id")) { + int terrain_set = metadata_dict["terrain_set"]; + int terrain_id = metadata_dict["terrain_id"]; + if (per_terrain_terrains_patterns[terrain_set][terrain_id].has(terrains_pattern)) { + new_terrain_set = terrain_set; + need_tree_item_switch = false; + } + } + } + + if (need_tree_item_switch) { + for (tree_item = terrains_tree->get_root()->get_first_child(); tree_item; tree_item = tree_item->get_next_visible()) { + Dictionary metadata_dict = tree_item->get_metadata(0); + if (metadata_dict.has("terrain_set") && metadata_dict.has("terrain_id")) { + int terrain_set = metadata_dict["terrain_set"]; + int terrain_id = metadata_dict["terrain_id"]; + if (per_terrain_terrains_patterns[terrain_set][terrain_id].has(terrains_pattern)) { + // Found + new_terrain_set = terrain_set; + tree_item->select(0); + _update_tiles_list(); + break; + } + } + } + } + + // Find the list item for the given tile. + if (tree_item) { + for (int i = 0; i < terrains_tile_list->get_item_count(); i++) { + Dictionary metadata_dict = terrains_tile_list->get_item_metadata(i); + TileSet::TerrainsPattern in_meta_terrains_pattern(*tile_set, new_terrain_set); + in_meta_terrains_pattern.set_terrains_from_array(metadata_dict["terrains_pattern"]); + if (in_meta_terrains_pattern == terrains_pattern) { + terrains_tile_list->select(i); + break; + } + } + } else { + ERR_PRINT("Terrain tile not found."); + } + } + picker_button->set_pressed(false); + } break; + case DRAG_TYPE_PAINT: { + undo_redo->create_action(TTR("Paint terrain")); + for (const KeyValue<Vector2i, TileMapCell> &E : drag_modified) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + undo_redo->commit_action(false); + } break; + case DRAG_TYPE_LINE: { + Map<Vector2i, TileMapCell> to_draw = _draw_line(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(mpos), drag_erasing); + undo_redo->create_action(TTR("Paint terrain")); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + } + undo_redo->commit_action(); + } break; + case DRAG_TYPE_RECT: { + Map<Vector2i, TileMapCell> to_draw = _draw_rect(tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(mpos), drag_erasing); + undo_redo->create_action(TTR("Paint terrain")); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + } + undo_redo->commit_action(); + } break; + case DRAG_TYPE_BUCKET: { + undo_redo->create_action(TTR("Paint terrain")); + for (const KeyValue<Vector2i, TileMapCell> &E : drag_modified) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, E.key, tile_map->get_cell_source_id(tile_map_layer, E.key), tile_map->get_cell_atlas_coords(tile_map_layer, E.key), tile_map->get_cell_alternative_tile(tile_map_layer, E.key)); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + undo_redo->commit_action(false); + } break; + + default: + break; + } + drag_type = DRAG_TYPE_NONE; +} + +void TileMapEditorTerrainsPlugin::_mouse_exited_viewport() { + has_mouse = false; + CanvasItemEditor::get_singleton()->update_viewport(); +} + +void TileMapEditorTerrainsPlugin::_update_selection() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Get the selected terrain. + selected_terrains_pattern = TileSet::TerrainsPattern(); + selected_terrain_set = -1; + + TreeItem *selected_tree_item = terrains_tree->get_selected(); + if (selected_tree_item && selected_tree_item->get_metadata(0)) { + Dictionary metadata_dict = selected_tree_item->get_metadata(0); + // Selected terrain + selected_terrain_set = metadata_dict["terrain_set"]; + + // Selected tile + if (erase_button->is_pressed()) { + selected_terrains_pattern = TileSet::TerrainsPattern(*tile_set, selected_terrain_set); + } else if (terrains_tile_list->is_anything_selected()) { + metadata_dict = terrains_tile_list->get_item_metadata(terrains_tile_list->get_selected_items()[0]); + selected_terrains_pattern = TileSet::TerrainsPattern(*tile_set, selected_terrain_set); + selected_terrains_pattern.set_terrains_from_array(metadata_dict["terrains_pattern"]); + } + } +} + +bool TileMapEditorTerrainsPlugin::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { + if (!main_vbox_container->is_visible_in_tree()) { + // If the bottom editor is not visible, we ignore inputs. + return false; + } + + if (CanvasItemEditor::get_singleton()->get_current_tool() != CanvasItemEditor::TOOL_SELECT) { + return false; + } + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return false; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return false; + } + + if (tile_map_layer < 0) { + return false; + } + ERR_FAIL_COND_V(tile_map_layer >= tile_map->get_layers_count(), false); + + _update_selection(); + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + has_mouse = true; + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2 mpos = xform.affine_inverse().xform(mm->get_position()); + + switch (drag_type) { + case DRAG_TYPE_PAINT: { + if (selected_terrain_set >= 0) { + Map<Vector2i, TileMapCell> to_draw = _draw_line(tile_map->world_to_map(drag_last_mouse_pos), tile_map->world_to_map(mpos), drag_erasing); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_modified.has(E.key)) { + drag_modified[E.key] = tile_map->get_cell(tile_map_layer, E.key); + } + tile_map->set_cell(tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + } + } break; + default: + break; + } + drag_last_mouse_pos = mpos; + CanvasItemEditor::get_singleton()->update_viewport(); + + return true; + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + has_mouse = true; + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2 mpos = xform.affine_inverse().xform(mb->get_position()); + + if (mb->get_button_index() == MouseButton::LEFT || mb->get_button_index() == MouseButton::RIGHT) { + if (mb->is_pressed()) { + // Pressed + if (erase_button->is_pressed() || mb->get_button_index() == MouseButton::RIGHT) { + drag_erasing = true; + } + + if (picker_button->is_pressed()) { + drag_type = DRAG_TYPE_PICK; + } else { + // Paint otherwise. + if (tool_buttons_group->get_pressed_button() == paint_tool_button && !Input::get_singleton()->is_key_pressed(Key::CTRL) && !Input::get_singleton()->is_key_pressed(Key::SHIFT)) { + if (selected_terrain_set < 0 || !selected_terrains_pattern.is_valid()) { + return true; + } + + drag_type = DRAG_TYPE_PAINT; + drag_start_mouse_pos = mpos; + + drag_modified.clear(); + Vector2i cell = tile_map->world_to_map(mpos); + Map<Vector2i, TileMapCell> to_draw = _draw_line(cell, cell, drag_erasing); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + drag_modified[E.key] = tile_map->get_cell(tile_map_layer, E.key); + tile_map->set_cell(tile_map_layer, E.key, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + } else if (tool_buttons_group->get_pressed_button() == line_tool_button || (tool_buttons_group->get_pressed_button() == paint_tool_button && Input::get_singleton()->is_key_pressed(Key::SHIFT) && !Input::get_singleton()->is_key_pressed(Key::CTRL))) { + if (selected_terrain_set < 0 || !selected_terrains_pattern.is_valid()) { + return true; + } + drag_type = DRAG_TYPE_LINE; + drag_start_mouse_pos = mpos; + drag_modified.clear(); + } else if (tool_buttons_group->get_pressed_button() == rect_tool_button || (tool_buttons_group->get_pressed_button() == paint_tool_button && Input::get_singleton()->is_key_pressed(Key::SHIFT) && Input::get_singleton()->is_key_pressed(Key::CTRL))) { + if (selected_terrain_set < 0 || !selected_terrains_pattern.is_valid()) { + return true; + } + drag_type = DRAG_TYPE_RECT; + drag_start_mouse_pos = mpos; + drag_modified.clear(); + } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button) { + if (selected_terrain_set < 0 || !selected_terrains_pattern.is_valid()) { + return true; + } + drag_type = DRAG_TYPE_BUCKET; + drag_start_mouse_pos = mpos; + drag_modified.clear(); + Vector<Vector2i> line = TileMapEditor::get_line(tile_map, tile_map->world_to_map(drag_last_mouse_pos), tile_map->world_to_map(mpos)); + for (int i = 0; i < line.size(); i++) { + if (!drag_modified.has(line[i])) { + Map<Vector2i, TileMapCell> to_draw = _draw_bucket_fill(line[i], bucket_contiguous_checkbox->is_pressed(), drag_erasing); + for (const KeyValue<Vector2i, TileMapCell> &E : to_draw) { + if (!drag_erasing && E.value.source_id == TileSet::INVALID_SOURCE) { + continue; + } + Vector2i coords = E.key; + if (!drag_modified.has(coords)) { + drag_modified.insert(coords, tile_map->get_cell(tile_map_layer, coords)); + } + tile_map->set_cell(tile_map_layer, coords, E.value.source_id, E.value.get_atlas_coords(), E.value.alternative_tile); + } + } + } + } + } + } else { + // Released + _stop_dragging(); + drag_erasing = false; + } + + CanvasItemEditor::get_singleton()->update_viewport(); + + return true; + } + drag_last_mouse_pos = mpos; + } + + return false; +} + +void TileMapEditorTerrainsPlugin::forward_canvas_draw_over_viewport(Control *p_overlay) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + if (tile_map_layer < 0) { + return; + } + ERR_FAIL_INDEX(tile_map_layer, tile_map->get_layers_count()); + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + if (!tile_map->is_visible_in_tree()) { + return; + } + + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Vector2i tile_shape_size = tile_set->get_tile_size(); + + // Handle the preview of the tiles to be placed. + if (main_vbox_container->is_visible_in_tree() && has_mouse) { // Only if the tilemap editor is opened and the viewport is hovered. + Set<Vector2i> preview; + Rect2i drawn_grid_rect; + + if (drag_type == DRAG_TYPE_PICK) { + // Draw the area being picked. + Vector2i coords = tile_map->world_to_map(drag_last_mouse_pos); + if (tile_map->get_cell_source_id(tile_map_layer, coords) != TileSet::INVALID_SOURCE) { + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(coords)); + tile_xform.set_scale(tile_shape_size); + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, Color(1.0, 1.0, 1.0), false); + } + } else if (!picker_button->is_pressed() && !(drag_type == DRAG_TYPE_NONE && Input::get_singleton()->is_key_pressed(Key::CTRL) && !Input::get_singleton()->is_key_pressed(Key::SHIFT))) { + bool expand_grid = false; + if (tool_buttons_group->get_pressed_button() == paint_tool_button && drag_type == DRAG_TYPE_NONE) { + // Preview for a single tile. + preview.insert(tile_map->world_to_map(drag_last_mouse_pos)); + expand_grid = true; + } else if (tool_buttons_group->get_pressed_button() == line_tool_button || drag_type == DRAG_TYPE_LINE) { + if (drag_type == DRAG_TYPE_NONE) { + // Preview for a single tile. + preview.insert(tile_map->world_to_map(drag_last_mouse_pos)); + } else if (drag_type == DRAG_TYPE_LINE) { + // Preview for a line. + Vector<Vector2i> line = TileMapEditor::get_line(tile_map, tile_map->world_to_map(drag_start_mouse_pos), tile_map->world_to_map(drag_last_mouse_pos)); + for (int i = 0; i < line.size(); i++) { + preview.insert(line[i]); + } + expand_grid = true; + } + } else if (drag_type == DRAG_TYPE_RECT) { + // Preview for a rect. + Rect2i rect; + rect.set_position(tile_map->world_to_map(drag_start_mouse_pos)); + rect.set_end(tile_map->world_to_map(drag_last_mouse_pos)); + rect = rect.abs(); + + Map<Vector2i, TileSet::TerrainsPattern> to_draw; + for (int x = rect.position.x; x <= rect.get_end().x; x++) { + for (int y = rect.position.y; y <= rect.get_end().y; y++) { + preview.insert(Vector2i(x, y)); + } + } + expand_grid = true; + } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button && drag_type == DRAG_TYPE_NONE) { + // Preview for a fill. + preview = _get_cells_for_bucket_fill(tile_map->world_to_map(drag_last_mouse_pos), bucket_contiguous_checkbox->is_pressed()); + } + + // Expand the grid if needed + if (expand_grid && !preview.is_empty()) { + drawn_grid_rect = Rect2i(preview.front()->get(), Vector2i(1, 1)); + for (const Vector2i &E : preview) { + drawn_grid_rect.expand_to(E); + } + } + } + + if (!preview.is_empty()) { + const int fading = 5; + + // Draw the lines of the grid behind the preview. + bool display_grid = EditorSettings::get_singleton()->get("editors/tiles_editor/display_grid"); + if (display_grid) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + if (drawn_grid_rect.size.x > 0 && drawn_grid_rect.size.y > 0) { + drawn_grid_rect = drawn_grid_rect.grow(fading); + for (int x = drawn_grid_rect.position.x; x < (drawn_grid_rect.position.x + drawn_grid_rect.size.x); x++) { + for (int y = drawn_grid_rect.position.y; y < (drawn_grid_rect.position.y + drawn_grid_rect.size.y); y++) { + Vector2i pos_in_rect = Vector2i(x, y) - drawn_grid_rect.position; + + // Fade out the border of the grid. + float left_opacity = CLAMP(Math::inverse_lerp(0.0f, (float)fading, (float)pos_in_rect.x), 0.0f, 1.0f); + float right_opacity = CLAMP(Math::inverse_lerp((float)drawn_grid_rect.size.x, (float)(drawn_grid_rect.size.x - fading), (float)pos_in_rect.x), 0.0f, 1.0f); + float top_opacity = CLAMP(Math::inverse_lerp(0.0f, (float)fading, (float)pos_in_rect.y), 0.0f, 1.0f); + float bottom_opacity = CLAMP(Math::inverse_lerp((float)drawn_grid_rect.size.y, (float)(drawn_grid_rect.size.y - fading), (float)pos_in_rect.y), 0.0f, 1.0f); + float opacity = CLAMP(MIN(left_opacity, MIN(right_opacity, MIN(top_opacity, bottom_opacity))) + 0.1, 0.0f, 1.0f); + + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(Vector2(x, y))); + tile_xform.set_scale(tile_shape_size); + Color color = grid_color; + color.a = color.a * opacity; + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, color, false); + } + } + } + } + + // Draw the preview. + for (const Vector2i &E : preview) { + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(E)); + tile_xform.set_scale(tile_set->get_tile_size()); + if (drag_erasing || erase_button->is_pressed()) { + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, Color(0.0, 0.0, 0.0, 0.5), true); + } else { + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, Color(1.0, 1.0, 1.0, 0.5), true); + } + } + } + } +} + +void TileMapEditorTerrainsPlugin::_update_terrains_cache() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Organizes tiles into structures. + per_terrain_terrains_patterns.resize(tile_set->get_terrain_sets_count()); + for (int i = 0; i < tile_set->get_terrain_sets_count(); i++) { + per_terrain_terrains_patterns[i].resize(tile_set->get_terrains_count(i)); + for (int j = 0; j < (int)per_terrain_terrains_patterns[i].size(); j++) { + per_terrain_terrains_patterns[i][j].clear(); + } + } + + for (int source_index = 0; source_index < tile_set->get_source_count(); source_index++) { + int source_id = tile_set->get_source_id(source_index); + Ref<TileSetSource> source = tile_set->get_source(source_id); + + Ref<TileSetAtlasSource> atlas_source = source; + if (atlas_source.is_valid()) { + for (int tile_index = 0; tile_index < source->get_tiles_count(); tile_index++) { + Vector2i tile_id = source->get_tile_id(tile_index); + for (int alternative_index = 0; alternative_index < source->get_alternative_tiles_count(tile_id); alternative_index++) { + int alternative_id = source->get_alternative_tile_id(tile_id, alternative_index); + + TileData *tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(tile_id, alternative_id)); + int terrain_set = tile_data->get_terrain_set(); + if (terrain_set >= 0) { + ERR_FAIL_INDEX(terrain_set, (int)per_terrain_terrains_patterns.size()); + + TileMapCell cell; + cell.source_id = source_id; + cell.set_atlas_coords(tile_id); + cell.alternative_tile = alternative_id; + + TileSet::TerrainsPattern terrains_pattern = tile_data->get_terrains_pattern(); + // Terrain bits. + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(terrain_set, bit)) { + int terrain = terrains_pattern.get_terrain(bit); + if (terrain >= 0 && terrain < (int)per_terrain_terrains_patterns[terrain_set].size()) { + per_terrain_terrains_patterns[terrain_set][terrain].insert(terrains_pattern); + } + } + } + } + } + } + } + } +} + +void TileMapEditorTerrainsPlugin::_update_terrains_tree() { + terrains_tree->clear(); + terrains_tree->create_item(); + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + // Fill in the terrain list. + Vector<Vector<Ref<Texture2D>>> icons = tile_set->generate_terrains_icons(Size2(16, 16) * EDSCALE); + for (int terrain_set_index = 0; terrain_set_index < tile_set->get_terrain_sets_count(); terrain_set_index++) { + // Add an item for the terrain set. + TreeItem *terrain_set_tree_item = terrains_tree->create_item(); + String matches; + if (tile_set->get_terrain_set_mode(terrain_set_index) == TileSet::TERRAIN_MODE_MATCH_CORNERS_AND_SIDES) { + terrain_set_tree_item->set_icon(0, main_vbox_container->get_theme_icon(SNAME("TerrainMatchCornersAndSides"), SNAME("EditorIcons"))); + matches = String(TTR("Matches Corners and Sides")); + } else if (tile_set->get_terrain_set_mode(terrain_set_index) == TileSet::TERRAIN_MODE_MATCH_CORNERS) { + terrain_set_tree_item->set_icon(0, main_vbox_container->get_theme_icon(SNAME("TerrainMatchCorners"), SNAME("EditorIcons"))); + matches = String(TTR("Matches Corners Only")); + } else { + terrain_set_tree_item->set_icon(0, main_vbox_container->get_theme_icon(SNAME("TerrainMatchSides"), SNAME("EditorIcons"))); + matches = String(TTR("Matches Sides Only")); + } + terrain_set_tree_item->set_text(0, vformat("Terrain Set %d (%s)", terrain_set_index, matches)); + terrain_set_tree_item->set_selectable(0, false); + + for (int terrain_index = 0; terrain_index < tile_set->get_terrains_count(terrain_set_index); terrain_index++) { + // Add the item to the terrain list. + TreeItem *terrain_tree_item = terrains_tree->create_item(terrain_set_tree_item); + terrain_tree_item->set_text(0, tile_set->get_terrain_name(terrain_set_index, terrain_index)); + terrain_tree_item->set_icon_max_width(0, 32 * EDSCALE); + terrain_tree_item->set_icon(0, icons[terrain_set_index][terrain_index]); + + Dictionary metadata_dict; + metadata_dict["terrain_set"] = terrain_set_index; + metadata_dict["terrain_id"] = terrain_index; + terrain_tree_item->set_metadata(0, metadata_dict); + } + } +} + +void TileMapEditorTerrainsPlugin::_update_tiles_list() { + terrains_tile_list->clear(); + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + TreeItem *selected_tree_item = terrains_tree->get_selected(); + if (selected_tree_item && selected_tree_item->get_metadata(0)) { + Dictionary metadata_dict = selected_tree_item->get_metadata(0); + int selected_terrain_set = metadata_dict["terrain_set"]; + int selected_terrain_id = metadata_dict["terrain_id"]; + ERR_FAIL_INDEX(selected_terrain_set, tile_set->get_terrain_sets_count()); + ERR_FAIL_INDEX(selected_terrain_id, tile_set->get_terrains_count(selected_terrain_set)); + + // Sort the items in a map by the number of corresponding terrains. + Map<int, Set<TileSet::TerrainsPattern>> sorted; + + for (Set<TileSet::TerrainsPattern>::Element *E = per_terrain_terrains_patterns[selected_terrain_set][selected_terrain_id].front(); E; E = E->next()) { + // Count the number of matching sides/terrains. + int count = 0; + + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(selected_terrain_set, bit) && E->get().get_terrain(bit) == selected_terrain_id) { + count++; + } + } + sorted[count].insert(E->get()); + } + + for (Map<int, Set<TileSet::TerrainsPattern>>::Element *E_set = sorted.back(); E_set; E_set = E_set->prev()) { + for (Set<TileSet::TerrainsPattern>::Element *E = E_set->get().front(); E; E = E->next()) { + TileSet::TerrainsPattern terrains_pattern = E->get(); + + // Get the icon. + Ref<Texture2D> icon; + Rect2 region; + bool transpose = false; + + double max_probability = -1.0; + for (const TileMapCell &cell : tile_set->get_tiles_for_terrains_pattern(selected_terrain_set, terrains_pattern)) { + Ref<TileSetSource> source = tile_set->get_source(cell.source_id); + + Ref<TileSetAtlasSource> atlas_source = source; + if (atlas_source.is_valid()) { + TileData *tile_data = Object::cast_to<TileData>(atlas_source->get_tile_data(cell.get_atlas_coords(), cell.alternative_tile)); + if (tile_data->get_probability() > max_probability) { + icon = atlas_source->get_texture(); + region = atlas_source->get_tile_texture_region(cell.get_atlas_coords()); + if (tile_data->get_flip_h()) { + region.position.x += region.size.x; + region.size.x = -region.size.x; + } + if (tile_data->get_flip_v()) { + region.position.y += region.size.y; + region.size.y = -region.size.y; + } + transpose = tile_data->get_transpose(); + max_probability = tile_data->get_probability(); + } + } + } + + // Create the ItemList's item. + int item_index = terrains_tile_list->add_item(""); + terrains_tile_list->set_item_icon(item_index, icon); + terrains_tile_list->set_item_icon_region(item_index, region); + terrains_tile_list->set_item_icon_transposed(item_index, transpose); + Dictionary list_metadata_dict; + list_metadata_dict["terrains_pattern"] = terrains_pattern.get_terrains_as_array(); + terrains_tile_list->set_item_metadata(item_index, list_metadata_dict); + } + } + if (terrains_tile_list->get_item_count() > 0) { + terrains_tile_list->select(0); + } + } +} + +void TileMapEditorTerrainsPlugin::_update_theme() { + paint_tool_button->set_icon(main_vbox_container->get_theme_icon(SNAME("Edit"), SNAME("EditorIcons"))); + line_tool_button->set_icon(main_vbox_container->get_theme_icon(SNAME("CurveLinear"), SNAME("EditorIcons"))); + rect_tool_button->set_icon(main_vbox_container->get_theme_icon(SNAME("Rectangle"), SNAME("EditorIcons"))); + bucket_tool_button->set_icon(main_vbox_container->get_theme_icon(SNAME("Bucket"), SNAME("EditorIcons"))); + + picker_button->set_icon(main_vbox_container->get_theme_icon(SNAME("ColorPick"), SNAME("EditorIcons"))); + erase_button->set_icon(main_vbox_container->get_theme_icon(SNAME("Eraser"), SNAME("EditorIcons"))); +} + +void TileMapEditorTerrainsPlugin::edit(ObjectID p_tile_map_id, int p_tile_map_layer) { + _stop_dragging(); // Avoids staying in a wrong drag state. + + tile_map_id = p_tile_map_id; + tile_map_layer = p_tile_map_layer; + + _update_terrains_cache(); + _update_terrains_tree(); + _update_tiles_list(); +} + +TileMapEditorTerrainsPlugin::TileMapEditorTerrainsPlugin() { + main_vbox_container = memnew(VBoxContainer); + main_vbox_container->connect("tree_entered", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_theme)); + main_vbox_container->connect("theme_changed", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_theme)); + main_vbox_container->set_name("Terrains"); + + HSplitContainer *tilemap_tab_terrains = memnew(HSplitContainer); + tilemap_tab_terrains->set_h_size_flags(Control::SIZE_EXPAND_FILL); + tilemap_tab_terrains->set_v_size_flags(Control::SIZE_EXPAND_FILL); + main_vbox_container->add_child(tilemap_tab_terrains); + + terrains_tree = memnew(Tree); + terrains_tree->set_h_size_flags(Control::SIZE_EXPAND_FILL); + terrains_tree->set_stretch_ratio(0.25); + terrains_tree->set_custom_minimum_size(Size2i(70, 0) * EDSCALE); + terrains_tree->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + terrains_tree->set_hide_root(true); + terrains_tree->connect("item_selected", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_tiles_list)); + tilemap_tab_terrains->add_child(terrains_tree); + + terrains_tile_list = memnew(ItemList); + terrains_tile_list->set_h_size_flags(Control::SIZE_EXPAND_FILL); + terrains_tile_list->set_max_columns(0); + terrains_tile_list->set_same_column_width(true); + terrains_tile_list->set_fixed_icon_size(Size2(30, 30) * EDSCALE); + terrains_tile_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + tilemap_tab_terrains->add_child(terrains_tile_list); + + // --- Toolbar --- + toolbar = memnew(HBoxContainer); + + HBoxContainer *tilemap_tiles_tools_buttons = memnew(HBoxContainer); + + tool_buttons_group.instantiate(); + + paint_tool_button = memnew(Button); + paint_tool_button->set_flat(true); + paint_tool_button->set_toggle_mode(true); + paint_tool_button->set_button_group(tool_buttons_group); + paint_tool_button->set_pressed(true); + paint_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/paint_tool", "Paint", Key::D)); + paint_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(paint_tool_button); + + line_tool_button = memnew(Button); + line_tool_button->set_flat(true); + line_tool_button->set_toggle_mode(true); + line_tool_button->set_button_group(tool_buttons_group); + line_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/line_tool", "Line", Key::L)); + line_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(line_tool_button); + + rect_tool_button = memnew(Button); + rect_tool_button->set_flat(true); + rect_tool_button->set_toggle_mode(true); + rect_tool_button->set_button_group(tool_buttons_group); + rect_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/rect_tool", "Rect", Key::R)); + rect_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(rect_tool_button); + + bucket_tool_button = memnew(Button); + bucket_tool_button->set_flat(true); + bucket_tool_button->set_toggle_mode(true); + bucket_tool_button->set_button_group(tool_buttons_group); + bucket_tool_button->set_shortcut(ED_SHORTCUT("tiles_editor/bucket_tool", "Bucket", Key::B)); + bucket_tool_button->connect("pressed", callable_mp(this, &TileMapEditorTerrainsPlugin::_update_toolbar)); + tilemap_tiles_tools_buttons->add_child(bucket_tool_button); + + toolbar->add_child(tilemap_tiles_tools_buttons); + + // -- TileMap tool settings -- + tools_settings = memnew(HBoxContainer); + toolbar->add_child(tools_settings); + + tools_settings_vsep = memnew(VSeparator); + tools_settings->add_child(tools_settings_vsep); + + // Picker + picker_button = memnew(Button); + picker_button->set_flat(true); + picker_button->set_toggle_mode(true); + picker_button->set_shortcut(ED_SHORTCUT("tiles_editor/picker", "Picker", Key::P)); + picker_button->connect("pressed", callable_mp(CanvasItemEditor::get_singleton(), &CanvasItemEditor::update_viewport)); + tools_settings->add_child(picker_button); + + // Erase button. + erase_button = memnew(Button); + erase_button->set_flat(true); + erase_button->set_toggle_mode(true); + erase_button->set_shortcut(ED_SHORTCUT("tiles_editor/eraser", "Eraser", Key::E)); + erase_button->connect("pressed", callable_mp(CanvasItemEditor::get_singleton(), &CanvasItemEditor::update_viewport)); + tools_settings->add_child(erase_button); + + // Separator 2. + tools_settings_vsep_2 = memnew(VSeparator); + tools_settings->add_child(tools_settings_vsep_2); + + // Continuous checkbox. + bucket_contiguous_checkbox = memnew(CheckBox); + bucket_contiguous_checkbox->set_flat(true); + bucket_contiguous_checkbox->set_text(TTR("Contiguous")); + bucket_contiguous_checkbox->set_pressed(true); + tools_settings->add_child(bucket_contiguous_checkbox); +} + +TileMapEditorTerrainsPlugin::~TileMapEditorTerrainsPlugin() { +} + +void TileMapEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: + missing_tile_texture = get_theme_icon(SNAME("StatusWarning"), SNAME("EditorIcons")); + warning_pattern_texture = get_theme_icon(SNAME("WarningPattern"), SNAME("EditorIcons")); + advanced_menu_button->set_icon(get_theme_icon(SNAME("Tools"), SNAME("EditorIcons"))); + toggle_grid_button->set_icon(get_theme_icon(SNAME("Grid"), SNAME("EditorIcons"))); + toggle_grid_button->set_pressed(EditorSettings::get_singleton()->get("editors/tiles_editor/display_grid")); + toogle_highlight_selected_layer_button->set_icon(get_theme_icon(SNAME("TileMapHighlightSelected"), SNAME("EditorIcons"))); + break; + case NOTIFICATION_INTERNAL_PROCESS: + if (is_visible_in_tree() && tileset_changed_needs_update) { + _update_bottom_panel(); + _update_layers_selection(); + tabs_plugins[tabs_bar->get_current_tab()]->tile_set_changed(); + CanvasItemEditor::get_singleton()->update_viewport(); + tileset_changed_needs_update = false; + } + break; + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: + toggle_grid_button->set_pressed(EditorSettings::get_singleton()->get("editors/tiles_editor/display_grid")); + break; + case NOTIFICATION_VISIBILITY_CHANGED: + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (tile_map) { + if (is_visible_in_tree()) { + tile_map->set_selected_layer(tile_map_layer); + } else { + tile_map->set_selected_layer(-1); + } + } + break; + } +} + +void TileMapEditor::_on_grid_toggled(bool p_pressed) { + EditorSettings::get_singleton()->set("editors/tiles_editor/display_grid", p_pressed); +} + +void TileMapEditor::_layers_selection_button_draw() { + if (!has_theme_icon(SNAME("arrow"), SNAME("OptionButton"))) { + return; + } + + RID ci = layers_selection_button->get_canvas_item(); + Ref<Texture2D> arrow = Control::get_theme_icon(SNAME("arrow"), SNAME("OptionButton")); + + Color clr = Color(1, 1, 1); + if (get_theme_constant(SNAME("modulate_arrow"))) { + switch (layers_selection_button->get_draw_mode()) { + case BaseButton::DRAW_PRESSED: + clr = get_theme_color(SNAME("font_pressed_color")); + break; + case BaseButton::DRAW_HOVER: + clr = get_theme_color(SNAME("font_hover_color")); + break; + case BaseButton::DRAW_DISABLED: + clr = get_theme_color(SNAME("font_disabled_color")); + break; + default: + if (layers_selection_button->has_focus()) { + clr = get_theme_color(SNAME("font_focus_color")); + } else { + clr = get_theme_color(SNAME("font_color")); + } + } + } + + Size2 size = layers_selection_button->get_size(); + + Point2 ofs; + if (is_layout_rtl()) { + ofs = Point2(get_theme_constant(SNAME("arrow_margin"), SNAME("OptionButton")), int(Math::abs((size.height - arrow->get_height()) / 2))); + } else { + ofs = Point2(size.width - arrow->get_width() - get_theme_constant(SNAME("arrow_margin"), SNAME("OptionButton")), int(Math::abs((size.height - arrow->get_height()) / 2))); + } + Rect2 dst_rect = Rect2(ofs, arrow->get_size()); + if (!layers_selection_button->is_pressed()) { + dst_rect.size = -dst_rect.size; + } + arrow->draw_rect(ci, dst_rect, false, clr); +} + +void TileMapEditor::_layers_selection_button_pressed() { + if (!layers_selection_popup->is_visible()) { + Size2 size = layers_selection_popup->get_contents_minimum_size(); + size.x = MAX(size.x, layers_selection_button->get_size().x); + layers_selection_popup->set_position(layers_selection_button->get_screen_position() - Size2(0, size.y * get_global_transform().get_scale().y)); + layers_selection_popup->set_size(size); + layers_selection_popup->popup(); + } else { + layers_selection_popup->hide(); + } +} + +void TileMapEditor::_layers_selection_id_pressed(int p_id) { + tile_map_layer = p_id; + _update_layers_selection(); +} + +void TileMapEditor::_advanced_menu_button_id_pressed(int p_id) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + if (p_id == 0) { // Replace Tile Proxies + undo_redo->create_action(TTR("Replace Tiles with Proxies")); + for (int layer_index = 0; layer_index < tile_map->get_layers_count(); layer_index++) { + TypedArray<Vector2i> used_cells = tile_map->get_used_cells(layer_index); + for (int i = 0; i < used_cells.size(); i++) { + Vector2i cell_coords = used_cells[i]; + TileMapCell from = tile_map->get_cell(layer_index, cell_coords); + Array to_array = tile_set->map_tile_proxy(from.source_id, from.get_atlas_coords(), from.alternative_tile); + TileMapCell to; + to.source_id = to_array[0]; + to.set_atlas_coords(to_array[1]); + to.alternative_tile = to_array[2]; + if (from != to) { + undo_redo->add_do_method(tile_map, "set_cell", tile_map_layer, cell_coords, to.source_id, to.get_atlas_coords(), to.alternative_tile); + undo_redo->add_undo_method(tile_map, "set_cell", tile_map_layer, cell_coords, from.source_id, from.get_atlas_coords(), from.alternative_tile); + } + } + } + undo_redo->commit_action(); + } +} + +void TileMapEditor::_update_bottom_panel() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + Ref<TileSet> tile_set = tile_map->get_tileset(); + + // Update the visibility of controls. + missing_tileset_label->set_visible(!tile_set.is_valid()); + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].panel->hide(); + } + if (tile_set.is_valid()) { + tabs_data[tabs_bar->get_current_tab()].panel->show(); + } +} + +Vector<Vector2i> TileMapEditor::get_line(TileMap *p_tile_map, Vector2i p_from_cell, Vector2i p_to_cell) { + ERR_FAIL_COND_V(!p_tile_map, Vector<Vector2i>()); + + Ref<TileSet> tile_set = p_tile_map->get_tileset(); + ERR_FAIL_COND_V(!tile_set.is_valid(), Vector<Vector2i>()); + + if (tile_set->get_tile_shape() == TileSet::TILE_SHAPE_SQUARE) { + return Geometry2D::bresenham_line(p_from_cell, p_to_cell); + } else { + // Adapt the bresenham line algorithm to half-offset shapes. + // See this blog post: http://zvold.blogspot.com/2010/01/bresenhams-line-drawing-algorithm-on_26.html + Vector<Point2i> points; + + bool transposed = tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_VERTICAL; + p_from_cell = TileMap::transform_coords_layout(p_from_cell, tile_set->get_tile_offset_axis(), tile_set->get_tile_layout(), TileSet::TILE_LAYOUT_STACKED); + p_to_cell = TileMap::transform_coords_layout(p_to_cell, tile_set->get_tile_offset_axis(), tile_set->get_tile_layout(), TileSet::TILE_LAYOUT_STACKED); + if (transposed) { + SWAP(p_from_cell.x, p_from_cell.y); + SWAP(p_to_cell.x, p_to_cell.y); + } + + Vector2i delta = p_to_cell - p_from_cell; + delta = Vector2i(2 * delta.x + ABS(p_to_cell.y % 2) - ABS(p_from_cell.y % 2), delta.y); + Vector2i sign = delta.sign(); + + Vector2i current = p_from_cell; + points.push_back(TileMap::transform_coords_layout(transposed ? Vector2i(current.y, current.x) : current, tile_set->get_tile_offset_axis(), TileSet::TILE_LAYOUT_STACKED, tile_set->get_tile_layout())); + + int err = 0; + if (ABS(delta.y) < ABS(delta.x)) { + Vector2i err_step = 3 * delta.abs(); + while (current != p_to_cell) { + err += err_step.y; + if (err > ABS(delta.x)) { + if (sign.x == 0) { + current += Vector2(sign.y, 0); + } else { + current += Vector2(bool(current.y % 2) ^ (sign.x < 0) ? sign.x : 0, sign.y); + } + err -= err_step.x; + } else { + current += Vector2i(sign.x, 0); + err += err_step.y; + } + points.push_back(TileMap::transform_coords_layout(transposed ? Vector2i(current.y, current.x) : current, tile_set->get_tile_offset_axis(), TileSet::TILE_LAYOUT_STACKED, tile_set->get_tile_layout())); + } + } else { + Vector2i err_step = delta.abs(); + while (current != p_to_cell) { + err += err_step.x; + if (err > 0) { + if (sign.x == 0) { + current += Vector2(0, sign.y); + } else { + current += Vector2(bool(current.y % 2) ^ (sign.x < 0) ? sign.x : 0, sign.y); + } + err -= err_step.y; + } else { + if (sign.x == 0) { + current += Vector2(0, sign.y); + } else { + current += Vector2(bool(current.y % 2) ^ (sign.x > 0) ? -sign.x : 0, sign.y); + } + err += err_step.y; + } + points.push_back(TileMap::transform_coords_layout(transposed ? Vector2i(current.y, current.x) : current, tile_set->get_tile_offset_axis(), TileSet::TILE_LAYOUT_STACKED, tile_set->get_tile_layout())); + } + } + + return points; + } +} + +void TileMapEditor::_tile_map_changed() { + tileset_changed_needs_update = true; +} + +void TileMapEditor::_tab_changed(int p_tab_id) { + // Make the plugin edit the correct tilemap. + tabs_plugins[tabs_bar->get_current_tab()]->edit(tile_map_id, tile_map_layer); + + // Update toolbar. + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].toolbar->hide(); + } + tabs_data[p_tab_id].toolbar->show(); + + // Update visible panel. + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].panel->hide(); + } + if (tile_map && tile_map->get_tileset().is_valid()) { + tabs_data[tabs_bar->get_current_tab()].panel->show(); + } + + // Graphical update. + tabs_data[tabs_bar->get_current_tab()].panel->update(); + CanvasItemEditor::get_singleton()->update_viewport(); +} + +void TileMapEditor::_layers_select_next_or_previous(bool p_next) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + if (tile_map->get_layers_count() < 1) { + return; + } + + if (tile_map_layer < 0) { + tile_map_layer = 0; + } + + int inc = p_next ? 1 : -1; + int origin_layer = tile_map_layer; + tile_map_layer = Math::posmod((tile_map_layer + inc), tile_map->get_layers_count()); + while (tile_map_layer != origin_layer) { + if (tile_map->is_layer_enabled(tile_map_layer)) { + break; + } + tile_map_layer = Math::posmod((tile_map_layer + inc), tile_map->get_layers_count()); + } + + _update_layers_selection(); +} + +void TileMapEditor::_update_layers_selection() { + layers_selection_popup->clear(); + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + // Update the selected layer. + if (is_visible_in_tree() && tile_map->get_layers_count() >= 1) { + tile_map_layer = CLAMP(tile_map_layer, 0, tile_map->get_layers_count() - 1); + + // Search for an enabled layer if the current one is not. + int origin_layer = tile_map_layer; + while (tile_map_layer >= 0 && !tile_map->is_layer_enabled(tile_map_layer)) { + tile_map_layer--; + } + if (tile_map_layer < 0) { + tile_map_layer = origin_layer; + while (tile_map_layer < tile_map->get_layers_count() && !tile_map->is_layer_enabled(tile_map_layer)) { + tile_map_layer++; + } + } + if (tile_map_layer >= tile_map->get_layers_count()) { + tile_map_layer = -1; + } + } else { + tile_map_layer = -1; + } + tile_map->set_selected_layer(toogle_highlight_selected_layer_button->is_pressed() ? tile_map_layer : -1); + + // Build the list of layers. + for (int i = 0; i < tile_map->get_layers_count(); i++) { + String name = tile_map->get_layer_name(i); + layers_selection_popup->add_item(name.is_empty() ? vformat(TTR("Layer #%d"), i) : name, i); + layers_selection_popup->set_item_as_radio_checkable(i, true); + layers_selection_popup->set_item_disabled(i, !tile_map->is_layer_enabled(i)); + layers_selection_popup->set_item_checked(i, i == tile_map_layer); + } + + // Update the button label. + if (tile_map_layer >= 0) { + layers_selection_button->set_text(layers_selection_popup->get_item_text(tile_map_layer)); + } else { + layers_selection_button->set_text(TTR("Select a layer")); + } + + // Set button minimum width. + Size2 min_button_size = Size2(layers_selection_popup->get_contents_minimum_size().x, 0); + if (has_theme_icon(SNAME("arrow"), SNAME("OptionButton"))) { + Ref<Texture2D> arrow = Control::get_theme_icon(SNAME("arrow"), SNAME("OptionButton")); + min_button_size.x += arrow->get_size().x; + } + layers_selection_button->set_custom_minimum_size(min_button_size); + layers_selection_button->update(); + + tabs_plugins[tabs_bar->get_current_tab()]->edit(tile_map_id, tile_map_layer); +} + +void TileMapEditor::_move_tile_map_array_element(Object *p_undo_redo, Object *p_edited, String p_array_prefix, int p_from_index, int p_to_pos) { + UndoRedo *undo_redo = Object::cast_to<UndoRedo>(p_undo_redo); + ERR_FAIL_COND(!undo_redo); + + TileMap *tile_map = Object::cast_to<TileMap>(p_edited); + if (!tile_map) { + return; + } + + // Compute the array indices to save. + int begin = 0; + int end; + if (p_array_prefix == "layer_") { + end = tile_map->get_layers_count(); + } else { + ERR_FAIL_MSG("Invalid array prefix for TileSet."); + } + if (p_from_index < 0) { + // Adding new. + if (p_to_pos >= 0) { + begin = p_to_pos; + } else { + end = 0; // Nothing to save when adding at the end. + } + } else if (p_to_pos < 0) { + // Removing. + begin = p_from_index; + } else { + // Moving. + begin = MIN(p_from_index, p_to_pos); + end = MIN(MAX(p_from_index, p_to_pos) + 1, end); + } + +#define ADD_UNDO(obj, property) undo_redo->add_undo_property(obj, property, obj->get(property)); + // Save layers' properties. + if (p_from_index < 0) { + undo_redo->add_undo_method(tile_map, "remove_layer", p_to_pos < 0 ? tile_map->get_layers_count() : p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_undo_method(tile_map, "add_layer", p_from_index); + } + + List<PropertyInfo> properties; + tile_map->get_property_list(&properties); + for (PropertyInfo pi : properties) { + if (pi.name.begins_with(p_array_prefix)) { + String str = pi.name.trim_prefix(p_array_prefix); + int to_char_index = 0; + while (to_char_index < str.length()) { + if (str[to_char_index] < '0' || str[to_char_index] > '9') { + break; + } + to_char_index++; + } + if (to_char_index > 0) { + int array_index = str.left(to_char_index).to_int(); + if (array_index >= begin && array_index < end) { + ADD_UNDO(tile_map, pi.name); + } + } + } + } +#undef ADD_UNDO + + if (p_from_index < 0) { + undo_redo->add_do_method(tile_map, "add_layer", p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_do_method(tile_map, "remove_layer", p_from_index); + } else { + undo_redo->add_do_method(tile_map, "move_layer", p_from_index, p_to_pos); + } +} + +bool TileMapEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { + if (ED_IS_SHORTCUT("tiles_editor/select_next_layer", p_event) && p_event->is_pressed()) { + _layers_select_next_or_previous(true); + return true; + } + + if (ED_IS_SHORTCUT("tiles_editor/select_previous_layer", p_event) && p_event->is_pressed()) { + _layers_select_next_or_previous(false); + return true; + } + + return tabs_plugins[tabs_bar->get_current_tab()]->forward_canvas_gui_input(p_event); +} + +void TileMapEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (!tile_set.is_valid()) { + return; + } + + if (!tile_map->is_visible_in_tree()) { + return; + } + + Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * tile_map->get_global_transform(); + Transform2D xform_inv = xform.affine_inverse(); + Vector2i tile_shape_size = tile_set->get_tile_size(); + + // Draw tiles with invalid IDs in the grid. + if (tile_map_layer >= 0) { + ERR_FAIL_COND(tile_map_layer >= tile_map->get_layers_count()); + TypedArray<Vector2i> used_cells = tile_map->get_used_cells(tile_map_layer); + for (int i = 0; i < used_cells.size(); i++) { + Vector2i coords = used_cells[i]; + int tile_source_id = tile_map->get_cell_source_id(tile_map_layer, coords); + if (tile_source_id >= 0) { + Vector2i tile_atlas_coords = tile_map->get_cell_atlas_coords(tile_map_layer, coords); + int tile_alternative_tile = tile_map->get_cell_alternative_tile(tile_map_layer, coords); + + TileSetSource *source = nullptr; + if (tile_set->has_source(tile_source_id)) { + source = *tile_set->get_source(tile_source_id); + } + + if (!source || !source->has_tile(tile_atlas_coords) || !source->has_alternative_tile(tile_atlas_coords, tile_alternative_tile)) { + // Generate a random color from the hashed values of the tiles. + Array a = tile_set->map_tile_proxy(tile_source_id, tile_atlas_coords, tile_alternative_tile); + if (int(a[0]) == tile_source_id && Vector2i(a[1]) == tile_atlas_coords && int(a[2]) == tile_alternative_tile) { + // Only display the pattern if we have no proxy tile. + Array to_hash; + to_hash.push_back(tile_source_id); + to_hash.push_back(tile_atlas_coords); + to_hash.push_back(tile_alternative_tile); + uint32_t hash = RandomPCG(to_hash.hash()).rand(); + + Color color; + color = color.from_hsv( + (float)((hash >> 24) & 0xFF) / 256.0, + Math::lerp(0.5, 1.0, (float)((hash >> 16) & 0xFF) / 256.0), + Math::lerp(0.5, 1.0, (float)((hash >> 8) & 0xFF) / 256.0), + 0.8); + + // Draw the scaled tile. + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(coords)); + tile_xform.set_scale(tile_shape_size); + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, color, true, warning_pattern_texture); + } + + // Draw the warning icon. + int min_axis = missing_tile_texture->get_size().min_axis(); + Vector2 icon_size; + icon_size[min_axis] = tile_set->get_tile_size()[min_axis] / 3; + icon_size[(min_axis + 1) % 2] = (icon_size[min_axis] * missing_tile_texture->get_size()[(min_axis + 1) % 2] / missing_tile_texture->get_size()[min_axis]); + Rect2 rect = Rect2(xform.xform(tile_map->map_to_world(coords)) - (icon_size * xform.get_scale() / 2), icon_size * xform.get_scale()); + p_overlay->draw_texture_rect(missing_tile_texture, rect); + } + } + } + } + + // Fading on the border. + const int fading = 5; + + // Determine the drawn area. + Size2 screen_size = p_overlay->get_size(); + Rect2i screen_rect; + screen_rect.position = tile_map->world_to_map(xform_inv.xform(Vector2())); + screen_rect.expand_to(tile_map->world_to_map(xform_inv.xform(Vector2(0, screen_size.height)))); + screen_rect.expand_to(tile_map->world_to_map(xform_inv.xform(Vector2(screen_size.width, 0)))); + screen_rect.expand_to(tile_map->world_to_map(xform_inv.xform(screen_size))); + screen_rect = screen_rect.grow(1); + + Rect2i tilemap_used_rect = tile_map->get_used_rect(); + + Rect2i displayed_rect = tilemap_used_rect.intersection(screen_rect); + displayed_rect = displayed_rect.grow(fading); + + // Reduce the drawn area to avoid crashes if needed. + int max_size = 100; + if (displayed_rect.size.x > max_size) { + displayed_rect = displayed_rect.grow_individual(-(displayed_rect.size.x - max_size) / 2, 0, -(displayed_rect.size.x - max_size) / 2, 0); + } + if (displayed_rect.size.y > max_size) { + displayed_rect = displayed_rect.grow_individual(0, -(displayed_rect.size.y - max_size) / 2, 0, -(displayed_rect.size.y - max_size) / 2); + } + + // Draw the grid. + bool display_grid = EditorSettings::get_singleton()->get("editors/tiles_editor/display_grid"); + if (display_grid) { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + for (int x = displayed_rect.position.x; x < (displayed_rect.position.x + displayed_rect.size.x); x++) { + for (int y = displayed_rect.position.y; y < (displayed_rect.position.y + displayed_rect.size.y); y++) { + Vector2i pos_in_rect = Vector2i(x, y) - displayed_rect.position; + + // Fade out the border of the grid. + float left_opacity = CLAMP(Math::inverse_lerp(0.0f, (float)fading, (float)pos_in_rect.x), 0.0f, 1.0f); + float right_opacity = CLAMP(Math::inverse_lerp((float)displayed_rect.size.x, (float)(displayed_rect.size.x - fading), (float)pos_in_rect.x), 0.0f, 1.0f); + float top_opacity = CLAMP(Math::inverse_lerp(0.0f, (float)fading, (float)pos_in_rect.y), 0.0f, 1.0f); + float bottom_opacity = CLAMP(Math::inverse_lerp((float)displayed_rect.size.y, (float)(displayed_rect.size.y - fading), (float)pos_in_rect.y), 0.0f, 1.0f); + float opacity = CLAMP(MIN(left_opacity, MIN(right_opacity, MIN(top_opacity, bottom_opacity))) + 0.1, 0.0f, 1.0f); + + Transform2D tile_xform; + tile_xform.set_origin(tile_map->map_to_world(Vector2(x, y))); + tile_xform.set_scale(tile_shape_size); + Color color = grid_color; + color.a = color.a * opacity; + tile_set->draw_tile_shape(p_overlay, xform * tile_xform, color, false); + } + } + } + + // Draw the IDs for debug. + /*Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label")); + for (int x = displayed_rect.position.x; x < (displayed_rect.position.x + displayed_rect.size.x); x++) { + for (int y = displayed_rect.position.y; y < (displayed_rect.position.y + displayed_rect.size.y); y++) { + p_overlay->draw_string(font, xform.xform(tile_map->map_to_world(Vector2(x, y))) + Vector2i(-tile_shape_size.x / 2, 0), vformat("%s", Vector2(x, y))); + } + }*/ + + // Draw the plugins. + tabs_plugins[tabs_bar->get_current_tab()]->forward_canvas_draw_over_viewport(p_overlay); +} + +void TileMapEditor::edit(TileMap *p_tile_map) { + if (p_tile_map && p_tile_map->get_instance_id() == tile_map_id) { + return; + } + + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (tile_map) { + // Unselect layer if we are changing tile_map. + if (tile_map != p_tile_map) { + tile_map->set_selected_layer(-1); + } + + // Disconnect to changes. + tile_map->disconnect("changed", callable_mp(this, &TileMapEditor::_tile_map_changed)); + } + + if (p_tile_map) { + // Change the edited object. + tile_map_id = p_tile_map->get_instance_id(); + tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + // Connect to changes. + if (!tile_map->is_connected("changed", callable_mp(this, &TileMapEditor::_tile_map_changed))) { + tile_map->connect("changed", callable_mp(this, &TileMapEditor::_tile_map_changed)); + } + } else { + tile_map_id = ObjectID(); + } + + _update_layers_selection(); + + // Call the plugins. + tabs_plugins[tabs_bar->get_current_tab()]->edit(tile_map_id, tile_map_layer); + + _tile_map_changed(); +} + +TileMapEditor::TileMapEditor() { + set_process_internal(true); + + // Shortcuts. + ED_SHORTCUT("tiles_editor/select_next_layer", TTR("Select Next Tile Map Layer"), Key::PAGEUP); + ED_SHORTCUT("tiles_editor/select_previous_layer", TTR("Select Previous Tile Map Layer"), Key::PAGEDOWN); + + // TileMap editor plugins + tile_map_editor_plugins.push_back(memnew(TileMapEditorTilesPlugin)); + tile_map_editor_plugins.push_back(memnew(TileMapEditorTerrainsPlugin)); + + // TabBar. + tabs_bar = memnew(TabBar); + tabs_bar->set_clip_tabs(false); + for (int plugin_index = 0; plugin_index < tile_map_editor_plugins.size(); plugin_index++) { + Vector<TileMapEditorPlugin::TabData> tabs_vector = tile_map_editor_plugins[plugin_index]->get_tabs(); + for (int tab_index = 0; tab_index < tabs_vector.size(); tab_index++) { + tabs_bar->add_tab(tabs_vector[tab_index].panel->get_name()); + tabs_data.push_back(tabs_vector[tab_index]); + tabs_plugins.push_back(tile_map_editor_plugins[plugin_index]); + } + } + tabs_bar->connect("tab_changed", callable_mp(this, &TileMapEditor::_tab_changed)); + + // --- TileMap toolbar --- + tile_map_toolbar = memnew(HBoxContainer); + tile_map_toolbar->set_h_size_flags(SIZE_EXPAND_FILL); + add_child(tile_map_toolbar); + + // Tabs. + tile_map_toolbar->add_child(tabs_bar); + + // Tabs toolbars. + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + tabs_data[tab_index].toolbar->hide(); + if (!tabs_data[tab_index].toolbar->get_parent()) { + tile_map_toolbar->add_child(tabs_data[tab_index].toolbar); + } + } + + // Wide empty separation control. + Control *h_empty_space = memnew(Control); + h_empty_space->set_h_size_flags(SIZE_EXPAND_FILL); + tile_map_toolbar->add_child(h_empty_space); + + // Layer selector. + layers_selection_popup = memnew(PopupMenu); + layers_selection_popup->connect("id_pressed", callable_mp(this, &TileMapEditor::_layers_selection_id_pressed)); + layers_selection_popup->set_close_on_parent_focus(false); + + layers_selection_button = memnew(Button); + layers_selection_button->set_toggle_mode(true); + layers_selection_button->connect("draw", callable_mp(this, &TileMapEditor::_layers_selection_button_draw)); + layers_selection_button->connect("pressed", callable_mp(this, &TileMapEditor::_layers_selection_button_pressed)); + layers_selection_button->connect("hidden", callable_mp((Window *)layers_selection_popup, &Popup::hide)); + layers_selection_button->set_tooltip(TTR("Tile Map Layer")); + layers_selection_button->add_child(layers_selection_popup); + tile_map_toolbar->add_child(layers_selection_button); + + toogle_highlight_selected_layer_button = memnew(Button); + toogle_highlight_selected_layer_button->set_flat(true); + toogle_highlight_selected_layer_button->set_toggle_mode(true); + toogle_highlight_selected_layer_button->set_pressed(true); + toogle_highlight_selected_layer_button->connect("pressed", callable_mp(this, &TileMapEditor::_update_layers_selection)); + toogle_highlight_selected_layer_button->set_tooltip(TTR("Highlight Selected TileMap Layer")); + tile_map_toolbar->add_child(toogle_highlight_selected_layer_button); + + tile_map_toolbar->add_child(memnew(VSeparator)); + + // Grid toggle. + toggle_grid_button = memnew(Button); + toggle_grid_button->set_flat(true); + toggle_grid_button->set_toggle_mode(true); + toggle_grid_button->set_tooltip(TTR("Toggle grid visibility.")); + toggle_grid_button->connect("toggled", callable_mp(this, &TileMapEditor::_on_grid_toggled)); + tile_map_toolbar->add_child(toggle_grid_button); + + // Advanced settings menu button. + advanced_menu_button = memnew(MenuButton); + advanced_menu_button->set_flat(true); + advanced_menu_button->get_popup()->add_item(TTR("Automatically Replace Tiles with Proxies")); + advanced_menu_button->get_popup()->connect("id_pressed", callable_mp(this, &TileMapEditor::_advanced_menu_button_id_pressed)); + tile_map_toolbar->add_child(advanced_menu_button); + + missing_tileset_label = memnew(Label); + missing_tileset_label->set_text(TTR("The edited TileMap node has no TileSet resource.")); + missing_tileset_label->set_h_size_flags(SIZE_EXPAND_FILL); + missing_tileset_label->set_v_size_flags(SIZE_EXPAND_FILL); + missing_tileset_label->set_align(Label::ALIGN_CENTER); + missing_tileset_label->set_valign(Label::VALIGN_CENTER); + missing_tileset_label->hide(); + add_child(missing_tileset_label); + + for (unsigned int tab_index = 0; tab_index < tabs_data.size(); tab_index++) { + add_child(tabs_data[tab_index].panel); + tabs_data[tab_index].panel->set_v_size_flags(SIZE_EXPAND_FILL); + tabs_data[tab_index].panel->set_visible(tab_index == 0); + tabs_data[tab_index].panel->set_h_size_flags(SIZE_EXPAND_FILL); + } + + _tab_changed(0); + + // Registers UndoRedo inspector callback. + EditorNode::get_singleton()->get_editor_data().add_move_array_element_function(SNAME("TileMap"), callable_mp(this, &TileMapEditor::_move_tile_map_array_element)); +} + +TileMapEditor::~TileMapEditor() { + for (int i = 0; i < tile_map_editor_plugins.size(); i++) { + memdelete(tile_map_editor_plugins[i]); + } +} diff --git a/editor/plugins/tiles/tile_map_editor.h b/editor/plugins/tiles/tile_map_editor.h new file mode 100644 index 0000000000..f462119727 --- /dev/null +++ b/editor/plugins/tiles/tile_map_editor.h @@ -0,0 +1,362 @@ +/*************************************************************************/ +/* tile_map_editor.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILE_MAP_EDITOR_H +#define TILE_MAP_EDITOR_H + +#include "tile_atlas_view.h" + +#include "core/os/thread.h" +#include "core/typedefs.h" +#include "editor/editor_node.h" +#include "scene/2d/tile_map.h" +#include "scene/gui/box_container.h" +#include "scene/gui/tab_bar.h" + +class TileMapEditorPlugin : public Object { +public: + struct TabData { + Control *toolbar; + Control *panel; + }; + + virtual Vector<TabData> get_tabs() const { + return Vector<TabData>(); + }; + + virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) { return false; }; + virtual void forward_canvas_draw_over_viewport(Control *p_overlay){}; + virtual void tile_set_changed(){}; + virtual void edit(ObjectID p_tile_map_id, int p_tile_map_layer){}; +}; + +class TileMapEditorTilesPlugin : public TileMapEditorPlugin { + GDCLASS(TileMapEditorTilesPlugin, TileMapEditorPlugin); + +private: + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + ObjectID tile_map_id; + int tile_map_layer = -1; + virtual void edit(ObjectID p_tile_map_id, int p_tile_map_layer) override; + + ///// Toolbar ///// + HBoxContainer *toolbar; + + Ref<ButtonGroup> tool_buttons_group; + Button *select_tool_button; + Button *paint_tool_button; + Button *line_tool_button; + Button *rect_tool_button; + Button *bucket_tool_button; + + HBoxContainer *tools_settings; + + VSeparator *tools_settings_vsep; + Button *picker_button; + Button *erase_button; + + VSeparator *tools_settings_vsep_2; + CheckBox *bucket_contiguous_checkbox; + CheckBox *random_tile_checkbox; + float scattering = 0.0; + Label *scatter_label; + SpinBox *scatter_spinbox; + void _on_random_tile_checkbox_toggled(bool p_pressed); + void _on_scattering_spinbox_changed(double p_value); + + void _update_toolbar(); + + ///// Tilemap editing. ///// + bool has_mouse = false; + void _mouse_exited_viewport(); + + enum DragType { + DRAG_TYPE_NONE = 0, + DRAG_TYPE_SELECT, + DRAG_TYPE_MOVE, + DRAG_TYPE_PAINT, + DRAG_TYPE_LINE, + DRAG_TYPE_RECT, + DRAG_TYPE_BUCKET, + DRAG_TYPE_PICK, + DRAG_TYPE_CLIPBOARD_PASTE, + }; + DragType drag_type = DRAG_TYPE_NONE; + bool drag_erasing = false; + Vector2 drag_start_mouse_pos; + Vector2 drag_last_mouse_pos; + Map<Vector2i, TileMapCell> drag_modified; + + TileMapCell _pick_random_tile(Ref<TileMapPattern> p_pattern); + Map<Vector2i, TileMapCell> _draw_line(Vector2 p_start_drag_mouse_pos, Vector2 p_from_mouse_pos, Vector2 p_to_mouse_pos, bool p_erase); + Map<Vector2i, TileMapCell> _draw_rect(Vector2i p_start_cell, Vector2i p_end_cell, bool p_erase); + Map<Vector2i, TileMapCell> _draw_bucket_fill(Vector2i p_coords, bool p_contiguous, bool p_erase); + void _stop_dragging(); + + ///// Selection system. ///// + Set<Vector2i> tile_map_selection; + Ref<TileMapPattern> tile_map_clipboard; + Ref<TileMapPattern> selection_pattern; + void _set_tile_map_selection(const TypedArray<Vector2i> &p_selection); + TypedArray<Vector2i> _get_tile_map_selection() const; + + Set<TileMapCell> tile_set_selection; + + void _update_selection_pattern_from_tilemap_selection(); + void _update_selection_pattern_from_tileset_tiles_selection(); + void _update_selection_pattern_from_tileset_pattern_selection(); + void _update_tileset_selection_from_selection_pattern(); + void _update_fix_selected_and_hovered(); + void _fix_invalid_tiles_in_tile_map_selection(); + + ///// Bottom panel common //// + void _tab_changed(); + + ///// Bottom panel tiles //// + VBoxContainer *tiles_bottom_panel; + Label *missing_source_label; + Label *invalid_source_label; + + ItemList *sources_list; + + Ref<Texture2D> missing_atlas_texture_icon; + void _update_tile_set_sources_list(); + + void _update_source_display(); + + // Atlas sources. + TileMapCell hovered_tile; + TileAtlasView *tile_atlas_view; + HSplitContainer *atlas_sources_split_container; + + bool tile_set_dragging_selection = false; + Vector2i tile_set_drag_start_mouse_pos; + + Control *tile_atlas_control; + void _tile_atlas_control_mouse_exited(); + void _tile_atlas_control_gui_input(const Ref<InputEvent> &p_event); + void _tile_atlas_control_draw(); + + Control *alternative_tiles_control; + void _tile_alternatives_control_draw(); + void _tile_alternatives_control_mouse_exited(); + void _tile_alternatives_control_gui_input(const Ref<InputEvent> &p_event); + + void _update_atlas_view(); + + // Scenes collection sources. + ItemList *scene_tiles_list; + + void _update_scenes_collection_view(); + void _scene_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, Variant p_ud); + void _scenes_list_multi_selected(int p_index, bool p_selected); + void _scenes_list_nothing_selected(); + + ///// Bottom panel patterns //// + VBoxContainer *patterns_bottom_panel; + ItemList *patterns_item_list; + Label *patterns_help_label; + void _patterns_item_list_gui_input(const Ref<InputEvent> &p_event); + void _pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture); + bool select_last_pattern = false; + void _update_patterns_list(); + + // General + void _update_theme(); + + // Update callback + virtual void tile_set_changed() override; + +protected: + static void _bind_methods(); + +public: + virtual Vector<TabData> get_tabs() const override; + virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) override; + virtual void forward_canvas_draw_over_viewport(Control *p_overlay) override; + + TileMapEditorTilesPlugin(); + ~TileMapEditorTilesPlugin(); +}; + +class TileMapEditorTerrainsPlugin : public TileMapEditorPlugin { + GDCLASS(TileMapEditorTerrainsPlugin, TileMapEditorPlugin); + +private: + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + ObjectID tile_map_id; + int tile_map_layer = -1; + virtual void edit(ObjectID p_tile_map_id, int p_tile_map_layer) override; + + // Toolbar. + HBoxContainer *toolbar; + + Ref<ButtonGroup> tool_buttons_group; + Button *paint_tool_button; + Button *line_tool_button; + Button *rect_tool_button; + Button *bucket_tool_button; + + HBoxContainer *tools_settings; + + VSeparator *tools_settings_vsep; + Button *picker_button; + Button *erase_button; + + VSeparator *tools_settings_vsep_2; + CheckBox *bucket_contiguous_checkbox; + void _update_toolbar(); + + // Main vbox. + VBoxContainer *main_vbox_container; + + // TileMap editing. + bool has_mouse = false; + void _mouse_exited_viewport(); + + enum DragType { + DRAG_TYPE_NONE = 0, + DRAG_TYPE_PAINT, + DRAG_TYPE_LINE, + DRAG_TYPE_RECT, + DRAG_TYPE_BUCKET, + DRAG_TYPE_PICK, + }; + DragType drag_type = DRAG_TYPE_NONE; + bool drag_erasing = false; + Vector2 drag_start_mouse_pos; + Vector2 drag_last_mouse_pos; + Map<Vector2i, TileMapCell> drag_modified; + + // Painting + Map<Vector2i, TileMapCell> _draw_terrains(const Map<Vector2i, TileSet::TerrainsPattern> &p_to_paint, int p_terrain_set) const; + Map<Vector2i, TileMapCell> _draw_line(Vector2i p_start_cell, Vector2i p_end_cell, bool p_erase); + Map<Vector2i, TileMapCell> _draw_rect(Vector2i p_start_cell, Vector2i p_end_cell, bool p_erase); + Set<Vector2i> _get_cells_for_bucket_fill(Vector2i p_coords, bool p_contiguous); + Map<Vector2i, TileMapCell> _draw_bucket_fill(Vector2i p_coords, bool p_contiguous, bool p_erase); + void _stop_dragging(); + + int selected_terrain_set = -1; + TileSet::TerrainsPattern selected_terrains_pattern; + void _update_selection(); + + // Bottom panel. + Tree *terrains_tree; + ItemList *terrains_tile_list; + + // Cache. + LocalVector<LocalVector<Set<TileSet::TerrainsPattern>>> per_terrain_terrains_patterns; + + // Update functions. + void _update_terrains_cache(); + void _update_terrains_tree(); + void _update_tiles_list(); + void _update_theme(); + + // Update callback + virtual void tile_set_changed() override; + +public: + virtual Vector<TabData> get_tabs() const override; + virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) override; + virtual void forward_canvas_draw_over_viewport(Control *p_overlay) override; + + TileMapEditorTerrainsPlugin(); + ~TileMapEditorTerrainsPlugin(); +}; + +class TileMapEditor : public VBoxContainer { + GDCLASS(TileMapEditor, VBoxContainer); + +private: + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + bool tileset_changed_needs_update = false; + ObjectID tile_map_id; + int tile_map_layer = -1; + + // Vector to keep plugins. + Vector<TileMapEditorPlugin *> tile_map_editor_plugins; + + // Toolbar. + HBoxContainer *tile_map_toolbar; + + PopupMenu *layers_selection_popup; + Button *layers_selection_button; + Button *toogle_highlight_selected_layer_button; + void _layers_selection_button_draw(); + void _layers_selection_button_pressed(); + void _layers_selection_id_pressed(int p_id); + + Button *toggle_grid_button; + void _on_grid_toggled(bool p_pressed); + + MenuButton *advanced_menu_button; + void _advanced_menu_button_id_pressed(int p_id); + + // Bottom panel. + Label *missing_tileset_label; + TabBar *tabs_bar; + LocalVector<TileMapEditorPlugin::TabData> tabs_data; + LocalVector<TileMapEditorPlugin *> tabs_plugins; + void _update_bottom_panel(); + + // TileMap. + Ref<Texture2D> missing_tile_texture; + Ref<Texture2D> warning_pattern_texture; + + // CallBack. + void _tile_map_changed(); + void _tab_changed(int p_tab_changed); + + // Updates. + void _layers_select_next_or_previous(bool p_next); + void _update_layers_selection(); + + // Inspector undo/redo callback. + void _move_tile_map_array_element(Object *p_undo_redo, Object *p_edited, String p_array_prefix, int p_from_index, int p_to_pos); + +protected: + void _notification(int p_what); + void _draw_shape(Control *p_control, Rect2 p_region, TileSet::TileShape p_shape, TileSet::TileOffsetAxis p_offset_axis, Color p_color); + +public: + bool forward_canvas_gui_input(const Ref<InputEvent> &p_event); + void forward_canvas_draw_over_viewport(Control *p_overlay); + + void edit(TileMap *p_tile_map); + + TileMapEditor(); + ~TileMapEditor(); + + // Static functions. + static Vector<Vector2i> get_line(TileMap *p_tile_map, Vector2i p_from_cell, Vector2i p_to_cell); +}; + +#endif // TILE_MAP_EDITOR_PLUGIN_H diff --git a/editor/plugins/tiles/tile_proxies_manager_dialog.cpp b/editor/plugins/tiles/tile_proxies_manager_dialog.cpp new file mode 100644 index 0000000000..9e47a44b34 --- /dev/null +++ b/editor/plugins/tiles/tile_proxies_manager_dialog.cpp @@ -0,0 +1,476 @@ +/*************************************************************************/ +/* tile_proxies_manager_dialog.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tile_proxies_manager_dialog.h" + +#include "editor/editor_scale.h" + +void TileProxiesManagerDialog::_right_clicked(int p_item, Vector2 p_local_mouse_pos, Object *p_item_list) { + ItemList *item_list = Object::cast_to<ItemList>(p_item_list); + popup_menu->set_size(Vector2(1, 1)); + popup_menu->set_position(get_position() + item_list->get_global_mouse_position()); + popup_menu->popup(); +} + +void TileProxiesManagerDialog::_menu_id_pressed(int p_id) { + if (p_id == 0) { + // Delete. + _delete_selected_bindings(); + } +} + +void TileProxiesManagerDialog::_delete_selected_bindings() { + undo_redo->create_action(TTR("Remove Tile Proxies")); + + Vector<int> source_level_selected = source_level_list->get_selected_items(); + for (int i = 0; i < source_level_selected.size(); i++) { + int key = source_level_list->get_item_metadata(source_level_selected[i]); + int val = tile_set->get_source_level_tile_proxy(key); + undo_redo->add_do_method(*tile_set, "remove_source_level_tile_proxy", key); + undo_redo->add_undo_method(*tile_set, "set_source_level_tile_proxy", key, val); + } + + Vector<int> coords_level_selected = coords_level_list->get_selected_items(); + for (int i = 0; i < coords_level_selected.size(); i++) { + Array key = coords_level_list->get_item_metadata(coords_level_selected[i]); + Array val = tile_set->get_coords_level_tile_proxy(key[0], key[1]); + undo_redo->add_do_method(*tile_set, "remove_coords_level_tile_proxy", key[0], key[1]); + undo_redo->add_undo_method(*tile_set, "set_coords_level_tile_proxy", key[0], key[1], val[0], val[1]); + } + + Vector<int> alternative_level_selected = alternative_level_list->get_selected_items(); + for (int i = 0; i < alternative_level_selected.size(); i++) { + Array key = alternative_level_list->get_item_metadata(alternative_level_selected[i]); + Array val = tile_set->get_coords_level_tile_proxy(key[0], key[1]); + undo_redo->add_do_method(*tile_set, "remove_alternative_level_tile_proxy", key[0], key[1], key[2]); + undo_redo->add_undo_method(*tile_set, "set_alternative_level_tile_proxy", key[0], key[1], key[2], val[0], val[1], val[2]); + } + undo_redo->add_do_method(this, "_update_lists"); + undo_redo->add_undo_method(this, "_update_lists"); + undo_redo->commit_action(); + + commited_actions_count += 1; +} + +void TileProxiesManagerDialog::_update_lists() { + source_level_list->clear(); + coords_level_list->clear(); + alternative_level_list->clear(); + + Array proxies = tile_set->get_source_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + String text = vformat("%s", proxy[0]).rpad(5) + "-> " + vformat("%s", proxy[1]); + int id = source_level_list->add_item(text); + source_level_list->set_item_metadata(id, proxy[0]); + } + + proxies = tile_set->get_coords_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + String text = vformat("%s, %s", proxy[0], proxy[1]).rpad(17) + "-> " + vformat("%s, %s", proxy[2], proxy[3]); + int id = coords_level_list->add_item(text); + coords_level_list->set_item_metadata(id, proxy.slice(0, 2)); + } + + proxies = tile_set->get_alternative_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + String text = vformat("%s, %s, %s", proxy[0], proxy[1], proxy[2]).rpad(24) + "-> " + vformat("%s, %s, %s", proxy[3], proxy[4], proxy[5]); + int id = alternative_level_list->add_item(text); + alternative_level_list->set_item_metadata(id, proxy.slice(0, 3)); + } +} + +void TileProxiesManagerDialog::_update_enabled_property_editors() { + if (from.source_id == TileSet::INVALID_SOURCE) { + from.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + to.set_atlas_coords(TileSetSource::INVALID_ATLAS_COORDS); + from.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + to.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + coords_from_property_editor->hide(); + coords_to_property_editor->hide(); + alternative_from_property_editor->hide(); + alternative_to_property_editor->hide(); + } else if (from.get_atlas_coords().x == -1 || from.get_atlas_coords().y == -1) { + from.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + to.alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE; + coords_from_property_editor->show(); + coords_to_property_editor->show(); + alternative_from_property_editor->hide(); + alternative_to_property_editor->hide(); + } else { + coords_from_property_editor->show(); + coords_to_property_editor->show(); + alternative_from_property_editor->show(); + alternative_to_property_editor->show(); + } + + source_from_property_editor->update_property(); + source_to_property_editor->update_property(); + coords_from_property_editor->update_property(); + coords_to_property_editor->update_property(); + alternative_from_property_editor->update_property(); + alternative_to_property_editor->update_property(); +} + +void TileProxiesManagerDialog::_property_changed(const String &p_path, const Variant &p_value, const String &p_name, bool p_changing) { + _set(p_path, p_value); +} + +void TileProxiesManagerDialog::_add_button_pressed() { + if (from.source_id != TileSet::INVALID_SOURCE && to.source_id != TileSet::INVALID_SOURCE) { + Vector2i from_coords = from.get_atlas_coords(); + Vector2i to_coords = to.get_atlas_coords(); + if (from_coords.x >= 0 && from_coords.y >= 0 && to_coords.x >= 0 && to_coords.y >= 0) { + if (from.alternative_tile != TileSetSource::INVALID_TILE_ALTERNATIVE && to.alternative_tile != TileSetSource::INVALID_TILE_ALTERNATIVE) { + undo_redo->create_action(TTR("Create Alternative-level Tile Proxy")); + undo_redo->add_do_method(*tile_set, "set_alternative_level_tile_proxy", from.source_id, from.get_atlas_coords(), from.alternative_tile, to.source_id, to.get_atlas_coords(), to.alternative_tile); + if (tile_set->has_alternative_level_tile_proxy(from.source_id, from.get_atlas_coords(), from.alternative_tile)) { + Array a = tile_set->get_alternative_level_tile_proxy(from.source_id, from.get_atlas_coords(), from.alternative_tile); + undo_redo->add_undo_method(*tile_set, "set_alternative_level_tile_proxy", to.source_id, to.get_atlas_coords(), to.alternative_tile, a[0], a[1], a[2]); + } else { + undo_redo->add_undo_method(*tile_set, "remove_alternative_level_tile_proxy", from.source_id, from.get_atlas_coords(), from.alternative_tile); + } + } else { + undo_redo->create_action(TTR("Create Coords-level Tile Proxy")); + undo_redo->add_do_method(*tile_set, "set_coords_level_tile_proxy", from.source_id, from.get_atlas_coords(), to.source_id, to.get_atlas_coords()); + if (tile_set->has_coords_level_tile_proxy(from.source_id, from.get_atlas_coords())) { + Array a = tile_set->get_coords_level_tile_proxy(from.source_id, from.get_atlas_coords()); + undo_redo->add_undo_method(*tile_set, "set_coords_level_tile_proxy", to.source_id, to.get_atlas_coords(), a[0], a[1]); + } else { + undo_redo->add_undo_method(*tile_set, "remove_coords_level_tile_proxy", from.source_id, from.get_atlas_coords()); + } + } + } else { + undo_redo->create_action(TTR("Create source-level Tile Proxy")); + undo_redo->add_do_method(*tile_set, "set_source_level_tile_proxy", from.source_id, to.source_id); + if (tile_set->has_source_level_tile_proxy(from.source_id)) { + undo_redo->add_undo_method(*tile_set, "set_source_level_tile_proxy", to.source_id, tile_set->get_source_level_tile_proxy(from.source_id)); + } else { + undo_redo->add_undo_method(*tile_set, "remove_source_level_tile_proxy", from.source_id); + } + } + undo_redo->add_do_method(this, "_update_lists"); + undo_redo->add_undo_method(this, "_update_lists"); + undo_redo->commit_action(); + commited_actions_count++; + } +} + +void TileProxiesManagerDialog::_clear_invalid_button_pressed() { + undo_redo->create_action(TTR("Delete All Invalid Tile Proxies")); + + undo_redo->add_do_method(*tile_set, "cleanup_invalid_tile_proxies"); + + Array proxies = tile_set->get_source_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + undo_redo->add_undo_method(*tile_set, "set_source_level_tile_proxy", proxy[0], proxy[1]); + } + + proxies = tile_set->get_coords_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + undo_redo->add_undo_method(*tile_set, "set_coords_level_tile_proxy", proxy[0], proxy[1], proxy[2], proxy[3]); + } + + proxies = tile_set->get_alternative_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + undo_redo->add_undo_method(*tile_set, "set_alternative_level_tile_proxy", proxy[0], proxy[1], proxy[2], proxy[3], proxy[4], proxy[5]); + } + undo_redo->add_do_method(this, "_update_lists"); + undo_redo->add_undo_method(this, "_update_lists"); + undo_redo->commit_action(); +} + +void TileProxiesManagerDialog::_clear_all_button_pressed() { + undo_redo->create_action(TTR("Delete All Tile Proxies")); + + undo_redo->add_do_method(*tile_set, "clear_tile_proxies"); + + Array proxies = tile_set->get_source_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + undo_redo->add_undo_method(*tile_set, "set_source_level_tile_proxy", proxy[0], proxy[1]); + } + + proxies = tile_set->get_coords_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + undo_redo->add_undo_method(*tile_set, "set_coords_level_tile_proxy", proxy[0], proxy[1], proxy[2], proxy[3]); + } + + proxies = tile_set->get_alternative_level_tile_proxies(); + for (int i = 0; i < proxies.size(); i++) { + Array proxy = proxies[i]; + undo_redo->add_undo_method(*tile_set, "set_alternative_level_tile_proxy", proxy[0], proxy[1], proxy[2], proxy[3], proxy[4], proxy[5]); + } + undo_redo->add_do_method(this, "_update_lists"); + undo_redo->add_undo_method(this, "_update_lists"); + undo_redo->commit_action(); +} + +bool TileProxiesManagerDialog::_set(const StringName &p_name, const Variant &p_value) { + if (p_name == "from_source") { + from.source_id = MAX(int(p_value), -1); + } else if (p_name == "from_coords") { + from.set_atlas_coords(Vector2i(p_value).max(Vector2i(-1, -1))); + } else if (p_name == "from_alternative") { + from.alternative_tile = MAX(int(p_value), -1); + } else if (p_name == "to_source") { + to.source_id = MAX(int(p_value), 0); + } else if (p_name == "to_coords") { + to.set_atlas_coords(Vector2i(p_value).max(Vector2i(0, 0))); + } else if (p_name == "to_alternative") { + to.alternative_tile = MAX(int(p_value), 0); + } else { + return false; + } + _update_enabled_property_editors(); + return true; +} + +bool TileProxiesManagerDialog::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "from_source") { + r_ret = from.source_id; + } else if (p_name == "from_coords") { + r_ret = from.get_atlas_coords(); + } else if (p_name == "from_alternative") { + r_ret = from.alternative_tile; + } else if (p_name == "to_source") { + r_ret = to.source_id; + } else if (p_name == "to_coords") { + r_ret = to.get_atlas_coords(); + } else if (p_name == "to_alternative") { + r_ret = to.alternative_tile; + } else { + return false; + } + return true; +} + +void TileProxiesManagerDialog::_unhandled_key_input(Ref<InputEvent> p_event) { + ERR_FAIL_COND(p_event.is_null()); + + if (p_event->is_pressed() && !p_event->is_echo() && (Object::cast_to<InputEventKey>(p_event.ptr()) || Object::cast_to<InputEventJoypadButton>(p_event.ptr()) || Object::cast_to<InputEventAction>(*p_event))) { + if (!is_inside_tree() || !is_visible()) { + return; + } + + if (popup_menu->activate_item_by_event(p_event, false)) { + set_input_as_handled(); + } + } +} + +void TileProxiesManagerDialog::cancel_pressed() { + for (int i = 0; i < commited_actions_count; i++) { + undo_redo->undo(); + } + commited_actions_count = 0; +} + +void TileProxiesManagerDialog::_bind_methods() { + ClassDB::bind_method(D_METHOD("_update_lists"), &TileProxiesManagerDialog::_update_lists); + ClassDB::bind_method(D_METHOD("_unhandled_key_input"), &TileProxiesManagerDialog::_unhandled_key_input); +} + +void TileProxiesManagerDialog::update_tile_set(Ref<TileSet> p_tile_set) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + tile_set = p_tile_set; + commited_actions_count = 0; + _update_lists(); +} + +TileProxiesManagerDialog::TileProxiesManagerDialog() { + // Tile proxy management window. + set_title(TTR("Tile Proxies Management")); + set_process_unhandled_key_input(true); + + to.source_id = 0; + to.set_atlas_coords(Vector2i()); + to.alternative_tile = 0; + + VBoxContainer *vbox_container = memnew(VBoxContainer); + vbox_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + vbox_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + add_child(vbox_container); + + Label *source_level_label = memnew(Label); + source_level_label->set_text(TTR("Source-level proxies")); + vbox_container->add_child(source_level_label); + + source_level_list = memnew(ItemList); + source_level_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + source_level_list->set_select_mode(ItemList::SELECT_MULTI); + source_level_list->set_allow_rmb_select(true); + source_level_list->connect("item_rmb_selected", callable_mp(this, &TileProxiesManagerDialog::_right_clicked), varray(source_level_list)); + vbox_container->add_child(source_level_list); + + Label *coords_level_label = memnew(Label); + coords_level_label->set_text(TTR("Coords-level proxies")); + vbox_container->add_child(coords_level_label); + + coords_level_list = memnew(ItemList); + coords_level_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + coords_level_list->set_select_mode(ItemList::SELECT_MULTI); + coords_level_list->set_allow_rmb_select(true); + coords_level_list->connect("item_rmb_selected", callable_mp(this, &TileProxiesManagerDialog::_right_clicked), varray(coords_level_list)); + vbox_container->add_child(coords_level_list); + + Label *alternative_level_label = memnew(Label); + alternative_level_label->set_text(TTR("Alternative-level proxies")); + vbox_container->add_child(alternative_level_label); + + alternative_level_list = memnew(ItemList); + alternative_level_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + alternative_level_list->set_select_mode(ItemList::SELECT_MULTI); + alternative_level_list->set_allow_rmb_select(true); + alternative_level_list->connect("item_rmb_selected", callable_mp(this, &TileProxiesManagerDialog::_right_clicked), varray(alternative_level_list)); + vbox_container->add_child(alternative_level_list); + + popup_menu = memnew(PopupMenu); + popup_menu->add_shortcut(ED_GET_SHORTCUT("ui_text_delete")); + popup_menu->connect("id_pressed", callable_mp(this, &TileProxiesManagerDialog::_menu_id_pressed)); + add_child(popup_menu); + + // Add proxy panel. + HSeparator *h_separator = memnew(HSeparator); + vbox_container->add_child(h_separator); + + Label *add_label = memnew(Label); + add_label->set_text(TTR("Add a new tile proxy:")); + vbox_container->add_child(add_label); + + HBoxContainer *hboxcontainer = memnew(HBoxContainer); + vbox_container->add_child(hboxcontainer); + + // From + VBoxContainer *vboxcontainer_from = memnew(VBoxContainer); + vboxcontainer_from->set_h_size_flags(Control::SIZE_EXPAND_FILL); + hboxcontainer->add_child(vboxcontainer_from); + + source_from_property_editor = memnew(EditorPropertyInteger); + source_from_property_editor->set_label(TTR("From Source")); + source_from_property_editor->set_object_and_property(this, "from_source"); + source_from_property_editor->connect("property_changed", callable_mp(this, &TileProxiesManagerDialog::_property_changed)); + source_from_property_editor->set_selectable(false); + source_from_property_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + source_from_property_editor->setup(-1, 99999, 1, true, false); + vboxcontainer_from->add_child(source_from_property_editor); + + coords_from_property_editor = memnew(EditorPropertyVector2i); + coords_from_property_editor->set_label(TTR("From Coords")); + coords_from_property_editor->set_object_and_property(this, "from_coords"); + coords_from_property_editor->connect("property_changed", callable_mp(this, &TileProxiesManagerDialog::_property_changed)); + coords_from_property_editor->set_selectable(false); + coords_from_property_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + coords_from_property_editor->setup(-1, 99999, true); + coords_from_property_editor->hide(); + vboxcontainer_from->add_child(coords_from_property_editor); + + alternative_from_property_editor = memnew(EditorPropertyInteger); + alternative_from_property_editor->set_label(TTR("From Alternative")); + alternative_from_property_editor->set_object_and_property(this, "from_alternative"); + alternative_from_property_editor->connect("property_changed", callable_mp(this, &TileProxiesManagerDialog::_property_changed)); + alternative_from_property_editor->set_selectable(false); + alternative_from_property_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + alternative_from_property_editor->setup(-1, 99999, 1, true, false); + alternative_from_property_editor->hide(); + vboxcontainer_from->add_child(alternative_from_property_editor); + + // To + VBoxContainer *vboxcontainer_to = memnew(VBoxContainer); + vboxcontainer_to->set_h_size_flags(Control::SIZE_EXPAND_FILL); + hboxcontainer->add_child(vboxcontainer_to); + + source_to_property_editor = memnew(EditorPropertyInteger); + source_to_property_editor->set_label(TTR("To Source")); + source_to_property_editor->set_object_and_property(this, "to_source"); + source_to_property_editor->connect("property_changed", callable_mp(this, &TileProxiesManagerDialog::_property_changed)); + source_to_property_editor->set_selectable(false); + source_to_property_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + source_to_property_editor->setup(-1, 99999, 1, true, false); + vboxcontainer_to->add_child(source_to_property_editor); + + coords_to_property_editor = memnew(EditorPropertyVector2i); + coords_to_property_editor->set_label(TTR("To Coords")); + coords_to_property_editor->set_object_and_property(this, "to_coords"); + coords_to_property_editor->connect("property_changed", callable_mp(this, &TileProxiesManagerDialog::_property_changed)); + coords_to_property_editor->set_selectable(false); + coords_to_property_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + coords_to_property_editor->setup(-1, 99999, true); + coords_to_property_editor->hide(); + vboxcontainer_to->add_child(coords_to_property_editor); + + alternative_to_property_editor = memnew(EditorPropertyInteger); + alternative_to_property_editor->set_label(TTR("To Alternative")); + alternative_to_property_editor->set_object_and_property(this, "to_alternative"); + alternative_to_property_editor->connect("property_changed", callable_mp(this, &TileProxiesManagerDialog::_property_changed)); + alternative_to_property_editor->set_selectable(false); + alternative_to_property_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + alternative_to_property_editor->setup(-1, 99999, 1, true, false); + alternative_to_property_editor->hide(); + vboxcontainer_to->add_child(alternative_to_property_editor); + + Button *add_button = memnew(Button); + add_button->set_text(TTR("Add")); + add_button->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + add_button->connect("pressed", callable_mp(this, &TileProxiesManagerDialog::_add_button_pressed)); + vbox_container->add_child(add_button); + + h_separator = memnew(HSeparator); + vbox_container->add_child(h_separator); + + // Generic actions. + Label *generic_actions_label = memnew(Label); + generic_actions_label->set_text(TTR("Global actions:")); + vbox_container->add_child(generic_actions_label); + + hboxcontainer = memnew(HBoxContainer); + vbox_container->add_child(hboxcontainer); + + Button *clear_invalid_button = memnew(Button); + clear_invalid_button->set_text(TTR("Clear Invalid")); + clear_invalid_button->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + clear_invalid_button->connect("pressed", callable_mp(this, &TileProxiesManagerDialog::_clear_invalid_button_pressed)); + hboxcontainer->add_child(clear_invalid_button); + + Button *clear_all_button = memnew(Button); + clear_all_button->set_text(TTR("Clear All")); + clear_all_button->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + clear_all_button->connect("pressed", callable_mp(this, &TileProxiesManagerDialog::_clear_all_button_pressed)); + hboxcontainer->add_child(clear_all_button); + + h_separator = memnew(HSeparator); + vbox_container->add_child(h_separator); +} diff --git a/editor/plugins/tiles/tile_proxies_manager_dialog.h b/editor/plugins/tiles/tile_proxies_manager_dialog.h new file mode 100644 index 0000000000..6849be2cd6 --- /dev/null +++ b/editor/plugins/tiles/tile_proxies_manager_dialog.h @@ -0,0 +1,90 @@ +/*************************************************************************/ +/* tile_proxies_manager_dialog.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILE_PROXIES_MANAGER_DIALOG_H +#define TILE_PROXIES_MANAGER_DIALOG_H + +#include "editor/editor_node.h" +#include "editor/editor_properties.h" + +#include "scene/2d/tile_map.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/item_list.h" + +class TileProxiesManagerDialog : public ConfirmationDialog { + GDCLASS(TileProxiesManagerDialog, ConfirmationDialog); + +private: + int commited_actions_count = 0; + Ref<TileSet> tile_set; + + UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo(); + + TileMapCell from; + TileMapCell to; + + // GUI + ItemList *source_level_list; + ItemList *coords_level_list; + ItemList *alternative_level_list; + + EditorPropertyInteger *source_from_property_editor; + EditorPropertyVector2i *coords_from_property_editor; + EditorPropertyInteger *alternative_from_property_editor; + EditorPropertyInteger *source_to_property_editor; + EditorPropertyVector2i *coords_to_property_editor; + EditorPropertyInteger *alternative_to_property_editor; + + PopupMenu *popup_menu; + void _right_clicked(int p_item, Vector2 p_local_mouse_pos, Object *p_item_list); + void _menu_id_pressed(int p_id); + void _delete_selected_bindings(); + void _update_lists(); + void _update_enabled_property_editors(); + void _property_changed(const String &p_path, const Variant &p_value, const String &p_name, bool p_changing); + void _add_button_pressed(); + + void _clear_invalid_button_pressed(); + void _clear_all_button_pressed(); + +protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _unhandled_key_input(Ref<InputEvent> p_event); + virtual void cancel_pressed() override; + static void _bind_methods(); + +public: + void update_tile_set(Ref<TileSet> p_tile_set); + + TileProxiesManagerDialog(); +}; + +#endif // TILE_PROXIES_MANAGER_DIALOG_H diff --git a/editor/plugins/tiles/tile_set_atlas_source_editor.cpp b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp new file mode 100644 index 0000000000..a48c0e795c --- /dev/null +++ b/editor/plugins/tiles/tile_set_atlas_source_editor.cpp @@ -0,0 +1,2728 @@ +/*************************************************************************/ +/* tile_set_atlas_source_editor.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tile_set_atlas_source_editor.h" + +#include "tiles_editor_plugin.h" + +#include "editor/editor_inspector.h" +#include "editor/editor_scale.h" +#include "editor/progress_dialog.h" + +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/control.h" +#include "scene/gui/item_list.h" +#include "scene/gui/separator.h" +#include "scene/gui/split_container.h" +#include "scene/gui/tab_container.h" + +#include "core/core_string_names.h" +#include "core/math/geometry_2d.h" +#include "core/os/keyboard.h" + +void TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::set_id(int p_id) { + ERR_FAIL_COND(p_id < 0); + if (source_id == p_id) { + return; + } + ERR_FAIL_COND_MSG(tile_set->has_source(p_id), vformat("Cannot change TileSet Atlas Source ID. Another source exists with id %d.", p_id)); + + int previous_source = source_id; + source_id = p_id; // source_id must be updated before, because it's used by the source list update. + tile_set->set_source_id(previous_source, p_id); + emit_signal(SNAME("changed"), "id"); +} + +int TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::get_id() { + return source_id; +} + +bool TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::_set(const StringName &p_name, const Variant &p_value) { + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } + bool valid = false; + tile_set_atlas_source->set(name, p_value, &valid); + if (valid) { + emit_signal(SNAME("changed"), String(name).utf8().get_data()); + } + return valid; +} + +bool TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::_get(const StringName &p_name, Variant &r_ret) const { + if (!tile_set_atlas_source) { + return false; + } + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } + bool valid = false; + r_ret = tile_set_atlas_source->get(name, &valid); + return valid; +} + +void TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::_get_property_list(List<PropertyInfo> *p_list) const { + p_list->push_back(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D")); + p_list->push_back(PropertyInfo(Variant::VECTOR2I, "margins", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::VECTOR2I, "separation", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::VECTOR2I, "texture_region_size", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::BOOL, "use_texture_padding", PROPERTY_HINT_NONE, "")); +} + +void TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::_bind_methods() { + // -- Shape and layout -- + ClassDB::bind_method(D_METHOD("set_id", "id"), &TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::set_id); + ClassDB::bind_method(D_METHOD("get_id"), &TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::get_id); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "id"), "set_id", "get_id"); + + ADD_SIGNAL(MethodInfo("changed", PropertyInfo(Variant::STRING, "what"))); +} + +void TileSetAtlasSourceEditor::TileSetAtlasSourceProxyObject::edit(Ref<TileSet> p_tile_set, TileSetAtlasSource *p_tile_set_atlas_source, int p_source_id) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + ERR_FAIL_COND(!p_tile_set_atlas_source); + ERR_FAIL_COND(p_source_id < 0); + ERR_FAIL_COND(p_tile_set->get_source(p_source_id) != p_tile_set_atlas_source); + + if (p_tile_set == tile_set && p_tile_set_atlas_source == tile_set_atlas_source && p_source_id == source_id) { + return; + } + + // Disconnect to changes. + if (tile_set_atlas_source) { + tile_set_atlas_source->disconnect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); + } + + tile_set = p_tile_set; + tile_set_atlas_source = p_tile_set_atlas_source; + source_id = p_source_id; + + // Connect to changes. + if (tile_set_atlas_source) { + if (!tile_set_atlas_source->is_connected(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed))) { + tile_set_atlas_source->connect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); + } + } + + notify_property_list_changed(); +} + +// -- Proxy object used by the tile inspector -- +bool TileSetAtlasSourceEditor::AtlasTileProxyObject::_set(const StringName &p_name, const Variant &p_value) { + if (!tile_set_atlas_source) { + return false; + } + + // ID and size related properties. + if (tiles.size() == 1) { + const Vector2i &coords = tiles.front()->get().tile; + const int &alternative = tiles.front()->get().alternative; + + if (alternative == 0) { + Vector<String> components = String(p_name).split("/", true, 2); + if (p_name == "atlas_coords") { + Vector2i as_vector2i = Vector2i(p_value); + bool has_room_for_tile = tile_set_atlas_source->has_room_for_tile(as_vector2i, tile_set_atlas_source->get_tile_size_in_atlas(coords), tile_set_atlas_source->get_tile_animation_columns(coords), tile_set_atlas_source->get_tile_animation_separation(coords), tile_set_atlas_source->get_tile_animation_frames_count(coords), coords); + ERR_FAIL_COND_V(!has_room_for_tile, false); + + if (tiles_set_atlas_source_editor->selection.front()->get().tile == coords) { + tiles_set_atlas_source_editor->selection.clear(); + tiles_set_atlas_source_editor->selection.insert({ as_vector2i, 0 }); + tiles_set_atlas_source_editor->_update_tile_id_label(); + } + + tile_set_atlas_source->move_tile_in_atlas(coords, as_vector2i); + tiles.clear(); + tiles.insert({ as_vector2i, 0 }); + emit_signal(SNAME("changed"), "atlas_coords"); + return true; + } else if (p_name == "size_in_atlas") { + Vector2i as_vector2i = Vector2i(p_value); + bool has_room_for_tile = tile_set_atlas_source->has_room_for_tile(coords, as_vector2i, tile_set_atlas_source->get_tile_animation_columns(coords), tile_set_atlas_source->get_tile_animation_separation(coords), tile_set_atlas_source->get_tile_animation_frames_count(coords), coords); + ERR_FAIL_COND_V_EDMSG(!has_room_for_tile, false, "Invalid size or not enough room in the atlas for the tile."); + tile_set_atlas_source->move_tile_in_atlas(coords, TileSetSource::INVALID_ATLAS_COORDS, as_vector2i); + emit_signal(SNAME("changed"), "size_in_atlas"); + return true; + } + } else if (alternative > 0) { + if (p_name == "alternative_id") { + int as_int = int(p_value); + ERR_FAIL_COND_V(as_int < 0, false); + ERR_FAIL_COND_V_MSG(tile_set_atlas_source->has_alternative_tile(coords, as_int), false, vformat("Cannot change alternative tile ID. Another alternative exists with id %d for tile at coords %s.", as_int, coords)); + + if (tiles_set_atlas_source_editor->selection.front()->get().alternative == alternative) { + tiles_set_atlas_source_editor->selection.clear(); + tiles_set_atlas_source_editor->selection.insert({ coords, as_int }); + } + + int previous_alternative_tile = alternative; + tiles.clear(); + tiles.insert({ coords, as_int }); // tiles must be updated before. + tile_set_atlas_source->set_alternative_tile_id(coords, previous_alternative_tile, as_int); + + emit_signal(SNAME("changed"), "alternative_id"); + return true; + } + } + } + + // Animation. + // Check if all tiles have an alternative_id of 0. + bool all_alternatve_id_zero = true; + for (TileSelection tile : tiles) { + if (tile.alternative != 0) { + all_alternatve_id_zero = false; + break; + } + } + + if (all_alternatve_id_zero) { + Vector<String> components = String(p_name).split("/", true, 2); + if (p_name == "animation_columns") { + for (TileSelection tile : tiles) { + bool has_room_for_tile = tile_set_atlas_source->has_room_for_tile(tile.tile, tile_set_atlas_source->get_tile_size_in_atlas(tile.tile), p_value, tile_set_atlas_source->get_tile_animation_separation(tile.tile), tile_set_atlas_source->get_tile_animation_frames_count(tile.tile), tile.tile); + if (!has_room_for_tile) { + ERR_PRINT("No room for tile"); + } else { + tile_set_atlas_source->set_tile_animation_columns(tile.tile, p_value); + } + } + emit_signal(SNAME("changed"), "animation_columns"); + return true; + } else if (p_name == "animation_separation") { + for (TileSelection tile : tiles) { + bool has_room_for_tile = tile_set_atlas_source->has_room_for_tile(tile.tile, tile_set_atlas_source->get_tile_size_in_atlas(tile.tile), tile_set_atlas_source->get_tile_animation_columns(tile.tile), p_value, tile_set_atlas_source->get_tile_animation_frames_count(tile.tile), tile.tile); + if (!has_room_for_tile) { + ERR_PRINT("No room for tile"); + } else { + tile_set_atlas_source->set_tile_animation_separation(tile.tile, p_value); + } + } + emit_signal(SNAME("changed"), "animation_separation"); + return true; + } else if (p_name == "animation_speed") { + for (TileSelection tile : tiles) { + tile_set_atlas_source->set_tile_animation_speed(tile.tile, p_value); + } + emit_signal(SNAME("changed"), "animation_speed"); + return true; + } else if (p_name == "animation_frames_count") { + for (TileSelection tile : tiles) { + bool has_room_for_tile = tile_set_atlas_source->has_room_for_tile(tile.tile, tile_set_atlas_source->get_tile_size_in_atlas(tile.tile), tile_set_atlas_source->get_tile_animation_columns(tile.tile), tile_set_atlas_source->get_tile_animation_separation(tile.tile), p_value, tile.tile); + if (!has_room_for_tile) { + ERR_PRINT("No room for tile"); + } else { + tile_set_atlas_source->set_tile_animation_frames_count(tile.tile, p_value); + } + } + notify_property_list_changed(); + emit_signal(SNAME("changed"), "animation_separation"); + return true; + } else if (components.size() == 2 && components[0].begins_with("animation_frame_") && components[0].trim_prefix("animation_frame_").is_valid_int()) { + for (TileSelection tile : tiles) { + int frame = components[0].trim_prefix("animation_frame_").to_int(); + if (frame < 0 || frame >= tile_set_atlas_source->get_tile_animation_frames_count(tile.tile)) { + ERR_PRINT(vformat("No tile animation frame with index %d", frame)); + } else { + if (components[1] == "duration") { + tile_set_atlas_source->set_tile_animation_frame_duration(tile.tile, frame, p_value); + return true; + } + } + } + } + } + + // Other properties. + bool any_valid = false; + for (Set<TileSelection>::Element *E = tiles.front(); E; E = E->next()) { + const Vector2i &coords = E->get().tile; + const int &alternative = E->get().alternative; + + bool valid = false; + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(coords, alternative)); + ERR_FAIL_COND_V(!tile_data, false); + tile_data->set(p_name, p_value, &valid); + + any_valid |= valid; + } + + if (any_valid) { + emit_signal(SNAME("changed"), String(p_name).utf8().get_data()); + } + + return any_valid; +} + +bool TileSetAtlasSourceEditor::AtlasTileProxyObject::_get(const StringName &p_name, Variant &r_ret) const { + if (!tile_set_atlas_source) { + return false; + } + + // ID and size related properties.s + if (tiles.size() == 1) { + const Vector2i &coords = tiles.front()->get().tile; + const int &alternative = tiles.front()->get().alternative; + + if (alternative == 0) { + Vector<String> components = String(p_name).split("/", true, 2); + if (p_name == "atlas_coords") { + r_ret = coords; + return true; + } else if (p_name == "size_in_atlas") { + r_ret = tile_set_atlas_source->get_tile_size_in_atlas(coords); + return true; + } + } else if (alternative > 0) { + if (p_name == "alternative_id") { + r_ret = alternative; + return true; + } + } + } + + // Animation. + // Check if all tiles have an alternative_id of 0. + bool all_alternatve_id_zero = true; + for (TileSelection tile : tiles) { + if (tile.alternative != 0) { + all_alternatve_id_zero = false; + break; + } + } + + if (all_alternatve_id_zero) { + const Vector2i &coords = tiles.front()->get().tile; + + Vector<String> components = String(p_name).split("/", true, 2); + if (p_name == "animation_columns") { + r_ret = tile_set_atlas_source->get_tile_animation_columns(coords); + return true; + } else if (p_name == "animation_separation") { + r_ret = tile_set_atlas_source->get_tile_animation_separation(coords); + return true; + } else if (p_name == "animation_speed") { + r_ret = tile_set_atlas_source->get_tile_animation_speed(coords); + return true; + } else if (p_name == "animation_frames_count") { + r_ret = tile_set_atlas_source->get_tile_animation_frames_count(coords); + return true; + } else if (components.size() == 2 && components[0].begins_with("animation_frame_") && components[0].trim_prefix("animation_frame_").is_valid_int()) { + int frame = components[0].trim_prefix("animation_frame_").to_int(); + if (components[1] == "duration") { + if (frame < 0 || frame >= tile_set_atlas_source->get_tile_animation_frames_count(coords)) { + return false; + } + r_ret = tile_set_atlas_source->get_tile_animation_frame_duration(coords, frame); + return true; + } + } + } + + for (Set<TileSelection>::Element *E = tiles.front(); E; E = E->next()) { + // Return the first tile with a property matching the name. + // Note: It's a little bit annoying, but the behavior is the same the one in MultiNodeEdit. + const Vector2i &coords = E->get().tile; + const int &alternative = E->get().alternative; + + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(coords, alternative)); + ERR_FAIL_COND_V(!tile_data, false); + + bool valid = false; + r_ret = tile_data->get(p_name, &valid); + if (valid) { + return true; + } + } + + return false; +} + +void TileSetAtlasSourceEditor::AtlasTileProxyObject::_get_property_list(List<PropertyInfo> *p_list) const { + if (!tile_set_atlas_source) { + return; + } + + // ID and size related properties. + if (tiles.size() == 1) { + if (tiles.front()->get().alternative == 0) { + p_list->push_back(PropertyInfo(Variant::VECTOR2I, "atlas_coords", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::VECTOR2I, "size_in_atlas", PROPERTY_HINT_NONE, "")); + } else { + p_list->push_back(PropertyInfo(Variant::INT, "alternative_id", PROPERTY_HINT_NONE, "")); + } + } + + // Animation. + // Check if all tiles have an alternative_id of 0. + bool all_alternatve_id_zero = true; + for (TileSelection tile : tiles) { + if (tile.alternative != 0) { + all_alternatve_id_zero = false; + break; + } + } + + if (all_alternatve_id_zero) { + p_list->push_back(PropertyInfo(Variant::NIL, "Animation", PROPERTY_HINT_NONE, "animation_", PROPERTY_USAGE_GROUP)); + p_list->push_back(PropertyInfo(Variant::INT, "animation_columns", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::VECTOR2I, "animation_separation", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::FLOAT, "animation_speed", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::INT, "animation_frames_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_ARRAY, "Frames,animation_frame_")); + // Not optimal, but returns value for the first tile. This is similar to what MultiNodeEdit does. + if (tile_set_atlas_source->get_tile_animation_frames_count(tiles.front()->get().tile) == 1) { + p_list->push_back(PropertyInfo(Variant::FLOAT, "animation_frame_0/duration", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY)); + } else { + for (int i = 0; i < tile_set_atlas_source->get_tile_animation_frames_count(tiles.front()->get().tile); i++) { + p_list->push_back(PropertyInfo(Variant::FLOAT, vformat("animation_frame_%d/duration", i), PROPERTY_HINT_NONE, "")); + } + } + } + + // Get the list of properties common to all tiles (similar to what's done in MultiNodeEdit). + struct PropertyId { + int occurence_id = 0; + String property; + bool operator<(const PropertyId &p_other) const { + return occurence_id == p_other.occurence_id ? property < p_other.property : occurence_id < p_other.occurence_id; + } + }; + struct PLData { + int uses = 0; + PropertyInfo property_info; + }; + Map<PropertyId, PLData> usage; + + List<PLData *> data_list; + for (Set<TileSelection>::Element *E = tiles.front(); E; E = E->next()) { + const Vector2i &coords = E->get().tile; + const int &alternative = E->get().alternative; + + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(coords, alternative)); + ERR_FAIL_COND(!tile_data); + + List<PropertyInfo> list; + tile_data->get_property_list(&list); + + Map<String, int> counts; // Counts the number of time a property appears (useful for groups that may appear more than once) + for (List<PropertyInfo>::Element *E_property = list.front(); E_property; E_property = E_property->next()) { + const String &property_string = E_property->get().name; + if (!tile_data->is_allowing_transform() && (property_string == "flip_h" || property_string == "flip_v" || property_string == "transpose")) { + continue; + } + + if (!counts.has(property_string)) { + counts[property_string] = 1; + } else { + counts[property_string] += 1; + } + + PropertyInfo stored_property_info = E_property->get(); + stored_property_info.usage |= PROPERTY_USAGE_STORAGE; // Ignore the storage flag in comparing properties. + + PropertyId id = { counts[property_string], property_string }; + if (!usage.has(id)) { + usage[id] = { 1, stored_property_info }; + data_list.push_back(&usage[id]); + } else if (usage[id].property_info == stored_property_info) { + usage[id].uses += 1; + } + } + } + + // Add only properties that are common to all tiles. + for (const PLData *E : data_list) { + if (E->uses == tiles.size()) { + p_list->push_back(E->property_info); + } + } +} + +void TileSetAtlasSourceEditor::AtlasTileProxyObject::edit(TileSetAtlasSource *p_tile_set_atlas_source, Set<TileSelection> p_tiles) { + ERR_FAIL_COND(!p_tile_set_atlas_source); + ERR_FAIL_COND(p_tiles.is_empty()); + for (Set<TileSelection>::Element *E = p_tiles.front(); E; E = E->next()) { + ERR_FAIL_COND(E->get().tile == TileSetSource::INVALID_ATLAS_COORDS); + ERR_FAIL_COND(E->get().alternative < 0); + } + + // Disconnect to changes. + for (Set<TileSelection>::Element *E = tiles.front(); E; E = E->next()) { + const Vector2i &coords = E->get().tile; + const int &alternative = E->get().alternative; + + if (tile_set_atlas_source && tile_set_atlas_source->has_tile(coords) && tile_set_atlas_source->has_alternative_tile(coords, alternative)) { + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(coords, alternative)); + if (tile_data->is_connected(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed))) { + tile_data->disconnect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); + } + } + } + + tile_set_atlas_source = p_tile_set_atlas_source; + tiles = Set<TileSelection>(p_tiles); + + // Connect to changes. + for (Set<TileSelection>::Element *E = p_tiles.front(); E; E = E->next()) { + const Vector2i &coords = E->get().tile; + const int &alternative = E->get().alternative; + + if (tile_set_atlas_source->has_tile(coords) && tile_set_atlas_source->has_alternative_tile(coords, alternative)) { + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(coords, alternative)); + if (!tile_data->is_connected(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed))) { + tile_data->connect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); + } + } + } + + notify_property_list_changed(); +} + +void TileSetAtlasSourceEditor::AtlasTileProxyObject::_bind_methods() { + ADD_SIGNAL(MethodInfo("changed", PropertyInfo(Variant::STRING, "what"))); +} + +void TileSetAtlasSourceEditor::_inspector_property_selected(String p_property) { + selected_property = p_property; + _update_atlas_view(); + _update_current_tile_data_editor(); +} + +void TileSetAtlasSourceEditor::_update_tile_id_label() { + if (selection.size() == 1) { + TileSelection selected = selection.front()->get(); + tool_tile_id_label->set_text(vformat("%d, %s, %d", tile_set_atlas_source_id, selected.tile, selected.alternative)); + tool_tile_id_label->set_tooltip(vformat(TTR("Selected tile:\nSource: %d\nAtlas coordinates: %s\nAlternative: %d"), tile_set_atlas_source_id, selected.tile, selected.alternative)); + tool_tile_id_label->show(); + } else { + tool_tile_id_label->hide(); + } +} + +void TileSetAtlasSourceEditor::_update_source_inspector() { + // Update the proxy object. + atlas_source_proxy_object->edit(tile_set, tile_set_atlas_source, tile_set_atlas_source_id); +} + +void TileSetAtlasSourceEditor::_update_fix_selected_and_hovered_tiles() { + // Fix selected. + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + TileSelection selected = E->get(); + if (!tile_set_atlas_source->has_tile(selected.tile) || !tile_set_atlas_source->has_alternative_tile(selected.tile, selected.alternative)) { + selection.erase(E); + } + } + + // Fix hovered. + if (!tile_set_atlas_source->has_tile(hovered_base_tile_coords)) { + hovered_base_tile_coords = TileSetSource::INVALID_ATLAS_COORDS; + } + Vector2i coords = Vector2i(hovered_alternative_tile_coords.x, hovered_alternative_tile_coords.y); + int alternative = hovered_alternative_tile_coords.z; + if (!tile_set_atlas_source->has_tile(coords) || !tile_set_atlas_source->has_alternative_tile(coords, alternative)) { + hovered_alternative_tile_coords = Vector3i(TileSetSource::INVALID_ATLAS_COORDS.x, TileSetSource::INVALID_ATLAS_COORDS.y, TileSetSource::INVALID_TILE_ALTERNATIVE); + } +} + +void TileSetAtlasSourceEditor::_update_atlas_source_inspector() { + // Update visibility. + bool visible = tools_button_group->get_pressed_button() == tool_setup_atlas_source_button; + atlas_source_inspector_label->set_visible(visible); + atlas_source_inspector->set_visible(visible); +} + +void TileSetAtlasSourceEditor::_update_tile_inspector() { + // Update visibility. + if (tools_button_group->get_pressed_button() == tool_select_button) { + if (!selection.is_empty()) { + tile_proxy_object->edit(tile_set_atlas_source, selection); + } + tile_inspector_label->show(); + tile_inspector->set_visible(!selection.is_empty()); + tile_inspector_no_tile_selected_label->set_visible(selection.is_empty()); + } else { + tile_inspector_label->hide(); + tile_inspector->hide(); + tile_inspector_no_tile_selected_label->hide(); + } +} + +void TileSetAtlasSourceEditor::_update_tile_data_editors() { + String previously_selected; + if (tile_data_editors_tree && tile_data_editors_tree->get_selected()) { + previously_selected = tile_data_editors_tree->get_selected()->get_metadata(0); + } + + tile_data_editors_tree->clear(); + + TreeItem *root = tile_data_editors_tree->create_item(); + + TreeItem *group; +#define ADD_TILE_DATA_EDITOR_GROUP(text) \ + group = tile_data_editors_tree->create_item(root); \ + group->set_custom_bg_color(0, group_color); \ + group->set_selectable(0, false); \ + group->set_disable_folding(true); \ + group->set_text(0, text); + + TreeItem *item; +#define ADD_TILE_DATA_EDITOR(parent, text, property) \ + item = tile_data_editors_tree->create_item(parent); \ + item->set_text(0, text); \ + item->set_metadata(0, property); \ + if (property == previously_selected) { \ + item->select(0); \ + } + + // Theming. + tile_data_editors_tree->add_theme_constant_override("vseparation", 1); + tile_data_editors_tree->add_theme_constant_override("hseparation", 3); + + Color group_color = get_theme_color(SNAME("prop_category"), SNAME("Editor")); + + // List of editors. + // --- Rendering --- + ADD_TILE_DATA_EDITOR_GROUP("Rendering"); + + ADD_TILE_DATA_EDITOR(group, "Texture Offset", "texture_offset"); + if (!tile_data_editors.has("texture_offset")) { + TileDataTextureOffsetEditor *tile_data_texture_offset_editor = memnew(TileDataTextureOffsetEditor); + tile_data_texture_offset_editor->hide(); + tile_data_texture_offset_editor->setup_property_editor(Variant::VECTOR2, "texture_offset"); + tile_data_texture_offset_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_texture_offset_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors["texture_offset"] = tile_data_texture_offset_editor; + } + + ADD_TILE_DATA_EDITOR(group, "Modulate", "modulate"); + if (!tile_data_editors.has("modulate")) { + TileDataDefaultEditor *tile_data_modulate_editor = memnew(TileDataDefaultEditor()); + tile_data_modulate_editor->hide(); + tile_data_modulate_editor->setup_property_editor(Variant::COLOR, "modulate", "", Color(1.0, 1.0, 1.0, 1.0)); + tile_data_modulate_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_modulate_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors["modulate"] = tile_data_modulate_editor; + } + + ADD_TILE_DATA_EDITOR(group, "Z Index", "z_index"); + if (!tile_data_editors.has("z_index")) { + TileDataDefaultEditor *tile_data_z_index_editor = memnew(TileDataDefaultEditor()); + tile_data_z_index_editor->hide(); + tile_data_z_index_editor->setup_property_editor(Variant::INT, "z_index"); + tile_data_z_index_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_z_index_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors["z_index"] = tile_data_z_index_editor; + } + + ADD_TILE_DATA_EDITOR(group, "Y Sort Origin", "y_sort_origin"); + if (!tile_data_editors.has("y_sort_origin")) { + TileDataYSortEditor *tile_data_y_sort_editor = memnew(TileDataYSortEditor); + tile_data_y_sort_editor->hide(); + tile_data_y_sort_editor->setup_property_editor(Variant::INT, "y_sort_origin"); + tile_data_y_sort_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_y_sort_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors["y_sort_origin"] = tile_data_y_sort_editor; + } + + for (int i = 0; i < tile_set->get_occlusion_layers_count(); i++) { + ADD_TILE_DATA_EDITOR(group, vformat("Occlusion Layer %d", i), vformat("occlusion_layer_%d", i)); + if (!tile_data_editors.has(vformat("occlusion_layer_%d", i))) { + TileDataOcclusionShapeEditor *tile_data_occlusion_shape_editor = memnew(TileDataOcclusionShapeEditor()); + tile_data_occlusion_shape_editor->hide(); + tile_data_occlusion_shape_editor->set_occlusion_layer(i); + tile_data_occlusion_shape_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_occlusion_shape_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors[vformat("occlusion_layer_%d", i)] = tile_data_occlusion_shape_editor; + } + } + for (int i = tile_set->get_occlusion_layers_count(); tile_data_editors.has(vformat("occlusion_layer_%d", i)); i++) { + tile_data_editors[vformat("occlusion_layer_%d", i)]->queue_delete(); + tile_data_editors.erase(vformat("occlusion_layer_%d", i)); + } + + // --- Rendering --- + ADD_TILE_DATA_EDITOR(root, "Terrains", "terrain_set"); + if (!tile_data_editors.has("terrain_set")) { + TileDataTerrainsEditor *tile_data_terrains_editor = memnew(TileDataTerrainsEditor); + tile_data_terrains_editor->hide(); + tile_data_terrains_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_terrains_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors["terrain_set"] = tile_data_terrains_editor; + } + + // --- Miscellaneous --- + ADD_TILE_DATA_EDITOR(root, "Probability", "probability"); + if (!tile_data_editors.has("probability")) { + TileDataDefaultEditor *tile_data_probability_editor = memnew(TileDataDefaultEditor()); + tile_data_probability_editor->hide(); + tile_data_probability_editor->setup_property_editor(Variant::FLOAT, "probability", "", 1.0); + tile_data_probability_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_probability_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors["probability"] = tile_data_probability_editor; + } + + // --- Physics --- + ADD_TILE_DATA_EDITOR_GROUP("Physics"); + for (int i = 0; i < tile_set->get_physics_layers_count(); i++) { + ADD_TILE_DATA_EDITOR(group, vformat("Physics Layer %d", i), vformat("physics_layer_%d", i)); + if (!tile_data_editors.has(vformat("physics_layer_%d", i))) { + TileDataCollisionEditor *tile_data_collision_editor = memnew(TileDataCollisionEditor()); + tile_data_collision_editor->hide(); + tile_data_collision_editor->set_physics_layer(i); + tile_data_collision_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_collision_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors[vformat("physics_layer_%d", i)] = tile_data_collision_editor; + } + } + for (int i = tile_set->get_physics_layers_count(); tile_data_editors.has(vformat("physics_layer_%d", i)); i++) { + tile_data_editors[vformat("physics_layer_%d", i)]->queue_delete(); + tile_data_editors.erase(vformat("physics_layer_%d", i)); + } + + // --- Navigation --- + ADD_TILE_DATA_EDITOR_GROUP("Navigation"); + for (int i = 0; i < tile_set->get_navigation_layers_count(); i++) { + ADD_TILE_DATA_EDITOR(group, vformat("Navigation Layer %d", i), vformat("navigation_layer_%d", i)); + if (!tile_data_editors.has(vformat("navigation_layer_%d", i))) { + TileDataNavigationEditor *tile_data_navigation_editor = memnew(TileDataNavigationEditor()); + tile_data_navigation_editor->hide(); + tile_data_navigation_editor->set_navigation_layer(i); + tile_data_navigation_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_navigation_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors[vformat("navigation_layer_%d", i)] = tile_data_navigation_editor; + } + } + for (int i = tile_set->get_navigation_layers_count(); tile_data_editors.has(vformat("navigation_layer_%d", i)); i++) { + tile_data_editors[vformat("navigation_layer_%d", i)]->queue_delete(); + tile_data_editors.erase(vformat("navigation_layer_%d", i)); + } + + // --- Custom Data --- + ADD_TILE_DATA_EDITOR_GROUP("Custom Data"); + for (int i = 0; i < tile_set->get_custom_data_layers_count(); i++) { + if (tile_set->get_custom_data_name(i).is_empty()) { + ADD_TILE_DATA_EDITOR(group, vformat("Custom Data %d", i), vformat("custom_data_%d", i)); + } else { + ADD_TILE_DATA_EDITOR(group, tile_set->get_custom_data_name(i), vformat("custom_data_%d", i)); + } + if (!tile_data_editors.has(vformat("custom_data_%d", i))) { + TileDataDefaultEditor *tile_data_custom_data_editor = memnew(TileDataDefaultEditor()); + tile_data_custom_data_editor->hide(); + tile_data_custom_data_editor->setup_property_editor(tile_set->get_custom_data_type(i), vformat("custom_data_%d", i), tile_set->get_custom_data_name(i)); + tile_data_custom_data_editor->connect("needs_redraw", callable_mp((CanvasItem *)tile_atlas_control_unscaled, &Control::update)); + tile_data_custom_data_editor->connect("needs_redraw", callable_mp((CanvasItem *)alternative_tiles_control_unscaled, &Control::update)); + tile_data_editors[vformat("custom_data_%d", i)] = tile_data_custom_data_editor; + } + } + for (int i = tile_set->get_custom_data_layers_count(); tile_data_editors.has(vformat("custom_data_%d", i)); i++) { + tile_data_editors[vformat("custom_data_%d", i)]->queue_delete(); + tile_data_editors.erase(vformat("custom_data_%d", i)); + } + +#undef ADD_TILE_DATA_EDITOR_GROUP +#undef ADD_TILE_DATA_EDITOR + + // Add tile data editors as children. + for (KeyValue<String, TileDataEditor *> &E : tile_data_editors) { + // Tile Data Editor. + TileDataEditor *tile_data_editor = E.value; + if (!tile_data_editor->is_inside_tree()) { + tile_data_painting_editor_container->add_child(tile_data_editor); + } + tile_data_editor->set_tile_set(tile_set); + + // Toolbar. + Control *toolbar = tile_data_editor->get_toolbar(); + if (!toolbar->is_inside_tree()) { + tool_settings_tile_data_toolbar_container->add_child(toolbar); + } + toolbar->hide(); + } + + // Update visibility. + bool is_visible = tools_button_group->get_pressed_button() == tool_paint_button; + tile_data_editor_dropdown_button->set_visible(is_visible); + if (tile_data_editors_tree->get_selected()) { + tile_data_editor_dropdown_button->set_text(tile_data_editors_tree->get_selected()->get_text(0)); + } else { + tile_data_editor_dropdown_button->set_text(TTR("Select a property editor")); + } + tile_data_editors_label->set_visible(is_visible); +} + +void TileSetAtlasSourceEditor::_update_current_tile_data_editor() { + // Find the property to use. + String property; + if (tools_button_group->get_pressed_button() == tool_select_button && tile_inspector->is_visible() && !tile_inspector->get_selected_path().is_empty()) { + Vector<String> components = tile_inspector->get_selected_path().split("/"); + if (components.size() >= 1) { + property = components[0]; + + // Workaround for terrains as they don't have a common first component. + if (property.begins_with("terrains_")) { + property = "terrain_set"; + } + } + } else if (tools_button_group->get_pressed_button() == tool_paint_button && tile_data_editors_tree->get_selected()) { + property = tile_data_editors_tree->get_selected()->get_metadata(0); + tile_data_editor_dropdown_button->set_text(tile_data_editors_tree->get_selected()->get_text(0)); + } + + // Hide all editors but the current one. + for (const KeyValue<String, TileDataEditor *> &E : tile_data_editors) { + E.value->hide(); + E.value->get_toolbar()->hide(); + } + if (tile_data_editors.has(property)) { + current_tile_data_editor = tile_data_editors[property]; + } else { + current_tile_data_editor = nullptr; + } + + // Get the correct editor for the TileData's property. + if (current_tile_data_editor) { + current_tile_data_editor_toolbar = current_tile_data_editor->get_toolbar(); + current_property = property; + current_tile_data_editor->set_visible(tools_button_group->get_pressed_button() == tool_paint_button); + current_tile_data_editor_toolbar->set_visible(tools_button_group->get_pressed_button() == tool_paint_button); + } +} + +void TileSetAtlasSourceEditor::_tile_data_editor_dropdown_button_draw() { + if (!has_theme_icon(SNAME("arrow"), SNAME("OptionButton"))) { + return; + } + + RID ci = tile_data_editor_dropdown_button->get_canvas_item(); + Ref<Texture2D> arrow = Control::get_theme_icon(SNAME("arrow"), SNAME("OptionButton")); + Color clr = Color(1, 1, 1); + if (get_theme_constant(SNAME("modulate_arrow"))) { + switch (tile_data_editor_dropdown_button->get_draw_mode()) { + case BaseButton::DRAW_PRESSED: + clr = get_theme_color(SNAME("font_pressed_color")); + break; + case BaseButton::DRAW_HOVER: + clr = get_theme_color(SNAME("font_hover_color")); + break; + case BaseButton::DRAW_DISABLED: + clr = get_theme_color(SNAME("font_disabled_color")); + break; + default: + if (tile_data_editor_dropdown_button->has_focus()) { + clr = get_theme_color(SNAME("font_focus_color")); + } else { + clr = get_theme_color(SNAME("font_color")); + } + } + } + + Size2 size = tile_data_editor_dropdown_button->get_size(); + + Point2 ofs; + if (is_layout_rtl()) { + ofs = Point2(get_theme_constant(SNAME("arrow_margin"), SNAME("OptionButton")), int(Math::abs((size.height - arrow->get_height()) / 2))); + } else { + ofs = Point2(size.width - arrow->get_width() - get_theme_constant(SNAME("arrow_margin"), SNAME("OptionButton")), int(Math::abs((size.height - arrow->get_height()) / 2))); + } + arrow->draw(ci, ofs, clr); +} + +void TileSetAtlasSourceEditor::_tile_data_editor_dropdown_button_pressed() { + Size2 size = tile_data_editor_dropdown_button->get_size(); + tile_data_editors_popup->set_position(tile_data_editor_dropdown_button->get_screen_position() + Size2(0, size.height * get_global_transform().get_scale().y)); + tile_data_editors_popup->set_size(Size2(size.width, 0)); + tile_data_editors_popup->popup(); +} + +void TileSetAtlasSourceEditor::_tile_data_editors_tree_selected() { + tile_data_editors_popup->call_deferred(SNAME("hide")); + _update_current_tile_data_editor(); + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); +} + +void TileSetAtlasSourceEditor::_update_atlas_view() { + // Update the atlas display. + tile_atlas_view->set_atlas_source(*tile_set, tile_set_atlas_source, tile_set_atlas_source_id); + + // Create a bunch of buttons to add alternative tiles. + for (int i = 0; i < alternative_tiles_control->get_child_count(); i++) { + alternative_tiles_control->get_child(i)->queue_delete(); + } + + Vector2i pos; + Vector2 texture_region_base_size = tile_set_atlas_source->get_texture_region_size(); + int texture_region_base_size_min = MIN(texture_region_base_size.x, texture_region_base_size.y); + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i tile_id = tile_set_atlas_source->get_tile_id(i); + int alternative_count = tile_set_atlas_source->get_alternative_tiles_count(tile_id); + if (alternative_count > 1) { + // Compute the right extremity of alternative. + int y_increment = 0; + pos.x = 0; + for (int j = 1; j < alternative_count; j++) { + int alternative_id = tile_set_atlas_source->get_alternative_tile_id(tile_id, j); + Rect2i rect = tile_atlas_view->get_alternative_tile_rect(tile_id, alternative_id); + pos.x = MAX(pos.x, rect.get_end().x); + y_increment = MAX(y_increment, rect.size.y); + } + + // Create and position the button. + Button *button = memnew(Button); + alternative_tiles_control->add_child(button); + button->set_flat(true); + button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons"))); + button->add_theme_style_override("normal", memnew(StyleBoxEmpty)); + button->add_theme_style_override("hover", memnew(StyleBoxEmpty)); + button->add_theme_style_override("focus", memnew(StyleBoxEmpty)); + button->add_theme_style_override("pressed", memnew(StyleBoxEmpty)); + button->connect("pressed", callable_mp(tile_set_atlas_source, &TileSetAtlasSource::create_alternative_tile), varray(tile_id, TileSetSource::INVALID_TILE_ALTERNATIVE)); + button->set_rect(Rect2(Vector2(pos.x, pos.y + (y_increment - texture_region_base_size.y) / 2.0), Vector2(texture_region_base_size_min, texture_region_base_size_min))); + button->set_expand_icon(true); + + pos.y += y_increment; + } + } + tile_atlas_view->set_padding(Side::SIDE_RIGHT, texture_region_base_size_min); + + // Redraw everything. + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + tile_atlas_view->update(); + + // Synchronize atlas view. + TilesEditorPlugin::get_singleton()->synchronize_atlas_view(tile_atlas_view); +} + +void TileSetAtlasSourceEditor::_update_toolbar() { + // Show the tools and settings. + if (tools_button_group->get_pressed_button() == tool_setup_atlas_source_button) { + if (current_tile_data_editor_toolbar) { + current_tile_data_editor_toolbar->hide(); + } + tool_settings_vsep->show(); + tools_settings_erase_button->show(); + tool_advanced_menu_buttom->show(); + } else if (tools_button_group->get_pressed_button() == tool_select_button) { + if (current_tile_data_editor_toolbar) { + current_tile_data_editor_toolbar->hide(); + } + tool_settings_vsep->hide(); + tools_settings_erase_button->hide(); + tool_advanced_menu_buttom->hide(); + } else if (tools_button_group->get_pressed_button() == tool_paint_button) { + if (current_tile_data_editor_toolbar) { + current_tile_data_editor_toolbar->show(); + } + tool_settings_vsep->hide(); + tools_settings_erase_button->hide(); + tool_advanced_menu_buttom->hide(); + } +} + +void TileSetAtlasSourceEditor::_tile_atlas_control_mouse_exited() { + hovered_base_tile_coords = TileSetSource::INVALID_ATLAS_COORDS; + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + tile_atlas_view->update(); +} + +void TileSetAtlasSourceEditor::_tile_atlas_view_transform_changed() { + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); +} + +void TileSetAtlasSourceEditor::_tile_atlas_control_gui_input(const Ref<InputEvent> &p_event) { + // Update the hovered coords. + hovered_base_tile_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + + // Forward the event to the current tile data editor if we are in the painting mode. + if (tools_button_group->get_pressed_button() == tool_paint_button) { + if (current_tile_data_editor) { + current_tile_data_editor->forward_painting_atlas_gui_input(tile_atlas_view, tile_set_atlas_source, p_event); + } + // Update only what's needed. + tile_set_changed_needs_update = false; + + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + tile_atlas_view->update(); + return; + } else { + // Handle the event. + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + Vector2i start_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + Vector2i last_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_last_mouse_pos); + Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + + Vector2i grid_size = tile_set_atlas_source->get_atlas_grid_size(); + + if (drag_type == DRAG_TYPE_NONE) { + if (selection.size() == 1) { + // Change the cursor depending on the hovered thing. + TileSelection selected = selection.front()->get(); + if (selected.tile != TileSetSource::INVALID_ATLAS_COORDS && selected.alternative == 0) { + Vector2 mouse_local_pos = tile_atlas_control->get_local_mouse_position(); + Vector2i size_in_atlas = tile_set_atlas_source->get_tile_size_in_atlas(selected.tile); + Rect2 region = tile_set_atlas_source->get_tile_texture_region(selected.tile); + Size2 zoomed_size = resize_handle->get_size() / tile_atlas_view->get_zoom(); + Rect2 rect = region.grow_individual(zoomed_size.x, zoomed_size.y, 0, 0); + const Vector2i coords[] = { Vector2i(0, 0), Vector2i(1, 0), Vector2i(1, 1), Vector2i(0, 1) }; + const Vector2i directions[] = { Vector2i(0, -1), Vector2i(1, 0), Vector2i(0, 1), Vector2i(-1, 0) }; + CursorShape cursor_shape = CURSOR_ARROW; + bool can_grow[4]; + for (int i = 0; i < 4; i++) { + can_grow[i] = tile_set_atlas_source->has_room_for_tile(selected.tile + directions[i], tile_set_atlas_source->get_tile_size_in_atlas(selected.tile), tile_set_atlas_source->get_tile_animation_columns(selected.tile), tile_set_atlas_source->get_tile_animation_separation(selected.tile), tile_set_atlas_source->get_tile_animation_frames_count(selected.tile), selected.tile); + can_grow[i] |= (i % 2 == 0) ? size_in_atlas.y > 1 : size_in_atlas.x > 1; + } + for (int i = 0; i < 4; i++) { + Vector2 pos = rect.position + rect.size * coords[i]; + if (can_grow[i] && can_grow[(i + 3) % 4] && Rect2(pos, zoomed_size).has_point(mouse_local_pos)) { + cursor_shape = (i % 2) ? CURSOR_BDIAGSIZE : CURSOR_FDIAGSIZE; + } + Vector2 next_pos = rect.position + rect.size * coords[(i + 1) % 4]; + if (can_grow[i] && Rect2((pos + next_pos) / 2.0, zoomed_size).has_point(mouse_local_pos)) { + cursor_shape = (i % 2) ? CURSOR_HSIZE : CURSOR_VSIZE; + } + } + tile_atlas_control->set_default_cursor_shape(cursor_shape); + } + } + } else if (drag_type == DRAG_TYPE_CREATE_BIG_TILE) { + // Create big tile. + new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + + Rect2i new_rect = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); + new_rect.size += Vector2i(1, 1); + // Check if the new tile can fit in the new rect. + if (tile_set_atlas_source->has_room_for_tile(new_rect.position, new_rect.size, tile_set_atlas_source->get_tile_animation_columns(drag_current_tile), tile_set_atlas_source->get_tile_animation_separation(drag_current_tile), tile_set_atlas_source->get_tile_animation_frames_count(drag_current_tile), drag_current_tile)) { + // Move and resize the tile. + tile_set_atlas_source->move_tile_in_atlas(drag_current_tile, new_rect.position, new_rect.size); + drag_current_tile = new_rect.position; + } + } else if (drag_type == DRAG_TYPE_CREATE_TILES) { + // Create tiles. + last_base_tiles_coords = last_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + + Vector<Point2i> line = Geometry2D::bresenham_line(last_base_tiles_coords, new_base_tiles_coords); + for (int i = 0; i < line.size(); i++) { + if (tile_set_atlas_source->get_tile_at_coords(line[i]) == TileSetSource::INVALID_ATLAS_COORDS) { + tile_set_atlas_source->create_tile(line[i]); + drag_modified_tiles.insert(line[i]); + } + } + + drag_last_mouse_pos = tile_atlas_control->get_local_mouse_position(); + + } else if (drag_type == DRAG_TYPE_REMOVE_TILES) { + // Remove tiles. + last_base_tiles_coords = last_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + + Vector<Point2i> line = Geometry2D::bresenham_line(last_base_tiles_coords, new_base_tiles_coords); + for (int i = 0; i < line.size(); i++) { + Vector2i base_tile_coords = tile_set_atlas_source->get_tile_at_coords(line[i]); + if (base_tile_coords != TileSetSource::INVALID_ATLAS_COORDS) { + drag_modified_tiles.insert(base_tile_coords); + } + } + + drag_last_mouse_pos = tile_atlas_control->get_local_mouse_position(); + } else if (drag_type == DRAG_TYPE_MOVE_TILE) { + // Move tile. + Vector2 mouse_offset = (Vector2(tile_set_atlas_source->get_tile_size_in_atlas(drag_current_tile)) / 2.0 - Vector2(0.5, 0.5)) * tile_set->get_tile_size(); + Vector2i coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position() - mouse_offset); + coords = coords.max(Vector2i(0, 0)).min(grid_size - Vector2i(1, 1)); + if (drag_current_tile != coords && tile_set_atlas_source->has_room_for_tile(coords, tile_set_atlas_source->get_tile_size_in_atlas(drag_current_tile), tile_set_atlas_source->get_tile_animation_columns(drag_current_tile), tile_set_atlas_source->get_tile_animation_separation(drag_current_tile), tile_set_atlas_source->get_tile_animation_frames_count(drag_current_tile), drag_current_tile)) { + tile_set_atlas_source->move_tile_in_atlas(drag_current_tile, coords); + selection.clear(); + selection.insert({ coords, 0 }); + drag_current_tile = coords; + + // Update only what's needed. + tile_set_changed_needs_update = false; + _update_tile_inspector(); + _update_atlas_view(); + _update_tile_id_label(); + _update_current_tile_data_editor(); + } + } else if (drag_type == DRAG_TYPE_MAY_POPUP_MENU) { + if (Vector2(drag_start_mouse_pos).distance_to(tile_atlas_control->get_local_mouse_position()) > 5.0 * EDSCALE) { + drag_type = DRAG_TYPE_NONE; + } + } else if (drag_type >= DRAG_TYPE_RESIZE_TOP_LEFT && drag_type <= DRAG_TYPE_RESIZE_LEFT) { + // Resizing a tile. + new_base_tiles_coords = new_base_tiles_coords.max(Vector2i(-1, -1)).min(grid_size); + + Rect2i old_rect = Rect2i(drag_current_tile, tile_set_atlas_source->get_tile_size_in_atlas(drag_current_tile)); + Rect2i new_rect = old_rect; + + if (drag_type == DRAG_TYPE_RESIZE_LEFT || drag_type == DRAG_TYPE_RESIZE_TOP_LEFT || drag_type == DRAG_TYPE_RESIZE_BOTTOM_LEFT) { + new_rect.position.x = MIN(new_base_tiles_coords.x + 1, old_rect.get_end().x - 1); + new_rect.size.x = old_rect.get_end().x - new_rect.position.x; + } + if (drag_type == DRAG_TYPE_RESIZE_TOP || drag_type == DRAG_TYPE_RESIZE_TOP_LEFT || drag_type == DRAG_TYPE_RESIZE_TOP_RIGHT) { + new_rect.position.y = MIN(new_base_tiles_coords.y + 1, old_rect.get_end().y - 1); + new_rect.size.y = old_rect.get_end().y - new_rect.position.y; + } + + if (drag_type == DRAG_TYPE_RESIZE_RIGHT || drag_type == DRAG_TYPE_RESIZE_TOP_RIGHT || drag_type == DRAG_TYPE_RESIZE_BOTTOM_RIGHT) { + new_rect.set_end(Vector2i(MAX(new_base_tiles_coords.x, old_rect.position.x + 1), new_rect.get_end().y)); + } + if (drag_type == DRAG_TYPE_RESIZE_BOTTOM || drag_type == DRAG_TYPE_RESIZE_BOTTOM_LEFT || drag_type == DRAG_TYPE_RESIZE_BOTTOM_RIGHT) { + new_rect.set_end(Vector2i(new_rect.get_end().x, MAX(new_base_tiles_coords.y, old_rect.position.y + 1))); + } + + if (tile_set_atlas_source->has_room_for_tile(new_rect.position, new_rect.size, tile_set_atlas_source->get_tile_animation_columns(drag_current_tile), tile_set_atlas_source->get_tile_animation_separation(drag_current_tile), tile_set_atlas_source->get_tile_animation_frames_count(drag_current_tile), drag_current_tile)) { + tile_set_atlas_source->move_tile_in_atlas(drag_current_tile, new_rect.position, new_rect.size); + selection.clear(); + selection.insert({ new_rect.position, 0 }); + drag_current_tile = new_rect.position; + + // Update only what's needed. + tile_set_changed_needs_update = false; + _update_tile_inspector(); + _update_atlas_view(); + _update_tile_id_label(); + _update_current_tile_data_editor(); + } + } + + // Redraw for the hovered tile. + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + tile_atlas_view->update(); + return; + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + Vector2 mouse_local_pos = tile_atlas_control->get_local_mouse_position(); + if (mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { + // Left click pressed. + if (tools_button_group->get_pressed_button() == tool_setup_atlas_source_button) { + if (tools_settings_erase_button->is_pressed()) { + // Erasing + if (mb->is_ctrl_pressed() || mb->is_shift_pressed()) { + // Remove tiles using rect. + + // Setup the dragging info. + drag_type = DRAG_TYPE_REMOVE_TILES_USING_RECT; + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + } else { + // Remove tiles. + + // Setup the dragging info. + drag_type = DRAG_TYPE_REMOVE_TILES; + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + + // Remove a first tile. + Vector2i coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + coords = tile_set_atlas_source->get_tile_at_coords(coords); + } + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + drag_modified_tiles.insert(coords); + } + } + } else { + // Creating + if (mb->is_shift_pressed()) { + // Create a big tile. + Vector2i coords = tile_atlas_view->get_atlas_tile_coords_at_pos(mouse_local_pos); + if (coords != TileSetSource::INVALID_ATLAS_COORDS && tile_set_atlas_source->get_tile_at_coords(coords) == TileSetSource::INVALID_ATLAS_COORDS) { + // Setup the dragging info, only if we start on an empty tile. + drag_type = DRAG_TYPE_CREATE_BIG_TILE; + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + drag_current_tile = coords; + + // Create a tile. + tile_set_atlas_source->create_tile(coords); + } + } else if (mb->is_ctrl_pressed()) { + // Create tiles using rect. + drag_type = DRAG_TYPE_CREATE_TILES_USING_RECT; + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + } else { + // Create tiles. + + // Setup the dragging info. + drag_type = DRAG_TYPE_CREATE_TILES; + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + + // Create a first tile if needed. + Vector2i coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + if (coords != TileSetSource::INVALID_ATLAS_COORDS && tile_set_atlas_source->get_tile_at_coords(coords) == TileSetSource::INVALID_ATLAS_COORDS) { + tile_set_atlas_source->create_tile(coords); + drag_modified_tiles.insert(coords); + } + } + } + } else if (tools_button_group->get_pressed_button() == tool_select_button) { + // Dragging a handle. + drag_type = DRAG_TYPE_NONE; + if (selection.size() == 1) { + TileSelection selected = selection.front()->get(); + if (selected.tile != TileSetSource::INVALID_ATLAS_COORDS && selected.alternative == 0) { + Vector2i size_in_atlas = tile_set_atlas_source->get_tile_size_in_atlas(selected.tile); + Rect2 region = tile_set_atlas_source->get_tile_texture_region(selected.tile); + Size2 zoomed_size = resize_handle->get_size() / tile_atlas_view->get_zoom(); + Rect2 rect = region.grow_individual(zoomed_size.x, zoomed_size.y, 0, 0); + const Vector2i coords[] = { Vector2i(0, 0), Vector2i(1, 0), Vector2i(1, 1), Vector2i(0, 1) }; + const Vector2i directions[] = { Vector2i(0, -1), Vector2i(1, 0), Vector2i(0, 1), Vector2i(-1, 0) }; + CursorShape cursor_shape = CURSOR_ARROW; + bool can_grow[4]; + for (int i = 0; i < 4; i++) { + can_grow[i] = tile_set_atlas_source->has_room_for_tile(selected.tile + directions[i], tile_set_atlas_source->get_tile_size_in_atlas(selected.tile), tile_set_atlas_source->get_tile_animation_columns(selected.tile), tile_set_atlas_source->get_tile_animation_separation(selected.tile), tile_set_atlas_source->get_tile_animation_frames_count(selected.tile), selected.tile); + can_grow[i] |= (i % 2 == 0) ? size_in_atlas.y > 1 : size_in_atlas.x > 1; + } + for (int i = 0; i < 4; i++) { + Vector2 pos = rect.position + rect.size * coords[i]; + if (can_grow[i] && can_grow[(i + 3) % 4] && Rect2(pos, zoomed_size).has_point(mouse_local_pos)) { + drag_type = (DragType)((int)DRAG_TYPE_RESIZE_TOP_LEFT + i * 2); + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + drag_current_tile = selected.tile; + drag_start_tile_shape = Rect2i(selected.tile, tile_set_atlas_source->get_tile_size_in_atlas(selected.tile)); + cursor_shape = (i % 2) ? CURSOR_BDIAGSIZE : CURSOR_FDIAGSIZE; + } + Vector2 next_pos = rect.position + rect.size * coords[(i + 1) % 4]; + if (can_grow[i] && Rect2((pos + next_pos) / 2.0, zoomed_size).has_point(mouse_local_pos)) { + drag_type = (DragType)((int)DRAG_TYPE_RESIZE_TOP + i * 2); + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + drag_current_tile = selected.tile; + drag_start_tile_shape = Rect2i(selected.tile, tile_set_atlas_source->get_tile_size_in_atlas(selected.tile)); + cursor_shape = (i % 2) ? CURSOR_HSIZE : CURSOR_VSIZE; + } + } + tile_atlas_control->set_default_cursor_shape(cursor_shape); + } + } + + // Selecting then dragging a tile. + if (drag_type == DRAG_TYPE_NONE) { + TileSelection selected = { TileSetSource::INVALID_ATLAS_COORDS, TileSetSource::INVALID_TILE_ALTERNATIVE }; + Vector2i coords = tile_atlas_view->get_atlas_tile_coords_at_pos(mouse_local_pos); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + coords = tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + selected = { coords, 0 }; + } + } + + bool shift = mb->is_shift_pressed(); + if (!shift && selection.size() == 1 && selected.tile != TileSetSource::INVALID_ATLAS_COORDS && selection.has(selected)) { + // Start move dragging. + drag_type = DRAG_TYPE_MOVE_TILE; + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + drag_current_tile = selected.tile; + drag_start_tile_shape = Rect2i(selected.tile, tile_set_atlas_source->get_tile_size_in_atlas(selected.tile)); + tile_atlas_control->set_default_cursor_shape(CURSOR_MOVE); + } else { + // Start selection dragging. + drag_type = DRAG_TYPE_RECT_SELECT; + drag_start_mouse_pos = mouse_local_pos; + drag_last_mouse_pos = drag_start_mouse_pos; + } + } + } + } else { + // Left click released. + _end_dragging(); + } + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + tile_atlas_view->update(); + return; + } else if (mb->get_button_index() == MouseButton::RIGHT) { + // Right click pressed. + if (mb->is_pressed()) { + drag_type = DRAG_TYPE_MAY_POPUP_MENU; + drag_start_mouse_pos = tile_atlas_control->get_local_mouse_position(); + } else { + // Right click released. + _end_dragging(); + } + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + tile_atlas_view->update(); + return; + } + } + } +} + +void TileSetAtlasSourceEditor::_end_dragging() { + switch (drag_type) { + case DRAG_TYPE_CREATE_TILES: + undo_redo->create_action(TTR("Create tiles")); + for (Set<Vector2i>::Element *E = drag_modified_tiles.front(); E; E = E->next()) { + undo_redo->add_do_method(tile_set_atlas_source, "create_tile", E->get()); + undo_redo->add_undo_method(tile_set_atlas_source, "remove_tile", E->get()); + } + undo_redo->commit_action(false); + break; + case DRAG_TYPE_CREATE_BIG_TILE: + undo_redo->create_action(TTR("Create a tile")); + undo_redo->add_do_method(tile_set_atlas_source, "create_tile", drag_current_tile, tile_set_atlas_source->get_tile_size_in_atlas(drag_current_tile)); + undo_redo->add_undo_method(tile_set_atlas_source, "remove_tile", drag_current_tile); + undo_redo->commit_action(false); + break; + case DRAG_TYPE_REMOVE_TILES: { + List<PropertyInfo> list; + tile_set_atlas_source->get_property_list(&list); + Map<Vector2i, List<const PropertyInfo *>> per_tile = _group_properties_per_tiles(list, tile_set_atlas_source); + undo_redo->create_action(TTR("Remove tiles")); + for (Set<Vector2i>::Element *E = drag_modified_tiles.front(); E; E = E->next()) { + Vector2i coords = E->get(); + undo_redo->add_do_method(tile_set_atlas_source, "remove_tile", coords); + undo_redo->add_undo_method(tile_set_atlas_source, "create_tile", coords); + if (per_tile.has(coords)) { + for (List<const PropertyInfo *>::Element *E_property = per_tile[coords].front(); E_property; E_property = E_property->next()) { + String property = E_property->get()->name; + Variant value = tile_set_atlas_source->get(property); + if (value.get_type() != Variant::NIL) { + undo_redo->add_undo_method(tile_set_atlas_source, "set", E_property->get()->name, value); + } + } + } + } + undo_redo->commit_action(); + } break; + case DRAG_TYPE_CREATE_TILES_USING_RECT: { + Vector2i start_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + Rect2i area = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); + area.set_end((area.get_end() + Vector2i(1, 1)).min(tile_set_atlas_source->get_atlas_grid_size())); + undo_redo->create_action(TTR("Create tiles")); + for (int x = area.get_position().x; x < area.get_end().x; x++) { + for (int y = area.get_position().y; y < area.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + if (tile_set_atlas_source->get_tile_at_coords(coords) == TileSetSource::INVALID_ATLAS_COORDS) { + undo_redo->add_do_method(tile_set_atlas_source, "create_tile", coords); + undo_redo->add_undo_method(tile_set_atlas_source, "remove_tile", coords); + } + } + } + undo_redo->commit_action(); + } break; + case DRAG_TYPE_REMOVE_TILES_USING_RECT: { + Vector2i start_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + Rect2i area = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); + area.set_end((area.get_end() + Vector2i(1, 1)).min(tile_set_atlas_source->get_atlas_grid_size())); + List<PropertyInfo> list; + tile_set_atlas_source->get_property_list(&list); + Map<Vector2i, List<const PropertyInfo *>> per_tile = _group_properties_per_tiles(list, tile_set_atlas_source); + + Set<Vector2i> to_delete; + for (int x = area.get_position().x; x < area.get_end().x; x++) { + for (int y = area.get_position().y; y < area.get_end().y; y++) { + Vector2i coords = tile_set_atlas_source->get_tile_at_coords(Vector2i(x, y)); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + to_delete.insert(coords); + } + } + } + + undo_redo->create_action(TTR("Remove tiles")); + undo_redo->add_do_method(this, "_set_selection_from_array", Array()); + for (Set<Vector2i>::Element *E = to_delete.front(); E; E = E->next()) { + Vector2i coords = E->get(); + undo_redo->add_do_method(tile_set_atlas_source, "remove_tile", coords); + undo_redo->add_undo_method(tile_set_atlas_source, "create_tile", coords); + if (per_tile.has(coords)) { + for (List<const PropertyInfo *>::Element *E_property = per_tile[coords].front(); E_property; E_property = E_property->next()) { + String property = E_property->get()->name; + Variant value = tile_set_atlas_source->get(property); + if (value.get_type() != Variant::NIL) { + undo_redo->add_undo_method(tile_set_atlas_source, "set", E_property->get()->name, value); + } + } + } + } + undo_redo->add_undo_method(this, "_set_selection_from_array", _get_selection_as_array()); + undo_redo->commit_action(); + } break; + case DRAG_TYPE_MOVE_TILE: + if (drag_current_tile != drag_start_tile_shape.position) { + undo_redo->create_action(TTR("Move a tile")); + undo_redo->add_do_method(tile_set_atlas_source, "move_tile_in_atlas", drag_start_tile_shape.position, drag_current_tile, tile_set_atlas_source->get_tile_size_in_atlas(drag_current_tile)); + undo_redo->add_do_method(this, "_set_selection_from_array", _get_selection_as_array()); + undo_redo->add_undo_method(tile_set_atlas_source, "move_tile_in_atlas", drag_current_tile, drag_start_tile_shape.position, drag_start_tile_shape.size); + Array array; + array.push_back(drag_start_tile_shape.position); + array.push_back(0); + undo_redo->add_undo_method(this, "_set_selection_from_array", array); + undo_redo->commit_action(false); + } + break; + case DRAG_TYPE_RECT_SELECT: { + Vector2i start_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + ERR_FAIL_COND(start_base_tiles_coords == TileSetSource::INVALID_ATLAS_COORDS); + ERR_FAIL_COND(new_base_tiles_coords == TileSetSource::INVALID_ATLAS_COORDS); + + Rect2i region = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); + region.size += Vector2i(1, 1); + + undo_redo->create_action(TTR("Select tiles")); + undo_redo->add_undo_method(this, "_set_selection_from_array", _get_selection_as_array()); + + // Determine if we clear, then add or remove to the selection. + bool add_to_selection = true; + if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) { + Vector2i coords = tile_set_atlas_source->get_tile_at_coords(start_base_tiles_coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + if (selection.has({ coords, 0 })) { + add_to_selection = false; + } + } + } else { + selection.clear(); + } + + // Modify the selection. + for (int x = region.position.x; x < region.get_end().x; x++) { + for (int y = region.position.y; y < region.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + coords = tile_set_atlas_source->get_tile_at_coords(coords); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + if (add_to_selection && !selection.has({ coords, 0 })) { + selection.insert({ coords, 0 }); + } else if (!add_to_selection && selection.has({ coords, 0 })) { + selection.erase({ coords, 0 }); + } + } + } + } + _update_tile_inspector(); + _update_tile_id_label(); + _update_current_tile_data_editor(); + undo_redo->add_do_method(this, "_set_selection_from_array", _get_selection_as_array()); + undo_redo->commit_action(false); + } break; + case DRAG_TYPE_MAY_POPUP_MENU: { + Vector2 mouse_local_pos = tile_atlas_control->get_local_mouse_position(); + TileSelection selected = { tile_atlas_view->get_atlas_tile_coords_at_pos(mouse_local_pos), 0 }; + if (selected.tile != TileSetSource::INVALID_ATLAS_COORDS) { + selected.tile = tile_set_atlas_source->get_tile_at_coords(selected.tile); + } + + // Set the selection if needed. + if (selection.size() <= 1) { + if (selected.tile != TileSetSource::INVALID_ATLAS_COORDS) { + undo_redo->create_action(TTR("Select tiles")); + undo_redo->add_undo_method(this, "_set_selection_from_array", _get_selection_as_array()); + selection.clear(); + selection.insert(selected); + undo_redo->add_do_method(this, "_set_selection_from_array", _get_selection_as_array()); + undo_redo->commit_action(false); + _update_tile_inspector(); + _update_tile_id_label(); + _update_current_tile_data_editor(); + } + } + + // Pops up the correct menu, depending on whether we have a tile or not. + if (selected.tile != TileSetSource::INVALID_ATLAS_COORDS && selection.has(selected)) { + // We have a tile. + menu_option_coords = selected.tile; + menu_option_alternative = 0; + base_tile_popup_menu->popup(Rect2i(get_global_mouse_position(), Size2i())); + } else if (hovered_base_tile_coords != TileSetSource::INVALID_ATLAS_COORDS) { + // We don't have a tile, but can create one. + menu_option_coords = hovered_base_tile_coords; + menu_option_alternative = TileSetSource::INVALID_TILE_ALTERNATIVE; + empty_base_tile_popup_menu->popup(Rect2i(get_global_mouse_position(), Size2i())); + } + } break; + case DRAG_TYPE_RESIZE_TOP_LEFT: + case DRAG_TYPE_RESIZE_TOP: + case DRAG_TYPE_RESIZE_TOP_RIGHT: + case DRAG_TYPE_RESIZE_RIGHT: + case DRAG_TYPE_RESIZE_BOTTOM_RIGHT: + case DRAG_TYPE_RESIZE_BOTTOM: + case DRAG_TYPE_RESIZE_BOTTOM_LEFT: + case DRAG_TYPE_RESIZE_LEFT: + if (drag_start_tile_shape != Rect2i(drag_current_tile, tile_set_atlas_source->get_tile_size_in_atlas(drag_current_tile))) { + undo_redo->create_action(TTR("Resize a tile")); + undo_redo->add_do_method(tile_set_atlas_source, "move_tile_in_atlas", drag_start_tile_shape.position, drag_current_tile, tile_set_atlas_source->get_tile_size_in_atlas(drag_current_tile)); + undo_redo->add_do_method(this, "_set_selection_from_array", _get_selection_as_array()); + undo_redo->add_undo_method(tile_set_atlas_source, "move_tile_in_atlas", drag_current_tile, drag_start_tile_shape.position, drag_start_tile_shape.size); + Array array; + array.push_back(drag_start_tile_shape.position); + array.push_back(0); + undo_redo->add_undo_method(this, "_set_selection_from_array", array); + undo_redo->commit_action(false); + } + break; + default: + break; + } + + drag_modified_tiles.clear(); + drag_type = DRAG_TYPE_NONE; + tile_atlas_control->set_default_cursor_shape(CURSOR_ARROW); +} + +Map<Vector2i, List<const PropertyInfo *>> TileSetAtlasSourceEditor::_group_properties_per_tiles(const List<PropertyInfo> &r_list, const TileSetAtlasSource *p_atlas) { + // Group properties per tile. + Map<Vector2i, List<const PropertyInfo *>> per_tile; + for (const List<PropertyInfo>::Element *E_property = r_list.front(); E_property; E_property = E_property->next()) { + Vector<String> components = String(E_property->get().name).split("/", true, 1); + if (components.size() >= 1) { + Vector<String> coord_arr = components[0].split(":"); + if (coord_arr.size() == 2 && coord_arr[0].is_valid_int() && coord_arr[1].is_valid_int()) { + Vector2i coords = Vector2i(coord_arr[0].to_int(), coord_arr[1].to_int()); + per_tile[coords].push_back(&(E_property->get())); + } + } + } + return per_tile; +} + +void TileSetAtlasSourceEditor::_menu_option(int p_option) { + switch (p_option) { + case TILE_DELETE: { + List<PropertyInfo> list; + tile_set_atlas_source->get_property_list(&list); + Map<Vector2i, List<const PropertyInfo *>> per_tile = _group_properties_per_tiles(list, tile_set_atlas_source); + undo_redo->create_action(TTR("Remove tile")); + + // Remove tiles + Set<Vector2i> removed; + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + TileSelection selected = E->get(); + if (selected.alternative == 0) { + // Remove a tile. + undo_redo->add_do_method(tile_set_atlas_source, "remove_tile", selected.tile); + undo_redo->add_undo_method(tile_set_atlas_source, "create_tile", selected.tile); + removed.insert(selected.tile); + if (per_tile.has(selected.tile)) { + for (List<const PropertyInfo *>::Element *E_property = per_tile[selected.tile].front(); E_property; E_property = E_property->next()) { + String property = E_property->get()->name; + Variant value = tile_set_atlas_source->get(property); + if (value.get_type() != Variant::NIL) { + undo_redo->add_undo_method(tile_set_atlas_source, "set", E_property->get()->name, value); + } + } + } + } + } + + // Remove alternatives + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + TileSelection selected = E->get(); + if (selected.alternative > 0 && !removed.has(selected.tile)) { + // Remove an alternative tile. + undo_redo->add_do_method(tile_set_atlas_source, "remove_alternative_tile", selected.tile, selected.alternative); + undo_redo->add_undo_method(tile_set_atlas_source, "create_alternative_tile", selected.tile, selected.alternative); + if (per_tile.has(selected.tile)) { + for (List<const PropertyInfo *>::Element *E_property = per_tile[selected.tile].front(); E_property; E_property = E_property->next()) { + Vector<String> components = E_property->get()->name.split("/", true, 2); + if (components.size() >= 2 && components[1].is_valid_int() && components[1].to_int() == selected.alternative) { + String property = E_property->get()->name; + Variant value = tile_set_atlas_source->get(property); + if (value.get_type() != Variant::NIL) { + undo_redo->add_undo_method(tile_set_atlas_source, "set", E_property->get()->name, value); + } + } + } + } + } + } + undo_redo->commit_action(); + _update_fix_selected_and_hovered_tiles(); + _update_tile_id_label(); + } break; + case TILE_CREATE: { + undo_redo->create_action(TTR("Create a tile")); + undo_redo->add_do_method(tile_set_atlas_source, "create_tile", menu_option_coords); + Array array; + array.push_back(menu_option_coords); + array.push_back(0); + undo_redo->add_do_method(this, "_set_selection_from_array", array); + undo_redo->add_undo_method(tile_set_atlas_source, "remove_tile", menu_option_coords); + undo_redo->add_undo_method(this, "_set_selection_from_array", _get_selection_as_array()); + undo_redo->commit_action(); + _update_tile_id_label(); + } break; + case TILE_CREATE_ALTERNATIVE: { + undo_redo->create_action(TTR("Create tile alternatives")); + Array array; + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + if (E->get().alternative == 0) { + int next_id = tile_set_atlas_source->get_next_alternative_tile_id(E->get().tile); + undo_redo->add_do_method(tile_set_atlas_source, "create_alternative_tile", E->get().tile, next_id); + array.push_back(E->get().tile); + array.push_back(next_id); + undo_redo->add_undo_method(tile_set_atlas_source, "remove_alternative_tile", E->get().tile, next_id); + } + } + undo_redo->add_do_method(this, "_set_selection_from_array", array); + undo_redo->add_undo_method(this, "_set_selection_from_array", _get_selection_as_array()); + undo_redo->commit_action(); + _update_tile_id_label(); + } break; + case ADVANCED_AUTO_CREATE_TILES: { + _auto_create_tiles(); + } break; + case ADVANCED_AUTO_REMOVE_TILES: { + _auto_remove_tiles(); + } break; + } +} + +void TileSetAtlasSourceEditor::_unhandled_key_input(const Ref<InputEvent> &p_event) { + // Check for shortcuts. + if (ED_IS_SHORTCUT("tiles_editor/delete_tile", p_event)) { + if (tools_button_group->get_pressed_button() == tool_select_button && !selection.is_empty()) { + _menu_option(TILE_DELETE); + accept_event(); + } + } +} + +void TileSetAtlasSourceEditor::_set_selection_from_array(Array p_selection) { + ERR_FAIL_COND((p_selection.size() % 2) != 0); + selection.clear(); + for (int i = 0; i < p_selection.size() / 2; i++) { + TileSelection selected = { p_selection[i * 2], p_selection[i * 2 + 1] }; + if (tile_set_atlas_source->has_tile(selected.tile) && tile_set_atlas_source->has_alternative_tile(selected.tile, selected.alternative)) { + selection.insert(selected); + } + } + _update_tile_inspector(); + _update_tile_id_label(); + _update_atlas_view(); + _update_current_tile_data_editor(); +} + +Array TileSetAtlasSourceEditor::_get_selection_as_array() { + Array output; + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + output.push_back(E->get().tile); + output.push_back(E->get().alternative); + } + return output; +} + +void TileSetAtlasSourceEditor::_tile_atlas_control_draw() { + // Colors. + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + + // Draw the selected tile. + if (tools_button_group->get_pressed_button() == tool_select_button) { + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + TileSelection selected = E->get(); + if (selected.alternative == 0) { + // Draw the rect. + for (int frame = 0; frame < tile_set_atlas_source->get_tile_animation_frames_count(selected.tile); frame++) { + Color color = selection_color; + if (frame > 0) { + color.a *= 0.3; + } + Rect2 region = tile_set_atlas_source->get_tile_texture_region(selected.tile, frame); + tile_atlas_control->draw_rect(region, color, false); + } + } + } + + if (selection.size() == 1) { + // Draw the resize handles (only when it's possible to expand). + TileSelection selected = selection.front()->get(); + if (selected.alternative == 0) { + Vector2i size_in_atlas = tile_set_atlas_source->get_tile_size_in_atlas(selected.tile); + Size2 zoomed_size = resize_handle->get_size() / tile_atlas_view->get_zoom(); + Rect2 region = tile_set_atlas_source->get_tile_texture_region(selected.tile); + Rect2 rect = region.grow_individual(zoomed_size.x, zoomed_size.y, 0, 0); + Vector2i coords[] = { Vector2i(0, 0), Vector2i(1, 0), Vector2i(1, 1), Vector2i(0, 1) }; + Vector2i directions[] = { Vector2i(0, -1), Vector2i(1, 0), Vector2i(0, 1), Vector2i(-1, 0) }; + bool can_grow[4]; + for (int i = 0; i < 4; i++) { + can_grow[i] = tile_set_atlas_source->has_room_for_tile(selected.tile + directions[i], tile_set_atlas_source->get_tile_size_in_atlas(selected.tile), tile_set_atlas_source->get_tile_animation_columns(selected.tile), tile_set_atlas_source->get_tile_animation_separation(selected.tile), tile_set_atlas_source->get_tile_animation_frames_count(selected.tile), selected.tile); + can_grow[i] |= (i % 2 == 0) ? size_in_atlas.y > 1 : size_in_atlas.x > 1; + } + for (int i = 0; i < 4; i++) { + Vector2 pos = rect.position + rect.size * coords[i]; + if (can_grow[i] && can_grow[(i + 3) % 4]) { + tile_atlas_control->draw_texture_rect(resize_handle, Rect2(pos, zoomed_size), false); + } else { + tile_atlas_control->draw_texture_rect(resize_handle_disabled, Rect2(pos, zoomed_size), false); + } + Vector2 next_pos = rect.position + rect.size * coords[(i + 1) % 4]; + if (can_grow[i]) { + tile_atlas_control->draw_texture_rect(resize_handle, Rect2((pos + next_pos) / 2.0, zoomed_size), false); + } else { + tile_atlas_control->draw_texture_rect(resize_handle_disabled, Rect2((pos + next_pos) / 2.0, zoomed_size), false); + } + } + } + } + } + + if (drag_type == DRAG_TYPE_REMOVE_TILES) { + // Draw the tiles to be removed. + for (Set<Vector2i>::Element *E = drag_modified_tiles.front(); E; E = E->next()) { + for (int frame = 0; frame < tile_set_atlas_source->get_tile_animation_frames_count(E->get()); frame++) { + tile_atlas_control->draw_rect(tile_set_atlas_source->get_tile_texture_region(E->get(), frame), Color(0.0, 0.0, 0.0), false); + } + } + } else if (drag_type == DRAG_TYPE_RECT_SELECT || drag_type == DRAG_TYPE_REMOVE_TILES_USING_RECT) { + // Draw tiles to be removed. + Vector2i start_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + Rect2i area = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); + area.set_end((area.get_end() + Vector2i(1, 1)).min(tile_set_atlas_source->get_atlas_grid_size())); + + Color color = Color(0.0, 0.0, 0.0); + if (drag_type == DRAG_TYPE_RECT_SELECT) { + color = selection_color.lightened(0.2); + } + + Set<Vector2i> to_paint; + for (int x = area.get_position().x; x < area.get_end().x; x++) { + for (int y = area.get_position().y; y < area.get_end().y; y++) { + Vector2i coords = tile_set_atlas_source->get_tile_at_coords(Vector2i(x, y)); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + to_paint.insert(coords); + } + } + } + + for (Set<Vector2i>::Element *E = to_paint.front(); E; E = E->next()) { + Vector2i coords = E->get(); + tile_atlas_control->draw_rect(tile_set_atlas_source->get_tile_texture_region(coords), color, false); + } + } else if (drag_type == DRAG_TYPE_CREATE_TILES_USING_RECT) { + // Draw tiles to be created. + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i tile_size = tile_set_atlas_source->get_texture_region_size(); + + Vector2i start_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + Rect2i area = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); + area.set_end((area.get_end() + Vector2i(1, 1)).min(tile_set_atlas_source->get_atlas_grid_size())); + for (int x = area.get_position().x; x < area.get_end().x; x++) { + for (int y = area.get_position().y; y < area.get_end().y; y++) { + Vector2i coords = Vector2i(x, y); + if (tile_set_atlas_source->get_tile_at_coords(coords) == TileSetSource::INVALID_ATLAS_COORDS) { + Vector2i origin = margins + (coords * (tile_size + separation)); + tile_atlas_control->draw_rect(Rect2i(origin, tile_size), Color(1.0, 1.0, 1.0), false); + } + } + } + } + + // Draw the hovered tile. + if (drag_type == DRAG_TYPE_REMOVE_TILES_USING_RECT || drag_type == DRAG_TYPE_CREATE_TILES_USING_RECT) { + // Draw the rect. + Vector2i start_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(drag_start_mouse_pos); + Vector2i new_base_tiles_coords = tile_atlas_view->get_atlas_tile_coords_at_pos(tile_atlas_control->get_local_mouse_position()); + Rect2i area = Rect2i(start_base_tiles_coords, new_base_tiles_coords - start_base_tiles_coords).abs(); + area.set_end((area.get_end() + Vector2i(1, 1)).min(tile_set_atlas_source->get_atlas_grid_size())); + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i tile_size = tile_set_atlas_source->get_texture_region_size(); + Vector2i origin = margins + (area.position * (tile_size + separation)); + tile_atlas_control->draw_rect(Rect2i(origin, area.size * tile_size), Color(1.0, 1.0, 1.0), false); + } else { + Vector2i grid_size = tile_set_atlas_source->get_atlas_grid_size(); + if (hovered_base_tile_coords.x >= 0 && hovered_base_tile_coords.y >= 0 && hovered_base_tile_coords.x < grid_size.x && hovered_base_tile_coords.y < grid_size.y) { + Vector2i hovered_tile = tile_set_atlas_source->get_tile_at_coords(hovered_base_tile_coords); + if (hovered_tile != TileSetSource::INVALID_ATLAS_COORDS) { + // Draw existing hovered tile. + for (int frame = 0; frame < tile_set_atlas_source->get_tile_animation_frames_count(hovered_tile); frame++) { + Color color = Color(1.0, 1.0, 1.0); + if (frame > 0) { + color.a *= 0.3; + } + tile_atlas_control->draw_rect(tile_set_atlas_source->get_tile_texture_region(hovered_tile, frame), color, false); + } + } else { + // Draw empty tile, only in add/remove tiles mode. + if (tools_button_group->get_pressed_button() == tool_setup_atlas_source_button) { + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i tile_size = tile_set_atlas_source->get_texture_region_size(); + Vector2i origin = margins + (hovered_base_tile_coords * (tile_size + separation)); + tile_atlas_control->draw_rect(Rect2i(origin, tile_size), Color(1.0, 1.0, 1.0), false); + } + } + } + } +} + +void TileSetAtlasSourceEditor::_tile_atlas_control_unscaled_draw() { + if (current_tile_data_editor) { + // Draw the preview of the selected property. + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i coords = tile_set_atlas_source->get_tile_id(i); + Rect2i texture_region = tile_set_atlas_source->get_tile_texture_region(coords); + Vector2i position = texture_region.get_center() + tile_set_atlas_source->get_tile_effective_texture_offset(coords, 0); + + Transform2D xform = tile_atlas_control->get_parent_control()->get_transform(); + xform.translate(position); + + if (tools_button_group->get_pressed_button() == tool_select_button && selection.has({ coords, 0 })) { + continue; + } + + TileMapCell cell; + cell.source_id = tile_set_atlas_source_id; + cell.set_atlas_coords(coords); + cell.alternative_tile = 0; + current_tile_data_editor->draw_over_tile(tile_atlas_control_unscaled, xform, cell); + } + + // Draw the selection on top of other. + if (tools_button_group->get_pressed_button() == tool_select_button) { + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + if (E->get().alternative != 0) { + continue; + } + Rect2i texture_region = tile_set_atlas_source->get_tile_texture_region(E->get().tile); + Vector2i position = texture_region.get_center() + tile_set_atlas_source->get_tile_effective_texture_offset(E->get().tile, 0); + + Transform2D xform = tile_atlas_control->get_parent_control()->get_transform(); + xform.translate(position); + + TileMapCell cell; + cell.source_id = tile_set_atlas_source_id; + cell.set_atlas_coords(E->get().tile); + cell.alternative_tile = 0; + current_tile_data_editor->draw_over_tile(tile_atlas_control_unscaled, xform, cell, true); + } + } + + // Call the TileData's editor custom draw function. + if (tools_button_group->get_pressed_button() == tool_paint_button) { + Transform2D xform = tile_atlas_control->get_parent_control()->get_transform(); + current_tile_data_editor->forward_draw_over_atlas(tile_atlas_view, tile_set_atlas_source, tile_atlas_control_unscaled, xform); + } + } +} + +void TileSetAtlasSourceEditor::_tile_alternatives_control_gui_input(const Ref<InputEvent> &p_event) { + // Update the hovered alternative tile. + hovered_alternative_tile_coords = tile_atlas_view->get_alternative_tile_at_pos(alternative_tiles_control->get_local_mouse_position()); + + // Forward the event to the current tile data editor if we are in the painting mode. + if (tools_button_group->get_pressed_button() == tool_paint_button) { + if (current_tile_data_editor) { + current_tile_data_editor->forward_painting_alternatives_gui_input(tile_atlas_view, tile_set_atlas_source, p_event); + } + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + tile_atlas_view->update(); + return; + } + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + } + + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + drag_type = DRAG_TYPE_NONE; + + Vector2 mouse_local_pos = alternative_tiles_control->get_local_mouse_position(); + if (mb->get_button_index() == MouseButton::LEFT) { + if (mb->is_pressed()) { + // Left click pressed. + if (tools_button_group->get_pressed_button() == tool_select_button) { + Vector3 tile = tile_atlas_view->get_alternative_tile_at_pos(mouse_local_pos); + + selection.clear(); + TileSelection selected = { Vector2i(tile.x, tile.y), int(tile.z) }; + if (selected.tile != TileSetSource::INVALID_ATLAS_COORDS) { + selection.insert(selected); + } + + _update_tile_inspector(); + _update_tile_id_label(); + } + } + } else if (mb->get_button_index() == MouseButton::RIGHT) { + if (mb->is_pressed()) { + // Right click pressed + Vector3 tile = tile_atlas_view->get_alternative_tile_at_pos(mouse_local_pos); + + selection.clear(); + TileSelection selected = { Vector2i(tile.x, tile.y), int(tile.z) }; + if (selected.tile != TileSetSource::INVALID_ATLAS_COORDS) { + selection.insert(selected); + } + + _update_tile_inspector(); + _update_tile_id_label(); + + if (selection.size() == 1) { + selected = selection.front()->get(); + menu_option_coords = selected.tile; + menu_option_alternative = selected.alternative; + alternative_tile_popup_menu->popup(Rect2i(get_global_mouse_position(), Size2i())); + } + } + } + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); + } +} + +void TileSetAtlasSourceEditor::_tile_alternatives_control_mouse_exited() { + hovered_alternative_tile_coords = Vector3i(TileSetSource::INVALID_ATLAS_COORDS.x, TileSetSource::INVALID_ATLAS_COORDS.y, TileSetSource::INVALID_TILE_ALTERNATIVE); + tile_atlas_control->update(); + tile_atlas_control_unscaled->update(); + alternative_tiles_control->update(); + alternative_tiles_control_unscaled->update(); +} + +void TileSetAtlasSourceEditor::_tile_alternatives_control_draw() { + Color grid_color = EditorSettings::get_singleton()->get("editors/tiles_editor/grid_color"); + Color selection_color = Color().from_hsv(Math::fposmod(grid_color.get_h() + 0.5, 1.0), grid_color.get_s(), grid_color.get_v(), 1.0); + + // Update the hovered alternative tile. + if (tools_button_group->get_pressed_button() == tool_select_button) { + // Draw hovered tile. + Vector2i coords = Vector2(hovered_alternative_tile_coords.x, hovered_alternative_tile_coords.y); + if (coords != TileSetSource::INVALID_ATLAS_COORDS) { + Rect2i rect = tile_atlas_view->get_alternative_tile_rect(coords, hovered_alternative_tile_coords.z); + if (rect != Rect2i()) { + alternative_tiles_control->draw_rect(rect, Color(1.0, 1.0, 1.0), false); + } + } + + // Draw selected tile. + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + TileSelection selected = E->get(); + if (selected.alternative >= 1) { + Rect2i rect = tile_atlas_view->get_alternative_tile_rect(selected.tile, selected.alternative); + if (rect != Rect2i()) { + alternative_tiles_control->draw_rect(rect, selection_color, false); + } + } + } + } +} + +void TileSetAtlasSourceEditor::_tile_alternatives_control_unscaled_draw() { + // Draw the preview of the selected property. + if (current_tile_data_editor) { + // Draw the preview of the currently selected property. + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i coords = tile_set_atlas_source->get_tile_id(i); + for (int j = 0; j < tile_set_atlas_source->get_alternative_tiles_count(coords); j++) { + int alternative_tile = tile_set_atlas_source->get_alternative_tile_id(coords, j); + if (alternative_tile == 0) { + continue; + } + Rect2i rect = tile_atlas_view->get_alternative_tile_rect(coords, alternative_tile); + Vector2 position = rect.get_center(); + + Transform2D xform = alternative_tiles_control->get_parent_control()->get_transform(); + xform.translate(position); + + if (tools_button_group->get_pressed_button() == tool_select_button && selection.has({ coords, alternative_tile })) { + continue; + } + + TileMapCell cell; + cell.source_id = tile_set_atlas_source_id; + cell.set_atlas_coords(coords); + cell.alternative_tile = alternative_tile; + current_tile_data_editor->draw_over_tile(alternative_tiles_control_unscaled, xform, cell); + } + } + + // Draw the selection on top of other. + if (tools_button_group->get_pressed_button() == tool_select_button) { + for (Set<TileSelection>::Element *E = selection.front(); E; E = E->next()) { + if (E->get().alternative == 0) { + continue; + } + Rect2i rect = tile_atlas_view->get_alternative_tile_rect(E->get().tile, E->get().alternative); + Vector2 position = rect.get_center(); + + Transform2D xform = alternative_tiles_control->get_parent_control()->get_transform(); + xform.translate(position); + + TileMapCell cell; + cell.source_id = tile_set_atlas_source_id; + cell.set_atlas_coords(E->get().tile); + cell.alternative_tile = E->get().alternative; + current_tile_data_editor->draw_over_tile(alternative_tiles_control_unscaled, xform, cell, true); + } + } + + // Call the TileData's editor custom draw function. + if (tools_button_group->get_pressed_button() == tool_paint_button) { + Transform2D xform = tile_atlas_control->get_parent_control()->get_transform(); + current_tile_data_editor->forward_draw_over_alternatives(tile_atlas_view, tile_set_atlas_source, alternative_tiles_control_unscaled, xform); + } + } +} + +void TileSetAtlasSourceEditor::_tile_set_changed() { + tile_set_changed_needs_update = true; +} + +void TileSetAtlasSourceEditor::_tile_proxy_object_changed(String p_what) { + tile_set_changed_needs_update = false; // Avoid updating too many things. + _update_atlas_view(); +} + +void TileSetAtlasSourceEditor::_atlas_source_proxy_object_changed(String p_what) { + if (p_what == "texture" && !atlas_source_proxy_object->get("texture").is_null()) { + confirm_auto_create_tiles->popup_centered(); + } else if (p_what == "id") { + emit_signal(SNAME("source_id_changed"), atlas_source_proxy_object->get_id()); + } +} + +void TileSetAtlasSourceEditor::_undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, String p_property, Variant p_new_value) { + UndoRedo *undo_redo = Object::cast_to<UndoRedo>(p_undo_redo); + ERR_FAIL_COND(!undo_redo); + +#define ADD_UNDO(obj, property) undo_redo->add_undo_property(obj, property, obj->get(property)); + + undo_redo->start_force_keep_in_merge_ends(); + AtlasTileProxyObject *tile_data_proxy = Object::cast_to<AtlasTileProxyObject>(p_edited); + if (tile_data_proxy) { + Vector<String> components = String(p_property).split("/", true, 2); + if (components.size() == 2 && components[1] == "polygons_count") { + int layer_index = components[0].trim_prefix("physics_layer_").to_int(); + int new_polygons_count = p_new_value; + int old_polygons_count = tile_data_proxy->get(vformat("physics_layer_%d/polygons_count", layer_index)); + if (new_polygons_count < old_polygons_count) { + for (int i = new_polygons_count; i < old_polygons_count; i++) { + ADD_UNDO(tile_data_proxy, vformat("physics_layer_%d/polygon_%d/points", layer_index, i)); + ADD_UNDO(tile_data_proxy, vformat("physics_layer_%d/polygon_%d/one_way", layer_index, i)); + ADD_UNDO(tile_data_proxy, vformat("physics_layer_%d/polygon_%d/one_way_margin", layer_index, i)); + } + } + } else if (p_property == "terrain_set") { + int current_terrain_set = tile_data_proxy->get("terrain_set"); + for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(i); + if (tile_set->is_valid_peering_bit_terrain(current_terrain_set, bit)) { + ADD_UNDO(tile_data_proxy, "terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[i])); + } + } + } + } + + TileSetAtlasSourceProxyObject *atlas_source_proxy = Object::cast_to<TileSetAtlasSourceProxyObject>(p_edited); + if (atlas_source_proxy) { + TileSetAtlasSource *atlas_source = atlas_source_proxy->get_edited(); + ERR_FAIL_COND(!atlas_source); + + PackedVector2Array arr; + if (p_property == "texture") { + arr = atlas_source->get_tiles_to_be_removed_on_change(p_new_value, atlas_source->get_margins(), atlas_source->get_separation(), atlas_source->get_texture_region_size()); + } else if (p_property == "margins") { + arr = atlas_source->get_tiles_to_be_removed_on_change(atlas_source->get_texture(), p_new_value, atlas_source->get_separation(), atlas_source->get_texture_region_size()); + } else if (p_property == "separation") { + arr = atlas_source->get_tiles_to_be_removed_on_change(atlas_source->get_texture(), atlas_source->get_margins(), p_new_value, atlas_source->get_texture_region_size()); + } else if (p_property == "texture_region_size") { + arr = atlas_source->get_tiles_to_be_removed_on_change(atlas_source->get_texture(), atlas_source->get_margins(), atlas_source->get_separation(), p_new_value); + } + + if (!arr.is_empty()) { + // Get all properties assigned to a tile. + List<PropertyInfo> properties; + atlas_source->get_property_list(&properties); + + for (int i = 0; i < arr.size(); i++) { + Vector2i coords = arr[i]; + String prefix = vformat("%d:%d/", coords.x, coords.y); + for (PropertyInfo pi : properties) { + if (pi.name.begins_with(prefix)) { + ADD_UNDO(atlas_source, pi.name); + } + } + } + } + } + undo_redo->end_force_keep_in_merge_ends(); + +#undef ADD_UNDO +} + +void TileSetAtlasSourceEditor::edit(Ref<TileSet> p_tile_set, TileSetAtlasSource *p_tile_set_atlas_source, int p_source_id) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + ERR_FAIL_COND(!p_tile_set_atlas_source); + ERR_FAIL_COND(p_source_id < 0); + ERR_FAIL_COND(p_tile_set->get_source(p_source_id) != p_tile_set_atlas_source); + + if (p_tile_set == tile_set && p_tile_set_atlas_source == tile_set_atlas_source && p_source_id == tile_set_atlas_source_id) { + return; + } + + // Remove listener for old objects. + if (tile_set.is_valid()) { + tile_set->disconnect("changed", callable_mp(this, &TileSetAtlasSourceEditor::_tile_set_changed)); + } + + // Clear the selection. + selection.clear(); + + // Change the edited object. + tile_set = p_tile_set; + tile_set_atlas_source = p_tile_set_atlas_source; + tile_set_atlas_source_id = p_source_id; + + // Add the listener again. + if (tile_set.is_valid()) { + tile_set->connect("changed", callable_mp(this, &TileSetAtlasSourceEditor::_tile_set_changed)); + } + + // Update everything. + _update_source_inspector(); + + // Update the selected tile. + _update_fix_selected_and_hovered_tiles(); + _update_tile_id_label(); + _update_atlas_view(); + _update_atlas_source_inspector(); + _update_tile_inspector(); + _update_tile_data_editors(); + _update_current_tile_data_editor(); +} + +void TileSetAtlasSourceEditor::init_source() { + confirm_auto_create_tiles->popup_centered(); +} + +void TileSetAtlasSourceEditor::_auto_create_tiles() { + if (!tile_set_atlas_source) { + return; + } + + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + if (texture.is_valid()) { + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i texture_region_size = tile_set_atlas_source->get_texture_region_size(); + Size2i grid_size = tile_set_atlas_source->get_atlas_grid_size(); + undo_redo->create_action(TTR("Create tiles in non-transparent texture regions")); + for (int y = 0; y < grid_size.y; y++) { + for (int x = 0; x < grid_size.x; x++) { + // Check if we have a tile at the coord + Vector2i coords = Vector2i(x, y); + if (tile_set_atlas_source->get_tile_at_coords(coords) == TileSetSource::INVALID_ATLAS_COORDS) { + // Check if the texture is empty at the given coords. + Rect2i region = Rect2i(margins + (coords * (texture_region_size + separation)), texture_region_size); + bool is_opaque = false; + for (int region_x = region.get_position().x; region_x < region.get_end().x; region_x++) { + for (int region_y = region.get_position().y; region_y < region.get_end().y; region_y++) { + if (texture->is_pixel_opaque(region_x, region_y)) { + is_opaque = true; + break; + } + } + if (is_opaque) { + break; + } + } + + // If we do have opaque pixels, create a tile. + if (is_opaque) { + undo_redo->add_do_method(tile_set_atlas_source, "create_tile", coords); + undo_redo->add_undo_method(tile_set_atlas_source, "remove_tile", coords); + } + } + } + } + undo_redo->commit_action(); + } +} + +void TileSetAtlasSourceEditor::_auto_remove_tiles() { + if (!tile_set_atlas_source) { + return; + } + + Ref<Texture2D> texture = tile_set_atlas_source->get_texture(); + if (texture.is_valid()) { + Vector2i margins = tile_set_atlas_source->get_margins(); + Vector2i separation = tile_set_atlas_source->get_separation(); + Vector2i texture_region_size = tile_set_atlas_source->get_texture_region_size(); + Vector2i grid_size = tile_set_atlas_source->get_atlas_grid_size(); + + undo_redo->create_action(TTR("Remove tiles in fully transparent texture regions")); + + List<PropertyInfo> list; + tile_set_atlas_source->get_property_list(&list); + Map<Vector2i, List<const PropertyInfo *>> per_tile = _group_properties_per_tiles(list, tile_set_atlas_source); + + for (int i = 0; i < tile_set_atlas_source->get_tiles_count(); i++) { + Vector2i coords = tile_set_atlas_source->get_tile_id(i); + Vector2i size_in_atlas = tile_set_atlas_source->get_tile_size_in_atlas(coords); + + // Skip tiles outside texture. + if ((coords.x + size_in_atlas.x) > grid_size.x || (coords.y + size_in_atlas.y) > grid_size.y) { + continue; + } + + // Check if the texture is empty at the given coords. + Rect2i region = Rect2i(margins + (coords * (texture_region_size + separation)), texture_region_size * size_in_atlas); + bool is_opaque = false; + for (int region_x = region.get_position().x; region_x < region.get_end().x; region_x++) { + for (int region_y = region.get_position().y; region_y < region.get_end().y; region_y++) { + if (texture->is_pixel_opaque(region_x, region_y)) { + is_opaque = true; + break; + } + } + if (is_opaque) { + break; + } + } + + // If we do have opaque pixels, create a tile. + if (!is_opaque) { + undo_redo->add_do_method(tile_set_atlas_source, "remove_tile", coords); + undo_redo->add_undo_method(tile_set_atlas_source, "create_tile", coords); + if (per_tile.has(coords)) { + for (List<const PropertyInfo *>::Element *E_property = per_tile[coords].front(); E_property; E_property = E_property->next()) { + String property = E_property->get()->name; + Variant value = tile_set_atlas_source->get(property); + if (value.get_type() != Variant::NIL) { + undo_redo->add_undo_method(tile_set_atlas_source, "set", E_property->get()->name, value); + } + } + } + } + } + undo_redo->commit_action(); + } +} + +void TileSetAtlasSourceEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: + tool_setup_atlas_source_button->set_icon(get_theme_icon(SNAME("Tools"), SNAME("EditorIcons"))); + tool_select_button->set_icon(get_theme_icon(SNAME("ToolSelect"), SNAME("EditorIcons"))); + tool_paint_button->set_icon(get_theme_icon(SNAME("CanvasItem"), SNAME("EditorIcons"))); + + tools_settings_erase_button->set_icon(get_theme_icon(SNAME("Eraser"), SNAME("EditorIcons"))); + + tool_advanced_menu_buttom->set_icon(get_theme_icon(SNAME("GuiTabMenuHl"), SNAME("EditorIcons"))); + + resize_handle = get_theme_icon(SNAME("EditorHandle"), SNAME("EditorIcons")); + resize_handle_disabled = get_theme_icon(SNAME("EditorHandleDisabled"), SNAME("EditorIcons")); + break; + case NOTIFICATION_INTERNAL_PROCESS: + if (tile_set_changed_needs_update) { + // Update everything. + _update_source_inspector(); + + // Update the selected tile. + _update_fix_selected_and_hovered_tiles(); + _update_tile_id_label(); + _update_atlas_view(); + _update_atlas_source_inspector(); + _update_tile_inspector(); + _update_tile_data_editors(); + _update_current_tile_data_editor(); + + tile_set_changed_needs_update = false; + } + break; + default: + break; + } +} + +void TileSetAtlasSourceEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_unhandled_key_input"), &TileSetAtlasSourceEditor::_unhandled_key_input); + ClassDB::bind_method(D_METHOD("_set_selection_from_array"), &TileSetAtlasSourceEditor::_set_selection_from_array); + + ADD_SIGNAL(MethodInfo("source_id_changed", PropertyInfo(Variant::INT, "source_id"))); +} + +TileSetAtlasSourceEditor::TileSetAtlasSourceEditor() { + set_process_unhandled_key_input(true); + set_process_internal(true); + + // -- Right side -- + HSplitContainer *split_container_right_side = memnew(HSplitContainer); + split_container_right_side->set_h_size_flags(SIZE_EXPAND_FILL); + add_child(split_container_right_side); + + // Middle panel. + ScrollContainer *middle_panel = memnew(ScrollContainer); + middle_panel->set_enable_h_scroll(false); + middle_panel->set_custom_minimum_size(Size2i(200, 0) * EDSCALE); + split_container_right_side->add_child(middle_panel); + + VBoxContainer *middle_vbox_container = memnew(VBoxContainer); + middle_vbox_container->set_h_size_flags(SIZE_EXPAND_FILL); + middle_panel->add_child(middle_vbox_container); + + // Tile inspector. + tile_inspector_label = memnew(Label); + tile_inspector_label->set_text(TTR("Tile Properties:")); + middle_vbox_container->add_child(tile_inspector_label); + + tile_proxy_object = memnew(AtlasTileProxyObject(this)); + tile_proxy_object->connect("changed", callable_mp(this, &TileSetAtlasSourceEditor::_tile_proxy_object_changed)); + + tile_inspector = memnew(EditorInspector); + tile_inspector->set_undo_redo(undo_redo); + tile_inspector->set_enable_v_scroll(false); + tile_inspector->edit(tile_proxy_object); + tile_inspector->set_use_folding(true); + tile_inspector->connect("property_selected", callable_mp(this, &TileSetAtlasSourceEditor::_inspector_property_selected)); + middle_vbox_container->add_child(tile_inspector); + + tile_inspector_no_tile_selected_label = memnew(Label); + tile_inspector_no_tile_selected_label->set_align(Label::ALIGN_CENTER); + tile_inspector_no_tile_selected_label->set_text(TTR("No tile selected.")); + middle_vbox_container->add_child(tile_inspector_no_tile_selected_label); + + // Property values palette. + tile_data_editors_popup = memnew(Popup); + + tile_data_editors_label = memnew(Label); + tile_data_editors_label->set_text(TTR("Paint Properties:")); + middle_vbox_container->add_child(tile_data_editors_label); + + tile_data_editor_dropdown_button = memnew(Button); + tile_data_editor_dropdown_button->connect("draw", callable_mp(this, &TileSetAtlasSourceEditor::_tile_data_editor_dropdown_button_draw)); + tile_data_editor_dropdown_button->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_tile_data_editor_dropdown_button_pressed)); + middle_vbox_container->add_child(tile_data_editor_dropdown_button); + tile_data_editor_dropdown_button->add_child(tile_data_editors_popup); + + tile_data_editors_tree = memnew(Tree); + tile_data_editors_tree->set_hide_root(true); + tile_data_editors_tree->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + tile_data_editors_tree->set_h_scroll_enabled(false); + tile_data_editors_tree->set_v_scroll_enabled(false); + tile_data_editors_tree->connect("item_selected", callable_mp(this, &TileSetAtlasSourceEditor::_tile_data_editors_tree_selected)); + tile_data_editors_popup->add_child(tile_data_editors_tree); + + tile_data_painting_editor_container = memnew(VBoxContainer); + tile_data_painting_editor_container->set_h_size_flags(SIZE_EXPAND_FILL); + middle_vbox_container->add_child(tile_data_painting_editor_container); + + // Atlas source inspector. + atlas_source_inspector_label = memnew(Label); + atlas_source_inspector_label->set_text(TTR("Atlas Properties:")); + middle_vbox_container->add_child(atlas_source_inspector_label); + + atlas_source_proxy_object = memnew(TileSetAtlasSourceProxyObject()); + atlas_source_proxy_object->connect("changed", callable_mp(this, &TileSetAtlasSourceEditor::_atlas_source_proxy_object_changed)); + + atlas_source_inspector = memnew(EditorInspector); + atlas_source_inspector->set_undo_redo(undo_redo); + atlas_source_inspector->set_enable_v_scroll(false); + atlas_source_inspector->edit(atlas_source_proxy_object); + middle_vbox_container->add_child(atlas_source_inspector); + + // Right panel. + VBoxContainer *right_panel = memnew(VBoxContainer); + right_panel->set_h_size_flags(SIZE_EXPAND_FILL); + right_panel->set_v_size_flags(SIZE_EXPAND_FILL); + split_container_right_side->add_child(right_panel); + + // -- Dialogs -- + confirm_auto_create_tiles = memnew(AcceptDialog); + confirm_auto_create_tiles->set_title(TTR("Auto Create Tiles in Non-Transparent Texture Regions?")); + confirm_auto_create_tiles->set_text(TTR("The atlas's texture was modified.\nWould you like to automatically create tiles in the atlas?")); + confirm_auto_create_tiles->get_ok_button()->set_text(TTR("Yes")); + confirm_auto_create_tiles->add_cancel_button()->set_text(TTR("No")); + confirm_auto_create_tiles->connect("confirmed", callable_mp(this, &TileSetAtlasSourceEditor::_auto_create_tiles)); + add_child(confirm_auto_create_tiles); + + // -- Toolbox -- + tools_button_group.instantiate(); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_fix_selected_and_hovered_tiles).unbind(1)); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_tile_id_label).unbind(1)); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_atlas_source_inspector).unbind(1)); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_tile_inspector).unbind(1)); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_tile_data_editors).unbind(1)); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_current_tile_data_editor).unbind(1)); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_atlas_view).unbind(1)); + tools_button_group->connect("pressed", callable_mp(this, &TileSetAtlasSourceEditor::_update_toolbar).unbind(1)); + + toolbox = memnew(HBoxContainer); + right_panel->add_child(toolbox); + + tool_setup_atlas_source_button = memnew(Button); + tool_setup_atlas_source_button->set_flat(true); + tool_setup_atlas_source_button->set_toggle_mode(true); + tool_setup_atlas_source_button->set_pressed(true); + tool_setup_atlas_source_button->set_button_group(tools_button_group); + tool_setup_atlas_source_button->set_tooltip(TTR("Atlas setup. Add/Remove tiles tool (use the shift key to create big tiles, control for rectangle editing).")); + toolbox->add_child(tool_setup_atlas_source_button); + + tool_select_button = memnew(Button); + tool_select_button->set_flat(true); + tool_select_button->set_toggle_mode(true); + tool_select_button->set_pressed(false); + tool_select_button->set_button_group(tools_button_group); + tool_select_button->set_tooltip(TTR("Select tiles.")); + toolbox->add_child(tool_select_button); + + tool_paint_button = memnew(Button); + tool_paint_button->set_flat(true); + tool_paint_button->set_toggle_mode(true); + tool_paint_button->set_button_group(tools_button_group); + tool_paint_button->set_tooltip(TTR("Paint properties.")); + toolbox->add_child(tool_paint_button); + + // Tool settings. + tool_settings = memnew(HBoxContainer); + toolbox->add_child(tool_settings); + + tool_settings_vsep = memnew(VSeparator); + tool_settings->add_child(tool_settings_vsep); + + tool_settings_tile_data_toolbar_container = memnew(HBoxContainer); + tool_settings->add_child(tool_settings_tile_data_toolbar_container); + + tools_settings_erase_button = memnew(Button); + tools_settings_erase_button->set_flat(true); + tools_settings_erase_button->set_toggle_mode(true); + tools_settings_erase_button->set_shortcut(ED_SHORTCUT("tiles_editor/eraser", "Eraser", Key::E)); + tools_settings_erase_button->set_shortcut_context(this); + tool_settings->add_child(tools_settings_erase_button); + + tool_advanced_menu_buttom = memnew(MenuButton); + tool_advanced_menu_buttom->set_flat(true); + tool_advanced_menu_buttom->get_popup()->add_item(TTR("Create Tiles in Non-Transparent Texture Regions"), ADVANCED_AUTO_CREATE_TILES); + tool_advanced_menu_buttom->get_popup()->add_item(TTR("Remove Tiles in Fully Transparent Texture Regions"), ADVANCED_AUTO_REMOVE_TILES); + tool_advanced_menu_buttom->get_popup()->connect("id_pressed", callable_mp(this, &TileSetAtlasSourceEditor::_menu_option)); + toolbox->add_child(tool_advanced_menu_buttom); + + _update_toolbar(); + + // Right side of toolbar. + Control *middle_space = memnew(Control); + middle_space->set_h_size_flags(SIZE_EXPAND_FILL); + toolbox->add_child(middle_space); + + tool_tile_id_label = memnew(Label); + tool_tile_id_label->set_mouse_filter(Control::MOUSE_FILTER_STOP); + toolbox->add_child(tool_tile_id_label); + _update_tile_id_label(); + + // Tile atlas view. + tile_atlas_view = memnew(TileAtlasView); + tile_atlas_view->set_h_size_flags(SIZE_EXPAND_FILL); + tile_atlas_view->set_v_size_flags(SIZE_EXPAND_FILL); + tile_atlas_view->connect("transform_changed", callable_mp(TilesEditorPlugin::get_singleton(), &TilesEditorPlugin::set_atlas_view_transform)); + tile_atlas_view->connect("transform_changed", callable_mp(this, &TileSetAtlasSourceEditor::_tile_atlas_view_transform_changed).unbind(2)); + right_panel->add_child(tile_atlas_view); + + base_tile_popup_menu = memnew(PopupMenu); + base_tile_popup_menu->add_shortcut(ED_SHORTCUT("tiles_editor/delete", TTR("Delete"), Key::KEY_DELETE), TILE_DELETE); + base_tile_popup_menu->add_item(TTR("Create an Alternative Tile"), TILE_CREATE_ALTERNATIVE); + base_tile_popup_menu->connect("id_pressed", callable_mp(this, &TileSetAtlasSourceEditor::_menu_option)); + tile_atlas_view->add_child(base_tile_popup_menu); + + empty_base_tile_popup_menu = memnew(PopupMenu); + empty_base_tile_popup_menu->add_item(TTR("Create a Tile"), TILE_CREATE); + empty_base_tile_popup_menu->connect("id_pressed", callable_mp(this, &TileSetAtlasSourceEditor::_menu_option)); + tile_atlas_view->add_child(empty_base_tile_popup_menu); + + tile_atlas_control = memnew(Control); + tile_atlas_control->connect("draw", callable_mp(this, &TileSetAtlasSourceEditor::_tile_atlas_control_draw)); + tile_atlas_control->connect("mouse_exited", callable_mp(this, &TileSetAtlasSourceEditor::_tile_atlas_control_mouse_exited)); + tile_atlas_control->connect("gui_input", callable_mp(this, &TileSetAtlasSourceEditor::_tile_atlas_control_gui_input)); + tile_atlas_view->add_control_over_atlas_tiles(tile_atlas_control); + + tile_atlas_control_unscaled = memnew(Control); + tile_atlas_control_unscaled->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + tile_atlas_control_unscaled->connect("draw", callable_mp(this, &TileSetAtlasSourceEditor::_tile_atlas_control_unscaled_draw)); + tile_atlas_view->add_control_over_atlas_tiles(tile_atlas_control_unscaled, false); + tile_atlas_control_unscaled->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + + alternative_tile_popup_menu = memnew(PopupMenu); + alternative_tile_popup_menu->add_shortcut(ED_SHORTCUT("tiles_editor/delete_tile", TTR("Delete"), Key::KEY_DELETE), TILE_DELETE); + alternative_tile_popup_menu->connect("id_pressed", callable_mp(this, &TileSetAtlasSourceEditor::_menu_option)); + tile_atlas_view->add_child(alternative_tile_popup_menu); + + alternative_tiles_control = memnew(Control); + alternative_tiles_control->connect("draw", callable_mp(this, &TileSetAtlasSourceEditor::_tile_alternatives_control_draw)); + alternative_tiles_control->connect("mouse_exited", callable_mp(this, &TileSetAtlasSourceEditor::_tile_alternatives_control_mouse_exited)); + alternative_tiles_control->connect("gui_input", callable_mp(this, &TileSetAtlasSourceEditor::_tile_alternatives_control_gui_input)); + tile_atlas_view->add_control_over_alternative_tiles(alternative_tiles_control); + + alternative_tiles_control_unscaled = memnew(Control); + alternative_tiles_control_unscaled->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + alternative_tiles_control_unscaled->connect("draw", callable_mp(this, &TileSetAtlasSourceEditor::_tile_alternatives_control_unscaled_draw)); + tile_atlas_view->add_control_over_alternative_tiles(alternative_tiles_control_unscaled, false); + alternative_tiles_control_unscaled->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + + tile_atlas_view_missing_source_label = memnew(Label); + tile_atlas_view_missing_source_label->set_text(TTR("Add or select an atlas texture to the left panel.")); + tile_atlas_view_missing_source_label->set_align(Label::ALIGN_CENTER); + tile_atlas_view_missing_source_label->set_valign(Label::VALIGN_CENTER); + tile_atlas_view_missing_source_label->set_h_size_flags(SIZE_EXPAND_FILL); + tile_atlas_view_missing_source_label->set_v_size_flags(SIZE_EXPAND_FILL); + tile_atlas_view_missing_source_label->hide(); + right_panel->add_child(tile_atlas_view_missing_source_label); + + EditorNode::get_singleton()->get_editor_data().add_undo_redo_inspector_hook_callback(callable_mp(this, &TileSetAtlasSourceEditor::_undo_redo_inspector_callback)); + + // Inspector plugin. + Ref<EditorInspectorPluginTileData> tile_data_inspector_plugin; + tile_data_inspector_plugin.instantiate(); + EditorInspector::add_inspector_plugin(tile_data_inspector_plugin); +} + +TileSetAtlasSourceEditor::~TileSetAtlasSourceEditor() { + memdelete(tile_proxy_object); + memdelete(atlas_source_proxy_object); +} + +////// EditorPropertyTilePolygon ////// + +void EditorPropertyTilePolygon::_add_focusable_children(Node *p_node) { + Control *control = Object::cast_to<Control>(p_node); + if (control && control->get_focus_mode() != Control::FOCUS_NONE) { + add_focusable(control); + } + for (int i = 0; i < p_node->get_child_count(); i++) { + _add_focusable_children(p_node->get_child(i)); + } +} + +void EditorPropertyTilePolygon::_polygons_changed() { + if (String(count_property).is_empty()) { + if (base_type == "OccluderPolygon2D") { + // Single OccluderPolygon2D. + Ref<OccluderPolygon2D> occluder; + if (generic_tile_polygon_editor->get_polygon_count() >= 1) { + occluder.instantiate(); + occluder->set_polygon(generic_tile_polygon_editor->get_polygon(0)); + } + emit_changed(get_edited_property(), occluder); + } else if (base_type == "NavigationPolygon") { + Ref<NavigationPolygon> navigation_polygon; + if (generic_tile_polygon_editor->get_polygon_count() >= 1) { + navigation_polygon.instantiate(); + for (int i = 0; i < generic_tile_polygon_editor->get_polygon_count(); i++) { + Vector<Vector2> polygon = generic_tile_polygon_editor->get_polygon(i); + navigation_polygon->add_outline(polygon); + } + navigation_polygon->make_polygons_from_outlines(); + } + emit_changed(get_edited_property(), navigation_polygon); + } + } else { + if (base_type.is_empty()) { + // Multiple array of vertices. + Vector<String> changed_properties; + Array values; + int count = generic_tile_polygon_editor->get_polygon_count(); + changed_properties.push_back(count_property); + values.push_back(count); + for (int i = 0; i < count; i++) { + changed_properties.push_back(vformat(element_pattern, i)); + values.push_back(generic_tile_polygon_editor->get_polygon(i)); + } + emit_signal("multiple_properties_changed", changed_properties, values, false); + } + } +} + +void EditorPropertyTilePolygon::update_property() { + TileSetAtlasSourceEditor::AtlasTileProxyObject *atlas_tile_proxy_object = Object::cast_to<TileSetAtlasSourceEditor::AtlasTileProxyObject>(get_edited_object()); + ERR_FAIL_COND(!atlas_tile_proxy_object); + ERR_FAIL_COND(atlas_tile_proxy_object->get_edited_tiles().is_empty()); + + TileSetAtlasSource *tile_set_atlas_source = atlas_tile_proxy_object->get_edited_tile_set_atlas_source(); + generic_tile_polygon_editor->set_tile_set(Ref<TileSet>(tile_set_atlas_source->get_tile_set())); + + // Set the background + Vector2i coords = atlas_tile_proxy_object->get_edited_tiles().front()->get().tile; + int alternative = atlas_tile_proxy_object->get_edited_tiles().front()->get().alternative; + TileData *tile_data = Object::cast_to<TileData>(tile_set_atlas_source->get_tile_data(coords, alternative)); + generic_tile_polygon_editor->set_background(tile_set_atlas_source->get_texture(), tile_set_atlas_source->get_tile_texture_region(coords), tile_set_atlas_source->get_tile_effective_texture_offset(coords, alternative), tile_data->get_flip_h(), tile_data->get_flip_v(), tile_data->get_transpose(), tile_data->get_modulate()); + + // Reset the polygons. + generic_tile_polygon_editor->clear_polygons(); + + if (String(count_property).is_empty()) { + if (base_type == "OccluderPolygon2D") { + // Single OccluderPolygon2D. + Ref<OccluderPolygon2D> occluder = get_edited_object()->get(get_edited_property()); + generic_tile_polygon_editor->clear_polygons(); + if (occluder.is_valid()) { + generic_tile_polygon_editor->add_polygon(occluder->get_polygon()); + } + } else if (base_type == "NavigationPolygon") { + // Single OccluderPolygon2D. + Ref<NavigationPolygon> navigation_polygon = get_edited_object()->get(get_edited_property()); + generic_tile_polygon_editor->clear_polygons(); + if (navigation_polygon.is_valid()) { + for (int i = 0; i < navigation_polygon->get_outline_count(); i++) { + generic_tile_polygon_editor->add_polygon(navigation_polygon->get_outline(i)); + } + } + } + } else { + int count = get_edited_object()->get(count_property); + if (base_type.is_empty()) { + // Multiple array of vertices. + generic_tile_polygon_editor->clear_polygons(); + for (int i = 0; i < count; i++) { + generic_tile_polygon_editor->add_polygon(get_edited_object()->get(vformat(element_pattern, i))); + } + } + } +} + +void EditorPropertyTilePolygon::setup_single_mode(const StringName &p_property, const String &p_base_type) { + set_object_and_property(nullptr, p_property); + base_type = p_base_type; + + generic_tile_polygon_editor->set_multiple_polygon_mode(false); +} + +void EditorPropertyTilePolygon::setup_multiple_mode(const StringName &p_property, const StringName &p_count_property, const String &p_element_pattern, const String &p_base_type) { + set_object_and_property(nullptr, p_property); + count_property = p_count_property; + element_pattern = p_element_pattern; + base_type = p_base_type; + + generic_tile_polygon_editor->set_multiple_polygon_mode(true); +} + +EditorPropertyTilePolygon::EditorPropertyTilePolygon() { + // Setup the polygon editor. + generic_tile_polygon_editor = memnew(GenericTilePolygonEditor); + generic_tile_polygon_editor->set_use_undo_redo(false); + generic_tile_polygon_editor->clear_polygons(); + add_child(generic_tile_polygon_editor); + generic_tile_polygon_editor->connect("polygons_changed", callable_mp(this, &EditorPropertyTilePolygon::_polygons_changed)); + + // Add all focussable children of generic_tile_polygon_editor as focussable. + _add_focusable_children(generic_tile_polygon_editor); +} + +////// EditorInspectorPluginTileData ////// + +bool EditorInspectorPluginTileData::can_handle(Object *p_object) { + return Object::cast_to<TileSetAtlasSourceEditor::AtlasTileProxyObject>(p_object) != nullptr; +} + +bool EditorInspectorPluginTileData::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const uint32_t p_usage, const bool p_wide) { + Vector<String> components = String(p_path).split("/", true, 2); + if (components.size() == 2 && components[0].begins_with("occlusion_layer_") && components[0].trim_prefix("occlusion_layer_").is_valid_int()) { + // Occlusion layers. + int layer_index = components[0].trim_prefix("occlusion_layer_").to_int(); + ERR_FAIL_COND_V(layer_index < 0, false); + if (components[1] == "polygon") { + EditorPropertyTilePolygon *ep = memnew(EditorPropertyTilePolygon); + ep->setup_single_mode(p_path, "OccluderPolygon2D"); + add_property_editor(p_path, ep); + return true; + } + } else if (components.size() >= 2 && components[0].begins_with("physics_layer_") && components[0].trim_prefix("physics_layer_").is_valid_int()) { + // Physics layers. + int layer_index = components[0].trim_prefix("physics_layer_").to_int(); + ERR_FAIL_COND_V(layer_index < 0, false); + if (components[1] == "polygons_count") { + EditorPropertyTilePolygon *ep = memnew(EditorPropertyTilePolygon); + ep->setup_multiple_mode(vformat("physics_layer_%d/polygons", layer_index), vformat("physics_layer_%d/polygons_count", layer_index), vformat("physics_layer_%d/polygon_%%d/points", layer_index), ""); + Vector<String> properties; + properties.push_back(p_path); + int count = p_object->get(vformat("physics_layer_%d/polygons_count", layer_index)); + for (int i = 0; i < count; i++) { + properties.push_back(vformat(vformat("physics_layer_%d/polygon_%d/points", layer_index, i))); + } + add_property_editor_for_multiple_properties("Polygons", properties, ep); + return true; + } else if (components.size() == 3 && components[1].begins_with("polygon_") && components[1].trim_prefix("polygon_").is_valid_int()) { + int polygon_index = components[1].trim_prefix("polygon_").to_int(); + ERR_FAIL_COND_V(polygon_index < 0, false); + if (components[2] == "points") { + return true; + } + } + } else if (components.size() == 2 && components[0].begins_with("navigation_layer_") && components[0].trim_prefix("navigation_layer_").is_valid_int()) { + // Navigation layers. + int layer_index = components[0].trim_prefix("navigation_layer_").to_int(); + ERR_FAIL_COND_V(layer_index < 0, false); + if (components[1] == "polygon") { + EditorPropertyTilePolygon *ep = memnew(EditorPropertyTilePolygon); + ep->setup_single_mode(p_path, "NavigationPolygon"); + add_property_editor(p_path, ep); + return true; + } + } + return false; +} diff --git a/editor/plugins/tiles/tile_set_atlas_source_editor.h b/editor/plugins/tiles/tile_set_atlas_source_editor.h new file mode 100644 index 0000000000..bd1fd2e7d0 --- /dev/null +++ b/editor/plugins/tiles/tile_set_atlas_source_editor.h @@ -0,0 +1,318 @@ +/*************************************************************************/ +/* tile_set_atlas_source_editor.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILE_SET_ATLAS_SOURCE_EDITOR_H +#define TILE_SET_ATLAS_SOURCE_EDITOR_H + +#include "tile_atlas_view.h" +#include "tile_data_editors.h" + +#include "editor/editor_node.h" +#include "scene/gui/split_container.h" +#include "scene/resources/tile_set.h" + +class TileSet; + +class TileSetAtlasSourceEditor : public HBoxContainer { + GDCLASS(TileSetAtlasSourceEditor, HBoxContainer); + +public: + // A class to store which tiles are selected. + struct TileSelection { + Vector2i tile = TileSetSource::INVALID_ATLAS_COORDS; + int alternative = TileSetSource::INVALID_TILE_ALTERNATIVE; + + bool operator<(const TileSelection &p_other) const { + if (tile == p_other.tile) { + return alternative < p_other.alternative; + } else { + return tile < p_other.tile; + } + } + }; + + // -- Proxy object for an atlas source, needed by the inspector -- + class TileSetAtlasSourceProxyObject : public Object { + GDCLASS(TileSetAtlasSourceProxyObject, Object); + + private: + Ref<TileSet> tile_set; + TileSetAtlasSource *tile_set_atlas_source = nullptr; + int source_id = TileSet::INVALID_SOURCE; + + protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + static void _bind_methods(); + + public: + void set_id(int p_id); + int get_id(); + + void edit(Ref<TileSet> p_tile_set, TileSetAtlasSource *p_tile_set_atlas_source, int p_source_id); + TileSetAtlasSource *get_edited() { return tile_set_atlas_source; }; + }; + + // -- Proxy object for a tile, needed by the inspector -- + class AtlasTileProxyObject : public Object { + GDCLASS(AtlasTileProxyObject, Object); + + private: + TileSetAtlasSourceEditor *tiles_set_atlas_source_editor; + + TileSetAtlasSource *tile_set_atlas_source = nullptr; + Set<TileSelection> tiles = Set<TileSelection>(); + + protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + + static void _bind_methods(); + + public: + TileSetAtlasSource *get_edited_tile_set_atlas_source() const { return tile_set_atlas_source; }; + Set<TileSelection> get_edited_tiles() const { return tiles; }; + + // Update the proxyed object. + void edit(TileSetAtlasSource *p_tile_set_atlas_source, Set<TileSelection> p_tiles = Set<TileSelection>()); + + AtlasTileProxyObject(TileSetAtlasSourceEditor *p_tiles_set_atlas_source_editor) { + tiles_set_atlas_source_editor = p_tiles_set_atlas_source_editor; + } + }; + +private: + Ref<TileSet> tile_set; + TileSetAtlasSource *tile_set_atlas_source = nullptr; + int tile_set_atlas_source_id = TileSet::INVALID_SOURCE; + + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + + bool tile_set_changed_needs_update = false; + + // -- Properties painting -- + VBoxContainer *tile_data_painting_editor_container; + Label *tile_data_editors_label; + Button *tile_data_editor_dropdown_button; + Popup *tile_data_editors_popup; + Tree *tile_data_editors_tree; + void _tile_data_editor_dropdown_button_draw(); + void _tile_data_editor_dropdown_button_pressed(); + + // -- Tile data editors -- + String current_property; + Control *current_tile_data_editor_toolbar = nullptr; + Map<String, TileDataEditor *> tile_data_editors; + TileDataEditor *current_tile_data_editor = nullptr; + void _tile_data_editors_tree_selected(); + + // -- Inspector -- + AtlasTileProxyObject *tile_proxy_object; + Label *tile_inspector_label; + EditorInspector *tile_inspector; + Label *tile_inspector_no_tile_selected_label; + String selected_property; + void _inspector_property_selected(String p_property); + + TileSetAtlasSourceProxyObject *atlas_source_proxy_object; + Label *atlas_source_inspector_label; + EditorInspector *atlas_source_inspector; + + // -- Atlas view -- + HBoxContainer *toolbox; + Label *tile_atlas_view_missing_source_label; + TileAtlasView *tile_atlas_view; + + // Dragging + enum DragType { + DRAG_TYPE_NONE = 0, + DRAG_TYPE_CREATE_TILES, + DRAG_TYPE_CREATE_TILES_USING_RECT, + DRAG_TYPE_CREATE_BIG_TILE, + DRAG_TYPE_REMOVE_TILES, + DRAG_TYPE_REMOVE_TILES_USING_RECT, + + DRAG_TYPE_MOVE_TILE, + + DRAG_TYPE_RECT_SELECT, + + DRAG_TYPE_MAY_POPUP_MENU, + + // Warning: keep in this order. + DRAG_TYPE_RESIZE_TOP_LEFT, + DRAG_TYPE_RESIZE_TOP, + DRAG_TYPE_RESIZE_TOP_RIGHT, + DRAG_TYPE_RESIZE_RIGHT, + DRAG_TYPE_RESIZE_BOTTOM_RIGHT, + DRAG_TYPE_RESIZE_BOTTOM, + DRAG_TYPE_RESIZE_BOTTOM_LEFT, + DRAG_TYPE_RESIZE_LEFT, + }; + DragType drag_type = DRAG_TYPE_NONE; + Vector2i drag_start_mouse_pos; + Vector2i drag_last_mouse_pos; + Vector2i drag_current_tile; + + Rect2i drag_start_tile_shape; + Set<Vector2i> drag_modified_tiles; + void _end_dragging(); + + Map<Vector2i, List<const PropertyInfo *>> _group_properties_per_tiles(const List<PropertyInfo> &r_list, const TileSetAtlasSource *p_atlas); + + // Popup functions. + enum MenuOptions { + TILE_CREATE, + TILE_CREATE_ALTERNATIVE, + TILE_DELETE, + + ADVANCED_AUTO_CREATE_TILES, + ADVANCED_AUTO_REMOVE_TILES, + }; + Vector2i menu_option_coords; + int menu_option_alternative = TileSetSource::INVALID_TILE_ALTERNATIVE; + void _menu_option(int p_option); + + // Tool buttons. + Ref<ButtonGroup> tools_button_group; + Button *tool_setup_atlas_source_button; + Button *tool_select_button; + Button *tool_paint_button; + Label *tool_tile_id_label; + + // Tool settings. + HBoxContainer *tool_settings; + VSeparator *tool_settings_vsep; + HBoxContainer *tool_settings_tile_data_toolbar_container; + Button *tools_settings_erase_button; + MenuButton *tool_advanced_menu_buttom; + + // Selection. + Set<TileSelection> selection; + + void _set_selection_from_array(Array p_selection); + Array _get_selection_as_array(); + + // A control on the tile atlas to draw and handle input events. + Vector2i hovered_base_tile_coords = TileSetSource::INVALID_ATLAS_COORDS; + + PopupMenu *base_tile_popup_menu; + PopupMenu *empty_base_tile_popup_menu; + Ref<Texture2D> resize_handle; + Ref<Texture2D> resize_handle_disabled; + Control *tile_atlas_control; + Control *tile_atlas_control_unscaled; + void _tile_atlas_control_draw(); + void _tile_atlas_control_unscaled_draw(); + void _tile_atlas_control_mouse_exited(); + void _tile_atlas_control_gui_input(const Ref<InputEvent> &p_event); + void _tile_atlas_view_transform_changed(); + + // A control over the alternative tiles. + Vector3i hovered_alternative_tile_coords = Vector3i(TileSetSource::INVALID_ATLAS_COORDS.x, TileSetSource::INVALID_ATLAS_COORDS.y, TileSetSource::INVALID_TILE_ALTERNATIVE); + + PopupMenu *alternative_tile_popup_menu; + Control *alternative_tiles_control; + Control *alternative_tiles_control_unscaled; + void _tile_alternatives_control_draw(); + void _tile_alternatives_control_unscaled_draw(); + void _tile_alternatives_control_mouse_exited(); + void _tile_alternatives_control_gui_input(const Ref<InputEvent> &p_event); + + // -- Update functions -- + void _update_tile_id_label(); + void _update_source_inspector(); + void _update_fix_selected_and_hovered_tiles(); + void _update_atlas_source_inspector(); + void _update_tile_inspector(); + void _update_tile_data_editors(); + void _update_current_tile_data_editor(); + void _update_manage_tile_properties_button(); + void _update_atlas_view(); + void _update_toolbar(); + + // -- input events -- + void _unhandled_key_input(const Ref<InputEvent> &p_event); + + // -- Misc -- + void _auto_create_tiles(); + void _auto_remove_tiles(); + AcceptDialog *confirm_auto_create_tiles; + + void _tile_set_changed(); + void _tile_proxy_object_changed(String p_what); + void _atlas_source_proxy_object_changed(String p_what); + + void _undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, String p_property, Variant p_new_value); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void edit(Ref<TileSet> p_tile_set, TileSetAtlasSource *p_tile_set_source, int p_source_id); + void init_source(); + + TileSetAtlasSourceEditor(); + ~TileSetAtlasSourceEditor(); +}; + +class EditorPropertyTilePolygon : public EditorProperty { + GDCLASS(EditorPropertyTilePolygon, EditorProperty); + + StringName count_property; + String element_pattern; + String base_type; + + void _add_focusable_children(Node *p_node); + + GenericTilePolygonEditor *generic_tile_polygon_editor; + void _polygons_changed(); + +public: + virtual void update_property() override; + void setup_single_mode(const StringName &p_property, const String &p_base_type); + void setup_multiple_mode(const StringName &p_property, const StringName &p_count_property, const String &p_element_pattern, const String &p_base_type); + EditorPropertyTilePolygon(); +}; + +class EditorInspectorPluginTileData : public EditorInspectorPlugin { + GDCLASS(EditorInspectorPluginTileData, EditorInspectorPlugin); + + void _occlusion_polygon_set_callback(); + void _polygons_changed(Object *p_generic_tile_polygon_editor, Object *p_object, const String &p_path); + +public: + virtual bool can_handle(Object *p_object) override; + virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const uint32_t p_usage, const bool p_wide = false) override; +}; + +#endif // TILE_SET_ATLAS_SOURCE_EDITOR_H diff --git a/editor/plugins/tiles/tile_set_editor.cpp b/editor/plugins/tiles/tile_set_editor.cpp new file mode 100644 index 0000000000..915ce50836 --- /dev/null +++ b/editor/plugins/tiles/tile_set_editor.cpp @@ -0,0 +1,772 @@ +/*************************************************************************/ +/* tile_set_editor.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tile_set_editor.h" + +#include "tile_data_editors.h" +#include "tiles_editor_plugin.h" + +#include "editor/editor_scale.h" + +#include "scene/gui/box_container.h" +#include "scene/gui/control.h" +#include "scene/gui/tab_container.h" + +TileSetEditor *TileSetEditor::singleton = nullptr; + +void TileSetEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { + ERR_FAIL_COND(!tile_set.is_valid()); + + if (!_can_drop_data_fw(p_point, p_data, p_from)) { + return; + } + + if (p_from == sources_list) { + // Handle dropping a texture in the list of atlas resources. + int source_id = TileSet::INVALID_SOURCE; + int added = 0; + Dictionary d = p_data; + Vector<String> files = d["files"]; + for (int i = 0; i < files.size(); i++) { + Ref<Texture2D> resource = ResourceLoader::load(files[i]); + if (resource.is_valid()) { + // Retrieve the id for the next created source. + source_id = tile_set->get_next_source_id(); + + // Actually create the new source. + Ref<TileSetAtlasSource> atlas_source = memnew(TileSetAtlasSource); + atlas_source->set_texture(resource); + undo_redo->create_action(TTR("Add a new atlas source")); + undo_redo->add_do_method(*tile_set, "add_source", atlas_source, source_id); + undo_redo->add_do_method(*atlas_source, "set_texture_region_size", tile_set->get_tile_size()); + undo_redo->add_undo_method(*tile_set, "remove_source", source_id); + undo_redo->commit_action(); + added += 1; + } + } + + if (added == 1) { + tile_set_atlas_source_editor->init_source(); + } + + // Update the selected source (thus triggering an update). + _update_sources_list(source_id); + } +} + +bool TileSetEditor::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { + ERR_FAIL_COND_V(!tile_set.is_valid(), false); + + if (p_from == sources_list) { + Dictionary d = p_data; + + if (!d.has("type")) { + return false; + } + + // Check if we have a Texture2D. + if (String(d["type"]) == "files") { + Vector<String> files = d["files"]; + + if (files.size() == 0) { + return false; + } + + for (int i = 0; i < files.size(); i++) { + String file = files[i]; + String ftype = EditorFileSystem::get_singleton()->get_file_type(file); + + if (!ClassDB::is_parent_class(ftype, "Texture2D")) { + return false; + } + } + + return true; + } + } + return false; +} + +void TileSetEditor::_update_sources_list(int force_selected_id) { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Get the previously selected id. + int old_selected = TileSet::INVALID_SOURCE; + if (sources_list->get_current() >= 0) { + int source_id = sources_list->get_item_metadata(sources_list->get_current()); + if (tile_set->has_source(source_id)) { + old_selected = source_id; + } + } + + int to_select = TileSet::INVALID_SOURCE; + if (force_selected_id >= 0) { + to_select = force_selected_id; + } else if (old_selected >= 0) { + to_select = old_selected; + } + + // Clear the list. + sources_list->clear(); + + // Update the atlas sources. + for (int i = 0; i < tile_set->get_source_count(); i++) { + int source_id = tile_set->get_source_id(i); + + TileSetSource *source = *tile_set->get_source(source_id); + + Ref<Texture2D> texture; + String item_text; + + // Common to all type of sources. + if (!source->get_name().is_empty()) { + item_text = vformat(TTR("%s (id:%d)"), source->get_name(), source_id); + } + + // Atlas source. + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(source); + if (atlas_source) { + texture = atlas_source->get_texture(); + if (item_text.is_empty()) { + if (texture.is_valid()) { + item_text = vformat("%s (ID:%d)", texture->get_path().get_file(), source_id); + } else { + item_text = vformat(TTR("No Texture Atlas Source (ID:%d)"), source_id); + } + } + } + + // Scene collection source. + TileSetScenesCollectionSource *scene_collection_source = Object::cast_to<TileSetScenesCollectionSource>(source); + if (scene_collection_source) { + texture = get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons")); + if (item_text.is_empty()) { + item_text = vformat(TTR("Scene Collection Source (ID:%d)"), source_id); + } + } + + // Use default if not valid. + if (item_text.is_empty()) { + item_text = vformat(TTR("Unknown Type Source (ID:%d)"), source_id); + } + if (!texture.is_valid()) { + texture = missing_texture_texture; + } + + sources_list->add_item(item_text, texture); + sources_list->set_item_metadata(i, source_id); + } + + // Set again the current selected item if needed. + if (to_select >= 0) { + for (int i = 0; i < sources_list->get_item_count(); i++) { + if ((int)sources_list->get_item_metadata(i) == to_select) { + sources_list->set_current(i); + if (old_selected != to_select) { + sources_list->emit_signal(SNAME("item_selected"), sources_list->get_current()); + } + break; + } + } + } + + // If nothing is selected, select the first entry. + if (sources_list->get_current() < 0 && sources_list->get_item_count() > 0) { + sources_list->set_current(0); + if (old_selected != int(sources_list->get_item_metadata(0))) { + sources_list->emit_signal(SNAME("item_selected"), sources_list->get_current()); + } + } + + // If there is no source left, hide all editors and show the label. + _source_selected(sources_list->get_current()); + + // Synchronize the lists. + TilesEditorPlugin::get_singleton()->set_sources_lists_current(sources_list->get_current()); +} + +void TileSetEditor::_source_selected(int p_source_index) { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Update the selected source. + sources_delete_button->set_disabled(p_source_index < 0); + + if (p_source_index >= 0) { + int source_id = sources_list->get_item_metadata(p_source_index); + TileSetAtlasSource *atlas_source = Object::cast_to<TileSetAtlasSource>(*tile_set->get_source(source_id)); + TileSetScenesCollectionSource *scenes_collection_source = Object::cast_to<TileSetScenesCollectionSource>(*tile_set->get_source(source_id)); + if (atlas_source) { + no_source_selected_label->hide(); + tile_set_atlas_source_editor->edit(*tile_set, atlas_source, source_id); + tile_set_atlas_source_editor->show(); + tile_set_scenes_collection_source_editor->hide(); + } else if (scenes_collection_source) { + no_source_selected_label->hide(); + tile_set_atlas_source_editor->hide(); + tile_set_scenes_collection_source_editor->edit(*tile_set, scenes_collection_source, source_id); + tile_set_scenes_collection_source_editor->show(); + } else { + no_source_selected_label->show(); + tile_set_atlas_source_editor->hide(); + tile_set_scenes_collection_source_editor->hide(); + } + } else { + no_source_selected_label->show(); + tile_set_atlas_source_editor->hide(); + tile_set_scenes_collection_source_editor->hide(); + } +} + +void TileSetEditor::_source_delete_pressed() { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Update the selected source. + int to_delete = sources_list->get_item_metadata(sources_list->get_current()); + + Ref<TileSetSource> source = tile_set->get_source(to_delete); + + // Remove the source. + undo_redo->create_action(TTR("Remove source")); + undo_redo->add_do_method(*tile_set, "remove_source", to_delete); + undo_redo->add_undo_method(*tile_set, "add_source", source, to_delete); + undo_redo->commit_action(); + + _update_sources_list(); +} + +void TileSetEditor::_source_add_id_pressed(int p_id_pressed) { + ERR_FAIL_COND(!tile_set.is_valid()); + + switch (p_id_pressed) { + case 0: { + int source_id = tile_set->get_next_source_id(); + + Ref<TileSetAtlasSource> atlas_source = memnew(TileSetAtlasSource); + + // Add a new source. + undo_redo->create_action(TTR("Add atlas source")); + undo_redo->add_do_method(*tile_set, "add_source", atlas_source, source_id); + undo_redo->add_do_method(*atlas_source, "set_texture_region_size", tile_set->get_tile_size()); + undo_redo->add_undo_method(*tile_set, "remove_source", source_id); + undo_redo->commit_action(); + + _update_sources_list(source_id); + } break; + case 1: { + int source_id = tile_set->get_next_source_id(); + + Ref<TileSetScenesCollectionSource> scene_collection_source = memnew(TileSetScenesCollectionSource); + + // Add a new source. + undo_redo->create_action(TTR("Add atlas source")); + undo_redo->add_do_method(*tile_set, "add_source", scene_collection_source, source_id); + undo_redo->add_undo_method(*tile_set, "remove_source", source_id); + undo_redo->commit_action(); + + _update_sources_list(source_id); + } break; + default: + ERR_FAIL(); + } +} + +void TileSetEditor::_sources_advanced_menu_id_pressed(int p_id_pressed) { + ERR_FAIL_COND(!tile_set.is_valid()); + + switch (p_id_pressed) { + case 0: { + atlas_merging_dialog->update_tile_set(tile_set); + atlas_merging_dialog->popup_centered_ratio(0.5); + } break; + case 1: { + tile_proxies_manager_dialog->update_tile_set(tile_set); + tile_proxies_manager_dialog->popup_centered_ratio(0.5); + } break; + } +} + +void TileSetEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: + sources_delete_button->set_icon(get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"))); + sources_add_button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons"))); + sources_advanced_menu_button->set_icon(get_theme_icon(SNAME("GuiTabMenuHl"), SNAME("EditorIcons"))); + missing_texture_texture = get_theme_icon(SNAME("TileSet"), SNAME("EditorIcons")); + break; + case NOTIFICATION_INTERNAL_PROCESS: + if (tile_set_changed_needs_update) { + if (tile_set.is_valid()) { + tile_set->set_edited(true); + } + _update_sources_list(); + _update_patterns_list(); + tile_set_changed_needs_update = false; + } + break; + default: + break; + } +} + +void TileSetEditor::_patterns_item_list_gui_input(const Ref<InputEvent> &p_event) { + ERR_FAIL_COND(!tile_set.is_valid()); + + if (ED_IS_SHORTCUT("tiles_editor/delete", p_event) && p_event->is_pressed() && !p_event->is_echo()) { + Vector<int> selected = patterns_item_list->get_selected_items(); + undo_redo->create_action(TTR("Remove TileSet patterns")); + for (int i = 0; i < selected.size(); i++) { + int pattern_index = selected[i]; + undo_redo->add_do_method(*tile_set, "remove_pattern", pattern_index); + undo_redo->add_undo_method(*tile_set, "add_pattern", tile_set->get_pattern(pattern_index), pattern_index); + } + undo_redo->commit_action(); + patterns_item_list->accept_event(); + } +} + +void TileSetEditor::_pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture) { + // TODO optimize ? + for (int i = 0; i < patterns_item_list->get_item_count(); i++) { + if (patterns_item_list->get_item_metadata(i) == p_pattern) { + patterns_item_list->set_item_icon(i, p_texture); + break; + } + } +} + +void TileSetEditor::_update_patterns_list() { + ERR_FAIL_COND(!tile_set.is_valid()); + + // Recreate the items. + patterns_item_list->clear(); + for (int i = 0; i < tile_set->get_patterns_count(); i++) { + int id = patterns_item_list->add_item(""); + patterns_item_list->set_item_metadata(id, tile_set->get_pattern(i)); + TilesEditorPlugin::get_singleton()->queue_pattern_preview(tile_set, tile_set->get_pattern(i), callable_mp(this, &TileSetEditor::_pattern_preview_done)); + } + + // Update the label visibility. + patterns_help_label->set_visible(patterns_item_list->get_item_count() == 0); +} + +void TileSetEditor::_tile_set_changed() { + tile_set_changed_needs_update = true; +} + +void TileSetEditor::_tab_changed(int p_tab_changed) { + split_container->set_visible(p_tab_changed == 0); + patterns_item_list->set_visible(p_tab_changed == 1); +} + +void TileSetEditor::_move_tile_set_array_element(Object *p_undo_redo, Object *p_edited, String p_array_prefix, int p_from_index, int p_to_pos) { + UndoRedo *undo_redo = Object::cast_to<UndoRedo>(p_undo_redo); + ERR_FAIL_COND(!undo_redo); + + TileSet *tile_set = Object::cast_to<TileSet>(p_edited); + if (!tile_set) { + return; + } + + Vector<String> components = String(p_array_prefix).split("/", true, 2); + + // Compute the array indices to save. + int begin = 0; + int end; + if (p_array_prefix == "occlusion_layer_") { + end = tile_set->get_occlusion_layers_count(); + } else if (p_array_prefix == "physics_layer_") { + end = tile_set->get_physics_layers_count(); + } else if (p_array_prefix == "terrain_set_") { + end = tile_set->get_terrain_sets_count(); + } else if (components.size() >= 2 && components[0].begins_with("terrain_set_") && components[0].trim_prefix("terrain_set_").is_valid_int() && components[1] == "terrain_") { + int terrain_set = components[0].trim_prefix("terrain_set_").to_int(); + end = tile_set->get_terrains_count(terrain_set); + } else if (p_array_prefix == "navigation_layer_") { + end = tile_set->get_navigation_layers_count(); + } else if (p_array_prefix == "custom_data_layer_") { + end = tile_set->get_custom_data_layers_count(); + } else { + ERR_FAIL_MSG("Invalid array prefix for TileSet."); + } + if (p_from_index < 0) { + // Adding new. + if (p_to_pos >= 0) { + begin = p_to_pos; + } else { + end = 0; // Nothing to save when adding at the end. + } + } else if (p_to_pos < 0) { + // Removing. + begin = p_from_index; + } else { + // Moving. + begin = MIN(p_from_index, p_to_pos); + end = MIN(MAX(p_from_index, p_to_pos) + 1, end); + } + +#define ADD_UNDO(obj, property) undo_redo->add_undo_property(obj, property, obj->get(property)); + // Save layers' properties. + List<PropertyInfo> properties; + tile_set->get_property_list(&properties); + for (PropertyInfo pi : properties) { + if (pi.name.begins_with(p_array_prefix)) { + String str = pi.name.trim_prefix(p_array_prefix); + int to_char_index = 0; + while (to_char_index < str.length()) { + if (str[to_char_index] < '0' || str[to_char_index] > '9') { + break; + } + to_char_index++; + } + if (to_char_index > 0) { + int array_index = str.left(to_char_index).to_int(); + if (array_index >= begin && array_index < end) { + ADD_UNDO(tile_set, pi.name); + } + } + } + } + + // Save properties for TileSetAtlasSources tile data + for (int i = 0; i < tile_set->get_source_count(); i++) { + int source_id = tile_set->get_source_id(i); + + Ref<TileSetAtlasSource> tas = tile_set->get_source(source_id); + if (tas.is_valid()) { + for (int j = 0; j < tas->get_tiles_count(); j++) { + Vector2i tile_id = tas->get_tile_id(j); + for (int k = 0; k < tas->get_alternative_tiles_count(tile_id); k++) { + int alternative_id = tas->get_alternative_tile_id(tile_id, k); + TileData *tile_data = Object::cast_to<TileData>(tas->get_tile_data(tile_id, alternative_id)); + ERR_FAIL_COND(!tile_data); + + // Actually saving stuff. + if (p_array_prefix == "occlusion_layer_") { + for (int layer_index = begin; layer_index < end; layer_index++) { + ADD_UNDO(tile_data, vformat("occlusion_layer_%d/polygon", layer_index)); + } + } else if (p_array_prefix == "physics_layer_") { + for (int layer_index = begin; layer_index < end; layer_index++) { + ADD_UNDO(tile_data, vformat("physics_layer_%d/polygons_count", layer_index)); + for (int polygon_index = 0; polygon_index < tile_data->get_collision_polygons_count(layer_index); polygon_index++) { + ADD_UNDO(tile_data, vformat("physics_layer_%d/polygon_%d/points", layer_index, polygon_index)); + ADD_UNDO(tile_data, vformat("physics_layer_%d/polygon_%d/one_way", layer_index, polygon_index)); + ADD_UNDO(tile_data, vformat("physics_layer_%d/polygon_%d/one_way_margin", layer_index, polygon_index)); + } + } + } else if (p_array_prefix == "terrain_set_") { + ADD_UNDO(tile_data, "terrain_set"); + for (int terrain_set_index = begin; terrain_set_index < end; terrain_set_index++) { + for (int l = 0; l < TileSet::CELL_NEIGHBOR_MAX; l++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(l); + if (tile_data->is_valid_peering_bit_terrain(bit)) { + ADD_UNDO(tile_data, "terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[l])); + } + } + } + } else if (components.size() >= 2 && components[0].begins_with("terrain_set_") && components[0].trim_prefix("terrain_set_").is_valid_int() && components[1] == "terrain_") { + for (int terrain_index = 0; terrain_index < TileSet::CELL_NEIGHBOR_MAX; terrain_index++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(terrain_index); + if (tile_data->is_valid_peering_bit_terrain(bit)) { + ADD_UNDO(tile_data, "terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[terrain_index])); + } + } + } else if (p_array_prefix == "navigation_layer_") { + for (int layer_index = begin; layer_index < end; layer_index++) { + ADD_UNDO(tile_data, vformat("navigation_layer_%d/polygon", layer_index)); + } + } else if (p_array_prefix == "custom_data_layer_") { + for (int layer_index = begin; layer_index < end; layer_index++) { + ADD_UNDO(tile_data, vformat("custom_data_%d", layer_index)); + } + } + } + } + } + } +#undef ADD_UNDO + + // Add do method. + if (p_array_prefix == "occlusion_layer_") { + if (p_from_index < 0) { + undo_redo->add_do_method(tile_set, "add_occlusion_layer", p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_do_method(tile_set, "remove_occlusion_layer", p_from_index); + } else { + undo_redo->add_do_method(tile_set, "move_occlusion_layer", p_from_index, p_to_pos); + } + } else if (p_array_prefix == "physics_layer_") { + if (p_from_index < 0) { + undo_redo->add_do_method(tile_set, "add_physics_layer", p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_do_method(tile_set, "remove_physics_layer", p_from_index); + } else { + undo_redo->add_do_method(tile_set, "move_physics_layer", p_from_index, p_to_pos); + } + } else if (p_array_prefix == "terrain_set_") { + if (p_from_index < 0) { + undo_redo->add_do_method(tile_set, "add_terrain_set", p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_do_method(tile_set, "remove_terrain_set", p_from_index); + } else { + undo_redo->add_do_method(tile_set, "move_terrain_set", p_from_index, p_to_pos); + } + } else if (components.size() >= 2 && components[0].begins_with("terrain_set_") && components[0].trim_prefix("terrain_set_").is_valid_int() && components[1] == "terrain_") { + int terrain_set = components[0].trim_prefix("terrain_set_").to_int(); + if (p_from_index < 0) { + undo_redo->add_do_method(tile_set, "add_terrain", terrain_set, p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_do_method(tile_set, "remove_terrain", terrain_set, p_from_index); + } else { + undo_redo->add_do_method(tile_set, "move_terrain", terrain_set, p_from_index, p_to_pos); + } + } else if (p_array_prefix == "navigation_layer_") { + if (p_from_index < 0) { + undo_redo->add_do_method(tile_set, "add_navigation_layer", p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_do_method(tile_set, "remove_navigation_layer", p_from_index); + } else { + undo_redo->add_do_method(tile_set, "move_navigation_layer", p_from_index, p_to_pos); + } + } else if (p_array_prefix == "custom_data_layer_") { + if (p_from_index < 0) { + undo_redo->add_do_method(tile_set, "add_custom_data_layer", p_to_pos); + } else if (p_to_pos < 0) { + undo_redo->add_do_method(tile_set, "remove_custom_data_layer", p_from_index); + } else { + undo_redo->add_do_method(tile_set, "move_custom_data_layer", p_from_index, p_to_pos); + } + } +} + +void TileSetEditor::_undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, String p_property, Variant p_new_value) { + UndoRedo *undo_redo = Object::cast_to<UndoRedo>(p_undo_redo); + ERR_FAIL_COND(!undo_redo); + +#define ADD_UNDO(obj, property) undo_redo->add_undo_property(obj, property, obj->get(property)); + TileSet *tile_set = Object::cast_to<TileSet>(p_edited); + if (tile_set) { + Vector<String> components = p_property.split("/", true, 3); + for (int i = 0; i < tile_set->get_source_count(); i++) { + int source_id = tile_set->get_source_id(i); + + Ref<TileSetAtlasSource> tas = tile_set->get_source(source_id); + if (tas.is_valid()) { + for (int j = 0; j < tas->get_tiles_count(); j++) { + Vector2i tile_id = tas->get_tile_id(j); + for (int k = 0; k < tas->get_alternative_tiles_count(tile_id); k++) { + int alternative_id = tas->get_alternative_tile_id(tile_id, k); + TileData *tile_data = Object::cast_to<TileData>(tas->get_tile_data(tile_id, alternative_id)); + ERR_FAIL_COND(!tile_data); + + if (components.size() == 2 && components[0].begins_with("terrain_set_") && components[0].trim_prefix("terrain_set_").is_valid_int() && components[1] == "mode") { + ADD_UNDO(tile_data, "terrain_set"); + for (int l = 0; l < TileSet::CELL_NEIGHBOR_MAX; l++) { + TileSet::CellNeighbor bit = TileSet::CellNeighbor(l); + if (tile_data->is_valid_peering_bit_terrain(bit)) { + ADD_UNDO(tile_data, "terrains_peering_bit/" + String(TileSet::CELL_NEIGHBOR_ENUM_TO_TEXT[l])); + } + } + } else if (components.size() == 2 && components[0].begins_with("custom_data_layer_") && components[0].trim_prefix("custom_data_layer_").is_valid_int() && components[1] == "type") { + int custom_data_layer = components[0].trim_prefix("custom_data_layer_").is_valid_int(); + ADD_UNDO(tile_data, vformat("custom_data_%d", custom_data_layer)); + } + } + } + } + } + } +#undef ADD_UNDO +} + +void TileSetEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_can_drop_data_fw"), &TileSetEditor::_can_drop_data_fw); + ClassDB::bind_method(D_METHOD("_drop_data_fw"), &TileSetEditor::_drop_data_fw); +} + +void TileSetEditor::edit(Ref<TileSet> p_tile_set) { + if (p_tile_set == tile_set) { + return; + } + + // Remove listener. + if (tile_set.is_valid()) { + tile_set->disconnect("changed", callable_mp(this, &TileSetEditor::_tile_set_changed)); + } + + // Change the edited object. + tile_set = p_tile_set; + + // Add the listener again. + if (tile_set.is_valid()) { + tile_set->connect("changed", callable_mp(this, &TileSetEditor::_tile_set_changed)); + _update_sources_list(); + _update_patterns_list(); + } + + tile_set_atlas_source_editor->hide(); + tile_set_scenes_collection_source_editor->hide(); + no_source_selected_label->show(); +} + +TileSetEditor::TileSetEditor() { + singleton = this; + + set_process_internal(true); + + // TabBar. + tabs_bar = memnew(TabBar); + tabs_bar->set_clip_tabs(false); + tabs_bar->add_tab(TTR("Tiles")); + tabs_bar->add_tab(TTR("Patterns")); + tabs_bar->connect("tab_changed", callable_mp(this, &TileSetEditor::_tab_changed)); + + tile_set_toolbar = memnew(HBoxContainer); + tile_set_toolbar->set_h_size_flags(SIZE_EXPAND_FILL); + tile_set_toolbar->add_child(tabs_bar); + add_child(tile_set_toolbar); + + //// Tiles //// + // Split container. + split_container = memnew(HSplitContainer); + split_container->set_name(TTR("Tiles")); + split_container->set_h_size_flags(SIZE_EXPAND_FILL); + split_container->set_v_size_flags(SIZE_EXPAND_FILL); + add_child(split_container); + + // Sources list. + VBoxContainer *split_container_left_side = memnew(VBoxContainer); + split_container_left_side->set_h_size_flags(SIZE_EXPAND_FILL); + split_container_left_side->set_v_size_flags(SIZE_EXPAND_FILL); + split_container_left_side->set_stretch_ratio(0.25); + split_container_left_side->set_custom_minimum_size(Size2i(70, 0) * EDSCALE); + split_container->add_child(split_container_left_side); + + sources_list = memnew(ItemList); + sources_list->set_fixed_icon_size(Size2i(60, 60) * EDSCALE); + sources_list->set_h_size_flags(SIZE_EXPAND_FILL); + sources_list->set_v_size_flags(SIZE_EXPAND_FILL); + sources_list->connect("item_selected", callable_mp(this, &TileSetEditor::_source_selected)); + sources_list->connect("item_selected", callable_mp(TilesEditorPlugin::get_singleton(), &TilesEditorPlugin::set_sources_lists_current)); + sources_list->connect("visibility_changed", callable_mp(TilesEditorPlugin::get_singleton(), &TilesEditorPlugin::synchronize_sources_list), varray(sources_list)); + sources_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + sources_list->set_drag_forwarding(this); + split_container_left_side->add_child(sources_list); + + HBoxContainer *sources_bottom_actions = memnew(HBoxContainer); + sources_bottom_actions->set_alignment(HBoxContainer::ALIGN_END); + split_container_left_side->add_child(sources_bottom_actions); + + sources_delete_button = memnew(Button); + sources_delete_button->set_flat(true); + sources_delete_button->set_disabled(true); + sources_delete_button->connect("pressed", callable_mp(this, &TileSetEditor::_source_delete_pressed)); + sources_bottom_actions->add_child(sources_delete_button); + + sources_add_button = memnew(MenuButton); + sources_add_button->set_flat(true); + sources_add_button->get_popup()->add_item(TTR("Atlas")); + sources_add_button->get_popup()->add_item(TTR("Scenes Collection")); + sources_add_button->get_popup()->connect("id_pressed", callable_mp(this, &TileSetEditor::_source_add_id_pressed)); + sources_bottom_actions->add_child(sources_add_button); + + sources_advanced_menu_button = memnew(MenuButton); + sources_advanced_menu_button->set_flat(true); + sources_advanced_menu_button->get_popup()->add_item(TTR("Open Atlas Merging Tool")); + sources_advanced_menu_button->get_popup()->add_item(TTR("Manage Tile Proxies")); + sources_advanced_menu_button->get_popup()->connect("id_pressed", callable_mp(this, &TileSetEditor::_sources_advanced_menu_id_pressed)); + sources_bottom_actions->add_child(sources_advanced_menu_button); + + atlas_merging_dialog = memnew(AtlasMergingDialog); + add_child(atlas_merging_dialog); + + tile_proxies_manager_dialog = memnew(TileProxiesManagerDialog); + add_child(tile_proxies_manager_dialog); + + // Right side container. + VBoxContainer *split_container_right_side = memnew(VBoxContainer); + split_container_right_side->set_h_size_flags(SIZE_EXPAND_FILL); + split_container_right_side->set_v_size_flags(SIZE_EXPAND_FILL); + split_container->add_child(split_container_right_side); + + // No source selected. + no_source_selected_label = memnew(Label); + no_source_selected_label->set_text(TTR("No TileSet source selected. Select or create a TileSet source.")); + no_source_selected_label->set_h_size_flags(SIZE_EXPAND_FILL); + no_source_selected_label->set_v_size_flags(SIZE_EXPAND_FILL); + no_source_selected_label->set_align(Label::ALIGN_CENTER); + no_source_selected_label->set_valign(Label::VALIGN_CENTER); + split_container_right_side->add_child(no_source_selected_label); + + // Atlases editor. + tile_set_atlas_source_editor = memnew(TileSetAtlasSourceEditor); + tile_set_atlas_source_editor->set_h_size_flags(SIZE_EXPAND_FILL); + tile_set_atlas_source_editor->set_v_size_flags(SIZE_EXPAND_FILL); + tile_set_atlas_source_editor->connect("source_id_changed", callable_mp(this, &TileSetEditor::_update_sources_list)); + split_container_right_side->add_child(tile_set_atlas_source_editor); + tile_set_atlas_source_editor->hide(); + + // Scenes collection editor. + tile_set_scenes_collection_source_editor = memnew(TileSetScenesCollectionSourceEditor); + tile_set_scenes_collection_source_editor->set_h_size_flags(SIZE_EXPAND_FILL); + tile_set_scenes_collection_source_editor->set_v_size_flags(SIZE_EXPAND_FILL); + tile_set_scenes_collection_source_editor->connect("source_id_changed", callable_mp(this, &TileSetEditor::_update_sources_list)); + split_container_right_side->add_child(tile_set_scenes_collection_source_editor); + tile_set_scenes_collection_source_editor->hide(); + + //// Patterns //// + int thumbnail_size = 64; + patterns_item_list = memnew(ItemList); + patterns_item_list->set_max_columns(0); + patterns_item_list->set_icon_mode(ItemList::ICON_MODE_TOP); + patterns_item_list->set_fixed_column_width(thumbnail_size * 3 / 2); + patterns_item_list->set_max_text_lines(2); + patterns_item_list->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); + patterns_item_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + patterns_item_list->connect("gui_input", callable_mp(this, &TileSetEditor::_patterns_item_list_gui_input)); + add_child(patterns_item_list); + patterns_item_list->hide(); + + patterns_help_label = memnew(Label); + patterns_help_label->set_text(TTR("Add new patterns in the TileMap editing mode.")); + patterns_help_label->set_anchors_and_offsets_preset(Control::PRESET_CENTER); + patterns_item_list->add_child(patterns_help_label); + + // Registers UndoRedo inspector callback. + EditorNode::get_singleton()->get_editor_data().add_move_array_element_function(SNAME("TileSet"), callable_mp(this, &TileSetEditor::_move_tile_set_array_element)); + EditorNode::get_singleton()->get_editor_data().add_undo_redo_inspector_hook_callback(callable_mp(this, &TileSetEditor::_undo_redo_inspector_callback)); +} + +TileSetEditor::~TileSetEditor() { + if (tile_set.is_valid()) { + tile_set->disconnect("changed", callable_mp(this, &TileSetEditor::_tile_set_changed)); + } +} diff --git a/editor/plugins/tiles/tile_set_editor.h b/editor/plugins/tiles/tile_set_editor.h new file mode 100644 index 0000000000..58312ce3df --- /dev/null +++ b/editor/plugins/tiles/tile_set_editor.h @@ -0,0 +1,108 @@ +/*************************************************************************/ +/* tile_set_editor.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILE_SET_EDITOR_H +#define TILE_SET_EDITOR_H + +#include "atlas_merging_dialog.h" +#include "scene/gui/box_container.h" +#include "scene/resources/tile_set.h" +#include "tile_proxies_manager_dialog.h" +#include "tile_set_atlas_source_editor.h" +#include "tile_set_scenes_collection_source_editor.h" + +class TileSetEditor : public VBoxContainer { + GDCLASS(TileSetEditor, VBoxContainer); + + static TileSetEditor *singleton; + +private: + Ref<TileSet> tile_set; + bool tile_set_changed_needs_update = false; + HSplitContainer *split_container; + + // TabBar. + HBoxContainer *tile_set_toolbar; + TabBar *tabs_bar; + + // Tiles. + Label *no_source_selected_label; + TileSetAtlasSourceEditor *tile_set_atlas_source_editor; + TileSetScenesCollectionSourceEditor *tile_set_scenes_collection_source_editor; + + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + + void _drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); + bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; + + void _update_sources_list(int force_selected_id = -1); + + // Sources management. + Button *sources_delete_button; + MenuButton *sources_add_button; + MenuButton *sources_advanced_menu_button; + ItemList *sources_list; + Ref<Texture2D> missing_texture_texture; + void _source_selected(int p_source_index); + void _source_delete_pressed(); + void _source_add_id_pressed(int p_id_pressed); + void _sources_advanced_menu_id_pressed(int p_id_pressed); + + AtlasMergingDialog *atlas_merging_dialog; + TileProxiesManagerDialog *tile_proxies_manager_dialog; + + // Patterns. + ItemList *patterns_item_list; + Label *patterns_help_label; + void _patterns_item_list_gui_input(const Ref<InputEvent> &p_event); + void _pattern_preview_done(Ref<TileMapPattern> p_pattern, Ref<Texture2D> p_texture); + bool select_last_pattern = false; + void _update_patterns_list(); + + void _tile_set_changed(); + void _tab_changed(int p_tab_changed); + + void _move_tile_set_array_element(Object *p_undo_redo, Object *p_edited, String p_array_prefix, int p_from_index, int p_to_pos); + void _undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, String p_property, Variant p_new_value); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + _FORCE_INLINE_ static TileSetEditor *get_singleton() { return singleton; } + + void edit(Ref<TileSet> p_tile_set); + + TileSetEditor(); + ~TileSetEditor(); +}; + +#endif // TILE_SET_EDITOR_PLUGIN_H diff --git a/editor/plugins/tiles/tile_set_scenes_collection_source_editor.cpp b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.cpp new file mode 100644 index 0000000000..d687d9651d --- /dev/null +++ b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.cpp @@ -0,0 +1,533 @@ +/*************************************************************************/ +/* tile_set_scenes_collection_source_editor.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tile_set_scenes_collection_source_editor.h" + +#include "editor/editor_resource_preview.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +#include "scene/gui/item_list.h" + +#include "core/core_string_names.h" + +void TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::set_id(int p_id) { + ERR_FAIL_COND(p_id < 0); + if (source_id == p_id) { + return; + } + ERR_FAIL_COND_MSG(tile_set->has_source(p_id), vformat("Cannot change TileSet Scenes Collection source ID. Another TileSet source exists with id %d.", p_id)); + + int previous_source = source_id; + source_id = p_id; // source_id must be updated before, because it's used by the source list update. + tile_set->set_source_id(previous_source, p_id); + emit_signal(SNAME("changed"), "id"); +} + +int TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::get_id() { + return source_id; +} + +bool TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_set(const StringName &p_name, const Variant &p_value) { + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } + bool valid = false; + tile_set_scenes_collection_source->set(name, p_value, &valid); + if (valid) { + emit_signal(SNAME("changed"), String(name).utf8().get_data()); + } + return valid; +} + +bool TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_get(const StringName &p_name, Variant &r_ret) const { + if (!tile_set_scenes_collection_source) { + return false; + } + String name = p_name; + if (name == "name") { + // Use the resource_name property to store the source's name. + name = "resource_name"; + } + bool valid = false; + r_ret = tile_set_scenes_collection_source->get(name, &valid); + return valid; +} + +void TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_get_property_list(List<PropertyInfo> *p_list) const { + p_list->push_back(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE, "")); +} + +void TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::_bind_methods() { + // -- Shape and layout -- + ClassDB::bind_method(D_METHOD("set_id", "id"), &TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::set_id); + ClassDB::bind_method(D_METHOD("get_id"), &TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::get_id); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "id"), "set_id", "get_id"); + + ADD_SIGNAL(MethodInfo("changed", PropertyInfo(Variant::STRING, "what"))); +} + +void TileSetScenesCollectionSourceEditor::TileSetScenesCollectionProxyObject::edit(Ref<TileSet> p_tile_set, TileSetScenesCollectionSource *p_tile_set_scenes_collection_source, int p_source_id) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + ERR_FAIL_COND(!p_tile_set_scenes_collection_source); + ERR_FAIL_COND(p_source_id < 0); + ERR_FAIL_COND(p_tile_set->get_source(p_source_id) != p_tile_set_scenes_collection_source); + + if (tile_set == p_tile_set && tile_set_scenes_collection_source == p_tile_set_scenes_collection_source && source_id == p_source_id) { + return; + } + + // Disconnect to changes. + if (tile_set_scenes_collection_source) { + tile_set_scenes_collection_source->disconnect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); + } + + tile_set = p_tile_set; + tile_set_scenes_collection_source = p_tile_set_scenes_collection_source; + source_id = p_source_id; + + // Connect to changes. + if (tile_set_scenes_collection_source) { + if (!tile_set_scenes_collection_source->is_connected(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed))) { + tile_set_scenes_collection_source->connect(CoreStringNames::get_singleton()->property_list_changed, callable_mp((Object *)this, &Object::notify_property_list_changed)); + } + } + + notify_property_list_changed(); +} + +// -- Proxy object used by the tile inspector -- +bool TileSetScenesCollectionSourceEditor::SceneTileProxyObject::_set(const StringName &p_name, const Variant &p_value) { + if (!tile_set_scenes_collection_source) { + return false; + } + + if (p_name == "id") { + int as_int = int(p_value); + ERR_FAIL_COND_V(as_int < 0, false); + ERR_FAIL_COND_V(tile_set_scenes_collection_source->has_scene_tile_id(as_int), false); + tile_set_scenes_collection_source->set_scene_tile_id(scene_id, as_int); + scene_id = as_int; + emit_signal(SNAME("changed"), "id"); + for (int i = 0; i < tile_set_scenes_collection_source_editor->scene_tiles_list->get_item_count(); i++) { + if (int(tile_set_scenes_collection_source_editor->scene_tiles_list->get_item_metadata(i)) == scene_id) { + tile_set_scenes_collection_source_editor->scene_tiles_list->select(i); + break; + } + } + return true; + } else if (p_name == "scene") { + tile_set_scenes_collection_source->set_scene_tile_scene(scene_id, p_value); + emit_signal(SNAME("changed"), "scene"); + return true; + } else if (p_name == "display_placeholder") { + tile_set_scenes_collection_source->set_scene_tile_display_placeholder(scene_id, p_value); + emit_signal(SNAME("changed"), "display_placeholder"); + return true; + } + + return false; +} + +bool TileSetScenesCollectionSourceEditor::SceneTileProxyObject::_get(const StringName &p_name, Variant &r_ret) const { + if (!tile_set_scenes_collection_source) { + return false; + } + + if (p_name == "id") { + r_ret = scene_id; + return true; + } else if (p_name == "scene") { + r_ret = tile_set_scenes_collection_source->get_scene_tile_scene(scene_id); + return true; + } else if (p_name == "display_placeholder") { + r_ret = tile_set_scenes_collection_source->get_scene_tile_display_placeholder(scene_id); + return true; + } + + return false; +} + +void TileSetScenesCollectionSourceEditor::SceneTileProxyObject::_get_property_list(List<PropertyInfo> *p_list) const { + if (!tile_set_scenes_collection_source) { + return; + } + + p_list->push_back(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_NONE, "")); + p_list->push_back(PropertyInfo(Variant::OBJECT, "scene", PROPERTY_HINT_RESOURCE_TYPE, "PackedScene")); + p_list->push_back(PropertyInfo(Variant::BOOL, "display_placeholder", PROPERTY_HINT_NONE, "")); +} + +void TileSetScenesCollectionSourceEditor::SceneTileProxyObject::edit(TileSetScenesCollectionSource *p_tile_set_scenes_collection_source, int p_scene_id) { + ERR_FAIL_COND(!p_tile_set_scenes_collection_source); + ERR_FAIL_COND(!p_tile_set_scenes_collection_source->has_scene_tile_id(p_scene_id)); + + if (tile_set_scenes_collection_source == p_tile_set_scenes_collection_source && scene_id == p_scene_id) { + return; + } + + tile_set_scenes_collection_source = p_tile_set_scenes_collection_source; + scene_id = p_scene_id; + + notify_property_list_changed(); +} + +void TileSetScenesCollectionSourceEditor::SceneTileProxyObject::_bind_methods() { + ADD_SIGNAL(MethodInfo("changed", PropertyInfo(Variant::STRING, "what"))); +} + +void TileSetScenesCollectionSourceEditor::_scenes_collection_source_proxy_object_changed(String p_what) { + if (p_what == "id") { + emit_signal(SNAME("source_id_changed"), scenes_collection_source_proxy_object->get_id()); + } +} + +void TileSetScenesCollectionSourceEditor::_tile_set_scenes_collection_source_changed() { + tile_set_scenes_collection_source_changed_needs_update = true; +} + +void TileSetScenesCollectionSourceEditor::_scene_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, Variant p_ud) { + int index = p_ud; + + if (index >= 0 && index < scene_tiles_list->get_item_count()) { + scene_tiles_list->set_item_icon(index, p_preview); + } +} + +void TileSetScenesCollectionSourceEditor::_scenes_list_item_activated(int p_index) { + Ref<PackedScene> packed_scene = tile_set_scenes_collection_source->get_scene_tile_scene(scene_tiles_list->get_item_metadata(p_index)); + if (packed_scene.is_valid()) { + EditorNode::get_singleton()->open_request(packed_scene->get_path()); + } +} + +void TileSetScenesCollectionSourceEditor::_source_add_pressed() { + int scene_id = tile_set_scenes_collection_source->get_next_scene_tile_id(); + undo_redo->create_action(TTR("Add a Scene Tile")); + undo_redo->add_do_method(tile_set_scenes_collection_source, "create_scene_tile", Ref<PackedScene>(), scene_id); + undo_redo->add_undo_method(tile_set_scenes_collection_source, "remove_scene_tile", scene_id); + undo_redo->commit_action(); + _update_scenes_list(); + _update_action_buttons(); + _update_tile_inspector(); +} + +void TileSetScenesCollectionSourceEditor::_source_delete_pressed() { + Vector<int> selected_indices = scene_tiles_list->get_selected_items(); + ERR_FAIL_COND(selected_indices.size() <= 0); + int scene_id = scene_tiles_list->get_item_metadata(selected_indices[0]); + + undo_redo->create_action(TTR("Remove a Scene Tile")); + undo_redo->add_do_method(tile_set_scenes_collection_source, "remove_scene_tile", scene_id); + undo_redo->add_undo_method(tile_set_scenes_collection_source, "create_scene_tile", tile_set_scenes_collection_source->get_scene_tile_scene(scene_id), scene_id); + undo_redo->commit_action(); + _update_scenes_list(); + _update_action_buttons(); + _update_tile_inspector(); +} + +void TileSetScenesCollectionSourceEditor::_update_source_inspector() { + // Update the proxy object. + scenes_collection_source_proxy_object->edit(tile_set, tile_set_scenes_collection_source, tile_set_source_id); +} + +void TileSetScenesCollectionSourceEditor::_update_tile_inspector() { + Vector<int> selected_indices = scene_tiles_list->get_selected_items(); + bool has_atlas_tile_selected = (selected_indices.size() > 0); + + // Update the proxy object. + if (has_atlas_tile_selected) { + int scene_id = scene_tiles_list->get_item_metadata(selected_indices[0]); + tile_proxy_object->edit(tile_set_scenes_collection_source, scene_id); + } + + // Update visibility. + tile_inspector_label->set_visible(has_atlas_tile_selected); + tile_inspector->set_visible(has_atlas_tile_selected); +} + +void TileSetScenesCollectionSourceEditor::_update_action_buttons() { + Vector<int> selected_indices = scene_tiles_list->get_selected_items(); + scene_tile_delete_button->set_disabled(selected_indices.size() <= 0); +} + +void TileSetScenesCollectionSourceEditor::_update_scenes_list() { + if (!tile_set_scenes_collection_source) { + return; + } + + // Get the previously selected id. + Vector<int> selected_indices = scene_tiles_list->get_selected_items(); + int old_selected_scene_id = (selected_indices.size() > 0) ? int(scene_tiles_list->get_item_metadata(selected_indices[0])) : -1; + + // Clear the list. + scene_tiles_list->clear(); + + // Rebuild the list. + int to_reselect = -1; + for (int i = 0; i < tile_set_scenes_collection_source->get_scene_tiles_count(); i++) { + int scene_id = tile_set_scenes_collection_source->get_scene_tile_id(i); + + Ref<PackedScene> scene = tile_set_scenes_collection_source->get_scene_tile_scene(scene_id); + + int item_index = 0; + if (scene.is_valid()) { + item_index = scene_tiles_list->add_item(vformat("%s (path:%s id:%d)", scene->get_path().get_file().get_basename(), scene->get_path(), scene_id)); + Variant udata = i; + EditorResourcePreview::get_singleton()->queue_edited_resource_preview(scene, this, "_scene_thumbnail_done", udata); + } else { + item_index = scene_tiles_list->add_item(TTR("Tile with Invalid Scene"), get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons"))); + } + scene_tiles_list->set_item_metadata(item_index, scene_id); + + if (old_selected_scene_id >= 0 && scene_id == old_selected_scene_id) { + to_reselect = i; + } + } + + // Reselect if needed. + if (to_reselect >= 0) { + scene_tiles_list->select(to_reselect); + } + + // Icon size update. + int int_size = int(EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size")) * EDSCALE; + scene_tiles_list->set_fixed_icon_size(Vector2(int_size, int_size)); +} + +void TileSetScenesCollectionSourceEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: + scene_tile_add_button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons"))); + scene_tile_delete_button->set_icon(get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"))); + _update_scenes_list(); + break; + case NOTIFICATION_INTERNAL_PROCESS: + if (tile_set_scenes_collection_source_changed_needs_update) { + // Update everything. + _update_source_inspector(); + _update_scenes_list(); + _update_action_buttons(); + _update_tile_inspector(); + tile_set_scenes_collection_source_changed_needs_update = false; + } + break; + case NOTIFICATION_VISIBILITY_CHANGED: + // Update things just in case. + _update_scenes_list(); + _update_action_buttons(); + break; + default: + break; + } +} + +void TileSetScenesCollectionSourceEditor::edit(Ref<TileSet> p_tile_set, TileSetScenesCollectionSource *p_tile_set_scenes_collection_source, int p_source_id) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + ERR_FAIL_COND(!p_tile_set_scenes_collection_source); + ERR_FAIL_COND(p_source_id < 0); + ERR_FAIL_COND(p_tile_set->get_source(p_source_id) != p_tile_set_scenes_collection_source); + + if (p_tile_set == tile_set && p_tile_set_scenes_collection_source == tile_set_scenes_collection_source && p_source_id == tile_set_source_id) { + return; + } + + // Remove listener for old objects. + if (tile_set_scenes_collection_source) { + tile_set_scenes_collection_source->disconnect("changed", callable_mp(this, &TileSetScenesCollectionSourceEditor::_tile_set_scenes_collection_source_changed)); + } + + // Change the edited object. + tile_set = p_tile_set; + tile_set_scenes_collection_source = p_tile_set_scenes_collection_source; + tile_set_source_id = p_source_id; + + // Add the listener again. + if (tile_set_scenes_collection_source) { + tile_set_scenes_collection_source->connect("changed", callable_mp(this, &TileSetScenesCollectionSourceEditor::_tile_set_scenes_collection_source_changed)); + } + + // Update everything. + _update_source_inspector(); + _update_scenes_list(); + _update_action_buttons(); + _update_tile_inspector(); +} + +void TileSetScenesCollectionSourceEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { + if (!_can_drop_data_fw(p_point, p_data, p_from)) { + return; + } + + if (p_from == scene_tiles_list) { + // Handle dropping a texture in the list of atlas resources. + int scene_id = -1; + Dictionary d = p_data; + Vector<String> files = d["files"]; + for (int i = 0; i < files.size(); i++) { + Ref<PackedScene> resource = ResourceLoader::load(files[i]); + if (resource.is_valid()) { + scene_id = tile_set_scenes_collection_source->get_next_scene_tile_id(); + undo_redo->create_action(TTR("Add a Scene Tile")); + undo_redo->add_do_method(tile_set_scenes_collection_source, "create_scene_tile", resource, scene_id); + undo_redo->add_undo_method(tile_set_scenes_collection_source, "remove_scene_tile", scene_id); + undo_redo->commit_action(); + } + } + + _update_scenes_list(); + _update_action_buttons(); + _update_tile_inspector(); + } +} + +bool TileSetScenesCollectionSourceEditor::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { + if (p_from == scene_tiles_list) { + Dictionary d = p_data; + + if (!d.has("type")) { + return false; + } + + // Check if we have a Texture2D. + if (String(d["type"]) == "files") { + Vector<String> files = d["files"]; + + if (files.size() == 0) { + return false; + } + + for (int i = 0; i < files.size(); i++) { + String file = files[i]; + String ftype = EditorFileSystem::get_singleton()->get_file_type(file); + + if (!ClassDB::is_parent_class(ftype, "PackedScene")) { + return false; + } + } + + return true; + } + } + return false; +} + +void TileSetScenesCollectionSourceEditor::_bind_methods() { + ADD_SIGNAL(MethodInfo("source_id_changed", PropertyInfo(Variant::INT, "source_id"))); + + ClassDB::bind_method(D_METHOD("_scene_thumbnail_done"), &TileSetScenesCollectionSourceEditor::_scene_thumbnail_done); + ClassDB::bind_method(D_METHOD("_can_drop_data_fw"), &TileSetScenesCollectionSourceEditor::_can_drop_data_fw); + ClassDB::bind_method(D_METHOD("_drop_data_fw"), &TileSetScenesCollectionSourceEditor::_drop_data_fw); +} + +TileSetScenesCollectionSourceEditor::TileSetScenesCollectionSourceEditor() { + // -- Right side -- + HSplitContainer *split_container_right_side = memnew(HSplitContainer); + split_container_right_side->set_h_size_flags(SIZE_EXPAND_FILL); + add_child(split_container_right_side); + + // Middle panel. + ScrollContainer *middle_panel = memnew(ScrollContainer); + middle_panel->set_enable_h_scroll(false); + middle_panel->set_custom_minimum_size(Size2i(200, 0) * EDSCALE); + split_container_right_side->add_child(middle_panel); + + VBoxContainer *middle_vbox_container = memnew(VBoxContainer); + middle_vbox_container->set_h_size_flags(SIZE_EXPAND_FILL); + middle_panel->add_child(middle_vbox_container); + + // Scenes collection source inspector. + scenes_collection_source_inspector_label = memnew(Label); + scenes_collection_source_inspector_label->set_text(TTR("Scenes collection properties:")); + middle_vbox_container->add_child(scenes_collection_source_inspector_label); + + scenes_collection_source_proxy_object = memnew(TileSetScenesCollectionProxyObject()); + scenes_collection_source_proxy_object->connect("changed", callable_mp(this, &TileSetScenesCollectionSourceEditor::_scenes_collection_source_proxy_object_changed)); + + scenes_collection_source_inspector = memnew(EditorInspector); + scenes_collection_source_inspector->set_undo_redo(undo_redo); + scenes_collection_source_inspector->set_enable_v_scroll(false); + scenes_collection_source_inspector->edit(scenes_collection_source_proxy_object); + middle_vbox_container->add_child(scenes_collection_source_inspector); + + // Tile inspector. + tile_inspector_label = memnew(Label); + tile_inspector_label->set_text(TTR("Tile properties:")); + tile_inspector_label->hide(); + middle_vbox_container->add_child(tile_inspector_label); + + tile_proxy_object = memnew(SceneTileProxyObject(this)); + tile_proxy_object->connect("changed", callable_mp(this, &TileSetScenesCollectionSourceEditor::_update_scenes_list).unbind(1)); + tile_proxy_object->connect("changed", callable_mp(this, &TileSetScenesCollectionSourceEditor::_update_action_buttons).unbind(1)); + + tile_inspector = memnew(EditorInspector); + tile_inspector->set_undo_redo(undo_redo); + tile_inspector->set_enable_v_scroll(false); + tile_inspector->edit(tile_proxy_object); + tile_inspector->set_use_folding(true); + middle_vbox_container->add_child(tile_inspector); + + // Scenes list. + VBoxContainer *right_vbox_container = memnew(VBoxContainer); + split_container_right_side->add_child(right_vbox_container); + + scene_tiles_list = memnew(ItemList); + scene_tiles_list->set_h_size_flags(SIZE_EXPAND_FILL); + scene_tiles_list->set_v_size_flags(SIZE_EXPAND_FILL); + scene_tiles_list->set_drag_forwarding(this); + scene_tiles_list->connect("item_selected", callable_mp(this, &TileSetScenesCollectionSourceEditor::_update_tile_inspector).unbind(1)); + scene_tiles_list->connect("item_selected", callable_mp(this, &TileSetScenesCollectionSourceEditor::_update_action_buttons).unbind(1)); + scene_tiles_list->connect("item_activated", callable_mp(this, &TileSetScenesCollectionSourceEditor::_scenes_list_item_activated)); + scene_tiles_list->set_texture_filter(CanvasItem::TEXTURE_FILTER_NEAREST); + right_vbox_container->add_child(scene_tiles_list); + + HBoxContainer *scenes_bottom_actions = memnew(HBoxContainer); + right_vbox_container->add_child(scenes_bottom_actions); + + scene_tile_add_button = memnew(Button); + scene_tile_add_button->set_flat(true); + scene_tile_add_button->connect("pressed", callable_mp(this, &TileSetScenesCollectionSourceEditor::_source_add_pressed)); + scenes_bottom_actions->add_child(scene_tile_add_button); + + scene_tile_delete_button = memnew(Button); + scene_tile_delete_button->set_flat(true); + scene_tile_delete_button->set_disabled(true); + scene_tile_delete_button->connect("pressed", callable_mp(this, &TileSetScenesCollectionSourceEditor::_source_delete_pressed)); + scenes_bottom_actions->add_child(scene_tile_delete_button); +} + +TileSetScenesCollectionSourceEditor::~TileSetScenesCollectionSourceEditor() { + memdelete(scenes_collection_source_proxy_object); + memdelete(tile_proxy_object); +} diff --git a/editor/plugins/tiles/tile_set_scenes_collection_source_editor.h b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.h new file mode 100644 index 0000000000..4e33128be5 --- /dev/null +++ b/editor/plugins/tiles/tile_set_scenes_collection_source_editor.h @@ -0,0 +1,141 @@ +/*************************************************************************/ +/* tile_set_scenes_collection_source_editor.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILE_SET_SCENES_COLLECTION_SOURCE_EDITOR_H +#define TILE_SET_SCENES_COLLECTION_SOURCE_EDITOR_H + +#include "editor/editor_node.h" +#include "scene/gui/box_container.h" +#include "scene/resources/tile_set.h" + +class TileSetScenesCollectionSourceEditor : public HBoxContainer { + GDCLASS(TileSetScenesCollectionSourceEditor, HBoxContainer); + +private: + // -- Proxy object for an atlas source, needed by the inspector -- + class TileSetScenesCollectionProxyObject : public Object { + GDCLASS(TileSetScenesCollectionProxyObject, Object); + + private: + Ref<TileSet> tile_set; + TileSetScenesCollectionSource *tile_set_scenes_collection_source = nullptr; + int source_id = -1; + + protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + static void _bind_methods(); + + public: + void set_id(int p_id); + int get_id(); + + void edit(Ref<TileSet> p_tile_set, TileSetScenesCollectionSource *p_tile_set_scenes_collection_source, int p_source_id); + }; + + // -- Proxy object for a tile, needed by the inspector -- + class SceneTileProxyObject : public Object { + GDCLASS(SceneTileProxyObject, Object); + + private: + TileSetScenesCollectionSourceEditor *tile_set_scenes_collection_source_editor; + + TileSetScenesCollectionSource *tile_set_scenes_collection_source = nullptr; + int source_id; + int scene_id; + + protected: + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + + static void _bind_methods(); + + public: + // Update the proxyed object. + void edit(TileSetScenesCollectionSource *p_tile_set_atlas_source, int p_scene_id); + + SceneTileProxyObject(TileSetScenesCollectionSourceEditor *p_tiles_set_scenes_collection_source_editor) { + tile_set_scenes_collection_source_editor = p_tiles_set_scenes_collection_source_editor; + } + }; + +private: + Ref<TileSet> tile_set; + TileSetScenesCollectionSource *tile_set_scenes_collection_source = nullptr; + int tile_set_source_id = -1; + + UndoRedo *undo_redo = EditorNode::get_undo_redo(); + + bool tile_set_scenes_collection_source_changed_needs_update = false; + + // Source inspector. + TileSetScenesCollectionProxyObject *scenes_collection_source_proxy_object; + Label *scenes_collection_source_inspector_label; + EditorInspector *scenes_collection_source_inspector; + + // Tile inspector. + SceneTileProxyObject *tile_proxy_object; + Label *tile_inspector_label; + EditorInspector *tile_inspector; + + ItemList *scene_tiles_list; + Button *scene_tile_add_button; + Button *scene_tile_delete_button; + + void _tile_set_scenes_collection_source_changed(); + void _scenes_collection_source_proxy_object_changed(String p_what); + void _scene_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, Variant p_ud); + void _scenes_list_item_activated(int p_index); + + void _source_add_pressed(); + void _source_delete_pressed(); + + // Update methods. + void _update_source_inspector(); + void _update_tile_inspector(); + void _update_scenes_list(); + void _update_action_buttons(); + + void _drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); + bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void edit(Ref<TileSet> p_tile_set, TileSetScenesCollectionSource *p_tile_set_scenes_collection_source, int p_source_id); + TileSetScenesCollectionSourceEditor(); + ~TileSetScenesCollectionSourceEditor(); +}; + +#endif diff --git a/editor/plugins/tiles/tiles_editor_plugin.cpp b/editor/plugins/tiles/tiles_editor_plugin.cpp new file mode 100644 index 0000000000..47dfc57b0f --- /dev/null +++ b/editor/plugins/tiles/tiles_editor_plugin.cpp @@ -0,0 +1,323 @@ +/*************************************************************************/ +/* tiles_editor_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "tiles_editor_plugin.h" + +#include "core/os/mutex.h" + +#include "editor/editor_node.h" +#include "editor/editor_scale.h" +#include "editor/plugins/canvas_item_editor_plugin.h" + +#include "scene/2d/tile_map.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/control.h" +#include "scene/gui/separator.h" +#include "scene/resources/tile_set.h" + +#include "tile_set_editor.h" + +TilesEditorPlugin *TilesEditorPlugin::singleton = nullptr; + +void TilesEditorPlugin::_preview_frame_started() { + RS::get_singleton()->request_frame_drawn_callback(callable_mp(const_cast<TilesEditorPlugin *>(this), &TilesEditorPlugin::_pattern_preview_done)); +} + +void TilesEditorPlugin::_pattern_preview_done() { + pattern_preview_done.post(); +} + +void TilesEditorPlugin::_thread_func(void *ud) { + TilesEditorPlugin *te = (TilesEditorPlugin *)ud; + te->_thread(); +} + +void TilesEditorPlugin::_thread() { + pattern_thread_exited.clear(); + while (!pattern_thread_exit.is_set()) { + pattern_preview_sem.wait(); + + pattern_preview_mutex.lock(); + if (pattern_preview_queue.size()) { + QueueItem item = pattern_preview_queue.front()->get(); + pattern_preview_queue.pop_front(); + pattern_preview_mutex.unlock(); + + int thumbnail_size = EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size"); + thumbnail_size *= EDSCALE; + Vector2 thumbnail_size2 = Vector2(thumbnail_size, thumbnail_size); + + if (item.pattern.is_valid() && !item.pattern->is_empty()) { + // Generate the pattern preview + SubViewport *viewport = memnew(SubViewport); + viewport->set_size(thumbnail_size2); + viewport->set_disable_input(true); + viewport->set_transparent_background(true); + viewport->set_update_mode(SubViewport::UPDATE_ONCE); + + TileMap *tile_map = memnew(TileMap); + tile_map->set_tileset(item.tile_set); + tile_map->set_pattern(0, Vector2(), item.pattern); + viewport->add_child(tile_map); + + TypedArray<Vector2i> used_cells = tile_map->get_used_cells(0); + + Rect2 encompassing_rect = Rect2(); + encompassing_rect.set_position(tile_map->map_to_world(used_cells[0])); + for (int i = 0; i < used_cells.size(); i++) { + Vector2i cell = used_cells[i]; + Vector2 world_pos = tile_map->map_to_world(cell); + encompassing_rect.expand_to(world_pos); + + // Texture. + Ref<TileSetAtlasSource> atlas_source = tile_set->get_source(tile_map->get_cell_source_id(0, cell)); + if (atlas_source.is_valid()) { + Vector2i coords = tile_map->get_cell_atlas_coords(0, cell); + int alternative = tile_map->get_cell_alternative_tile(0, cell); + + Vector2 center = world_pos - atlas_source->get_tile_effective_texture_offset(coords, alternative); + encompassing_rect.expand_to(center - atlas_source->get_tile_texture_region(coords).size / 2); + encompassing_rect.expand_to(center + atlas_source->get_tile_texture_region(coords).size / 2); + } + } + + Vector2 scale = thumbnail_size2 / MAX(encompassing_rect.size.x, encompassing_rect.size.y); + tile_map->set_scale(scale); + tile_map->set_position(-(scale * encompassing_rect.get_center()) + thumbnail_size2 / 2); + + // Add the viewport at the lasst moment to avoid rendering too early. + EditorNode::get_singleton()->add_child(viewport); + + RS::get_singleton()->connect(SNAME("frame_pre_draw"), callable_mp(const_cast<TilesEditorPlugin *>(this), &TilesEditorPlugin::_preview_frame_started), Vector<Variant>(), Object::CONNECT_ONESHOT); + + pattern_preview_done.wait(); + + Ref<Image> image = viewport->get_texture()->get_image(); + Ref<ImageTexture> image_texture; + image_texture.instantiate(); + image_texture->create_from_image(image); + + // Find the index for the given pattern. TODO: optimize. + Variant args[] = { item.pattern, image_texture }; + const Variant *args_ptr[] = { &args[0], &args[1] }; + Variant r; + Callable::CallError error; + item.callback.call(args_ptr, 2, r, error); + + viewport->queue_delete(); + } else { + pattern_preview_mutex.unlock(); + } + } + } + pattern_thread_exited.set(); +} + +void TilesEditorPlugin::_tile_map_changed() { + tile_map_changed_needs_update = true; +} + +void TilesEditorPlugin::_update_editors() { + // If tile_map is not edited, we change the edited only if we are not editing a tile_set. + tileset_editor->edit(tile_set); + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (tile_map) { + tilemap_editor->edit(tile_map); + } else { + tilemap_editor->edit(nullptr); + } + + // Update the viewport. + CanvasItemEditor::get_singleton()->update_viewport(); +} + +void TilesEditorPlugin::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_INTERNAL_PROCESS: { + if (tile_map_changed_needs_update) { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (tile_map) { + tile_set = tile_map->get_tileset(); + } + _update_editors(); + tile_map_changed_needs_update = false; + } + } break; + } +} + +void TilesEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + // Disable and hide invalid editors. + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + tileset_editor_button->set_visible(tile_set.is_valid()); + tilemap_editor_button->set_visible(tile_map); + if (tile_map) { + editor_node->make_bottom_panel_item_visible(tilemap_editor); + } else { + editor_node->make_bottom_panel_item_visible(tileset_editor); + } + + } else { + tileset_editor_button->hide(); + tilemap_editor_button->hide(); + editor_node->hide_bottom_panel(); + } +} + +void TilesEditorPlugin::queue_pattern_preview(Ref<TileSet> p_tile_set, Ref<TileMapPattern> p_pattern, Callable p_callback) { + ERR_FAIL_COND(!p_tile_set.is_valid()); + ERR_FAIL_COND(!p_pattern.is_valid()); + { + MutexLock lock(pattern_preview_mutex); + pattern_preview_queue.push_back({ p_tile_set, p_pattern, p_callback }); + } + pattern_preview_sem.post(); +} + +void TilesEditorPlugin::set_sources_lists_current(int p_current) { + atlas_sources_lists_current = p_current; +} + +void TilesEditorPlugin::synchronize_sources_list(Object *p_current) { + ItemList *item_list = Object::cast_to<ItemList>(p_current); + ERR_FAIL_COND(!item_list); + + if (item_list->is_visible_in_tree()) { + if (atlas_sources_lists_current < 0 || atlas_sources_lists_current >= item_list->get_item_count()) { + item_list->deselect_all(); + } else { + item_list->set_current(atlas_sources_lists_current); + item_list->emit_signal(SNAME("item_selected"), atlas_sources_lists_current); + } + } +} + +void TilesEditorPlugin::set_atlas_view_transform(float p_zoom, Vector2 p_scroll) { + atlas_view_zoom = p_zoom; + atlas_view_scroll = p_scroll; +} + +void TilesEditorPlugin::synchronize_atlas_view(Object *p_current) { + TileAtlasView *tile_atlas_view = Object::cast_to<TileAtlasView>(p_current); + ERR_FAIL_COND(!tile_atlas_view); + + if (tile_atlas_view->is_visible_in_tree()) { + tile_atlas_view->set_transform(atlas_view_zoom, atlas_view_scroll); + } +} + +void TilesEditorPlugin::edit(Object *p_object) { + // Disconnect to changes. + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (tile_map) { + tile_map->disconnect("changed", callable_mp(this, &TilesEditorPlugin::_tile_map_changed)); + } + + // Update edited objects. + tile_set = Ref<TileSet>(); + if (p_object) { + if (p_object->is_class("TileMap")) { + tile_map_id = p_object->get_instance_id(); + tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + tile_set = tile_map->get_tileset(); + editor_node->make_bottom_panel_item_visible(tilemap_editor); + } else if (p_object->is_class("TileSet")) { + tile_set = Ref<TileSet>(p_object); + if (tile_map) { + if (tile_map->get_tileset() != tile_set || !tile_map->is_inside_tree()) { + tile_map = nullptr; + tile_map_id = ObjectID(); + } + } + editor_node->make_bottom_panel_item_visible(tileset_editor); + } + } + + // Update the editors. + _update_editors(); + + // Add change listener. + if (tile_map) { + tile_map->connect("changed", callable_mp(this, &TilesEditorPlugin::_tile_map_changed)); + } +} + +bool TilesEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("TileMap") || p_object->is_class("TileSet"); +} + +TilesEditorPlugin::TilesEditorPlugin(EditorNode *p_node) { + set_process_internal(true); + + // Update the singleton. + singleton = this; + + editor_node = p_node; + + // Tileset editor. + tileset_editor = memnew(TileSetEditor); + tileset_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + tileset_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL); + tileset_editor->set_custom_minimum_size(Size2(0, 200) * EDSCALE); + tileset_editor->hide(); + + // Tilemap editor. + tilemap_editor = memnew(TileMapEditor); + tilemap_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); + tilemap_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL); + tilemap_editor->set_custom_minimum_size(Size2(0, 200) * EDSCALE); + tilemap_editor->hide(); + + // Pattern preview generation thread. + pattern_preview_thread.start(_thread_func, this); + + // Bottom buttons. + tileset_editor_button = p_node->add_bottom_panel_item(TTR("TileSet"), tileset_editor); + tileset_editor_button->hide(); + tilemap_editor_button = p_node->add_bottom_panel_item(TTR("TileMap"), tilemap_editor); + tilemap_editor_button->hide(); + + // Initialization. + _update_editors(); +} + +TilesEditorPlugin::~TilesEditorPlugin() { + if (pattern_preview_thread.is_started()) { + pattern_thread_exit.set(); + pattern_preview_sem.post(); + while (!pattern_thread_exited.is_set()) { + OS::get_singleton()->delay_usec(10000); + RenderingServer::get_singleton()->sync(); //sync pending stuff, as thread may be blocked on visual server + } + pattern_preview_thread.wait_to_finish(); + } +} diff --git a/editor/plugins/tiles/tiles_editor_plugin.h b/editor/plugins/tiles/tiles_editor_plugin.h new file mode 100644 index 0000000000..33493040f6 --- /dev/null +++ b/editor/plugins/tiles/tiles_editor_plugin.h @@ -0,0 +1,113 @@ +/*************************************************************************/ +/* tiles_editor_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TILES_EDITOR_PLUGIN_H +#define TILES_EDITOR_PLUGIN_H + +#include "editor/editor_plugin.h" +#include "scene/gui/box_container.h" + +#include "tile_atlas_view.h" +#include "tile_map_editor.h" +#include "tile_set_editor.h" + +class TilesEditorPlugin : public EditorPlugin { + GDCLASS(TilesEditorPlugin, EditorPlugin); + + static TilesEditorPlugin *singleton; + +private: + EditorNode *editor_node; + + bool tile_map_changed_needs_update = false; + ObjectID tile_map_id; + Ref<TileSet> tile_set; + + Button *tilemap_editor_button; + TileMapEditor *tilemap_editor; + + Button *tileset_editor_button; + TileSetEditor *tileset_editor; + + void _update_editors(); + + // For synchronization. + int atlas_sources_lists_current = 0; + float atlas_view_zoom = 1.0; + Vector2 atlas_view_scroll = Vector2(); + + void _tile_map_changed(); + + // Patterns preview generation. + struct QueueItem { + Ref<TileSet> tile_set; + Ref<TileMapPattern> pattern; + Callable callback; + }; + List<QueueItem> pattern_preview_queue; + Mutex pattern_preview_mutex; + Semaphore pattern_preview_sem; + Thread pattern_preview_thread; + SafeFlag pattern_thread_exit; + SafeFlag pattern_thread_exited; + Semaphore pattern_preview_done; + void _preview_frame_started(); + void _pattern_preview_done(); + static void _thread_func(void *ud); + void _thread(); + +protected: + void _notification(int p_what); + +public: + _FORCE_INLINE_ static TilesEditorPlugin *get_singleton() { return singleton; } + + virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) override { return tilemap_editor->forward_canvas_gui_input(p_event); } + virtual void forward_canvas_draw_over_viewport(Control *p_overlay) override { tilemap_editor->forward_canvas_draw_over_viewport(p_overlay); } + + // Pattern preview API. + void queue_pattern_preview(Ref<TileSet> p_tile_set, Ref<TileMapPattern> p_pattern, Callable p_callback); + + // To synchronize the atlas sources lists. + void set_sources_lists_current(int p_current); + void synchronize_sources_list(Object *p_current); + + void set_atlas_view_transform(float p_zoom, Vector2 p_scroll); + void synchronize_atlas_view(Object *p_current); + + virtual void edit(Object *p_object) override; + virtual bool handles(Object *p_object) const override; + virtual void make_visible(bool p_visible) override; + + TilesEditorPlugin(EditorNode *p_node); + ~TilesEditorPlugin(); +}; + +#endif // TILES_EDITOR_PLUGIN_H |