Browse Source

Optimize PasteImage_UpdateableChange, Viewports refactor

Equbuxu 3 years ago
parent
commit
ddcfebff11

+ 8 - 2
src/ChunkyImageLib/ChunkyImage.cs

@@ -257,11 +257,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
     }
 
-    public void EnqueueDrawImage(ShapeCorners corners, Surface image)
+    /// <summary>
+    /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects. 
+    /// It will hovewer copy the surface right away which can be slow (in updateable changes especially). 
+    /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
+    /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
+    /// </summary>
+    public void EnqueueDrawImage(ShapeCorners corners, Surface image, bool copyImage = true)
     {
         lock (lockObject)
         {
-            ImageOperation operation = new(corners, image);
+            ImageOperation operation = new(corners, image, copyImage);
             EnqueueOperation(operation);
         }
     }

+ 19 - 6
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -8,11 +8,12 @@ internal record class ImageOperation : IDrawOperation
     private SKMatrix transformMatrix;
     private ShapeCorners corners;
     private Surface toPaint;
+    private bool imageWasCopied = false;
     private static SKPaint ReplacingPaint = new() { BlendMode = SKBlendMode.Src };
 
     public bool IgnoreEmptyChunks => false;
 
-    public ImageOperation(Vector2i pos, Surface image)
+    public ImageOperation(Vector2i pos, Surface image, bool copyImage = true)
     {
         corners = new()
         {
@@ -24,15 +25,26 @@ internal record class ImageOperation : IDrawOperation
         transformMatrix = SKMatrix.CreateIdentity();
         transformMatrix.TransX = pos.X;
         transformMatrix.TransY = pos.Y;
-        // copying is required for thread safety
-        toPaint = new Surface(image);
+
+        // copying is needed for thread safety
+        if (copyImage)
+            toPaint = new Surface(image);
+        else
+            toPaint = image;
+        imageWasCopied = copyImage;
     }
 
-    public ImageOperation(ShapeCorners corners, Surface image)
+    public ImageOperation(ShapeCorners corners, Surface image, bool copyImage = true)
     {
         this.corners = corners;
         transformMatrix = OperationHelper.CreateMatrixFromPoints(corners, image.Size);
-        toPaint = new Surface(image);
+
+        // copying is needed for thread safety
+        if (copyImage)
+            toPaint = new Surface(image);
+        else
+            toPaint = image;
+        imageWasCopied = copyImage;
     }
 
     public void DrawOnChunk(Chunk chunk, Vector2i chunkPos)
@@ -56,6 +68,7 @@ internal record class ImageOperation : IDrawOperation
 
     public void Dispose()
     {
-        toPaint.Dispose();
+        if (imageWasCopied)
+            toPaint.Dispose();
     }
 }

+ 8 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs

@@ -12,6 +12,8 @@ internal class PasteImage_UpdateableChange : UpdateableChange
     private readonly Surface imageToPaste;
     private CommittedChunkStorage? savedChunks;
 
+    private bool hasEnqueudImage = false;
+
     public PasteImage_UpdateableChange(ShapeCorners corners, Surface imageToPaste, Guid memberGuid, bool drawOnMask)
     {
         this.corners = corners;
@@ -30,7 +32,8 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         var prevChunks = targetImage.FindAffectedChunks();
 
         targetImage.CancelChanges();
-        targetImage.EnqueueDrawImage(corners, imageToPaste);
+        targetImage.EnqueueDrawImage(corners, imageToPaste, false);
+        hasEnqueudImage = true;
 
         var affectedChunks = targetImage.FindAffectedChunks();
         affectedChunks.UnionWith(prevChunks);
@@ -44,6 +47,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         savedChunks?.Dispose();
         savedChunks = new(targetImage, targetImage.FindAffectedChunks());
         targetImage.CommitChanges();
+        hasEnqueudImage = false;
         ignoreInUndo = false;
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
     }
@@ -62,6 +66,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         savedChunks.ApplyChunksToImage(targetImage);
         var chunks = targetImage.FindAffectedChunks();
         targetImage.CommitChanges();
+        hasEnqueudImage = false;
         savedChunks.Dispose();
         savedChunks = null;
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
@@ -69,6 +74,8 @@ internal class PasteImage_UpdateableChange : UpdateableChange
 
     public override void Dispose()
     {
+        if (hasEnqueudImage)
+            throw new InvalidOperationException("Attempted to dispose the change while it's internally stored image is still used enqueued in some ChunkyImage. Most likely someone tried to dispose a change after ApplyTemporarily was called but before the subsequent call to Apply. Don't do that.");
         imageToPaste.Dispose();
         savedChunks?.Dispose();
     }

+ 0 - 70
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformHelper.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Windows;
-using System.Windows.Media;
 using ChunkyImageLib.DataHolders;
 
 namespace PixiEditorPrototype.CustomControls.TransformOverlay;
@@ -9,19 +8,6 @@ internal static class TransformHelper
     public const double AnchorSize = 10;
     public const double MoveHandleSize = 16;
 
-    private static Pen blackPen = new Pen(Brushes.Black, 1);
-    private static Pen blackDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 4 }, 0) };
-    private static Pen whiteDashedPen = new Pen(Brushes.White, 1) { DashStyle = new DashStyle(new double[] { 2, 4 }, 2) };
-    private static Pen blackFreqDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 2 }, 0) };
-    private static Pen whiteFreqDashedPen = new Pen(Brushes.White, 1) { DashStyle = new DashStyle(new double[] { 2, 2 }, 2) };
-
-    private static PathGeometry handleGeometry = new()
-    {
-        FillRule = FillRule.Nonzero,
-        Figures = (PathFigureCollection?)new PathFigureCollectionConverter()
-            .ConvertFrom("M 0.50025839 0 0.4248062 0.12971572 0.34987079 0.25994821 h 0.1002584 V 0.45012906 H 0.25994831 V 0.34987066 L 0.12971577 0.42480604 0 0.5002582 0.12971577 0.57519373 0.25994831 0.65012926 V 0.5498709 H 0.45012919 V 0.74005175 H 0.34987079 L 0.42480619 0.87028439 0.50025839 1 0.57519399 0.87028439 0.65012959 0.74005175 H 0.54987119 V 0.5498709 H 0.74005211 V 0.65012926 L 0.87028423 0.57519358 1 0.5002582 0.87028423 0.42480604 0.74005169 0.34987066 v 0.1002584 H 0.54987077 V 0.25994821 h 0.1002584 L 0.5751938 0.12971572 Z"),
-    };
-
     public static Rect ToAnchorRect(Vector2d pos, double zoomboxScale)
     {
         double scaled = AnchorSize / zoomboxScale;
@@ -258,62 +244,6 @@ internal static class TransformHelper
         return delta.X < scaled && delta.Y < scaled;
     }
 
