Browse Source

Made previews work

flabbet 1 year ago
parent
commit
2c192b4bc7

+ 2 - 0
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -20,4 +20,6 @@ public interface IReadOnlyChunkyImage
     AffectedArea FindAffectedArea(int fromOperationIndex = 0);
     HashSet<VecI> FindCommittedChunks();
     HashSet<VecI> FindAllChunks();
+    VecI CommittedSize { get; }
+    VecI LatestSize { get; }
 }

+ 3 - 2
src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs

@@ -207,9 +207,10 @@ internal class AffectedAreasGatherer
     private void AddToImagePreviews(Guid memberGuid, AffectedArea area, bool ignoreSelf = false)
     {
         var path = tracker.Document.FindMemberPath(memberGuid);
-        if (path.Count < 2)
+        int minCount = ignoreSelf ? 2 : 1;
+        if (path.Count < minCount)
             return;
-        for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
+        for (int i = ignoreSelf ? 1 : 0; i < path.Count; i++)
         {
             var member = path[i];
             if (!ImagePreviewAreas.ContainsKey(member.Id))

+ 46 - 6
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -73,6 +73,7 @@ internal class MemberPreviewUpdater
         }).ConfigureAwait(true);
 
         RecreatePreviewBitmaps(changedMainPreviewBounds!, changedMaskPreviewBounds!);
+        
         var renderInfos = await Task.Run(() => Render(changedMainPreviewBounds!, changedMaskPreviewBounds))
             .ConfigureAwait(true);
 
@@ -387,6 +388,7 @@ internal class MemberPreviewUpdater
         RenderWholeCanvasPreview(mainPreviewChunksToRerender, maskPreviewChunksToRerender, infos);
         RenderMainPreviews(mainPreviewChunksToRerender, recreatedMainPreviewSizes, infos);
         RenderMaskPreviews(maskPreviewChunksToRerender, recreatedMaskPreviewSizes, infos);
+        RenderNodePreviews();
 
         return infos;
 
@@ -436,8 +438,8 @@ internal class MemberPreviewUpdater
                 _ => ChunkResolution.Eighth,
             };
             var pos = chunkPos * resolution.PixelSize();
-            /*var rendered = ChunkRenderer.MergeWholeStructure(chunkPos, resolution,
-                internals.Tracker.Document.StructureRoot, doc.AnimationHandler.ActiveFrameBindable);
+            var rendered = DocumentEvaluator.RenderChunk(chunkPos, resolution,
+                internals.Tracker.Document.NodeGraph, doc.AnimationHandler.ActiveFrameBindable);
             doc.PreviewSurface.DrawingSurface.Canvas.Save();
             doc.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
             doc.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
@@ -453,7 +455,7 @@ internal class MemberPreviewUpdater
                 renderedChunk.DrawOnSurface(doc.PreviewSurface.DrawingSurface, pos, SmoothReplacingPaint);
             }
 
-            doc.PreviewSurface.DrawingSurface.Canvas.Restore();*/
+            doc.PreviewSurface.DrawingSurface.Canvas.Restore();
         }
 
         if (somethingChanged)
@@ -549,7 +551,7 @@ internal class MemberPreviewUpdater
             var pos = chunk * ChunkResolution.Full.PixelSize();
             // drawing in full res here is kinda slow
             // we could switch to a lower resolution based on (canvas size / preview size) to make it run faster
-            /*OneOf<Chunk, EmptyChunk> rendered = ChunkRenderer.MergeWholeStructure(chunk, ChunkResolution.Full, folder,
+            OneOf<Chunk, EmptyChunk> rendered = DocumentEvaluator.RenderChunk(chunk, ChunkResolution.Full, folder,
                 doc.AnimationHandler.ActiveFrameBindable);
             if (rendered.IsT0)
             {
@@ -561,7 +563,7 @@ internal class MemberPreviewUpdater
             {
                 memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(),
                     ChunkResolution.Full.PixelSize(), ClearPaint);
-            }*/
+            }
         }
 
         memberVM.PreviewSurface.DrawingSurface.Canvas.Restore();
