Ver código fonte

Scaling to center works

flabbet 8 meses atrás
pai
commit
63683f27ad

+ 47 - 17
src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -19,10 +19,11 @@ namespace PixiEditor.ViewModels.Document.TransformOverlays;
 internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 {
     private DocumentViewModel document;
-    
+
     private TransformOverlayUndoStack<(ShapeCorners, TransformState)>? undoStack = null;
 
     private TransformState internalState;
+
     public TransformState InternalState
     {
         get => internalState;
@@ -30,6 +31,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private TransformCornerFreedom cornerFreedom;
+
     public TransformCornerFreedom CornerFreedom
     {
         get => cornerFreedom;
@@ -37,13 +39,23 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private TransformSideFreedom sideFreedom;
+
     public TransformSideFreedom SideFreedom
     {
         get => sideFreedom;
         set => SetProperty(ref sideFreedom, value);
     }
 
+    private bool scaleFromCenter;
+
+    public bool ScaleFromCenter
+    {
+        get => scaleFromCenter;
+        set => SetProperty(ref scaleFromCenter, value);
+    }
+
     private bool lockRotation;
+
     public bool LockRotation
     {
         get => lockRotation;
@@ -51,6 +63,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private bool snapToAngles;
+
     public bool SnapToAngles
     {
         get => snapToAngles;
@@ -58,6 +71,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private bool transformActive;
+
     public bool TransformActive
     {
         get => transformActive;
@@ -70,7 +84,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
             if (value)
             {
-                document.ActionDisplays[nameof(DocumentTransformViewModel)] = new LocalizedString($"TRANSFORM_ACTION_DISPLAY_{activeTransformMode.GetDescription()}");
+                document.ActionDisplays[nameof(DocumentTransformViewModel)] =
+                    new LocalizedString($"TRANSFORM_ACTION_DISPLAY_{activeTransformMode.GetDescription()}");
             }
             else
             {
@@ -80,6 +95,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private bool showTransformControls;
+
     public bool ShowTransformControls
     {
         get => showTransformControls;
@@ -89,6 +105,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     public event Action<MouseOnCanvasEventArgs>? PassthroughPointerPressed;
 
     private bool coverWholeScreen;
+
     public bool CoverWholeScreen
     {
         get => coverWholeScreen;
@@ -96,6 +113,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private ShapeCorners corners;
+
     public ShapeCorners Corners
     {
         get => corners;
@@ -112,25 +130,27 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     {
         get => showHandles;
         set => SetProperty(ref showHandles, value);
-    } 
-    
+    }
+
     private bool isSizeBoxEnabled;
 
     public bool IsSizeBoxEnabled
     {
         get => isSizeBoxEnabled;
         set => SetProperty(ref isSizeBoxEnabled, value);
-    } 
+    }
 
     private bool enableSnapping = true;
+
     public bool EnableSnapping
     {
         get => enableSnapping;
         set => SetProperty(ref enableSnapping, value);
     }
-    
-    
+
+
     private ExecutionTrigger<ShapeCorners> requestedCornersExecutor;
+
     public ExecutionTrigger<ShapeCorners> RequestCornersExecutor
     {
         get => requestedCornersExecutor;
@@ -138,18 +158,21 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     private ICommand? actionCompletedCommand = null;
+
     public ICommand? ActionCompletedCommand
     {
         get => actionCompletedCommand;
         set => SetProperty(ref actionCompletedCommand, value);
     }
 
-    private RelayCommand<MouseOnCanvasEventArgs>? passThroughPointerPressedCommand; 
+    private RelayCommand<MouseOnCanvasEventArgs>? passThroughPointerPressedCommand;
+
     public RelayCommand<MouseOnCanvasEventArgs> PassThroughPointerPressedCommand
     {
         get
         {
-            return passThroughPointerPressedCommand ??= new RelayCommand<MouseOnCanvasEventArgs>(x => PassthroughPointerPressed?.Invoke(x));
+            return passThroughPointerPressedCommand ??=
+                new RelayCommand<MouseOnCanvasEventArgs>(x => PassthroughPointerPressed?.Invoke(x));
         }
     }
 
@@ -166,7 +189,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
                 return;
 
             var lastState = undoStack.PeekCurrent();
-            if (lastState is not null && lastState.Value.Item1.AlmostEquals(Corners) && lastState.Value.Item2.AlmostEquals(InternalState))
+            if (lastState is not null && lastState.Value.Item1.AlmostEquals(Corners) &&
+                lastState.Value.Item2.AlmostEquals(InternalState))
                 return;
 
             undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Move);
@@ -208,7 +232,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         return true;
     }
 
-    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0; 
+    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0;
     public bool HasRedo => undoStack is not null && undoStack.RedoCount > 0;
 
     public void HideTransform()
@@ -221,7 +245,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         ShowTransformControls = false;
     }
 
-    public void ShowTransform(DocumentTransformMode mode, bool coverWholeScreen, ShapeCorners initPos, bool showApplyButton)
+    public void ShowTransform(DocumentTransformMode mode, bool coverWholeScreen, ShapeCorners initPos,
+        bool showApplyButton)
     {
         if (undoStack is not null || initPos.IsPartiallyDegenerate)
             return;
@@ -253,21 +278,24 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
             requestedCornerFreedom = TransformCornerFreedom.ScaleProportionally;
             requestedSideFreedom = TransformSideFreedom.ScaleProportionally;
         }
-        else if (isCtrlDown)
+        else if (isAltDown)
         {
             requestedCornerFreedom = TransformCornerFreedom.Free;
             requestedSideFreedom = TransformSideFreedom.Free;
         }
-        else if (isAltDown)
+        /*else if (isAltDown)
         {
+        TODO: Add shear to the transform overlay
             requestedSideFreedom = TransformSideFreedom.Shear;
-        }
+        }*/
         else
         {
             requestedCornerFreedom = TransformCornerFreedom.Scale;
             requestedSideFreedom = TransformSideFreedom.Stretch;
         }
 
+        ScaleFromCenter = isCtrlDown;
+
         switch (activeTransformMode)
         {
             case DocumentTransformMode.Scale_Rotate_Shear_Perspective:
@@ -290,6 +318,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
                 break;
         }
     }
-    
-    public override string ToString() => !TransformActive ? "Not active" : $"Transform Mode: {activeTransformMode}; Corner Freedom: {CornerFreedom}; Side Freedom: {SideFreedom}";
+
+    public override string ToString() => !TransformActive
+        ? "Not active"
+        : $"Transform Mode: {activeTransformMode}; Corner Freedom: {CornerFreedom}; Side Freedom: {SideFreedom}";
 }

+ 6 - 0
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -326,6 +326,11 @@ internal class ViewportOverlays
         };
 
         Binding zoomboxAngleBinding = new() { Source = Viewport, Path = "Zoombox.Angle", Mode = BindingMode.OneWay };
+        
+        Binding scaleFromCenterBinding = new()
+        {
+            Source = Viewport, Path = "Document.TransformViewModel.ScaleFromCenter", Mode = BindingMode.OneWay
+        };
 
         transformOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         transformOverlay.Bind(TransformOverlay.ActionCompletedProperty, actionCompletedBinding);
@@ -342,6 +347,7 @@ internal class ViewportOverlays
         transformOverlay.Bind(TransformOverlay.ZoomboxAngleProperty, zoomboxAngleBinding);
         transformOverlay.Bind(TransformOverlay.ShowHandlesProperty, showHandlesBinding);
         transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
+        transformOverlay.Bind(TransformOverlay.ScaleFromCenterProperty, scaleFromCenterBinding);
     }
     
     private void BindVectorPathOverlay()

+ 13 - 1
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -160,6 +160,15 @@ internal class TransformOverlay : Overlay
         set => SetValue(IsSizeBoxEnabledProperty, value);
     }
 
+    public static readonly StyledProperty<bool> ScaleFromCenterProperty = AvaloniaProperty.Register<TransformOverlay, bool>(
+        nameof(ScaleFromCenter));
+
+    public bool ScaleFromCenter
+    {
+        get => GetValue(ScaleFromCenterProperty);
+        set => SetValue(ScaleFromCenterProperty, value);
+    }
+
     static TransformOverlay()
     {
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
@@ -754,7 +763,9 @@ internal class TransformOverlay : Overlay
 
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
             ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1,
-                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos, SnappingController,
+                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
+                ScaleFromCenter,
+                SnappingController,
                 out string snapX, out string snapY);
 
             HighlightSnappedAxis(snapX, snapY);
@@ -836,6 +847,7 @@ internal class TransformOverlay : Overlay
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromSide
             ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1,
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos + snapped.Delta,
+                ScaleFromCenter,
                 SnappingController, out string snapX, out string snapY);
 
             string finalSnapX = snapped.SnapAxisXName ?? snapX;