-    public static void DrawOverlay
-        (DrawingContext context, Vector2d size, ShapeCorners corners, Vector2d origin, double zoomboxScale)
-    {
-        // draw transparent background to enable mouse input everywhere
-        context.DrawRectangle(Brushes.Transparent, null, new Rect(new Point(0, 0), new Size(size.X, size.Y)));
-
-        blackPen.Thickness = 1 / zoomboxScale;
-        blackDashedPen.Thickness = 1 / zoomboxScale;
-        whiteDashedPen.Thickness = 1 / zoomboxScale;
-        blackFreqDashedPen.Thickness = 1 / zoomboxScale;
-        whiteFreqDashedPen.Thickness = 1 / zoomboxScale;
-
-        Vector2d topLeft = corners.TopLeft;
-        Vector2d topRight = corners.TopRight;
-        Vector2d bottomLeft = corners.BottomLeft;
-        Vector2d bottomRight = corners.BottomRight;
-
-        // lines
-        context.DrawLine(blackDashedPen, ToPoint(topLeft), ToPoint(topRight));
-        context.DrawLine(whiteDashedPen, ToPoint(topLeft), ToPoint(topRight));
-        context.DrawLine(blackDashedPen, ToPoint(topLeft), ToPoint(bottomLeft));
-        context.DrawLine(whiteDashedPen, ToPoint(topLeft), ToPoint(bottomLeft));
-        context.DrawLine(blackDashedPen, ToPoint(bottomRight), ToPoint(bottomLeft));
-        context.DrawLine(whiteDashedPen, ToPoint(bottomRight), ToPoint(bottomLeft));
-        context.DrawLine(blackDashedPen, ToPoint(bottomRight), ToPoint(topRight));
-        context.DrawLine(whiteDashedPen, ToPoint(bottomRight), ToPoint(topRight));
-
-        // corner anchors
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect(topLeft, zoomboxScale));
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect(topRight, zoomboxScale));
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect(bottomLeft, zoomboxScale));
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect(bottomRight, zoomboxScale));
-
-        // side anchors
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect((topLeft - topRight) / 2 + topRight, zoomboxScale));
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect((topLeft - bottomLeft) / 2 + bottomLeft, zoomboxScale));
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect((bottomLeft - bottomRight) / 2 + bottomRight, zoomboxScale));
-        context.DrawRectangle(Brushes.White, blackPen, ToAnchorRect((topRight - bottomRight) / 2 + bottomRight, zoomboxScale));
-
-        // origin
-        double radius = AnchorSize / zoomboxScale / 2;
-        context.DrawEllipse(Brushes.Transparent, blackFreqDashedPen, ToPoint(origin), radius, radius);
-        context.DrawEllipse(Brushes.Transparent, whiteFreqDashedPen, ToPoint(origin), radius, radius);
-
-        // move handle
-        Vector2d handlePos = GetDragHandlePos(corners, zoomboxScale);
-        const double CrossSize = MoveHandleSize - 1;
-        context.DrawRectangle(Brushes.White, blackPen, ToHandleRect(handlePos, zoomboxScale));
-        handleGeometry.Transform = new MatrixTransform(
-            0, CrossSize / zoomboxScale,
-            CrossSize / zoomboxScale, 0,
-            handlePos.X - CrossSize / (zoomboxScale * 2), handlePos.Y - CrossSize / (zoomboxScale * 2)
-            );
-        context.DrawGeometry(Brushes.Black, null, handleGeometry);
-    }
-
     public static Vector2d GetDragHandlePos(ShapeCorners corners, double zoomboxScale)
     {
         Vector2d max = new(

+ 112 - 29
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformOverlay.cs

@@ -32,6 +32,16 @@ internal class TransformOverlay : Control
     public static readonly DependencyProperty SnapToAnglesProperty =
         DependencyProperty.Register(nameof(SnapToAngles), typeof(bool), typeof(TransformOverlay), new PropertyMetadata(false));
 
+    public static readonly DependencyProperty InternalStateProperty =
+        DependencyProperty.Register(nameof(InternalState), typeof(TransformState), typeof(TransformOverlay),
+            new FrameworkPropertyMetadata(default(TransformState), FrameworkPropertyMetadataOptions.AffectsRender));
+
+    public TransformState InternalState
+    {
+        get => (TransformState)GetValue(InternalStateProperty);
+        set => SetValue(InternalStateProperty, value);
+    }
+
     public bool SnapToAngles
     {
         get => (bool)GetValue(SnapToAnglesProperty);
@@ -67,8 +77,6 @@ internal class TransformOverlay : Control
         set => SetValue(ZoomboxScaleProperty, value);
     }
 
-    private Vector2d origin;
-
     private bool isMoving = false;
     private Vector2d mousePosOnStartMove = new();
     private Vector2d originOnStartMove = new();
@@ -77,21 +85,86 @@ internal class TransformOverlay : Control
     private bool isRotating = false;
     private Vector2d mousePosOnStartRotate = new();
     private ShapeCorners cornersOnStartRotate = new();
-    private double proportionalAngle1 = 0;
-    private double proportionalAngle2 = 0;
     private double propAngle1OnStartRotate = 0;
     private double propAngle2OnStartRotate = 0;
 
     private Anchor? capturedAnchor;
-    private bool originWasManuallyDragged = false;
     private ShapeCorners cornersOnStartAnchorDrag;
     private Vector2d mousePosOnStartAnchorDrag;
     private Vector2d originOnStartAnchorDrag;
 
+    private Pen blackPen = new Pen(Brushes.Black, 1);
+    private Pen blackDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 4 }, 0) };
+    private Pen whiteDashedPen = new Pen(Brushes.White, 1) { DashStyle = new DashStyle(new double[] { 2, 4 }, 2) };
+    private Pen blackFreqDashedPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new double[] { 2, 2 }, 0) };
+    private Pen whiteFreqDashedPen = new Pen(Brushes.White, 1) { DashStyle = new DashStyle(new double[] { 2, 2 }, 2) };
+
+    private PathGeometry handleGeometry = new()
+    {
+        FillRule = FillRule.Nonzero,
+        Figures = (PathFigureCollection?)new PathFigureCollectionConverter()
+            .ConvertFrom("M 0.50025839 0 0.4248062 0.12971572 0.34987079 0.25994821 h 0.1002584 V 0.45012906 H 0.25994831 V 0.34987066 L 0.12971577 0.42480604 0 0.5002582 0.12971577 0.57519373 0.25994831 0.65012926 V 0.5498709 H 0.45012919 V 0.74005175 H 0.34987079 L 0.42480619 0.87028439 0.50025839 1 0.57519399 0.87028439 0.65012959 0.74005175 H 0.54987119 V 0.5498709 H 0.74005211 V 0.65012926 L 0.87028423 0.57519358 1 0.5002582 0.87028423 0.42480604 0.74005169 0.34987066 v 0.1002584 H 0.54987077 V 0.25994821 h 0.1002584 L 0.5751938 0.12971572 Z"),
+    };
     protected override void OnRender(DrawingContext drawingContext)
     {
         base.OnRender(drawingContext);
-        TransformHelper.DrawOverlay(drawingContext, new(ActualWidth, ActualHeight), Corners, origin, ZoomboxScale);
+        DrawOverlay(drawingContext, new(ActualWidth, ActualHeight), Corners, InternalState.Origin, ZoomboxScale);
+    }
+
+    private void DrawOverlay
+        (DrawingContext context, Vector2d size, ShapeCorners corners, Vector2d origin, double zoomboxScale)
+    {
+        // draw transparent background to enable mouse input everywhere
+        context.DrawRectangle(Brushes.Transparent, null, new Rect(new Point(0, 0), new Size(size.X, size.Y)));
+
+        blackPen.Thickness = 1 / zoomboxScale;
+        blackDashedPen.Thickness = 1 / zoomboxScale;
+        whiteDashedPen.Thickness = 1 / zoomboxScale;
+        blackFreqDashedPen.Thickness = 1 / zoomboxScale;
+        whiteFreqDashedPen.Thickness = 1 / zoomboxScale;
+
+        Vector2d topLeft = corners.TopLeft;
+        Vector2d topRight = corners.TopRight;
+        Vector2d bottomLeft = corners.BottomLeft;
+        Vector2d bottomRight = corners.BottomRight;
+
+        // lines
+        context.DrawLine(blackDashedPen, TransformHelper.ToPoint(topLeft), TransformHelper.ToPoint(topRight));
+        context.DrawLine(whiteDashedPen, TransformHelper.ToPoint(topLeft), TransformHelper.ToPoint(topRight));
+        context.DrawLine(blackDashedPen, TransformHelper.ToPoint(topLeft), TransformHelper.ToPoint(bottomLeft));
+        context.DrawLine(whiteDashedPen, TransformHelper.ToPoint(topLeft), TransformHelper.ToPoint(bottomLeft));
+        context.DrawLine(blackDashedPen, TransformHelper.ToPoint(bottomRight), TransformHelper.ToPoint(bottomLeft));
+        context.DrawLine(whiteDashedPen, TransformHelper.ToPoint(bottomRight), TransformHelper.ToPoint(bottomLeft));
+        context.DrawLine(blackDashedPen, TransformHelper.ToPoint(bottomRight), TransformHelper.ToPoint(topRight));
+        context.DrawLine(whiteDashedPen, TransformHelper.ToPoint(bottomRight), TransformHelper.ToPoint(topRight));
+
+        // corner anchors
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect(topLeft, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect(topRight, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect(bottomLeft, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect(bottomRight, zoomboxScale));
+
+        // side anchors
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect((topLeft - topRight) / 2 + topRight, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect((topLeft - bottomLeft) / 2 + bottomLeft, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect((bottomLeft - bottomRight) / 2 + bottomRight, zoomboxScale));
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToAnchorRect((topRight - bottomRight) / 2 + bottomRight, zoomboxScale));
+
+        // origin
+        double radius = TransformHelper.AnchorSize / zoomboxScale / 2;
+        context.DrawEllipse(Brushes.Transparent, blackFreqDashedPen, TransformHelper.ToPoint(origin), radius, radius);
+        context.DrawEllipse(Brushes.Transparent, whiteFreqDashedPen, TransformHelper.ToPoint(origin), radius, radius);
+
+        // move handle
+        Vector2d handlePos = TransformHelper.GetDragHandlePos(corners, zoomboxScale);
+        const double CrossSize = TransformHelper.MoveHandleSize - 1;
+        context.DrawRectangle(Brushes.White, blackPen, TransformHelper.ToHandleRect(handlePos, zoomboxScale));
+        handleGeometry.Transform = new MatrixTransform(
+            0, CrossSize / zoomboxScale,
+            CrossSize / zoomboxScale, 0,
+            handlePos.X - CrossSize / (zoomboxScale * 2), handlePos.Y - CrossSize / (zoomboxScale * 2)
+            );
+        context.DrawGeometry(Brushes.Black, null, handleGeometry);
     }
 
     protected override void OnMouseDown(MouseButtonEventArgs e)
@@ -100,19 +173,19 @@ internal class TransformOverlay : Control
 
         e.Handled = true;
         var pos = TransformHelper.ToVector2d(e.GetPosition(this));
-        var anchor = TransformHelper.GetAnchorInPosition(pos, Corners, origin, ZoomboxScale);
+        var anchor = TransformHelper.GetAnchorInPosition(pos, Corners, InternalState.Origin, ZoomboxScale);
         if (anchor is not null)
         {
             capturedAnchor = anchor;
             cornersOnStartAnchorDrag = Corners;
-            originOnStartAnchorDrag = origin;
+            originOnStartAnchorDrag = InternalState.Origin;
             mousePosOnStartAnchorDrag = pos;
         }
         else if (Corners.IsPointInside(pos) || TransformHelper.IsWithinTransformHandle(TransformHelper.GetDragHandlePos(Corners, ZoomboxScale), pos, ZoomboxScale))
         {
             isMoving = true;
             mousePosOnStartMove = TransformHelper.ToVector2d(e.GetPosition(this));
-            originOnStartMove = origin;
+            originOnStartMove = InternalState.Origin;
             cornersOnStartMove = Corners;
         }
         else
@@ -120,8 +193,8 @@ internal class TransformOverlay : Control
             isRotating = true;
             mousePosOnStartRotate = TransformHelper.ToVector2d(e.GetPosition(this));
             cornersOnStartRotate = Corners;
-            propAngle1OnStartRotate = proportionalAngle1;
-            propAngle2OnStartRotate = proportionalAngle2;
+            propAngle1OnStartRotate = InternalState.ProportionalAngle1;
+            propAngle2OnStartRotate = InternalState.ProportionalAngle2;
         }
         CaptureMouse();
     }
