Browse Source

Merge pull request #939 from PixiEditor/custom-render-output-sizes

Added custom render output sizes
Krzysztof Krysiński 3 months ago
parent
commit
73d1b37ff2
30 changed files with 422 additions and 169 deletions
  1. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs
  2. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  3. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DocumentInfoNode.cs
  4. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs
  5. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  6. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  7. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  8. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  9. 6 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs
  10. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  13. 38 14
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/EvaluateGraph_Change.cs
  15. 31 5
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  16. 5 3
      src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs
  17. 2 1
      src/PixiEditor/Data/Localization/Languages/en.json
  18. 1 1
      src/PixiEditor/Models/Handlers/INodeGraphHandler.cs
  19. 2 0
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  20. 1 1
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  21. 35 8
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  22. 4 4
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  23. 38 17
      src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs
  24. 125 9
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  25. 1 1
      src/PixiEditor/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs
  26. 0 7
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  27. 2 20
      src/PixiEditor/Views/Overlays/GridLinesOverlay.cs
  28. 36 39
      src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  29. 69 15
      src/PixiEditor/Views/Rendering/Scene.cs
  30. 1 1
      tests/PixiEditor.Tests/BlendingTests.cs

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs

@@ -13,7 +13,7 @@ public class SceneObjectRenderContext : RenderContext
     public RenderOutputProperty TargetPropertyOutput { get; }
     public RenderOutputProperty TargetPropertyOutput { get; }
 
 
     public SceneObjectRenderContext(RenderOutputProperty targetPropertyOutput, DrawingSurface surface, RectD localBounds, KeyFrameTime frameTime,
     public SceneObjectRenderContext(RenderOutputProperty targetPropertyOutput, DrawingSurface surface, RectD localBounds, KeyFrameTime frameTime,
-        ChunkResolution chunkResolution, VecI docSize, bool renderSurfaceIsScene, ColorSpace processingColorSpace, double opacity) : base(surface, frameTime, chunkResolution, docSize, processingColorSpace, opacity)
+        ChunkResolution chunkResolution, VecI renderOutputSize, VecI documentSize, bool renderSurfaceIsScene, ColorSpace processingColorSpace, double opacity) : base(surface, frameTime, chunkResolution, renderOutputSize, documentSize, processingColorSpace, opacity)
     {
     {
         TargetPropertyOutput = targetPropertyOutput;
         TargetPropertyOutput = targetPropertyOutput;
         LocalBounds = localBounds;
         LocalBounds = localBounds;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -72,7 +72,7 @@ public class CreateImageNode : Node, IPreviewRenderable
         int saved = surface.DrawingSurface.Canvas.Save();
         int saved = surface.DrawingSurface.Canvas.Save();
 
 
         RenderContext ctx = new RenderContext(surface.DrawingSurface, context.FrameTime, context.ChunkResolution,
         RenderContext ctx = new RenderContext(surface.DrawingSurface, context.FrameTime, context.ChunkResolution,
-            context.DocumentSize, context.ProcessingColorSpace);
+            context.RenderOutputSize, context.DocumentSize, context.ProcessingColorSpace);
 
 
         surface.DrawingSurface.Canvas.SetMatrix(surface.DrawingSurface.Canvas.TotalMatrix.Concat(ContentMatrix.Value));
         surface.DrawingSurface.Canvas.SetMatrix(surface.DrawingSurface.Canvas.TotalMatrix.Concat(ContentMatrix.Value));
 
 

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DocumentInfoNode.cs

@@ -9,16 +9,24 @@ public class DocumentInfoNode : Node
     public OutputProperty<VecI> Size { get; }
     public OutputProperty<VecI> Size { get; }
     public OutputProperty<VecD> Center { get; }
     public OutputProperty<VecD> Center { get; }
 
 
+    public OutputProperty<VecI> RenderOutputSize { get; }
+    public OutputProperty<VecI> RenderOutputCenter { get; }
+
     public DocumentInfoNode()
     public DocumentInfoNode()
     {
     {
         Size = CreateOutput("Size", "SIZE", new VecI(0, 0));
         Size = CreateOutput("Size", "SIZE", new VecI(0, 0));
         Center = CreateOutput("Center", "CENTER", new VecD(0, 0));
         Center = CreateOutput("Center", "CENTER", new VecD(0, 0));
+        RenderOutputSize = CreateOutput("RenderOutputSize", "RENDER_OUTPUT_SIZE", new VecI(0, 0));
+        RenderOutputCenter = CreateOutput("RenderOutputCenter", "RENDER_OUTPUT_CENTER", new VecI(0, 0));
     }
     }
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         Size.Value = context.DocumentSize;
         Size.Value = context.DocumentSize;
         Center.Value = new VecD(context.DocumentSize.X / 2.0, context.DocumentSize.Y / 2.0);
         Center.Value = new VecD(context.DocumentSize.X / 2.0, context.DocumentSize.Y / 2.0);
+
+        RenderOutputSize.Value = context.RenderOutputSize;
+        RenderOutputCenter.Value = new VecI(context.RenderOutputSize.X / 2, context.RenderOutputSize.Y / 2);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs

@@ -52,7 +52,7 @@ public class OutlineNode : RenderNode, IRenderInput
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         base.OnExecute(context);
         base.OnExecute(context);
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
 
 
         Kernel finalKernel = Type.Value switch
         Kernel finalKernel = Type.Value switch
         {
         {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -39,7 +39,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         base.OnExecute(context);
         base.OnExecute(context);
-        documentSize = context.DocumentSize;
+        documentSize = context.RenderOutputSize;
     }
     }
 
 
     public override void Render(SceneObjectRenderContext sceneContext)
     public override void Render(SceneObjectRenderContext sceneContext)

+ 3 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -44,7 +44,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             }
             }
 
 
-            if (AllowHighDpiRendering || renderOnto.DeviceClipBounds.Size == context.DocumentSize)
+            if (AllowHighDpiRendering || renderOnto.DeviceClipBounds.Size == context.RenderOutputSize)
             {
             {
                 DrawLayerInScene(context, renderOnto, useFilters);
                 DrawLayerInScene(context, renderOnto, useFilters);
             }
             }
@@ -56,7 +56,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                     BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
                     BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
                 };
                 };
 
 
-                var tempSurface = TryInitWorkingSurface(context.DocumentSize, context.ChunkResolution,
+                var tempSurface = TryInitWorkingSurface(context.RenderOutputSize, context.ChunkResolution,
                     context.ProcessingColorSpace, 22);
                     context.ProcessingColorSpace, 22);
 
 
                 DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters, targetPaint);
                 DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters, targetPaint);
@@ -70,7 +70,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
 
         VecI size = AllowHighDpiRendering
         VecI size = AllowHighDpiRendering
             ? renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos
             ? renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos
-            : context.DocumentSize;
+            : context.RenderOutputSize;
         int saved = renderOnto.Canvas.Save();
         int saved = renderOnto.Canvas.Save();
 
 
         var adjustedResolution = AllowHighDpiRendering ? ChunkResolution.Full : context.ChunkResolution;
         var adjustedResolution = AllowHighDpiRendering ? ChunkResolution.Full : context.ChunkResolution;

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