+ 57 - 7
src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs

@@ -11,7 +11,7 @@ internal static class TransformUpdateHelper
 
     public static ShapeCorners? UpdateShapeFromCorner
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
-        VecD desiredPos,
+        VecD desiredPos, bool scaleFromCenter,
         SnappingController? snappingController, out string snapX, out string snapY)
     {
         if (!TransformHelper.IsCorner(targetCorner))
@@ -79,6 +79,7 @@ internal static class TransformUpdateHelper
             // find by how much move each corner
             VecD delta = (desiredPos - targetPos).Rotate(-angle);
             VecD leftNeighDelta, rightNeighDelta;
+
             if (corners.IsPartiallyDegenerate)
             {
                 // handle cases where we'd need to scale by infinity
@@ -97,6 +98,24 @@ internal static class TransformUpdateHelper
                 rightNeighDelta = newRightPos.Value - rightNeighTrans;
             }
 
+            VecD oppositeDelta = VecD.Zero;
+            if (scaleFromCenter)
+            {
+                oppositeDelta = -delta;
+                bool swapped = leftNeighbor is Anchor.TopLeft or Anchor.BottomRight;
+
+                if (swapped)
+                {
+                    leftNeighDelta += new VecD(0, oppositeDelta.Y);
+                    rightNeighDelta += new VecD(oppositeDelta.X, 0);
+                }
+                else
+                {
+                    leftNeighDelta += new VecD(oppositeDelta.X, 0);
+                    rightNeighDelta += new VecD(0, oppositeDelta.Y);
+                }
+            }
+
             // handle cases where the transform overlay is squished into a line or a single point
             bool squishedWithLeft = leftNeighTrans.TaxicabLength < epsilon;
             bool squishedWithRight = rightNeighTrans.TaxicabLength < epsilon;
@@ -121,6 +140,8 @@ internal static class TransformUpdateHelper
                 (leftNeighTrans + leftNeighDelta).Rotate(angle) + oppositePos);
             corners = TransformHelper.UpdateCorner(corners, rightNeighbor,
                 (rightNeighTrans + rightNeighDelta).Rotate(angle) + oppositePos);
