Browse Source

Implement TileMap patterns palette

Gilles Roudière 3 years ago
parent
commit
1a95f893c4

+ 3 - 3
doc/classes/Control.xml

@@ -763,10 +763,10 @@
 		</method>
 		<method name="set_drag_forwarding">
 			<return type="void" />
-			<argument index="0" name="target" type="Node" />
+			<argument index="0" name="target" type="Object" />
 			<description>
-				Forwards the handling of this control's drag and drop to [code]target[/code] node.
-				Forwarding can be implemented in the target node similar to the methods [method _get_drag_data], [method _can_drop_data], and [method _drop_data] but with two differences:
+				Forwards the handling of this control's drag and drop to [code]target[/code] object.
+				Forwarding can be implemented in the target object similar to the methods [method _get_drag_data], [method _can_drop_data], and [method _drop_data] but with two differences:
 				1. The function name must be suffixed with [b]_fw[/b]
 				2. The function must take an extra argument that is the control doing the forwarding
 				[codeblocks]

+ 26 - 0
doc/classes/TileMap.xml

@@ -117,6 +117,14 @@
 				Returns the neighboring cell to the one at coordinates [code]coords[/code], indentified by the [code]neighbor[/code] direction. This method takes into account the different layouts a TileMap can take.
 			</description>
 		</method>
+		<method name="get_pattern">
+			<return type="TileMapPattern" />
+			<argument index="0" name="layer" type="int" />
+			<argument index="1" name="coords_array" type="Vector2i[]" />
+			<description>
+				Creates a new [TileMapPattern] from the given layer and set of cells.
+			</description>
+		</method>
 		<method name="get_surrounding_tiles">
 			<return type="Vector2i[]" />
 			<argument index="0" name="coords" type="Vector2i" />
@@ -151,6 +159,15 @@
 				Returns if a layer Y-sorts its tiles.
 			</description>
 		</method>
+		<method name="map_pattern">
+			<return type="Vector2i" />
+			<argument index="0" name="position_in_tilemap" type="Vector2i" />
+			<argument index="1" name="coords_in_pattern" type="Vector2i" />
+			<argument index="2" name="pattern" type="TileMapPattern" />
+			<description>
+				Returns for the given coodinate [code]coords_in_pattern[/code] in a [TileMapPattern] the corresponding cell coordinates if the pattern was pasted at the [code]position_in_tilemap[/code] coordinates (see [method set_pattern]). This mapping is required as in half-offset tile shapes, the mapping might not work by calculating [code]position_in_tile_map + coords_in_pattern[/code]
+			</description>
+		</method>
 		<method name="map_to_world" qualifiers="const">
 			<return type="Vector2" />
 			<argument index="0" name="map_position" type="Vector2i" />
@@ -237,6 +254,15 @@
 				Sets a layers Z-index value. This Z-index is added to each tile's Z-index value.
 			</description>
 		</method>
+		<method name="set_pattern">
+			<return type="void" />
+			<argument index="0" name="layer" type="int" />
+			<argument index="1" name="position" type="Vector2i" />
+			<argument index="2" name="pattern" type="TileMapPattern" />
+			<description>
+				Paste the given [TileMapPattern] at the given [code]position[/code] and [code]layer[/code] in the tile map.
+			</description>
+		</method>
 		<method name="world_to_map" qualifiers="const">
 			<return type="Vector2i" />
 			<argument index="0" name="world_position" type="Vector2" />

+ 85 - 0
doc/classes/TileMapPattern.xml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="TileMapPattern" inherits="Resource" version="4.0">
+	<brief_description>
+		Holds a pattern to be copied from or pasted into [TileMap]s.
+	</brief_description>
+	<description>
+		This resource holds a set of cells to help bulk manipulations of [TileMap].
+		A pattern always start at the [code](0,0)[/code] coordinates and cannot have cells with negative coordinates.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_cell_alternative_tile" qualifiers="const">
+			<return type="int" />
+			<argument index="0" name="coords" type="Vector2i" />
+			<description>
+				Returns the tile alternative ID of the cell at [code]coords[/code].
+			</description>
+		</method>
+		<method name="get_cell_atlas_coords" qualifiers="const">
+			<return type="Vector2i" />
+			<argument index="0" name="coords" type="Vector2i" />
+			<description>
+				Returns the tile atlas coordinates ID of the cell at [code]coords[/code].
+			</description>
+		</method>
+		<method name="get_cell_source_id" qualifiers="const">
+			<return type="int" />
+			<argument index="0" name="coords" type="Vector2i" />
+			<description>
+				Returns the tile source ID of the cell at [code]coords[/code].
+			</description>
+		</method>
+		<method name="get_size" qualifiers="const">
+			<return type="Vector2i" />
+			<description>
+				Returns the size, in cells, of the pattern.
+			</description>
+		</method>
+		<method name="get_used_cells" qualifiers="const">
+			<return type="Vector2i[]" />
+			<description>
+				Returns the list of used cell coordinates in the pattern.
+			</description>
+		</method>
+		<method name="has_cell" qualifiers="const">
+			<return type="bool" />
+			<argument index="0" name="coords" type="Vector2i" />
+			<description>
+				Returns whether the pattern has a tile at the given coordinates.
+			</description>
+		</method>
+		<method name="is_empty" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns whether the pattern is empty or not.
+			</description>
+		</method>
+		<method name="remove_cell">
+			<return type="void" />
+			<argument index="0" name="coords" type="Vector2i" />
+			<argument index="1" name="arg1" type="bool" />
+			<description>
+				Remove the cell at the given coordinates.
+			</description>
+		</method>
+		<method name="set_cell">
+			<return type="void" />
+			<argument index="0" name="coords" type="Vector2i" />
+			<argument index="1" name="source_id" type="int" default="-1" />
+			<argument index="2" name="atlas_coords" type="Vector2i" default="Vector2i(-1, -1)" />
+			<argument index="3" name="alternative_tile" type="int" default="-1" />
+			<description>
+				Sets the tile indentifiers for the cell at coordinates [code]coords[/code]. See [method TileMap.set_cell].
+			</description>
+		</method>
+		<method name="set_size">
+			<return type="void" />
+			<argument index="0" name="size" type="Vector2i" />
+			<description>
+				Sets the size of the pattern.
+			</description>
+		</method>
+	</methods>
+</class>