@@ -32,10 +32,10 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
     {
     {
         if (!string.IsNullOrEmpty(context.TargetOutput)) return;
         if (!string.IsNullOrEmpty(context.TargetOutput)) return;
 
 
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
 
 
         int saved = context.RenderSurface.Canvas.Save();
         int saved = context.RenderSurface.Canvas.Save();
-        context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
+        context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y));
         Input.Value?.Paint(context, context.RenderSurface);
         Input.Value?.Paint(context, context.RenderSurface);
 
 
         context.RenderSurface.Canvas.RestoreToCount(saved);
         context.RenderSurface.Canvas.RestoreToCount(saved);

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -39,18 +39,18 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
             }
             }
         }
         }
 
 
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
     }
     }
 
 
     protected virtual void Paint(RenderContext context, DrawingSurface surface)
     protected virtual void Paint(RenderContext context, DrawingSurface surface)
     {
     {
         DrawingSurface target = surface;
         DrawingSurface target = surface;
         bool useIntermediate = !AllowHighDpiRendering
         bool useIntermediate = !AllowHighDpiRendering
-                               && context.DocumentSize is { X: > 0, Y: > 0 }
-                               && surface.DeviceClipBounds.Size != context.DocumentSize;
+                               && context.RenderOutputSize is { X: > 0, Y: > 0 }
+                               && surface.DeviceClipBounds.Size != context.RenderOutputSize;
         if (useIntermediate)
         if (useIntermediate)
         {
         {
-            Texture intermediate = textureCache.RequestTexture(-6451, context.DocumentSize, context.ProcessingColorSpace);
+            Texture intermediate = textureCache.RequestTexture(-6451, context.RenderOutputSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
             target = intermediate.DrawingSurface;
         }
         }
 
 

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs

@@ -48,7 +48,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     {
     {
         base.OnExecute(context);
         base.OnExecute(context);
 
 
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
 
 
         if (lastShaderCode != ShaderCode.Value)
         if (lastShaderCode != ShaderCode.Value)
         {
         {
@@ -88,7 +88,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         Uniforms uniforms;
         Uniforms uniforms;
         uniforms = new Uniforms();
         uniforms = new Uniforms();
 
 
-        uniforms.Add("iResolution", new Uniform("iResolution", (VecD)context.DocumentSize));
+        uniforms.Add("iResolution", new Uniform("iResolution", (VecD)context.RenderOutputSize));
         uniforms.Add("iNormalizedTime", new Uniform("iNormalizedTime", (float)context.FrameTime.NormalizedTime));
         uniforms.Add("iNormalizedTime", new Uniform("iNormalizedTime", (float)context.FrameTime.NormalizedTime));
         uniforms.Add("iFrame", new Uniform("iFrame", context.FrameTime.Frame));
         uniforms.Add("iFrame", new Uniform("iFrame", context.FrameTime.Frame));
 
 
@@ -101,7 +101,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
             return uniforms;
             return uniforms;
         }
         }
 
 
-        Texture texture = RequestTexture(50, context.DocumentSize, context.ProcessingColorSpace);
+        Texture texture = RequestTexture(50, context.RenderOutputSize, context.ProcessingColorSpace);
         Background.Value.Paint(context, texture.DrawingSurface);
         Background.Value.Paint(context, texture.DrawingSurface);
 
 
         var snapshot = texture.DrawingSurface.Snapshot();
         var snapshot = texture.DrawingSurface.Snapshot();
@@ -128,17 +128,17 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         {
         {
             if (ColorSpace.Value == ColorSpaceType.Srgb && !context.ProcessingColorSpace.IsSrgb)
             if (ColorSpace.Value == ColorSpaceType.Srgb && !context.ProcessingColorSpace.IsSrgb)
             {
             {
-                targetSurface = RequestTexture(51, context.DocumentSize,
+                targetSurface = RequestTexture(51, context.RenderOutputSize,
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()).DrawingSurface;
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()).DrawingSurface;
             }
             }
             else if (ColorSpace.Value == ColorSpaceType.LinearSrgb && context.ProcessingColorSpace.IsSrgb)
             else if (ColorSpace.Value == ColorSpaceType.LinearSrgb && context.ProcessingColorSpace.IsSrgb)
             {
             {
-                targetSurface = RequestTexture(51, context.DocumentSize,
+                targetSurface = RequestTexture(51, context.RenderOutputSize,
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear()).DrawingSurface;
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear()).DrawingSurface;
             }
             }
         }
         }
 
 
-        targetSurface.Canvas.DrawRect(0, 0, context.DocumentSize.X, context.DocumentSize.Y, paint);
+        targetSurface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
 
 
         if (targetSurface != surface)
         if (targetSurface != surface)
         {
         {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs

@@ -22,7 +22,7 @@ public class DistributePointsNode : ShapeNode<PointsVectorData>
 
 
     protected override PointsVectorData? GetShapeData(RenderContext context)
     protected override PointsVectorData? GetShapeData(RenderContext context)
     {
     {
-        return GetPointsRandomly(context.DocumentSize);
+        return GetPointsRandomly(context.RenderOutputSize);
     }
     }
 
 
     private PointsVectorData GetPointsRandomly(VecI size)
     private PointsVectorData GetPointsRandomly(VecI size)

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -182,7 +182,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         RectD localBounds = new RectD(0, 0, sceneSize.X, sceneSize.Y);
         RectD localBounds = new RectD(0, 0, sceneSize.X, sceneSize.Y);
 
 
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
-            context.FrameTime, context.ChunkResolution, context.DocumentSize, renderTarget == context.RenderSurface,
+            context.FrameTime, context.ChunkResolution, context.RenderOutputSize, context.DocumentSize, renderTarget == context.RenderSurface,
             context.ProcessingColorSpace,
             context.ProcessingColorSpace,
             context.Opacity);
             context.Opacity);
         renderObjectContext.FullRerender = context.FullRerender;
         renderObjectContext.FullRerender = context.FullRerender;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs

@@ -62,7 +62,7 @@ public class TileNode : RenderNode
         if (paint == null)
         if (paint == null)
             return;
             return;
 
 
-        surface.Canvas.DrawRect(0, 0, context.DocumentSize.X, context.DocumentSize.Y, paint);
+        surface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

+ 38 - 14
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs

@@ -10,21 +10,24 @@ public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
 {
 {
     public const string OutputNamePropertyName = "OutputName";
     public const string OutputNamePropertyName = "OutputName";
     public const string IsDefaultExportPropertyName = "IsDefaultExport";
     public const string IsDefaultExportPropertyName = "IsDefaultExport";
-    public const string ExportSizePropertyName = "ExportSize";
+    public const string SizePropertyName = "Size";
     public RenderInputProperty Input { get; }
     public RenderInputProperty Input { get; }
     public InputProperty<string> OutputName { get; }
     public InputProperty<string> OutputName { get; }
     public InputProperty<bool> IsDefaultExport { get; }
     public InputProperty<bool> IsDefaultExport { get; }
-    public InputProperty<VecI> ExportSize { get; }
+    public InputProperty<VecI> Size { get; }
 
 
     private VecI? lastDocumentSize;
     private VecI? lastDocumentSize;
+
+    private TextureCache textureCache = new TextureCache();
+
     public CustomOutputNode()
     public CustomOutputNode()
     {
     {
         Input = new RenderInputProperty(this, OutputNode.InputPropertyName, "BACKGROUND", null);
         Input = new RenderInputProperty(this, OutputNode.InputPropertyName, "BACKGROUND", null);
         AddInputProperty(Input);
         AddInputProperty(Input);
-        
+
         OutputName = CreateInput(OutputNamePropertyName, "OUTPUT_NAME", "");
         OutputName = CreateInput(OutputNamePropertyName, "OUTPUT_NAME", "");
         IsDefaultExport = CreateInput(IsDefaultExportPropertyName, "IS_DEFAULT_EXPORT", false);
         IsDefaultExport = CreateInput(IsDefaultExportPropertyName, "IS_DEFAULT_EXPORT", false);
-        ExportSize = CreateInput(ExportSizePropertyName, "EXPORT_SIZE", VecI.Zero);
+        Size = CreateInput(SizePropertyName, "SIZE", VecI.Zero);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()
@@ -36,25 +39,46 @@ public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
     {
     {
         if (context.TargetOutput == OutputName.Value)
         if (context.TargetOutput == OutputName.Value)
         {
         {
-            lastDocumentSize = context.DocumentSize;
+            VecI targetSize = Size.Value.ShortestAxis <= 0
+                ? context.RenderOutputSize
+                : Size.Value;
+
+            lastDocumentSize = targetSize;
+
+            DrawingSurface targetSurface = context.RenderSurface;
+
+            if(context.RenderOutputSize != targetSize)
+            {
+                targetSurface = textureCache.RequestTexture(0, targetSize, context.ProcessingColorSpace).DrawingSurface;
+            }
+
+            int saved = targetSurface.Canvas.Save();
+            targetSurface.Canvas.ClipRect(new RectD(0, 0, targetSize.X, targetSize.Y));
 
 
-            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);
+            RenderContext outputContext = new RenderContext(context.RenderSurface, context.FrameTime, context.ChunkResolution,
+                targetSize, context.DocumentSize, context.ProcessingColorSpace, context.Opacity) { TargetOutput = OutputName.Value, };
 
 
-            context.RenderSurface.Canvas.RestoreToCount(saved);
+            Input.Value?.Paint(outputContext, targetSurface);
+
+            targetSurface.Canvas.RestoreToCount(saved);
+
+            if (targetSurface != context.RenderSurface)
+            {
+                context.RenderSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+            }
         }
         }
     }
     }
 
 
     RenderInputProperty IRenderInput.Background => Input;
     RenderInputProperty IRenderInput.Background => Input;
+
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
     {
         if (lastDocumentSize == null)
         if (lastDocumentSize == null)
         {
         {
             return null;
             return null;
         }
         }
-        
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y); 
+
+        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
     }
     }
 
 
     public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
@@ -63,12 +87,12 @@ public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
         {
         {
             return false;
             return false;
         }
         }
-        
+
         int saved = renderOn.Canvas.Save();
         int saved = renderOn.Canvas.Save();
         Input.Value.Paint(context, renderOn);
         Input.Value.Paint(context, renderOn);
-        
+
         renderOn.Canvas.RestoreToCount(saved);
         renderOn.Canvas.RestoreToCount(saved);
-        
+
         return true;
         return true;
     }
     }
 }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/EvaluateGraph_Change.cs

@@ -32,7 +32,7 @@ internal class EvaluateGraph_Change : Change
 
 
         using Texture renderTexture = Texture.ForProcessing(target.Size, target.ProcessingColorSpace);
         using Texture renderTexture = Texture.ForProcessing(target.Size, target.ProcessingColorSpace);
         RenderContext context =
         RenderContext context =
-            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, target.Size,
+            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, target.Size, target.Size,
                 target.ProcessingColorSpace) { FullRerender = true };
                 target.ProcessingColorSpace) { FullRerender = true };
         foreach (var nodeToEvaluate in queue)
         foreach (var nodeToEvaluate in queue)
         {
         {

+ 31 - 5
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -12,6 +12,7 @@ using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Text;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 
 
 namespace PixiEditor.ChangeableDocument.Rendering;
 namespace PixiEditor.ChangeableDocument.Rendering;
 
 
@@ -65,7 +66,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
 
 
-        RenderContext context = new(renderTexture.DrawingSurface, frame, resolution, Document.Size,
+        RenderContext context = new(renderTexture.DrawingSurface, frame, resolution, Document.Size, Document.Size,
             Document.ProcessingColorSpace);
             Document.ProcessingColorSpace);
         context.FullRerender = true;
         context.FullRerender = true;
         IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
@@ -111,7 +112,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
 
 
-        RenderContext context = new(renderTexture.DrawingSurface, frameTime, resolution, Document.Size,
+        RenderContext context = new(renderTexture.DrawingSurface, frameTime, resolution, Document.Size, Document.Size,
             Document.ProcessingColorSpace);
             Document.ProcessingColorSpace);
         context.FullRerender = true;
         context.FullRerender = true;
 
 
@@ -225,9 +226,6 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
 
 
-        RenderContext context =
-            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, Document.Size,
-                Document.ProcessingColorSpace) { FullRerender = true };
 
 
         bool hasCustomOutput = !string.IsNullOrEmpty(customOutput) && customOutput != "DEFAULT";
         bool hasCustomOutput = !string.IsNullOrEmpty(customOutput) && customOutput != "DEFAULT";
 
 
@@ -235,6 +233,10 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
             ? RenderingUtils.SolveFinalNodeGraph(customOutput, Document)
             ? RenderingUtils.SolveFinalNodeGraph(customOutput, Document)
             : Document.NodeGraph;
             : Document.NodeGraph;
 
 
+        RenderContext context =
+            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, SolveRenderOutputSize(customOutput, graph, Document.Size),
+                Document.Size, Document.ProcessingColorSpace) { FullRerender = true };
+
         if (hasCustomOutput)
         if (hasCustomOutput)
         {
         {
             context.TargetOutput = customOutput;
             context.TargetOutput = customOutput;
@@ -334,6 +336,30 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         return found ?? (membersOnlyGraph.OutputNode as IRenderInput)?.Background;
         return found ?? (membersOnlyGraph.OutputNode as IRenderInput)?.Background;
     }
     }
 
 
+    private static VecI SolveRenderOutputSize(string? targetOutput, IReadOnlyNodeGraph finalGraph, VecI documentSize)
+    {
+        VecI finalSize = documentSize;
+        if (targetOutput != null)
+        {
+            var outputNode = finalGraph.AllNodes.FirstOrDefault(n =>
+                n is CustomOutputNode outputNode && outputNode.OutputName.Value == targetOutput);
+
+            if (outputNode is CustomOutputNode customOutputNode)
+            {
+                if (customOutputNode.Size.Value.ShortestAxis > 0)
+                {
+                    finalSize = customOutputNode.Size.Value;
+                }
+            }
+            else
+            {
+                finalSize = documentSize;
+            }
+        }
+
+        return finalSize;
+    }
+
     public void Dispose()
     public void Dispose()
     {
     {
         renderTexture?.Dispose();
         renderTexture?.Dispose();

+ 5 - 3
src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs

@@ -13,8 +13,9 @@ public class RenderContext
 
 
     public KeyFrameTime FrameTime { get; }
     public KeyFrameTime FrameTime { get; }
     public ChunkResolution ChunkResolution { get; }
     public ChunkResolution ChunkResolution { get; }
+    public VecI RenderOutputSize { get; set; }
+
     public VecI DocumentSize { get; set; }
     public VecI DocumentSize { get; set; }
-    
     public DrawingSurface RenderSurface { get; set; }
     public DrawingSurface RenderSurface { get; set; }
     public bool FullRerender { get; set; } = false;
     public bool FullRerender { get; set; } = false;
     
     
@@ -23,14 +24,15 @@ public class RenderContext
 
 
 
 
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,
-        VecI docSize, ColorSpace processingColorSpace, double opacity = 1) 
+        VecI renderOutputSize, VecI documentSize, ColorSpace processingColorSpace, double opacity = 1)
     {
     {
         RenderSurface = renderSurface;
         RenderSurface = renderSurface;
         FrameTime = frameTime;
         FrameTime = frameTime;
         ChunkResolution = chunkResolution;
         ChunkResolution = chunkResolution;
-        DocumentSize = docSize;
+        RenderOutputSize = renderOutputSize;
         Opacity = opacity;
         Opacity = opacity;
         ProcessingColorSpace = processingColorSpace;
         ProcessingColorSpace = processingColorSpace;
+        DocumentSize = documentSize;
     }
     }
 
 
     public static DrawingApiBlendMode GetDrawingBlendMode(BlendMode blendMode)
     public static DrawingApiBlendMode GetDrawingBlendMode(BlendMode blendMode)

+ 2 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1045,5 +1045,6 @@
   "EXPORT_ZONE_NODE": "Export Zone",
   "EXPORT_ZONE_NODE": "Export Zone",
   "IS_DEFAULT_EXPORT": "Is Default Export",
   "IS_DEFAULT_EXPORT": "Is Default Export",
   "EXPORT_OUTPUT": "Export Output",
   "EXPORT_OUTPUT": "Export Output",
-  "EXPORT_SIZE": "Export Size"
+  "RENDER_OUTPUT_SIZE": "Render Output Size",
+  "RENDER_OUTPUT_CENTER": "Render Output Center"
 }
 }

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