@@ -149,17 +222,20 @@ internal class TransformOverlay : Control
                 TopRight = cornersOnStartMove.TopRight + delta,
             };
 
-            origin = originOnStartMove + delta;
+            InternalState = InternalState with { Origin = originOnStartMove + delta };
         }
         else if (isRotating)
         {
             var pos = TransformHelper.ToVector2d(e.GetPosition(this));
-            var angle = (mousePosOnStartRotate - origin).CCWAngleTo(pos - origin);
+            var angle = (mousePosOnStartRotate - InternalState.Origin).CCWAngleTo(pos - InternalState.Origin);
             if (SnapToAngles)
                 angle = TransformHelper.FindSnappingAngle(cornersOnStartRotate, angle);
-            proportionalAngle1 = propAngle1OnStartRotate + angle;
-            proportionalAngle2 = propAngle2OnStartRotate + angle;
-            Corners = TransformUpdateHelper.UpdateShapeFromRotation(cornersOnStartRotate, origin, angle);
+            InternalState = InternalState with
+            {
+                ProportionalAngle1 = propAngle1OnStartRotate + angle,
+                ProportionalAngle2 = propAngle2OnStartRotate + angle,
+            };
+            Corners = TransformUpdateHelper.UpdateShapeFromRotation(cornersOnStartRotate, InternalState.Origin, angle);
         }
     }
 