+ 28 - 0
doc/classes/TileSet.xml

@@ -46,6 +46,14 @@
 				Occlusion layers allow assigning occlusion polygons to atlas tiles.
 			</description>
 		</method>
+		<method name="add_pattern">
+			<return type="int" />
+			<argument index="0" name="pattern" type="TileMapPattern" />
+			<argument index="1" name="index" type="int" default="-1" />
+			<description>
+				Adds a [TileMapPattern] to be stored in the TileSet resouce. If provided, insert it at the given [code]index[/code].
+			</description>
+		</method>
 		<method name="add_physics_layer">
 			<return type="void" />
 			<argument index="0" name="to_position" type="int" default="-1" />
@@ -154,6 +162,19 @@
 				Returns the occlusion layers count.
 			</description>
 		</method>
+		<method name="get_pattern">
+			<return type="TileMapPattern" />
+			<argument index="0" name="index" type="int" default="-1" />
+			<description>
+				Returns the [TileMapPattern] at the given [code]index[/code].
+			</description>
+		</method>
+		<method name="get_patterns_count">
+			<return type="int" />
+			<description>
+				Returns the number of [TileMapPattern] this tile set handles.
+			</description>
+		</method>
 		<method name="get_physics_layer_collision_layer" qualifiers="const">
 			<return type="int" />
 			<argument index="0" name="layer_index" type="int" />
@@ -374,6 +395,13 @@
 				Removes the occlusion layer at index [code]layer_index[/code]. Also updates the atlas tiles accordingly.
 			</description>
 		</method>
+		<method name="remove_pattern">
+			<return type="void" />
+			<argument index="0" name="index" type="int" />
+			<description>
+				Remove the [TileMapPattern] at the given index.
+			</description>
+		</method>
 		<method name="remove_physics_layer">
 			<return type="void" />
 			<argument index="0" name="layer_index" type="int" />

+ 18 - 0
editor/plugins/editor_preview_plugins.h

@@ -173,4 +173,22 @@ public:
 	EditorFontPreviewPlugin();
 	~EditorFontPreviewPlugin();
 };
+
+class EditorTileMapPatternPreviewPlugin : public EditorResourcePreviewGenerator {
+	GDCLASS(EditorTileMapPatternPreviewPlugin, EditorResourcePreviewGenerator);
+
+	mutable SafeFlag preview_done;
+
+	void _preview_done(const Variant &p_udata);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual bool handles(const String &p_type) const override;
+	virtual Ref<Texture2D> generate(const RES &p_from, const Size2 &p_size) const override;
+
+	EditorTileMapPatternPreviewPlugin();
+	~EditorTileMapPatternPreviewPlugin();
+};
 #endif // EDITORPREVIEWPLUGINS_H

File diff suppressed because it is too large
+ 374 - 200
editor/plugins/tiles/tile_map_editor.cpp


+ 42 - 17
editor/plugins/tiles/tile_map_editor.h

@@ -33,17 +33,24 @@
 
 #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/tabs.h"
 
-class TileMapEditorPlugin : public VBoxContainer {
+class TileMapEditorPlugin : public Object {
 public:
-	virtual Control *get_toolbar() const {
-		return memnew(Control);
+	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(){};
@@ -106,7 +113,7 @@ private:
 	Map<Vector2i, TileMapCell> drag_modified;
 	bool rmb_erasing = false;
 
-	TileMapCell _pick_random_tile(const TileMapPattern *p_pattern);
+	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);
 	Map<Vector2i, TileMapCell> _draw_rect(Vector2i p_start_cell, Vector2i p_end_cell);
 	Map<Vector2i, TileMapCell> _draw_bucket_fill(Vector2i p_coords, bool p_contiguous);
@@ -115,20 +122,25 @@ private:
 
 	///// Selection system. /////
 	Set<Vector2i> tile_map_selection;
-	TileMapPattern *tile_map_clipboard = memnew(TileMapPattern);
-	TileMapPattern *selection_pattern = memnew(TileMapPattern);
+	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_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. ////.
+	///// Bottom panel common ////
+	void _tab_changed();
+
+	///// Bottom panel tiles ////
+	VBoxContainer *tiles_bottom_panel;
 	Label *missing_source_label;
 	Label *invalid_source_label;
 
@@ -137,7 +149,7 @@ private:
 	Ref<Texture2D> missing_atlas_texture_icon;
 	void _update_tile_set_sources_list();
 
-	void _update_bottom_panel();
+	void _update_source_display();
 
 	// Atlas sources.
 	TileMapCell hovered_tile;
@@ -167,15 +179,26 @@ private:
 	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:
-	void _notification(int p_what);
 	static void _bind_methods();
 
 public:
-	virtual Control *get_toolbar() const override;
+	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;
 
@@ -205,6 +228,9 @@ private:
 
 	void _update_toolbar();
 
+	// Main vbox.
+	VBoxContainer *main_vbox_container;
+
 	// TileMap editing.
 	enum DragType {
 		DRAG_TYPE_NONE = 0,
@@ -281,16 +307,13 @@ private:
 	void _update_terrains_cache();
 	void _update_terrains_tree();
 	void _update_tiles_list();
+	void _update_theme();
 
 	// Update callback
 	virtual void tile_set_changed() override;
 
-protected:
-	void _notification(int p_what);
-	//	static void _bind_methods();
-
 public:
-	virtual Control *get_toolbar() const override;
+	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;
 
@@ -328,7 +351,9 @@ private:
 
 	// Bottom panel.
 	Label *missing_tileset_label;
-	Tabs *tabs;
+	Tabs *tabs_bar;
+	LocalVector<TileMapEditorPlugin::TabData> tabs_data;
+	LocalVector<TileMapEditorPlugin *> tabs_plugins;
 	void _update_bottom_panel();
 
 	// TileMap.

+ 83 - 5
editor/plugins/tiles/tile_set_editor.cpp

@@ -156,9 +156,9 @@ void TileSetEditor::_update_sources_list(int force_selected_id) {
 			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);
+					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);
+					item_text = vformat(TTR("No Texture Atlas Source (ID:%d)"), source_id);
 				}
 			}
 		}
@@ -168,13 +168,13 @@ void TileSetEditor::_update_sources_list(int force_selected_id) {
 		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);
