Browse Source

Implemented preview painter for structure members

flabbet 10 months ago
parent
commit
34df18ac93

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

@@ -1,9 +1,10 @@
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 public interface IPreviewRenderable
 {
-    public bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame);
+    public bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame);
 }

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

@@ -9,7 +9,7 @@ using PixiEditor.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("Folder")]
-public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
+public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPreviewRenderable
 {
     public RenderInputProperty Content { get; }
 
@@ -163,4 +163,22 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
             }
         }
     }
+
+    public bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame)
+    {
+        if (Content.Connection != null)
+        {
+            var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node);
+            while (executionQueue.Count > 0)
+            {
+                IReadOnlyNode node = executionQueue.Dequeue();
+                if (node is IPreviewRenderable previewRenderable)
+                {
+                    previewRenderable.RenderPreview(renderOn, resolution, frame);
+                }
+            }
+        }
+
+        return true;
+    }
 }

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

@@ -33,7 +33,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     };
 
     private Texture fullResrenderedSurface; 
-
+    private int renderedSurfaceFrame = -1;
     public ImageLayerNode(VecI size)
     {
         RawOutput = CreateOutput<Texture>(nameof(RawOutput), "RAW_LAYER_OUTPUT", null);
@@ -111,7 +111,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         workingSurface.Canvas.RestoreToCount(saved);
     }
 
-    public override bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame)
+    public override bool RenderPreview(DrawingSurface renderOnto, ChunkResolution resolution, int frame)
     {
         var img = GetLayerImageAtFrame(frame);
 
@@ -120,12 +120,17 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             return false;
         }
 
-        img.DrawMostUpToDateChunkOn(
-            chunk,
-            resolution,
-            renderOn.DrawingSurface,
-            chunk * resolution.PixelSize(),
-            blendPaint);
+        if (renderedSurfaceFrame == frame)
+        {
+            renderOnto.Canvas.DrawSurface(fullResrenderedSurface.DrawingSurface, VecI.Zero, blendPaint);
+        }
+        else
+        {
+            img.DrawMostUpToDateRegionOn(
+                new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
+                resolution,
+                renderOnto, VecI.Zero, blendPaint);
+        }
 
         return true;
     }
@@ -178,7 +183,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         }
     }
 
-
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageAtFrame(int frame) => GetLayerImageAtFrame(frame);
 
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageByKeyFrameGuid(Guid keyFrameGuid) =>
@@ -196,6 +200,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         var img = GetLayerImageAtFrame(frameTime.Frame);
 
         RenderChunkyImageChunk(chunkPos, resolution, img, ref fullResrenderedSurface);
+        renderedSurfaceFrame = frameTime.Frame;
     }
     
     public void ForEveryFrame(Action<ChunkyImage> action)

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

@@ -167,7 +167,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         return workingSurface;
     }
 
-    public abstract bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame);
+    public abstract bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame);
 
     void IClipSource.DrawOnTexture(SceneObjectRenderContext context, DrawingSurface drawOnto)
     {

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

@@ -65,7 +65,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         Rasterize(workingSurface, ctx.ChunkResolution, paint);
     }
 
-    public override bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame)
+    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame)
     {
         if (ShapeData == null)
         {
@@ -94,7 +94,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         Matrix3X3 matrix = ShapeData.TransformationMatrix;
         Rasterize(toRasterizeOn.DrawingSurface, resolution, paint);
 
-        renderOn.DrawingSurface.Canvas.DrawSurface(toRasterizeOn.DrawingSurface, 0, 0, paint);
+        renderOn.Canvas.DrawSurface(toRasterizeOn.DrawingSurface, 0, 0, paint);
 
         toRasterizeOn.DrawingSurface.Canvas.RestoreToCount(save);
         return true;

+ 20 - 20
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -40,7 +40,7 @@ internal class ActionAccumulator
         {
             queuedActions.Add((ActionSource.User, action));
         }
-        
+
         queuedActions.Add((ActionSource.Automated, new ChangeBoundary_Action()));
         TryExecuteAccumulatedActions();
     }
@@ -51,10 +51,10 @@ internal class ActionAccumulator
         {
             queuedActions.Add((ActionSource.User, action));
         }
-        
+
         TryExecuteAccumulatedActions();
     }
-    
+
     public void AddActions(ActionSource source, IAction action)
     {
         queuedActions.Add((source, action));
@@ -95,7 +95,8 @@ internal class ActionAccumulator
             List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
             bool undoBoundaryPassed =
                 toExecute.Any(static action => action.action is ChangeBoundary_Action or Redo_Action or Undo_Action);
-            bool viewportRefreshRequest = toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
+            bool viewportRefreshRequest =
+                toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
             foreach (IChangeInfo info in optimizedChanges)
             {
                 internals.Updater.ApplyChangeFromChangeInfo(info);
@@ -111,16 +112,15 @@ internal class ActionAccumulator
             if (DrawingBackendApi.Current.IsHardwareAccelerated)
             {
                 renderResult.AddRange(canvasUpdater.UpdateGatheredChunksSync(affectedAreas,
-                    undoBoundaryPassed || viewportRefreshRequest)); 
-                renderResult.AddRange(previewUpdater.UpdateGatheredChunksSync(affectedAreas, undoBoundaryPassed));
+                    undoBoundaryPassed || viewportRefreshRequest));
             }
             else
             {
                 renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas,
                     undoBoundaryPassed || viewportRefreshRequest));
-                renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
             }
 