@@ -21,5 +21,5 @@ internal interface INodeGraphHandler
    public void RemoveConnection(Guid nodeId, string property);
    public void RemoveConnection(Guid nodeId, string property);
    public void RemoveConnections(Guid nodeId);
    public void RemoveConnections(Guid nodeId);
    public void UpdateAvailableRenderOutputs();
    public void UpdateAvailableRenderOutputs();
-   public void GetComputedPropertyValue(INodePropertyHandler property);
+   public void RequestUpdateComputedPropertyValue(INodePropertyHandler property);
 }
 }

+ 2 - 0
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -31,4 +31,6 @@ public interface INodeHandler : INotifyPropertyChanged, IDisposable
     public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func);
     public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func);
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func);
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func);
+    public IReadOnlyDictionary<string, INodePropertyHandler> InputPropertyMap { get; }
+    public IReadOnlyDictionary<string, INodePropertyHandler> OutputPropertyMap { get; }
 }
 }

+ 1 - 1
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -154,7 +154,7 @@ public class PreviewPainter : IDisposable
 
 
             renderTexture.DrawingSurface.Canvas.SetMatrix(matrix ?? Matrix3X3.Identity);
             renderTexture.DrawingSurface.Canvas.SetMatrix(matrix ?? Matrix3X3.Identity);
 
 
-            RenderContext context = new(renderTexture.DrawingSurface, FrameTime, ChunkResolution.Full, DocumentSize,
+            RenderContext context = new(renderTexture.DrawingSurface, FrameTime, ChunkResolution.Full, DocumentSize, DocumentSize,
                 ProcessingColorSpace);
                 ProcessingColorSpace);
 
 
             dirtyTextures.Remove(texture);
             dirtyTextures.Remove(texture);

+ 35 - 8
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -10,6 +10,7 @@ using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 
 
 namespace PixiEditor.Models.Rendering;
 namespace PixiEditor.Models.Rendering;
@@ -73,9 +74,10 @@ internal class SceneRenderer : IDisposable
         Texture? renderTexture = null;
         Texture? renderTexture = null;
         bool restoreCanvas = false;
         bool restoreCanvas = false;
 
 
-        if (RenderInDocumentSize())
+        VecI finalSize = SolveRenderOutputSize(targetOutput, finalGraph, Document.Size);
+        if (RenderInOutputSize(finalGraph))
         {
         {
-            renderTexture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
+            renderTexture = Texture.ForProcessing(finalSize, Document.ProcessingColorSpace);
             renderTarget = renderTexture.DrawingSurface;
             renderTarget = renderTexture.DrawingSurface;
         }
         }
         else
         else
@@ -93,7 +95,7 @@ internal class SceneRenderer : IDisposable
         }
         }
 
 
         RenderContext context = new(renderTarget, DocumentViewModel.AnimationHandler.ActiveFrameTime,
         RenderContext context = new(renderTarget, DocumentViewModel.AnimationHandler.ActiveFrameTime,
-            resolution, Document.Size, Document.ProcessingColorSpace);
+            resolution, finalSize, Document.Size, Document.ProcessingColorSpace);
         context.TargetOutput = targetOutput;
         context.TargetOutput = targetOutput;
         finalGraph.Execute(context);
         finalGraph.Execute(context);
 
 
@@ -110,9 +112,33 @@ internal class SceneRenderer : IDisposable
         return renderTexture;
         return renderTexture;
     }
     }
 
 
-    private bool RenderInDocumentSize()
+    private static VecI SolveRenderOutputSize(string? targetOutput, IReadOnlyNodeGraph finalGraph, VecI documentSize)
     {
     {
-        return !HighResRendering || !HighDpiRenderNodePresent(Document.NodeGraph);
+        VecI finalSize = documentSize;
+        if (targetOutput != null)
+        {
+            var outputNode = finalGraph.AllNodes.FirstOrDefault(n =>
+                n is CustomOutputNode outputNode && outputNode.OutputName.Value == targetOutput);
+
+            if (outputNode is CustomOutputNode customOutputNode)
+            {
+                if (customOutputNode.Size.Value.ShortestAxis > 0)
+                {
+                    finalSize = customOutputNode.Size.Value;
+                }
+            }
+            else
+            {
+                finalSize = documentSize;
+            }
+        }
+
+        return finalSize;
+    }
+
+    private bool RenderInOutputSize(IReadOnlyNodeGraph finalGraph)
+    {
+        return !HighResRendering || !HighDpiRenderNodePresent(finalGraph);
     }
     }
 
 
     private bool ShouldRerender(DrawingSurface target, ChunkResolution resolution, string? targetOutput,
     private bool ShouldRerender(DrawingSurface target, ChunkResolution resolution, string? targetOutput,
@@ -136,7 +162,7 @@ internal class SceneRenderer : IDisposable
             return true;
             return true;
         }
         }
 
 
-        bool renderInDocumentSize = RenderInDocumentSize();
+        bool renderInDocumentSize = RenderInOutputSize(finalGraph);
         VecI compareSize = renderInDocumentSize ? Document.Size : target.DeviceClipBounds.Size;
         VecI compareSize = renderInDocumentSize ? Document.Size : target.DeviceClipBounds.Size;
 
 
         if (cachedTexture.DrawingSurface.DeviceClipBounds.Size != compareSize)
         if (cachedTexture.DrawingSurface.DeviceClipBounds.Size != compareSize)
