Editor/Tilemap/TilemapEditor.cs

Editor tool for painting tilemaps in the editor. It manages a TileMap instance, shows a sidebar for layers and tileset selection, tracks mouse hover and paint strokes, and uses a TileBrush to draw or erase tiles on the TileMap.

File Access
using Editor;
using Sandbox;
using System;
using Saandy.Tilemapper;
using Editor.TerrainEditor;

namespace Saandy.Editor.Tilemapper;

[EditorTool]
[Title( "Tilemap Painter" )]
[Icon( "grid_3x3" )]
public class TilemapEditor : EditorTool
{
	private TileMap _tilemap;

	private Vector3 _hoverWorldPosition;
	private Vector2Int _hoverCell;
	private bool _hasHoverCell;

	private bool _hasLastPaintCell;
	private Vector2Int _lastPaintCell;

	private bool _wasLeftDown;
	private bool _wasRightDown;

	private TilemapTilesetPickerWidget _tilesetPicker;
	private TilemapLayerListWidget _layerList;

	[Property]
	public TilesetResource SelectedTileset { get; set; }

	private TileBrush _brush => SelectedTileset == null ? null : TileBrush.GetBrush( SelectedTileset.BrushType );

	public TilemapEditor()
	{
		RebuildSidebarOnSelectionChange = false;
	}

	public override Widget CreateToolSidebar()
	{
		var sidebar = new ToolSidebarWidget();
		sidebar.AddTitle( "Tilemap Painter", "grid_3x3" );
		sidebar.MinimumWidth = 340;


		// Layers
		{
			var group = sidebar.AddGroup( "Layers" );

			_layerList = new TilemapLayerListWidget( sidebar );
			_layerList.MinimumHeight = 190;
			_layerList.HorizontalSizeMode = SizeMode.Flexible;
			_layerList.VerticalSizeMode = SizeMode.CanGrow;
			_layerList.SetTilemap( _tilemap );
			group.Add( _layerList );

			var addLayerButton = new Button( "Add Layer", "add" );
			addLayerButton.Clicked += () =>
			{
				if ( _tilemap == null || !_tilemap.IsValid() )
					_tilemap = TileMap.GetOrCreate( Scene );

				_tilemap?.AddLayer();
				_layerList?.SetTilemap( _tilemap );
			};
			group.Add( addLayerButton );

			var removeLayerButton = new Button( "Remove Selected Layer", "delete" );
			removeLayerButton.Clicked += () =>
			{
				if ( _tilemap == null || !_tilemap.IsValid() )
					return;

				_tilemap.RemoveLayer( _tilemap.ActiveLayerIndex );
				_layerList?.SetTilemap( _tilemap );
			};
			group.Add( removeLayerButton );
		}

		// Tileset selection
		{
			var group = sidebar.AddGroup( "Tileset Resources", SizeMode.Flexible );

			_tilesetPicker = new TilemapTilesetPickerWidget( sidebar );
			_tilesetPicker.MinimumHeight = 420;
			_tilesetPicker.HorizontalSizeMode = SizeMode.Flexible;
			_tilesetPicker.VerticalSizeMode = SizeMode.Flexible;
			_tilesetPicker.SetSelectedTileset( SelectedTileset );
			_tilesetPicker.OnTilesetSelected = tileset =>
			{
				SelectedTileset = tileset;
				_tilesetPicker.SetSelectedTileset( SelectedTileset );
			};

			group.Add( _tilesetPicker );
		}

		// Actions
		{
			var group = sidebar.AddGroup( "Actions" );

			var refreshButton = new Button( "Refresh Tilesets", "refresh" );
			refreshButton.Clicked += () =>
			{
				_tilesetPicker?.RefreshTilesets();
				SelectDefaultTilesetIfNeeded();
				_tilesetPicker?.SetSelectedTileset( SelectedTileset );
			};
			refreshButton.ToolTip = "Reload the list of TilesetResource assets from ResourceLibrary.GetAll<TilesetResource>().";
			group.Add( refreshButton );
		}

		SelectDefaultTilesetIfNeeded();
		_tilesetPicker?.SetSelectedTileset( SelectedTileset );

		return sidebar;
	}