@@ -177,32 +253,36 @@ internal class TransformOverlay : Control
         if (TransformHelper.IsCorner((Anchor)capturedAnchor))
         {
             var targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos - mousePosOnStartAnchorDrag;
-            var newCorners = TransformUpdateHelper.UpdateShapeFromCorner((Anchor)capturedAnchor, CornerFreedom, proportionalAngle1, proportionalAngle2, cornersOnStartAnchorDrag, targetPos);
+            var newCorners = TransformUpdateHelper.UpdateShapeFromCorner
+                ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1, InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos);
             if (newCorners is not null)
             {
                 bool shouldSnap = (CornerFreedom is TransformCornerFreedom.ScaleProportionally or TransformCornerFreedom.Scale) && Corners.IsSnappedToPixels;
                 Corners = shouldSnap ? TransformHelper.SnapToPixels((ShapeCorners)newCorners) : (ShapeCorners)newCorners;
             }
-            if (!originWasManuallyDragged)
-                origin = TransformHelper.OriginFromCorners(Corners);
+            if (!InternalState.OriginWasManuallyDragged)
+                InternalState = InternalState with { Origin = TransformHelper.OriginFromCorners(Corners) };
         }
         else if (TransformHelper.IsSide((Anchor)capturedAnchor))
         {
             var targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos - mousePosOnStartAnchorDrag;
-            var newCorners = TransformUpdateHelper.UpdateShapeFromSide((Anchor)capturedAnchor, SideFreedom, proportionalAngle1, proportionalAngle2, cornersOnStartAnchorDrag, targetPos);
+            var newCorners = TransformUpdateHelper.UpdateShapeFromSide
+                ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1, InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos);
             if (newCorners is not null)
             {
                 bool shouldSnap = (SideFreedom is TransformSideFreedom.ScaleProportionally or TransformSideFreedom.Stretch) && Corners.IsSnappedToPixels;
                 Corners = shouldSnap ? TransformHelper.SnapToPixels((ShapeCorners)newCorners) : (ShapeCorners)newCorners;
             }
-            if (!originWasManuallyDragged)
-                origin = TransformHelper.OriginFromCorners(Corners);
+            if (!InternalState.OriginWasManuallyDragged)
+                InternalState = InternalState with { Origin = TransformHelper.OriginFromCorners(Corners) };
         }
         else if (capturedAnchor == Anchor.Origin)
         {
-            originWasManuallyDragged = true;
-            origin = originOnStartAnchorDrag + pos - mousePosOnStartAnchorDrag;
-            InvalidateVisual();
+            InternalState = InternalState with
+            {
+                OriginWasManuallyDragged = true,
+                Origin = originOnStartAnchorDrag + pos - mousePosOnStartAnchorDrag,
+            };
         }
     }
 
@@ -228,11 +308,14 @@ internal class TransformOverlay : Control
     private static void OnRequestedCorners(DependencyObject obj, DependencyPropertyChangedEventArgs args)
     {
         TransformOverlay overlay = (TransformOverlay)obj;
-        overlay.originWasManuallyDragged = false;
         overlay.Corners = (ShapeCorners)args.NewValue;
-        overlay.proportionalAngle1 = (overlay.Corners.BottomRight - overlay.Corners.TopLeft).Angle;
-        overlay.proportionalAngle2 = (overlay.Corners.TopRight - overlay.Corners.BottomLeft).Angle;
-        overlay.origin = TransformHelper.OriginFromCorners(overlay.Corners);
+        overlay.InternalState = new()
+        {
+            ProportionalAngle1 = (overlay.Corners.BottomRight - overlay.Corners.TopLeft).Angle,
+            ProportionalAngle2 = (overlay.Corners.TopRight - overlay.Corners.BottomLeft).Angle,
+            OriginWasManuallyDragged = false,
+            Origin = TransformHelper.OriginFromCorners(overlay.Corners),
+        };
     }
 
     private bool ReleaseAnchor()

+ 10 - 0
src/PixiEditorPrototype/CustomControls/TransformOverlay/TransformState.cs