+				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);
+			item_text = vformat(TTR("Unknown Type Source (ID:%d)"), source_id);
 		}
 		if (!texture.is_valid()) {
 			texture = missing_texture_texture;
@@ -327,6 +327,7 @@ void TileSetEditor::_notification(int p_what) {
 					tile_set->set_edited(true);
 				}
 				_update_sources_list();
+				_update_patterns_list();
 				tile_set_changed_needs_update = false;
 			}
 			break;
@@ -335,10 +336,56 @@ void TileSetEditor::_notification(int p_what) {
 	}
 }
 
+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));
+		TilesEditor::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);
@@ -582,6 +629,7 @@ void TileSetEditor::edit(Ref<TileSet> p_tile_set) {
 	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();
@@ -594,8 +642,20 @@ TileSetEditor::TileSetEditor() {
 
 	set_process_internal(true);
 
+	// Tabs.
+	tabs_bar = memnew(Tabs);
+	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);
+
+	//// Tiles ////
 	// Split container.
-	HSplitContainer *split_container = memnew(HSplitContainer);
+	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);
@@ -681,6 +741,24 @@ TileSetEditor::TileSetEditor() {
 	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));

+ 17 - 0
editor/plugins/tiles/tile_set_editor.h

@@ -46,7 +46,13 @@ class TileSetEditor : public VBoxContainer {
 private:
 	Ref<TileSet> tile_set;
 	bool tile_set_changed_needs_update = false;
+	HSplitContainer *split_container;
 
+	// Tabs.
+	HBoxContainer *tile_set_toolbar;
+	Tabs *tabs_bar;
+
+	// Tiles.
 	Label *no_source_selected_label;
 	TileSetAtlasSourceEditor *tile_set_atlas_source_editor;
 	TileSetScenesCollectionSourceEditor *tile_set_scenes_collection_source_editor;
@@ -69,7 +75,16 @@ private:
 	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);
@@ -82,6 +97,8 @@ public:
 	_FORCE_INLINE_ static TileSetEditor *get_singleton() { return singleton; }
 
 	void edit(Ref<TileSet> p_tile_set);
+	Control *get_toolbar() { return tile_set_toolbar; };
+
 	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;
 

+ 123 - 2
editor/plugins/tiles/tiles_editor_plugin.cpp

@@ -30,17 +30,18 @@
 
 #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/resources/tile_set.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"
 
@@ -65,6 +66,99 @@ void TilesEditor::_notification(int p_what) {
 	}
 }
 
+void TilesEditor::_pattern_preview_done(const Variant &p_udata) {
+	pattern_preview_done.set();
+}
+
+void TilesEditor::_thread_func(void *ud) {
+	TilesEditor *te = (TilesEditor *)ud;
+	te->_thread();
+}
+
+void TilesEditor::_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);
+
+				pattern_preview_done.clear();
+				RS::get_singleton()->request_frame_drawn_callback(const_cast<TilesEditor *>(this), "_pattern_preview_done", Variant());
+
+				while (!pattern_preview_done.is_set()) {
+					OS::get_singleton()->delay_usec(10);
+				}
+
+				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 TilesEditor::_tile_map_changed() {
 	tile_map_changed_needs_update = true;
 }
@@ -83,6 +177,7 @@ void TilesEditor::_update_editors() {
 	// Set editors visibility.
 	tilemap_toolbar->set_visible(!tileset_tilemap_switch_button->is_pressed());
 	tilemap_editor->set_visible(!tileset_tilemap_switch_button->is_pressed());
+	tileset_toolbar->set_visible(tileset_tilemap_switch_button->is_pressed());
 	tileset_editor->set_visible(tileset_tilemap_switch_button->is_pressed());
 
 	// Enable/disable the switch button.
@@ -150,6 +245,16 @@ void TilesEditor::synchronize_atlas_view(Object *p_current) {
 	}
 }
 
+void TilesEditor::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 TilesEditor::edit(Object *p_object) {
 	// Disconnect to changes.
 	TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id));
@@ -192,6 +297,7 @@ void TilesEditor::edit(Object *p_object) {
 }
 
 void TilesEditor::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("_pattern_preview_done", "pattern"), &TilesEditor::_pattern_preview_done);
 }
 
 TilesEditor::TilesEditor(EditorNode *p_editor) {
@@ -229,12 +335,27 @@ TilesEditor::TilesEditor(EditorNode *p_editor) {
 	tileset_editor->hide();
 	add_child(tileset_editor);
 
+	tileset_toolbar = tileset_editor->get_toolbar();
+	toolbar->add_child(tileset_toolbar);
+
+	// Pattern preview generation thread.
+	pattern_preview_thread.start(_thread_func, this);
+
 	// Initialization.
 	_update_switch_button();
 	_update_editors();
 }
 
 TilesEditor::~TilesEditor() {
+	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();
+	}
 }
 
 ///////////////////////////////////////////////////////////////

+ 21 - 0
editor/plugins/tiles/tiles_editor_plugin.h

@@ -53,6 +53,7 @@ private:
 	Control *tilemap_toolbar;
 	TileMapEditor *tilemap_editor;
 
+	Control *tileset_toolbar;
 	TileSetEditor *tileset_editor;
 
 	void _update_switch_button();
@@ -63,6 +64,23 @@ private:
 	float atlas_view_zoom = 1.0;
 	Vector2 atlas_view_scroll = Vector2();
 
+	// 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;
+	mutable SafeFlag pattern_preview_done;
+	void _pattern_preview_done(const Variant &p_udata);
+	static void _thread_func(void *ud);
+	void _thread();
+
 	void _tile_map_changed();
 
 protected:
@@ -82,6 +100,9 @@ public:
 	void set_atlas_view_transform(float p_zoom, Vector2 p_scroll);
 	void synchronize_atlas_view(Object *p_current);
 
+	// Pattern preview API.
+	void queue_pattern_preview(Ref<TileSet> p_tile_set, Ref<TileMapPattern> p_pattern, Callable p_callback);
+
 	void edit(Object *p_object);
 
 	TilesEditor(EditorNode *p_editor);

+ 10 - 97
scene/2d/tile_map.cpp

@@ -34,98 +34,6 @@
 
 #include "servers/navigation_server_2d.h"
 