@@ -581,11 +583,18 @@ internal class MemberPreviewUpdater
         foreach (var chunk in area.Chunks)
         {
             var pos = chunk * ChunkResolution.Full.PixelSize();
-            if (!layer.Execute(doc.AnimationHandler.ActiveFrameBindable).DrawCommittedChunkOn(chunk,
+            IReadOnlyChunkyImage? result = layer is IReadOnlyImageNode raster
+                ? raster.GetLayerImageAtFrame(doc.AnimationHandler.ActiveFrameBindable)
+                : layer.Execute(doc.AnimationHandler.ActiveFrameBindable);
+            
+            if (!result.DrawCommittedChunkOn(
+                    chunk,
                     ChunkResolution.Full, memberVM.PreviewSurface.DrawingSurface, pos,
                     scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
+            {
                 memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
                     ChunkyImage.FullChunkSize, ClearPaint);
+            }
         }
 
         memberVM.PreviewSurface.DrawingSurface.Canvas.Restore();
@@ -674,4 +683,35 @@ internal class MemberPreviewUpdater
             infos.Add(new MaskPreviewDirty_RenderInfo(guid));
         }
     }
+    
+    private void RenderNodePreviews()
+    {
+        // TODO: recreate only changed previews
+        internals.Tracker.Document.NodeGraph.TryTraverse(node =>
+        {
+            if (node.CachedResult != null)
+            {
+               var nodeVm = doc.StructureHelper.FindNode<INodeHandler>(node.Id);
+
+               // TODO: do it in recreate preview bitmaps
+               if (nodeVm.ResultPreview == null)
+               {
+                     nodeVm.ResultPreview = new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
+               }
+               
+               float scalingX = (float)nodeVm.ResultPreview.Size.X / node.CachedResult.CommittedSize.X;
+               float scalingY = (float)nodeVm.ResultPreview.Size.Y / node.CachedResult.CommittedSize.Y;
+               
+               nodeVm.ResultPreview.DrawingSurface.Canvas.Save();
+               nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
+               
+               nodeVm.ResultPreview.DrawingSurface.Canvas.Clear();
+               node.CachedResult.DrawCommittedRegionOn(
+                   new RectI(0, 0, node.CachedResult.CommittedSize.X, node.CachedResult.CommittedSize.Y), ChunkResolution.Full,
+                   nodeVm.ResultPreview.DrawingSurface, new VecI(0, 0), ReplacingPaint);
+               
+               nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
+            }
+        });
+    }
 }

+ 2 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/ConnectionView.axaml

@@ -4,9 +4,10 @@
 
     <ControlTheme TargetType="nodes:ConnectionView" x:Key="{x:Type nodes:ConnectionView}">
         <Setter Property="ClipToBounds" Value="False"/>
+        <Setter Property="IsHitTestVisible" Value="False"/>
         <Setter Property="Template">
             <ControlTemplate>
-                    <Line Stroke="{DynamicResource ThemeForegroundBrush}" StrokeThickness="2"
+                    <nodes:ConnectionLine Color="{DynamicResource ThemeForegroundBrush}" Thickness="2"
                           StartPoint="{Binding StartPoint, RelativeSource={RelativeSource TemplatedParent}}"
                           EndPoint="{Binding EndPoint, RelativeSource={RelativeSource TemplatedParent}}" />
             </ControlTemplate>

+ 1 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml

@@ -15,7 +15,7 @@
                                 />
                         </Flyout>
                         </Grid.ContextFlyout>
-                        <ItemsControl ClipToBounds="False"
+                        <ItemsControl ZIndex="1" ClipToBounds="False"
                                       ItemsSource="{Binding NodeGraph.AllNodes, RelativeSource={RelativeSource TemplatedParent}}">
                             <ItemsControl.ItemsPanel>
                                 <ItemsPanelTemplate>

+ 50 - 42
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml

@@ -6,7 +6,7 @@
         <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}" />
         <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
         <Setter Property="BorderThickness" Value="1" />
-        <Setter Property="CornerRadius" Value="5" />
+        <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
         <Setter Property="Margin" Value="5" />
         <Setter Property="Padding" Value="5" />
         <Setter Property="Template">
@@ -15,50 +15,58 @@
                         BorderBrush="{TemplateBinding BorderBrush}"
                         BorderThickness="{TemplateBinding BorderThickness}"
                         CornerRadius="{TemplateBinding CornerRadius}"
+                        BoxShadow="0 0 5 0 Black"
                         Margin="{TemplateBinding Margin}"
-                        Name="RootBorder"
-                        Padding="{TemplateBinding Padding}">
-                    <Grid>
-                        <Grid.RowDefinitions>
-                            <RowDefinition Height="Auto" />
-                            <RowDefinition Height="Auto" />
-                            <RowDefinition Height="Auto" />
-                        </Grid.RowDefinitions>
-                        <TextBlock Grid.ColumnSpan="3" Grid.Row="0" Text="{TemplateBinding DisplayName}"
-                                   FontWeight="Bold" />
-                        <Grid Grid.Row="1">
-                            <Grid.ColumnDefinitions>
-                                <ColumnDefinition Width="0.5*" />
-                                <ColumnDefinition Width="0.5*" />
-                            </Grid.ColumnDefinitions>
+                        Name="RootBorder">
+                        <Grid>
+                            <Grid.RowDefinitions>
+                                <RowDefinition Height="Auto" />
+                                <RowDefinition Height="Auto" />
+                                <RowDefinition Height="Auto" />
+                            </Grid.RowDefinitions>
+                            <Border Padding="{TemplateBinding Padding}" Grid.ColumnSpan="3" Grid.Row="0"
+                                    CornerRadius="4.5, 4.5, 0 ,0"
+                                    Background="{DynamicResource ThemeControlHighBrush}">
+                                <TextBlock Text="{TemplateBinding DisplayName}"
+                                           FontWeight="Bold" />
+                            </Border>
+                            <Border Grid.Row="1" Background="{DynamicResource ThemeControlMidBrush}"
+                                    Padding="{TemplateBinding Padding}">
+                                <Grid>
+                                    <Grid.ColumnDefinitions>
+                                        <ColumnDefinition Width="0.5*" />
+                                        <ColumnDefinition Width="0.5*" />
+                                    </Grid.ColumnDefinitions>
 
-                            <ItemsControl ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
-                                <ItemsControl.ItemContainerTheme>
-                                    <ControlTheme TargetType="ContentPresenter">
-                                        <Setter Property="DataContext" Value="." />
-                                    </ControlTheme>
-                                </ItemsControl.ItemContainerTheme>
-                            </ItemsControl>
-                            <ItemsControl Grid.Column="1" ItemsSource="{TemplateBinding Outputs}" ClipToBounds="False">
-                                <ItemsControl.ItemContainerTheme>
-                                    <ControlTheme TargetType="ContentPresenter">
-                                        <Setter Property="DataContext" Value="." />
-                                    </ControlTheme>
-                                </ItemsControl.ItemContainerTheme>
-                            </ItemsControl>
+                                    <ItemsControl ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
+                                        <ItemsControl.ItemContainerTheme>
+                                            <ControlTheme TargetType="ContentPresenter">
+                                                <Setter Property="DataContext" Value="." />
+                                            </ControlTheme>
+                                        </ItemsControl.ItemContainerTheme>
+                                    </ItemsControl>
+                                    <ItemsControl Grid.Column="1" ItemsSource="{TemplateBinding Outputs}"
+                                                  ClipToBounds="False">
+                                        <ItemsControl.ItemContainerTheme>
+                                            <ControlTheme TargetType="ContentPresenter">
+                                                <Setter Property="DataContext" Value="." />
+                                            </ControlTheme>
+                                        </ItemsControl.ItemContainerTheme>
+                                    </ItemsControl>
+                                </Grid>
+                            </Border>
+                            <Border CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
+                                <visuals:SurfaceControl Width="200" Height="200"
+                                                        Surface="{TemplateBinding ResultPreview}"
+                                                        RenderOptions.BitmapInterpolationMode="None">
+                                    <visuals:SurfaceControl.Background>
+                                        <ImageBrush Source="/Images/CheckerTile.png"
+                                                    TileMode="Tile" DestinationRect="0, 0, 25, 25" />
+                                    </visuals:SurfaceControl.Background>
+                                </visuals:SurfaceControl>
+                            </Border>
                         </Grid>
