Browse Source

Added custom output picker

flabbet 7 months ago
parent
commit
811a3b7048

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -11,7 +11,8 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 {
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
-    public OutputNode? OutputNode => Nodes.OfType<OutputNode>().FirstOrDefault();
+    public Node? OutputNode => CustomOutputNode ?? Nodes.OfType<OutputNode>().FirstOrDefault();
+    public Node? CustomOutputNode { get; set; }
 
     IReadOnlyCollection<IReadOnlyNode> IReadOnlyNodeGraph.AllNodes => Nodes;
     IReadOnlyNode IReadOnlyNodeGraph.OutputNode => OutputNode;

+ 12 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CustomOutputNode.cs

@@ -10,6 +10,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [NodeInfo("CustomOutput")]
 public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
 {
+    public const string OutputNamePropertyName = "OutputName";
     public RenderInputProperty Input { get; } 
     public InputProperty<string> OutputName { get; }
     
@@ -19,7 +20,7 @@ public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
         Input = new RenderInputProperty(this, OutputNode.InputPropertyName, "BACKGROUND", null);
         AddInputProperty(Input);
         
-        OutputName = CreateInput("OutputName", "OUTPUT_NAME", "");
+        OutputName = CreateInput(OutputNamePropertyName, "OUTPUT_NAME", "");
     }
 
     public override Node CreateCopy()
@@ -29,13 +30,16 @@ public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
 
     protected override void OnExecute(RenderContext context)
     {
-        lastDocumentSize = context.DocumentSize;
-        
-        int saved = context.RenderSurface.Canvas.Save();
-        context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
-        Input.Value?.Paint(context, context.RenderSurface);
-        
-        context.RenderSurface.Canvas.RestoreToCount(saved);
+        if (context.TargetOutput == OutputName.Value)
+        {
+            lastDocumentSize = context.DocumentSize;
+
+            int saved = context.RenderSurface.Canvas.Save();
+            context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
+            Input.Value?.Paint(context, context.RenderSurface);
+
+            context.RenderSurface.Canvas.RestoreToCount(saved);
+        }
     }
 
     RenderInputProperty IRenderInput.Background => Input;

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -28,6 +28,8 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
 
     protected override void OnExecute(RenderContext context)
     {
+        if(!string.IsNullOrEmpty(context.TargetOutput)) return;
+        
         lastDocumentSize = context.DocumentSize;
         
         int saved = context.RenderSurface.Canvas.Save();

+ 7 - 2
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -202,7 +202,12 @@ public class DocumentRenderer : IPreviewRenderable
         NodeGraph membersOnlyGraph,
         Dictionary<Guid, Guid> nodeMapping)
     {
-        if(input == null) return membersOnlyGraph.OutputNode.Input;
+        if (input == null)
+        {
+            if(membersOnlyGraph.OutputNode is IRenderInput inputNode) return inputNode.Background;
+
+            return null;
+        }
         
         if (nodeMapping.ContainsKey(input.Node?.Id ?? Guid.Empty))
         {
@@ -228,6 +233,6 @@ public class DocumentRenderer : IPreviewRenderable
             return true;
         });
         
-        return found ?? membersOnlyGraph.OutputNode.Input;
+        return found ?? (membersOnlyGraph.OutputNode as IRenderInput)?.Background;
     }
 }

+ 2 - 1
src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs

@@ -18,7 +18,8 @@ public class RenderContext
     public DrawingSurface RenderSurface { get; set; }
     public bool FullRerender { get; set; } = false;
     
-    public ColorSpace ProcessingColorSpace { get; set; } 
+    public ColorSpace ProcessingColorSpace { get; set; }
+    public string? TargetOutput { get; set; }   
 
 
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,

+ 5 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -666,6 +666,11 @@ internal class DocumentUpdater
         ProcessStructureMemberProperty(info, property);
         
         property.InternalSetValue(info.Value);
+        
+        if (info.Property == CustomOutputNode.OutputNamePropertyName)
+        {
+            doc.NodeGraphHandler.UpdateAvailableRenderOutputs();
+        }
     }
     
     private void ProcessStructureMemberProperty(PropertyValueUpdated_ChangeInfo info, INodePropertyHandler property)

