Browse Source

Multiple viewports infrastructure

Equbuxu 3 years ago
parent
commit
217c6feb2c

+ 5 - 31
src/ChunkyImageLib/DataHolders/ChunkResolution.cs

@@ -1,37 +1,11 @@
 namespace ChunkyImageLib.DataHolders
 {
+    [Flags]
     public enum ChunkResolution
     {
-        Full,
-        Half,
-        Quarter,
-        Eighth
-    }
-
-    public static class ChunkResolutionEx
-    {
-        public static double Multiplier(this ChunkResolution resolution)
-        {
-            return resolution switch
-            {
-                ChunkResolution.Full => 1.0,
-                ChunkResolution.Half => 1.0 / 2,
-                ChunkResolution.Quarter => 1.0 / 4,
-                ChunkResolution.Eighth => 1.0 / 8,
-                _ => 1,
-            };
-        }
-
-        public static int PixelSize(this ChunkResolution resolution)
-        {
-            return resolution switch
-            {
-                ChunkResolution.Full => ChunkPool.FullChunkSize,
-                ChunkResolution.Half => ChunkPool.FullChunkSize / 2,
-                ChunkResolution.Quarter => ChunkPool.FullChunkSize / 4,
-                ChunkResolution.Eighth => ChunkPool.FullChunkSize / 8,
-                _ => ChunkPool.FullChunkSize
-            };
-        }
+        Full = 1,
+        Half = 2,
+        Quarter = 4,
+        Eighth = 8
     }
 }

+ 29 - 0
src/ChunkyImageLib/DataHolders/ChunkResolutionEx.cs

@@ -0,0 +1,29 @@
+namespace ChunkyImageLib.DataHolders
+{
+    public static class ChunkResolutionEx
+    {
+        public static double Multiplier(this ChunkResolution resolution)
+        {
+            return resolution switch
+            {
+                ChunkResolution.Full => 1.0,
+                ChunkResolution.Half => 1.0 / 2,
+                ChunkResolution.Quarter => 1.0 / 4,
+                ChunkResolution.Eighth => 1.0 / 8,
+                _ => 1,
+            };
+        }
+
+        public static int PixelSize(this ChunkResolution resolution)
+        {
+            return resolution switch
+            {
+                ChunkResolution.Full => ChunkPool.FullChunkSize,
+                ChunkResolution.Half => ChunkPool.FullChunkSize / 2,
+                ChunkResolution.Quarter => ChunkPool.FullChunkSize / 4,
+                ChunkResolution.Eighth => ChunkPool.FullChunkSize / 8,
+                _ => ChunkPool.FullChunkSize
+            };
+        }
+    }
+}

+ 14 - 3
src/PixiEditor.Zoombox/Zoombox.xaml.cs

@@ -1,10 +1,10 @@
-using ChunkyImageLib.DataHolders;
-using System;
+using System;
 using System.ComponentModel;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Markup;
+using ChunkyImageLib.DataHolders;
 
 namespace PixiEditor.Zoombox
 {
@@ -36,6 +36,9 @@ namespace PixiEditor.Zoombox
         public static readonly DependencyProperty DimensionsProperty =
             DependencyProperty.Register(nameof(Dimensions), typeof(Vector2d), typeof(Zoombox));
 
+        public static readonly DependencyProperty RealDimensionsProperty =
+            DependencyProperty.Register(nameof(RealDimensions), typeof(Vector2d), typeof(Zoombox));
+
         public static readonly DependencyProperty AngleProperty =
             DependencyProperty.Register(nameof(Angle), typeof(double), typeof(Zoombox), new(0.0, OnPropertyChange));
 
@@ -107,6 +110,12 @@ namespace PixiEditor.Zoombox
             set => SetValue(DimensionsProperty, value);
         }
 
+        public Vector2d RealDimensions
+        {
+            get => (Vector2d)GetValue(RealDimensionsProperty);
+            set => SetValue(RealDimensionsProperty, value);
+        }
+
         public event EventHandler<ViewportRoutedEventArgs> ViewportMoved
         {
             add => AddHandler(ViewportMovedEvent, value);
@@ -181,11 +190,13 @@ namespace PixiEditor.Zoombox
 
         private void RaiseViewportEvent()
         {
+            var realDim = new Vector2d(mainCanvas.ActualWidth, mainCanvas.ActualHeight);
+            RealDimensions = realDim;
             RaiseEvent(new ViewportRoutedEventArgs(
                 ViewportMovedEvent,
                 Center,
                 Dimensions,
-                new(mainCanvas.ActualWidth, mainCanvas.ActualHeight),
+                realDim,
                 Angle));
         }
 

+ 23 - 37
src/PixiEditorPrototype/Models/ActionAccumulator.cs

@@ -1,4 +1,5 @@
-using ChunkyImageLib.DataHolders;
+using System.Collections.Generic;
+using System.Linq;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos;
@@ -6,10 +7,6 @@ using PixiEditorPrototype.Models.Rendering;
 using PixiEditorPrototype.Models.Rendering.RenderInfos;
 using PixiEditorPrototype.ViewModels;
 using SkiaSharp;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Windows.Media.Imaging;
 
 namespace PixiEditorPrototype.Models
 {
@@ -28,7 +25,7 @@ namespace PixiEditorPrototype.Models
             this.document = doc;
             this.helpers = helpers;
 
-            renderer = new(helpers);
+            renderer = new(doc, helpers);
         }
 
         public void AddFinishedActions(params IAction[] actions)
@@ -64,16 +61,19 @@ namespace PixiEditorPrototype.Models
                     helpers.Updater.ApplyChangeFromChangeInfo(info);
                 }
 