@@ -224,6 +250,7 @@ internal class SceneRenderer : IDisposable
         double alphaFalloffMultiplier = 1.0 / animationData.OnionFrames;
         double alphaFalloffMultiplier = 1.0 / animationData.OnionFrames;
 
 
         var finalGraph = RenderingUtils.SolveFinalNodeGraph(targetOutput, Document);
         var finalGraph = RenderingUtils.SolveFinalNodeGraph(targetOutput, Document);
+        var renderOutputSize = SolveRenderOutputSize(targetOutput, finalGraph, Document.Size);
 
 
         // Render previous frames'
         // Render previous frames'
         for (int i = 1; i <= animationData.OnionFrames; i++)
         for (int i = 1; i <= animationData.OnionFrames; i++)
@@ -236,7 +263,7 @@ internal class SceneRenderer : IDisposable
 
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
 
 
-            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace,
+            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size, Document.ProcessingColorSpace,
                 finalOpacity);
                 finalOpacity);
             onionContext.TargetOutput = targetOutput;
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);
             finalGraph.Execute(onionContext);
@@ -252,7 +279,7 @@ internal class SceneRenderer : IDisposable
             }
             }
 
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
-            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace,
+            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size, Document.ProcessingColorSpace,
                 finalOpacity);
                 finalOpacity);
             onionContext.TargetOutput = targetOutput;
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);
             finalGraph.Execute(onionContext);

+ 4 - 4
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -602,7 +602,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
             Internals.ActionAccumulator.AddActions(
             Internals.ActionAccumulator.AddActions(
                 new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.OutputNamePropertyName, true),
                 new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.OutputNamePropertyName, true),
-                new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.ExportSizePropertyName, true));
+                new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.SizePropertyName, true));
         }
         }
 
 
         block.ExecuteQueuedActions();
         block.ExecuteQueuedActions();
@@ -628,7 +628,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             }
             }
 
 
             VecI originalSize =
             VecI originalSize =
-                exportZone.Inputs.FirstOrDefault(x => x.PropertyName == CustomOutputNode.ExportSizePropertyName)
+                exportZone.Inputs.FirstOrDefault(x => x.PropertyName == CustomOutputNode.SizePropertyName)
                     ?.ComputedValue as VecI? ?? SizeBindable;
                     ?.ComputedValue as VecI? ?? SizeBindable;
             if (originalSize.ShortestAxis <= 0)
             if (originalSize.ShortestAxis <= 0)
             {
             {
@@ -663,7 +663,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             Internals.ActionAccumulator.AddActions(
             Internals.ActionAccumulator.AddActions(
                 new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.OutputNamePropertyName, true),
                 new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.OutputNamePropertyName, true),
                 new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.IsDefaultExportPropertyName, true),
                 new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.IsDefaultExportPropertyName, true),
-                new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.ExportSizePropertyName, true));
+                new GetComputedPropertyValue_Action(node.Id, CustomOutputNode.SizePropertyName, true));
         }
         }
 
 
         block.ExecuteQueuedActions();
         block.ExecuteQueuedActions();
@@ -684,7 +684,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             return SizeBindable;
             return SizeBindable;
 
 
         var exportSize =
         var exportSize =
-            exportNode.Inputs.FirstOrDefault(x => x.PropertyName == CustomOutputNode.ExportSizePropertyName);
+            exportNode.Inputs.FirstOrDefault(x => x.PropertyName == CustomOutputNode.SizePropertyName);
 
 
         if (exportSize is null)
         if (exportSize is null)
             return SizeBindable;
             return SizeBindable;

+ 38 - 17
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -27,6 +27,8 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
     public StructureTree StructureTree { get; } = new();
     public StructureTree StructureTree { get; } = new();
     public INodeHandler? OutputNode { get; private set; }
     public INodeHandler? OutputNode { get; private set; }
 
 
+    public Dictionary<string, INodeHandler> CustomRenderOutputs { get; } = new();
+
     private DocumentInternalParts Internals { get; }
     private DocumentInternalParts Internals { get; }
 
 
     public NodeGraphViewModel(DocumentViewModel documentViewModel, DocumentInternalParts internals)
     public NodeGraphViewModel(DocumentViewModel documentViewModel, DocumentInternalParts internals)
@@ -115,7 +117,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
             connection.OutputProperty.ConnectedInputs.Remove(connection.InputProperty);
             connection.OutputProperty.ConnectedInputs.Remove(connection.InputProperty);
             Connections.Remove(connection);
             Connections.Remove(connection);
         }
         }
-        
+
         var node = AllNodes.FirstOrDefault(x => x.Id == nodeId);
         var node = AllNodes.FirstOrDefault(x => x.Id == nodeId);
         if (node != null)
         if (node != null)
         {
         {
@@ -224,9 +226,25 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
         Internals.ActionAccumulator.AddFinishedActions(new UpdatePropertyValue_Action(node.Id, property, value));
         Internals.ActionAccumulator.AddFinishedActions(new UpdatePropertyValue_Action(node.Id, property, value));
     }
     }
 
 
-    public void GetComputedPropertyValue(INodePropertyHandler property)
+    public void RequestUpdateComputedPropertyValue(INodePropertyHandler property)
     {
     {
-        Internals.ActionAccumulator.AddFinishedActions(new GetComputedPropertyValue_Action(property.Node.Id, property.PropertyName, property.IsInput));
+        Internals.ActionAccumulator.AddActions(
+            new GetComputedPropertyValue_Action(property.Node.Id, property.PropertyName, property.IsInput));
+    }
+
+    public T GetComputedPropertyValue<T>(INodePropertyHandler property)
+    {
+        var node = Internals.Tracker.Document.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == property.Node.Id);
+        if (property.IsInput)
+        {
+            var prop = node.GetInputProperty(property.PropertyName);
+            if (prop == null) return default;
+            return prop.Value is T value ? value : default;
+        }
+
+        var output = node.GetOutputProperty(property.PropertyName);
+        if (output == null) return default;
+        return output.Value is T outputValue ? outputValue : default;
     }
     }
 
 
     public void EndChangeNodePosition()
     public void EndChangeNodePosition()
@@ -273,7 +291,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
 
 
     public void RemoveNodes(Guid[] selectedNodes)
     public void RemoveNodes(Guid[] selectedNodes)
     {
     {
-        List<IAction> actions = new(); 
+        List<IAction> actions = new();
 
 
         for (int i = 0; i < selectedNodes.Length; i++)
         for (int i = 0; i < selectedNodes.Length; i++)
         {
         {
@@ -331,10 +349,10 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
 
 
         Internals.ActionAccumulator.AddFinishedActions(action);
         Internals.ActionAccumulator.AddFinishedActions(action);
     }
     }
-    
+
     public void UpdateAvailableRenderOutputs()
     public void UpdateAvailableRenderOutputs()
     {
     {
-        List<string> outputs = new();
+        Dictionary<string, INodeHandler> outputs = new();
         foreach (var node in AllNodes)
         foreach (var node in AllNodes)
         {
         {
             if (node.InternalName == typeof(CustomOutputNode).GetCustomAttribute<NodeInfoAttribute>().UniqueName)
             if (node.InternalName == typeof(CustomOutputNode).GetCustomAttribute<NodeInfoAttribute>().UniqueName)
@@ -344,40 +362,43 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
 
 
                 if (nameInput is { Value: string name } && !string.IsNullOrEmpty(name))
                 if (nameInput is { Value: string name } && !string.IsNullOrEmpty(name))
                 {
                 {
-                    if(outputs.Contains(name)) continue;
-                    
-                    outputs.Add(name);
+                    outputs[name] = node;
                 }
                 }
             }
             }
             else if (node.InternalName == typeof(OutputNode).GetCustomAttribute<NodeInfoAttribute>().UniqueName)
             else if (node.InternalName == typeof(OutputNode).GetCustomAttribute<NodeInfoAttribute>().UniqueName)
             {
             {
-                outputs.Insert(0, "DEFAULT");
+                outputs["DEFAULT"] = node;
             }
             }
         }
         }
-        
+
         RemoveExcessiveRenderOutputs(outputs);
         RemoveExcessiveRenderOutputs(outputs);
         AddMissingRenderOutputs(outputs);
         AddMissingRenderOutputs(outputs);
     }
     }
 
 
-    private void RemoveExcessiveRenderOutputs(List<string> outputs)
+    private void RemoveExcessiveRenderOutputs(Dictionary<string, INodeHandler> outputs)
     {
     {
         for (int i = AvailableRenderOutputs.Count - 1; i >= 0; i--)
         for (int i = AvailableRenderOutputs.Count - 1; i >= 0; i--)
         {
         {
-            if (!outputs.Contains(AvailableRenderOutputs[i]))
+            var outputName = AvailableRenderOutputs[i];
+            if (!outputs.ContainsKey(outputName))
             {
             {
                 AvailableRenderOutputs.RemoveAt(i);
                 AvailableRenderOutputs.RemoveAt(i);
             }
             }
+
+            CustomRenderOutputs.Remove(outputName);
         }
         }
     }
     }
-    
-    private void AddMissingRenderOutputs(List<string> outputs)
+
+    private void AddMissingRenderOutputs(Dictionary<string, INodeHandler> outputs)
     {
     {
         foreach (var output in outputs)
         foreach (var output in outputs)
         {
         {
-            if (!AvailableRenderOutputs.Contains(output))
+            if (!AvailableRenderOutputs.Contains(output.Key))
             {
             {
-                AvailableRenderOutputs.Add(output);
+                AvailableRenderOutputs.Add(output.Key);
             }
             }
+
+            CustomRenderOutputs[output.Key] = output.Value;
         }
         }
     }
     }
 
 

