Editor/Tileset/TilesetTools/TilesetTool.cs
using Editor;
using Sandbox;
using SpriteTools.TilesetTool.Tools;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SpriteTools.TilesetTool;
[EditorTool]
[Title( "Tileset Tool" )]
[Description( "Paint 2D tiles from a tileset" )]
[Icon( "dashboard" )]
[Group( "7" )]
public partial class TilesetTool : EditorTool
{
public static TilesetTool Active { get; private set; }
public ToolSettings Settings { get; private set; } = new();
public class ToolSettings
{
[Group( "Brush" ), Property, Editor( "angle" )] public int Angle { get; set; } = 0;
[Group( "Brush" ), Property] public bool HorizontalFlip { get; set; } = false;
[Group( "Brush" ), Property] public bool VerticalFlip { get; set; } = false;
[Group( "Brush" ), Property, Editor( "autotile_index" )] public int AutotileBrush { get; set; } = -1;
[Group( "Brush" ), Property, WideMode( HasLabel = false ), Order( 999999 )]
public Preview.Preview Preview { get; set; } = new();
}
public override IEnumerable<EditorTool> GetSubtools ()
{
yield return new PaintTileTool( this );
yield return new EraserTileTool( this );
yield return new LineTileTool( this );
yield return new RectangleTileTool( this );
}
public TilesetComponent SelectedComponent;
public TilesetComponent.Layer SelectedLayer
{
get => _selectedLayer;
set
{
if ( _selectedLayer == value ) return;
var previousTileset = _selectedLayer?.TilesetResource;
_selectedLayer = value;
if ( value is not null && ( previousTileset == null || _selectedLayer?.TilesetResource != previousTileset ) )
{
Settings.AutotileBrush = -1;
_sceneObject?.UpdateTileset( value.TilesetResource );
SelectedTile = value?.TilesetResource?.Tiles?.FirstOrDefault();
if ( !string.IsNullOrEmpty( _selectedLayer?.TilesetResource?.FilePath ) )
{
Preview.PreviewWidget.Current?.UpdateTexture( _selectedLayer.TilesetResource.FilePath );
}
}
}
}
TilesetComponent.Layer _selectedLayer;
public TilesetResource.Tile SelectedTile
{
get => _selectedTile;
set
{
if ( _selectedTile == value ) return;
_selectedTile = value;
_sceneObject?.MultiTilePositions?.Clear();
UpdateInspector?.Invoke();
}
}
TilesetResource.Tile _selectedTile;
internal Action UpdateInspector { get; set; }
bool WasGridActive = true;
Vector2Int GridSize => SelectedLayer?.TilesetResource?.TileSize ?? new Vector2Int( 32, 32 );
internal TilesetPreviewObject _sceneObject;
public override void OnEnabled ()
{
Active = this;
base.OnEnabled();
AllowGameObjectSelection = false;
Selection.Clear();
Selection.Set( this );
InitGrid();
InitPreviewObject();
UpdateComponent();
if ( _componentToOpen is not null )
{
SelectedComponent = _componentToOpen;
SelectedLayer = SelectedComponent?.Layers?.FirstOrDefault();
_componentToOpen = null;
}
}
public override void OnDisabled ()
{
Active = null;
base.OnDisabled();
ResetGrid();
RemovePreviewObject();
}
public override void OnUpdate ()
{
base.OnUpdate();
if ( SelectedComponent.Transform is null ) return;
var firstSelected = Selection.FirstOrDefault();
if ( !_resetting && ( firstSelected != this || Selection.Count > 1 ) )
{
if ( firstSelected is TilesetTool )
{
ResetTool();
}
else
{
EditorToolManager.SetTool( "object" );
}
return;
}
if ( SceneViewWidget.Current.LastSelectedViewportWidget?.SceneView?.Tools?.CurrentTool?.CurrentTool is null )
{
_sceneObject.RenderingEnabled = false;
}
if ( SelectedLayer is null ) return;
var state = SceneViewWidget.Current.LastSelectedViewportWidget.State;
var gridSize = GridSize * ( SelectedLayer.TilesetResource?.TileScale ?? 1f );
using ( Gizmo.Scope( "grid" ) )
{
Gizmo.Draw.IgnoreDepth = state.Is2D;
Gizmo.Draw.Grid( SelectedComponent.WorldPosition, state.GridAxis, gridSize, state.GridOpacity, 0.01f, 0.01f );
}
}
static TilesetComponent _componentToOpen;
public static void OpenComponent ( TilesetComponent component )
{
if ( Active is not null )
{
Active.SelectedComponent = component;
Active.SelectedLayer = component.Layers.FirstOrDefault();
}
else
{
_componentToOpen = component;
EditorToolManager.SetTool( nameof( TilesetTool ) );
}
}
// This is used for when the scene is reloaded via an undo/redo snapshot
bool _resetting = false;
async void ResetTool ()
{
_resetting = true;
Active = null;
var componentId = SelectedComponent?.GameObject?.Id ?? Guid.Empty;
EditorToolManager.SetTool( "object" );
while ( Manager?.CurrentSession is null )
{
await Task.Delay( 1 );
}
Selection.Clear();
while ( TilesetToolInspector.Active.IsValid )
{
await Task.Delay( 1 );
}
if ( componentId != Guid.Empty )
{
var scene = SceneEditorSession.Active.Scene;
var component = scene.Directory.FindByGuid( componentId ).GetComponent<TilesetComponent>();
component ??= scene.GetAllComponents<TilesetComponent>().FirstOrDefault();
if ( component is not null )
{
OpenComponent( component );
}
}
else
{
EditorToolManager.SetTool( nameof( TilesetTool ) );
}
_resetting = false;
}
void UpdateComponent ()
{
var component = Scene.GetAllComponents<TilesetComponent>().FirstOrDefault();
if ( !component.IsValid() )
{
var go = new GameObject()
{
Name = "Tileset Object"
};
component = go.Components.GetOrCreate<TilesetComponent>();
}
if ( component.IsValid() )
{
SelectedComponent = component;
SelectedLayer = SelectedComponent?.Layers?.FirstOrDefault();
}
}
internal void PlaceAutotile ( Guid brushId, Vector2Int position, bool update = true )
{
if ( SelectedLayer is null ) return;
bool isMerging = CurrentTool is BaseTileTool tool && tool.ShouldMergeAutotiles;
SelectedLayer.SetAutotile( brushId, position, true, update, isMerging );
}
internal void EraseAutoTile ( AutotileBrush brush, Vector2Int position, bool update = true )
{
if ( SelectedLayer is null ) return;
SelectedLayer.SetAutotile( brush.Id, position, false, update );
}
internal void PlaceTile ( Vector2Int position, Guid tileId, Vector2Int cellPosition, bool rebuild = true )
{
if ( SelectedLayer is null ) return;
if ( Math.Abs( position.x ) >= int.MaxValue || Math.Abs( position.y ) >= int.MaxValue )
{
Log.Warning( $"Attempted to place tile {position} out of bounds" );
return;
}
SelectedLayer.SetTile( position, tileId, cellPosition, Settings.Angle, Settings.HorizontalFlip, Settings.VerticalFlip, rebuild );
}
internal void EraseTile ( Vector2 position, bool rebuild = true )
{
if ( SelectedLayer is null ) return;
SelectedLayer.RemoveTile( (Vector2Int)position );
if ( rebuild ) SelectedComponent.IsDirty = true;
}
void InitPreviewObject ()
{
RemovePreviewObject();
_sceneObject = new TilesetPreviewObject( this, Scene.SceneWorld );
if ( SelectedLayer is not null )
_sceneObject.UpdateTileset( SelectedLayer.TilesetResource );
_sceneObject.Flags.CastShadows = false;
_sceneObject.Flags.IsOpaque = false;
_sceneObject.Flags.IsTranslucent = true;
_sceneObject.RenderingEnabled = true;
}
void RemovePreviewObject ()
{
_sceneObject?.Delete();
_sceneObject = null;
}
void InitGrid ()
{
WasGridActive = SceneViewWidget.Current.LastSelectedViewportWidget.State.ShowGrid;
SceneViewWidget.Current.LastSelectedViewportWidget.State.ShowGrid = false;
}
void ResetGrid ()
{
SceneViewWidget.Current.LastSelectedViewportWidget.State.ShowGrid = WasGridActive;
}
[Shortcut( "tileset-tools.tileset-tool", "SHIFT+T", typeof( SceneViewportWidget ) )]
public static void ActivateSubTool ()
{
EditorToolManager.SetTool( nameof( TilesetTool ) );
}
[Shortcut( "tileset-tools.rotate-left", "Q", typeof( SceneViewportWidget ) )]
public static void RotateLeft ()
{
if ( Active is null ) return;
Active.Settings.Angle = ( Active.Settings.Angle + 90 ) % 360;
}
[Shortcut( "tileset-tools.rotate-right", "W", typeof( SceneViewportWidget ) )]
public static void RotateRight ()
{
if ( Active is null ) return;
Active.Settings.Angle -= 90;
if ( Active.Settings.Angle < 0 )
{
Active.Settings.Angle += 360;
}
}
[Shortcut( "tileset-tools.flip-horizontal", "A", typeof( SceneViewportWidget ) )]
public static void FlipHorizontal ()
{
if ( Active is null ) return;
Active.Settings.HorizontalFlip = !Active.Settings.HorizontalFlip;
}
[Shortcut( "tileset-tools.flip-vertical", "S", typeof( SceneViewportWidget ) )]
public static void FlipVertical ()
{
if ( Active is null ) return;
Active.Settings.VerticalFlip = !Active.Settings.VerticalFlip;
}
}
internal sealed class TilesetPreviewObject : SceneCustomObject
{
TilesetTool Tool;
Material Material;
internal List<(Vector2Int, Vector2Int, Guid)> MultiTilePositions = new();
public TilesetPreviewObject ( TilesetTool tool, SceneWorld world ) : base( world )
{
Tool = tool;
}
internal void UpdateTileset ( TilesetResource tileset )
{
if ( tileset is null ) return;
Material = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
Material.Set( "Texture", Texture.LoadFromFileSystem( tileset.FilePath, Sandbox.FileSystem.Mounted ) );
}
internal void ClearPositions ()
{
MultiTilePositions.Clear();
}
internal void SetPositions ( List<Vector2Int> positions, List<Vector2Int> cellPositions = null, List<Guid> guids = null )
{
if ( cellPositions is null )
{
MultiTilePositions = positions.Select( x => (x, new Vector2Int( -1, -1 ), Guid.Empty) ).ToList();
return;
}
guids ??= new();
MultiTilePositions.Clear();
for ( int i = 0; i < positions.Count; i++ )
{
Vector2Int cellPos = new Vector2Int( -1, -1 );
Guid cellId = Guid.Empty;
if ( i < cellPositions.Count )
cellPos = cellPositions[i];
if ( i < guids.Count )
cellId = guids[i];
MultiTilePositions.Add( (positions[i], cellPos, cellId) );
}
}
internal void SetPositions ( List<Vector2> positions, List<Vector2> cellPositions = null, List<Guid> guids = null )
{
if ( cellPositions is null )
{
MultiTilePositions = positions.Select( x => ((Vector2Int)x, new Vector2Int( -1, -1 ), Guid.Empty) ).ToList();
return;
}
guids ??= new();
MultiTilePositions.Clear();
for ( int i = 0; i < positions.Count; i++ )
{
Vector2Int cellPos = new Vector2Int( -1, -1 );
Guid cellId = Guid.Empty;
if ( i < cellPositions.Count )
cellPos = (Vector2Int)cellPositions[i];
if ( i < guids.Count )
cellId = guids[i];
MultiTilePositions.Add( ((Vector2Int)positions[i], cellPos, cellId) );
}
}
internal void SetPositions ( List<(Vector2Int, Vector2Int, Guid)> positions )
{
MultiTilePositions = positions;
}
public override void RenderSceneObject ()
{
if ( Material is null ) return;
var selectedTile = Tool?.SelectedTile;
if ( selectedTile is null ) return;
var layer = Tool?.SelectedLayer;
if ( layer is null ) return;
var tileset = selectedTile.Tileset;
if ( tileset is null ) return;
var brush = AutotileWidget.Instance?.Brush;
var tileSize = tileset.GetTileSize();
var scale = Vector2Int.One;
// if (TilesetTool.Active.CurrentTool is PaintTileTool) scale = selectedTile.Size;
var tiling = tileset.GetTiling() * scale;
var positions = MultiTilePositions.ToList();
if ( positions.Count == 0 ) positions.Add( (Vector2Int.Zero, new Vector2Int( -1, -1 ), Guid.Empty) );
int i = 0;
var vertexCount = positions.Count * 6;
var vertex = ArrayPool<Vertex>.Shared.Rent( vertexCount );
foreach ( var pos in positions )
{
var offsetPos = pos.Item1;
var tilePosition = ( pos.Item2.x == -1 || pos.Item2.y == -1 ) ? selectedTile.Position : pos.Item2;
if ( brush is null )
{
if ( Tool.Settings.Angle == 90 )
offsetPos = new Vector2Int( -offsetPos.y, offsetPos.x );
else if ( Tool.Settings.Angle == 180 )
offsetPos = new Vector2Int( -offsetPos.x, -offsetPos.y );
else if ( Tool.Settings.Angle == 270 )
offsetPos = new Vector2Int( offsetPos.y, -offsetPos.x );
}
var offset = tileset.GetOffset( tilePosition );
var position = new Vector3( offsetPos.x * tileSize.x, offsetPos.y * tileSize.y, Position.z ) - new Vector3( 0, ( scale.y - 1 ) * tileSize.y, 0 );
var size = tileSize * scale;
var topLeft = new Vector3( position.x, position.y, position.z );
var topRight = new Vector3( position.x + size.x, position.y, position.z );
var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );
if ( Tool.Settings.HorizontalFlip ) offset.x = -offset.x - tiling.x;
if ( !Tool.Settings.VerticalFlip ) offset.y = -offset.y - tiling.y;
var uvTopLeft = new Vector2( offset.x, offset.y );
var uvTopRight = new Vector2( offset.x + tiling.x, offset.y );
var uvBottomRight = new Vector2( offset.x + tiling.x, offset.y + tiling.y );
var uvBottomLeft = new Vector2( offset.x, offset.y + tiling.y );
if ( brush is null )
{
if ( Tool.Settings.Angle == 90 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvBottomLeft;
uvBottomLeft = uvBottomRight;
uvBottomRight = uvTopRight;
uvTopRight = tempUv;
}
else if ( Tool.Settings.Angle == 180 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvBottomRight;
uvBottomRight = tempUv;
tempUv = uvTopRight;
uvTopRight = uvBottomLeft;
uvBottomLeft = tempUv;
}
else if ( Tool.Settings.Angle == 270 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvTopRight;
uvTopRight = uvBottomRight;
uvBottomRight = uvBottomLeft;
uvBottomLeft = tempUv;
}
}
vertex[i] = new Vertex( topLeft );
vertex[i].TexCoord0 = uvTopLeft;
vertex[i].TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertex[i].Color = Color.White;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( topRight );
vertex[i].TexCoord0 = uvTopRight;
vertex[i].TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertex[i].Color = Color.White;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomRight );
vertex[i].TexCoord0 = uvBottomRight;
vertex[i].TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertex[i].Color = Color.White;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomRight );
vertex[i].TexCoord0 = uvBottomRight;
vertex[i].TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertex[i].Color = Color.White;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomLeft );
vertex[i].TexCoord0 = uvBottomLeft;
vertex[i].TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertex[i].Color = Color.White;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( topLeft );
vertex[i].TexCoord0 = uvTopLeft;
vertex[i].TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertex[i].Color = Color.White;
vertex[i].Normal = Vector3.Up;
i++;
}
Graphics.Draw( vertex, vertexCount, Material, Attributes );
ArrayPool<Vertex>.Shared.Return( vertex );
}
}