Browse Source

Made scene work with gpu

flabbet 11 months ago
parent
commit
fcdaeac067

+ 3 - 2
src/PixiEditor.DrawingApi.Core/Texture.cs

@@ -13,6 +13,7 @@ public class Texture : IDisposable
     public VecI Size { get; }
     public DrawingSurface Surface { get; }
 
+    
     public bool IsDisposed { get; private set; }
 
     public Texture(VecI size)
@@ -24,9 +25,9 @@ public class Texture : IDisposable
                 {
                     GpuBacked = true
                 });
+        
     }
 
-
     public static Texture Load(string path)
     {
         if (!File.Exists(path))
@@ -96,7 +97,7 @@ public class Texture : IDisposable
     {
         if (IsDisposed)
             return;
-        
+
         IsDisposed = true;
         Surface.Dispose();
     }

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaSurfaceImplementation.cs

@@ -152,7 +152,7 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 
         public void Flush(DrawingSurface drawingSurface)
         {
-            ManagedInstances[drawingSurface.ObjectPointer].Flush(true, true);
+            ManagedInstances[drawingSurface.ObjectPointer].Flush(true, false);
         }
     }
 }

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

@@ -139,7 +139,7 @@ internal class ActionAccumulator
                         RectI finalRect = new RectI(VecI.Zero, new(bitmap.Size.X, bitmap.Size.Y));
 
                         RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
-                        bitmap.AddDirtyRect(dirtyRect);
+                        // bitmap.AddDirtyRect(dirtyRect);
                     }
                     break;
                 case PreviewDirty_RenderInfo info:
@@ -161,7 +161,7 @@ internal class ActionAccumulator
                     break;
                 case CanvasPreviewDirty_RenderInfo:
                     {
-                        document.PreviewSurface.AddDirtyRect(new RectI(0, 0, document.PreviewSurface.Size.X, document.PreviewSurface.Size.Y));
+                        //document.PreviewSurface.AddDirtyRect(new RectI(0, 0, document.PreviewSurface.Size.X, document.PreviewSurface.Size.Y));
                     }
                     break;
                 case NodePreviewDirty_RenderInfo info:

