Editor/RectEditorExport/Window.cs
using Editor.TextureEditor;
using Sandbox.Helpers;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text.Json;
namespace Editor.rectedittemplateexporter;
[EditorForAssetType( "rect" )]
[EditorApp( "Hotspot Editor", "dashboard", "For defining hotspot materials" )]
public partial class Window : DockWindow, IAssetEditor
{
public bool CanOpenMultipleAssets => false;
public Document Document { get; private set; } = new();
private Asset Asset;
private Asset[] MaterialReferences;
private ToolBar ToolBar;
private RectView RectView;
private Properties Properties;
private MaterialReference MaterialReference;
private UndoSystem UndoSystem;
private string DefaultDockState;
public int GridPower { get; set; } = 4;
public bool GridEnabled { get; set; } = true;
public Window()
{
Name = "RectEditor";
Size = new Vector2( 1000, 800 );
DeleteOnClose = true;
if ( AssetType.All.FirstOrDefault( x => x.FileExtension == "rect" ) is AssetType assetType )
SetWindowIcon( assetType.Icon128 );
InitUndo();
NewDocument();
CreateUI();
}
private void InitUndo()
{
UndoSystem = new UndoSystem();
UndoSystem.SetSnapshotFunction( SnapshotForUndo );
UndoSystem.Initialize();
}
[Shortcut( "editor.quit", "CTRL+Q" )]
void Quit()
{
Close();
}
[Shortcut( "editor.undo", "CTRL+Z" )]
private bool Undo()
{
return UndoSystem.Undo();
}
[Shortcut( "editor.redo", "CTRL+Y" )]
private bool Redo()
{
return UndoSystem.Redo();
}
public void Snapshot( string title )
{
UndoSystem.Snapshot( title );
}
[Shortcut( "editor.delete", "DEL" )]
private void DeleteSelected()
{
if ( !Document.SelectedRectangles.Any() )
return;
Document.DeleteRectangles( Document.SelectedRectangles.ToArray() );
Snapshot( "Delete Rectangles" );
}
[Shortcut( "editor.clear-selection", "ESC" )]
private void ClearSelection()
{
if ( !Document.SelectedRectangles.Any() )
return;
Document.ClearSelection();
Snapshot( "Clear Selection" );
}
[Shortcut( "editor.select-all", "CTRL+A" )]
private void SelectAll()
{
Document.SelectAll();
Snapshot( "Select All" );
}
private void CreateToolBar()
{
ToolBar = new ToolBar( this, "RectToolbar" );
ToolBar.MinimumSize = 24;
ToolBar.SetIconSize( 24 );
AddToolBar( ToolBar, ToolbarPosition.Top );
ToolBar.AddOption( new Option( "New", "common/new.png", New ) { ShortcutName = "editor.new" } );
ToolBar.AddOption( new Option( "Open", "common/open.png", Open ) { ShortcutName = "editor.open" } );
ToolBar.AddOption( new Option( "Save", "common/save.png", Save ) { ShortcutName = "editor.save" } );
ToolBar.AddSeparator();
ToolBar.AddOption( new Option( "Undo", "undo", () => Undo() ) { ShortcutName = "editor.undo" } );
ToolBar.AddOption( new Option( "Redo", "redo", () => Redo() ) { ShortcutName = "editor.redo" } );
ToolBar.AddSeparator();
ToolBar.AddOption( new Option( "Grid Enabled", "grid_4x4" )
{
ShortcutName = "grid.toggle-grid",
Checkable = true,
Checked = GridEnabled,
Toggled = SetGridEnabled
} );
ToolBar.AddOption( new Option( "Increase Grid Size", "keyboard_arrow_up", BiggerGrid ) { ShortcutName = "editor.increase-grid-size" } );
ToolBar.AddOption( new Option( "Decrease Grid Size", "keyboard_arrow_down", SmallerGrid ) { ShortcutName = "editor.decrease-grid-size" } );
ToolBar.AddSeparator();
ToolBar.AddOption( new Option( "SaveAsTex", "texture", SaveAsTex ) { ShortcutName = "editor.saveastex" } );
ToolBar.AddSeparator();
}
[Shortcut( "grid.toggle-grid", "CTRL+G" )]
private void SetGridEnabled( bool enabled )
{
GridEnabled = enabled;
Update();
}
[Shortcut( "grid.increase-grid-size", "]" )]
private void BiggerGrid()
{
GridPower = Math.Max( GridPower - 1, 1 );
Update();
}
[Shortcut( "grid.decrease-grid-size", "[" )]
private void SmallerGrid()
{
GridPower = Math.Min( GridPower + 1, 8 );
Update();
}
private void CreateUI()
{
CreateToolBar();
CreateMenu();
UpdateTitle();
DockManager.RegisterDockType( "Rect View", "space_dashboard", null, false );
RectView = new RectView( this );
DockManager.AddDock( null, RectView, DockArea.Right, DockManager.DockProperty.HideOnClose, 0.0f );
DockManager.RegisterDockType( "Properties", "edit", null, false );
Properties = new Properties( this );
UpdateProperties();
DockManager.AddDock( null, Properties, DockArea.Left, DockManager.DockProperty.HideOnClose, 0.2f );
DockManager.RegisterDockType( "Material Reference", "texture", null, false );
MaterialReference = new MaterialReference( this, OnReferenceChanged );
MaterialReference.SetReferences( MaterialReferences );
DockManager.AddDock( Properties, MaterialReference, DockArea.Bottom, DockManager.DockProperty.HideOnClose, 0.4f );
DockManager.Update();
DefaultDockState = DockManager.State;
if ( StateCookie != Name )
{
StateCookie = Name;
}
else
{
RestoreFromStateCookie();
}
}
private void OnDocumentModified()
{
UpdateTitle();
UpdateProperties();
Update();
}
private void UpdateProperties()
{
if ( !Properties.IsValid() )
return;
if ( Document.SelectedRectangles.Count > 1 )
{
var me = new MultiSerializedObject();
foreach ( var rectangle in Document.SelectedRectangles )
{
var so = rectangle.GetSerialized();
if ( so is not null )
{
me.Add( so );
}
}
me.Rebuild();
Properties.SerializedObject = me;
}
else if ( Document.SelectedRectangles.Count > 0 )
{
Properties.SerializedObject = Document.SelectedRectangles.FirstOrDefault()?.GetSerialized();
}
else
{
Properties.SerializedObject = null;
}
}
private void UpdateTitle()
{
var title = Asset is null ? "Untitled" : Asset.Path;
WindowTitle = Document is not null && Document.Modified ? $"{title}*" : title;
}
private void OnReferenceChanged( Asset asset )
{
RectView.SetMaterial( asset?.LoadResource<Material>() );
}
public void CreateMenu()
{
var edit = MenuBar.AddMenu( "Edit" );
edit.AddOption( new Option( "Select All", null, SelectAll ) { ShortcutName = "editor.select-all" } );
edit.AddOption( new Option( "Clear Selection", null, ClearSelection ) { ShortcutName = "editor.clear-selection" } );
edit.AddOption( new Option( "Delete Selected", null, DeleteSelected ) { ShortcutName = "editor.delete" } );
var view = MenuBar.AddMenu( "View" );
view.AboutToShow += () => OnViewMenu( view );
}
private void OnViewMenu( Menu view )
{
view.Clear();
view.AddOption( "Restore To Default", "settings_backup_restore", RestoreDefaultDockLayout );
view.AddSeparator();
foreach ( var dock in DockManager.DockTypes )
{
var o = view.AddOption( dock.Title, dock.Icon );
o.Checkable = true;
o.Checked = DockManager.IsDockOpen( dock.Title );
o.Toggled += ( b ) => DockManager.SetDockState( dock.Title, b );
}
}
public void AssetOpen( Asset asset )
{
Show();
var assetData = RectAssetData.Find( asset );
if ( assetData is null )
return;
Asset = asset;
Document = new Document( assetData, OnDocumentModified );
MaterialReferences = AssetSystem.All.Where( x => x.AssetType == AssetType.Material && x.GetAdditionalRelatedFiles()
.Any( x => x.Contains( asset.Name ) ) )
.ToArray();
MaterialReference.SetReferences( MaterialReferences );
UndoSystem.Initialize();
OnDocumentModified();
}
protected override void OnClosed()
{
base.OnClosed();
}
[Shortcut( "editor.new", "CTRL+N" )]
private void New()
{
NewDocument();
}
private void NewDocument()
{
Document = new Document();
Document.OnModified = OnDocumentModified;
Asset = null;
MaterialReferences = null;
MaterialReference?.SetReferences( null );
UndoSystem.Initialize();
OnDocumentModified();
}
[Shortcut( "editor.open", "CTRL+O" )]
private void Open()
{
var fd = new FileDialog( null )
{
Title = "Open Rect",
DefaultSuffix = $".rect"
};
fd.SetNameFilter( "Rect (*.rect)" );
if ( !fd.Execute() )
return;
Open( fd.SelectedFile );
}
public void Open( string path )
{
var asset = AssetSystem.FindByPath( path );
if ( asset is null )
return;
if ( asset.AssetType.FileExtension != "rect" )
return;
AssetOpen( asset );
}
[Shortcut( "editor.save", "CTRL+S" )]
private void Save()
{
if ( Document is null )
return;
string savePath = null;
if ( Asset is null )
{
var fd = new FileDialog( null )
{
Title = $"Save Rect",
DefaultSuffix = $".rect"
};
fd.SelectFile( $"untitled.rect" );
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter( "Rect (*.rect)" );
if ( !fd.Execute() )
return;
savePath = fd.SelectedFile;
if ( string.IsNullOrWhiteSpace( savePath ) )
return;
}
else if ( AssetSystem.IsCloudInstalled( Asset.Package ) )
{
return;
}
var assetData = new RectAssetData
{
RectangleSets = new List<RectAssetData.SubrectSet>()
};
var subrectSet = new RectAssetData.SubrectSet
{
Rectangles = new List<RectAssetData.Subrect>()
};
foreach ( var rectangle in Document.Rectangles )
{
subrectSet.Rectangles.Add( new RectAssetData.Subrect
{
Min = new int[] { (int)(rectangle.Min.x.Clamp( 0, 1 ) * 32768), (int)(rectangle.Min.y.Clamp( 0, 1 ) * 32768) },
Max = new int[] { (int)(rectangle.Max.x.Clamp( 0, 1 ) * 32768), (int)(rectangle.Max.y.Clamp( 0, 1 ) * 32768) },
Properties = new RectAssetData.Properties
{
AllowRotation = rectangle.AllowRotation,
AllowTiling = rectangle.AllowTiling,
}
} );
}
assetData.RectangleSets.Add( subrectSet );
var path = Asset is null ? savePath : Asset.GetSourceFile( true );
System.IO.File.WriteAllText( path, Json.Serialize( assetData ) );
Asset ??= AssetSystem.RegisterFile( savePath );
MainAssetBrowser.Instance?.UpdateAssetList();
Document.Modified = false;
string rectFilePath = savePath;
OnDocumentModified();
}
private void SaveAsTex()
{
string rectFilePath = Asset.Path;
string rectFilePathtemp = Asset.AbsolutePath;
string directoryPath = Path.GetDirectoryName( rectFilePathtemp );
string outputTexturePath = directoryPath;
int textureWidth = 2048;
int textureHeight = 2048;
GenerateTextureFromRectFile( rectFilePath, outputTexturePath, textureWidth, textureHeight );
}
public static void GenerateTextureFromRectFile( string rectFilePath, string outputTexturePath, int textureWidth = 1024, int textureHeight = 1024 )
{
if ( string.IsNullOrWhiteSpace( rectFilePath ) || string.IsNullOrWhiteSpace( outputTexturePath ) )
{
Log.Error( "File paths cannot be empty." );
return;
}
string rectData;
try
{
rectData = FileSystem.Mounted.ReadAllText( rectFilePath );
}
catch ( Exception ex )
{
Log.Error( $"Failed to read the .rect file: {ex.Message}" );
return;
}
RectAssetData assetData;
try
{
assetData = System.Text.Json.JsonSerializer.Deserialize<RectAssetData>( rectData, new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
} );
}
catch ( Exception ex )
{
Log.Error( $"Failed to parse the .rect file: {ex.Message}" );
return;
}
if ( assetData == null || assetData.RectangleSets == null )
{
Log.Error( "The .rect file contains no data." );
return;
}
// Create a blank texture
var texture = Texture.Create( textureWidth, textureHeight )
.WithFormat( ImageFormat.RGBA8888 )
.WithDynamicUsage()
.Finish();
var pixels = new byte[textureWidth * textureHeight * 4];
Random random = new Random();
List<byte> allValues = Enumerable.Range( 0, 256 ).Select( x => (byte)x ).ToList();
for ( int i = allValues.Count - 1; i > 0; i-- )
{
int j = random.Next( 0, i + 1 );
byte temp = allValues[i];
allValues[i] = allValues[j];
allValues[j] = temp;
}
int grayIndex = 0;
foreach ( var set in assetData.RectangleSets )
{
foreach ( var rect in set.Rectangles )
{
byte grayValue = allValues[grayIndex];
grayIndex = (grayIndex + 1) % allValues.Count;
Log.Info( "Gray Value: " + grayValue.ToString() );
int x1 = rect.Min[0] * textureWidth / 32768;
int y1 = rect.Min[1] * textureHeight / 32768;
int x2 = rect.Max[0] * textureWidth / 32768;
int y2 = rect.Max[1] * textureHeight / 32768;
x1 = Math.Max( 0, Math.Min( textureWidth, x1 ) );
y1 = Math.Max( 0, Math.Min( textureHeight, y1 ) );
x2 = Math.Max( 0, Math.Min( textureWidth, x2 ) );
y2 = Math.Max( 0, Math.Min( textureHeight, y2 ) );
// Fill rectangle with grayscale value
for ( int y = y1; y < y2; y++ )
{
for ( int x = x1; x < x2; x++ )
{
int index = (y * textureWidth + x) * 4;
pixels[index + 0] = grayValue; // R
pixels[index + 1] = grayValue; // G
pixels[index + 2] = grayValue; // B
pixels[index + 3] = 255; // Alpha
}
}
}
}
// Update the texture with pixel data
texture.Update( pixels );
var asset = AssetSystem.FindByPath( rectFilePath );
Log.Info( asset );
if ( asset is null )
return;
if ( asset.AssetType.FileExtension != "rect" )
return;
string assetName = Path.GetFileNameWithoutExtension( asset.Name );
try
{
var pixelMap = Pixmap.FromTexture( texture );
string filePath = Path.Combine( outputTexturePath, $"{assetName}_SDBASE.png" );
pixelMap.SavePng( filePath );
}
catch ( Exception ex )
{
Log.Error( $"Failed to save the texture: {ex.Message}" );
}
}
private Action SnapshotForUndo()
{
var json = Json.Serialize( Document );
return () =>
{
Document = Json.Deserialize<Document>( json );
Document.OnModified = OnDocumentModified;
OnDocumentModified();
};
}
public void SelectMember( string memberName )
{
throw new NotImplementedException();
}
protected override void RestoreDefaultDockLayout()
{
DockManager.State = DefaultDockState;
SaveToStateCookie();
}
[EditorEvent.Hotload]
public void OnHotload()
{
SaveToStateCookie();
RemoveToolBar( ToolBar );
ToolBar.Destroy();
ToolBar = null;
DockManager.Clear();
MenuBar.Clear();
CreateUI();
}
}