+ 125 - 9
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -1,4 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
 using System.Reflection;
 using System.Reflection;
 using Avalonia;
 using Avalonia;
 using Avalonia.Media;
 using Avalonia.Media;
@@ -26,14 +28,22 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     private IBrush? categoryBrush;
     private IBrush? categoryBrush;
     private string? nodeNameBindable;
     private string? nodeNameBindable;
     private VecD position;
     private VecD position;
-    private ObservableRangeCollection<INodePropertyHandler> inputs = new();
-    private ObservableRangeCollection<INodePropertyHandler> outputs = new();
+    private ObservableRangeCollection<INodePropertyHandler> inputs;
+    private ObservableRangeCollection<INodePropertyHandler> outputs;
     private PreviewPainter resultPainter;
     private PreviewPainter resultPainter;
     private bool isSelected;
     private bool isSelected;
     private string? icon;
     private string? icon;
 
 
     protected Guid id;
     protected Guid id;
 
 
+    public IReadOnlyDictionary<string, INodePropertyHandler> InputPropertyMap => inputPropertyMap;
+    public IReadOnlyDictionary<string, INodePropertyHandler> OutputPropertyMap => outputPropertyMap;
+
+    private Dictionary<string, INodePropertyHandler> inputPropertyMap = new Dictionary<string, INodePropertyHandler>();
+
+    private Dictionary<string, INodePropertyHandler> outputPropertyMap =
+        new Dictionary<string, INodePropertyHandler>();
+
     public Guid Id { get => id; private set => id = value; }
     public Guid Id { get => id; private set => id = value; }
 
 
     public LocalizedString DisplayName
     public LocalizedString DisplayName
@@ -104,13 +114,45 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     public ObservableRangeCollection<INodePropertyHandler> Inputs
     public ObservableRangeCollection<INodePropertyHandler> Inputs
     {
     {
         get => inputs;
         get => inputs;
-        set => SetProperty(ref inputs, value);
+        set
+        {
+            if (inputs != null)
+            {
+                inputs.CollectionChanged -= UpdateInputPropertyMapEvent;
+            }
+
+            if (SetProperty(ref inputs, value))
+            {
+                AddInputPropertyMap();
+            }
+
+            if (inputs != null)
+            {
+                inputs.CollectionChanged += UpdateInputPropertyMapEvent;
+            }
+        }
     }
     }
 
 
     public ObservableRangeCollection<INodePropertyHandler> Outputs
     public ObservableRangeCollection<INodePropertyHandler> Outputs
     {
     {
         get => outputs;
         get => outputs;
-        set => SetProperty(ref outputs, value);
+        set
+        {
+            if (outputs != null)
+            {
+                outputs.CollectionChanged -= UpdateOutputPropertyMapEvent;
+            }
+
+            if (SetProperty(ref outputs, value))
+            {
+                AddOutputPropertyMap();
+            }
+
+            if (outputs != null)
+            {
+                outputs.CollectionChanged += UpdateOutputPropertyMapEvent;
+            }
+        }
     }
     }
 
 
     public PreviewPainter ResultPainter
     public PreviewPainter ResultPainter
@@ -144,6 +186,9 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
 
         displayName = attribute.DisplayName;
         displayName = attribute.DisplayName;
         Category = attribute.Category;
         Category = attribute.Category;
+
+        Inputs = new ObservableRangeCollection<INodePropertyHandler>();
+        Outputs = new ObservableRangeCollection<INodePropertyHandler>();
     }
     }
 
 
     public NodeViewModel(string nodeNameBindable, Guid id, VecD position, DocumentViewModel document,
     public NodeViewModel(string nodeNameBindable, Guid id, VecD position, DocumentViewModel document,
@@ -154,6 +199,9 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         this.position = position;
         this.position = position;
         Document = document;
         Document = document;
         Internals = internals;
         Internals = internals;
+
+        Inputs = new ObservableRangeCollection<INodePropertyHandler>();
+        Outputs = new ObservableRangeCollection<INodePropertyHandler>();
     }
     }
 
 
     public void SetPosition(VecD newPosition)
     public void SetPosition(VecD newPosition)
@@ -350,7 +398,8 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func)
+    public void TraverseForwards(
+        Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler)>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler)>();
@@ -380,6 +429,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
+
     public virtual void Dispose()
     public virtual void Dispose()
     {
     {
         ResultPainter?.Dispose();
         ResultPainter?.Dispose();
@@ -387,22 +437,88 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
 
     public NodePropertyViewModel FindInputProperty(string propName)
     public NodePropertyViewModel FindInputProperty(string propName)
     {
     {
-        return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel;
+        if (string.IsNullOrEmpty(propName))
+        {
+            return null;
+        }
+
+        return inputPropertyMap.TryGetValue(propName, out var prop) ? prop as NodePropertyViewModel : null;
     }
     }
 
 
     public NodePropertyViewModel<T> FindInputProperty<T>(string propName)
     public NodePropertyViewModel<T> FindInputProperty<T>(string propName)
     {
     {
-        return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel<T>;
+        if (string.IsNullOrEmpty(propName))
+        {
+            return null;
+        }
+
+        return inputPropertyMap.TryGetValue(propName, out var prop) ? prop as NodePropertyViewModel<T> : null;
     }
     }
 
 
     public NodePropertyViewModel FindOutputProperty(string propName)
     public NodePropertyViewModel FindOutputProperty(string propName)
     {
     {
-        return Outputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel;
+        if (string.IsNullOrEmpty(propName))
+        {
+            return null;
+        }
+
+        return outputPropertyMap.TryGetValue(propName, out var prop) ? prop as NodePropertyViewModel : null;
     }
     }
 
 
     public NodePropertyViewModel<T> FindOutputProperty<T>(string propName)
     public NodePropertyViewModel<T> FindOutputProperty<T>(string propName)
     {
     {
-        return Outputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel<T>;
+        if (string.IsNullOrEmpty(propName))
+        {
+            return null;
+        }
+
+        return outputPropertyMap.TryGetValue(propName, out var prop) ? prop as NodePropertyViewModel<T> : null;
+    }
+
+    private void UpdateInputPropertyMapEvent(object? sender,
+        NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
+    {
+        AddInputPropertyMap();
+    }
+
+    private void UpdateOutputPropertyMapEvent(object? sender,
+        NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
+    {
+        AddOutputPropertyMap();
+    }
+
+    private void AddInputPropertyMap()
+    {
+        inputPropertyMap.Clear();
+        if (Inputs == null)
+        {
+            return;
+        }
+
+        foreach (var item in Inputs)
+        {
+            if (item == null) continue;
+            inputPropertyMap[item.PropertyName] = item;
+        }
+    }
+
+    private void AddOutputPropertyMap()
+    {
+        outputPropertyMap.Clear();
+        if (Outputs == null)
+        {
+            return;
+        }
+
+        foreach (var item in Outputs)
+        {
+            if (item == null)
+            {
+                continue;
+            }
+
+            outputPropertyMap[item.PropertyName] = item;
+        }
     }
     }
 }
 }
 
 

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -94,6 +94,6 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     [Command.Internal("PixiEditor.NodeGraph.GetComputedPropertyValue")]
     [Command.Internal("PixiEditor.NodeGraph.GetComputedPropertyValue")]
     public void GetComputedPropertyValue(INodePropertyHandler property)
     public void GetComputedPropertyValue(INodePropertyHandler property)
     {
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.GetComputedPropertyValue(property);
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.RequestUpdateComputedPropertyValue(property);
     }
     }
 }
 }

+ 0 - 7
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -132,11 +132,6 @@ internal class ViewportOverlays
         Binding isVisBinding = new() { Source = Viewport, Path = "GridLinesVisible", Mode = BindingMode.OneWay };
         Binding isVisBinding = new() { Source = Viewport, Path = "GridLinesVisible", Mode = BindingMode.OneWay };
         gridLinesOverlay.Bind(Visual.IsVisibleProperty, isVisBinding);
         gridLinesOverlay.Bind(Visual.IsVisibleProperty, isVisBinding);
 
 
-        Binding binding = new() { Source = Viewport, Path = "Document.Width", Mode = BindingMode.OneWay };
-        gridLinesOverlay.Bind(GridLinesOverlay.PixelWidthProperty, binding);
-        binding = new Binding { Source = Viewport, Path = "Document.Height", Mode = BindingMode.OneWay };
-        gridLinesOverlay.Bind(GridLinesOverlay.PixelHeightProperty, binding);
-
         Binding xBinding = new() { Source = Viewport, Path = "GridLinesXSize", Mode = BindingMode.OneWay };
         Binding xBinding = new() { Source = Viewport, Path = "GridLinesXSize", Mode = BindingMode.OneWay };
         gridLinesOverlay.Bind(GridLinesOverlay.GridXSizeProperty, xBinding);
         gridLinesOverlay.Bind(GridLinesOverlay.GridXSizeProperty, xBinding);
         Binding yBinding = new() { Source = Viewport, Path = "GridLinesYSize", Mode = BindingMode.OneWay };
         Binding yBinding = new() { Source = Viewport, Path = "GridLinesYSize", Mode = BindingMode.OneWay };
@@ -177,7 +172,6 @@ internal class ViewportOverlays
         {
         {
             Source = Viewport, Path = "Document.AnySymmetryAxisEnabledBindable", Mode = BindingMode.OneWay
             Source = Viewport, Path = "Document.AnySymmetryAxisEnabledBindable", Mode = BindingMode.OneWay
         };
         };
-        Binding sizeBinding = new() { Source = Viewport, Path = "Document.SizeBindable", Mode = BindingMode.OneWay };
         Binding isHitTestVisibleBinding = new()
         Binding isHitTestVisibleBinding = new()
         {
         {
             Source = Viewport,
             Source = Viewport,
@@ -203,7 +197,6 @@ internal class ViewportOverlays
         };
         };
 
 
         symmetryOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         symmetryOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