@@ -0,0 +1,10 @@
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditorPrototype.CustomControls.TransformOverlay;
+internal struct TransformState
+{
+    public bool OriginWasManuallyDragged { get; set; }
+    public Vector2d Origin { get; set; }
+    public double ProportionalAngle1 { get; set; }
+    public double ProportionalAngle2 { get; set; }
+}

+ 4 - 1
src/PixiEditorPrototype/Models/ActionAccumulator.cs

@@ -74,7 +74,10 @@ internal class ActionAccumulator
                 bitmap.Unlock();
             }
 
-            document.ForceRefreshView();
+            foreach (var (_, value) in helpers.State.Viewports)
+            {
+                value.InvalidateVisual();
+            }
         }
 
         executing = false;

+ 15 - 10
src/PixiEditorPrototype/Models/DocumentUpdater.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib.DataHolders;
@@ -53,6 +54,9 @@ internal class DocumentUpdater
             case RefreshViewport_PassthroughAction info:
                 ProcessRefreshViewport(info);
                 break;
+            case RemoveViewport_PassthroughAction info:
+                ProcessRemoveViewport(info);
+                break;
             case StructureMemberMask_ChangeInfo info:
                 ProcessStructureMemberMask(info);
                 break;
@@ -85,26 +89,27 @@ internal class DocumentUpdater
 
     private void ProcessRefreshViewport(RefreshViewport_PassthroughAction info)
     {
-        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);
+        helper.State.Viewports[info.Location.GuidValue] = info.Location;
+    }
+
+    private void ProcessRemoveViewport(RemoveViewport_PassthroughAction info)
+    {
+        helper.State.Viewports.Remove(info.GuidValue);
     }
 
     private void ProcessSize(Size_ChangeInfo info)
     {
         var size = helper.Tracker.Document.Size;
+        Dictionary<ChunkResolution, WriteableBitmap> newBitmaps = new();
         foreach (var (res, surf) in doc.Surfaces)
         {
             surf.Dispose();
-            doc.Bitmaps[res] = CreateBitmap((Vector2i)(size * res.Multiplier()));
-            doc.Surfaces[res] = CreateSKSurface(doc.Bitmaps[res]);
+            newBitmaps[res] = CreateBitmap((Vector2i)(size * res.Multiplier()));
+            doc.Surfaces[res] = CreateSKSurface(newBitmaps[res]);
         }
 
+        doc.Bitmaps = newBitmaps;
+        doc.RaisePropertyChanged(nameof(doc.Bitmaps));
         doc.RaisePropertyChanged(nameof(doc.Width));
         doc.RaisePropertyChanged(nameof(doc.Height));
     }

+ 2 - 3
src/PixiEditorPrototype/Models/RefreshViewport_PassthroughAction.cs

@@ -1,7 +1,6 @@
-using System;
-using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
 namespace PixiEditorPrototype.Models;
 
-internal record class RefreshViewport_PassthroughAction(Guid GuidValue) : IAction, IChangeInfo;
+internal record class RefreshViewport_PassthroughAction(ViewportLocation Location) : IAction, IChangeInfo;

+ 6 - 0
src/PixiEditorPrototype/Models/RemoveViewport_PassthroughAction.cs

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

+ 2 - 18
src/PixiEditorPrototype/Models/ViewportLocation.cs

@@ -2,21 +2,5 @@
 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;
-        }
-    }
-}
+internal readonly record struct ViewportLocation
+    (double Angle, Vector2d Center, Vector2d RealDimensions, Vector2d Dimensions, ChunkResolution Resolution, Guid GuidValue, Action InvalidateVisual);

+ 71 - 0
src/PixiEditorPrototype/UserControls/Viewport/Viewport.xaml