+ 1 - 0
src/PixiEditor/Models/Handlers/INodeGraphHandler.cs

@@ -20,4 +20,5 @@ internal interface INodeGraphHandler
    public void SetConnection(NodeConnectionViewModel connection);
    public void RemoveConnection(Guid nodeId, string property);
    public void RemoveConnections(Guid nodeId);
+   public void UpdateAvailableRenderOutputs();
 }

+ 43 - 4
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -4,6 +4,7 @@ using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Handlers;
@@ -22,14 +23,14 @@ internal class SceneRenderer
         DocumentViewModel = documentViewModel;
     }
 
-    public void RenderScene(DrawingSurface target, ChunkResolution resolution)
+    public void RenderScene(DrawingSurface target, ChunkResolution resolution, string? targetOutput = null)
     {
         if(Document.Renderer.IsBusy || DocumentViewModel.Busy) return;
         RenderOnionSkin(target, resolution);
-        RenderGraph(target, resolution);
+        RenderGraph(target, resolution, targetOutput);
     }
 
-    private void RenderGraph(DrawingSurface target, ChunkResolution resolution)
+    private void RenderGraph(DrawingSurface target, ChunkResolution resolution, string? targetOutput)
     {
         DrawingSurface renderTarget = target;
         Texture? texture = null;
@@ -42,7 +43,8 @@ internal class SceneRenderer
 
         RenderContext context = new(renderTarget, DocumentViewModel.AnimationHandler.ActiveFrameTime,
             resolution, Document.Size, Document.ProcessingColorSpace);
-        Document.NodeGraph.Execute(context);
+        context.TargetOutput = targetOutput;
+        SolveFinalNodeGraph(context.TargetOutput).Execute(context);
         
         if(texture != null)
         {
@@ -51,6 +53,43 @@ internal class SceneRenderer
         }
     }
 
+    private IReadOnlyNodeGraph SolveFinalNodeGraph(string? targetOutput)
+    {
+        if (targetOutput == null)
+        {
+            return Document.NodeGraph;
+        }
+
+        CustomOutputNode[] outputNodes = Document.NodeGraph.AllNodes.OfType<CustomOutputNode>().ToArray();
+        
+        foreach (CustomOutputNode outputNode in outputNodes)
+        {
+            if (outputNode.OutputName.Value == targetOutput)
+            {
+                return GraphFromOutputNode(outputNode);
+            }
+        }
+
+        return Document.NodeGraph;
+    }
+
+    private IReadOnlyNodeGraph GraphFromOutputNode(CustomOutputNode outputNode)
+    {
+        NodeGraph graph = new();
+        outputNode.TraverseBackwards(n =>
+        {
+            if (n is Node node)
+            {
+                graph.AddNode(node);
+            }
+            
+            return true;
+        });
+        
+        graph.CustomOutputNode = outputNode;
+        return graph;
+    }
+
     private bool HighDpiRenderNodePresent(IReadOnlyNodeGraph documentNodeGraph)
     {
         bool highDpiRenderNodePresent = false;

+ 52 - 0
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -22,6 +22,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
     public ObservableCollection<NodeFrameViewModelBase> Frames { get; } = new();
+    public ObservableCollection<string> AvailableRenderOutputs { get; } = new();
     public StructureTree StructureTree { get; } = new();
     public INodeHandler? OutputNode { get; private set; }
 
@@ -43,6 +44,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
         AllNodes.Add(node);
         StructureTree.Update(this);
+        UpdateAvailableRenderOutputs();
     }
 
     public void RemoveNode(Guid nodeId)
@@ -55,6 +57,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         }
 
         StructureTree.Update(this);
+        UpdateAvailableRenderOutputs();
     }
 
     public void AddFrame(Guid frameId, IEnumerable<Guid> nodes)
@@ -322,4 +325,53 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
         Internals.ActionAccumulator.AddFinishedActions(action);
     }