-        symmetryOverlay.Bind(SymmetryOverlay.SizeProperty, sizeBinding);
         symmetryOverlay.Bind(InputElement.IsHitTestVisibleProperty, isHitTestVisibleBinding);
         symmetryOverlay.Bind(InputElement.IsHitTestVisibleProperty, isHitTestVisibleBinding);
         symmetryOverlay.Bind(SymmetryOverlay.HorizontalAxisVisibleProperty, horizontalAxisVisibleBinding);
         symmetryOverlay.Bind(SymmetryOverlay.HorizontalAxisVisibleProperty, horizontalAxisVisibleBinding);
         symmetryOverlay.Bind(SymmetryOverlay.VerticalAxisVisibleProperty, verticalAxisVisibleBinding);
         symmetryOverlay.Bind(SymmetryOverlay.VerticalAxisVisibleProperty, verticalAxisVisibleBinding);

+ 2 - 20
src/PixiEditor/Views/Overlays/GridLinesOverlay.cs

@@ -18,24 +18,6 @@ public class GridLinesOverlay : Overlay
     public static readonly StyledProperty<double> GridYSizeProperty = AvaloniaProperty.Register<GridLinesOverlay, double>(
     public static readonly StyledProperty<double> GridYSizeProperty = AvaloniaProperty.Register<GridLinesOverlay, double>(
         nameof(GridYSize));
         nameof(GridYSize));
 
 
