Files
sbox-public/game/editor/ShaderGraph/Code/MainWindow.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

1268 lines
31 KiB
C#

namespace Editor.ShaderGraph;
[EditorForAssetType( "shdrfunc" )]
public class MainWindowFunc : MainWindow, IAssetEditor
{
public override bool IsSubgraph => true;
public override string FileType => "Shader Sub-Graph";
public override string FileExtension => "shdrfunc";
void IAssetEditor.SelectMember( string memberName )
{
throw new NotImplementedException();
}
}
[EditorForAssetType( "shdrgrph" )]
[EditorApp( "Shader Graph", "gradient", "edit shaders" )]
public class MainWindowShader : MainWindow, IAssetEditor
{
public override bool IsSubgraph => false;
public override string FileType => "Shader Graph";
public override string FileExtension => "shdrgrph";
void IAssetEditor.SelectMember( string memberName )
{
throw new NotImplementedException();
}
}
public class MainWindow : DockWindow
{
public virtual bool IsSubgraph => false;
public virtual string FileType => "Shader Graph";
public virtual string FileExtension => "shdrgrph";
protected ShaderGraph _graph;
protected ShaderGraphView _graphView;
private Asset _asset;
public string AssetPath => _asset?.Path;
private Widget _graphCanvas;
private Properties _properties;
protected PreviewPanel _preview;
protected Output _output;
private UndoHistory _undoHistory;
private PaletteWidget _palette;
private readonly UndoStack _undoStack = new();
private Option _undoOption;
private Option _redoOption;
private Option _undoMenuOption;
private Option _redoMenuOption;
public UndoStack UndoStack => _undoStack;
private bool _dirty = false;
private string _generatedCode;
private readonly Dictionary<string, Texture> _textureAttributes = new();
private readonly Dictionary<string, Color> _float4Attributes = new();
private readonly Dictionary<string, Vector3> _float3Attributes = new();
private readonly Dictionary<string, Vector2> _float2Attributes = new();
private readonly Dictionary<string, float> _floatAttributes = new();
private readonly Dictionary<string, bool> _boolAttributes = new();
private readonly List<BaseNode> _compiledNodes = new();
private bool _isCompiling = false;
private bool _isPendingCompile = false;
private RealTimeSince _timeSinceCompile;
private Menu _recentFilesMenu;
private readonly List<string> _recentFiles = new();
private Option _fileHistoryBack;
private Option _fileHistoryForward;
private Option _fileHistoryHome;
private List<string> _fileHistory = new();
private int _fileHistoryPosition = 0;
private string _defaultDockState;
public bool CanOpenMultipleAssets => true;
public MainWindow()
{
DeleteOnClose = true;
Title = FileType;
Size = new Vector2( 1700, 1050 );
_graph = new();
_graph.IsSubgraph = IsSubgraph;
CreateToolBar();
_recentFiles = FileSystem.Temporary.ReadJsonOrDefault( "shadergraph_recentfiles.json", _recentFiles )
.Where( System.IO.File.Exists ).ToList();
CreateUI();
Show();
CreateNew();
}
public void AssetOpen( Asset asset )
{
if ( asset == null || string.IsNullOrWhiteSpace( asset.AbsolutePath ) )
return;
PromptSave( () => Open( asset.AbsolutePath ) );
}
private void RestoreShader()
{
if ( !_preview.IsValid() )
return;
_preview.Material = Material.Load( "materials/core/shader_editor.vmat" );
}
public void OnNodeSelected( BaseNode node )
{
_properties.Target = node != null ? node : _graph;
_preview.SetStage( _compiledNodes.IndexOf( node ) + 1 );
}
private void OpenGeneratedShader()
{
if ( _asset is null )
{
Save();
}
else
{
var path = System.IO.Path.ChangeExtension( _asset.AbsolutePath, ".shader" );
var asset = AssetSystem.FindByPath( path );
asset?.OpenInEditor();
}
}
private void Screenshot()
{
if ( _asset is null )
return;
var path = FileSystem.Root.GetFullPath( $"/screenshots/shadergraphs/{_asset.Name}.png" );
System.IO.Directory.CreateDirectory( System.IO.Path.GetDirectoryName( path ) );
_graphView.Capture( $"screenshots/shadergraphs/{_asset.Name}.png" );
EditorUtility.OpenFileFolder( path );
}
protected virtual void Compile()
{
_shaderCompileErrors.Clear();
var compileErrors = new List<GraphCompiler.Error>();
foreach ( var node in _graph.Nodes )
{
if ( node is IErroringNode erroring )
{
var errors = erroring.GetErrors();
if ( errors.Count > 0 )
{
_shaderCompileErrors.AddRange( errors );
if ( IsSubgraph )
{
foreach ( var error in errors )
{
compileErrors.Add( new() { Message = error, Node = node } );
}
}
}
}
}
_output.Errors = compileErrors;
if ( string.IsNullOrWhiteSpace( _generatedCode ) )
{
RestoreShader();
return;
}
if ( _isCompiling )
{
_isPendingCompile = true;
return;
}
var assetPath = $"shadergraph/{_asset?.Name ?? "untitled"}_shadergraph.generated.shader";
var resourcePath = System.IO.Path.Combine( ".source2/temp", assetPath );
FileSystem.Root.CreateDirectory( ".source2/temp/shadergraph" );
FileSystem.Root.WriteAllText( resourcePath, _generatedCode );
_isCompiling = true;
_preview.IsCompiling = _isCompiling;
RestoreShader();
_timeSinceCompile = 0;
CompileAsync( resourcePath );
}
private async void CompileAsync( string path )
{
var options = new Sandbox.Engine.Shaders.ShaderCompileOptions
{
ConsoleOutput = true
};
var result = await EditorUtility.CompileShader( FileSystem.Root, path, options );
if ( result.Success )
{
var asset = AssetSystem.RegisterFile( FileSystem.Root.GetFullPath( path ) );
while ( !asset.IsCompiledAndUpToDate )
{
await Task.Yield();
}
}
MainThread.Queue( () => OnCompileFinished( result.Success ? 0 : 1 ) );
}
protected readonly List<string> _shaderCompileErrors = new();
private struct StatusMessage
{
public string Status { get; set; }
public string Message { get; set; }
}
internal void OnOutputData( object sender, DataReceivedEventArgs e )
{
var str = e.Data;
if ( str == null )
return;
MainThread.Queue( () =>
{
var trimmed = str.Trim();
if ( trimmed.StartsWith( '{' ) && trimmed.EndsWith( '}' ) )
{
var status = Json.Deserialize<StatusMessage>( trimmed );
if ( status.Status == "Error" )
{
if ( !string.IsNullOrWhiteSpace( status.Message ) )
{
var lines = status.Message.Split( '\n' );
foreach ( var line in lines.Where( x => !string.IsNullOrWhiteSpace( x ) ) )
_shaderCompileErrors.Add( line );
}
}
}
} );
}
internal void OnErrorData( object sender, DataReceivedEventArgs e )
{
if ( e == null || e.Data == null )
return;
MainThread.Queue( () =>
{
var error = $"{e.Data}";
if ( !string.IsNullOrWhiteSpace( error ) )
_shaderCompileErrors.Add( error );
} );
}
private void OnCompileFinished( int exitCode )
{
_isCompiling = false;
if ( _isPendingCompile )
{
_isPendingCompile = false;
Compile();
return;
}
if ( exitCode == 0 && _shaderCompileErrors.Count == 0 )
{
Log.Info( $"Compile finished in {_timeSinceCompile}" );
var shaderPath = $"shadergraph/{_asset?.Name ?? "untitled"}_shadergraph.generated.shader";
// Reload the shader otherwise it's gonna be the old wank
// Alternatively Material.Create could be made to force reload the shader
ConsoleSystem.Run( $"mat_reloadshaders {shaderPath}" );
_preview.Material = Material.Create( $"{_asset?.Name ?? "untitled"}_shadergraph_generated", shaderPath );
}
else
{
Log.Error( $"Compile failed in {_timeSinceCompile}" );
_output.Errors = _shaderCompileErrors.Select( x => new GraphCompiler.Error { Message = x } );
DockManager.RaiseDock( "Output" );
RestoreShader();
ClearAttributes();
}
_preview.IsCompiling = _isCompiling;
_preview.PostProcessing = _graph.Domain == ShaderDomain.PostProcess;
_shaderCompileErrors.Clear();
}
private void OnAttribute( string name, object value )
{
if ( value == null )
return;
switch ( value )
{
case Color v:
_float4Attributes.Add( name, v );
_preview?.SetAttribute( name, v );
break;
case Vector4 v:
_float4Attributes.Add( name, v );
_preview?.SetAttribute( name, (Color)v );
break;
case Vector3 v:
_float3Attributes.Add( name, v );
_preview?.SetAttribute( name, v );
break;
case Vector2 v:
_float2Attributes.Add( name, v );
_preview?.SetAttribute( name, v );
break;
case float v:
_floatAttributes.Add( name, v );
_preview?.SetAttribute( name, v );
break;
case bool v:
_boolAttributes.Add( name, v );
_preview?.SetAttribute( name, v );
break;
case Texture v:
_textureAttributes.Add( name, v );
_preview?.SetAttribute( name, v );
break;
default:
throw new InvalidOperationException( $"Unsupported attribute type: {value.GetType()}" );
}
}
private string GeneratePreviewCode()
{
ClearAttributes();
var resultNode = _graph.Nodes.OfType<BaseResult>().FirstOrDefault();
var compiler = new GraphCompiler( _graph, true );
compiler.OnAttribute = OnAttribute;
// Evaluate all nodes
foreach ( var node in _graph.Nodes.OfType<BaseNode>() )
{
var property = node.GetType().GetProperties( BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static )
.FirstOrDefault( x => x.GetGetMethod() != null && x.PropertyType == typeof( NodeResult.Func ) );
if ( property == null )
continue;
var output = property.GetCustomAttribute<BaseNode.OutputAttribute>();
if ( output == null )
continue;
var result = compiler.Result( new NodeInput { Identifier = node.Identifier, Output = property.Name } );
if ( !result.IsValid() )
continue;
var componentType = result.ComponentType;
if ( componentType == null )
continue;
// While we're here, let's check the output plugs and update their handle configs to the result type
var nodeUI = _graphView.FindNode( node );
if ( !nodeUI.IsValid() )
continue;
var plugOut = nodeUI.Outputs.FirstOrDefault( x => ((BasePlug)x.Inner).Info.Property == property );
if ( !plugOut.IsValid() )
continue;
plugOut.PropertyType = componentType;
// We also have to update everything so they get repainted
nodeUI.Update();
foreach ( var input in nodeUI.Inputs )
{
if ( !input.IsValid() || !input.Connection.IsValid() )
continue;
input.Connection.Update();
}
}
_compiledNodes.Clear();
_compiledNodes.AddRange( compiler.Nodes );
if ( _properties.IsValid() && _properties.Target is BaseNode targetNode )
{
_preview.SetStage( _compiledNodes.IndexOf( targetNode ) + 1 );
}
if ( resultNode != null )
{
var nodeUI = _graphView.FindNode( resultNode );
if ( nodeUI.IsValid() )
{
nodeUI.Update();
foreach ( var input in nodeUI.Inputs )
{
if ( !input.IsValid() || !input.Connection.IsValid() )
continue;
input.Connection.Update();
}
}
}
var code = compiler.Generate();
if ( compiler.Errors.Any() )
{
_output.Errors = compiler.Errors;
DockManager.RaiseDock( "Output" );
_generatedCode = null;
RestoreShader();
return null;
}
_output.Clear();
if ( _generatedCode != code )
{
_generatedCode = code;
Compile();
}
return code;
}
private string GenerateShaderCode()
{
var compiler = new GraphCompiler( _graph, false );
return compiler.Generate();
}
public void OnUndoPushed()
{
_undoHistory.History = _undoStack.Names;
}
public void SetDirty( bool evaluate = true )
{
Update();
_dirty = true;
_graphCanvas.WindowTitle = $"{_asset?.Name ?? "untitled"}*";
if ( evaluate )
GeneratePreviewCode();
}
[EditorEvent.Frame]
protected void Frame()
{
_undoOption.Enabled = _undoStack.CanUndo;
_redoOption.Enabled = _undoStack.CanRedo;
_undoMenuOption.Enabled = _undoStack.CanUndo;
_redoMenuOption.Enabled = _undoStack.CanRedo;
_undoOption.Text = _undoStack.UndoName;
_redoOption.Text = _undoStack.RedoName;
_undoMenuOption.Text = _undoStack.UndoName ?? "Undo";
_redoMenuOption.Text = _undoStack.RedoName ?? "Redo";
_undoOption.StatusTip = _undoStack.UndoName;
_redoOption.StatusTip = _undoStack.RedoName;
_undoMenuOption.StatusTip = _undoStack.UndoName;
_redoMenuOption.StatusTip = _undoStack.RedoName;
if ( _undoHistory is not null )
{
_undoHistory.UndoLevel = _undoStack.UndoLevel;
}
CheckForChanges();
}
private void CheckForChanges()
{
bool wasDirty = false;
foreach ( var node in _graph.Nodes )
{
if ( node is ShaderNode shaderNode && shaderNode.IsDirty )
{
shaderNode.IsDirty = false;
wasDirty = true;
}
}
if ( wasDirty )
{
_graphView.ChildValuesChanged( null );
}
}
[Shortcut( "editor.undo", "CTRL+Z", ShortcutType.Window )]
private void Undo()
{
if ( _undoStack.Undo() is UndoOp op )
{
Log.Info( $"Undo ({op.name})" );
_redoOption.Enabled = _undoStack.CanUndo;
_graph.ClearNodes();
_graph.DeserializeNodes( op.undoBuffer );
_graphView.RebuildFromGraph();
SetDirty();
}
}
[Shortcut( "editor.redo", "CTRL+Y", ShortcutType.Window )]
private void Redo()
{
if ( _undoStack.Redo() is UndoOp op )
{
Log.Info( $"Redo ({op.name})" );
_redoOption.Enabled = _undoStack.CanRedo;
_graph.ClearNodes();
_graph.DeserializeNodes( op.redoBuffer );
_graphView.RebuildFromGraph();
SetDirty();
}
}
private void SetUndoLevel( int level )
{
if ( _undoStack.SetUndoLevel( level ) is UndoOp op )
{
Log.Info( $"SetUndoLevel ({op.name})" );
_graph.ClearNodes();
_graph.DeserializeNodes( op.redoBuffer );
_graphView.RebuildFromGraph();
SetDirty();
}
}
[Shortcut( "editor.cut", "CTRL+X", ShortcutType.Window )]
private void CutSelection()
{
_graphView.CutSelection();
}
[Shortcut( "editor.copy", "CTRL+C", ShortcutType.Window )]
private void CopySelection()
{
_graphView.CopySelection();
}
[Shortcut( "editor.paste", "CTRL+V", ShortcutType.Window )]
private void PasteSelection()
{
_graphView.PasteSelection();
}
[Shortcut( "editor.duplicate", "CTRL+D", ShortcutType.Window )]
private void DuplicateSelection()
{
_graphView.DuplicateSelection();
}
[Shortcut( "editor.select-all", "CTRL+A", ShortcutType.Window )]
private void SelectAll()
{
_graphView.SelectAll();
}
[Shortcut( "editor.clear-selection", "ESC", ShortcutType.Window )]
private void ClearSelection()
{
_graphView.ClearSelection();
}
[Shortcut( "gameObject.frame", "F", ShortcutType.Window )]
private void CenterOnSelection()
{
_graphView.CenterOnSelection();
}
private void CreateToolBar()
{
var toolBar = new ToolBar( this, "ShaderGraphToolbar" );
AddToolBar( toolBar, ToolbarPosition.Top );
toolBar.AddOption( "New", "common/new.png", New ).StatusTip = "New Graph";
toolBar.AddOption( "Open", "common/open.png", Open ).StatusTip = "Open Graph";
toolBar.AddOption( "Save", "common/save.png", () => Save() ).StatusTip = "Save Graph";
toolBar.AddSeparator();
_undoOption = toolBar.AddOption( "Undo", "undo", Undo );
_redoOption = toolBar.AddOption( "Redo", "redo", Redo );
toolBar.AddSeparator();
toolBar.AddOption( "Cut", "common/cut.png", CutSelection );
toolBar.AddOption( "Copy", "common/copy.png", CopySelection );
toolBar.AddOption( "Paste", "common/paste.png", PasteSelection );
toolBar.AddOption( "Select All", "select_all", SelectAll );
toolBar.AddSeparator();
toolBar.AddOption( "Compile", "refresh", Compile ).StatusTip = "Compile Graph";
toolBar.AddOption( "Open Generated Shader", "common/edit.png", OpenGeneratedShader ).StatusTip = "Open Generated Shader";
toolBar.AddOption( "Take Screenshot", "photo_camera", Screenshot ).StatusTip = "Take Screenshot";
_undoOption.Enabled = false;
_redoOption.Enabled = false;
}
public void BuildMenuBar()
{
var file = MenuBar.AddMenu( "File" );
file.AddOption( "New", "common/new.png", New, "editor.new" ).StatusTip = "New Graph";
file.AddOption( "Open", "common/open.png", Open, "editor.open" ).StatusTip = "Open Graph";
file.AddOption( "Save", "common/save.png", Save, "editor.save" ).StatusTip = "Save Graph";
file.AddOption( "Save As...", "common/save.png", SaveAs, "editor.save-as" ).StatusTip = "Save Graph As...";
file.AddSeparator();
_recentFilesMenu = file.AddMenu( "Recent Files" );
file.AddSeparator();
file.AddOption( "Quit", null, Quit, "editor.quit" ).StatusTip = "Quit";
var edit = MenuBar.AddMenu( "Edit" );
_undoMenuOption = edit.AddOption( "Undo", "undo", Undo, "editor.undo" );
_redoMenuOption = edit.AddOption( "Redo", "redo", Redo, "editor.redo" );
_undoMenuOption.Enabled = _undoStack.CanUndo;
_redoMenuOption.Enabled = _undoStack.CanRedo;
edit.AddSeparator();
edit.AddOption( "Cut", "common/cut.png", CutSelection, "editor.cut" );
edit.AddOption( "Copy", "common/copy.png", CopySelection, "editor.copy" );
edit.AddOption( "Paste", "common/paste.png", PasteSelection, "editor.paste" );
edit.AddOption( "Select All", "select_all", SelectAll, "editor.select-all" );
RefreshRecentFiles();
var view = MenuBar.AddMenu( "View" );
view.AboutToShow += () => OnViewMenu( view );
}
[Shortcut( "editor.quit", "CTRL+Q", ShortcutType.Window )]
void Quit()
{
Close();
}
void RefreshRecentFiles()
{
_recentFilesMenu.Enabled = _recentFiles.Count > 0;
_recentFilesMenu.Clear();
_recentFilesMenu.AddOption( "Clear recent files", null, ClearRecentFiles )
.StatusTip = "Clear recent files";
_recentFilesMenu.AddSeparator();
const int maxFilesToDisplay = 10;
int fileCount = 0;
for ( int i = _recentFiles.Count - 1; i >= 0; i-- )
{
if ( fileCount >= maxFilesToDisplay )
break;
var filePath = _recentFiles[i];
_recentFilesMenu.AddOption( $"{++fileCount} - {filePath}", null, () => PromptSave( () => Open( filePath ) ) )
.StatusTip = $"Open {filePath}";
}
}
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 );
}
view.AddSeparator();
var style = view.AddOption( "Grid-Aligned Wires", "turn_sharp_right" );
style.Checkable = true;
style.Checked = ShaderGraphView.EnableGridAlignedWires;
style.Toggled += b => ShaderGraphView.EnableGridAlignedWires = b;
}
private void ClearRecentFiles()
{
if ( _recentFiles.Count == 0 )
return;
_recentFiles.Clear();
RefreshRecentFiles();
SaveRecentFiles();
}
private void AddToRecentFiles( string filePath )
{
filePath = filePath.ToLower();
// If file is already recent, remove it so it'll become the most recent
if ( _recentFiles.Contains( filePath ) )
{
_recentFiles.RemoveAll( x => x == filePath );
}
_recentFiles.Add( filePath );
RefreshRecentFiles();
SaveRecentFiles();
}
private void SaveRecentFiles()
{
FileSystem.Temporary.WriteJson( "shadergraph_recentfiles.json", _recentFiles );
}
private void PromptSave( Action action )
{
if ( !_dirty )
{
action?.Invoke();
return;
}
var confirm = new PopupWindow(
"Save Current Graph", "The open graph has unsaved changes. Would you like to save now?", "Cancel",
new Dictionary<string, Action>()
{
{ "No", () => action?.Invoke() },
{ "Yes", () => { if ( SaveInternal( false ) ) action?.Invoke(); } }
}
);
confirm.Show();
}
[Shortcut( "editor.new", "CTRL+N", ShortcutType.Window )]
public void New()
{
PromptSave( CreateNew );
}
public void CreateNew()
{
_asset = null;
_graph = new();
_dirty = false;
_graphView.Graph = _graph;
_graphCanvas.WindowTitle = "untitled";
_preview.Model = null;
_preview.Tint = Color.White;
_undoStack.Clear();
_undoHistory.History = _undoStack.Names;
_generatedCode = "";
_properties.Target = _graph;
_output.Clear();
if ( !IsSubgraph )
{
var result = _graphView.CreateNewNode( _graphView.FindNodeType( typeof( Result ) ), 0 );
_graphView.Scale = 1;
_graphView.CenterOn( result.Size * 0.5f );
}
else
{
var result = _graphView.CreateNewNode( _graphView.FindNodeType( typeof( FunctionResult ) ), 0 );
_graphView.Scale = 1;
_graphView.CenterOn( result.Size * 0.5f );
}
ClearAttributes();
RestoreShader();
}
private void ClearAttributes()
{
_textureAttributes.Clear();
_float4Attributes.Clear();
_float3Attributes.Clear();
_float2Attributes.Clear();
_floatAttributes.Clear();
_boolAttributes.Clear();
_compiledNodes.Clear();
_preview?.ClearAttributes();
}
[Shortcut( "editor.open", "CTRL+O", ShortcutType.Window )]
public void Open()
{
var fd = new FileDialog( null )
{
Title = $"Open {FileType}",
DefaultSuffix = $".{FileExtension}"
};
fd.SetNameFilter( $"{FileType} ( *.{FileExtension})" );
if ( !fd.Execute() )
return;
PromptSave( () => Open( fd.SelectedFile ) );
}
public void Open( string path, bool addToPath = true )
{
var asset = AssetSystem.FindByPath( path );
if ( asset == null )
return;
if ( asset == _asset )
{
Focus();
return;
}
var graph = new ShaderGraph();
graph.Deserialize( System.IO.File.ReadAllText( path ) );
graph.Path = asset.RelativePath;
graph.IsSubgraph = IsSubgraph;
_preview.Model = string.IsNullOrWhiteSpace( graph.Model ) ? null : Model.Load( graph.Model );
_preview.LoadSettings( graph.PreviewSettings );
_asset = asset;
_graph = graph;
_dirty = false;
_graphView.Graph = _graph;
_graphCanvas.WindowTitle = _asset.Name;
_undoStack.Clear();
_undoHistory.History = _undoStack.Names;
_generatedCode = "";
_properties.Target = _graph;
if ( addToPath )
AddFileHistory( path );
_output.Clear();
var center = Vector2.Zero;
var resultNode = graph.Nodes.OfType<BaseResult>().FirstOrDefault();
if ( resultNode != null )
{
var nodeUI = _graphView.FindNode( resultNode );
if ( nodeUI.IsValid() )
{
center = nodeUI.Position + nodeUI.Size * 0.5f;
}
}
_graphView.Scale = 1;
_graphView.CenterOn( center );
_graphView.RestoreViewFromCookie();
ClearAttributes();
AddToRecentFiles( path );
GeneratePreviewCode();
}
[Shortcut( "editor.save-as", "CTRL+SHIFT+S", ShortcutType.Window )]
public void SaveAs()
{
SaveInternal( true );
}
[Shortcut( "editor.save", "CTRL+S", ShortcutType.Window )]
public void Save()
{
SaveInternal( false );
}
private void AddFileHistory( string path )
{
var lastFileHistory = _fileHistory.LastOrDefault();
if ( _fileHistoryPosition < _fileHistory.Count - 1 )
{
_fileHistory.RemoveRange( _fileHistoryPosition + 1, _fileHistory.Count - _fileHistoryPosition - 1 );
}
if ( path != lastFileHistory )
_fileHistory.Add( path );
_fileHistoryPosition = _fileHistory.Count - 1;
UpdateFileHistoryButtons();
}
private void FileHistoryForward()
{
if ( _fileHistoryPosition < _fileHistory.Count - 1 )
{
_fileHistoryPosition++;
PromptSave( () =>
{
Open( _fileHistory[_fileHistoryPosition], false );
UpdateFileHistoryButtons();
} );
}
}
private void FileHistoryBack()
{
if ( _fileHistoryPosition > 0 )
{
_fileHistoryPosition--;
PromptSave( () =>
{
Open( _fileHistory[_fileHistoryPosition], false );
UpdateFileHistoryButtons();
} );
}
}
private void FileHistoryHome()
{
if ( _fileHistory.Count == 0 ) return;
PromptSave( () =>
{
Open( _fileHistory.First() );
UpdateFileHistoryButtons();
} );
}
private void UpdateFileHistoryButtons()
{
_fileHistoryForward.Enabled = _fileHistoryPosition < _fileHistory.Count - 1;
_fileHistoryBack.Enabled = _fileHistoryPosition > 0;
_fileHistoryHome.Enabled = _asset.Path != _fileHistory.FirstOrDefault();
}
private bool SaveInternal( bool saveAs )
{
var savePath = _asset == null || saveAs ? GetSavePath() : _asset.AbsolutePath;
if ( string.IsNullOrWhiteSpace( savePath ) )
return false;
_preview.SaveSettings( _graph.PreviewSettings );
// Write serialized graph to asset file
System.IO.File.WriteAllText( savePath, _graph.Serialize() );
if ( saveAs )
{
// If we're saving as, we want to register the new asset
_asset = null;
}
// Register asset if we haven't already
_asset ??= AssetSystem.RegisterFile( savePath );
if ( _asset == null )
{
Log.Warning( $"Unable to register asset {savePath}" );
return false;
}
MainAssetBrowser.Instance?.Local.UpdateAssetList();
_dirty = false;
_graphCanvas.WindowTitle = _asset.Name;
if ( IsSubgraph )
{
Compile();
}
else
{
var shaderPath = System.IO.Path.ChangeExtension( savePath, ".shader" );
var code = GenerateShaderCode();
if ( string.IsNullOrWhiteSpace( code ) )
return false;
// Write generated shader to file
System.IO.File.WriteAllText( shaderPath, code );
}
AddToRecentFiles( savePath );
EditorEvent.Run( "shadergraph.update.subgraph", _asset.RelativePath );
return true;
}
private string GetSavePath()
{
var fd = new FileDialog( null )
{
Title = $"Save {FileType}",
DefaultSuffix = $".{FileExtension}"
};
fd.SelectFile( $"untitled.{FileExtension}" );
fd.SetFindFile();
fd.SetModeSave();
fd.SetNameFilter( $"{FileType} (*.{FileExtension})" );
if ( !fd.Execute() )
return null;
return fd.SelectedFile;
}
public void CreateUI()
{
BuildMenuBar();
DockManager.RegisterDockType( "Graph", "account_tree", null, false );
DockManager.RegisterDockType( "Preview", "photo", null, false );
DockManager.RegisterDockType( "Properties", "edit", null, false );
DockManager.RegisterDockType( "Output", "notes", null, false );
DockManager.RegisterDockType( "Console", "text_snippet", null, false );
DockManager.RegisterDockType( "Undo History", "history", null, false );
DockManager.RegisterDockType( "Palette", "palette", null, false );
_graphCanvas = new Widget( this ) { WindowTitle = $"{(_asset != null ? _asset.Name : "untitled")}{(_dirty ? "*" : "")}" };
_graphCanvas.Name = "Graph";
_graphCanvas.SetWindowIcon( "account_tree" );
_graphCanvas.Layout = Layout.Column();
var graphToolBar = new ToolBar( _graphCanvas, "GraphToolBar" );
graphToolBar.SetIconSize( 16 );
_fileHistoryBack = graphToolBar.AddOption( null, "arrow_back" );
_fileHistoryForward = graphToolBar.AddOption( null, "arrow_forward" );
graphToolBar.AddSeparator();
_fileHistoryHome = graphToolBar.AddOption( null, "common/home.png" );
_fileHistoryBack.Triggered += FileHistoryBack;
_fileHistoryForward.Triggered += FileHistoryForward;
_fileHistoryHome.Triggered += FileHistoryHome;
_fileHistoryBack.Enabled = false;
_fileHistoryForward.Enabled = false;
_fileHistoryHome.Enabled = false;
var stretcher = new Widget( graphToolBar );
stretcher.Layout = Layout.Row();
stretcher.Layout.AddStretchCell( 1 );
graphToolBar.AddWidget( stretcher );
graphToolBar.AddWidget( new GamePerformanceBar( () => (1000.0f / PerformanceStats.LastSecond.FrameAvg).ToString( "n0" ) + "fps" ) { ToolTip = "Frames Per Second Average" } );
graphToolBar.AddWidget( new GamePerformanceBar( () => PerformanceStats.LastSecond.FrameAvg.ToString( "0.00" ) + "ms" ) { ToolTip = "Frame Time Average (milliseconds)" } );
graphToolBar.AddWidget( new GamePerformanceBar( () => PerformanceStats.ApproximateProcessMemoryUsage.FormatBytes() ) { ToolTip = "Approximate Memory Usage" } );
_graphCanvas.Layout.Add( graphToolBar );
_graphView = new ShaderGraphView( _graphCanvas, this );
_graphView.BilinearFiltering = false;
var types = EditorTypeLibrary.GetTypes<ShaderNode>()
.Where( x => !x.IsAbstract ).OrderBy( x => x.Name );
foreach ( var type in types )
{
_graphView.AddNodeType( type );
}
var subgraphs = AssetSystem.All.Where( x => x.Path.EndsWith( ".shdrfunc", StringComparison.OrdinalIgnoreCase ) );
foreach ( var subgraph in subgraphs )
{
_graphView.AddNodeType( subgraph.Path );
}
_graphView.Graph = _graph;
_graphView.OnChildValuesChanged += ( w ) => SetDirty();
_graphCanvas.Layout.Add( _graphView, 1 );
_output = new Output( this );
_output.OnNodeSelected += ( node ) =>
{
var nodeUI = _graphView.SelectNode( node );
_graphView.Scale = 1;
_graphView.CenterOn( nodeUI.Center );
};
_preview = new PreviewPanel( this, _graph.Model )
{
OnModelChanged = ( model ) => _graph.Model = model?.Name
};
foreach ( var value in _textureAttributes )
{
_preview.SetAttribute( value.Key, value.Value );
}
foreach ( var value in _float4Attributes )
{
_preview.SetAttribute( value.Key, value.Value );
}
foreach ( var value in _float3Attributes )
{
_preview.SetAttribute( value.Key, value.Value );
}
foreach ( var value in _float2Attributes )
{
_preview.SetAttribute( value.Key, value.Value );
}
foreach ( var value in _floatAttributes )
{
_preview.SetAttribute( value.Key, value.Value );
}
foreach ( var value in _boolAttributes )
{
_preview.SetAttribute( value.Key, value.Value );
}
_properties = new Properties( this );
_properties.Target = _graph;
_properties.PropertyUpdated += OnPropertyUpdated;
_undoHistory = new UndoHistory( this, _undoStack );
_undoHistory.OnUndo = Undo;
_undoHistory.OnRedo = Redo;
_undoHistory.OnHistorySelected = SetUndoLevel;
_palette = new PaletteWidget( this, IsSubgraph );
DockManager.AddDock( null, _preview, DockArea.Left, DockManager.DockProperty.HideOnClose );
DockManager.AddDock( null, _graphCanvas, DockArea.Right, DockManager.DockProperty.HideCloseButton | DockManager.DockProperty.HideOnClose, 0.7f );
DockManager.AddDock( _graphCanvas, _output, DockArea.Bottom, DockManager.DockProperty.HideOnClose, 0.25f );
DockManager.AddDock( _preview, _properties, DockArea.Bottom, DockManager.DockProperty.HideOnClose, 0.5f );
// Yuck, console is internal but i want it, what is the correct way?
var console = EditorTypeLibrary.Create( "ConsoleWidget", typeof( Widget ), new[] { this } ) as Widget;
DockManager.AddDock( _output, console, DockArea.Inside, DockManager.DockProperty.HideOnClose );
DockManager.AddDock( _output, _undoHistory, DockArea.Inside, DockManager.DockProperty.HideOnClose );
DockManager.AddDock( _output, _palette, DockArea.Inside, DockManager.DockProperty.HideOnClose );
DockManager.RaiseDock( "Output" );
DockManager.Update();
_defaultDockState = DockManager.State;
if ( StateCookie != "ShaderGraph" )
{
StateCookie = "ShaderGraph";
}
else
{
RestoreFromStateCookie();
}
Compile();
}
private void OnPropertyUpdated()
{
_preview.PostProcessing = _graphView.Graph.Domain == ShaderDomain.PostProcess;
if ( _properties.Target is BaseNode node )
{
_graphView.UpdateNode( node );
}
var shouldEvaluate = _properties.Target is not CommentNode;
SetDirty( shouldEvaluate );
}
protected override void RestoreDefaultDockLayout()
{
DockManager.State = _defaultDockState;
SaveToStateCookie();
}
protected override bool OnClose()
{
if ( !_dirty )
{
return true;
}
var confirm = new PopupWindow(
"Save Current Graph", "The open graph has unsaved changes. Would you like to save now?", "Cancel",
new Dictionary<string, Action>()
{
{ "No", () => { _dirty = false; Close(); } },
{ "Yes", () => { if ( SaveInternal( false ) ) Close(); } }
}
);
confirm.Show();
return false;
}
[Event( "shadergraph.update.subgraph" )]
public void OnSubgraphUpdate( string updatedPath )
{
foreach ( var node in _graph.Nodes )
{
if ( node is SubgraphNode subgraphNode )
{
subgraphNode.Subgraph = null;
subgraphNode.OnNodeCreated();
}
}
GeneratePreviewCode();
}
}