Editor UI widget for a tileset rect/order tool. It displays the tileset source image, allows dragging to define tile rects, selecting and dragging existing tiles onto an optional template, and reorders/assigns tiles according to a template.
using Editor;
using Sandbox;
using Saandy.Tilemapper;
using System;
using System.Collections.Generic;
namespace Saandy.Editor.Tilemapper;
public sealed class TilesetRectOrderWidget : Widget
{
private readonly TilesetResource _tileset;
private readonly TileBrushEditorTemplate _template;
private readonly Action _onChanged;
private Texture _sourceTexture;
private Pixmap _sourcePixmap;
private Texture _templateTexture;
private Pixmap _templatePixmap;
private readonly Pixmap[] _templateSlotPixmaps;
private readonly Dictionary<Guid, Pixmap> _tileSpritePixmapCache = new();
private Vector2Int _sourceTextureSize = Vector2Int.One;
private Rect _sourceImageRect;
private Rect _templateRect;
private Rect _tileDataRect;
private TilesetTileDataWidget _tileDataWidget;
private bool _draggingNewRect;
private Vector2 _rectDragStart;
private Vector2 _rectDragEnd;
private Rect _pendingSourceRect;
private int _selectedTileIndex = -1;
private bool _draggingExistingTile;
private int _draggedTileIndex = -1;
private Vector2 _dragMousePosition;
private Vector2 _mouseLocalPosition;
// template slot -> tile Guid
private readonly Guid[] _templateSlotIds;
private string _statusText = "";
private int SlotCount => Math.Max( 0, _template?.SlotCount ?? 0 );
private int TemplateColumns => Math.Max( 1, _template?.Columns ?? 1 );
private int TemplateRows => Math.Max( 1, _template?.Rows ?? 1 );
private int TemplateSourceColumns => Math.Max( 1, _template?.TemplateSourceColumns ?? TemplateColumns );
private int TemplateSourceRows => Math.Max( 1, _template?.TemplateSourceRows ?? TemplateRows );
public bool HasTemplate => _template != null && _template.HasTemplate;
public TilesetRectOrderWidget( TilesetResource tileset, TileBrushEditorTemplate template, Action onChanged ) : base( null )
{
_tileset = tileset;
_template = template;
_onChanged = onChanged;
_templateSlotIds = new Guid[SlotCount];
_templateSlotPixmaps = new Pixmap[SlotCount];
MinimumHeight = HasTemplate
? Math.Max( 760, 160 + TemplateRows * 96 )
: 560;
FixedHeight = MinimumHeight;
MinimumWidth = 560;
MouseTracking = true;
HorizontalSizeMode = SizeMode.Flexible;
VerticalSizeMode = SizeMode.CanGrow;
_tileDataWidget = new TilesetTileDataWidget( this, _tileset, _onChanged );
_tileDataWidget.SetSelectedTileIndex( _selectedTileIndex );
Refresh();
LoadTemplateFromCurrentOrder();
}
public void Refresh()
{
_sourceTexture = _tileset?.GetTexture();
_sourceTextureSize = _tileset?.GetTextureSize() ?? Vector2Int.One;
_sourcePixmap = null;
_tileSpritePixmapCache.Clear();
if ( _sourceTexture != null && _sourceTexture.IsValid )
{
_sourcePixmap = Pixmap.FromTexture( _sourceTexture, true );
}
LoadTemplateTexture();
RemoveInvalidTemplateAssignments();
UpdateStatus();
_tileDataWidget?.RefreshFromData();
Update();
}
private void LoadTemplateTexture()
{
_templateTexture = null;
_templatePixmap = null;
for ( int i = 0; i < _templateSlotPixmaps.Length; i++ )
{
_templateSlotPixmaps[i] = null;
}
if ( !HasTemplate )
return;
if ( string.IsNullOrWhiteSpace( _template.TemplateTexturePath ) )
return;
_templateTexture = Texture.Load( _template.TemplateTexturePath );
if ( _templateTexture == null || !_templateTexture.IsValid )
return;
_templatePixmap = Pixmap.FromTexture( _templateTexture, true );
if ( _templatePixmap == null )
return;
BuildTemplateSlotPixmaps();
}
private void BuildTemplateSlotPixmaps()
{
if ( _templatePixmap == null )
return;
int cellW = Math.Max( 1, _templatePixmap.Width / TemplateSourceColumns );
int cellH = Math.Max( 1, _templatePixmap.Height / TemplateSourceRows );
for ( int i = 0; i < SlotCount; i++ )
{
int cellX = i % TemplateSourceColumns;
int cellY = i / TemplateSourceColumns;
var slotPixmap = new Pixmap( cellW, cellH );
slotPixmap.Clear( Color.Transparent );
using ( Paint.ToPixmap( slotPixmap ) )
{
Paint.ClearPen();
Paint.SetBrush( Color.Transparent );
Paint.DrawRect( new Rect( 0, 0, cellW, cellH ) );
Paint.Draw(
new Rect(
-cellX * cellW,
-cellY * cellH,
_templatePixmap.Width,
_templatePixmap.Height
),
_templatePixmap
);
}
_templateSlotPixmaps[i] = slotPixmap;
}
}
public void ClearTemplate()
{
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
_templateSlotIds[i] = Guid.Empty;
}
_selectedTileIndex = -1;
_tileDataWidget?.SetSelectedTileIndex( _selectedTileIndex );
_draggedTileIndex = -1;
_pendingSourceRect = default;
UpdateStatus();
Update();
}
public void LoadTemplateFromCurrentOrder()
{
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
_templateSlotIds[i] = Guid.Empty;
}
if ( HasTemplate && _tileset?.Tiles != null )
{
for ( int i = 0; i < SlotCount && i < _tileset.Tiles.Count; i++ )
{
var tile = _tileset.Tiles[i];
if ( tile == null )
continue;
_templateSlotIds[i] = tile.Id;
}
}
UpdateStatus();
Update();
}
private void UpdateStatus()
{
if ( _tileset == null )
{
_statusText = "No tileset.";
return;
}
if ( _sourceTexture == null || !_sourceTexture.IsValid )
{
_statusText = "No valid tileset image. Set Tileset Image first.";
return;
}
string selected = _selectedTileIndex >= 0
? $"Selected rect: {_selectedTileIndex}"
: "Selected rect: none";
string pending = _pendingSourceRect.Width > 0 && _pendingSourceRect.Height > 0
? $"Pending rect: {_pendingSourceRect.Left:0},{_pendingSourceRect.Top:0} {_pendingSourceRect.Width:0}x{_pendingSourceRect.Height:0}"
: "Pending rect: none";
if ( !HasTemplate )
{
_statusText =
$"Image: {_sourceTextureSize.x}x{_sourceTextureSize.y} | Rects: {_tileset.Tiles?.Count ?? 0} | No autotile template | {selected} | {pending}";
return;
}
int assignedSlots = 0;
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
if ( _templateSlotIds[i] != Guid.Empty )
assignedSlots++;
}
string templateStatus = _templatePixmap != null
? "Template: loaded"
: "Template: missing";
_statusText =
$"Image: {_sourceTextureSize.x}x{_sourceTextureSize.y} | Rects: {_tileset.Tiles?.Count ?? 0} | Template slots: {assignedSlots}/{SlotCount} | {templateStatus} | {selected} | {pending}";
}
protected override void OnPaint()
{
base.OnPaint();
Paint.ClearPen();
Paint.SetBrush( Color.Black.WithAlpha( 0.25f ) );
Paint.DrawRect( LocalRect );
CalculateLayoutRects();
DrawTitles();
DrawSourceImage();
DrawTemplate();
DrawStatusText();
DrawDraggedTileOverlay();
}
private void CalculateLayoutRects()
{
float top = 8.0f;
float padding = 8.0f;
float gap = 18.0f;
float statusHeight = 24.0f;
float bottomReserved = padding + statusHeight + 8.0f;
float width = Math.Max( 1.0f, LocalRect.Width - padding * 2.0f );
float leftWidth = HasTemplate ? width * 0.56f : width;
float rightWidth = HasTemplate ? width - leftWidth - gap : 0.0f;
float texW = Math.Max( 1, _sourceTextureSize.x );
float texH = Math.Max( 1, _sourceTextureSize.y );
float sourceMaxW = leftWidth;
float dataPanelHeight = Math.Max( 96.0f, _tileDataWidget?.PreferredContentHeight ?? 154.0f );
float dataPanelReservedHeight = dataPanelHeight + 24.0f;
float sourceMaxH = Math.Max( 1.0f, LocalRect.Height - top - 28.0f - dataPanelReservedHeight - bottomReserved - 24.0f );
float sourceScale = Math.Min( sourceMaxW / texW, sourceMaxH / texH );
if ( sourceScale > 1.0f )
sourceScale = MathF.Floor( sourceScale );
sourceScale = Math.Max( 1.0f, sourceScale );
_sourceImageRect = new Rect(
padding,
top + 28.0f,
texW * sourceScale,
texH * sourceScale
);
float tileDataTop = _sourceImageRect.Bottom + 24.0f;
_tileDataRect = new Rect(
padding,
tileDataTop,
leftWidth,
dataPanelHeight
);
UpdateTileDataWidgetGeometry();
if ( !HasTemplate )
{
_templateRect = default;
return;
}
float templateX = padding + leftWidth + gap;
float templateY = top + 28.0f;
float aspectH = rightWidth * TemplateRows / Math.Max( 1.0f, TemplateColumns );
float maxTemplateH = Math.Max( 1.0f, LocalRect.Height - templateY - bottomReserved );
_templateRect = new Rect(
templateX,
templateY,
rightWidth,
Math.Min( aspectH, maxTemplateH )
);
}
private void UpdateTileDataWidgetGeometry()
{
if ( _tileDataWidget == null )
return;
_tileDataWidget.Position = new Vector2( _tileDataRect.Left, _tileDataRect.Top );
_tileDataWidget.FixedWidth = Math.Max( 1.0f, _tileDataRect.Width );
_tileDataWidget.FixedHeight = Math.Max( 1.0f, _tileDataRect.Height );
}
private void DrawTitles()
{
Paint.SetPen( Color.White );
Paint.DrawText(
new Rect( _sourceImageRect.Left, _sourceImageRect.Top - 24.0f, _sourceImageRect.Width, 20.0f ),
"Source Image - drag empty space to define rects, drag existing rects to template",
TextFlag.LeftCenter
);
if ( !HasTemplate )
return;
Paint.DrawText(
new Rect( _templateRect.Left, _templateRect.Top - 24.0f, _templateRect.Width, 20.0f ),
_template?.Title ?? "Order Template",
TextFlag.LeftCenter
);
}
private void DrawStatusText()
{
if ( string.IsNullOrWhiteSpace( _statusText ) )
return;
Rect rect = new Rect(
8.0f,
Math.Max( 0.0f, LocalRect.Height - 30.0f ),
Math.Max( 1.0f, LocalRect.Width - 16.0f ),
22.0f
);
Paint.ClearPen();
Paint.SetBrush( Color.Black.WithAlpha( 0.35f ) );
Paint.DrawRect( rect, 3.0f );
Paint.SetPen( Color.White.WithAlpha( 0.90f ) );
Paint.DrawText( new Rect( rect.Left + 6.0f, rect.Top, rect.Width - 12.0f, rect.Height ), _statusText, TextFlag.LeftCenter );
}
private void DrawSourceImage()
{
if ( _sourcePixmap == null )
{
Paint.SetPen( Color.Red );
Paint.DrawText( _sourceImageRect, "No image loaded", TextFlag.Center );
return;
}
Paint.SetBrush( Color.Black );
Paint.ClearPen();
Paint.DrawRect( GrowRect( _sourceImageRect, 1.0f ) );
Paint.Draw( _sourceImageRect, _sourcePixmap );
DrawSourceGrid();
DrawExistingRectsOnSource();
DrawPendingRect();
}
private void DrawSourceGrid()
{
if ( _tileset == null )
return;
if ( _tileset.TileSize.x <= 0 || _tileset.TileSize.y <= 0 )
return;
int stepX = Math.Max( 1, _tileset.TileSize.x + Math.Max( 0, _tileset.TileSeparation.x ) );
int stepY = Math.Max( 1, _tileset.TileSize.y + Math.Max( 0, _tileset.TileSeparation.y ) );
Paint.SetPen( Color.Cyan.WithAlpha( 0.45f ), 1.0f );
for ( int x = 0; x <= _sourceTextureSize.x; x += stepX )
{
Vector2 a = SourcePixelToScreen( new Vector2( x, 0 ) );
Vector2 b = SourcePixelToScreen( new Vector2( x, _sourceTextureSize.y ) );
Paint.DrawLine( a, b );
}
for ( int y = 0; y <= _sourceTextureSize.y; y += stepY )
{
Vector2 a = SourcePixelToScreen( new Vector2( 0, y ) );
Vector2 b = SourcePixelToScreen( new Vector2( _sourceTextureSize.x, y ) );
Paint.DrawLine( a, b );
}
}
private void DrawExistingRectsOnSource()
{
if ( _tileset?.Tiles == null )
return;
for ( int i = 0; i < _tileset.Tiles.Count; i++ )
{
var tile = _tileset.Tiles[i];
if ( tile == null )
continue;
Rect rect = SourcePixelRectToScreen( tile.SourceRect );
bool selected = i == _selectedTileIndex;
bool assigned = HasTemplate && IsTileAssignedToTemplate( tile.Id );
Color border;
if ( selected )
border = Color.Yellow;
else if ( assigned )
border = Color.Green;
else
border = Color.Cyan;
Paint.ClearBrush();
Paint.SetPen( border, selected ? 3.0f : 1.0f );
Paint.DrawRect( rect );
DrawSmallTextTopLeft( rect, i.ToString(), Color.Red );
if ( assigned )
{
DrawSmallTextBottomCenter( rect, GetAssignedSlotText( tile.Id ), Color.Yellow );
}
}
}
private void DrawPendingRect()
{
Rect rect = _draggingNewRect ? GetCurrentDraggedSourceRect() : _pendingSourceRect;
if ( rect.Width <= 0 || rect.Height <= 0 )
return;
Rect screenRect = SourcePixelRectToScreen( rect );
Paint.ClearBrush();
Paint.SetPen( Color.Green, 2.0f );
Paint.DrawRect( screenRect );
}
private void DrawTemplate()
{
if ( !HasTemplate )
return;
Paint.SetBrush( Color.Black.WithAlpha( 0.35f ) );
Paint.ClearPen();
Paint.DrawRect( _templateRect );
for ( int i = 0; i < SlotCount; i++ )
{
Rect slot = GetTemplateSlotRect( i );
bool hovering = slot.IsInside( _mouseLocalPosition );
Guid assignedId = _templateSlotIds[i];
TileDefinition assignedTile = GetTileById( assignedId );
int assignedTileIndex = GetTileIndexById( assignedId );
bool assigned = assignedTile != null;
Color fill = assigned
? Color.Black.WithAlpha( 0.35f )
: Color.Black.WithAlpha( 0.25f );
if ( hovering && _draggingExistingTile )
fill = Color.Yellow.WithAlpha( 0.22f );
Color border = assigned
? Color.Yellow
: Color.White.WithAlpha( 0.6f );
Paint.SetBrush( fill );
Paint.SetPen( border, assigned ? 2.0f : 1.0f );
Paint.DrawRect( slot );
DrawTemplateSlotPlaceholder( i, slot );
if ( assigned )
{
DrawAssignedTileSpriteOverTemplateSlot( assignedTile, slot );
}
string label = _template.GetLabel( i );
string tileText = assigned ? $"Tile {assignedTileIndex}" : "empty";
DrawSmallTextTopLeft( slot, i.ToString(), Color.Red );
if ( assigned )
{
DrawSmallTextTopRight( slot, label, new Color( 0.2f, 0.8f, 0.2f ) );
}
else
{
DrawCenteredText( slot, label, Color.White.WithAlpha( 0.9f ) );
}
DrawSmallTextBottomCenter(
slot,
tileText,
assigned ? Color.Yellow : Color.White.WithAlpha( 0.55f )
);
}
}
private void DrawTemplateSlotPlaceholder( int slotIndex, Rect slot )
{
if ( slotIndex < 0 || slotIndex >= _templateSlotPixmaps.Length )
return;
var pixmap = _templateSlotPixmaps[slotIndex];
if ( pixmap == null )
return;
DrawPixmapInsideRect( pixmap, slot, 8.0f, 0.34f );
}
private void DrawAssignedTileSpriteOverTemplateSlot( TileDefinition tile, Rect slot )
{
var pixmap = GetTileSpritePixmap( tile );
if ( pixmap == null )
return;
Rect spriteBox = slot.Shrink( 14.0f );
Paint.SetBrush( Color.Black.WithAlpha( 0.70f ) );
Paint.SetPen( Color.Yellow.WithAlpha( 0.85f ), 1.0f );
Paint.DrawRect( spriteBox );
DrawPixmapInsideRect( pixmap, spriteBox, 4.0f, 0.5f );
}
private void DrawPixmapInsideRect( Pixmap pixmap, Rect rect, float padding, float verticalAnchor )
{
if ( pixmap == null )
return;
float availableW = Math.Max( 1.0f, rect.Width - padding * 2.0f );
float availableH = Math.Max( 1.0f, rect.Height - padding * 2.0f );
float scale = Math.Min(
availableW / Math.Max( 1.0f, pixmap.Width ),
availableH / Math.Max( 1.0f, pixmap.Height )
);
if ( scale > 1.0f )
scale = MathF.Floor( scale );
scale = Math.Max( 1.0f, scale );
float w = pixmap.Width * scale;
float h = pixmap.Height * scale;
Rect drawRect = new Rect(
rect.Left + (rect.Width - w) * 0.5f,
rect.Top + (rect.Height - h) * verticalAnchor,
w,
h
);
Paint.Draw( drawRect, pixmap );
}
private Pixmap GetTileSpritePixmap( TileDefinition tile )
{
if ( tile == null )
return null;
if ( tile.Id == Guid.Empty )
return null;
if ( _sourcePixmap == null )
return null;
if ( _tileSpritePixmapCache.TryGetValue( tile.Id, out var cached ) && cached != null )
return cached;
Rect sourceRect = tile.SourceRect;
int x = Math.Clamp( (int)MathF.Floor( sourceRect.Left ), 0, _sourcePixmap.Width );
int y = Math.Clamp( (int)MathF.Floor( sourceRect.Top ), 0, _sourcePixmap.Height );
int right = Math.Clamp( (int)MathF.Ceiling( sourceRect.Right ), 0, _sourcePixmap.Width );
int bottom = Math.Clamp( (int)MathF.Ceiling( sourceRect.Bottom ), 0, _sourcePixmap.Height );
int w = Math.Max( 1, right - x );
int h = Math.Max( 1, bottom - y );
var tilePixmap = new Pixmap( w, h );
tilePixmap.Clear( Color.Transparent );
using ( Paint.ToPixmap( tilePixmap ) )
{
Paint.ClearPen();
Paint.SetBrush( Color.Transparent );
Paint.DrawRect( new Rect( 0, 0, w, h ) );
// Crop the source image by drawing it offset into the small tile pixmap.
Paint.Draw(
new Rect(
-x,
-y,
_sourcePixmap.Width,
_sourcePixmap.Height
),
_sourcePixmap
);
}
_tileSpritePixmapCache[tile.Id] = tilePixmap;
return tilePixmap;
}
private void DrawDraggedTileOverlay()
{
if ( !_draggingExistingTile || _draggedTileIndex < 0 )
return;
var tile = GetTileByIndex( _draggedTileIndex );
Rect rect = new Rect(
_dragMousePosition.x - 30.0f,
_dragMousePosition.y - 30.0f,
60.0f,
60.0f
);
Paint.SetBrush( Color.Black.WithAlpha( 0.75f ) );
Paint.SetPen( Color.Yellow, 2.0f );
Paint.DrawRect( rect );
if ( tile != null )
{
var pixmap = GetTileSpritePixmap( tile );
if ( pixmap != null )
DrawPixmapInsideRect( pixmap, rect, 6.0f, 0.5f );
}
Paint.SetPen( Color.White );
Paint.DrawText(
new Rect( rect.Left, rect.Bottom - 16.0f, rect.Width, 14.0f ),
_draggedTileIndex.ToString(),
TextFlag.Center
);
}
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
_mouseLocalPosition = e.LocalPosition;
if ( !e.LeftMouseButton )
return;
Vector2 pos = e.LocalPosition;
if ( _sourceImageRect.IsInside( pos ) )
{
Vector2 pixel = ScreenToSourcePixel( pos );
int clickedTileIndex = GetTileIndexAtSourcePixel( pixel );
if ( clickedTileIndex >= 0 )
{
_selectedTileIndex = clickedTileIndex;
_tileDataWidget?.SetSelectedTileIndex( _selectedTileIndex );
_draggingExistingTile = true;
_draggedTileIndex = clickedTileIndex;
_dragMousePosition = pos;
UpdateStatus();
Update();
return;
}
_draggingNewRect = true;
_rectDragStart = SnapSourcePixel( pixel );
_rectDragEnd = _rectDragStart;
_pendingSourceRect = default;
UpdateStatus();
Update();
return;
}
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
_mouseLocalPosition = e.LocalPosition;
Vector2 pos = e.LocalPosition;
if ( _draggingNewRect )
{
_rectDragEnd = SnapSourcePixel( ScreenToSourcePixel( pos ) );
Update();
return;
}
if ( _draggingExistingTile )
{
_dragMousePosition = pos;
Update();
return;
}
Update();
}
protected override void OnMouseReleased( MouseEvent e )
{
base.OnMouseReleased( e );
_mouseLocalPosition = e.LocalPosition;
Vector2 pos = e.LocalPosition;
if ( _draggingNewRect )
{
_draggingNewRect = false;
_rectDragEnd = SnapSourcePixel( ScreenToSourcePixel( pos ) );
_pendingSourceRect = GetCurrentDraggedSourceRect();
UpdateStatus();
Update();
return;
}
if ( _draggingExistingTile )
{
_draggingExistingTile = false;
if ( HasTemplate )
{
int slot = GetTemplateSlotAtPosition( pos );
if ( slot >= 0 && _draggedTileIndex >= 0 )
{
AssignTileToTemplateSlot( _draggedTileIndex, slot );
}
}
_draggedTileIndex = -1;
UpdateStatus();
Update();
return;
}
}
public void AddPendingRect()
{
if ( _tileset == null )
return;
if ( _pendingSourceRect.Width <= 0 || _pendingSourceRect.Height <= 0 )
return;
_tileset.Tiles ??= new();
var tile = new TileDefinition
{
Name = $"Tile {_tileset.Tiles.Count}",
SourceRect = _pendingSourceRect
};
_tileset.AddTile( tile );
_selectedTileIndex = _tileset.Tiles.Count - 1;
_tileDataWidget?.SetSelectedTileIndex( _selectedTileIndex );
_pendingSourceRect = default;
_tileSpritePixmapCache.Clear();
_onChanged?.Invoke();
UpdateStatus();
Update();
}
public void DeleteSelectedTile()
{
if ( _tileset == null || _tileset.Tiles == null )
return;
if ( _selectedTileIndex < 0 || _selectedTileIndex >= _tileset.Tiles.Count )
return;
Guid removedId = _tileset.Tiles[_selectedTileIndex].Id;
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
if ( _templateSlotIds[i] == removedId )
_templateSlotIds[i] = Guid.Empty;
}
_tileset.RemoveTileAt( _selectedTileIndex );
_selectedTileIndex = -1;
_tileDataWidget?.SetSelectedTileIndex( _selectedTileIndex );
_tileSpritePixmapCache.Remove( removedId );
_onChanged?.Invoke();
UpdateStatus();
Update();
}
private void AssignTileToTemplateSlot( int tileIndex, int slot )
{
if ( !HasTemplate )
return;
if ( _tileset?.Tiles == null )
return;
if ( tileIndex < 0 || tileIndex >= _tileset.Tiles.Count )
return;
if ( slot < 0 || slot >= _templateSlotIds.Length )
return;
var tile = _tileset.Tiles[tileIndex];
if ( tile == null )
return;
if ( tile.Id == Guid.Empty )
tile.Id = Guid.NewGuid();
Guid tileId = tile.Id;
// Remove from any old slot first, so the same rect cannot appear twice.
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
if ( _templateSlotIds[i] == tileId )
_templateSlotIds[i] = Guid.Empty;
}
_templateSlotIds[slot] = tileId;
_selectedTileIndex = tileIndex;
_tileDataWidget?.SetSelectedTileIndex( _selectedTileIndex );
}
public void ApplyTemplateOrder()
{
if ( !HasTemplate )
return;
if ( _tileset?.Tiles == null )
return;
var ordered = new List<TileDefinition>();
for ( int i = 0; i < SlotCount; i++ )
{
var tile = GetTileById( _templateSlotIds[i] );
if ( tile == null )
continue;
if ( ordered.Contains( tile ) )
continue;
ordered.Add( tile );
}
if ( ordered.Count == 0 )
return;
_tileset.ReorderTiles( ordered );
LoadTemplateFromCurrentOrder();
_selectedTileIndex = -1;
_tileDataWidget?.SetSelectedTileIndex( _selectedTileIndex );
_tileSpritePixmapCache.Clear();
_onChanged?.Invoke();
UpdateStatus();
Update();
}
private Rect GetCurrentDraggedSourceRect()
{
float left = Math.Min( _rectDragStart.x, _rectDragEnd.x );
float top = Math.Min( _rectDragStart.y, _rectDragEnd.y );
float right = Math.Max( _rectDragStart.x, _rectDragEnd.x );
float bottom = Math.Max( _rectDragStart.y, _rectDragEnd.y );
left = Math.Clamp( left, 0, _sourceTextureSize.x );
right = Math.Clamp( right, 0, _sourceTextureSize.x );
top = Math.Clamp( top, 0, _sourceTextureSize.y );
bottom = Math.Clamp( bottom, 0, _sourceTextureSize.y );
return new Rect( left, top, right - left, bottom - top );
}
private int GetTileIndexAtSourcePixel( Vector2 pixel )
{
if ( _tileset?.Tiles == null )
return -1;
for ( int i = _tileset.Tiles.Count - 1; i >= 0; i-- )
{
var tile = _tileset.Tiles[i];
if ( tile == null )
continue;
if ( RectContains( tile.SourceRect, pixel ) )
return i;
}
return -1;
}
private int GetTemplateSlotAtPosition( Vector2 pos )
{
if ( !HasTemplate )
return -1;
for ( int i = 0; i < SlotCount; i++ )
{
if ( GetTemplateSlotRect( i ).IsInside( pos ) )
return i;
}
return -1;
}
private Rect GetTemplateSlotRect( int index )
{
int col = index % TemplateColumns;
int row = index / TemplateColumns;
float gap = 6.0f;
float slotW = (_templateRect.Width - gap * (TemplateColumns + 1)) / TemplateColumns;
float slotH = (_templateRect.Height - gap * (TemplateRows + 1)) / TemplateRows;
return new Rect(
_templateRect.Left + gap + col * (slotW + gap),
_templateRect.Top + gap + row * (slotH + gap),
slotW,
slotH
);
}
private Vector2 ScreenToSourcePixel( Vector2 localPosition )
{
float u = (localPosition.x - _sourceImageRect.Left) / Math.Max( 1.0f, _sourceImageRect.Width );
float v = (localPosition.y - _sourceImageRect.Top) / Math.Max( 1.0f, _sourceImageRect.Height );
u = Math.Clamp( u, 0.0f, 1.0f );
v = Math.Clamp( v, 0.0f, 1.0f );
return new Vector2(
u * _sourceTextureSize.x,
v * _sourceTextureSize.y
);
}
private Vector2 SourcePixelToScreen( Vector2 pixel )
{
float u = pixel.x / Math.Max( 1.0f, _sourceTextureSize.x );
float v = pixel.y / Math.Max( 1.0f, _sourceTextureSize.y );
return new Vector2(
_sourceImageRect.Left + u * _sourceImageRect.Width,
_sourceImageRect.Top + v * _sourceImageRect.Height
);
}
private Rect SourcePixelRectToScreen( Rect pixelRect )
{
Vector2 a = SourcePixelToScreen( new Vector2( pixelRect.Left, pixelRect.Top ) );
Vector2 b = SourcePixelToScreen( new Vector2( pixelRect.Right, pixelRect.Bottom ) );
return new Rect(
a.x,
a.y,
b.x - a.x,
b.y - a.y
);
}
private Vector2 SnapSourcePixel( Vector2 pixel )
{
if ( _tileset == null )
return pixel;
pixel.x = Math.Clamp( pixel.x, 0, _sourceTextureSize.x );
pixel.y = Math.Clamp( pixel.y, 0, _sourceTextureSize.y );
int tileW = Math.Max( 1, _tileset.TileSize.x );
int tileH = Math.Max( 1, _tileset.TileSize.y );
pixel.x = MathF.Round( pixel.x / tileW ) * tileW;
pixel.y = MathF.Round( pixel.y / tileH ) * tileH;
return pixel;
}
private bool RectContains( Rect rect, Vector2 point )
{
return point.x >= rect.Left
&& point.x < rect.Right
&& point.y >= rect.Top
&& point.y < rect.Bottom;
}
private bool IsTileAssignedToTemplate( Guid id )
{
if ( id == Guid.Empty )
return false;
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
if ( _templateSlotIds[i] == id )
return true;
}
return false;
}
private string GetAssignedSlotText( Guid id )
{
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
if ( _templateSlotIds[i] == id )
return $"Slot {i}";
}
return "";
}
private TileDefinition GetTileById( Guid id )
{
if ( id == Guid.Empty || _tileset?.Tiles == null )
return null;
foreach ( var tile in _tileset.Tiles )
{
if ( tile == null )
continue;
if ( tile.Id == id )
return tile;
}
return null;
}
private TileDefinition GetTileByIndex( int index )
{
if ( _tileset?.Tiles == null )
return null;
if ( index < 0 || index >= _tileset.Tiles.Count )
return null;
return _tileset.Tiles[index];
}
private int GetTileIndexById( Guid id )
{
if ( id == Guid.Empty || _tileset?.Tiles == null )
return -1;
for ( int i = 0; i < _tileset.Tiles.Count; i++ )
{
var tile = _tileset.Tiles[i];
if ( tile == null )
continue;
if ( tile.Id == id )
return i;
}
return -1;
}
private void RemoveInvalidTemplateAssignments()
{
for ( int i = 0; i < _templateSlotIds.Length; i++ )
{
if ( _templateSlotIds[i] == Guid.Empty )
continue;
if ( GetTileById( _templateSlotIds[i] ) == null )
_templateSlotIds[i] = Guid.Empty;
}
}
private Rect GrowRect( Rect rect, float amount )
{
return new Rect(
rect.Left - amount,
rect.Top - amount,
rect.Width + amount * 2.0f,
rect.Height + amount * 2.0f
);
}
private void DrawSmallTextTopLeft( Rect rect, string text, Color color )
{
Paint.SetPen( color );
Paint.DrawText(
new Rect( rect.Left + 4.0f, rect.Top + 2.0f, rect.Width - 8.0f, 16.0f ),
text,
TextFlag.LeftCenter
);
}
private void DrawSmallTextTopRight( Rect rect, string text, Color color )
{
Paint.SetPen( color );
Paint.DrawText(
new Rect( rect.Left + 4.0f, rect.Top + 2.0f, rect.Width - 8.0f, 16.0f ),
text,
TextFlag.RightCenter
);
}
private void DrawSmallTextBottomCenter( Rect rect, string text, Color color )
{
Paint.SetPen( color );
Paint.DrawText(
new Rect( rect.Left + 4.0f, rect.Bottom - 18.0f, rect.Width - 8.0f, 16.0f ),
text,
TextFlag.Center
);
}
private void DrawCenteredText( Rect rect, string text, Color color )
{
Paint.SetPen( color );
Paint.DrawText(
new Rect( rect.Left + 4.0f, rect.Top + 18.0f, rect.Width - 8.0f, rect.Height - 36.0f ),
text,
TextFlag.Center
);
}
}