@@ -0,0 +1,71 @@
+<UserControl x:Class="PixiEditorPrototype.UserControls.Viewport.Viewport"
+             x:ClassModifier="internal"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:local="clr-namespace:PixiEditorPrototype.UserControls.Viewport"
+             xmlns:zoombox="clr-namespace:PixiEditor.Zoombox;assembly=PixiEditor.Zoombox"
+             xmlns:to="clr-namespace:PixiEditorPrototype.CustomControls.TransformOverlay"
+             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
+             xmlns:conv="clr-namespace:PixiEditorPrototype.Converters"
+             mc:Ignorable="d"
+             x:Name="vpUc"
+             d:DesignHeight="450" d:DesignWidth="800">
+    <UserControl.Resources>
+        <conv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
+    </UserControl.Resources>
+    <Grid>
+        <zoombox:Zoombox x:Name="zoombox" UseTouchGestures="True"
+                             Center="{Binding Center, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+                             Angle="{Binding Angle, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+                             RealDimensions="{Binding RealDimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+                             Dimensions="{Binding Dimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+                             ZoomMode="{Binding ZoomMode, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}" 
+                             FlipX="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}"
+                             FlipY="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
+            <Border BorderThickness="1" Background="White" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center"
+                    DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
+                <Grid>
+                    <Image Focusable="True"
+                               Width="{Binding Document.Width}" Height="{Binding Document.Height}"
+                               Source="{Binding TargetBitmap}"
+                               RenderOptions.BitmapScalingMode="NearestNeighbor">
+                        <i:Interaction.Triggers>
+                            <i:EventTrigger EventName="MouseDown">
+                                <i:InvokeCommandAction Command="{Binding MouseDownCommand}" PassEventArgsToCommand="True"/>
+                            </i:EventTrigger>
+                            <i:EventTrigger EventName="MouseMove">
+                                <i:InvokeCommandAction Command="{Binding MouseMoveCommand}" PassEventArgsToCommand="True"/>
+                            </i:EventTrigger>
+                            <i:EventTrigger EventName="MouseUp">
+                                <i:InvokeCommandAction Command="{Binding MouseUpCommand}" PassEventArgsToCommand="True"/>
+                            </i:EventTrigger>
+                        </i:Interaction.Triggers>
+                    </Image>
+                    <to:TransformOverlay HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
+                                            Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={StaticResource BoolToVisibilityConverter}}"
+                                            Corners="{Binding Document.TransformViewModel.Corners, Mode=TwoWay}"
+                                            RequestedCorners="{Binding Document.TransformViewModel.RequestedCorners}"
+                                            CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"
+                                            SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
+                                            InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
+                                            ZoomboxScale="{Binding Zoombox.Scale}"/>
+                </Grid>
+            </Border>
+        </zoombox:Zoombox>
+        <Grid Focusable="False">
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="1*"/>
+                <ColumnDefinition Width="2*"/>
+                <ColumnDefinition Width="1*"/>
+            </Grid.ColumnDefinitions>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="1*"/>
+                <RowDefinition Height="2*"/>
+                <RowDefinition Height="1*"/>
+            </Grid.RowDefinitions>
+            <Border BorderBrush="Red" Grid.Row="1" Grid.Column="1" BorderThickness="1"/>
+        </Grid>
+    </Grid>
+</UserControl>

+ 225 - 0
src/PixiEditorPrototype/UserControls/Viewport/Viewport.xaml.cs

@@ -0,0 +1,225 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media.Imaging;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.Zoombox;
+using PixiEditorPrototype.Models;
+using PixiEditorPrototype.ViewModels;
+
+namespace PixiEditorPrototype.UserControls.Viewport;
+
+internal partial class Viewport : UserControl, INotifyPropertyChanged
+{
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    public static readonly DependencyProperty FlipXProperty =
+        DependencyProperty.Register(nameof(FlipX), typeof(bool), typeof(Viewport), new(false));
+
+    public static readonly DependencyProperty FlipYProperty =
+        DependencyProperty.Register(nameof(FlipY), typeof(bool), typeof(Viewport), new(false));
+
+    public static readonly DependencyProperty ZoomModeProperty =
+        DependencyProperty.Register(nameof(ZoomMode), typeof(ZoomboxMode), typeof(Viewport), new(ZoomboxMode.Normal));
+
+    public static readonly DependencyProperty DocumentProperty =
+        DependencyProperty.Register(nameof(Document), typeof(DocumentViewModel), typeof(Viewport), new(null, OnDocumentChange));
+
+    public static readonly DependencyProperty MouseDownCommandProperty =
+        DependencyProperty.Register(nameof(MouseDownCommand), typeof(ICommand), typeof(Viewport), new(null));
+
+    public static readonly DependencyProperty MouseMoveCommandProperty =
+        DependencyProperty.Register(nameof(MouseMoveCommand), typeof(ICommand), typeof(Viewport), new(null));
+
+    public static readonly DependencyProperty MouseUpCommandProperty =
+            DependencyProperty.Register(nameof(MouseUpCommand), typeof(ICommand), typeof(Viewport), new(null));
+
+    private static readonly DependencyProperty BitmapsProperty =
+        DependencyProperty.Register(nameof(Bitmaps), typeof(Dictionary<ChunkResolution, WriteableBitmap>), typeof(Viewport), new(null, OnBitmapsChange));
+
+    public Dictionary<ChunkResolution, WriteableBitmap>? Bitmaps
+    {
+        get => (Dictionary<ChunkResolution, WriteableBitmap>?)GetValue(BitmapsProperty);
+        set => SetValue(BitmapsProperty, value);
+    }
+
+    public ICommand? MouseDownCommand
+    {
+        get => (ICommand?)GetValue(MouseDownCommandProperty);
+        set => SetValue(MouseDownCommandProperty, value);
+    }
+
+    public ICommand? MouseMoveCommand
+    {
+        get => (ICommand?)GetValue(MouseMoveCommandProperty);
+        set => SetValue(MouseMoveCommandProperty, value);
+    }
+
+    public ICommand? MouseUpCommand
+    {
+        get => (ICommand?)GetValue(MouseUpCommandProperty);
+        set => SetValue(MouseUpCommandProperty, value);
+    }
+
+
+    public DocumentViewModel? Document
+    {
+        get => (DocumentViewModel)GetValue(DocumentProperty);
+        set => SetValue(DocumentProperty, value);
+    }
+
+    public ZoomboxMode ZoomMode
+    {
+        get => (ZoomboxMode)GetValue(ZoomModeProperty);
+        set => SetValue(ZoomModeProperty, value);
+    }
+
+    public bool FlipX
+    {
+        get => (bool)GetValue(FlipXProperty);
+        set => SetValue(FlipXProperty, value);
+    }
+
+    public bool FlipY
+    {
+        get => (bool)GetValue(FlipYProperty);
+        set => SetValue(FlipYProperty, value);
+    }
+
+    private double angle = 0;
+    public double Angle
+    {
+        get => angle;
+        set
+        {
+            angle = value;
+            PropertyChanged?.Invoke(this, new(nameof(Angle)));
+            Document?.AddOrUpdateViewport(GetLocation());
+        }
+    }
+
+    private Vector2d center = new(32, 32);
+    public Vector2d Center
+    {
+        get => center;
+        set
+        {
+            center = value;
+            PropertyChanged?.Invoke(this, new(nameof(Center)));
+            Document?.AddOrUpdateViewport(GetLocation());
+        }
+    }
+
+    private Vector2d realDimensions = new(double.MaxValue, double.MaxValue);
+    public Vector2d RealDimensions
+    {
+        get => realDimensions;
+        set
+        {
+            var oldRes = CalculateResolution();
+            realDimensions = value;
+            var newRes = CalculateResolution();
+
+            PropertyChanged?.Invoke(this, new(nameof(RealDimensions)));
+            Document?.AddOrUpdateViewport(GetLocation());
+
+            if (oldRes != newRes)
+                PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+        }
+    }
+
+    private Vector2d dimensions = new(64, 64);
+    public Vector2d Dimensions
+    {
+        get => dimensions;
+        set
+        {
+            var oldRes = CalculateResolution();
+            dimensions = value;
+            var newRes = CalculateResolution();
+
+            PropertyChanged?.Invoke(this, new(nameof(Dimensions)));
+            Document?.AddOrUpdateViewport(GetLocation());
+
+            if (oldRes != newRes)
+                PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));
+        }
+    }
+
+    public WriteableBitmap? TargetBitmap
+    {
+        get
+        {
+            return Document?.Bitmaps.TryGetValue(CalculateResolution(), out var value) == true ? value : null;
+        }
+    }
+
+    public Zoombox Zoombox => zoombox;
+
+    public Guid GuidValue { get; } = Guid.NewGuid();
+
+    public Viewport()
+    {
+        InitializeComponent();
+
+        Binding binding = new Binding();
+        binding.Source = this;
+        binding.Path = new PropertyPath("Document.Bitmaps");
+        SetBinding(BitmapsProperty, binding);
+
+        Loaded += OnLoad;
+        Unloaded += OnUnload;
+    }
+
+    private Image? GetImage() => (Image?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[0];
+    private void ForceRefreshFinalImage()
+    {
+        GetImage()?.InvalidateVisual();
+    }
+
+    private void OnUnload(object sender, RoutedEventArgs e)
+    {
+        Document?.RemoveViewport(GuidValue);
+    }
+
+    private void OnLoad(object sender, RoutedEventArgs e)
+    {
+        Document?.AddOrUpdateViewport(GetLocation());
+    }
+
+    private static void OnDocumentChange(DependencyObject viewportObj, DependencyPropertyChangedEventArgs args)
+    {
+        var oldDoc = (DocumentViewModel?)args.OldValue;
+        var newDoc = (DocumentViewModel?)args.NewValue;
+        var viewport = (Viewport)viewportObj;
+        oldDoc?.RemoveViewport(viewport.GuidValue);
+        newDoc?.AddOrUpdateViewport(viewport.GetLocation());
+    }
+
+    private static void OnBitmapsChange(DependencyObject viewportObj, DependencyPropertyChangedEventArgs args)
+    {
+        ((Viewport)viewportObj).PropertyChanged?.Invoke(viewportObj, new(nameof(TargetBitmap)));
+    }
+
+    private ChunkResolution CalculateResolution()
+    {
+        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;
+    }
+
+    private ViewportLocation GetLocation()
+    {
+        return new(Angle, Center, RealDimensions / 2, Dimensions / 2, CalculateResolution(), GuidValue, ForceRefreshFinalImage);
+    }
+}