-                var (bitmap, surface) = GetCorrespondingBitmap(document.RenderResolution);
-                bitmap.Lock();
+                foreach (var (_, bitmap) in document.Bitmaps)
+                {
+                    bitmap.Lock();
+                }
 
-                var renderResult = await renderer.ProcessChanges(
-                    result!,
-                    surface,
-                    document.RenderResolution);
-                AddDirtyRects(bitmap, renderResult);
+                var renderResult = await renderer.ProcessChanges(result);
+                AddDirtyRects(renderResult);
+
+                foreach (var (_, bitmap) in document.Bitmaps)
+                {
+                    bitmap.Unlock();
+                }
 
-                bitmap.Unlock();
                 document.ForceRefreshView();
             }
 
@@ -90,32 +90,18 @@ namespace PixiEditorPrototype.Models
             return true;
         }
 
-        private (WriteableBitmap, SKSurface) GetCorrespondingBitmap(ChunkResolution res)
+        private void AddDirtyRects(List<IRenderInfo> changes)
         {
-            var result = res switch
-            {
-                ChunkResolution.Full => (document.BitmapFull, document.SurfaceFull),
-                ChunkResolution.Half => (document.BitmapHalf, document.SurfaceHalf),
-                ChunkResolution.Quarter => (document.BitmapQuarter, document.SurfaceQuarter),
-                ChunkResolution.Eighth => (document.BitmapEighth, document.SurfaceEighth),
-                _ => (document.BitmapFull, document.SurfaceFull),
-            };
-            if (result.Item1 is null || result.Item2 is null)
-                throw new InvalidOperationException("Trying to get a bitmap of a non existing resolution");
-            return result!;
-        }
-
-        private static void AddDirtyRects(WriteableBitmap bitmap, List<IRenderInfo> changes)
-        {
-            SKRectI finalRect = SKRectI.Create(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
             foreach (IRenderInfo info in changes)
             {
-                if (info is DirtyRect_RenderInfo dirtyRectInfo)
-                {
-                    SKRectI dirtyRect = SKRectI.Create(dirtyRectInfo.Pos, dirtyRectInfo.Size);
-                    dirtyRect.Intersect(finalRect);
-                    bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
-                }
+                if (info is not DirtyRect_RenderInfo dirtyRectInfo)
+                    continue;
+                var bitmap = document.Bitmaps[dirtyRectInfo.Resolution];
+                SKRectI finalRect = SKRectI.Create(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
+
+                SKRectI dirtyRect = SKRectI.Create(dirtyRectInfo.Pos, dirtyRectInfo.Size);
+                dirtyRect.Intersect(finalRect);
+                bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
             }
         }
     }

+ 3 - 5
src/PixiEditorPrototype/Models/DocumentState.cs

@@ -1,12 +1,10 @@
-using ChunkyImageLib.DataHolders;
+using System;
+using System.Collections.Generic;
 
 namespace PixiEditorPrototype.Models
 {
     internal class DocumentState
     {
-        public Vector2d ViewportCenter { get; set; } = new(32, 32);
-        public Vector2d ViewportSize { get; set; } = new(64, 64);
-        public Vector2d ViewportRealSize { get; set; } = new(double.MaxValue, double.MaxValue);
-        public double ViewportAngle { get; set; } = 0;
+        public Dictionary<Guid, ViewportLocation> Viewports { get; set; } = new();
     }
 }

+ 19 - 52
src/PixiEditorPrototype/Models/DocumentUpdater.cs

@@ -47,8 +47,8 @@ namespace PixiEditorPrototype.Models
                 case Size_ChangeInfo info:
                     ProcessSize(info);
                     break;
-                case MoveViewport_PassthroughAction info:
-                    ProcessMoveViewport(info);
+                case RefreshViewport_PassthroughAction info:
+                    ProcessRefreshViewport(info);
                     break;
                 case StructureMemberMask_ChangeInfo info:
                     ProcessStructureMemberMask(info);
@@ -71,68 +71,35 @@ namespace PixiEditorPrototype.Models
             memberVm.RaisePropertyChanged(nameof(memberVm.HasMask));
         }
 