+            renderResult.AddRange(previewUpdater.UpdatePreviews(undoBoundaryPassed, affectedAreas.ImagePreviewAreas.Keys));
 
             if (undoBoundaryPassed)
             {
@@ -165,38 +165,38 @@ internal class ActionAccumulator
                 {
                     //TODO: Validate if it's required
                 }
-                break;
+                    break;
                 case PreviewDirty_RenderInfo info:
                 {
-                    var bitmap = document.StructureHelper.Find(info.GuidValue)?.PreviewSurface;
+                    /*var bitmap = document.StructureHelper.Find(info.GuidValue)?.PreviewPainter;
                     if (bitmap is null)
                         continue;
-                    bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
+                    bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));*/
                 }
-                break;
+                    break;
                 case MaskPreviewDirty_RenderInfo info:
                 {
-                    var bitmap = document.StructureHelper.Find(info.GuidValue)?.MaskPreviewSurface;
+                    /*var bitmap = document.StructureHelper.Find(info.GuidValue)?.MaskPreviewSurface;
                     if (bitmap is null)
                         continue;
-                    bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
+                    bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));*/
                 }
-                break;
+                    break;
                 case CanvasPreviewDirty_RenderInfo:
                 {
                     document.PreviewSurface.AddDirtyRect(new RectI(0, 0, document.PreviewSurface.Size.X,
                         document.PreviewSurface.Size.Y));
                 }
-                break;
+                    break;
                 case NodePreviewDirty_RenderInfo info:
                 {
-                    var node = document.StructureHelper.Find(info.NodeId);
-                    if (node is null || node.PreviewSurface is null)
+                    /*var node = document.StructureHelper.Find(info.NodeId);
+                    if (node is null || node.PreviewPainter is null)
                         continue;
-                    node.PreviewSurface.AddDirtyRect(new RectI(0, 0, node.PreviewSurface.Size.X,
-                        node.PreviewSurface.Size.Y));
+                    node.PreviewPainter.AddDirtyRect(new RectI(0, 0, node.PreviewPainter.Size.X,
+                        node.PreviewPainter.Size.Y));*/
                 }
-                break;
+                    break;
             }
         }
     }

+ 3 - 2
src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs

@@ -5,6 +5,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.Layers;
+using PixiEditor.Models.Rendering;
 using PixiEditor.Numerics;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