+ 3 - 3
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -345,11 +345,11 @@ internal class DocumentUpdater
     {
         VecI oldSize = doc.SizeBindable;
 
-        foreach ((ChunkResolution res, Surface surf) in doc.Surfaces)
+        foreach ((ChunkResolution res, Texture surf) in doc.Surfaces)
         {
             surf.Dispose();
             VecI size = (VecI)(info.Size * res.Multiplier());
-            doc.Surfaces[res] = new Surface(new VecI(Math.Max(size.X, 1), Math.Max(size.Y, 1))); //TODO: Bgra8888 was here
+            doc.Surfaces[res] = new Texture(new VecI(Math.Max(size.X, 1), Math.Max(size.Y, 1))); //TODO: Bgra8888 was here
         }
 
         doc.SetSize(info.Size);
@@ -358,7 +358,7 @@ internal class DocumentUpdater
 
         VecI documentPreviewSize = StructureHelpers.CalculatePreviewSize(info.Size);
         doc.PreviewSurface.Dispose();
-        doc.PreviewSurface = new Surface(documentPreviewSize); //TODO: Bgra8888 was here
+        doc.PreviewSurface = new Texture(documentPreviewSize); //TODO: Bgra8888 was here
 
         // TODO: Make sure property changed events are raised internally
         // UPDATE: I think I did, but I'll leave it commented out for now

+ 2 - 2
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -25,9 +25,9 @@ internal interface IDocument : IHandler
     public IAnimationHandler AnimationHandler { get; }
     public VectorPath SelectionPathBindable { get; }
     public INodeGraphHandler NodeGraphHandler { get; }
-    public Dictionary<ChunkResolution, Surface> Surfaces { get; set; }
+    public Dictionary<ChunkResolution, Texture> Surfaces { get; set; }
     public DocumentStructureModule StructureHelper { get; }
-    public Surface PreviewSurface { get; set; }
+    public Texture PreviewSurface { get; set; }
     public bool AllChangesSaved { get; }
     public string CoordinatesString { get; set; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }

+ 39 - 20
src/PixiEditor/Models/Rendering/CanvasUpdater.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Threading.Tasks;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
@@ -196,7 +197,7 @@ internal class CanvasUpdater
                 globalScaledClippingRectangle =
                     (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
 
-            Surface screenSurface = doc.Surfaces[resolution];
+            Texture screenSurface = doc.Surfaces[resolution];
             foreach (var chunkPos in chunks)
             {
                 RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle,
@@ -214,38 +215,56 @@ internal class CanvasUpdater
         }
     }
 
-    private void RenderChunk(VecI chunkPos, Surface screenSurface, ChunkResolution resolution,
+    private void RenderChunk(VecI chunkPos, Texture screenSurface, ChunkResolution resolution,
         RectI? globalClippingRectangle, RectI? globalScaledClippingRectangle)
     {
         if (screenSurface is null || screenSurface.IsDisposed)
             return;
 
-        if (globalScaledClippingRectangle is not null)
-        {
-            screenSurface.DrawingSurface.Canvas.Save();
-            screenSurface.DrawingSurface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
-        }
 
         doc.Renderer.RenderChunk(chunkPos, resolution, doc.AnimationHandler.ActiveFrameTime, globalClippingRectangle)
             .Switch(
                 (Chunk chunk) =>
                 {
-                    if (screenSurface.IsDisposed) return;
-                    
-                    screenSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface,
-                        chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
-                    chunk.Dispose();
+                    Dispatcher.UIThread.Post(() =>
+                    {
+                        if (screenSurface.IsDisposed) return;
+
+                        if (globalScaledClippingRectangle is not null)
+                        {
+                            screenSurface.Surface.Canvas.Save();
+                            screenSurface.Surface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
+                        }
+
+                        screenSurface.Surface.Canvas.DrawSurface(
+                            chunk.Surface.DrawingSurface,
+                            chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
+                        chunk.Dispose();
+
+
+                        if (globalScaledClippingRectangle is not null)
+                            screenSurface.Surface.Canvas.Restore();
+                    });
                 },
                 (EmptyChunk _) =>
                 {
-                    if (screenSurface.IsDisposed) return;
-                    
-                    var pos = chunkPos * resolution.PixelSize();
-                    screenSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
-                        resolution.PixelSize(), ClearPaint);
-                });
+                    Dispatcher.UIThread.Post(() =>
+                    {
+                        if (screenSurface.IsDisposed) return;
 
-        if (globalScaledClippingRectangle is not null)
-            screenSurface.DrawingSurface.Canvas.Restore();
+                        if (globalScaledClippingRectangle is not null)
+                        {
+                            screenSurface.Surface.Canvas.Save();
+                            screenSurface.Surface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
+                        }
+
+                        var pos = chunkPos * resolution.PixelSize();
+                        screenSurface.Surface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
+                            resolution.PixelSize(), ClearPaint);
+                        
+                        if (globalScaledClippingRectangle is not null)
+                            screenSurface.Surface.Canvas.Restore();
+                    });
+                });
     }
 }

+ 7 - 7
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -436,22 +436,22 @@ internal class MemberPreviewUpdater
             };
             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());
+            doc.PreviewSurface.Surface.Canvas.Save();
+            doc.PreviewSurface.Surface.Canvas.Scale(scaling);
+            doc.PreviewSurface.Surface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
+            doc.PreviewSurface.Surface.Canvas.Scale(1 / (float)resolution.Multiplier());
             if (rendered.IsT1)
             {
-                doc.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(),
+                doc.PreviewSurface.Surface.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);
+                renderedChunk.DrawChunkOn(doc.PreviewSurface.Surface, pos, SmoothReplacingPaint);
             }
 
-            doc.PreviewSurface.DrawingSurface.Canvas.Restore();
+            doc.PreviewSurface.Surface.Canvas.Restore();
         }
 
         if (somethingChanged)

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

@@ -168,17 +168,17 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public IStructureMemberHandler? SelectedStructureMember { get; private set; } = null;
 
     //TODO: It was DrawingSurface before, check if it's correct