	public override void OnEnabled()
	{
		AllowGameObjectSelection = false;

		_tilemap = TileMap.GetOrCreate( Scene );
		_layerList?.SetTilemap( _tilemap );

		SelectDefaultTilesetIfNeeded();
		_tilesetPicker?.SetSelectedTileset( SelectedTileset );
	}

	public override void OnDisabled()
	{
		base.OnDisabled();

		_hasHoverCell = false;
		_hasLastPaintCell = false;
		_wasLeftDown = false;
		_wasRightDown = false;
	}

	public override void OnUpdate()
	{
		if ( _tilemap == null || !_tilemap.IsValid() )
		{
			_tilemap = TileMap.GetOrCreate( Scene );
		}

		if ( _tilemap == null )
			return;

		_layerList?.SetTilemap( _tilemap );
		_tilemap.ForceRendererUpdate( Camera );

		UpdateHoverCell();

		bool leftDown = Gizmo.IsLeftMouseDown;
		bool rightDown = Gizmo.IsRightMouseDown;

		if ( rightDown )
		{
			UpdatePaintStroke( eraseMode: true, isNewStroke: !_wasRightDown );
		}
		else if ( leftDown )
		{
			UpdatePaintStroke( eraseMode: false, isNewStroke: !_wasLeftDown );
		}
		else
		{
			_hasLastPaintCell = false;
		}

		DrawHoverSquare();

		_wasLeftDown = leftDown;
		_wasRightDown = rightDown;
	}

	private void SelectDefaultTilesetIfNeeded()
	{
		if ( SelectedTileset != null && SelectedTileset.IsValid() )
			return;

		SelectedTileset = TilemapTilesetPickerWidget.GetFirstProjectTileset();
	}

	private void UpdateHoverCell()
	{
		var ray = Gizmo.CurrentRay;

		if ( !_tilemap.TryIntersectRayWithPlane( ray, out var hit ) )
		{
			_hasHoverCell = false;
			return;
		}

		_hoverWorldPosition = hit;
		_hasHoverCell = true;

		float tileSize = Math.Max( _tilemap.TileSize, 0.0001f );
		_hoverCell = _tilemap.WorldToCell( hit, tileSize );
	}

	private void UpdatePaintStroke( bool eraseMode, bool isNewStroke )
	{
		if ( !_hasHoverCell )
			return;

		if ( isNewStroke || !_hasLastPaintCell )
		{
			PaintLine( _hoverCell, _hoverCell, eraseMode );

			_lastPaintCell = _hoverCell;
			_hasLastPaintCell = true;

			return;
		}

		if ( _lastPaintCell == _hoverCell )
			return;

		PaintLine( _lastPaintCell, _hoverCell, eraseMode );

		_lastPaintCell = _hoverCell;
		_hasLastPaintCell = true;
	}

	private void PaintLine( Vector2Int from, Vector2Int to, bool eraseMode )
	{
		if ( _tilemap == null || !_tilemap.IsValid() )
			return;

		if ( SelectedTileset == null || !SelectedTileset.IsValid() )
			return;

		if ( _brush == null )
			return;

		_brush.Draw( _tilemap, SelectedTileset, from, to, eraseMode );
	}

	private void DrawHoverSquare()
	{
		if ( !_hasHoverCell )
			return;

		DrawCellOutline( _hoverCell, Color.FromBytes( 255, 220, 120 ) );
	}

	private void DrawCellOutline( Vector2Int cell, Color color )
	{
		if ( _tilemap == null )
			return;

		var bounds = _tilemap.GetCellBounds( cell, 0.04f );

		var prevColor = Gizmo.Draw.Color;
		var prevThickness = Gizmo.Draw.LineThickness;

		Gizmo.Draw.Color = color;
		Gizmo.Draw.LineThickness = 1.5f;
		Gizmo.Draw.LineBBox( bounds );

		Gizmo.Draw.Color = prevColor;
		Gizmo.Draw.LineThickness = prevThickness;
	}
}