@@ -13,8 +14,8 @@ namespace PixiEditor.Models.Handlers;
 internal interface IStructureMemberHandler : INodeHandler
 {
     public bool HasMaskBindable { get; }
-    public Texture? MaskPreviewSurface { get; set; }
-    public Texture? PreviewSurface { get; set; }
+    public PreviewPainter? MaskPreviewSurface { get; set; }
+    public PreviewPainter? PreviewPainter { get; set; }
     public bool MaskIsVisibleBindable { get; set; }
     public StructureMemberSelectionType Selection { get; set; }
     public float OpacityBindable { get; set; }

+ 1 - 0
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -30,6 +30,7 @@ internal class AffectedAreasGatherer
     public AffectedArea MainImageArea { get; private set; } = new();
     public Dictionary<Guid, AffectedArea> ImagePreviewAreas { get; private set; } = new();
     public Dictionary<Guid, AffectedArea> MaskPreviewAreas { get; private set; } = new();
+    
 
     private KeyFrameTime ActiveFrame { get; set; }
 

+ 40 - 521
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -1,20 +1,10 @@
 #nullable enable
 
-using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Threading.Tasks;
-using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
-using ChunkyImageLib.Operations;
-using PixiEditor.ViewModels.Document;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ChangeableDocument.Changeables.Interfaces;
-using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.Bridge;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Helpers;
@@ -32,12 +22,6 @@ internal class MemberPreviewUpdater
     private readonly IDocument doc;
     private readonly DocumentInternalParts internals;
 
-    private Dictionary<Guid, RectI> lastMainPreviewTightBounds = new();
-    private Dictionary<Guid, RectI> lastMaskPreviewTightBounds = new();
-
-    private Dictionary<Guid, AffectedArea> mainPreviewAreasAccumulator = new();
-    private Dictionary<Guid, AffectedArea> maskPreviewAreasAccumulator = new();
-
     private static readonly Paint SmoothReplacingPaint = new()
     {
         BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium, IsAntiAliased = true
@@ -54,251 +38,16 @@ internal class MemberPreviewUpdater
         this.internals = internals;
     }
 
-    /// <summary>
-    /// Don't call this outside ActionAccumulator
-    /// </summary>
-    public async Task<List<IRenderInfo>> UpdateGatheredChunks
-        (AffectedAreasGatherer chunkGatherer, bool rerenderPreviews)
+    public List<IRenderInfo> UpdatePreviews(bool rerenderPreviews, IEnumerable<Guid> keys)
     {
-        AddAreasToAccumulator(chunkGatherer);
         if (!rerenderPreviews)
             return new List<IRenderInfo>();
 
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?>? changedMainPreviewBounds = null;
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?>? changedMaskPreviewBounds = null;
-
-        int atFrame = doc.AnimationHandler.ActiveFrameBindable;
-
-        await Task.Run(() =>
-        {
-            changedMainPreviewBounds = FindChangedTightBounds(atFrame, false);
-            changedMaskPreviewBounds = FindChangedTightBounds(atFrame, true);
-        }).ConfigureAwait(true);
-
-        RecreatePreviewBitmaps(changedMainPreviewBounds!, changedMaskPreviewBounds!);
-
-        var renderInfos = await Task.Run(() => Render(changedMainPreviewBounds!, changedMaskPreviewBounds))
-            .ConfigureAwait(true);
-
-        CleanupUnusedTightBounds();
-
-        foreach (var a in changedMainPreviewBounds)
-        {
-            if (a.Value is not null)
-                lastMainPreviewTightBounds[a.Key] = a.Value.Value.tightBounds;
-            else
-                lastMainPreviewTightBounds.Remove(a.Key);
-        }
-
-        foreach (var a in changedMaskPreviewBounds)
-        {
-            if (a.Value is not null)
-                lastMaskPreviewTightBounds[a.Key] = a.Value.Value.tightBounds;
-            else
-                lastMaskPreviewTightBounds.Remove(a.Key);
-        }
+        var renderInfos = UpdatePreviewPainters(keys);
 
         return renderInfos;
     }
 
-    /// <summary>
-    /// Don't call this outside ActionAccumulator
-    /// </summary>
-    public List<IRenderInfo> UpdateGatheredChunksSync
-        (AffectedAreasGatherer chunkGatherer, bool rerenderPreviews)
-    {
-        AddAreasToAccumulator(chunkGatherer);
-        if (!rerenderPreviews)
-            return new List<IRenderInfo>();
-
-        int frame = doc.AnimationHandler.ActiveFrameBindable;
-
-        var changedMainPreviewBounds = FindChangedTightBounds(frame, false);
-        var changedMaskPreviewBounds = FindChangedTightBounds(frame, true);
-
-        RecreatePreviewBitmaps(changedMainPreviewBounds, changedMaskPreviewBounds);
-        var renderInfos = Render(changedMainPreviewBounds, changedMaskPreviewBounds);
-
-        CleanupUnusedTightBounds();
-
-        foreach (var a in changedMainPreviewBounds)
-        {
-            if (a.Value is not null)
-                lastMainPreviewTightBounds[a.Key] = a.Value.Value.tightBounds;
-        }
-
-        foreach (var a in changedMaskPreviewBounds)
-        {
-            if (a.Value is not null)
-                lastMaskPreviewTightBounds[a.Key] = a.Value.Value.tightBounds;
-        }
-
-        return renderInfos;
-    }
-
-    /// <summary>
-    /// Cleans up <see cref="lastMainPreviewTightBounds"/> and <see cref="lastMaskPreviewTightBounds"/> to get rid of tight bounds that belonged to now deleted layers
-    /// </summary>
-    private void CleanupUnusedTightBounds()
-    {
-        Dictionary<Guid, RectI> clearedLastMainPreviewTightBounds = new Dictionary<Guid, RectI>();
-        Dictionary<Guid, RectI> clearedLastMaskPreviewTightBounds = new Dictionary<Guid, RectI>();
-
-        internals.Tracker.Document.ForEveryReadonlyMember(member =>
-        {
-            if (lastMainPreviewTightBounds.ContainsKey(member.Id))
-                clearedLastMainPreviewTightBounds.Add(member.Id, lastMainPreviewTightBounds[member.Id]);
-            if (lastMaskPreviewTightBounds.ContainsKey(member.Id))
-                clearedLastMaskPreviewTightBounds.Add(member.Id, lastMaskPreviewTightBounds[member.Id]);
-        });
-
-        lastMainPreviewTightBounds = clearedLastMainPreviewTightBounds;
-        lastMaskPreviewTightBounds = clearedLastMaskPreviewTightBounds;
-    }
-
-    /// <summary>
-    /// Unions the areas inside <see cref="mainPreviewAreasAccumulator"/> and <see cref="maskPreviewAreasAccumulator"/> with the newly updated areas
-    /// </summary>
-    private void AddAreasToAccumulator(AffectedAreasGatherer areasGatherer)
-    {
-        AddAreas(areasGatherer.ImagePreviewAreas, mainPreviewAreasAccumulator);
-        AddAreas(areasGatherer.MaskPreviewAreas, maskPreviewAreasAccumulator);
-    }
-
-    private static void AddAreas(Dictionary<Guid, AffectedArea> from, Dictionary<Guid, AffectedArea> to)
-    {
-        foreach ((Guid guid, AffectedArea area) in from)
-        {
-            if (!to.ContainsKey(guid))
-                to[guid] = new AffectedArea();
-            var toArea = to[guid];
-            toArea.UnionWith(area);
-            to[guid] = toArea;
-        }
-    }
-
-    /// <summary>
-    /// Looks at the accumulated areas and determines which members need to have their preview bitmaps resized or deleted
-    /// </summary>
-    private Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> FindChangedTightBounds(int atFrame, bool forMasks)
-    {
-        // VecI? == null stands for "layer is empty, the preview needs to be deleted"
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> newPreviewBitmapSizes = new();
-
-        var targetAreas = forMasks ? maskPreviewAreasAccumulator : mainPreviewAreasAccumulator;
-        var targetLastBounds = forMasks ? lastMaskPreviewTightBounds : lastMainPreviewTightBounds;
-        foreach (var (guid, area) in targetAreas)
-        {
-            var member = internals.Tracker.Document.FindMember(guid);
-            if (member is null)
-                continue;
-
-            if (forMasks && member.EmbeddedMask is null)
-            {
-                newPreviewBitmapSizes.Add(guid, null);
-                continue;
-            }
-
-            RectI? tightBounds = GetOrFindMemberTightBounds(member, atFrame, area, forMasks);
-            RectI? maybeLastBounds = targetLastBounds.TryGetValue(guid, out RectI lastBounds) ? lastBounds : null;
-            if (tightBounds == maybeLastBounds)
-                continue;
-
-            if (tightBounds is null)
-            {
-                newPreviewBitmapSizes.Add(guid, null);
-                continue;
-            }
-
-            VecI previewSize = StructureHelpers.CalculatePreviewSize(tightBounds.Value.Size);
-            newPreviewBitmapSizes.Add(guid, (previewSize, tightBounds.Value));
-        }
-
-        return newPreviewBitmapSizes;
-    }
-
-    /// <summary>
-    /// Recreates the preview bitmaps using the passed sizes (or deletes them when new size is null)
-    /// </summary>
-    private void RecreatePreviewBitmaps(
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> newPreviewSizes,
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> newMaskSizes)
-    {
-        // update previews
-        foreach (var (guid, newSize) in newPreviewSizes)
-        {
-            IStructureMemberHandler member = doc.StructureHelper.FindOrThrow(guid);
-
-            if (newSize is null)
-            {
-                member.PreviewSurface?.Dispose();
-                member.PreviewSurface = null;
-            }
-            else
-            {
-                if (member.PreviewSurface is not null && member.PreviewSurface.Size.X == newSize.Value.previewSize.X &&
-                    member.PreviewSurface.Size.Y == newSize.Value.previewSize.Y)
-                {
-                    member.PreviewSurface!.DrawingSurface.Canvas.Clear();
-                }
-                else
-                {
-                    member.PreviewSurface?.Dispose();
-                    member.PreviewSurface = new Texture(newSize.Value.previewSize);
-                }
-            }
-        }
-
-        // update masks
-        foreach (var (guid, newSize) in newMaskSizes)
-        {
-            IStructureMemberHandler member = doc.StructureHelper.FindOrThrow(guid);
-
-            member.MaskPreviewSurface?.Dispose();
-            if (newSize is null)
-            {
-                member.MaskPreviewSurface = null;
-            }
-            else
-            {
-                member.MaskPreviewSurface = new Texture(newSize.Value.previewSize); // TODO: premul bgra8888 was here
-            }
-        }
-    }
-
-
-    /// <summary>
-    /// Returns the previosly known committed tight bounds if there are no reasons to believe they have changed (based on the passed <paramref name="currentlyAffectedArea"/>).
-    /// Otherwise, calculates the new bounds via <see cref="FindLayerTightBounds"/> and returns them.
-    /// </summary>
-    private RectI? GetOrFindMemberTightBounds(IReadOnlyStructureNode member, int atFrame,
-        AffectedArea currentlyAffectedArea, bool forMask)
-    {
-        if (forMask && member.EmbeddedMask is null)
-            throw new InvalidOperationException();
-
-        RectI? prevTightBounds = null;
-
-        var targetLastCollection = forMask ? lastMaskPreviewTightBounds : lastMainPreviewTightBounds;
-
-        if (targetLastCollection.TryGetValue(member.Id, out RectI tightBounds))
-            prevTightBounds = tightBounds;
-
-        if (prevTightBounds is not null && currentlyAffectedArea.GlobalArea is not null &&
-            prevTightBounds.Value.ContainsExclusive(currentlyAffectedArea.GlobalArea.Value))
-        {
-            // if the affected area is fully inside the previous tight bounds, the tight bounds couldn't possibly have changed
-            return prevTightBounds.Value;
-        }
-
-        return member switch
-        {
-            IReadOnlyLayerNode layer => FindLayerTightBounds(layer, atFrame, forMask),
-            IReadOnlyFolderNode folder => FindFolderTightBounds(folder, atFrame, forMask),
-            _ => throw new ArgumentOutOfRangeException()
-        };
-    }
-
     /// <summary>
     /// Finds the current committed tight bounds for a layer.
     /// </summary>
@@ -330,23 +79,6 @@ internal class MemberPreviewUpdater
             return FindImageTightBoundsFast(folder.EmbeddedMask);
         }
 