+    
+    public void UpdateAvailableRenderOutputs()
+    {
+        List<string> outputs = new();
+        foreach (var node in AllNodes)
+        {
+            if (node.InternalName == typeof(CustomOutputNode).GetCustomAttribute<NodeInfoAttribute>().UniqueName)
+            {
+                var nameInput =
+                    node.Inputs.FirstOrDefault(x => x.PropertyName == CustomOutputNode.OutputNamePropertyName);
+
+                if (nameInput is { Value: string name } && !string.IsNullOrEmpty(name))
+                {
+                    if(outputs.Contains(name)) continue;
+                    
+                    outputs.Add(name);
+                }
+            }
+            else if (node.InternalName == typeof(OutputNode).GetCustomAttribute<NodeInfoAttribute>().UniqueName)
+            {
+                outputs.Insert(0, "DEFAULT");
+            }
+        }
+        
+        RemoveExcessiveRenderOutputs(outputs);
+        AddMissingRenderOutputs(outputs);
+    }
+
+    private void RemoveExcessiveRenderOutputs(List<string> outputs)
+    {
+        for (int i = AvailableRenderOutputs.Count - 1; i >= 0; i--)
+        {
+            if (!outputs.Contains(AvailableRenderOutputs[i]))
+            {
+                AvailableRenderOutputs.RemoveAt(i);
+            }
+        }
+    }
+    
+    private void AddMissingRenderOutputs(List<string> outputs)
+    {
+        foreach (var output in outputs)
+        {
+            if (!AvailableRenderOutputs.Contains(output))
+            {
+                AvailableRenderOutputs.Add(output);
+            }
+        }
+    }
 }

+ 11 - 0
src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs

@@ -32,6 +32,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
     private string _index = "";
 
     private bool _flipX;
+    private string renderOutputName = "DEFAULT";
     private string id = Guid.NewGuid().ToString();
 
     public bool FlipX
@@ -55,6 +56,16 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
             OnPropertyChanged(nameof(FlipY));
         }
     }
+    
+    public string RenderOutputName
+    {
+        get => renderOutputName;
+        set
+        {
+            renderOutputName = value;
+            OnPropertyChanged(nameof(RenderOutputName));
+        }
+    }
 
     private ViewportColorChannels _channels = ViewportColorChannels.Default;
     

+ 2 - 0
src/PixiEditor/Views/Dock/DocumentTemplate.axaml

@@ -37,6 +37,8 @@
         Channels="{Binding Channels, Mode=TwoWay}"
         SnappingViewModel="{Binding ActiveDocument.SnappingViewModel, Source={viewModels1:MainVM DocumentManagerSVM}}"
         SnappingEnabled="{Binding ViewportSubViewModel.SnappingEnabled, Source={viewModels1:MainVM}, Mode=TwoWay}"
+        AvailableRenderOutputs="{Binding ActiveDocument.NodeGraph.AvailableRenderOutputs, Source={viewModels1:MainVM DocumentManagerSVM}}"
+        ViewportRenderOutput="{Binding RenderOutputName, Mode=TwoWay}"
         Document="{Binding Document}">
     </viewportControls:Viewport>
 </UserControl>

+ 1 - 11
src/PixiEditor/Views/Dock/DocumentTemplate.axaml.cs

@@ -1,14 +1,4 @@
-using System.ComponentModel;
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Input;
-using Avalonia.Interactivity;
-using PixiEditor.Models.Preferences;
-using PixiEditor.ViewModels.Dock;
-using PixiEditor.Views.Palettes;
-using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
-using PixiEditor.ViewModels.SubViewModels;
-using PixiEditor.ViewModels.Tools.Tools;
+using Avalonia.Controls;
 
 namespace PixiEditor.Views.Dock;
 

+ 13 - 0
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -131,6 +131,18 @@
                                           Content="{DynamicResource icon-snapping}"
                                           IsChecked="{Binding SnappingEnabled, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
                         </StackPanel>
+                        <Separator/>
+                        <ComboBox ItemsSource="{Binding AvailableRenderOutputs, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}}"
+                                  SelectedValue="{Binding ViewportRenderOutput, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"
+                                  SelectionChanged="RenderOutput_SelectionChanged"
+                                  ui:Translator.TooltipKey="RENDER_OUTPUT"
+                                  Margin="0 10 0 0">
+                            <ComboBox.ItemTemplate>
+                                <DataTemplate>
+                                    <TextBlock ui:Translator.Key="{Binding}" />
+                                </DataTemplate>
+                            </ComboBox.ItemTemplate>
+                        </ComboBox>
                     </StackPanel>
                 </Border>
             </overlays:TogglableFlyout.Child>