-        private void ProcessMoveViewport(MoveViewport_PassthroughAction info)
+        private void ProcessRefreshViewport(RefreshViewport_PassthroughAction info)
         {
-            var oldResolution = doc.RenderResolution;
-
-            helper.State.ViewportCenter = info.Center;
-            helper.State.ViewportSize = info.Size;
-            helper.State.ViewportAngle = info.Angle;
-            helper.State.ViewportRealSize = info.RealSize;
-
-            var newResolution = doc.RenderResolution;
-
-            if (oldResolution != newResolution)
-                doc.RaisePropertyChanged(nameof(doc.RenderBitmap));
+            var viewport = doc.GetViewport(info.GuidValue);
+            if (viewport is null)
+            {
+                helper.State.Viewports.Remove(info.GuidValue);
+                return;
+            }
+            helper.State.Viewports[info.GuidValue] = viewport.Value with { Dimensions = viewport.Value.Dimensions / 2, RealDimensions = viewport.Value.RealDimensions / 2 };
+            doc.UpdateViewportResolution(info.GuidValue, viewport.Value.Resolution);
         }
 
         private void ProcessSize(Size_ChangeInfo info)
         {
-            doc.SurfaceFull.Dispose();
-            doc.SurfaceHalf?.Dispose();
-            doc.SurfaceQuarter?.Dispose();
-            doc.SurfaceEighth?.Dispose();
-
-            doc.SurfaceHalf = null;
-            doc.SurfaceQuarter = null;
-            doc.SurfaceEighth = null;
-
-            doc.BitmapHalf = null;
-            doc.BitmapQuarter = null;
-            doc.BitmapEighth = null;
-
-            doc.BitmapFull = CreateBitmap(helper.Tracker.Document.Size);
-            doc.SurfaceFull = CreateSKSurface(doc.BitmapFull);
-
-            if (helper.Tracker.Document.Size.X > 512 && helper.Tracker.Document.Size.Y > 512)
-            {
-                doc.BitmapHalf = CreateBitmap(helper.Tracker.Document.Size / 2);
-                doc.SurfaceHalf = CreateSKSurface(doc.BitmapHalf);
-            }
-
-            if (helper.Tracker.Document.Size.X > 1024 && helper.Tracker.Document.Size.Y > 1024)
+            var size = helper.Tracker.Document.Size;
+            foreach (var (res, surf) in doc.Surfaces)
             {
-                doc.BitmapQuarter = CreateBitmap(helper.Tracker.Document.Size / 4);
-                doc.SurfaceQuarter = CreateSKSurface(doc.BitmapQuarter);
+                surf.Dispose();
+                doc.Bitmaps[res] = CreateBitmap((Vector2i)(size * res.Multiplier()));
+                doc.Surfaces[res] = CreateSKSurface(doc.Bitmaps[res]);
             }
 
-            if (helper.Tracker.Document.Size.X > 2048 && helper.Tracker.Document.Size.Y > 2048)
-            {
-                doc.BitmapEighth = CreateBitmap(helper.Tracker.Document.Size / 8);
-                doc.SurfaceEighth = CreateSKSurface(doc.BitmapEighth);
-            }
-
-            doc.RaisePropertyChanged(nameof(doc.BitmapFull));
-            doc.RaisePropertyChanged(nameof(doc.BitmapHalf));
-            doc.RaisePropertyChanged(nameof(doc.BitmapQuarter));
-            doc.RaisePropertyChanged(nameof(doc.BitmapEighth));
-
-            doc.RaisePropertyChanged(nameof(doc.RenderBitmap));
+            doc.RaisePropertyChanged(nameof(doc.Width));
+            doc.RaisePropertyChanged(nameof(doc.Height));
         }
 
         private WriteableBitmap CreateBitmap(Vector2i size)
         {
-            return new WriteableBitmap(size.X, size.Y, 96, 96, PixelFormats.Pbgra32, null);
+            return new WriteableBitmap(Math.Max(size.X, 1), Math.Max(size.Y, 1), 96, 96, PixelFormats.Pbgra32, null);
         }
 
         private SKSurface CreateSKSurface(WriteableBitmap bitmap)

+ 0 - 22
src/PixiEditorPrototype/Models/MoveViewport_PassthroughAction.cs

@@ -1,22 +0,0 @@
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Actions;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-
-namespace PixiEditorPrototype.Models
-{
-    internal record class MoveViewport_PassthroughAction : IAction, IChangeInfo
-    {
-        public MoveViewport_PassthroughAction(Vector2d center, Vector2d size, double angle, Vector2d realSize)
-        {
-            Center = center;
-            Size = size;
-            Angle = angle;
-            RealSize = realSize;
-        }
-
-        public Vector2d Center { get; }
-        public Vector2d Size { get; }
-        public Vector2d RealSize { get; }
-        public double Angle { get; }
-    }
-}

+ 8 - 0
src/PixiEditorPrototype/Models/RefreshViewport_PassthroughAction.cs

@@ -0,0 +1,8 @@
+using System;
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+
+namespace PixiEditorPrototype.Models
+{
+    internal record class RefreshViewport_PassthroughAction(Guid GuidValue) : IAction, IChangeInfo;
+}

+ 1 - 11
src/PixiEditorPrototype/Models/Rendering/RenderInfos/DirtyRect_RenderInfo.cs