-        /*RectI? combinedBounds = null;
-        foreach (var child in folder.Children)
-        {
-            RectI? curBounds = null;
-
-            if (child is IReadOnlyLayerNode childLayer)
-                curBounds = FindLayerTightBounds(childLayer, frame, false);
-            else if (child is IReadOnlyFolderNode childFolder)
-                curBounds = FindFolderTightBounds(childFolder, frame, false);
-
-            if (combinedBounds is null)
-                combinedBounds = curBounds;
-            else if (curBounds is not null)
-                combinedBounds = combinedBounds.Value.Union(curBounds.Value);
-        }
-
-        return combinedBounds;*/
         return (RectI?)folder.GetTightBounds(frame);
     }
 
@@ -374,163 +106,54 @@ internal class MemberPreviewUpdater
     /// <summary>
     /// Re-renders changed chunks using <see cref="mainPreviewAreasAccumulator"/> and <see cref="maskPreviewAreasAccumulator"/> along with the passed lists of bitmaps that need full re-render.
     /// </summary>
-    private List<IRenderInfo> Render(
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> recreatedMainPreviewSizes,
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> recreatedMaskPreviewSizes)
+    /// <param name="members"></param>
+    private List<IRenderInfo> UpdatePreviewPainters(IEnumerable<Guid> members)
     {
         List<IRenderInfo> infos = new();
 
-        var (mainPreviewChunksToRerender, maskPreviewChunksToRerender) = GetChunksToRerenderAndResetAccumulator();
-
-        RenderWholeCanvasPreview(mainPreviewChunksToRerender, maskPreviewChunksToRerender, infos);
-        RenderMainPreviews(mainPreviewChunksToRerender, recreatedMainPreviewSizes, infos);
-        RenderMaskPreviews(maskPreviewChunksToRerender, recreatedMaskPreviewSizes, infos);
+        RenderWholeCanvasPreview(infos);
+        RenderMainPreviews(infos, members);
+        RenderMaskPreviews(infos);
         RenderNodePreviews(infos);
 
         return infos;
-
-        // asynchronously re-render changed chunks (where tight bounds didn't change) or the whole preview image (where the tight bounds did change)
-
-        // don't forget to get rid of the bitmap recreation code in DocumentUpdater
-    }
-
-    private (Dictionary<Guid, AffectedArea> main, Dictionary<Guid, AffectedArea> mask)
-        GetChunksToRerenderAndResetAccumulator()
-    {
-        var result = (mainPreviewPostponedChunks: mainPreviewAreasAccumulator,
-            maskPreviewPostponedChunks: maskPreviewAreasAccumulator);
-        mainPreviewAreasAccumulator = new();
-        maskPreviewAreasAccumulator = new();
-        return result;
     }
 
     /// <summary>
     /// Re-renders the preview of the whole canvas which is shown as the tab icon
     /// </summary>