-    public Dictionary<ChunkResolution, Surface> Surfaces { get; set; } = new()
+    public Dictionary<ChunkResolution, Texture> Surfaces { get; set; } = new()
     {
-        [ChunkResolution.Full] = new Surface(new VecI(64, 64)),
-        [ChunkResolution.Half] = new Surface(new VecI(32, 32)),
-        [ChunkResolution.Quarter] = new Surface(new VecI(16, 16)),
-        [ChunkResolution.Eighth] = new Surface(new VecI(8, 8))
+        [ChunkResolution.Full] = new Texture(new VecI(64, 64)),
+        [ChunkResolution.Half] = new Texture(new VecI(32, 32)),
+        [ChunkResolution.Quarter] = new Texture(new VecI(16, 16)),
+        [ChunkResolution.Eighth] = new Texture(new VecI(8, 8))
     };
 
-    private Surface previewSurface;
+    private Texture previewSurface;
 
-    public Surface PreviewSurface
+    public Texture PreviewSurface
     {
         get => previewSurface;
         set
@@ -237,7 +237,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
 
         VecI previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
-        PreviewSurface = new Surface(new VecI(previewSize.X, previewSize.Y));
+        PreviewSurface = new Texture(new VecI(previewSize.X, previewSize.Y));
 
         ReferenceLayerViewModel = new(this, Internals);
 

+ 3 - 3
src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs

@@ -80,7 +80,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         Document = document;
         Document.SizeChanged += DocumentOnSizeChanged;
         Document.PropertyChanged += DocumentOnPropertyChanged;
-        TabCustomizationSettings.Icon = new SurfaceImage(Document.PreviewSurface);
+        TabCustomizationSettings.Icon = new TextureImage(Document.PreviewSurface);
     }
 
     private void DocumentOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -91,7 +91,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
         }
         else if (e.PropertyName == nameof(DocumentViewModel.PreviewSurface))
         {
-            TabCustomizationSettings.Icon = new SurfaceImage(Document.PreviewSurface);
+            TabCustomizationSettings.Icon = new TextureImage(Document.PreviewSurface);
         }
         else if (e.PropertyName == nameof(DocumentViewModel.AllChangesSaved))
         {
@@ -107,7 +107,7 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
 
     private void DocumentOnSizeChanged(object? sender, DocumentSizeChangedEventArgs e)
     {
-        TabCustomizationSettings.Icon = new SurfaceImage(Document.PreviewSurface);
+        TabCustomizationSettings.Icon = new TextureImage(Document.PreviewSurface);
         OnPropertyChanged(nameof(TabCustomizationSettings));
     }
 

+ 3 - 3
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml

@@ -14,10 +14,10 @@
              VerticalAlignment="Center"
              d:DesignHeight="450" d:DesignWidth="800">
 
-    <visuals:SurfaceControl
+    <visuals:TextureControl
         x:Name="mainImage"
         Focusable="True"
-        Surface="{Binding TargetBitmap, ElementName=uc}"
+        Texture="{Binding TargetBitmap, ElementName=uc}"
         Stretch="Uniform"
         SizeChanged="OnImageSizeChanged">
         <ui1:RenderOptionsBindable.BitmapInterpolationMode>
@@ -26,5 +26,5 @@
                 <Binding ElementName="mainImage" Path="Bounds.Width"/>
             </MultiBinding>
         </ui1:RenderOptionsBindable.BitmapInterpolationMode>
-    </visuals:SurfaceControl>
+    </visuals:TextureControl>
 </UserControl>

+ 9 - 11
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs

@@ -1,14 +1,12 @@
-using System.Collections.Generic;
-using System.ComponentModel;
+using System.ComponentModel;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Interactivity;
-using Avalonia.Media.Imaging;
-using ChunkyImageLib;
+using Avalonia.Media;
+using Avalonia.Threading;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Position;
 using PixiEditor.Numerics;
@@ -21,8 +19,8 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
         AvaloniaProperty.Register<FixedViewport, DocumentViewModel>(nameof(Document), null);
 
-    private static readonly StyledProperty<Dictionary<ChunkResolution, WriteableBitmap>> BitmapsProperty =
-        AvaloniaProperty.Register<FixedViewport, Dictionary<ChunkResolution, WriteableBitmap>>(nameof(Bitmaps), null);
+    private static readonly StyledProperty<Dictionary<ChunkResolution, Texture>> BitmapsProperty =
+        AvaloniaProperty.Register<FixedViewport, Dictionary<ChunkResolution, Texture>>(nameof(Bitmaps), null);
 
     public static readonly StyledProperty<bool> DelayedProperty =
         AvaloniaProperty.Register<FixedViewport, bool>(nameof(Delayed), false);
@@ -35,7 +33,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DelayedProperty, value);
     }
 