-    public static readonly StyledProperty<int> PixelWidthProperty = AvaloniaProperty.Register<GridLinesOverlay, int>(
-        nameof(PixelWidth));
-
-    public static readonly StyledProperty<int> PixelHeightProperty = AvaloniaProperty.Register<GridLinesOverlay, int>(
-        nameof(PixelHeight));
-
-    public int PixelHeight
-    {
-        get => GetValue(PixelHeightProperty);
-        set => SetValue(PixelHeightProperty, value);
-    }
-
-    public int PixelWidth
-    {
-        get => GetValue(PixelWidthProperty);
-        set => SetValue(PixelWidthProperty, value);
-    }
-
     public double GridXSize
     public double GridXSize
     {
     {
         get => GetValue(GridXSizeProperty);
         get => GetValue(GridXSizeProperty);
@@ -74,8 +56,8 @@ public class GridLinesOverlay : Overlay
     {
     {
         // Draw lines in vertical and horizontal directions, size should be relative to the scale
         // Draw lines in vertical and horizontal directions, size should be relative to the scale
 
 
-        double width = PixelWidth;
-        double height = PixelHeight;
+        double width = canvasBounds.Width;
+        double height = canvasBounds.Height;
 
 
         double columnWidth = GridXSize;
         double columnWidth = GridXSize;
         double rowHeight = GridYSize;
         double rowHeight = GridYSize;

+ 36 - 39
src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -83,7 +83,6 @@ internal class SymmetryOverlay : Overlay
 
 
     private SymmetryAxisDirection? capturedDirection;
     private SymmetryAxisDirection? capturedDirection;
     private SymmetryAxisDirection? hoveredDirection;
     private SymmetryAxisDirection? hoveredDirection;
-    public static readonly StyledProperty<VecI> SizeProperty = AvaloniaProperty.Register<SymmetryOverlay, VecI>(nameof(Size));
 
 
     private const double HandleSize = 12;
     private const double HandleSize = 12;
     private VectorPath handleGeometry = Handle.GetHandleGeometry("MarkerHandle").Path;
     private VectorPath handleGeometry = Handle.GetHandleGeometry("MarkerHandle").Path;
@@ -100,16 +99,12 @@ internal class SymmetryOverlay : Overlay
     
     
     private float PenThickness => 1.0f / (float)ZoomScale;
     private float PenThickness => 1.0f / (float)ZoomScale;
 
 
-    public VecI Size    
-    {
-        get { return (VecI)GetValue(SizeProperty); }
-        set { SetValue(SizeProperty, value); }
-    }
-
     private double horizontalAxisY;
     private double horizontalAxisY;
     private double verticalAxisX;
     private double verticalAxisX;
     private VecD pointerPosition;
     private VecD pointerPosition;
 
 
+    private VecF lastSize;
+
     static SymmetryOverlay()
     static SymmetryOverlay()
     {
     {
         AffectsRender<SymmetryOverlay>(HorizontalAxisVisibleProperty);
         AffectsRender<SymmetryOverlay>(HorizontalAxisVisibleProperty);
@@ -125,6 +120,8 @@ internal class SymmetryOverlay : Overlay
         if (!HorizontalAxisVisible && !VerticalAxisVisible)
         if (!HorizontalAxisVisible && !VerticalAxisVisible)
             return;
             return;
 
 
+        VecF size = (VecF)canvasBounds.Size;
+        lastSize = size;
         checkerBlack.StrokeWidth = PenThickness;
         checkerBlack.StrokeWidth = PenThickness;
         float dashWidth = DashWidth / (float)ZoomScale;
         float dashWidth = DashWidth / (float)ZoomScale;
         checkerWhite.PathEffect?.Dispose();
         checkerWhite.PathEffect?.Dispose();
@@ -141,12 +138,12 @@ internal class SymmetryOverlay : Overlay
             {
             {
                 if (horizontalAxisY != 0)
                 if (horizontalAxisY != 0)
                 {
                 {
-                    DrawHorizontalRuler(drawingContext, false);
+                    DrawHorizontalRuler(drawingContext, false, size);
                 }
                 }
 
 
-                if (horizontalAxisY != (int)Size.Y)
+                if (horizontalAxisY != (int)size.Y)
                 {
                 {
-                    DrawHorizontalRuler(drawingContext, true);
+                    DrawHorizontalRuler(drawingContext, true, size);
                 }
                 }
             }
             }
 
 
@@ -160,14 +157,14 @@ internal class SymmetryOverlay : Overlay
             
             
             save = drawingContext.Save();
             save = drawingContext.Save();
             drawingContext.Translate(0, (float)horizontalAxisY);
             drawingContext.Translate(0, (float)horizontalAxisY);
-            drawingContext.RotateDegrees(180, Size.X / 2, 0);
+            drawingContext.RotateDegrees(180, size.X / 2, 0);
             drawingContext.Scale((float)HandleSize / (float)ZoomScale, (float)HandleSize / (float)ZoomScale);
             drawingContext.Scale((float)HandleSize / (float)ZoomScale, (float)HandleSize / (float)ZoomScale);
             drawingContext.DrawPath(handleGeometry, borderPen);
             drawingContext.DrawPath(handleGeometry, borderPen);
 
 
             drawingContext.RestoreToCount(save);
             drawingContext.RestoreToCount(save);
 
 
-            drawingContext.DrawLine(new(0, horizontalAxisY), new(Size.X, horizontalAxisY), checkerBlack);
-            drawingContext.DrawLine(new(0, horizontalAxisY), new(Size.X, horizontalAxisY), checkerWhite);
+            drawingContext.DrawLine(new(0, horizontalAxisY), new(size.X, horizontalAxisY), checkerBlack);
+            drawingContext.DrawLine(new(0, horizontalAxisY), new(size.X, horizontalAxisY), checkerWhite);
         }
         }
         if (VerticalAxisVisible)
         if (VerticalAxisVisible)
         {
         {
@@ -175,12 +172,12 @@ internal class SymmetryOverlay : Overlay
             {
             {
                 if (verticalAxisX != 0)
                 if (verticalAxisX != 0)
                 {
                 {
-                    DrawVerticalRuler(drawingContext, false);
+                    DrawVerticalRuler(drawingContext, false, size);
                 }
                 }
 
 
-                if (verticalAxisX != (int)Size.X)
+                if (verticalAxisX != (int)size.X)
                 {
                 {
-                    DrawVerticalRuler(drawingContext, true);
+                    DrawVerticalRuler(drawingContext, true, size);
                 }
                 }
             }
             }
 
 
@@ -197,32 +194,32 @@ internal class SymmetryOverlay : Overlay
             saved = drawingContext.Save();
             saved = drawingContext.Save();
             drawingContext.RotateDegrees(90);
             drawingContext.RotateDegrees(90);
             drawingContext.Translate(0, (float)-verticalAxisX);
             drawingContext.Translate(0, (float)-verticalAxisX);
-            drawingContext.RotateDegrees(180, Size.Y / 2, 0);
+            drawingContext.RotateDegrees(180, size.Y / 2, 0);
             drawingContext.Scale((float)HandleSize / (float)ZoomScale, (float)HandleSize / (float)ZoomScale);
             drawingContext.Scale((float)HandleSize / (float)ZoomScale, (float)HandleSize / (float)ZoomScale);
             drawingContext.DrawPath(handleGeometry, borderPen);
             drawingContext.DrawPath(handleGeometry, borderPen);
 
 
             drawingContext.RestoreToCount(saved);
             drawingContext.RestoreToCount(saved);
 
 
-            drawingContext.DrawLine(new(verticalAxisX, 0), new(verticalAxisX, Size.Y), checkerBlack);
-            drawingContext.DrawLine(new(verticalAxisX, 0), new(verticalAxisX, Size.Y), checkerWhite);
+            drawingContext.DrawLine(new(verticalAxisX, 0), new(verticalAxisX, size.Y), checkerBlack);
+            drawingContext.DrawLine(new(verticalAxisX, 0), new(verticalAxisX, size.Y), checkerWhite);
         }
         }
     }
     }
 
 
-    private void DrawHorizontalRuler(Canvas drawingContext, bool upper)
+    private void DrawHorizontalRuler(Canvas drawingContext, bool upper, VecF size)
     {
     {
-        double start = upper ? Size.Y : 0;
-        bool drawRight = pointerPosition.X > Size.X / 2;
-        double xOffset = drawRight ? Size.X - RulerOffset * PenThickness * 2 : 0;
+        double start = upper ? size.Y : 0;
+        bool drawRight = pointerPosition.X > size.X / 2;
+        double xOffset = drawRight ? size.X - RulerOffset * PenThickness * 2 : 0;
 
 
         drawingContext.DrawLine(new VecD(RulerOffset * PenThickness + xOffset, start), new VecD(RulerOffset * PenThickness + xOffset, horizontalAxisY), rulerPen);
         drawingContext.DrawLine(new VecD(RulerOffset * PenThickness + xOffset, start), new VecD(RulerOffset * PenThickness + xOffset, horizontalAxisY), rulerPen);
         drawingContext.DrawLine(new VecD((RulerOffset - RulerWidth) * PenThickness + xOffset, start), new VecD((RulerOffset + RulerWidth) * PenThickness + xOffset, start), rulerPen);
         drawingContext.DrawLine(new VecD((RulerOffset - RulerWidth) * PenThickness + xOffset, start), new VecD((RulerOffset + RulerWidth) * PenThickness + xOffset, start), rulerPen);
         drawingContext.DrawLine(new VecD((RulerOffset - RulerWidth) * PenThickness + xOffset, horizontalAxisY), new VecD((RulerOffset + RulerWidth) * PenThickness + xOffset, horizontalAxisY), rulerPen);
         drawingContext.DrawLine(new VecD((RulerOffset - RulerWidth) * PenThickness + xOffset, horizontalAxisY), new VecD((RulerOffset + RulerWidth) * PenThickness + xOffset, horizontalAxisY), rulerPen);
 
 
-        string text = upper ? $"{start - horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({(start - horizontalAxisY) / Size.Y * 100:F1}%)" : $"{horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({horizontalAxisY / Size.Y * 100:F1}%)";
+        string text = upper ? $"{start - horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({(start - horizontalAxisY) / size.Y * 100:F1}%)" : $"{horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({horizontalAxisY / size.Y * 100:F1}%)";
 
 
         using Font font = Font.CreateDefault(14f / (float)ZoomScale);
         using Font font = Font.CreateDefault(14f / (float)ZoomScale);
         
         
-        if (Size.Y < font.Size * 2.5 || horizontalAxisY == (int)Size.Y && upper || horizontalAxisY == 0 && !upper)
+        if (size.Y < font.Size * 2.5 || horizontalAxisY == (int)size.Y && upper || horizontalAxisY == 0 && !upper)
         {
         {
             return;
             return;
         }
         }
@@ -231,27 +228,27 @@ internal class SymmetryOverlay : Overlay
 
 
         if (upper)
         if (upper)
         {
         {
-            textY += Size.Y / 2f;
+            textY += size.Y / 2f;
         }
         }
 
 
         drawingContext.DrawText(text, new VecD(RulerOffset * PenThickness - (drawRight ? -1 : 1) + xOffset, textY), drawRight ? TextAlign.Left : TextAlign.Right, font, textPaint);
         drawingContext.DrawText(text, new VecD(RulerOffset * PenThickness - (drawRight ? -1 : 1) + xOffset, textY), drawRight ? TextAlign.Left : TextAlign.Right, font, textPaint);
     }
     }
 
 
-    private void DrawVerticalRuler(Canvas drawingContext, bool right)
+    private void DrawVerticalRuler(Canvas drawingContext, bool right, VecF size)
     {
     {
-        double start = right ? Size.X : 0;
-        bool drawBottom = pointerPosition.Y > Size.Y / 2;
-        double yOffset = drawBottom ? Size.Y - RulerOffset * PenThickness * 2 : 0;
+        double start = right ? size.X : 0;
+        bool drawBottom = pointerPosition.Y > size.Y / 2;
+        double yOffset = drawBottom ? size.Y - RulerOffset * PenThickness * 2 : 0;
 
 
         drawingContext.DrawLine(new VecD(start, RulerOffset * PenThickness + yOffset), new VecD(verticalAxisX, RulerOffset * PenThickness + yOffset), rulerPen);
         drawingContext.DrawLine(new VecD(start, RulerOffset * PenThickness + yOffset), new VecD(verticalAxisX, RulerOffset * PenThickness + yOffset), rulerPen);
         drawingContext.DrawLine(new VecD(start, (RulerOffset - RulerWidth) * PenThickness + yOffset), new VecD(start, (RulerOffset + RulerWidth) * PenThickness + yOffset), rulerPen);
         drawingContext.DrawLine(new VecD(start, (RulerOffset - RulerWidth) * PenThickness + yOffset), new VecD(start, (RulerOffset + RulerWidth) * PenThickness + yOffset), rulerPen);
         drawingContext.DrawLine(new VecD(verticalAxisX, (RulerOffset - RulerWidth) * PenThickness + yOffset), new VecD(verticalAxisX, (RulerOffset + RulerWidth) * PenThickness + yOffset), rulerPen);
         drawingContext.DrawLine(new VecD(verticalAxisX, (RulerOffset - RulerWidth) * PenThickness + yOffset), new VecD(verticalAxisX, (RulerOffset + RulerWidth) * PenThickness + yOffset), rulerPen);
 
 
-        string text = right ? $"{start - verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({(start - verticalAxisX) / Size.X * 100:F1}%)" : $"{verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({verticalAxisX / Size.X * 100:F1}%)";
+        string text = right ? $"{start - verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({(start - verticalAxisX) / size.X * 100:F1}%)" : $"{verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({verticalAxisX / size.X * 100:F1}%)";
 
 
         using Font font = Font.CreateDefault(14f / (float)ZoomScale);
         using Font font = Font.CreateDefault(14f / (float)ZoomScale);
         
         
-        if (Size.X < font.MeasureText(text) * 2.5 || verticalAxisX == (int)Size.X && right || verticalAxisX == 0 && !right)
+        if (size.X < font.MeasureText(text) * 2.5 || verticalAxisX == (int)size.X && right || verticalAxisX == 0 && !right)
         {
         {
             return;
             return;
         }
         }
@@ -260,7 +257,7 @@ internal class SymmetryOverlay : Overlay
 
 
         if (right)
         if (right)
         {
         {
-            textX += Size.X / 2;
+            textX += size.X / 2;
         }
         }
 
 
         double textY = RulerOffset * PenThickness - ((drawBottom ? 5 : 2 + font.Size) * PenThickness) + yOffset;
         double textY = RulerOffset * PenThickness - ((drawBottom ? 5 : 2 + font.Size) * PenThickness) + yOffset;
@@ -276,9 +273,9 @@ internal class SymmetryOverlay : Overlay
     {
     {
         double radius = HandleSize * 4 / ZoomScale / 2;
         double radius = HandleSize * 4 / ZoomScale / 2;
         VecD left = new(-radius, horizontalAxisY);
         VecD left = new(-radius, horizontalAxisY);
-        VecD right = new(Size.X + radius, horizontalAxisY);
+        VecD right = new(lastSize.X + radius, horizontalAxisY);
         VecD up = new(verticalAxisX, -radius);
         VecD up = new(verticalAxisX, -radius);
-        VecD down = new(verticalAxisX, Size.Y + radius);
+        VecD down = new(verticalAxisX, lastSize.Y + radius);
 
 
         if (HorizontalAxisVisible && (Math.Abs((left - position).LongestAxis) < radius || Math.Abs((right - position).LongestAxis) < radius))
         if (HorizontalAxisVisible && (Math.Abs((left - position).LongestAxis) < radius || Math.Abs((right - position).LongestAxis) < radius))
             return SymmetryAxisDirection.Horizontal;
             return SymmetryAxisDirection.Horizontal;
@@ -336,11 +333,11 @@ internal class SymmetryOverlay : Overlay
             return;
             return;
         if (capturedDirection == SymmetryAxisDirection.Horizontal)
         if (capturedDirection == SymmetryAxisDirection.Horizontal)
         {
         {
-            horizontalAxisY = Math.Round(Math.Clamp(args.Point.Y, 0, Size.Y) * 2) / 2;
+            horizontalAxisY = Math.Round(Math.Clamp(args.Point.Y, 0, lastSize.Y) * 2) / 2;
 
 
             if (args.Modifiers.HasFlag(KeyModifiers.Shift))
             if (args.Modifiers.HasFlag(KeyModifiers.Shift))
             {
             {
-                double temp = Math.Round(horizontalAxisY / Size.Y * 8) / 8 * Size.Y;
+                double temp = Math.Round(horizontalAxisY / lastSize.Y * 8) / 8 * lastSize.Y;
                 horizontalAxisY = Math.Round(temp * 2) / 2;
                 horizontalAxisY = Math.Round(temp * 2) / 2;
             }
             }
 
 
@@ -348,12 +345,12 @@ internal class SymmetryOverlay : Overlay
         }
         }
         else if (capturedDirection == SymmetryAxisDirection.Vertical)
         else if (capturedDirection == SymmetryAxisDirection.Vertical)
         {
         {
-            verticalAxisX = Math.Round(Math.Clamp(args.Point.X, 0, Size.X) * 2) / 2;
+            verticalAxisX = Math.Round(Math.Clamp(args.Point.X, 0, lastSize.X) * 2) / 2;
 
 
             if (args.Modifiers.HasFlag(KeyModifiers.Control))
             if (args.Modifiers.HasFlag(KeyModifiers.Control))
             {
             {
 
 
-                double temp = Math.Round(verticalAxisX / Size.X * 8) / 8 * Size.X;
+                double temp = Math.Round(verticalAxisX / lastSize.X * 8) / 8 * lastSize.X;
                 verticalAxisX = Math.Round(temp * 2) / 2;
                 verticalAxisX = Math.Round(temp * 2) / 2;
             }
             }
 
 

+ 69 - 15
src/PixiEditor/Views/Rendering/Scene.cs

@@ -28,8 +28,10 @@ using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Rendering;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using Drawie.Skia;
 using Drawie.Skia;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
+using PixiEditor.ViewModels.Document.Nodes.Workspace;
 using PixiEditor.Views.Overlays;
 using PixiEditor.Views.Overlays;
 using PixiEditor.Views.Overlays.Pointers;
 using PixiEditor.Views.Overlays.Pointers;
 using PixiEditor.Views.Visuals;
 using PixiEditor.Views.Visuals;
@@ -74,8 +76,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(AutoBackgroundScaleProperty, value);
         set => SetValue(AutoBackgroundScaleProperty, value);
     }
     }
 
 
-    public static readonly StyledProperty<double> CustomBackgroundScaleXProperty = AvaloniaProperty.Register<Scene, double>(
-        nameof(CustomBackgroundScaleX));
+    public static readonly StyledProperty<double> CustomBackgroundScaleXProperty =
+        AvaloniaProperty.Register<Scene, double>(
+            nameof(CustomBackgroundScaleX));
 
 
     public double CustomBackgroundScaleX
     public double CustomBackgroundScaleX
     {
     {
@@ -83,8 +86,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(CustomBackgroundScaleXProperty, value);
         set => SetValue(CustomBackgroundScaleXProperty, value);
     }
     }
 
 
-    public static readonly StyledProperty<double> CustomBackgroundScaleYProperty = AvaloniaProperty.Register<Scene, double>(
-        nameof(CustomBackgroundScaleY));
+    public static readonly StyledProperty<double> CustomBackgroundScaleYProperty =
+        AvaloniaProperty.Register<Scene, double>(
+            nameof(CustomBackgroundScaleY));
 
 
     public double CustomBackgroundScaleY
     public double CustomBackgroundScaleY
     {
     {
@@ -191,6 +195,8 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         CustomBackgroundScaleXProperty.Changed.AddClassHandler<Scene>(Refresh);
         CustomBackgroundScaleXProperty.Changed.AddClassHandler<Scene>(Refresh);
         CustomBackgroundScaleYProperty.Changed.AddClassHandler<Scene>(Refresh);
         CustomBackgroundScaleYProperty.Changed.AddClassHandler<Scene>(Refresh);
         BackgroundBitmapProperty.Changed.AddClassHandler<Scene>(Refresh);
         BackgroundBitmapProperty.Changed.AddClassHandler<Scene>(Refresh);
+        RenderOutputProperty.Changed.AddClassHandler<Scene>(Refresh);
+        RenderOutputProperty.Changed.AddClassHandler<Scene>(UpdateRenderOutput);
     }
     }
 
 
     private static void Refresh(Scene scene, AvaloniaPropertyChangedEventArgs args)
     private static void Refresh(Scene scene, AvaloniaPropertyChangedEventArgs args)
@@ -290,7 +296,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
 
         renderTexture.Canvas.SetMatrix(matrix.ToSKMatrix().ToMatrix3X3());
         renderTexture.Canvas.SetMatrix(matrix.ToSKMatrix().ToMatrix3X3());
 
 
-        RectD dirtyBounds = new RectD(0, 0, Document.Width, Document.Height);
+        VecI outputSize = FindOutputSize();
+
+        RectD dirtyBounds = new RectD(0, 0, outputSize.X, outputSize.Y);
         RenderScene(dirtyBounds);
         RenderScene(dirtyBounds);
 
 
         renderTexture.Canvas.Restore();
         renderTexture.Canvas.Restore();
@@ -298,26 +306,23 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
 
     private void RenderScene(RectD bounds)
     private void RenderScene(RectD bounds)
     {
     {
+        var renderOutput = RenderOutput == "DEFAULT" ? null : RenderOutput;
         DrawCheckerboard(renderTexture.DrawingSurface, bounds);
         DrawCheckerboard(renderTexture.DrawingSurface, bounds);
         DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Background);
         DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Background);
         try
         try
         {
         {
-            SceneRenderer.RenderScene(renderTexture.DrawingSurface, CalculateResolution(),
-                RenderOutput == "DEFAULT" ? null : RenderOutput);
+            SceneRenderer.RenderScene(renderTexture.DrawingSurface, CalculateResolution(), renderOutput);
         }
         }
         catch (Exception e)
         catch (Exception e)
         {
         {
             renderTexture.DrawingSurface.Canvas.Clear();
             renderTexture.DrawingSurface.Canvas.Clear();
-            using Paint paint = new Paint
-            {
-                Color = Colors.White,
-                IsAntiAliased = true
-            };
+            using Paint paint = new Paint { Color = Colors.White, IsAntiAliased = true };
 
 
             using Font defaultSizedFont = Font.CreateDefault();
             using Font defaultSizedFont = Font.CreateDefault();
             defaultSizedFont.Size = 24;
             defaultSizedFont.Size = 24;
 
 
-            renderTexture.DrawingSurface.Canvas.DrawText(new LocalizedString("ERROR_GRAPH"), renderTexture.Size / 2f, TextAlign.Center, defaultSizedFont, paint);
+            renderTexture.DrawingSurface.Canvas.DrawText(new LocalizedString("ERROR_GRAPH"), renderTexture.Size / 2f,
+                TextAlign.Center, defaultSizedFont, paint);
         }
         }
 
 
         DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Foreground);
         DrawOverlays(renderTexture.DrawingSurface, bounds, OverlayRenderSorting.Foreground);
@@ -385,6 +390,26 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         }
         }
     }
     }
 
 
+    private VecI FindOutputSize()
+    {
+        VecI outputSize = Document.SizeBindable;
+
+        if (!string.IsNullOrEmpty(RenderOutput))
+        {
+            if (Document.NodeGraph.CustomRenderOutputs.TryGetValue(RenderOutput, out var node))
+            {
+                var prop = node?.Inputs.FirstOrDefault(x => x.PropertyName == CustomOutputNode.SizePropertyName);
+                if (prop != null)
+                {
+                    VecI size = Document.NodeGraph.GetComputedPropertyValue<VecI>(prop);
+                    outputSize = size;
+                }
+            }
+        }
+
+        return outputSize;
+    }
+
     protected override void OnPointerMoved(PointerEventArgs e)
     protected override void OnPointerMoved(PointerEventArgs e)
     {
     {
         base.OnPointerMoved(e);
         base.OnPointerMoved(e);
@@ -825,13 +850,42 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         if (e.NewValue is DocumentViewModel documentViewModel)
         if (e.NewValue is DocumentViewModel documentViewModel)
         {
         {
             documentViewModel.SizeChanged += scene.DocumentViewModelOnSizeChanged;
             documentViewModel.SizeChanged += scene.DocumentViewModelOnSizeChanged;
-            scene.ContentDimensions = documentViewModel.SizeBindable;
+            scene.ContentDimensions = scene.GetRenderOutputSize();
         }
         }
     }
     }
 
 
     private void DocumentViewModelOnSizeChanged(object? sender, DocumentSizeChangedEventArgs e)
     private void DocumentViewModelOnSizeChanged(object? sender, DocumentSizeChangedEventArgs e)
     {
     {
-        ContentDimensions = e.NewSize;
+        ContentDimensions = GetRenderOutputSize();
+    }
+
+    private VecI GetRenderOutputSize()
+    {
+        VecI outputSize = Document.SizeBindable;
+
+        if (!string.IsNullOrEmpty(RenderOutput))
+        {
+            if (Document.NodeGraph.CustomRenderOutputs.TryGetValue(RenderOutput, out var node))
+            {
+                var prop = node?.Inputs.FirstOrDefault(x => x.PropertyName == CustomOutputNode.SizePropertyName);
+                if (prop != null)
+                {
+                    VecI size = Document.NodeGraph.GetComputedPropertyValue<VecI>(prop);
+                    outputSize = size;
+                }
+            }
+        }
+
+        return outputSize;
+    }
+
+    private static void UpdateRenderOutput(Scene scene, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.NewValue is string newValue)
+        {
+            scene.ContentDimensions = scene.GetRenderOutputSize();
+            scene.CenterContent();
+        }
     }
     }
 
 
     private static void DefaultCursorChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
     private static void DefaultCursorChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)

+ 1 - 1
tests/PixiEditor.Tests/BlendingTests.cs

@@ -58,7 +58,7 @@ public class BlendingTests : PixiEditorTest
         secondImageLayer.BlendMode.NonOverridenValue = blendMode;
         secondImageLayer.BlendMode.NonOverridenValue = blendMode;
 
 
         Surface output = Surface.ForProcessing(VecI.One, ColorSpace.CreateSrgbLinear());
         Surface output = Surface.ForProcessing(VecI.One, ColorSpace.CreateSrgbLinear());
-        graph.Execute(new RenderContext(output.DrawingSurface, 0, ChunkResolution.Full, VecI.One, ColorSpace.CreateSrgbLinear(), 1));
+        graph.Execute(new RenderContext(output.DrawingSurface, 0, ChunkResolution.Full, VecI.One, VecI.One, ColorSpace.CreateSrgbLinear(), 1));
 
 
         Color result = output.GetSrgbPixel(VecI.Zero);
         Color result = output.GetSrgbPixel(VecI.Zero);
         Assert.Equal(expectedColor, result.ToRgbHex());
         Assert.Equal(expectedColor, result.ToRgbHex());