-    private void RenderWholeCanvasPreview(Dictionary<Guid, AffectedArea> mainPreviewChunks,
-        Dictionary<Guid, AffectedArea> maskPreviewChunks, List<IRenderInfo> infos)
+    private void RenderWholeCanvasPreview(List<IRenderInfo> infos)
     {
-        var cumulative = mainPreviewChunks
-            .Concat(maskPreviewChunks)
-            .Aggregate(new AffectedArea(), (set, pair) =>
-            {
-                set.UnionWith(pair.Value);
-                return set;
-            });
-        if (cumulative.GlobalArea is null)
-            return;
-
         var previewSize = StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size);
         float scaling = (float)previewSize.X / doc.SizeBindable.X;
 
-        bool somethingChanged = false;
-        foreach (var chunkPos in cumulative.Chunks)
-        {
-            somethingChanged = true;
-            ChunkResolution resolution = scaling switch
-            {
-                > 1 / 2f => ChunkResolution.Full,
-                > 1 / 4f => ChunkResolution.Half,
-                > 1 / 8f => ChunkResolution.Quarter,
-                _ => ChunkResolution.Eighth,
-            };
-            var pos = chunkPos * resolution.PixelSize();
-            //var rendered = doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime);
-            doc.PreviewSurface.DrawingSurface.Canvas.Save();
-            doc.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-            doc.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
-            doc.PreviewSurface.DrawingSurface.Canvas.Scale(1 / (float)resolution.Multiplier());
-            /*if (rendered.IsT1)
-            {
-                doc.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
-                    resolution.PixelSize(), ClearPaint);
-            }
-            else if (rendered.IsT0)
-            {
-                using var renderedChunk = rendered.AsT0;
-                renderedChunk.DrawChunkOn(doc.PreviewSurface.DrawingSurface, pos, SmoothReplacingPaint);
-            }*/
-
-            doc.PreviewSurface.DrawingSurface.Canvas.Restore();
-        }
-
-        if (somethingChanged)
-            infos.Add(new CanvasPreviewDirty_RenderInfo());
+        //infos.Add(new CanvasPreviewDirty_RenderInfo());
     }
 
-    private void RenderMainPreviews(
-        Dictionary<Guid, AffectedArea> mainPreviewChunks,
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> recreatedPreviewSizes,
-        List<IRenderInfo> infos)
+    private void RenderMainPreviews(List<IRenderInfo> infos, IEnumerable<Guid> members)
     {
-        foreach (var guid in mainPreviewChunks.Select(a => a.Key).Concat(recreatedPreviewSizes.Select(a => a.Key)))
+        Guid[] memberGuids = members.ToArray();
+        foreach (var node in doc.NodeGraphHandler.AllNodes)
         {
-            // find the true affected area
-            AffectedArea? affArea = null;
-            RectI? tightBounds = null;
-
-            if (mainPreviewChunks.TryGetValue(guid, out AffectedArea areaFromChunks))
-                affArea = areaFromChunks;
-
-            if (recreatedPreviewSizes.TryGetValue(guid, out (VecI _, RectI tightBounds)? value))
+            if (node is IStructureMemberHandler structureMemberHandler)
             {
-                if (value is null)
+                if (!memberGuids.Contains(node.Id))
                     continue;
-                tightBounds = value.Value.tightBounds;
-                affArea = new AffectedArea(
-                    OperationHelper.FindChunksTouchingRectangle(value.Value.tightBounds, ChunkyImage.FullChunkSize),
-                    value.Value.tightBounds);
-            }
-
-            if (affArea is null || affArea.Value.GlobalArea is null ||
-                affArea.Value.GlobalArea.Value.IsZeroOrNegativeArea)
-                continue;
-
-            // re-render the area
-            var memberVM = doc.StructureHelper.Find(guid);
-            if (memberVM is null || memberVM.PreviewSurface is null)
-                continue;
-
-            if (tightBounds is null)
-                tightBounds = lastMainPreviewTightBounds[guid];
-
-            var member = internals.Tracker.Document.FindMemberOrThrow(guid);
-
-            var previewSize = StructureHelpers.CalculatePreviewSize(tightBounds.Value.Size);
-            float scaling = (float)previewSize.X / tightBounds.Value.Width;
-            VecI position = tightBounds.Value.Pos;
-
-            if (memberVM is ILayerHandler)
-            {
-                RenderLayerMainPreview((IReadOnlyLayerNode)member,
-                    memberVM.PreviewSurface, affArea.Value, position, scaling,
-                    doc.AnimationHandler.ActiveFrameBindable);
+                
+                if (structureMemberHandler.PreviewPainter == null)
+                {
+                    var member = internals.Tracker.Document.FindMember(node.Id);
+                    if (member is not IPreviewRenderable previewRenderable)
+                        continue;
 
-                if (doc.AnimationHandler.FindKeyFrame(guid, out IKeyFrameHandler? keyFrame))
+                    structureMemberHandler.PreviewPainter = new PreviewPainter(previewRenderable, structureMemberHandler.TightBounds);
+                    structureMemberHandler.PreviewPainter.Repaint();
+                }
+                else
                 {
-                    if (keyFrame is IKeyFrameGroupHandler group)
-                    {
-                        RenderGroupPreview(keyFrame, memberVM, member, affArea, position, scaling);
-                        foreach (var child in group.Children)
-                        {
-                            if (member is IReadOnlyImageNode rasterLayer)
-                            {
-                                RenderAnimationFramePreview(rasterLayer, child, affArea.Value);
-                            }
-                        }
-                    }
+                    structureMemberHandler.PreviewPainter.Bounds = structureMemberHandler.TightBounds;
+                    structureMemberHandler.PreviewPainter.Repaint();
                 }
-
-                infos.Add(new PreviewDirty_RenderInfo(guid));
-            }
-            else if (memberVM is IFolderHandler)
-            {
-                RenderFolderMainPreview((IReadOnlyFolderNode)member, memberVM, affArea.Value, position, scaling);
-                infos.Add(new PreviewDirty_RenderInfo(guid));
-            }
-            else
-            {
-                throw new ArgumentOutOfRangeException();
             }
         }
     }