-    public Dictionary<ChunkResolution, WriteableBitmap>? Bitmaps
+    public Dictionary<ChunkResolution, Texture>? Bitmaps
     {
         get => GetValue(BitmapsProperty);
         set => SetValue(BitmapsProperty, value);
@@ -47,11 +45,11 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         set => SetValue(DocumentProperty, value);
     }
 
-    public Surface? TargetBitmap
+    public Texture? TargetBitmap
     {
         get
         {
-            if (Document?.Surfaces.TryGetValue(CalculateResolution(), out Surface? value) == true)
+            if (Document?.Surfaces.TryGetValue(CalculateResolution(), out Texture? value) == true)
                 return value;
             return null;
         }
@@ -146,7 +144,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         Document?.Operations.AddOrUpdateViewport(GetLocation());
     }
 
-    private static void OnBitmapsChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, WriteableBitmap>> args)
+    private static void OnBitmapsChange(AvaloniaPropertyChangedEventArgs<Dictionary<ChunkResolution, Texture>> args)
     {
         FixedViewport? viewport = (FixedViewport)args.Sender;
         viewport.PropertyChanged?.Invoke(viewport, new(nameof(TargetBitmap)));

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

@@ -267,11 +267,11 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         }
     }
 
-    public Surface? TargetBitmap
+    public Texture? TargetBitmap
     {
         get
         {
-            return Document?.Surfaces.TryGetValue(CalculateResolution(), out Surface? value) == true ? value : null;
+            return Document?.Surfaces.TryGetValue(CalculateResolution(), out Texture? value) == true ? value : null;
         }
     }
 

+ 27 - 10
src/PixiEditor/Views/Rendering/Scene.cs