@@ -179,6 +191,7 @@
             AllOverlays="{Binding ElementName=vpUc, Path=ActiveOverlays}"
             FadeOut="{Binding Source={viewModels:ToolVM ColorPickerToolViewModel}, Path=PickOnlyFromReferenceLayer, Mode=OneWay}"
             DefaultCursor="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ToolCursor, Mode=OneWay}"
+            RenderOutput="{Binding ViewportRenderOutput, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"
             CheckerImagePath="/Images/CheckerTile.png"
             PointerPressed="Scene_OnContextMenuOpening"
             ui:RenderOptionsBindable.BitmapInterpolationMode="{Binding Scale, Converter={converters:ScaleToBitmapScalingModeConverter}, RelativeSource={RelativeSource Self}}">

+ 34 - 2
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -5,6 +5,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.VisualTree;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Helpers;
@@ -288,6 +289,18 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         }
     }
 
+    public static readonly StyledProperty<ObservableCollection<string>> AvailableRenderOutputsProperty =
+        AvaloniaProperty.Register<Viewport, ObservableCollection<string>>(nameof(AvailableRenderOutputs));
+
+    public static readonly StyledProperty<string> ViewportRenderOutputProperty = AvaloniaProperty.Register<Viewport, string>(
+        nameof(ViewportRenderOutput), "DEFAULT");
+
+    public string ViewportRenderOutput
+    {
+        get => GetValue(ViewportRenderOutputProperty);
+        set => SetValue(ViewportRenderOutputProperty, value);
+    }
+
     public ObservableCollection<Overlay> ActiveOverlays { get; } = new();
 
     public Guid GuidValue { get; } = Guid.NewGuid();
@@ -350,6 +363,12 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         set { SetValue(SnappingEnabledProperty, value); }
     }
 
+    public ObservableCollection<string> AvailableRenderOutputs
+    {
+        get { return (ObservableCollection<string>)GetValue(AvailableRenderOutputsProperty); }
+        set { SetValue(AvailableRenderOutputsProperty, value); }
+    }
+
     private void ForceRefreshFinalImage()
     {
         Scene.InvalidateVisual();
@@ -521,8 +540,8 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private void Scene_OnContextMenuOpening(object? sender, PointerPressedEventArgs e)
     {
-        if(e.GetMouseButton(this) != MouseButton.Right) return;
-        
+        if (e.GetMouseButton(this) != MouseButton.Right) return;
+
         ViewportWindowViewModel vm = ((ViewportWindowViewModel)DataContext);
         var tools = vm.Owner.Owner.ToolsSubViewModel;
 
@@ -548,4 +567,17 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         Scene?.ContextFlyout?.ShowAt(Scene);
         e.Handled = true;
     }
+
+    private void RenderOutput_SelectionChanged(object? sender, SelectionChangedEventArgs e)
+    {
+        if (e.Source is ComboBox comboBox)
+        {
+            if(!comboBox.IsAttachedToVisualTree()) return;
+
+            if (e.AddedItems.Count > 0)
+            {
+                ViewportRenderOutput = (string)e.AddedItems[0];
+            }
+        }
+    }
 }

+ 8 - 1
src/PixiEditor/Views/Rendering/Scene.cs

@@ -106,6 +106,12 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(ChannelsProperty, value);
     }
 
+    public string RenderOutput
+    {
+        get { return (string)GetValue(RenderOutputProperty); }
+        set { SetValue(RenderOutputProperty, value); }
+    }
+
 
     private Bitmap? checkerBitmap;
 
@@ -134,6 +140,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     private PixelSize lastSize = PixelSize.Empty;
     private Cursor lastCursor;
+    public static readonly StyledProperty<string> RenderOutputProperty = AvaloniaProperty.Register<Scene, string>("RenderOutput");
 
     static Scene()
     {
@@ -254,7 +261,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     {
         DrawCheckerboard(renderTexture.DrawingSurface, bounds);
         DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Background);
-        SceneRenderer.RenderScene(renderTexture.DrawingSurface, CalculateResolution());
+        SceneRenderer.RenderScene(renderTexture.DrawingSurface, CalculateResolution(), RenderOutput == "DEFAULT" ? null : RenderOutput);
         DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Foreground);
     }