@@ -2,15 +2,5 @@
 
 namespace PixiEditorPrototype.Models.Rendering.RenderInfos
 {
-    public record struct DirtyRect_RenderInfo : IRenderInfo
-    {
-        public DirtyRect_RenderInfo(Vector2i pos, Vector2i size)
-        {
-            Pos = pos;
-            Size = size;
-        }
-
-        public Vector2i Pos { get; }
-        public Vector2i Size { get; }
-    }
+    public record class DirtyRect_RenderInfo(Vector2i Pos, Vector2i Size, ChunkResolution Resolution) : IRenderInfo;
 }

+ 56 - 34
src/PixiEditorPrototype/Models/Rendering/WriteableBitmapUpdater.cs

@@ -8,20 +8,22 @@ using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditorPrototype.Models.Rendering.RenderInfos;
+using PixiEditorPrototype.ViewModels;
 using SkiaSharp;
 
 namespace PixiEditorPrototype.Models.Rendering
 {
     internal class WriteableBitmapUpdater
     {
-        private DocumentHelpers helpers;
+        private readonly DocumentViewModel doc;
+        private readonly DocumentHelpers helpers;
 
-        private static SKPaint BlendingPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
-        private static SKPaint ReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
-        private static SKPaint SelectionPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver, Color = new(0xa0FFFFFF) };
-        private static SKPaint ClearPaint = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
+        private static readonly SKPaint BlendingPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        private static readonly SKPaint ReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
+        private static readonly SKPaint SelectionPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver, Color = new(0xa0FFFFFF) };
+        private static readonly SKPaint ClearPaint = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
 
-        private Dictionary<ChunkResolution, HashSet<Vector2i>> postponedChunks = new()
+        private readonly Dictionary<ChunkResolution, HashSet<Vector2i>> postponedChunks = new()
         {
             [ChunkResolution.Full] = new(),
             [ChunkResolution.Half] = new(),
@@ -29,22 +31,23 @@ namespace PixiEditorPrototype.Models.Rendering
             [ChunkResolution.Eighth] = new()
         };
 
-        public WriteableBitmapUpdater(DocumentHelpers helpers)
+        public WriteableBitmapUpdater(DocumentViewModel doc, DocumentHelpers helpers)
         {
+            this.doc = doc;
             this.helpers = helpers;
         }
 
-        public async Task<List<IRenderInfo>> ProcessChanges(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, ChunkResolution resolution)
+        public async Task<List<IRenderInfo>> ProcessChanges(IReadOnlyList<IChangeInfo?> changes)
         {
-            return await Task.Run(() => Render(changes, screenSurface, resolution)).ConfigureAwait(true);
+            return await Task.Run(() => Render(changes)).ConfigureAwait(true);
         }
 
-        public List<IRenderInfo> ProcessChangesSync(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, ChunkResolution resolution)
+        public List<IRenderInfo> ProcessChangesSync(IReadOnlyList<IChangeInfo?> changes)
         {
-            return Render(changes, screenSurface, resolution);
+            return Render(changes);
         }
 
-        private HashSet<Vector2i> FindChunksToRerender(IReadOnlyList<IChangeInfo> changes, ChunkResolution resolution)
+        private Dictionary<ChunkResolution, HashSet<Vector2i>> FindChunksToRerender(IReadOnlyList<IChangeInfo?> changes)
         {
             HashSet<Vector2i> affectedChunks = new();
             foreach (var change in changes)
@@ -95,25 +98,39 @@ namespace PixiEditorPrototype.Models.Rendering
                         else
                             AddAllChunks(affectedChunks);
                         break;
-                    case MoveViewport_PassthroughAction moveViewportInfo:
-
+                    case RefreshViewport_PassthroughAction moveViewportInfo:
                         break;
                 }
             }
 
-            postponedChunks[ChunkResolution.Full].UnionWith(affectedChunks);
-            postponedChunks[ChunkResolution.Half].UnionWith(affectedChunks);
-            postponedChunks[ChunkResolution.Quarter].UnionWith(affectedChunks);
-            postponedChunks[ChunkResolution.Eighth].UnionWith(affectedChunks);
+            foreach (var (_, postponed) in postponedChunks)
+            {
+                postponed.UnionWith(affectedChunks);
+            }
 
-            var chunksOnScreen = OperationHelper.FindChunksTouchingRectangle(
-                helpers.State.ViewportCenter,
-                helpers.State.ViewportSize,
-                -helpers.State.ViewportAngle,
-                ChunkResolution.Full.PixelSize());
+            var chunksOnScreen = new Dictionary<ChunkResolution, HashSet<Vector2i>>()
+            {
+                [ChunkResolution.Full] = new(),
+                [ChunkResolution.Half] = new(),
+                [ChunkResolution.Quarter] = new(),
+                [ChunkResolution.Eighth] = new()
+            };
 