-void TileMapPattern::set_cell(const Vector2i &p_coords, int p_source_id, const Vector2i p_atlas_coords, int p_alternative_tile) {
-	ERR_FAIL_COND_MSG(p_coords.x < 0 || p_coords.y < 0, vformat("Cannot set cell with negative coords in a TileMapPattern. Wrong coords: %s", p_coords));
-
-	size = size.max(p_coords + Vector2i(1, 1));
-	pattern[p_coords] = TileMapCell(p_source_id, p_atlas_coords, p_alternative_tile);
-}
-
-bool TileMapPattern::has_cell(const Vector2i &p_coords) const {
-	return pattern.has(p_coords);
-}
-
-void TileMapPattern::remove_cell(const Vector2i &p_coords, bool p_update_size) {
-	ERR_FAIL_COND(!pattern.has(p_coords));
-
-	pattern.erase(p_coords);
-	if (p_update_size) {
-		size = Vector2i();
-		for (const KeyValue<Vector2i, TileMapCell> &E : pattern) {
-			size = size.max(E.key + Vector2i(1, 1));
-		}
-	}
-}
-
-int TileMapPattern::get_cell_source_id(const Vector2i &p_coords) const {
-	ERR_FAIL_COND_V(!pattern.has(p_coords), TileSet::INVALID_SOURCE);
-
-	return pattern[p_coords].source_id;
-}
-
-Vector2i TileMapPattern::get_cell_atlas_coords(const Vector2i &p_coords) const {
-	ERR_FAIL_COND_V(!pattern.has(p_coords), TileSetSource::INVALID_ATLAS_COORDS);
-
-	return pattern[p_coords].get_atlas_coords();
-}
-
-int TileMapPattern::get_cell_alternative_tile(const Vector2i &p_coords) const {
-	ERR_FAIL_COND_V(!pattern.has(p_coords), TileSetSource::INVALID_TILE_ALTERNATIVE);
-
-	return pattern[p_coords].alternative_tile;
-}
-
-TypedArray<Vector2i> TileMapPattern::get_used_cells() const {
-	// Returns the cells used in the tilemap.
-	TypedArray<Vector2i> a;
-	a.resize(pattern.size());
-	int i = 0;
-	for (const KeyValue<Vector2i, TileMapCell> &E : pattern) {
-		Vector2i p(E.key.x, E.key.y);
-		a[i++] = p;
-	}
-
-	return a;
-}
-
-Vector2i TileMapPattern::get_size() const {
-	return size;
-}
-
-void TileMapPattern::set_size(const Vector2i &p_size) {
-	for (const KeyValue<Vector2i, TileMapCell> &E : pattern) {
-		Vector2i coords = E.key;
-		if (p_size.x <= coords.x || p_size.y <= coords.y) {
-			ERR_FAIL_MSG(vformat("Cannot set pattern size to %s, it contains a tile at %s. Size can only be increased.", p_size, coords));
-		};
-	}
-
-	size = p_size;
-}
-
-bool TileMapPattern::is_empty() const {
-	return pattern.is_empty();
-};
-
-void TileMapPattern::clear() {
-	size = Vector2i();
-	pattern.clear();
-};
-
-void TileMapPattern::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("set_cell", "coords", "source_id", "atlas_coords", "alternative_tile"), &TileMapPattern::set_cell, DEFVAL(TileSet::INVALID_SOURCE), DEFVAL(TileSetSource::INVALID_ATLAS_COORDS), DEFVAL(TileSetSource::INVALID_TILE_ALTERNATIVE));
-	ClassDB::bind_method(D_METHOD("has_cell", "coords"), &TileMapPattern::has_cell);
-	ClassDB::bind_method(D_METHOD("remove_cell", "coords"), &TileMapPattern::remove_cell);
-	ClassDB::bind_method(D_METHOD("get_cell_source_id", "coords"), &TileMapPattern::get_cell_source_id);
-	ClassDB::bind_method(D_METHOD("get_cell_atlas_coords", "coords"), &TileMapPattern::get_cell_atlas_coords);
-	ClassDB::bind_method(D_METHOD("get_cell_alternative_tile", "coords"), &TileMapPattern::get_cell_alternative_tile);
-
-	ClassDB::bind_method(D_METHOD("get_used_cells"), &TileMapPattern::get_used_cells);
-	ClassDB::bind_method(D_METHOD("get_size"), &TileMapPattern::get_size);
-	ClassDB::bind_method(D_METHOD("set_size", "size"), &TileMapPattern::set_size);
-	ClassDB::bind_method(D_METHOD("is_empty"), &TileMapPattern::is_empty);
-}
-
 Vector2i TileMap::transform_coords_layout(Vector2i p_coords, TileSet::TileOffsetAxis p_offset_axis, TileSet::TileLayout p_from_layout, TileSet::TileLayout p_to_layout) {
 	// Transform to stacked layout.
 	Vector2i output = p_coords;
@@ -1788,11 +1696,12 @@ int TileMap::get_cell_alternative_tile(int p_layer, const Vector2i &p_coords, bo
 	return E->get().alternative_tile;
 }
 
-TileMapPattern *TileMap::get_pattern(int p_layer, TypedArray<Vector2i> p_coords_array) {
+Ref<TileMapPattern> TileMap::get_pattern(int p_layer, TypedArray<Vector2i> p_coords_array) {
 	ERR_FAIL_INDEX_V(p_layer, (int)layers.size(), nullptr);
 	ERR_FAIL_COND_V(!tile_set.is_valid(), nullptr);
 
-	TileMapPattern *output = memnew(TileMapPattern);
+	Ref<TileMapPattern> output;
+	output.instantiate();
 	if (p_coords_array.is_empty()) {
 		return output;
 	}
@@ -1841,7 +1750,7 @@ TileMapPattern *TileMap::get_pattern(int p_layer, TypedArray<Vector2i> p_coords_
 	return output;
 }
 
-Vector2i TileMap::map_pattern(Vector2i p_position_in_tilemap, Vector2i p_coords_in_pattern, const TileMapPattern *p_pattern) {
+Vector2i TileMap::map_pattern(Vector2i p_position_in_tilemap, Vector2i p_coords_in_pattern, Ref<TileMapPattern> p_pattern) {
 	ERR_FAIL_COND_V(!p_pattern->has_cell(p_coords_in_pattern), Vector2i());
 
 	Vector2i output = p_position_in_tilemap + p_coords_in_pattern;
@@ -1864,7 +1773,7 @@ Vector2i TileMap::map_pattern(Vector2i p_position_in_tilemap, Vector2i p_coords_
 	return output;
 }
 
-void TileMap::set_pattern(int p_layer, Vector2i p_position, const TileMapPattern *p_pattern) {
+void TileMap::set_pattern(int p_layer, Vector2i p_position, const Ref<TileMapPattern> p_pattern) {
 	ERR_FAIL_INDEX(p_layer, (int)layers.size());
 	ERR_FAIL_COND(!tile_set.is_valid());
 
@@ -3076,6 +2985,10 @@ void TileMap::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("get_coords_for_body_rid", "body"), &TileMap::get_coords_for_body_rid);
 
+	ClassDB::bind_method(D_METHOD("get_pattern", "layer", "coords_array"), &TileMap::get_pattern);
+	ClassDB::bind_method(D_METHOD("map_pattern", "position_in_tilemap", "coords_in_pattern", "pattern"), &TileMap::map_pattern);
+	ClassDB::bind_method(D_METHOD("set_pattern", "layer", "position", "pattern"), &TileMap::set_pattern);
+
 	ClassDB::bind_method(D_METHOD("fix_invalid_tiles"), &TileMap::fix_invalid_tiles);
 	ClassDB::bind_method(D_METHOD("clear_layer", "layer"), &TileMap::clear_layer);
 	ClassDB::bind_method(D_METHOD("clear"), &TileMap::clear);
@@ -3092,7 +3005,7 @@ void TileMap::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("_update_dirty_quadrants"), &TileMap::_update_dirty_quadrants);
 
-	ClassDB::bind_method(D_METHOD("_set_tile_data", "layer"), &TileMap::_set_tile_data);
+	ClassDB::bind_method(D_METHOD("_set_tile_data", "layer", "data"), &TileMap::_set_tile_data);
 	ClassDB::bind_method(D_METHOD("_get_tile_data", "layer"), &TileMap::_get_tile_data);
 
 	ClassDB::bind_method(D_METHOD("_tile_set_changed_deferred_update"), &TileMap::_tile_set_changed_deferred_update);

+ 3 - 74
scene/2d/tile_map.h

@@ -37,51 +37,6 @@
 
 class TileSetAtlasSource;
 
-union TileMapCell {
-	struct {
-		int32_t source_id : 16;
-		int16_t coord_x : 16;
-		int16_t coord_y : 16;
-		int32_t alternative_tile : 16;
-	};
-
-	uint64_t _u64t;
-	TileMapCell(int p_source_id = -1, Vector2i p_atlas_coords = TileSetSource::INVALID_ATLAS_COORDS, int p_alternative_tile = TileSetSource::INVALID_TILE_ALTERNATIVE) {
-		source_id = p_source_id;
-		set_atlas_coords(p_atlas_coords);
-		alternative_tile = p_alternative_tile;
-	}
-
-	Vector2i get_atlas_coords() const {
-		return Vector2i(coord_x, coord_y);
-	}
-
-	void set_atlas_coords(const Vector2i &r_coords) {
-		coord_x = r_coords.x;
-		coord_y = r_coords.y;
-	}
-
-	bool operator<(const TileMapCell &p_other) const {
-		if (source_id == p_other.source_id) {
-			if (coord_x == p_other.coord_x) {
-				if (coord_y == p_other.coord_y) {
-					return alternative_tile < p_other.alternative_tile;
-				} else {
-					return coord_y < p_other.coord_y;
-				}
-			} else {
-				return coord_x < p_other.coord_x;
-			}
-		} else {
-			return source_id < p_other.source_id;
-		}
-	}
-
-	bool operator!=(const TileMapCell &p_other) const {
-		return !(source_id == p_other.source_id && coord_x == p_other.coord_x && coord_y == p_other.coord_y && alternative_tile == p_other.alternative_tile);
-	}
-};
-
 struct TileMapQuadrant {
 	struct CoordsWorldComparator {
 		_ALWAYS_INLINE_ bool operator()(const Vector2i &p_a, const Vector2i &p_b) const {
@@ -150,32 +105,6 @@ struct TileMapQuadrant {
 	}
 };
 
-class TileMapPattern : public Object {
-	GDCLASS(TileMapPattern, Object);
-
-	Vector2i size;
-	Map<Vector2i, TileMapCell> pattern;
-
-protected:
-	static void _bind_methods();
-
-public:
-	void set_cell(const Vector2i &p_coords, int p_source_id, const Vector2i p_atlas_coords, int p_alternative_tile = 0);
-	bool has_cell(const Vector2i &p_coords) const;
-	void remove_cell(const Vector2i &p_coords, bool p_update_size = true);
-	int get_cell_source_id(const Vector2i &p_coords) const;
-	Vector2i get_cell_atlas_coords(const Vector2i &p_coords) const;
-	int get_cell_alternative_tile(const Vector2i &p_coords) const;
-
-	TypedArray<Vector2i> get_used_cells() const;
-
-	Vector2i get_size() const;
-	void set_size(const Vector2i &p_size);
-	bool is_empty() const;
-
-	void clear();
-};
-
 class TileMap : public Node2D {
 	GDCLASS(TileMap, Node2D);
 
@@ -349,9 +278,9 @@ public:
 	Vector2i get_cell_atlas_coords(int p_layer, const Vector2i &p_coords, bool p_use_proxies = false) const;
 	int get_cell_alternative_tile(int p_layer, const Vector2i &p_coords, bool p_use_proxies = false) const;
 
-	TileMapPattern *get_pattern(int p_layer, TypedArray<Vector2i> p_coords_array);
-	Vector2i map_pattern(Vector2i p_position_in_tilemap, Vector2i p_coords_in_pattern, const TileMapPattern *p_pattern);
-	void set_pattern(int p_layer, Vector2i p_position, const TileMapPattern *p_pattern);
+	Ref<TileMapPattern> get_pattern(int p_layer, TypedArray<Vector2i> p_coords_array);
+	Vector2i map_pattern(Vector2i p_position_in_tilemap, Vector2i p_coords_in_pattern, Ref<TileMapPattern> p_pattern);
+	void set_pattern(int p_layer, Vector2i p_position, const Ref<TileMapPattern> p_pattern);
 
 	// Not exposed to users
 	TileMapCell get_cell(int p_layer, const Vector2i &p_coords, bool p_use_proxies = false) const;

+ 1 - 1
scene/gui/control.cpp

@@ -740,7 +740,7 @@ bool Control::has_point(const Point2 &p_point) const {
 	return Rect2(Point2(), get_size()).has_point(p_point);
 }
 
-void Control::set_drag_forwarding(Node *p_target) {
+void Control::set_drag_forwarding(Object *p_target) {
 	if (p_target) {
 		data.drag_owner = p_target->get_instance_id();
 	} else {

+ 1 - 1
scene/gui/control.h

@@ -355,7 +355,7 @@ public:
 	virtual Size2 get_minimum_size() const;
 	virtual Size2 get_combined_minimum_size() const;
 	virtual bool has_point(const Point2 &p_point) const;
-	virtual void set_drag_forwarding(Node *p_target);
+	virtual void set_drag_forwarding(Object *p_target);
 	virtual Variant get_drag_data(const Point2 &p_point);
 	virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const;
 	virtual void drop_data(const Point2 &p_point, const Variant &p_data);

+ 1 - 0
scene/register_scene_types.cpp

@@ -685,6 +685,7 @@ void register_scene_types() {
 	GDREGISTER_VIRTUAL_CLASS(TileSetSource);
 	GDREGISTER_CLASS(TileSetAtlasSource);
 	GDREGISTER_CLASS(TileSetScenesCollectionSource);
+	GDREGISTER_CLASS(TileMapPattern);
 	GDREGISTER_CLASS(TileData);
 	GDREGISTER_CLASS(TileMap);
 	GDREGISTER_CLASS(ParallaxBackground);

+ 238 - 0
scene/resources/tile_set.cpp

@@ -31,6 +31,7 @@
 #include "tile_set.h"
 
 #include "core/core_string_names.h"
+#include "core/io/marshalls.h"
 #include "core/math/geometry_2d.h"
 #include "core/templates/local_vector.h"
 
@@ -39,6 +40,189 @@
 #include "scene/resources/convex_polygon_shape_2d.h"
 #include "servers/navigation_server_2d.h"
 
+/////////////////////////////// TileMapPattern //////////////////////////////////////
+
+void TileMapPattern::_set_tile_data(const Vector<int> &p_data) {
+	int c = p_data.size();
+	const int *r = p_data.ptr();
+
+	int offset = 3;
+	ERR_FAIL_COND_MSG(c % offset != 0, "Corrupted tile data.");
+
+	clear();
+
+	for (int i = 0; i < c; i += offset) {
+		const uint8_t *ptr = (const uint8_t *)&r[i];
+		uint8_t local[12];
+		for (int j = 0; j < 12; j++) {
+			local[j] = ptr[j];
+		}
+
+#ifdef BIG_ENDIAN_ENABLED
+		SWAP(local[0], local[3]);
+		SWAP(local[1], local[2]);
+		SWAP(local[4], local[7]);
+		SWAP(local[5], local[6]);
+		SWAP(local[8], local[11]);
+		SWAP(local[9], local[10]);
+#endif
+
+		int16_t x = decode_uint16(&local[0]);
+		int16_t y = decode_uint16(&local[2]);
+		uint16_t source_id = decode_uint16(&local[4]);
+		uint16_t atlas_coords_x = decode_uint16(&local[6]);
+		uint16_t atlas_coords_y = decode_uint16(&local[8]);
+		uint16_t alternative_tile = decode_uint16(&local[10]);
+		set_cell(Vector2i(x, y), source_id, Vector2i(atlas_coords_x, atlas_coords_y), alternative_tile);
+	}
+	emit_signal(SNAME("changed"));
+}
+
+Vector<int> TileMapPattern::_get_tile_data() const {
+	// Export tile data to raw format
+	Vector<int> data;
+	data.resize(pattern.size() * 3);
+	int *w = data.ptrw();
+
+	// Save in highest format
+
+	int idx = 0;
+	for (const KeyValue<Vector2i, TileMapCell> &E : pattern) {
+		uint8_t *ptr = (uint8_t *)&w[idx];
+		encode_uint16((int16_t)(E.key.x), &ptr[0]);
+		encode_uint16((int16_t)(E.key.y), &ptr[2]);
+		encode_uint16(E.value.source_id, &ptr[4]);
+		encode_uint16(E.value.coord_x, &ptr[6]);
+		encode_uint16(E.value.coord_y, &ptr[8]);
+		encode_uint16(E.value.alternative_tile, &ptr[10]);
+		idx += 3;
+	}
+
+	return data;
+}
+
+void TileMapPattern::set_cell(const Vector2i &p_coords, int p_source_id, const Vector2i p_atlas_coords, int p_alternative_tile) {
+	ERR_FAIL_COND_MSG(p_coords.x < 0 || p_coords.y < 0, vformat("Cannot set cell with negative coords in a TileMapPattern. Wrong coords: %s", p_coords));
+
+	size = size.max(p_coords + Vector2i(1, 1));
+	pattern[p_coords] = TileMapCell(p_source_id, p_atlas_coords, p_alternative_tile);
+	emit_changed();
+}
+
+bool TileMapPattern::has_cell(const Vector2i &p_coords) const {
+	return pattern.has(p_coords);
+}
+
+void TileMapPattern::remove_cell(const Vector2i &p_coords, bool p_update_size) {
+	ERR_FAIL_COND(!pattern.has(p_coords));
+
+	pattern.erase(p_coords);
+	if (p_update_size) {
+		size = Vector2i();
+		for (const KeyValue<Vector2i, TileMapCell> &E : pattern) {
+			size = size.max(E.key + Vector2i(1, 1));
+		}
+	}
+	emit_changed();
+}
+
+int TileMapPattern::get_cell_source_id(const Vector2i &p_coords) const {
+	ERR_FAIL_COND_V(!pattern.has(p_coords), TileSet::INVALID_SOURCE);
+
+	return pattern[p_coords].source_id;
+}
+
+Vector2i TileMapPattern::get_cell_atlas_coords(const Vector2i &p_coords) const {
+	ERR_FAIL_COND_V(!pattern.has(p_coords), TileSetSource::INVALID_ATLAS_COORDS);
+
+	return pattern[p_coords].get_atlas_coords();
+}
+
+int TileMapPattern::get_cell_alternative_tile(const Vector2i &p_coords) const {
+	ERR_FAIL_COND_V(!pattern.has(p_coords), TileSetSource::INVALID_TILE_ALTERNATIVE);
+
+	return pattern[p_coords].alternative_tile;
+}
+
+TypedArray<Vector2i> TileMapPattern::get_used_cells() const {
+	// Returns the cells used in the tilemap.
+	TypedArray<Vector2i> a;
+	a.resize(pattern.size());
+	int i = 0;
+	for (const KeyValue<Vector2i, TileMapCell> &E : pattern) {
+		Vector2i p(E.key.x, E.key.y);
+		a[i++] = p;
+	}
+
+	return a;
+}
+
+Vector2i TileMapPattern::get_size() const {
+	return size;
+}
+
+void TileMapPattern::set_size(const Vector2i &p_size) {
+	for (const KeyValue<Vector2i, TileMapCell> &E : pattern) {
+		Vector2i coords = E.key;
+		if (p_size.x <= coords.x || p_size.y <= coords.y) {
+			ERR_FAIL_MSG(vformat("Cannot set pattern size to %s, it contains a tile at %s. Size can only be increased.", p_size, coords));
+		};
+	}
+
+	size = p_size;
+	emit_changed();
+}
+
+bool TileMapPattern::is_empty() const {
+	return pattern.is_empty();
+};
+
+void TileMapPattern::clear() {
+	size = Vector2i();
+	pattern.clear();
+	emit_changed();
+};
+
+bool TileMapPattern::_set(const StringName &p_name, const Variant &p_value) {
+	if (p_name == "tile_data") {
+		if (p_value.is_array()) {
+			_set_tile_data(p_value);
+			return true;
+		}
+		return false;
+	}
+	return false;
+}
+
+bool TileMapPattern::_get(const StringName &p_name, Variant &r_ret) const {
+	if (p_name == "tile_data") {
+		r_ret = _get_tile_data();
+		return true;
+	}
+	return false;
+}
+
+void TileMapPattern::_get_property_list(List<PropertyInfo> *p_list) const {
+	p_list->push_back(PropertyInfo(Variant::OBJECT, "tile_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+}
+
+void TileMapPattern::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("_set_tile_data", "data"), &TileMapPattern::_set_tile_data);
+	ClassDB::bind_method(D_METHOD("_get_tile_data"), &TileMapPattern::_get_tile_data);
+
+	ClassDB::bind_method(D_METHOD("set_cell", "coords", "source_id", "atlas_coords", "alternative_tile"), &TileMapPattern::set_cell, DEFVAL(TileSet::INVALID_SOURCE), DEFVAL(TileSetSource::INVALID_ATLAS_COORDS), DEFVAL(TileSetSource::INVALID_TILE_ALTERNATIVE));
+	ClassDB::bind_method(D_METHOD("has_cell", "coords"), &TileMapPattern::has_cell);
+	ClassDB::bind_method(D_METHOD("remove_cell", "coords"), &TileMapPattern::remove_cell);
+	ClassDB::bind_method(D_METHOD("get_cell_source_id", "coords"), &TileMapPattern::get_cell_source_id);
+	ClassDB::bind_method(D_METHOD("get_cell_atlas_coords", "coords"), &TileMapPattern::get_cell_atlas_coords);
+	ClassDB::bind_method(D_METHOD("get_cell_alternative_tile", "coords"), &TileMapPattern::get_cell_alternative_tile);
+
+	ClassDB::bind_method(D_METHOD("get_used_cells"), &TileMapPattern::get_used_cells);
+	ClassDB::bind_method(D_METHOD("get_size"), &TileMapPattern::get_size);
+	ClassDB::bind_method(D_METHOD("set_size", "size"), &TileMapPattern::set_size);
+	ClassDB::bind_method(D_METHOD("is_empty"), &TileMapPattern::is_empty);
+}
+
 /////////////////////////////// TileSet //////////////////////////////////////
 
 const int TileSet::INVALID_SOURCE = -1;
@@ -982,6 +1166,36 @@ void TileSet::clear_tile_proxies() {
 	emit_changed();
 }
 
+int TileSet::add_pattern(Ref<TileMapPattern> p_pattern, int p_index) {
+	ERR_FAIL_COND_V(!p_pattern.is_valid(), -1);
+	ERR_FAIL_COND_V_MSG(p_pattern->is_empty(), -1, "Cannot add an empty pattern to the TileSet.");
+	for (unsigned int i = 0; i < patterns.size(); i++) {
+		ERR_FAIL_COND_V_MSG(patterns[i] == p_pattern, -1, "TileSet has already this pattern.");
+	}
+	ERR_FAIL_COND_V(p_index > (int)patterns.size(), -1);
+	if (p_index < 0) {
+		p_index = patterns.size();
+	}
+	patterns.insert(p_index, p_pattern);
+	emit_changed();
+	return p_index;
+}
+
+Ref<TileMapPattern> TileSet::get_pattern(int p_index) {
+	ERR_FAIL_INDEX_V(p_index, (int)patterns.size(), Ref<TileMapPattern>());
+	return patterns[p_index];
+}
+
+void TileSet::remove_pattern(int p_index) {
+	ERR_FAIL_INDEX(p_index, (int)patterns.size());
+	patterns.remove(p_index);
+	emit_changed();
+}
+
+int TileSet::get_patterns_count() {
+	return patterns.size();
+}
+
 Vector<Vector2> TileSet::get_tile_shape_polygon() {
 	Vector<Vector2> points;
 	if (tile_shape == TileSet::TILE_SHAPE_SQUARE) {
@@ -2483,6 +2697,12 @@ bool TileSet::_set(const StringName &p_name, const Variant &p_value) {
 				return true;
 			}
 			return false;
+		} else if (components.size() == 1 && components[0].begins_with("pattern_") && components[0].trim_prefix("pattern_").is_valid_int()) {
+			int pattern_index = components[0].trim_prefix("pattern_").to_int();
+			for (int i = patterns.size(); i <= pattern_index; i++) {
+				add_pattern(p_value);
+			}
+			return true;
 		}
 
 #ifndef DISABLE_DEPRECATED
@@ -2606,6 +2826,13 @@ bool TileSet::_get(const StringName &p_name, Variant &r_ret) const {
 			return true;
 		}
 		return false;
+	} else if (components.size() == 1 && components[0].begins_with("pattern_") && components[0].trim_prefix("pattern_").is_valid_int()) {
+		int pattern_index = components[0].trim_prefix("pattern_").to_int();
+		if (pattern_index < 0 || pattern_index >= (int)patterns.size()) {
+			return false;
+		}
+		r_ret = patterns[pattern_index];
+		return true;
 	}
 
 	return false;
@@ -2686,6 +2913,11 @@ void TileSet::_get_property_list(List<PropertyInfo> *p_list) const {
 	p_list->push_back(PropertyInfo(Variant::ARRAY, "tile_proxies/source_level", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR));
 	p_list->push_back(PropertyInfo(Variant::ARRAY, "tile_proxies/coords_level", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR));
 	p_list->push_back(PropertyInfo(Variant::ARRAY, "tile_proxies/alternative_level", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR));
+
+	// Patterns.
+	for (unsigned int pattern_index = 0; pattern_index < patterns.size(); pattern_index++) {
+		p_list->push_back(PropertyInfo(Variant::OBJECT, vformat("pattern_%d", pattern_index), PROPERTY_HINT_RESOURCE_TYPE, "TileMapPattern", PROPERTY_USAGE_NOEDITOR));
+	}
 }
 
 void TileSet::_validate_property(PropertyInfo &property) const {
@@ -2799,6 +3031,12 @@ void TileSet::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("cleanup_invalid_tile_proxies"), &TileSet::cleanup_invalid_tile_proxies);
 	ClassDB::bind_method(D_METHOD("clear_tile_proxies"), &TileSet::clear_tile_proxies);
 
+	// Patterns
+	ClassDB::bind_method(D_METHOD("add_pattern", "pattern", "index"), &TileSet::add_pattern, DEFVAL(-1));
+	ClassDB::bind_method(D_METHOD("get_pattern", "index"), &TileSet::get_pattern, DEFVAL(-1));
+	ClassDB::bind_method(D_METHOD("remove_pattern", "index"), &TileSet::remove_pattern);
+	ClassDB::bind_method(D_METHOD("get_patterns_count"), &TileSet::get_patterns_count);
+
 	ADD_GROUP("Rendering", "");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "uv_clipping"), "set_uv_clipping", "is_uv_clipping");
 	ADD_ARRAY("occlusion_layers", "occlusion_layer_");

+ 86 - 0
scene/resources/tile_set.h

@@ -60,6 +60,84 @@ class TileSetPluginAtlasRendering;
 class TileSetPluginAtlasPhysics;
 class TileSetPluginAtlasNavigation;
 
+union TileMapCell {
+	struct {
+		int32_t source_id : 16;
+		int16_t coord_x : 16;
+		int16_t coord_y : 16;
+		int32_t alternative_tile : 16;
+	};
+
+	uint64_t _u64t;
+	TileMapCell(int p_source_id = -1, Vector2i p_atlas_coords = Vector2i(-1, -1), int p_alternative_tile = -1) { // default are INVALID_SOURCE, INVALID_ATLAS_COORDS, INVALID_TILE_ALTERNATIVE
+		source_id = p_source_id;
+		set_atlas_coords(p_atlas_coords);
+		alternative_tile = p_alternative_tile;
+	}
+
+	Vector2i get_atlas_coords() const {
+		return Vector2i(coord_x, coord_y);
+	}
+
+	void set_atlas_coords(const Vector2i &r_coords) {
+		coord_x = r_coords.x;
+		coord_y = r_coords.y;
+	}
+
+	bool operator<(const TileMapCell &p_other) const {
+		if (source_id == p_other.source_id) {
+			if (coord_x == p_other.coord_x) {
+				if (coord_y == p_other.coord_y) {
+					return alternative_tile < p_other.alternative_tile;
+				} else {
+					return coord_y < p_other.coord_y;
+				}
+			} else {
+				return coord_x < p_other.coord_x;
+			}
+		} else {
+			return source_id < p_other.source_id;
+		}
+	}
+
+	bool operator!=(const TileMapCell &p_other) const {
+		return !(source_id == p_other.source_id && coord_x == p_other.coord_x && coord_y == p_other.coord_y && alternative_tile == p_other.alternative_tile);
+	}
+};
+
+class TileMapPattern : public Resource {
+	GDCLASS(TileMapPattern, Resource);
+
+	Vector2i size;
+	Map<Vector2i, TileMapCell> pattern;
+
+	void _set_tile_data(const Vector<int> &p_data);
+	Vector<int> _get_tile_data() const;
+
+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_cell(const Vector2i &p_coords, int p_source_id, const Vector2i p_atlas_coords, int p_alternative_tile = 0);
+	bool has_cell(const Vector2i &p_coords) const;
+	void remove_cell(const Vector2i &p_coords, bool p_update_size = true);
+	int get_cell_source_id(const Vector2i &p_coords) const;
+	Vector2i get_cell_atlas_coords(const Vector2i &p_coords) const;
+	int get_cell_alternative_tile(const Vector2i &p_coords) const;
+
+	TypedArray<Vector2i> get_used_cells() const;
+
+	Vector2i get_size() const;
+	void set_size(const Vector2i &p_size);
+	bool is_empty() const;
+
+	void clear();
+};
+
 class TileSet : public Resource {
 	GDCLASS(TileSet, Resource);
 
@@ -245,6 +323,8 @@ private:
 	int next_source_id = 0;
 	// ---------------------
 
+	LocalVector<Ref<TileMapPattern>> patterns;
+
 	void _compute_next_source_id();
 	void _source_changed();
 
@@ -384,6 +464,12 @@ public:
 	void cleanup_invalid_tile_proxies();
 	void clear_tile_proxies();
 
+	// Patterns.
+	int add_pattern(Ref<TileMapPattern> p_pattern, int p_index = -1);
+	Ref<TileMapPattern> get_pattern(int p_index);
+	void remove_pattern(int p_index);
+	int get_patterns_count();
+
 	// Helpers
 	Vector<Vector2> get_tile_shape_polygon();
 	void draw_tile_shape(CanvasItem *p_canvas_item, Transform2D p_transform, Color p_color, bool p_filled = false, Ref<Texture2D> p_texture = Ref<Texture2D>());

Some files were not shown because too many files changed in this diff