@@ -538,19 +161,19 @@ internal class MemberPreviewUpdater
     private void RenderGroupPreview(IKeyFrameHandler keyFrame, IStructureMemberHandler memberVM,
         IReadOnlyStructureNode member, [DisallowNull] AffectedArea? affArea, VecI position, float scaling)
     {
-        bool isEditingRootImage = !member.KeyFrames.Any(x => x.IsInFrame(doc.AnimationHandler.ActiveFrameBindable));
+        /*bool isEditingRootImage = !member.KeyFrames.Any(x => x.IsInFrame(doc.AnimationHandler.ActiveFrameBindable));
         if (!isEditingRootImage && keyFrame.PreviewSurface is not null)
             return;
 
         if (keyFrame.PreviewSurface == null ||
-            keyFrame.PreviewSurface.Size != memberVM.PreviewSurface.Size)
+            keyFrame.PreviewSurface.Size != memberVM.PreviewPainter.Size)
         {
             keyFrame.PreviewSurface?.Dispose();
-            keyFrame.PreviewSurface = new Texture(memberVM.PreviewSurface.Size);
+            keyFrame.PreviewSurface = new Texture(memberVM.PreviewPainter.Size);
         }
 
         RenderLayerMainPreview((IReadOnlyLayerNode)member, keyFrame.PreviewSurface, affArea.Value,
-            position, scaling, 0);
+            position, scaling, 0);*/
     }
 
     /// <summary>
@@ -604,39 +227,6 @@ internal class MemberPreviewUpdater
         });*/
     }
 
-    /// <summary>
-    /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> layer
-    /// </summary>
-    private void RenderLayerMainPreview(IReadOnlyLayerNode layer, Texture surface, AffectedArea area,
-        VecI position, float scaling, int frame)
-    {
-        QueueRender(() =>
-        {
-            if(surface.IsDisposed)
-                return;
-            
-            surface.DrawingSurface.Canvas.Save();
-            surface.DrawingSurface.Canvas.Scale(scaling);
-            surface.DrawingSurface.Canvas.Translate(-position);
-            surface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
-
-            foreach (var chunk in area.Chunks)
-            {
-                var pos = chunk * ChunkResolution.Full.PixelSize();
-                if (layer is IPreviewRenderable renderable)
-                {
-                    if (!renderable.RenderPreview(surface, chunk, ChunkResolution.Full, frame))
-                    {
-                        surface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(),
-                            ChunkResolution.Full.PixelSize(), ClearPaint);
-                    }
-                }
-            }
-
-            surface.DrawingSurface.Canvas.Restore();
-        });
-    }
-
     private void RenderAnimationFramePreview(IReadOnlyImageNode node, IKeyFrameHandler keyFrameVM, AffectedArea area)
     {
         if (keyFrameVM.PreviewSurface is null)
@@ -645,7 +235,7 @@ internal class MemberPreviewUpdater
                 new Texture(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
         }
 
-        QueueRender(() =>
+        /*QueueRender(() =>
         {
             keyFrameVM.PreviewSurface!.DrawingSurface.Canvas.Save();
             float scaling = (float)keyFrameVM.PreviewSurface.Size.X / internals.Tracker.Document.Size.X;
@@ -662,71 +252,12 @@ internal class MemberPreviewUpdater
             }
 
             keyFrameVM.PreviewSurface!.DrawingSurface.Canvas.Restore();
-        });
+        });*/
     }
 
-    private void RenderMaskPreviews(
-        Dictionary<Guid, AffectedArea> maskPreviewChunks,
-        Dictionary<Guid, (VecI previewSize, RectI tightBounds)?> recreatedMaskSizes,
-        List<IRenderInfo> infos)
+    private void RenderMaskPreviews(List<IRenderInfo> infos)
     {
-        foreach (Guid guid in maskPreviewChunks.Select(a => a.Key).Concat(recreatedMaskSizes.Select(a => a.Key)))
-        {
-            // find the true affected area
-            AffectedArea? affArea = null;
-            RectI? tightBounds = null;
-
-            if (maskPreviewChunks.TryGetValue(guid, out AffectedArea areaFromChunks))
-                affArea = areaFromChunks;
-
-            if (recreatedMaskSizes.TryGetValue(guid, out (VecI _, RectI tightBounds)? value))
-            {
-                if (value is null)
-                    continue;
-                tightBounds = value.Value.tightBounds;
-                affArea = new AffectedArea(
-                    OperationHelper.FindChunksTouchingRectangle(value.Value.tightBounds, ChunkyImage.FullChunkSize),
-                    value.Value.tightBounds);
-            }
-
-            if (affArea is null || affArea.Value.GlobalArea is null ||
-                affArea.Value.GlobalArea.Value.IsZeroOrNegativeArea)
-                continue;
-
-            // re-render the area
-
-            var memberVM = doc.StructureHelper.Find(guid);
-            if (memberVM is null || !memberVM.HasMaskBindable || memberVM.MaskPreviewSurface is null)
-                continue;
-
-            if (tightBounds is null)
-                tightBounds = lastMaskPreviewTightBounds[guid];
-
-            var previewSize = StructureHelpers.CalculatePreviewSize(tightBounds.Value.Size);
-            float scaling = (float)previewSize.X / tightBounds.Value.Width;
-            VecI position = tightBounds.Value.Pos;
-
-            var member = internals.Tracker.Document.FindMemberOrThrow(guid);
-
-            QueueRender(() =>
-            {
-                memberVM.MaskPreviewSurface!.DrawingSurface.Canvas.Save();
-                memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-                memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Translate(-position);
-                memberVM.MaskPreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)affArea.Value.GlobalArea);
-                foreach (var chunk in affArea.Value.Chunks)
-                {
-                    var pos = chunk * ChunkResolution.Full.PixelSize();
-                    member.EmbeddedMask!.DrawMostUpToDateChunkOn
-                    (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface.DrawingSurface, pos,
-                        scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
-                }
-
-                memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Restore();
-            });
-
-            infos.Add(new MaskPreviewDirty_RenderInfo(guid));
-        }
+        //infos.Add(new MaskPreviewDirty_RenderInfo(guid));
     }
 
     private void RenderNodePreviews(List<IRenderInfo> infos)