+ 11 - 0
src/PixiEditorPrototype/ViewModels/DocumentTransformViewModel.cs

@@ -6,6 +6,17 @@ using PixiEditorPrototype.CustomControls.TransformOverlay;
 namespace PixiEditorPrototype.ViewModels;
 internal class DocumentTransformViewModel : INotifyPropertyChanged
 {
+    private TransformState internalState;
+    public TransformState InternalState
+    {
+        get => internalState;
+        set
+        {
+            internalState = value;
+            PropertyChanged?.Invoke(this, new(nameof(InternalState)));
+        }
+    }
+
     private TransformCornerFreedom cornerFreedom;
     public TransformCornerFreedom CornerFreedom
     {

+ 12 - 29
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -35,6 +35,14 @@ internal class DocumentViewModel : INotifyPropertyChanged
         }
     }
 
+    public Dictionary<ChunkResolution, WriteableBitmap> Bitmaps { get; set; } = new()
+    {
+        [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 event PropertyChangedEventHandler? PropertyChanged;
 
     public void RaisePropertyChanged(string name)
@@ -54,7 +62,6 @@ internal class DocumentViewModel : INotifyPropertyChanged
     public RelayCommand? ResizeCanvasCommand { get; }
     public RelayCommand? CombineCommand { get; }
     public RelayCommand? ClearHistoryCommand { get; }
-    public RelayCommand? MoveViewportCommand { get; }
     public RelayCommand? CreateMaskCommand { get; }
     public RelayCommand? DeleteMaskCommand { get; }
     public RelayCommand? ToggleLockTransparencyCommand { get; }
@@ -65,14 +72,6 @@ internal class DocumentViewModel : INotifyPropertyChanged
     public int Height => Helpers.Tracker.Document.Size.Y;
     public Guid GuidValue { get; } = Guid.NewGuid();
 
-    public Dictionary<ChunkResolution, WriteableBitmap> Bitmaps { get; set; } = new()
-    {
-        [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 Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
 
     public DocumentStateHandler StateHandler { get; }
@@ -105,7 +104,6 @@ internal class DocumentViewModel : INotifyPropertyChanged
         ResizeCanvasCommand = new RelayCommand(ResizeCanvas);
         CombineCommand = new RelayCommand(Combine);
         ClearHistoryCommand = new RelayCommand(ClearHistory);
-        MoveViewportCommand = new RelayCommand(MoveViewport);
         CreateMaskCommand = new RelayCommand(CreateMask);
         DeleteMaskCommand = new RelayCommand(DeleteMask);
         ToggleLockTransparencyCommand = new RelayCommand(ToggleLockTransparency);
@@ -220,24 +218,14 @@ internal class DocumentViewModel : INotifyPropertyChanged
         }
     }
 
-    public void ForceRefreshView()
-    {
-        owner.View?.ForceRefreshFinalImage();
-    }
-
-    public void UpdateViewportResolution(Guid viewportGuid, ChunkResolution resolution)
+    public void AddOrUpdateViewport(ViewportLocation location)
     {
-        owner.UpdateViewportResolution(viewportGuid, resolution);
+        Helpers.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(location));
     }
 
-    public ViewportLocation? GetViewport(Guid viewportGuid)
+    public void RemoveViewport(Guid viewportGuid)
     {
-        return owner.GetViewport(viewportGuid);
-    }
-
-    public void RefreshViewport(Guid viewportGuid)
-    {
-        Helpers.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(viewportGuid));
+        Helpers.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
     }
 
     private void PasteImage(object? args)
@@ -330,11 +318,6 @@ internal class DocumentViewModel : INotifyPropertyChanged
         Helpers.ActionAccumulator.AddActions(new ChangeBoundary_Action());
     }
 
-    private void MoveViewport(object? param)
-    {
-        Helpers.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(Guid.Empty));
-    }
-
     private void ClearHistory(object? param)
     {
         Helpers.ActionAccumulator.AddActions(new DeleteRecordedChanges_Action());

+ 0 - 22
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -6,15 +6,12 @@ using System.Windows.Media;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Zoombox;
 using PixiEditorPrototype.Models;
-using PixiEditorPrototype.Views;
 using SkiaSharp;
 
 namespace PixiEditorPrototype.ViewModels;
 
 internal class ViewModelMain : INotifyPropertyChanged
 {
-    public IMainView? View { get; set; }
-
     public DocumentViewModel? ActiveDocument => GetDocumentByGuid(activeDocumentGuid);
 
     public RelayCommand? MouseDownCommand { get; }
@@ -61,8 +58,6 @@ internal class ViewModelMain : INotifyPropertyChanged
 
     public ZoomboxMode ZoomboxMode { get; set; }
 
-    public ViewportViewModel MainViewport { get; }
-
     private Dictionary<Guid, DocumentViewModel> documents = new();
     private Guid activeDocumentGuid;
 
@@ -85,16 +80,6 @@ internal class ViewModelMain : INotifyPropertyChanged
         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)
@@ -102,13 +87,6 @@ internal class ViewModelMain : INotifyPropertyChanged
         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)
     {
         if (ActiveDocument is null || ZoomboxMode != ZoomboxMode.Normal || ActiveDocument.TransformViewModel.TransformActive)

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

@@ -1,94 +0,0 @@
-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;
-        }
-    }
-}