-            chunksOnScreen.IntersectWith(postponedChunks[resolution]);
-            postponedChunks[resolution].ExceptWith(chunksOnScreen);
+            foreach (var (_, viewport) in helpers.State.Viewports)
+            {
+                var viewportChunks = OperationHelper.FindChunksTouchingRectangle(
+                    viewport.Center,
+                    viewport.Dimensions,
+                    -viewport.Angle,
+                    ChunkResolution.Full.PixelSize());
+                chunksOnScreen[viewport.Resolution].UnionWith(viewportChunks);
+            }
+
+            foreach (var (res, postponed) in postponedChunks)
+            {
+                chunksOnScreen[res].IntersectWith(postponed);
+                postponed.ExceptWith(chunksOnScreen[res]);
+            }
 
             return chunksOnScreen;
         }
@@ -132,20 +149,25 @@ namespace PixiEditorPrototype.Models.Rendering
             }
         }
 
-        private List<IRenderInfo> Render(IReadOnlyList<IChangeInfo> changes, SKSurface screenSurface, ChunkResolution resolution)
+        private List<IRenderInfo> Render(IReadOnlyList<IChangeInfo?> changes)
         {
-            HashSet<Vector2i> chunks = FindChunksToRerender(changes, resolution);
+            Dictionary<ChunkResolution, HashSet<Vector2i>> chunksToRerender = FindChunksToRerender(changes);
 
             List<IRenderInfo> infos = new();
 
-            int chunkSize = resolution.PixelSize();
-            foreach (var chunkPos in chunks!)
+            foreach (var (resolution, chunks) in chunksToRerender)
             {
-                RenderChunk(chunkPos, screenSurface, resolution);
-                infos.Add(new DirtyRect_RenderInfo(
-                    chunkPos * chunkSize,
-                    new(chunkSize, chunkSize)
-                    ));
+                int chunkSize = resolution.PixelSize();
+                SKSurface screenSurface = doc.Surfaces[resolution];
+                foreach (var chunkPos in chunks)
+                {
+                    RenderChunk(chunkPos, screenSurface, resolution);
+                    infos.Add(new DirtyRect_RenderInfo(
+                        chunkPos * chunkSize,
+                        new(chunkSize, chunkSize),
+                        resolution
+                        ));
+                }
             }
 
             return infos;

+ 22 - 0
src/PixiEditorPrototype/Models/ViewportLocation.cs

@@ -0,0 +1,22 @@
+using System;
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditorPrototype.Models;
+internal readonly record struct ViewportLocation(double Angle, Vector2d Center, Vector2d RealDimensions, Vector2d Dimensions, Guid GuidValue)
+{
+    public ChunkResolution Resolution
+    {
+        get
+        {
+            Vector2d densityVec = Dimensions.Divide(RealDimensions);
+            double density = Math.Min(densityVec.X, densityVec.Y);
+            if (density > 8.01)
+                return ChunkResolution.Eighth;
+            else if (density > 4.01)
+                return ChunkResolution.Quarter;
+            else if (density > 2.01)
+                return ChunkResolution.Half;
+            return ChunkResolution.Full;
+        }
+    }
+}

+ 1 - 0
src/PixiEditorPrototype/PixiEditorPrototype.csproj

@@ -8,6 +8,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <PackageReference Include="Dirkster.AvalonDock" Version="4.70.1" />
     <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
     <PackageReference Include="PixiEditor.ColorPicker" Version="3.2.0" />
   </ItemGroup>

+ 33 - 57
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -14,7 +14,6 @@ using PixiEditor.ChangeableDocument.Actions.Root;
 using PixiEditor.ChangeableDocument.Actions.Structure;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Enums;
-using PixiEditor.Zoombox;
 using PixiEditorPrototype.Models;
 using SkiaSharp;
 
@@ -55,57 +54,19 @@ namespace PixiEditorPrototype.ViewModels
         public RelayCommand? CreateMaskCommand { get; }
         public RelayCommand? DeleteMaskCommand { get; }
 
+        public int Width => Helpers.Tracker.Document.Size.X;
+        public int Height => Helpers.Tracker.Document.Size.Y;
+        public Guid GuidValue { get; } = Guid.NewGuid();
 
-
-        public SKSurface SurfaceFull { get; set; }
-        public WriteableBitmap BitmapFull { get; set; } = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null);
-        public SKSurface? SurfaceHalf { get; set; } = null;
-        public WriteableBitmap? BitmapHalf { get; set; } = null;
-        public SKSurface? SurfaceQuarter { get; set; } = null;
-        public WriteableBitmap? BitmapQuarter { get; set; } = null;
-        public SKSurface? SurfaceEighth { get; set; } = null;
-        public WriteableBitmap? BitmapEighth { get; set; } = null;
-
-        public WriteableBitmap RenderBitmap
+        public Dictionary<ChunkResolution, WriteableBitmap> Bitmaps { get; set; } = new()
         {
-            get => GetCorrespondingBitmap(RenderResolution)!;
-        }
+            [ChunkResolution.Full] = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null),
+            [ChunkResolution.Half] = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Pbgra32, null),
+            [ChunkResolution.Quarter] = new WriteableBitmap(16, 16, 96, 96, PixelFormats.Pbgra32, null),
+            [ChunkResolution.Eighth] = new WriteableBitmap(8, 8, 96, 96, PixelFormats.Pbgra32, null),
+        };
 