-                        <Border CornerRadius="{DynamicResource ControlCornerRadius}" Grid.Row="2">
-                            <visuals:SurfaceControl Width="50" Height="50"
-                                                    Surface="{TemplateBinding ResultPreview}"
-                                                    RenderOptions.BitmapInterpolationMode="None">
-                                <visuals:SurfaceControl.Background>
-                                    <ImageBrush Source="/Images/CheckerTile.png"
-                                                TileMode="Tile" DestinationRect="0, 0, 25, 25" />
-                                </visuals:SurfaceControl.Background>
-                            </visuals:SurfaceControl>
-                        </Border>
-                    </Grid>
-                </Border>
+                    </Border>
             </ControlTemplate>
         </Setter>
 

+ 68 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/ConnectionLine.cs

@@ -0,0 +1,68 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes;
+
+public class ConnectionLine : Control
+{
+    public static readonly StyledProperty<SolidColorBrush> ColorProperty = AvaloniaProperty.Register<ConnectionLine, SolidColorBrush>("Color");
+    public static readonly StyledProperty<double> ThicknessProperty = AvaloniaProperty.Register<ConnectionLine, double>("Thickness");
+    public static readonly StyledProperty<Point> StartPointProperty = AvaloniaProperty.Register<ConnectionLine, Point>("StartPoint");
+    public static readonly StyledProperty<Point> EndPointProperty = AvaloniaProperty.Register<ConnectionLine, Point>("EndPoint");
+
+    public SolidColorBrush Color
+    {
+        get { return (SolidColorBrush)GetValue(ColorProperty); }
+        set { SetValue(ColorProperty, value); }
+    }
+
+    public double Thickness
+    {
+        get { return (double)GetValue(ThicknessProperty); }
+        set { SetValue(ThicknessProperty, value); }
+    }
+
+    public Point StartPoint
+    {
+        get { return (Point)GetValue(StartPointProperty); }
+        set { SetValue(StartPointProperty, value); }
+    }
+
+    public Point EndPoint
+    {
+        get { return (Point)GetValue(EndPointProperty); }
+        set { SetValue(EndPointProperty, value); }
+    }
+    
+    static ConnectionLine()
+    {
+        AffectsRender<ConnectionLine>(ColorProperty, ThicknessProperty, StartPointProperty, EndPointProperty);
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        var p1 = new Point(StartPoint.X, StartPoint.Y);
+        var p2 = new Point(EndPoint.X, EndPoint.Y);
+
+        // curved line
+        var controlPoint = new Point((p1.X + p2.X) / 2, p1.Y);
+        var controlPoint2 = new Point((p1.X + p2.X) / 2, p2.Y);
+        
+        if (p1.X < p2.X)
+        {
+            p1 = new Point(p1.X - 5, p1.Y);
+            p2 = new Point(p2.X + 5, p2.Y);
+            
+            controlPoint2 = new Point(p2.X, (p1.Y + p2.Y) / 2);
+            controlPoint = new Point(p1.X, (p1.Y + p2.Y) / 2);
+        }
+        
+        var geometry = new StreamGeometry();
+        using var ctx = geometry.Open();
+        ctx.BeginFigure(p1, false);
+        ctx.CubicBezierTo(controlPoint, controlPoint2, p2);
+        
+        context.DrawGeometry(Color, new Pen(Color, Thickness), geometry);
+    }
+}