@@ -734,13 +265,13 @@ internal class MemberPreviewUpdater
         /*using RenderingContext previewContext = new(doc.AnimationHandler.ActiveFrameTime,  VecI.Zero, ChunkResolution.Full, doc.SizeBindable);
 
         var outputNode = internals.Tracker.Document.NodeGraph.OutputNode;
-        
+
         if (outputNode is null)
             return;
 
         var executionQueue = internals.Tracker.Document.NodeGraph.AllNodes; //internals.Tracker.Document.NodeGraph.CalculateExecutionQueue(outputNode);
-        
-        foreach (var node in executionQueue) 
+
+        foreach (var node in executionQueue)
         {
             if (node is null)
                 continue;
@@ -766,7 +297,7 @@ internal class MemberPreviewUpdater
                 nodeVm.ResultPreview =
                     new Texture(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size, 150));
             }
-            
+
             float scalingX = (float)nodeVm.ResultPreview.Size.X / evaluated.Size.X;
             float scalingY = (float)nodeVm.ResultPreview.Size.Y / evaluated.Size.Y;
 
@@ -774,30 +305,18 @@ internal class MemberPreviewUpdater
             {
                 if(nodeVm.ResultPreview == null || nodeVm.ResultPreview.IsDisposed)
                     return;
-                
+
                 nodeVm.ResultPreview.DrawingSurface.Canvas.Save();
                 nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
 
                 nodeVm.ResultPreview.DrawingSurface.Canvas.DrawSurface(evaluated.DrawingSurface, 0, 0, ReplacingPaint);
 
                 nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
-                
+
                 evaluated.Dispose();
             });
 
             infos.Add(new NodePreviewDirty_RenderInfo(node.Id));
         }*/
     }
-
-    private void QueueRender(Action action)
-    {
-        if (!DrawingBackendApi.Current.IsHardwareAccelerated)
-        {
-            action();
-        }
-        else
-        {
-            Dispatcher.UIThread.Post(action, DispatcherPriority.Render);
-        }
-    }
 }

+ 35 - 0
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -0,0 +1,35 @@
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.Models.Rendering;
+
+public class PreviewPainter
+{
+    public RectD? Bounds { get; set; }
+    public IPreviewRenderable PreviewRenderable { get; set; }
+    public event Action RequestRepaint;
+    
+    public PreviewPainter(IPreviewRenderable previewRenderable, RectD? tightBounds)
+    {
+        PreviewRenderable = previewRenderable;
+        Bounds = tightBounds;
+    }
+
+    public void Paint(DrawingSurface renderOn, ChunkResolution resolution, KeyFrameTime frame) 
+    {
+        if (PreviewRenderable == null || Bounds == null)
+        {
+            return;
+        }
+
+        PreviewRenderable.RenderPreview(renderOn, resolution, frame.Frame);
+    }
+
+    public void Repaint()
+    {
+        RequestRepaint?.Invoke();
+    }
+}

+ 5 - 4
src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs

@@ -7,6 +7,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Layers;
+using PixiEditor.Models.Rendering;
 using PixiEditor.Numerics;
 using PixiEditor.ViewModels.Nodes;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
@@ -143,16 +144,16 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
         set => SetProperty(ref selection, value);
     }
 
-    private Texture? previewSurface;
-    private Texture? maskPreviewSurface;
+    private PreviewPainter? previewSurface;
+    private PreviewPainter? maskPreviewSurface;
 
-    public Texture? PreviewSurface
+    public PreviewPainter? PreviewPainter
     {
         get => previewSurface;
         set => SetProperty(ref previewSurface, value);
     }
 