-        public ChunkResolution RenderResolution
-        {
-            get
-            {
-                var targetRes = GetResolutionForViewport(Helpers.State.ViewportSize, Helpers.State.ViewportRealSize);
-                if (GetCorrespondingBitmap(targetRes) is not null)
-                    return targetRes;
-                return ChunkResolution.Full;
-            }
-        }
-
-        public ChunkResolution GetResolutionForViewport(Vector2d size, Vector2d realSize)
-        {
-            Vector2d densityVec = size.Divide(realSize);
-            double density = Math.Min(densityVec.X, densityVec.Y);
-            if (density > 8.01)
-                return ChunkResolution.Eighth;
-            else if (density > 4.01)
-                return ChunkResolution.Quarter;
-            else if (density > 2.01)
-                return ChunkResolution.Half;
-            return ChunkResolution.Full;
-        }
-
-        public WriteableBitmap? GetCorrespondingBitmap(ChunkResolution resolution)
-        {
-            return resolution switch
-            {
-                ChunkResolution.Full => BitmapFull,
-                ChunkResolution.Half => BitmapHalf,
-                ChunkResolution.Quarter => BitmapQuarter,
-                ChunkResolution.Eighth => BitmapEighth,
-                _ => BitmapFull,
-            };
-        }
+        public Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
 
         public int ResizeWidth { get; set; }
         public int ResizeHeight { get; set; }
@@ -136,10 +97,13 @@ namespace PixiEditorPrototype.ViewModels
             CreateMaskCommand = new RelayCommand(CreateMask);
             DeleteMaskCommand = new RelayCommand(DeleteMask);
 
-            SurfaceFull = SKSurface.Create(
-                new SKImageInfo(BitmapFull.PixelWidth, BitmapFull.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
-                BitmapFull.BackBuffer,
-                BitmapFull.BackBufferStride);
+            foreach (var bitmap in Bitmaps)
+            {
+                var surface = SKSurface.Create(
+                    new SKImageInfo(bitmap.Value.PixelWidth, bitmap.Value.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
+                    bitmap.Value.BackBuffer, bitmap.Value.BackBufferStride);
+                Surfaces[bitmap.Key] = surface;
+            }
         }
 
         bool startedRectangle = false;
@@ -184,6 +148,21 @@ namespace PixiEditorPrototype.ViewModels
             owner.View?.ForceRefreshFinalImage();
         }
 
+        public void UpdateViewportResolution(Guid viewportGuid, ChunkResolution resolution)
+        {
+            owner.UpdateViewportResolution(viewportGuid, resolution);
+        }
+
+        public ViewportLocation? GetViewport(Guid viewportGuid)
+        {
+            return owner.GetViewport(viewportGuid);
+        }
+
+        public void RefreshViewport(Guid viewportGuid)
+        {
+            Helpers.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(viewportGuid));
+        }
+
         private void ClearSelection(object? param)
         {
             Helpers.ActionAccumulator.AddFinishedActions(new ClearSelection_Action());
@@ -254,10 +233,7 @@ namespace PixiEditorPrototype.ViewModels
 
         private void MoveViewport(object? param)
         {
-            if (param is null)
-                throw new ArgumentNullException(nameof(param));
-            var args = (ViewportRoutedEventArgs)param;
-            Helpers.ActionAccumulator.AddActions(new MoveViewport_PassthroughAction(args.Center, args.Size / 2, args.Angle, args.RealSize / 2));
+            Helpers.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(Guid.Empty));
         }
 
         private void ClearHistory(object? param)

+ 48 - 16
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -1,4 +1,6 @@
-using System.ComponentModel;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
 using System.Windows.Input;
 using System.Windows.Media;
 using ChunkyImageLib.DataHolders;