@@ -13,8 +13,10 @@ using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.DrawingApi.Skia.Extensions;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Converters;
@@ -32,7 +34,7 @@ namespace PixiEditor.Views.Rendering;
 
 internal class Scene : Zoombox.Zoombox, ICustomHitTest
 {
-    public static readonly StyledProperty<Surface> SurfaceProperty = AvaloniaProperty.Register<SurfaceControl, Surface>(
+    public static readonly StyledProperty<Texture> SurfaceProperty = AvaloniaProperty.Register<SurfaceControl, Texture>(
         nameof(Surface));
 
     public static readonly StyledProperty<DocumentViewModel> DocumentProperty =
@@ -86,7 +88,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         set => SetValue(DocumentProperty, value);
     }
 
-    public Surface Surface
+    public Texture Surface
     {
         get => GetValue(SurfaceProperty);
         set => SetValue(SurfaceProperty, value);
@@ -145,6 +147,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         RectD dirtyBounds = new RectD(0, 0, Document.Width / resolutionScale, Document.Height / resolutionScale);
         Rect dirtyRect = new Rect(0, 0, Document.Width / resolutionScale, Document.Height / resolutionScale);
 
+        Surface.Surface.Flush();
         using var operation = new DrawSceneOperation(Surface, Document, CanvasPos, Scale * resolutionScale, angle,
             FlipX, FlipY,
             dirtyRect,
@@ -185,9 +188,9 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
                 }
 
                 overlay.ZoomScale = Scale;
-                
-                if(!overlay.CanRender()) continue;
-                
+
+                if (!overlay.CanRender()) continue;
+
                 overlay.RenderOverlay(context, dirtyBounds);
                 Cursor = overlay.Cursor ?? DefaultCursor;
             }
@@ -453,7 +456,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     private static void SurfaceChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
     {
-        if (e.NewValue is Surface surface)
+        if (e.NewValue is Texture surface)
         {
             scene.ContentDimensions = surface.Size;
         }
@@ -475,7 +478,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
 internal class DrawSceneOperation : SkiaDrawOperation
 {
-    public Surface Surface { get; set; }
+    public Texture Surface { get; set; }
     public DocumentViewModel Document { get; set; }
     public VecD ContentPosition { get; set; }
     public double Scale { get; set; }
@@ -489,7 +492,9 @@ internal class DrawSceneOperation : SkiaDrawOperation
 
     private SKPaint _paint = new SKPaint();
 
-    public DrawSceneOperation(Surface surface, DocumentViewModel document, VecD contentPosition, double scale,
+    private bool hardwareAccelerationAvailable = DrawingBackendApi.Current.IsHardwareAccelerated;
+
+    public DrawSceneOperation(Texture surface, DocumentViewModel document, VecD contentPosition, double scale,
         double angle, bool flipX, bool flipY, Rect dirtyBounds, Rect viewportBounds, double opacity,
         ColorMatrix colorMatrix) : base(dirtyBounds)
     {
@@ -520,13 +525,25 @@ internal class DrawSceneOperation : SkiaDrawOperation
             return;
         }
 
-        using Image snapshot = Surface.DrawingSurface.Snapshot(SurfaceRectToRender);
+        using var ctx = DrawingBackendApi.Current.RenderOnDifferentGrContext(lease.GrContext);
+
 
         var matrixValues = new float[ColorMatrix.Width * ColorMatrix.Height];
         ColorMatrix.TryGetMembers(matrixValues);
 
         _paint.ColorFilter = SKColorFilter.CreateColorMatrix(matrixValues);
-        canvas.DrawImage((SKImage)snapshot.Native, SurfaceRectToRender.X, SurfaceRectToRender.Y, _paint);
+
+        if (!hardwareAccelerationAvailable)
+        {
+            // snapshotting wanted region on CPU is faster than rendering whole surface on CPU,
+            // but slower than rendering whole surface on GPU
+            using Image snapshot = Surface.Surface.Snapshot(SurfaceRectToRender);
+            canvas.DrawImage((SKImage)snapshot.Native, SurfaceRectToRender.X, SurfaceRectToRender.Y, _paint);
+        }
+        else
+        {
+            canvas.DrawSurface(Surface.Surface.Native as SKSurface, 0, 0, _paint);
+        }
 
         canvas.Restore();
     }

+ 1 - 2
src/PixiEditor/Views/Visuals/TextureControl.cs

@@ -108,9 +108,8 @@ internal class DrawTextureOperation : SkiaDrawOperation
     public VecD TargetSize { get; }
     public Texture? Texture { get; }
     public Paint? Paint { get; }
-    
 
-    public DrawTextureOperation(Rect dirtyBounds, Stretch stretch, Texture? texture, Paint paint = null) :
+    public DrawTextureOperation(Rect dirtyBounds, Stretch stretch, Texture texture, Paint paint = null) :
         base(dirtyBounds)
     {
         Stretch = stretch;

+ 25 - 0
src/PixiEditor/Views/Visuals/TextureImage.cs

@@ -0,0 +1,25 @@
+using Avalonia;
+using Avalonia.Media;
+using ChunkyImageLib;
+using PixiEditor.DrawingApi.Core;
+
+namespace PixiEditor.Views.Visuals;
+
+public class TextureImage : IImage
+{
+    public Texture Texture { get; set; }
+    public Stretch Stretch { get; set; } = Stretch.Uniform;
+
+    public Size Size { get; }
+
+    public TextureImage(Texture texture)
+    {
+        Texture = texture;
+        Size = new Size(texture.Size.X, texture.Size.Y);
+    }
+
+    public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
+    {
+        context.Custom(new DrawTextureOperation(destRect, Stretch, Texture));
+    }
+}