-    public Texture? MaskPreviewSurface
+    public PreviewPainter? MaskPreviewSurface
     {
         get => maskPreviewSurface;
         set => SetProperty(ref maskPreviewSurface, value);

+ 6 - 4
src/PixiEditor/Views/Layers/FolderControl.axaml

@@ -65,15 +65,17 @@
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
-                            <visuals:TextureControl Texture="{Binding Folder.PreviewSurface, ElementName=folderControl}" 
-                                                    Stretch="Uniform" Width="30" Height="30">
+                            <visuals:PreviewPainterControl 
+                                PreviewPainter="{Binding Folder.PreviewPainter, ElementName=folderControl}"
+                                FrameToRender="{Binding Path=Folder.Document.AnimationDataViewModel.ActiveFrameBindable, ElementName=folderControl}"
+                                Width="30" Height="30">
                                 <ui:RenderOptionsBindable.BitmapInterpolationMode>
                                     <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
-                                        <Binding Path="Folder.PreviewSurface.Size.X" ElementName="folderControl"/>
+                                        <Binding Path="Folder.PreviewPainter.Bounds.Size.X" ElementName="folderControl"/>
                                         <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width"/>
                                     </MultiBinding>
                                 </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                            </visuals:TextureControl>
+                            </visuals:PreviewPainterControl>
                         </Border>
                         <Border 
                             Width="32" Height="32" 

+ 6 - 4
src/PixiEditor/Views/Layers/LayerControl.axaml

@@ -79,10 +79,12 @@
                                 <Binding ElementName="uc" Path="Layer.HasMaskBindable" />
                             </MultiBinding>
                         </Border.BorderBrush>
-                        <visuals:TextureControl Texture="{Binding Layer.PreviewSurface, ElementName=uc}"
-                                                Stretch="Uniform" Width="30"
-                                                Height="30"
-                                                RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False" />
+                        <visuals:PreviewPainterControl 
+                            ClipToBounds="True" 
+                            PreviewPainter="{Binding Layer.PreviewPainter, ElementName=uc}"
+                            FrameToRender="{Binding Layer.Document.AnimationHandler.ActiveFrameBindable, ElementName=uc}"
+                            Width="30" Height="30"
+                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False" />
                     </Border>
                     <Border
                         Width="32" Height="32"

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

@@ -124,9 +124,6 @@
                            VerticalAlignment="Top"
                            ToolSet="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet}" 
                            SwitchToolSetCommand="{xaml:Command Name=PixiEditor.Tools.SwitchToolSet, UseProvided=True}"/>
-        <!--
-        <rendering:UniversalScene Name="scene" ZIndex="1" SceneRenderer="{Binding Source={viewModels:MainVM DocumentManagerSVM}, Path=ActiveDocument.SceneRenderer}"/>
-        -->
         <rendering:Scene
             Focusable="False" Name="scene"
             ZIndex="1"

+ 109 - 0
src/PixiEditor/Views/Visuals/PreviewPainterControl.cs

@@ -0,0 +1,109 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.Models.Rendering;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.Views.Visuals;
+
+public class PreviewPainterControl : Control
+{
+    public static readonly StyledProperty<int> FrameToRenderProperty = AvaloniaProperty.Register<PreviewPainterControl, int>("FrameToRender");
+
+    public static readonly StyledProperty<PreviewPainter> PreviewPainterProperty =
+        AvaloniaProperty.Register<PreviewPainterControl, PreviewPainter>(
+            nameof(PreviewPainter));
+
+    public PreviewPainter PreviewPainter
+    {
+        get => GetValue(PreviewPainterProperty);
+        set => SetValue(PreviewPainterProperty, value);
+    }
+
+    public int FrameToRender
+    {
+        get { return (int)GetValue(FrameToRenderProperty); }
+        set { SetValue(FrameToRenderProperty, value); }
+    }
+
+    public PreviewPainterControl()
+    {
+        PreviewPainterProperty.Changed.Subscribe(PainterChanged);
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        if (PreviewPainter == null)
+        {
+            return;
+        }
+
+        using var renderOperation = new DrawPreviewOperation(Bounds, PreviewPainter, FrameToRender);
+        context.Custom(renderOperation);
+    }
+    
+    private void PainterChanged(AvaloniaPropertyChangedEventArgs<PreviewPainter> args)
+    {
+        if (args.OldValue.Value != null)
+        {
+            args.OldValue.Value.RequestRepaint -= OnPainterRenderRequest;
+        }
+        if (args.NewValue.Value != null)
+        {
+            args.NewValue.Value.RequestRepaint += OnPainterRenderRequest;
+        }
+    }
+
+    private void OnPainterRenderRequest()
+    {
+        InvalidateVisual();
+    }
+}
+
+internal class DrawPreviewOperation : SkiaDrawOperation
+{
+    public PreviewPainter PreviewPainter { get; }
+    private RectD bounds;
+    private int frame;
+
+    public DrawPreviewOperation(Rect dirtyBounds, PreviewPainter previewPainter, int frameToRender) : base(dirtyBounds)
+    {
+        PreviewPainter = previewPainter;
+        bounds = new RectD(dirtyBounds.X, dirtyBounds.Y, dirtyBounds.Width, dirtyBounds.Height);
+        frame = frameToRender;
+    }
+
+    public override void Render(ISkiaSharpApiLease lease)
+    {
+        if (PreviewPainter == null || PreviewPainter.Bounds == null)
+        {
+            return;
+        }
+        
+        DrawingSurface target = DrawingSurface.FromNative(lease.SkSurface);
+        
+        float scaleX = (float)(bounds.Width / PreviewPainter.Bounds.Value.Width);
+        float scaleY = (float)(bounds.Height / PreviewPainter.Bounds.Value.Width);
+
+        target.Canvas.Save();
+        
+        target.Canvas.Scale(scaleX, scaleY);
+        target.Canvas.Translate((float)-PreviewPainter.Bounds.Value.X, (float)-PreviewPainter.Bounds.Value.Y);
+        
+        // TODO: Implement ChunkResolution and frame
+        PreviewPainter.Paint(target, ChunkResolution.Full, frame);
+        
+        target.Canvas.Restore();
+        
+        DrawingSurface.Unmanage(target);
+    }
+
+    public override bool Equals(ICustomDrawOperation? other)
+    {
+        return other is DrawPreviewOperation operation && operation.PreviewPainter == PreviewPainter;
+    }
+}