@@ -12,7 +14,8 @@ namespace PixiEditorPrototype.ViewModels
     internal class ViewModelMain : INotifyPropertyChanged
     {
         public IMainView? View { get; set; }
-        public DocumentViewModel? ActiveDocument { get; }
+
+        public DocumentViewModel? ActiveDocument => GetDocumentByGuid(activeDocumentGuid);
 
         public RelayCommand? MouseDownCommand { get; }
         public RelayCommand? MouseMoveCommand { get; }
@@ -21,15 +24,6 @@ namespace PixiEditorPrototype.ViewModels
 
         public Color SelectedColor { get; set; } = Colors.Black;
 
-        private bool mouseIsDown = false;
-        private int mouseDownCanvasX = 0;
-        private int mouseDownCanvasY = 0;
-
-        private bool startedDrawingRect = false;
-        private bool startedSelectingRect = false;
-
-        private Tool activeTool = Tool.Rectangle;
-
         public event PropertyChangedEventHandler? PropertyChanged;
 
         public bool NormalZoombox
@@ -67,6 +61,20 @@ namespace PixiEditorPrototype.ViewModels
 
         public ZoomboxMode ZoomboxMode { get; set; }
 
+        public ViewportViewModel MainViewport { get; }
+
+        private Dictionary<Guid, DocumentViewModel> documents = new();
+        private Guid activeDocumentGuid;
+
+        private bool mouseIsDown = false;
+        private int mouseDownCanvasX = 0;
+        private int mouseDownCanvasY = 0;
+
+        private bool startedDrawingRect = false;
+        private bool startedSelectingRect = false;
+
+        private Tool activeTool = Tool.Rectangle;
+
         public ViewModelMain()
         {
             MouseDownCommand = new RelayCommand(MouseDown);
@@ -74,7 +82,31 @@ namespace PixiEditorPrototype.ViewModels
             MouseUpCommand = new RelayCommand(MouseUp);
             ChangeActiveToolCommand = new RelayCommand(ChangeActiveTool);
 
-            ActiveDocument = new DocumentViewModel(this);
+            var doc = new DocumentViewModel(this);
+            documents[doc.GuidValue] = doc;
+            activeDocumentGuid = doc.GuidValue;
+
+            MainViewport = new(this, activeDocumentGuid);
+            doc.RefreshViewport(MainViewport.GuidValue);
+        }
+
+        public ViewportLocation? GetViewport(Guid viewportGuid)
+        {
+            if (MainViewport.GuidValue != viewportGuid)
+                return null;
+            return new ViewportLocation(MainViewport.Angle, MainViewport.Center, MainViewport.RealDimensions, MainViewport.Dimensions, Guid.Empty);
+        }
+
+        public DocumentViewModel? GetDocumentByGuid(Guid guid)
+        {
+            return documents.TryGetValue(guid, out DocumentViewModel? value) ? value : null;
+        }
+
+        public void UpdateViewportResolution(Guid viewportGuid, ChunkResolution resolution)
+        {
+            if (viewportGuid != MainViewport.GuidValue)
+                return;
+            MainViewport.Resolution = resolution;
         }
 
         private void MouseDown(object? param)
@@ -85,8 +117,8 @@ namespace PixiEditorPrototype.ViewModels
             var args = (MouseButtonEventArgs)(param!);
             var source = (System.Windows.Controls.Image)args.Source;
             var pos = args.GetPosition(source);
-            mouseDownCanvasX = (int)(pos.X / source.Width * ActiveDocument.BitmapFull.PixelHeight);
-            mouseDownCanvasY = (int)(pos.Y / source.Height * ActiveDocument.BitmapFull.PixelHeight);
+            mouseDownCanvasX = (int)(pos.X / source.Width * ActiveDocument.Bitmaps[ChunkResolution.Full].PixelWidth);
+            mouseDownCanvasY = (int)(pos.Y / source.Height * ActiveDocument.Bitmaps[ChunkResolution.Full].PixelHeight);
         }
 
         private void MouseMove(object? param)
@@ -96,8 +128,8 @@ namespace PixiEditorPrototype.ViewModels
             var args = (MouseEventArgs)(param!);
             var source = (System.Windows.Controls.Image)args.Source;
             var pos = args.GetPosition(source);
-            int curX = (int)(pos.X / source.Width * ActiveDocument.BitmapFull.PixelHeight);
-            int curY = (int)(pos.Y / source.Height * ActiveDocument.BitmapFull.PixelHeight);
+            int curX = (int)(pos.X / source.Width * ActiveDocument.Bitmaps[ChunkResolution.Full].PixelWidth);
+            int curY = (int)(pos.Y / source.Height * ActiveDocument.Bitmaps[ChunkResolution.Full].PixelHeight);
 
             ProcessToolMouseMove(curX, curY);
         }

+ 94 - 0
src/PixiEditorPrototype/ViewModels/ViewportViewModel.cs

@@ -0,0 +1,94 @@
+using System;
+using System.ComponentModel;
+using System.Windows.Media.Imaging;
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditorPrototype.ViewModels;
+internal class ViewportViewModel : INotifyPropertyChanged
+{
+    public event PropertyChangedEventHandler? PropertyChanged;
+    private ViewModelMain mainVM;
+
+    public ViewportViewModel(ViewModelMain mainVM, Guid targetDocumentGuid)
+    {
+        this.mainVM = mainVM;
+        TargetDocumentGuid = targetDocumentGuid;
+    }
+
+    private double angle = 0;
+    public double Angle
+    {
+        get => angle;
+        set
+        {
+            angle = value;
+            PropertyChanged?.Invoke(this, new(nameof(Angle)));
+            mainVM.GetDocumentByGuid(TargetDocumentGuid)?.RefreshViewport(GuidValue);
+        }
+    }
+
+    private Vector2d center = new(32, 32);
+    public Vector2d Center
+    {
+        get => center;
+        set
+        {
+            center = value;
+            PropertyChanged?.Invoke(this, new(nameof(Center)));
+            mainVM.GetDocumentByGuid(TargetDocumentGuid)?.RefreshViewport(GuidValue);
+        }
+    }
+
+    private Vector2d realDimensions = new(double.MaxValue, double.MaxValue);
+    public Vector2d RealDimensions
+    {
+        get => realDimensions;
+        set
+        {
+            realDimensions = value;
+            PropertyChanged?.Invoke(this, new(nameof(RealDimensions)));
+            mainVM.GetDocumentByGuid(TargetDocumentGuid)?.RefreshViewport(GuidValue);
+        }
+    }
+
+    private Vector2d dimensions = new(64, 64);
+    public Vector2d Dimensions
+    {
+        get => dimensions;
+        set
+        {
+            dimensions = value;
+            PropertyChanged?.Invoke(this, new(nameof(Dimensions)));
+            mainVM.GetDocumentByGuid(TargetDocumentGuid)?.RefreshViewport(GuidValue);
+        }
+    }
+
+    public Guid GuidValue { get; } = Guid.NewGuid();
+
+    public Guid TargetDocumentGuid { get; }
+
+    private ChunkResolution resolution = ChunkResolution.Full;
+    public ChunkResolution Resolution
+    {
+        get => resolution;
+        set
+        {
+            if (value == resolution)
+                return;
+            resolution = value;
+            PropertyChanged?.Invoke(this, new(nameof(Resolution)));
+            PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+        }
+    }
+
+    public WriteableBitmap? TargetBitmap
+    {
+        get
+        {
+            var doc = mainVM.GetDocumentByGuid(TargetDocumentGuid);
+            if (doc is null)
+                return null;
+            return doc.Bitmaps.TryGetValue(Resolution, out var value) ? value : null;
+        }
+    }
+}