+ 19 - 21
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -307,7 +307,14 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         if (NodeGraph.OutputNode == null) return [];
 
         var list = new List<Node>();
-        FillNodePath(NodeGraph.OutputNode, guid, list);
+        
+        var targetNode = FindNode(guid);
+        if (targetNode == null)
+        {
+            return [];
+        }
+        
+        FillNodePath(targetNode, list);
         return list;
     }
 
@@ -320,37 +327,28 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         if (NodeGraph.OutputNode == null) return [];
 
         var list = new List<Node>();
-        FillNodePath(NodeGraph.OutputNode, guid, list);
+        var targetNode = FindNode(guid);
+        if (targetNode == null)
+        {
+            return [];
+        }
+        FillNodePath(targetNode, list);
         return list.Cast<StructureNode>().ToList();
     }
 
-    private bool FillNodePath(Node node, Guid guid, List<Node> toFill)
+    private bool FillNodePath(Node node, List<Node> toFill)
     {
-        if (node.Id == guid)
-        {
-            return true;
-        }
-
-        if (node is StructureNode structureNode)
+        node.TraverseForwards(newNode =>
         {
-            toFill.Add(structureNode);
-        }
-
-        bool found = false;
-
-        node.TraverseBackwards((newNode) =>
-        {
-            if (newNode is StructureNode strNode && newNode.Id == guid)
+            if (newNode is StructureNode strNode)
             {
                 toFill.Add(strNode);
-                found = true;
-                return false;
             }
 
             return true;
         });
-
-        return found;
+        
+        return true;
     }
 
     public List<Guid> ExtractLayers(IList<Guid> members)

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

@@ -2,5 +2,4 @@
 
 public interface IReadOnlyLayerNode : IReadOnlyStructureNode
 {
-    
 }

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

@@ -10,8 +10,9 @@ public interface IReadOnlyNode
     public IReadOnlyCollection<IOutputProperty> OutputProperties { get; }
     public IReadOnlyCollection<IReadOnlyNode> ConnectedOutputNodes { get; }
     public VecD Position { get; }
+    public IReadOnlyChunkyImage? CachedResult { get; }
 
-    public ChunkyImage? Execute(KeyFrameTime frame);
+    public IReadOnlyChunkyImage? Execute(KeyFrameTime frame);
     public bool Validate();
     
     /// <summary>

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

@@ -25,7 +25,7 @@ public class CircleNode : Node
         Output = CreateOutput<ChunkyImage>("Output", "OUTPUT", null);
     }
     