+ 0 - 6
src/PixiEditorPrototype/Views/IMainView.cs

@@ -1,6 +0,0 @@
-namespace PixiEditorPrototype.Views;
-
-internal interface IMainView
-{
-    void ForceRefreshFinalImage();
-}

+ 29 - 55
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -10,6 +10,7 @@
         xmlns:pe="clr-namespace:PixiEditorPrototype"
         xmlns:controls="clr-namespace:PixiEditorPrototype.CustomControls"
         xmlns:to="clr-namespace:PixiEditorPrototype.CustomControls.TransformOverlay"
+        xmlns:vp="clr-namespace:PixiEditorPrototype.UserControls.Viewport"
         xmlns:zoombox="clr-namespace:PixiEditor.Zoombox;assembly=PixiEditor.Zoombox"
         xmlns:models="clr-namespace:PixiEditorPrototype.Models"
         xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
@@ -166,62 +167,35 @@
                 <CheckBox x:Name="flipYCheckbox" Margin="5, 0">Flip Y</CheckBox>
             </StackPanel>
         </Border>
-
         <Grid>
-            <zoombox:Zoombox x:Name="zoombox" UseTouchGestures="True"
-                             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>
-                    <i:EventTrigger EventName="ViewportMoved">
-                        <i:InvokeCommandAction Command="{Binding ActiveDocument.MoveViewportCommand}" PassEventArgsToCommand="True"/>
-                    </i:EventTrigger>
-                </i:Interaction.Triggers>
-                <Border BorderThickness="1" Background="White" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center">
-                    <Grid>
-                        <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"/>
-                                </i:EventTrigger>
-                                <i:EventTrigger EventName="MouseMove">
-                                    <i:InvokeCommandAction Command="{Binding MouseMoveCommand}" PassEventArgsToCommand="True"/>
-                                </i:EventTrigger>
-                                <i:EventTrigger EventName="MouseUp">
-                                    <i:InvokeCommandAction Command="{Binding MouseUpCommand}" PassEventArgsToCommand="True"/>
-                                </i:EventTrigger>
-                            </i:Interaction.Triggers>
-                        </Image>
-                        <to:TransformOverlay HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
-                                            Visibility="{Binding ActiveDocument.TransformViewModel.TransformActive, Converter={StaticResource BoolToVisibilityConverter}}"
-                                            Corners="{Binding ActiveDocument.TransformViewModel.Corners, Mode=TwoWay}"
-                                            RequestedCorners="{Binding ActiveDocument.TransformViewModel.RequestedCorners}"
-                                            CornerFreedom="{Binding ActiveDocument.TransformViewModel.CornerFreedom}"
-                                            SideFreedom="{Binding ActiveDocument.TransformViewModel.SideFreedom}"
-                                            ZoomboxScale="{Binding RelativeSource={RelativeSource AncestorType=zoombox:Zoombox}, Path=Scale}"/>
-                    </Grid>
-                </Border>
-            </zoombox:Zoombox>
-            <Grid Focusable="False">
-                <Grid.ColumnDefinitions>
-                    <ColumnDefinition Width="1*"/>
-                    <ColumnDefinition Width="2*"/>
-                    <ColumnDefinition Width="1*"/>
-                </Grid.ColumnDefinitions>
-                <Grid.RowDefinitions>
-                    <RowDefinition Height="1*"/>
-                    <RowDefinition Height="2*"/>
-                    <RowDefinition Height="1*"/>
-                </Grid.RowDefinitions>
-                <Border BorderBrush="Red" Grid.Row="1" Grid.Column="1" BorderThickness="1"/>
-            </Grid>
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition/>
+                <ColumnDefinition/>
+            </Grid.ColumnDefinitions>
+            <Border BorderThickness="1" BorderBrush="Black" Margin="5">
+                <vp:Viewport
+                    Document="{Binding ActiveDocument}"
+                    FlipX="{Binding ElementName=flipXCheckbox, Path=IsChecked}"
+                    FlipY="{Binding ElementName=flipYCheckbox, Path=IsChecked}"
+                    ZoomMode="{Binding ZoomboxMode, Mode=TwoWay}"
+                    MouseDownCommand="{Binding MouseDownCommand}"
+                    MouseMoveCommand="{Binding MouseMoveCommand}"
+                    MouseUpCommand="{Binding MouseUpCommand}"
+                    Tag="First"
+                    />
+            </Border>
+            <Border BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="1">
+                <vp:Viewport
+                    Document="{Binding ActiveDocument}"
+                    FlipX="{Binding ElementName=flipXCheckbox, Path=IsChecked}"
+                    FlipY="{Binding ElementName=flipYCheckbox, Path=IsChecked}"
+                    ZoomMode="{Binding ZoomboxMode, Mode=TwoWay}"
+                    MouseDownCommand="{Binding MouseDownCommand}"
+                    MouseMoveCommand="{Binding MouseMoveCommand}"
+                    MouseUpCommand="{Binding MouseUpCommand}"
+                    Tag="Second"
+                    />
+            </Border>
         </Grid>
     </DockPanel>
 </Window>

+ 1 - 11
src/PixiEditorPrototype/Views/MainWindow.xaml.cs

@@ -1,21 +1,11 @@
 using System.Windows;
-using System.Windows.Controls;
-using PixiEditorPrototype.ViewModels;
 
 namespace PixiEditorPrototype.Views;
 
-internal partial class MainWindow : Window, IMainView
+internal partial class MainWindow : Window
 {
     public MainWindow()
     {
         InitializeComponent();
-        ((ViewModelMain)DataContext).View = this;
-    }
-
-    private Image? GetImage() => (Image?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[0];
-
-    public void ForceRefreshFinalImage()
-    {
-        GetImage()?.InvalidateVisual();
     }
 }