+ 9 - 4
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -138,7 +138,11 @@
 
         <Grid>
             <zoombox:Zoombox x:Name="zoombox" UseTouchGestures="True"
-                             ZoomMode="{Binding ZoomboxMode}" 
+                             Center="{Binding MainViewport.Center, Mode=OneWayToSource}"
+                             Angle="{Binding MainViewport.Angle, Mode=OneWayToSource}"
+                             RealDimensions="{Binding MainViewport.RealDimensions, Mode=OneWayToSource}"
+                             Dimensions="{Binding MainViewport.Dimensions, Mode=OneWayToSource}"
+                             ZoomMode="{Binding ZoomboxMode, Mode=TwoWay}" 
                              FlipX="{Binding ElementName=flipXCheckbox, Path=IsChecked}"
                              FlipY="{Binding ElementName=flipYCheckbox, Path=IsChecked}">
                 <i:Interaction.Triggers>
@@ -147,9 +151,10 @@
                     </i:EventTrigger>
                 </i:Interaction.Triggers>
                 <Border BorderThickness="1" Background="White" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center">
-                    <Image Source="{Binding ActiveDocument.RenderBitmap}" Focusable="True"
-                       Width="{Binding ActiveDocument.BitmapFull.PixelWidth}" Height="{Binding ActiveDocument.BitmapFull.PixelHeight}"
-                       RenderOptions.BitmapScalingMode="NearestNeighbor">
+                    <Image Focusable="True"
+                           Width="{Binding ActiveDocument.Width}" Height="{Binding ActiveDocument.Height}"
+                           Source="{Binding MainViewport.TargetBitmap}"
+                           RenderOptions.BitmapScalingMode="NearestNeighbor">
                         <i:Interaction.Triggers>
                             <i:EventTrigger EventName="MouseDown">
                                 <i:InvokeCommandAction Command="{Binding MouseDownCommand}" PassEventArgsToCommand="True"/>

+ 6 - 3
src/PixiEditorPrototype/Views/MainWindow.xaml.cs

@@ -1,6 +1,6 @@
-using PixiEditorPrototype.ViewModels;
-using System.Windows;
+using System.Windows;
 using System.Windows.Controls;
+using PixiEditorPrototype.ViewModels;
 
 namespace PixiEditorPrototype.Views
 {
@@ -11,9 +11,12 @@ namespace PixiEditorPrototype.Views
             InitializeComponent();
             ((ViewModelMain)DataContext).View = this;
         }
+
+        private Image? GetImage() => (Image?)((Border?)zoombox.AdditionalContent)?.Child;
+
         public void ForceRefreshFinalImage()
         {
-            ((Image?)((Border?)zoombox.AdditionalContent)?.Child)?.InvalidateVisual();
+            GetImage()?.InvalidateVisual();
         }
     }
 }

+ 2 - 2
src/README.md

@@ -12,7 +12,7 @@ Decouples the state of a document from the UI.
     - [x] Periodic cleaning of empty chunks
     - [x] ChunkPool multithreading support
     - [x] Dispose that returns borrowed chunks
-    - [ ] Finalizers that return borrowed chunks
+    - [x] ChunkyImage finalizer that returns borrowed chunks
     - [x] GetLatestChunk resolution parameter
         - [x] Support for different chunk sizes in chunkpool
         - [x] Rendering for different chunk sizes
@@ -49,7 +49,7 @@ Decouples the state of a document from the UI.
         - [x] UpdateableChange class for changes requiring preview
         - [x] Handling for changes that don't change anything
         - [x] Dispose changes
-        - [ ] Dispose the entire document
+        - [x] Dispose the entire document
         - [x] Basic undo stack infrastructure
         - [x] Ignored changes (changes that don't get recorded in undo)
         - [x] Clear undo/redo history