+            corners = TransformHelper.UpdateCorner(corners, opposite,
+                (oppositeDelta).Rotate(angle) + oppositePos);
 
             if (!corners.IsLegal)
                 return null;
@@ -151,7 +172,7 @@ internal static class TransformUpdateHelper
 
     public static ShapeCorners? UpdateShapeFromSide
     (Anchor targetSide, TransformSideFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
-        VecD desiredPos, SnappingController? snappingController, out string snapX, out string snapY)
+        VecD desiredPos, bool scaleFromCenter, SnappingController? snappingController, out string snapX, out string snapY)
     {
         if (!TransformHelper.IsSide(targetSide))
             throw new ArgumentException($"{targetSide} is not a side");
@@ -171,7 +192,7 @@ internal static class TransformUpdateHelper
 
             VecD direction = targetPos - oppositePos;
             direction = VecD.FromAngleAndLength(direction.Angle, 1 / direction.Length);
-            
+
             if (snappingController is not null)
             {
                 desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
@@ -203,10 +224,24 @@ internal static class TransformUpdateHelper
                     center + VecD.FromAngleAndLength(leftAngle, 1));
                 var updRightCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta,
                     center, center + VecD.FromAngleAndLength(rightAngle, 1));
-                var updLeftOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
-                    center + VecD.FromAngleAndLength(rightAngle, 1));
-                var updRightOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
-                    center + VecD.FromAngleAndLength(leftAngle, 1));
+
+                VecD? updLeftOppCorn = null, updRightOppCorn = null;
+                if (!scaleFromCenter)
+                {
+                    updLeftOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
+                        center + VecD.FromAngleAndLength(rightAngle, 1));
+                    updRightOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
+                        center + VecD.FromAngleAndLength(leftAngle, 1));
+                }
+                else if(updLeftCorn is not null && updRightCorn is not null)
+                {
+                    // Mirror the corners across the center
+                    VecD mirrorLeftOppCorn = 2 * center - ((VecD)updRightCorn + delta);
+                    VecD mirrorRightOppCorn = 2 * center - ((VecD)updLeftCorn + delta);
+
+                    updLeftOppCorn = mirrorLeftOppCorn;
+                    updRightOppCorn = mirrorRightOppCorn; 
+                }
 
                 if (updLeftCorn is null || updRightCorn is null || updLeftOppCorn is null || updRightOppCorn is null)
                     goto fallback;
@@ -260,9 +295,24 @@ internal static class TransformUpdateHelper
             }
 
             var delta = desiredPos - targetPos;
+            
             var newCorners = TransformHelper.UpdateCorner(corners, leftCorner, leftCornerPos + delta);
             newCorners = TransformHelper.UpdateCorner(newCorners, rightCorner, rightCornerPos + delta);
 
+            if (scaleFromCenter)
+            {
+                VecD oppositeDelta = -delta;
+                Anchor leftCornerOpp = TransformHelper.GetOpposite(leftCorner);
+                Anchor rightCornerOpp = TransformHelper.GetOpposite(rightCorner);
+                
+                var leftCornerOppPos = TransformHelper.GetAnchorPosition(corners, leftCornerOpp);
+                var rightCornerOppPos = TransformHelper.GetAnchorPosition(corners, rightCornerOpp);
+                
+                newCorners = TransformHelper.UpdateCorner(newCorners, leftCornerOpp, leftCornerOppPos + oppositeDelta);
+                newCorners = TransformHelper.UpdateCorner(newCorners, rightCornerOpp, rightCornerOppPos + oppositeDelta);
+            }
+
+
             return newCorners.IsLegal ? newCorners : null;
         }