-    public override ChunkyImage? OnExecute(KeyFrameTime frameTime)
+    protected override ChunkyImage? OnExecute(KeyFrameTime frameTime)
     {
         Output.Value = new ChunkyImage(new VecI(Radius.Value * 2, Radius.Value * 2));
         

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

@@ -20,7 +20,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
 
     public override Node CreateCopy() => new FolderNode { MemberName = MemberName };
 
-    public override ChunkyImage? OnExecute(KeyFrameTime frame)
+    protected override ChunkyImage? OnExecute(KeyFrameTime frame)
     {
         if (!IsVisible.Value || Content.Value == null)
         {

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

@@ -28,7 +28,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return true; 
     }
 
-    public override ChunkyImage OnExecute(KeyFrameTime frame)
+    protected override ChunkyImage OnExecute(KeyFrameTime frame)
     {
         if (!IsVisible.Value)
         {

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

@@ -27,7 +27,7 @@ public class MergeNode : Node, IBackgroundInput
         return new MergeNode();
     }
 
-    public override ChunkyImage? OnExecute(KeyFrameTime frame)
+    protected override ChunkyImage? OnExecute(KeyFrameTime frame)
     {
         if(Top.Value == null && Bottom.Value == null)
         {

+ 9 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -18,17 +18,19 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public IReadOnlyCollection<InputProperty> InputProperties => inputs;
     public IReadOnlyCollection<OutputProperty> OutputProperties => outputs;
     public IReadOnlyCollection<IReadOnlyNode> ConnectedOutputNodes => _connectedNodes;
+    public IReadOnlyChunkyImage CachedResult { get; private set; }
 
     IReadOnlyCollection<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyCollection<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     public VecD Position { get; set; }
 
-    public ChunkyImage? Execute(KeyFrameTime frameTime)
+    public IReadOnlyChunkyImage? Execute(KeyFrameTime frameTime)
     {
-        return OnExecute(frameTime);
+        CachedResult = OnExecute(frameTime);
+        return CachedResult;
     }
 
-    public abstract ChunkyImage? OnExecute(KeyFrameTime frameTime);
+    protected abstract ChunkyImage? OnExecute(KeyFrameTime frameTime);
     public abstract bool Validate();
 
     public void RemoveKeyFrame(Guid keyFrameGuid)
@@ -145,9 +147,9 @@ public abstract class Node : IReadOnlyNode, IDisposable
             }
         }
     }
-    
+
     public abstract Node CreateCopy();
-    
+
     public Node Clone()
     {
         var clone = CreateCopy();
@@ -160,11 +162,13 @@ public abstract class Node : IReadOnlyNode, IDisposable
             var newInput = input.Clone(clone);
             clone.inputs.Add(newInput);
         }
+
         foreach (var output in outputs)
         {
             var newOutput = output.Clone(clone);
             clone.outputs.Add(newOutput);
         }
+
         return clone;
     }
 

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

@@ -21,7 +21,7 @@ public class OutputNode : Node, IBackgroundInput
         return new OutputNode();
     }
 
-    public override ChunkyImage? OnExecute(KeyFrameTime frame)
+    protected override ChunkyImage? OnExecute(KeyFrameTime frame)
     {
         return Input.Value;
     }

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

@@ -32,7 +32,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
         Output = CreateOutput<ChunkyImage?>("Output", "OUTPUT", null);
     }
 
-    public abstract override ChunkyImage? OnExecute(KeyFrameTime frameTime);
+    protected abstract override ChunkyImage? OnExecute(KeyFrameTime frameTime);
     public abstract override bool Validate();
 
     public abstract RectI? GetTightBounds(KeyFrameTime frameTime);

+ 39 - 2
src/PixiEditor.ChangeableDocument/Rendering/DocumentEvaluator.cs

@@ -23,7 +23,7 @@ public static class DocumentEvaluator
 
             chunk.Surface.DrawingSurface.Canvas.Save();
             chunk.Surface.DrawingSurface.Canvas.Clear();
-            
+
             if (transformedClippingRect is not null)
             {
                 chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
@@ -31,7 +31,44 @@ public static class DocumentEvaluator
 
             evaluated.DrawMostUpToDateChunkOn(chunkPos, resolution, chunk.Surface.DrawingSurface, VecI.Zero,
                 context.ReplacingPaintWithOpacity);
-            
+
+            chunk.Surface.DrawingSurface.Canvas.Restore();
+
+            return chunk;
+        }
+        catch (ObjectDisposedException)
+        {
+            return new EmptyChunk();
+        }
+    }
+
+    public static OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution,
+        IReadOnlyNode node, int frame, RectI? globalClippingRect = null)
+    {
+        using RenderingContext context = new();
+        try
+        {
+            RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
+
+            IReadOnlyChunkyImage? evaluated = node.Execute(frame);
+            if (evaluated is null)
+            {
+                return new EmptyChunk();
+            }
+
+            Chunk chunk = Chunk.Create(resolution);
+
+            chunk.Surface.DrawingSurface.Canvas.Save();
+            chunk.Surface.DrawingSurface.Canvas.Clear();
+
+            if (transformedClippingRect is not null)
+            {
+                chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+            }
+
+            evaluated.DrawMostUpToDateChunkOn(chunkPos, resolution, chunk.Surface.DrawingSurface, VecI.Zero,
+                context.ReplacingPaintWithOpacity);
+
             chunk.Surface.DrawingSurface.Canvas.Restore();
 
             return chunk;