Browse Source

Merge branch 'master' into select-layer-move-tool

flabbet 9 months ago
parent
commit
15983b1804
39 changed files with 547 additions and 145 deletions
  1. 1 1
      src/PixiDocks
  2. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  3. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  4. 12 9
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  5. 1 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  6. 7 5
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  7. 4 3
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  8. 5 1
      src/PixiEditor/Data/Localization/Languages/en.json
  9. 1 0
      src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs
  10. 3 1
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  11. 72 20
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  12. 7 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs
  13. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  14. 1 0
      src/PixiEditor/Models/Handlers/IDocument.cs
  15. 4 0
      src/PixiEditor/Models/Handlers/ILineOverlayHandler.cs
  16. 3 0
      src/PixiEditor/Models/Handlers/ITransformHandler.cs
  17. 3 2
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  18. 5 12
      src/PixiEditor/ViewModels/Document/SnappingViewModel.cs
  19. 19 0
      src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs
  20. 17 1
      src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs
  21. 1 1
      src/PixiEditor/ViewModels/Menu/MenuBuilders/ToggleGridLinesMenuBuilder.cs
  22. 43 0
      src/PixiEditor/ViewModels/Menu/MenuBuilders/ToggleHighResPreviewMenuBuilder.cs
  23. 43 0
      src/PixiEditor/ViewModels/Menu/MenuBuilders/ToggleSnappingMenuBuilder.cs
  24. 35 3
      src/PixiEditor/ViewModels/SubViewModels/ViewOptionsViewModel.cs
  25. 24 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/EnumSettingViewModel.cs
  26. 13 4
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs
  27. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs
  28. 12 7
      src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs
  29. 9 1
      src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs
  30. 2 0
      src/PixiEditor/Views/Dock/DocumentTemplate.axaml
  31. 13 5
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  32. 7 1
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  33. 24 0
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  34. 2 1
      src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShape.cs
  35. 22 5
      src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs
  36. 35 5
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  37. 3 7
      src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  38. 81 46
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  39. 4 2
      src/PixiEditor/Views/Rendering/Scene.cs

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit e270d65878607f16f10c27c604b6e054d54e2fb5
+Subproject commit f9bdb75115d1152a5126706bc8dc2098f72decde

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -15,6 +15,7 @@ namespace PixiEditor.ChangeableDocument.Changeables;
 
 internal class Document : IChangeable, IReadOnlyDocument
 {
+    public Guid DocumentId { get; } = Guid.NewGuid();
     IReadOnlyNodeGraph IReadOnlyDocument.NodeGraph => NodeGraph;
     IReadOnlySelection IReadOnlyDocument.Selection => Selection;
     IReadOnlyAnimationData IReadOnlyDocument.AnimationData => AnimationData;

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -10,6 +10,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 public interface IReadOnlyDocument : IDisposable
 {    
+    public Guid DocumentId { get; }
     /// <summary>
     /// The root folder of the document
     /// </summary>

+ 12 - 9
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -197,7 +197,8 @@ public class DocumentChangeTracker : IDisposable
         undoStack.Clear();
     }
 
-    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessMakeChangeAction(IMakeChangeAction act)
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessMakeChangeAction(IMakeChangeAction act,
+        ActionSource source)
     {
         if (activeUpdateableChange is not null)
         {
@@ -216,13 +217,14 @@ public class DocumentChangeTracker : IDisposable
 
         var info = change.Apply(document, true, out bool ignoreInUndo);
         if (!ignoreInUndo)
-            AddToUndo(change, ActionSource.User);
+            AddToUndo(change, source);
         else
             change.Dispose();
         return info;
     }
 
-    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessStartOrUpdateChangeAction(IStartOrUpdateChangeAction act)
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessStartOrUpdateChangeAction(IStartOrUpdateChangeAction act,
+        ActionSource source)
     {
         if (activeUpdateableChange is null)
         {
@@ -237,7 +239,7 @@ public class DocumentChangeTracker : IDisposable
             {
                 var applyInfo = activeUpdateableChange.Apply(document, false, out bool ignoreInUndo);
                 if (!ignoreInUndo)
-                    AddToUndo(activeUpdateableChange, ActionSource.User);
+                    AddToUndo(activeUpdateableChange, source);
                 else
                     activeUpdateableChange.Dispose();
 
@@ -287,7 +289,8 @@ public class DocumentChangeTracker : IDisposable
         return false;
     }
 
-    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessEndChangeAction(IEndChangeAction act)
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessEndChangeAction(IEndChangeAction act,
+        ActionSource source)
     {
         if (activeUpdateableChange is null)
         {
@@ -304,7 +307,7 @@ public class DocumentChangeTracker : IDisposable
 
         var info = activeUpdateableChange.Apply(document, true, out bool ignoreInUndo);
         if (!ignoreInUndo)
-            AddToUndo(activeUpdateableChange, ActionSource.User);
+            AddToUndo(activeUpdateableChange, source);
         else
             activeUpdateableChange.Dispose();
         activeUpdateableChange = null;
@@ -326,13 +329,13 @@ public class DocumentChangeTracker : IDisposable
             switch (action.Item2)
             {
                 case IMakeChangeAction act:
-                    AddInfo(ProcessMakeChangeAction(act));
+                    AddInfo(ProcessMakeChangeAction(act, action.Item1));
                     break;
                 case IStartOrUpdateChangeAction act:
-                    AddInfo(ProcessStartOrUpdateChangeAction(act));
+                    AddInfo(ProcessStartOrUpdateChangeAction(act, action.Item1));
                     break;
                 case IEndChangeAction act:
-                    AddInfo(ProcessEndChangeAction(act));
+                    AddInfo(ProcessEndChangeAction(act, action.Item1));
                     break;
                 case Undo_Action:
                     AddInfo(Undo());

+ 1 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -57,6 +57,7 @@
             <system:String x:Key="icon-folder">&#xE92D;</system:String>
             <system:String x:Key="icon-globe">&#xE92E;</system:String>
             <system:String x:Key="icon-grid">&#xE92F;</system:String>
+            <system:String x:Key="icon-gridlines">&#xE941;</system:String>
             <system:String x:Key="icon-home">&#xE930;</system:String>
             <system:String x:Key="icon-image-90">&#xE931;</system:String>
             <system:String x:Key="icon-image">&#xE932;</system:String>

+ 7 - 5
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs

@@ -60,7 +60,8 @@ public static class PixiPerfectIcons
     public const string FolderPlus = "\ue92c";
     public const string Folder = "\ue92d";
     public const string Globe = "\ue92e";
-    public const string Grid = "\uE941";
+    public const string Grid = "\uE92F";
+    public const string GridLines = "\uE941";
     public const string Home = "\ue930";
     public const string RotateImageMinus90 = "\ue931";
     public const string Image = "\ue932";
@@ -130,10 +131,11 @@ public static class PixiPerfectIcons
     public const string Reset = "R"; // TODO: Create a reset icon
     public const string ToggleLayerVisible = "\u25a1;"; // TODO: Create a toggle layer visible icon
     public const string ToggleMask = "\u25a1;"; // TODO: Create a toggle mask icon
-    public static string Pen => "\uE971";
-    public static string LowResCircle => "\uE986";
-    public static string LowResSquare => "\uE988";
-    public static string LowResLine => "\uE989";
+    public const string Pen = "\uE971";
+    public const string LowResCircle = "\uE986";
+    public const string Snapping = "\ue987";
+    public const string LowResSquare = "\uE988";
+    public const string LowResLine = "\uE989";
 
     public static Stream GetFontStream()
     {

+ 4 - 3
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -41,7 +41,8 @@
         "Settings": {
           "AntiAliasing": true,
           "ExposeHardness": true,
-          "ExposeSpacing": true
+          "ExposeSpacing": true,
+          "BrushShapeSetting": "CircleSmooth" 
         }
       },
       "Select",
@@ -84,11 +85,11 @@
         "Settings": {
           "AntiAliasing": true,
           "ExposeHardness": true,
-          "ExposeSpacing": true
+          "ExposeSpacing": true,
+          "BrushShapeSetting": "CircleSmooth"
         }
       },
       "ColorPicker",
-      "Brightness",
       "Zoom"
     ]
   },

+ 5 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -759,5 +759,9 @@
   "HARDNESS_SETTING": "Hardness",
   "SPACING_SETTING": "Spacing",
   "ANTI_ALIASING_SETTING": "Anti-aliasing",
-  "TOLERANCE_LABEL": "Tolerance"
+  "TOLERANCE_LABEL": "Tolerance",
+  "TOGGLE_SNAPPING": "Toggle snapping",
+  "HIGH_RES_PREVIEW": "High Resolution Preview",
+  "LOW_RES_PREVIEW": "Document Resolution Preview",
+  "TOGGLE_HIGH_RES_PREVIEW": "Toggle high resolution preview"
 }

+ 1 - 0
src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs

@@ -6,4 +6,5 @@ public static class ClipboardDataFormats
     public const string LayerIdList = "PixiEditor.LayerIdList";
     public const string PositionFormat = "PixiEditor.Position";
     public const string ImageSlashPng = "image/png";
+    public const string DocumentFormat = "PixiEditor.Document"; 
 }

+ 3 - 1
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -185,7 +185,9 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<MenuItemBuilder, FileExitMenuBuilder>()
             .AddSingleton<MenuItemBuilder, SymmetryMenuBuilder>()
             .AddSingleton<MenuItemBuilder, OpenDockablesMenuBuilder>()
-            .AddSingleton<MenuItemBuilder, ToggleGridLinesMenuBuilder>();
+            .AddSingleton<MenuItemBuilder, ToggleGridLinesMenuBuilder>()
+            .AddSingleton<MenuItemBuilder, ToggleSnappingMenuBuilder>()
+            .AddSingleton<MenuItemBuilder, ToggleHighResPreviewMenuBuilder>();
     }
 
     public static IServiceCollection AddExtensionServices(this IServiceCollection collection, ExtensionLoader loader) =>

+ 72 - 20
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -34,7 +34,7 @@ namespace PixiEditor.Models.Controllers;
 internal static class ClipboardController
 {
     public static IClipboard Clipboard { get; private set; }
-    
+
     public static void Initialize(IClipboard clipboard)
     {
         Clipboard = clipboard;
@@ -74,25 +74,27 @@ internal static class ClipboardController
                 NoticeDialog.Show("SELECTED_AREA_EMPTY", "NOTHING_TO_COPY");
                 return;
             }
-            
+
             (surfaceToCopy, copyArea) = surface.AsT2;
         }
-        else if(document.TransformViewModel.TransformActive)
+        else if (document.TransformViewModel.TransformActive)
         {
-            var surface = document.TryExtractAreaFromSelected((RectI)document.TransformViewModel.Corners.AABBBounds.RoundOutwards());
+            var surface =
+                document.TryExtractAreaFromSelected(
+                    (RectI)document.TransformViewModel.Corners.AABBBounds.RoundOutwards());
             if (surface.IsT0 || surface.IsT1)
                 return;
-            
+
             (surfaceToCopy, copyArea) = surface.AsT2;
         }
         else if (document.SelectedStructureMember != null)
         {
             RectI bounds = new RectI(VecI.Zero, document.SizeBindable);
-            
+
             var surface = document.TryExtractAreaFromSelected(bounds);
             if (surface.IsT0 || surface.IsT1)
                 return;
-            
+
             (surfaceToCopy, copyArea) = surface.AsT2;
         }
 
@@ -100,20 +102,21 @@ internal static class ClipboardController
         {
             return;
         }
-        
+
         await AddImageToClipboard(surfaceToCopy, data);
 
         if (copyArea.Size != document.SizeBindable && copyArea.Pos != VecI.Zero && copyArea != RectI.Empty)
         {
             data.SetVecI(ClipboardDataFormats.PositionFormat, copyArea.Pos);
         }
-        
+
         string[] layerIds = document.GetSelectedMembers().Select(x => x.ToString()).ToArray();
         string layerIdsString = string.Join(";", layerIds);
-        
+
         byte[] layerIdsBytes = System.Text.Encoding.UTF8.GetBytes(layerIdsString);
-        
+
         data.Set(ClipboardDataFormats.LayerIdList, layerIdsBytes);
+        data.Set(ClipboardDataFormats.DocumentFormat, document.Id);
 
         await Clipboard.SetDataObjectAsync(data);
     }
@@ -142,20 +145,26 @@ internal static class ClipboardController
     /// </summary>
     public static bool TryPaste(DocumentViewModel document, IEnumerable<IDataObject> data, bool pasteAsNew = false)
     {
+        Guid sourceDocument = GetSourceDocument(data); 
         Guid[] layerIds = GetLayerIds(data);
+        
+        if (sourceDocument != document.Id)
+        {
+            layerIds = [];
+        }
 
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
-        
-        if (layerIds is { Length: > 0 } && !hasPos)
+
+        if (layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, document)))
         {
             foreach (var layerId in layerIds)
             {
                 document.Operations.DuplicateLayer(layerId);
             }
-            
+
             return true;
         }
-        
+
         List<DataImage> images = GetImage(data);
         if (images.Count == 0)
             return false;
@@ -172,7 +181,8 @@ internal static class ClipboardController
 
             if (pasteAsNew)
             {
-                var guid = document.Operations.CreateStructureMember(StructureMemberType.Layer, new LocalizedString("NEW_LAYER"), false);
+                var guid = document.Operations.CreateStructureMember(StructureMemberType.Layer,
+                    new LocalizedString("NEW_LAYER"), false);
 
                 if (guid == null)
                 {
@@ -193,6 +203,30 @@ internal static class ClipboardController
         document.Operations.PasteImagesAsLayers(images, document.AnimationDataViewModel.ActiveFrameBindable);
         return true;
     }
+    
+    private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, DocumentViewModel doc)
+    {
+        var dataObjects = data as IDataObject[] ?? data.ToArray();
+        
+        var dataObjectWithPos = dataObjects.FirstOrDefault(x => x.Contains(ClipboardDataFormats.PositionFormat));
+        VecI pos = VecI.Zero;
+
+        if (dataObjectWithPos != null)
+        {
+            pos = dataObjectWithPos.GetVecI(ClipboardDataFormats.PositionFormat);
+        }
+        
+        for (var i = 0; i < layerIds.Length; i++)
+        {
+            var layerId = layerIds[i];
+
+            var layer = doc.StructureHelper.Find(layerId);
+            if (layer is not { TightBounds: not null } || layer.TightBounds.Value.Pos != pos)
+                return false;
+        }
+        
+        return true;
+    }
 
     private static Guid[] GetLayerIds(IEnumerable<IDataObject> data)
     {
@@ -208,6 +242,21 @@ internal static class ClipboardController
 
         return [];
     }
+    
+    private static Guid GetSourceDocument(IEnumerable<IDataObject> data)
+    {
+        foreach (var dataObject in data)
+        {
+            if (dataObject.Contains(ClipboardDataFormats.DocumentFormat))
+            {
+                byte[] guidBytes = (byte[])dataObject.Get(ClipboardDataFormats.DocumentFormat);
+                string guidString = System.Text.Encoding.UTF8.GetString(guidBytes);
+                return Guid.Parse(guidString);
+            }
+        }
+
+        return Guid.Empty;
+    }
 
     /// <summary>
     ///     Pastes image from clipboard into new layer.
@@ -269,7 +318,9 @@ internal static class ClipboardController
             if (TryExtractSingleImage(dataObject, out var singleImage))
             {
                 surfaces.Add(new DataImage(singleImage,
-                    dataObject.Contains(ClipboardDataFormats.PositionFormat) ? dataObject.GetVecI(ClipboardDataFormats.PositionFormat) : pos));
+                    dataObject.Contains(ClipboardDataFormats.PositionFormat)
+                        ? dataObject.GetVecI(ClipboardDataFormats.PositionFormat)
+                        : pos));
                 continue;
             }
 
@@ -314,7 +365,8 @@ internal static class ClipboardController
                     }
 
                     string filename = Path.GetFullPath(path);
-                    surfaces.Add(new DataImage(filename, imported, dataObject.GetVecI(ClipboardDataFormats.PositionFormat)));
+                    surfaces.Add(new DataImage(filename, imported,
+                        dataObject.GetVecI(ClipboardDataFormats.PositionFormat)));
                 }
                 catch
                 {
@@ -410,11 +462,11 @@ internal static class ClipboardController
     {
         foreach (var format in formats)
         {
-            if(format == ClipboardDataFormats.Png)
+            if (format == ClipboardDataFormats.Png)
             {
                 return true;
             }
-            
+
             if (Importer.IsSupportedFile(format))
             {
                 return true;

+ 7 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs

@@ -60,6 +60,9 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
                 toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
             }
 
+            document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect.Inflate(1)), false);
+            document.TransformHandler.ShowHandles = false;
+            document.TransformHandler.IsSizeBoxEnabled = true;
             return ExecutionState.Success;
         }
 
@@ -209,6 +212,9 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
         noMovement = false;
 
         DrawShape((VecI)snapped.Floor(), lastRadians, false);
+
+        document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), false);
+        document!.TransformHandler.Corners = new ShapeCorners((RectD)lastRect);
     }
 
     protected VecD Snap(VecD pos, VecD adjustPos, bool highlight = false)
@@ -273,6 +279,7 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
         base.StartMode(mode);
         if (mode == ShapeToolMode.Transform)
         {
+            document.TransformHandler.HideTransform();
             document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), true);
         }
     }

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -54,6 +54,10 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
                 toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
             }
 
+            document.LineToolOverlayHandler.Show(startDrawingPos, startDrawingPos, false);
+            document.LineToolOverlayHandler.ShowHandles = false;
+            document.LineToolOverlayHandler.IsSizeBoxEnabled = true;
+            
             return ExecutionState.Success;
         }
 
@@ -103,6 +107,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         }
 
         HighlightSnapping(snapX, snapY);
+        document!.LineToolOverlayHandler.LineEnd = snapped;
 
         curPos = snapped;
 
@@ -119,6 +124,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             return;
         }
 
+        document!.LineToolOverlayHandler.Hide();
         document!.LineToolOverlayHandler.Show(startDrawingPos, curPos, true);
         base.OnLeftMouseButtonUp(argsPositionOnCanvas);
     }

+ 1 - 0
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -20,6 +20,7 @@ namespace PixiEditor.Models.Handlers;
 
 internal interface IDocument : IHandler
 {
+    public Guid Id { get; }
     public ObservableRangeCollection<PaletteColor> Palette { get; set; }
     public VecI SizeBindable { get; }
     public IStructureMemberHandler? SelectedStructureMember { get; }

+ 4 - 0
src/PixiEditor/Models/Handlers/ILineOverlayHandler.cs

@@ -12,4 +12,8 @@ internal interface ILineOverlayHandler
     public void Show(VecD startPos, VecD endPos, bool showApplyButton);
     public bool HasUndo { get; }
     public bool HasRedo { get; }
+    public VecD LineStart { get; set; }
+    public VecD LineEnd { get; set; }
+    public bool ShowHandles { get; set; }
+    public bool IsSizeBoxEnabled { get; set; }
 }

+ 3 - 0
src/PixiEditor/Models/Handlers/ITransformHandler.cs

@@ -18,4 +18,7 @@ internal interface ITransformHandler : IHandler
     public bool HasRedo { get; }
     public bool ShowTransformControls { get; set; }
     public event Action<MouseOnCanvasEventArgs> PassthroughPointerPressed;
+    public ShapeCorners Corners { get; set; }
+    public bool ShowHandles { get; set; }
+    public bool IsSizeBoxEnabled { get; set; }
 }

+ 3 - 2
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -195,6 +195,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     private VectorPath selectionPath = new VectorPath();
     public VectorPath SelectionPathBindable => selectionPath;
     public ObservableCollection<PaletteColor> Swatches { get; set; } = new();
+    public Guid Id => Internals.Tracker.Document.DocumentId;
     public ObservableRangeCollection<PaletteColor> Palette { get; set; } = new();
     public SnappingViewModel SnappingViewModel { get; }
     ISnappingHandler IDocument.SnappingHandler => SnappingViewModel;
@@ -620,7 +621,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
                     Renderer.RenderLayer(toPaintOn.DrawingSurface, layerVm.Id, ChunkResolution.Full,
                         AnimationDataViewModel.ActiveFrameTime);
-                    using Image snapshot = toPaintOn.DrawingSurface.Snapshot(bounds);
+                    using Image snapshot = toPaintOn.DrawingSurface.Snapshot(finalBounds);
                     output.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, paint);
                 });
             }
@@ -632,7 +633,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
 
         output.DrawingSurface.Canvas.Restore();
-        return (output, bounds);
+        return (output, finalBounds);
     }
 
     /// <summary>

+ 5 - 12
src/PixiEditor/ViewModels/Document/SnappingViewModel.cs

@@ -1,7 +1,10 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using System.Drawing;
+using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.UI.Common.Fonts;
 
 namespace PixiEditor.ViewModels.Document;
 
@@ -10,21 +13,11 @@ public class SnappingViewModel : PixiObservableObject, ISnappingHandler
     private bool snappingEnabled = true;
     public SnappingController SnappingController { get; } = new SnappingController();
 
-    public bool SnappingEnabled
-    {
-        get => snappingEnabled;
-        set
-        {
-            SetProperty(ref snappingEnabled, value);
-            SnappingController.SnappingEnabled = value;
-        }
-    }
-
     public SnappingViewModel()
     {
         SnappingController.AddXYAxis("Root", VecD.Zero);
     }
-
+    
     public void AddFromDocumentSize(VecD documentSize)
     {
         SnappingController.AddXYAxis("DocumentSize", documentSize);

+ 19 - 0
src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -105,7 +105,23 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
             TransformMoved?.Invoke(this, value);
         }
     }
+
+    private bool showHandles;
+
+    public bool ShowHandles
+    {
+        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
     {
@@ -219,6 +235,9 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         TransformActive = true;
         ShowTransformControls = showApplyButton;
 
+        IsSizeBoxEnabled = false;
+        ShowHandles = true;
+
         RequestCornersExecutor?.Execute(this, initPos);
         undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Initial);
     }

+ 17 - 1
src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs

@@ -35,7 +35,21 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
                 LineMoved?.Invoke(this, (lineStart, lineEnd));
         }
     }
-    
+
+    private bool showHandles;
+    public bool ShowHandles
+    {
+        get => showHandles;
+        set => SetProperty(ref showHandles, value);
+    }
+
+    private bool isSizeBoxEnabled;
+    public bool IsSizeBoxEnabled
+    {
+        get => isSizeBoxEnabled;
+        set => SetProperty(ref isSizeBoxEnabled, value);
+    }
+
     private bool isEnabled;
 
     public bool IsEnabled
@@ -78,6 +92,8 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         LineEnd = endPos; 
         IsEnabled = true;
         ShowApplyButton = showApplyButton;
+        ShowHandles = true;
+        IsSizeBoxEnabled = false;
     }
 
     public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0;

+ 1 - 1
src/PixiEditor/ViewModels/Menu/MenuBuilders/ToggleGridLinesMenuBuilder.cs

@@ -18,7 +18,7 @@ internal class ToggleGridLinesMenuBuilder : MenuItemBuilder
             Translator.SetKey(gridLinesItem, "TOGGLE_GRIDLINES");
             gridLinesItem.Icon = new Image()
             {
-                Source = PixiPerfectIcons.ToIcon(PixiPerfectIcons.Grid),
+                Source = PixiPerfectIcons.ToIcon(PixiPerfectIcons.GridLines),
                 Width = Models.Commands.XAML.Menu.IconDimensions,
                 Height = Models.Commands.XAML.Menu.IconDimensions
             };

+ 43 - 0
src/PixiEditor/ViewModels/Menu/MenuBuilders/ToggleHighResPreviewMenuBuilder.cs

@@ -0,0 +1,43 @@
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using PixiEditor.Extensions.UI;
+using PixiEditor.UI.Common.Controls;
+using PixiEditor.UI.Common.Fonts;
+
+namespace PixiEditor.ViewModels.Menu.MenuBuilders;
+
+internal class ToggleHighResPreviewMenuBuilder : MenuItemBuilder
+{
+    public override void ModifyMenuTree(ICollection<MenuItem> tree)
+    {
+        if (TryFindMenuItem(tree, "VIEW", out MenuItem? viewItem))
+        {
+            ToggleableMenuItem snappingItem = new ToggleableMenuItem();
+            Translator.SetKey(snappingItem, "TOGGLE_HIGH_RES_PREVIEW");
+            snappingItem.Icon = new Image()
+            {
+                Source = PixiPerfectIcons.ToIcon(PixiPerfectIcons.Circle),
+                Width = Models.Commands.XAML.Menu.IconDimensions,
+                Height = Models.Commands.XAML.Menu.IconDimensions
+            };
+
+            BindItem(snappingItem);
+            viewItem.Items.Add(snappingItem);
+        }
+    }
+
+    private void BindItem(ToggleableMenuItem gridLinesItem)
+    {
+        gridLinesItem.Bind(ToggleableMenuItem.IsCheckedProperty, new Binding("ViewportSubViewModel.HighResRender")
+        {
+            Source = ViewModelMain.Current,
+            Mode = BindingMode.TwoWay
+        });
+
+        gridLinesItem.Bind(InputElement.IsEnabledProperty, new Binding("!!DocumentManagerSubViewModel.ActiveDocument")
+        {
+            Source = ViewModelMain.Current
+        });
+    }
+}

+ 43 - 0
src/PixiEditor/ViewModels/Menu/MenuBuilders/ToggleSnappingMenuBuilder.cs

@@ -0,0 +1,43 @@
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using PixiEditor.Extensions.UI;
+using PixiEditor.UI.Common.Controls;
+using PixiEditor.UI.Common.Fonts;
+
+namespace PixiEditor.ViewModels.Menu.MenuBuilders;
+
+internal class ToggleSnappingMenuBuilder : MenuItemBuilder
+{
+    public override void ModifyMenuTree(ICollection<MenuItem> tree)
+    {
+        if (TryFindMenuItem(tree, "VIEW", out MenuItem? viewItem))
+        {
+            ToggleableMenuItem snappingItem = new ToggleableMenuItem();
+            Translator.SetKey(snappingItem, "TOGGLE_SNAPPING");
+            snappingItem.Icon = new Image()
+            {
+                Source = PixiPerfectIcons.ToIcon(PixiPerfectIcons.Snapping),
+                Width = Models.Commands.XAML.Menu.IconDimensions,
+                Height = Models.Commands.XAML.Menu.IconDimensions
+            };
+
+            BindItem(snappingItem);
+            viewItem.Items.Add(snappingItem);
+        }
+    }
+
+    private void BindItem(ToggleableMenuItem gridLinesItem)
+    {
+        gridLinesItem.Bind(ToggleableMenuItem.IsCheckedProperty, new Binding("ViewportSubViewModel.SnappingEnabled")
+        {
+            Source = ViewModelMain.Current,
+            Mode = BindingMode.TwoWay
+        });
+
+        gridLinesItem.Bind(InputElement.IsEnabledProperty, new Binding("!!DocumentManagerSubViewModel.ActiveDocument")
+        {
+            Source = ViewModelMain.Current
+        });
+    }
+}

+ 35 - 3
src/PixiEditor/ViewModels/SubViewModels/ViewOptionsViewModel.cs

@@ -13,22 +13,47 @@ internal class ViewOptionsViewModel : SubViewModel<ViewModelMain>
         get => gridLinesEnabled;
         set => SetProperty(ref gridLinesEnabled, value);
     }
+    
+    private bool snappingEnabled = true;
+    public bool SnappingEnabled
+    {
+        get => snappingEnabled;
+        set
+        {
+            SetProperty(ref snappingEnabled, value);
+            Owner.DocumentManagerSubViewModel.ActiveDocument.SnappingViewModel.SnappingController.SnappingEnabled = value;
+        }
+    }
+    
+    private bool highResRender = true;
+    public bool HighResRender
+    {
+        get => highResRender;
+        set
+        {
+            SetProperty(ref highResRender, value);
+            Owner.DocumentManagerSubViewModel.ActiveDocument.SceneRenderer.HighResRendering = value;
+        }
+    }
 
     public ViewOptionsViewModel(ViewModelMain owner)
         : base(owner)
     {
     }
 
-    [Command.Basic("PixiEditor.View.ToggleGrid", "TOGGLE_GRIDLINES", "TOGGLE_GRIDLINES", Key = Key.OemTilde, Modifiers = KeyModifiers.Control,
+    [Command.Basic("PixiEditor.View.ToggleGrid", "TOGGLE_GRIDLINES", "TOGGLE_GRIDLINES", Key = Key.OemTilde,
+        Modifiers = KeyModifiers.Control,
         Icon = PixiPerfectIcons.Grid)]
     public void ToggleGridLines()
     {
         GridLinesEnabled = !GridLinesEnabled;
     }
 
-    [Command.Basic("PixiEditor.View.ZoomIn", 1, "ZOOM_IN", "ZOOM_IN", CanExecute = "PixiEditor.HasDocument", Key = Key.OemPlus,
+    [Command.Basic("PixiEditor.View.ZoomIn", 1, "ZOOM_IN", "ZOOM_IN", CanExecute = "PixiEditor.HasDocument",
+        Key = Key.OemPlus,
         Icon = PixiPerfectIcons.ZoomIn, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.View.Zoomout", -1, "ZOOM_OUT", "ZOOM_OUT", CanExecute = "PixiEditor.HasDocument", Key = Key.OemMinus,
+    [Command.Basic("PixiEditor.View.Zoomout", -1, "ZOOM_OUT", "ZOOM_OUT", CanExecute = "PixiEditor.HasDocument",
+        Key = Key.OemMinus,
         Icon = PixiPerfectIcons.ZoomOut, AnalyticsTrack = true)]
     public void ZoomViewport(double zoom)
     {
@@ -37,4 +62,11 @@ internal class ViewOptionsViewModel : SubViewModel<ViewModelMain>
             return;
         viewport.ZoomViewportTrigger.Execute(this, zoom);
     }
+
+    [Command.Basic("PixiEditor.ToggleSnapping", "TOGGLE_SNAPPING", "TOGGLE_SNAPPING",
+        Icon = PixiPerfectIcons.Snapping)]
+    public void ToggleSnapping()
+    {
+        SnappingEnabled = !SnappingEnabled;
+    }
 }

+ 24 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/EnumSettingViewModel.cs

@@ -32,7 +32,7 @@ internal sealed class EnumSettingViewModel<TEnum> : Setting<TEnum, ComboBox>
     /// </summary>
     public override TEnum Value
     {
-        get => Enum.GetValues<TEnum>()[SelectedIndex];
+        get => hasOverwrittenValue ? GetOverwrittenEnum() : Enum.GetValues<TEnum>()[SelectedIndex];
         set
         {
             var values = Enum.GetValues<TEnum>();
@@ -63,4 +63,27 @@ internal sealed class EnumSettingViewModel<TEnum> : Setting<TEnum, ComboBox>
     {
         Value = defaultValue;
     }
+    
+    private TEnum GetOverwrittenEnum()
+    {
+        int index;
+        if (overwrittenValue is float floatVal)
+        {
+            index = (int)floatVal;
+        }
+        else if (overwrittenValue is int intVal)
+        {
+            index = intVal;
+        }
+        else if (overwrittenValue is string stringVal)
+        {
+            return Enum.Parse<TEnum>(stringVal);
+        }
+        else
+        {
+            throw new InvalidCastException("Overwritten value is not a valid type.");
+        }
+
+        return Enum.GetValues<TEnum>()[index];
+    }
 }

+ 13 - 4
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs

@@ -1,5 +1,6 @@
 using Avalonia.Controls;
 using CommunityToolkit.Mvvm.ComponentModel;
+using DiscordRPC;
 using PixiEditor.Extensions.Common.Localization;
 
 #pragma warning disable SA1402 // File may only contain a single type, Justification: "Same class with generic value"
@@ -49,11 +50,11 @@ internal abstract class Setting : ObservableObject
     private object _value;
     private bool isExposed = true;
     
-    private bool overwrittenExposed;
-    private object overwrittenValue;
+    protected bool overwrittenExposed;
+    protected object overwrittenValue;
 
-    private bool hasOverwrittenValue;
-    private bool hasOverwrittenExposed;
+    protected bool hasOverwrittenValue;
+    protected bool hasOverwrittenExposed;
     
     protected Setting(string name)
     {
@@ -101,6 +102,7 @@ internal abstract class Setting : ObservableObject
         hasOverwrittenValue = true;
         
         OnPropertyChanged(nameof(Value));
+        ValueChanged?.Invoke(this, new SettingValueChangedEventArgs<object>(_value, value));
     }
     
     public void SetOverwriteExposed(bool value)
@@ -113,6 +115,8 @@ internal abstract class Setting : ObservableObject
     
     public void ResetOverwrite()
     {
+        var old = overwrittenValue;
+        bool hadOverwrittenValue = hasOverwrittenValue;
         overwrittenValue = null;
         overwrittenExposed = false;
         hasOverwrittenValue = false;
@@ -120,5 +124,10 @@ internal abstract class Setting : ObservableObject
         
         OnPropertyChanged(nameof(Value));
         OnPropertyChanged(nameof(IsExposed));
+
+        if (hadOverwrittenValue)
+        {
+            ValueChanged?.Invoke(this, new SettingValueChangedEventArgs<object>(old, _value));
+        }
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -29,7 +29,7 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
     public override bool IsErasable => true;
     public override LocalizedString Tooltip => new LocalizedString("BRIGHTNESS_TOOL_TOOLTIP", Shortcut);
 
-    public override BrushShape BrushShape => BrushShape.Circle;
+    public override BrushShape BrushShape => BrushShape.CirclePixelated;
 
     public override string DefaultIcon => PixiPerfectIcons.Sun;
 

+ 12 - 7
src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs

@@ -21,17 +21,13 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
         Toolbar = ToolbarFactory.Create<EraserToolViewModel, PenToolbar>(this);
     }
 
-    [Settings.Inherited]
-    public int ToolSize => GetValue<int>();
+    [Settings.Inherited] public int ToolSize => GetValue<int>();
 
     public override bool IsErasable => true;
 
     public override string ToolNameLocalizationKey => "ERASER_TOOL";
-    public override BrushShape BrushShape => BrushShape.Circle;
-    public override Type[]? SupportedLayerTypes { get; } =
-    {
-        typeof(IRasterLayerHandler)
-    };
+    public override BrushShape BrushShape => BrushShapeSetting;
+    public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
     public override string DefaultIcon => PixiPerfectIcons.Eraser;
 
@@ -39,8 +35,17 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
+    [Settings.Enum("BRUSH_SHAPE_SETTING", BrushShape.CirclePixelated, ExposedByDefault = false,
+        Notify = nameof(BrushShapeChanged))]
+    public BrushShape BrushShapeSetting => GetValue<BrushShape>();
+
     public override void UseTool(VecD pos)
     {
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseEraserTool();
     }
+
+    private void BrushShapeChanged()
+    {
+        OnPropertyChanged(nameof(BrushShape));
+    }
 }

+ 9 - 1
src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -20,7 +20,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
         private int actualToolSize;
 
         public override string ToolNameLocalizationKey => "PEN_TOOL";
-        public override BrushShape BrushShape => BrushShape.Circle;
+        public override BrushShape BrushShape => BrushShapeSetting;
         
         public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
@@ -39,6 +39,9 @@ namespace PixiEditor.ViewModels.Tools.Tools
 
         [Settings.Bool("PIXEL_PERFECT_SETTING", Notify = nameof(PixelPerfectChanged), ExposedByDefault = false)]
         public bool PixelPerfectEnabled => GetValue<bool>();
+        
+        [Settings.Enum("BRUSH_SHAPE_SETTING", BrushShape.CirclePixelated, ExposedByDefault = false, Notify = nameof(BrushShapeChanged))]
+        public BrushShape BrushShapeSetting => GetValue<BrushShape>();
 
         public override string DefaultIcon => PixiPerfectIcons.Pen;
 
@@ -106,5 +109,10 @@ namespace PixiEditor.ViewModels.Tools.Tools
                 setting.Value = actualToolSize;
             }
         }
+        
+        private void BrushShapeChanged()
+        {
+            OnPropertyChanged(nameof(BrushShape));
+        }
     }
 }

+ 2 - 0
src/PixiEditor/Views/Dock/DocumentTemplate.axaml

@@ -28,6 +28,7 @@
         MiddleMouseClickedCommand="{Binding IoSubViewModel.PreviewMouseMiddleButtonCommand, Source={viewModels1:MainVM}}"
         Cursor="{Binding ToolsSubViewModel.ToolCursor, Source={viewModels1:MainVM}}"
         GridLinesVisible="{Binding ViewportSubViewModel.GridLinesEnabled, Source={viewModels1:MainVM}}"
+        HighResPreview="{Binding ViewportSubViewModel.HighResRender, Source={viewModels1:MainVM}, Mode=TwoWay}"
         ZoomMode="{Binding ToolsSubViewModel.ActiveTool, Source={viewModels1:MainVM}, Converter={converters:ActiveToolToZoomModeConverter}}"
         ZoomOutOnClick="{Binding ToolsSubViewModel.ZoomTool.ZoomOutOnClick, Source={viewModels1:MainVM}}"
         UseTouchGestures="{Binding StylusSubViewModel.UseTouchGestures, Source={viewModels1:MainVM}}"
@@ -35,6 +36,7 @@
         FlipY="{Binding FlipY, Mode=TwoWay}"
         Channels="{Binding Channels, Mode=TwoWay}"
         SnappingViewModel="{Binding ActiveDocument.SnappingViewModel, Source={viewModels1:MainVM DocumentManagerSVM}}"
+        SnappingEnabled="{Binding ViewportSubViewModel.SnappingEnabled, Source={viewModels1:MainVM}, Mode=TwoWay}"
         ContextRequested="Viewport_OnContextMenuOpening"
         Document="{Binding Document}">
         <viewportControls:Viewport.ContextFlyout>

+ 13 - 5
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -124,11 +124,19 @@
                                           Cursor="Hand" />
                         </StackPanel>
                         <Separator />
-                        <ToggleButton Margin="10 0 0 0" Width="32" Height="32"
-                                      ui:Translator.TooltipKey="TOGGLE_SNAPPING"
-                                      Classes="OverlayToggleButton pixi-icon"
-                                      Content="{DynamicResource icon-snapping}"
-                                      IsChecked="{Binding SnappingViewModel.SnappingEnabled, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
+                        <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
+                            <ToggleButton Margin="10 0 0 0" Width="32" Height="32"
+                                          ui:Translator.TooltipKey="TOGGLE_SNAPPING"
+                                          Classes="OverlayToggleButton pixi-icon"
+                                          Content="{DynamicResource icon-gridlines}"
+                                          IsChecked="{Binding GridLinesVisible, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
+                            
+                            <ToggleButton Margin="10 0 0 0" Width="32" Height="32"
+                                          ui:Translator.TooltipKey="TOGGLE_SNAPPING"
+                                          Classes="OverlayToggleButton pixi-icon"
+                                          Content="{DynamicResource icon-snapping}"
+                                          IsChecked="{Binding SnappingEnabled, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
+                        </StackPanel>
                     </StackPanel>
                 </Border>
             </overlays:TogglableFlyout.Child>

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

@@ -290,6 +290,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private MouseUpdateController? mouseUpdateController;
     private ViewportOverlays builtInOverlays = new();
+    public static readonly StyledProperty<bool> SnappingEnabledProperty = AvaloniaProperty.Register<Viewport, bool>("SnappingEnabled");
 
     static Viewport()
     {
@@ -337,6 +338,12 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         set { SetValue(HighResPreviewProperty, value); }
     }
 
+    public bool SnappingEnabled
+    {
+        get { return (bool)GetValue(SnappingEnabledProperty); }
+        set { SetValue(SnappingEnabledProperty, value); }
+    }
+
     private void ForceRefreshFinalImage()
     {
         Scene.InvalidateVisual();
@@ -491,7 +498,6 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     private static void OnHighResPreviewChanged(AvaloniaPropertyChangedEventArgs<bool> e)
     {
         Viewport? viewport = (Viewport)e.Sender;
-        viewport.Document.SceneRenderer.HighResRendering = e.NewValue.Value; 
         viewport.ForceRefreshFinalImage();
     }
 }

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

@@ -219,12 +219,24 @@ internal class ViewportOverlays
         {
             Source = Viewport, Path = "Document.LineToolOverlayViewModel.LineEnd", Mode = BindingMode.TwoWay
         };
+        
+        Binding showHandlesBinding = new()
+        {
+            Source = Viewport, Path = "Document.LineToolOverlayViewModel.ShowHandles", Mode = BindingMode.TwoWay
+        };
+        
+        Binding isSizeBoxEnabledBinding = new()
+        {
+            Source = Viewport, Path = "Document.LineToolOverlayViewModel.IsSizeBoxEnabled", Mode = BindingMode.TwoWay
+        };
 
         lineToolOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         lineToolOverlay.Bind(LineToolOverlay.SnappingControllerProperty, snappingBinding);
         lineToolOverlay.Bind(LineToolOverlay.ActionCompletedProperty, actionCompletedBinding);
         lineToolOverlay.Bind(LineToolOverlay.LineStartProperty, lineStartBinding);
         lineToolOverlay.Bind(LineToolOverlay.LineEndProperty, lineEndBinding);
+        lineToolOverlay.Bind(LineToolOverlay.ShowHandlesProperty, showHandlesBinding);
+        lineToolOverlay.Bind(LineToolOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
     }
 
     private void BindTransformOverlay()
@@ -289,6 +301,16 @@ internal class ViewportOverlays
         Binding passThroughPointerPressedBinding = new()
         {
             Source = Viewport, Path = "Document.TransformViewModel.PassThroughPointerPressedCommand", Mode = BindingMode.OneWay
+        }
+        
+        Binding showHandlesBinding = new()
+        {
+            Source = Viewport, Path = "Document.TransformViewModel.ShowHandles", Mode = BindingMode.TwoWay
+        };
+        
+        Binding isSizeBoxEnabledBinding = new()
+        {
+            Source = Viewport, Path = "Document.TransformViewModel.IsSizeBoxEnabled", Mode = BindingMode.TwoWay
         };
 
         Binding zoomboxAngleBinding = new() { Source = Viewport, Path = "Zoombox.Angle", Mode = BindingMode.OneWay };
@@ -306,6 +328,8 @@ internal class ViewportOverlays
         transformOverlay.Bind(TransformOverlay.InternalStateProperty, internalStateBinding);
         transformOverlay.Bind(TransformOverlay.PassthroughPointerPressedCommandProperty, passThroughPointerPressedBinding);
         transformOverlay.Bind(TransformOverlay.ZoomboxAngleProperty, zoomboxAngleBinding);
+        transformOverlay.Bind(TransformOverlay.ShowHandlesProperty, showHandlesBinding);
+        transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
     }
 
     private void BindSnappingOverlay()

+ 2 - 1
src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShape.cs

@@ -4,5 +4,6 @@ internal enum BrushShape
     Hidden,
     Pixel,
     Square,
-    Circle
+    CirclePixelated,
+    CircleSmooth
 }

+ 22 - 5
src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs

@@ -17,7 +17,8 @@ internal class BrushShapeOverlay : Overlay
         AvaloniaProperty.Register<BrushShapeOverlay, int>(nameof(BrushSize), defaultValue: 1);
 
     public static readonly StyledProperty<BrushShape> BrushShapeProperty =
-        AvaloniaProperty.Register<BrushShapeOverlay, BrushShape>(nameof(BrushShape), defaultValue: BrushShape.Circle);
+        AvaloniaProperty.Register<BrushShapeOverlay, BrushShape>(nameof(BrushShape),
+            defaultValue: BrushShape.CirclePixelated);
 
     public static readonly StyledProperty<Scene> SceneProperty = AvaloniaProperty.Register<BrushShapeOverlay, Scene>(
         nameof(Scene));
@@ -40,7 +41,7 @@ internal class BrushShapeOverlay : Overlay
         set => SetValue(BrushSizeProperty, value);
     }
 
-    private Paint paint = new Paint() { Color = Colors.LightGray, StrokeWidth = 1, Style = PaintStyle.Stroke};
+    private Paint paint = new Paint() { Color = Colors.LightGray, StrokeWidth = 1, Style = PaintStyle.Stroke };
     private VecD lastMousePos = new();
 
     private VectorPath threePixelCircle;
@@ -65,7 +66,7 @@ internal class BrushShapeOverlay : Overlay
             return;
 
         VecD rawPoint = args.Point;
-        lastMousePos = rawPoint; 
+        lastMousePos = rawPoint;
         Refresh();
     }
 
@@ -80,6 +81,7 @@ internal class BrushShapeOverlay : Overlay
         switch (BrushShape)
         {
             case BrushShape.Pixel:
+                paint.IsAntiAliased = false;
                 targetCanvas.DrawRect(
                     new RectD(new VecD(Math.Floor(lastMousePos.X), Math.Floor(lastMousePos.Y)), new VecD(1, 1)),
                     paint);
@@ -87,14 +89,19 @@ internal class BrushShapeOverlay : Overlay
             case BrushShape.Square:
                 targetCanvas.DrawRect(winRect, paint);
                 break;
-            case BrushShape.Circle:
+            case BrushShape.CirclePixelated:
                 DrawCircleBrushShape(targetCanvas, winRect);
                 break;
+            case BrushShape.CircleSmooth:
+                DrawCircleBrushShapeSmooth(targetCanvas, lastMousePos, BrushSize / 2f);
+                break;
         }
     }
 
     private void DrawCircleBrushShape(Canvas drawingContext, RectD winRect)
     {
+        paint.IsAntiAliased = false;
+        
         var rectI = new RectI((int)winRect.X, (int)winRect.Y, (int)winRect.Width, (int)winRect.Height);
         if (BrushSize < 3)
         {
@@ -124,11 +131,21 @@ internal class BrushShapeOverlay : Overlay
 
             var lp = new VecI((int)lastMousePos.X, (int)lastMousePos.Y);
             using VectorPath shifted = new VectorPath(lastNonTranslatedCircle);
-            shifted.Transform(Matrix3X3.CreateTranslation(lp.X - rectI.Width / 2, lp.Y - rectI.Height / 2)); // don't use float, truncation is intended 
+            shifted.Transform(Matrix3X3.CreateTranslation(lp.X - rectI.Width / 2,
+                lp.Y - rectI.Height / 2)); // don't use float, truncation is intended 
             drawingContext.DrawPath(shifted, paint);
         }
     }
 
+    private void DrawCircleBrushShapeSmooth(Canvas drawingContext, VecD lastMousePos, float radius)
+    {
+        VecD center = lastMousePos; 
+        paint.IsAntiAliased = true;
+        
+        drawingContext.DrawOval(new VecD(center.X, center.Y), new VecD(radius, radius),
+            paint);
+    }
+
     protected override void ZoomChanged(double newZoom)
     {
         paint.StrokeWidth = (float)(1.0f / newZoom);

+ 35 - 5
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -55,6 +55,24 @@ internal class LineToolOverlay : Overlay
         set => SetValue(SnappingControllerProperty, value);
     }
 
+    public static readonly StyledProperty<bool> ShowHandlesProperty = AvaloniaProperty.Register<LineToolOverlay, bool>(
+        nameof(ShowHandles), defaultValue: true);
+
+    public bool ShowHandles
+    {
+        get => GetValue(ShowHandlesProperty);
+        set => SetValue(ShowHandlesProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> IsSizeBoxEnabledProperty = AvaloniaProperty.Register<LineToolOverlay, bool>(
+        nameof(IsSizeBoxEnabled));
+
+    public bool IsSizeBoxEnabled
+    {
+        get => GetValue(IsSizeBoxEnabledProperty);
+        set => SetValue(IsSizeBoxEnabledProperty, value);
+    }
+    
     static LineToolOverlay()
     {
         LineStartProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
@@ -115,6 +133,12 @@ internal class LineToolOverlay : Overlay
         infoBox = new InfoBox();
     }
 
+    protected override void OnOverlayPointerMoved(OverlayPointerArgs args)
+    {
+        base.OnOverlayPointerMoved(args);
+        lastMousePos = args.Point;
+    }
+
     private void OnHandleRelease(Handle obj)
     {
         if (SnappingController != null)
@@ -125,6 +149,7 @@ internal class LineToolOverlay : Overlay
         }
         
         isDraggingHandle = false;
+        IsSizeBoxEnabled = false;
     }
 
     protected override void ZoomChanged(double newZoom)
@@ -159,12 +184,15 @@ internal class LineToolOverlay : Overlay
 
         context.DrawLine(new VecD(mappedStart.X, mappedStart.Y), new VecD(mappedEnd.X, mappedEnd.Y), blackPaint);
         context.DrawLine(new VecD(mappedStart.X, mappedStart.Y), new VecD(mappedEnd.X, mappedEnd.Y), whiteDashPaint);
-        
-        startHandle.Draw(context);
-        endHandle.Draw(context);
-        moveHandle.Draw(context);
 
-        if (isDraggingHandle)
+        if (ShowHandles)
+        {
+            startHandle.Draw(context);
+            endHandle.Draw(context);
+            moveHandle.Draw(context);
+        }
+
+        if (IsSizeBoxEnabled)
         {
             string length = $"L: {(mappedEnd - mappedStart).Length:0.#} px";
             infoBox.DrawInfo(context, length, lastMousePos);
@@ -192,6 +220,7 @@ internal class LineToolOverlay : Overlay
         
         lastMousePos = position;
         isDraggingHandle = true;
+        IsSizeBoxEnabled = true;
     }
 
     private void EndHandleOnDrag(Handle source, VecD position)
@@ -204,6 +233,7 @@ internal class LineToolOverlay : Overlay
         
         isDraggingHandle = true;
         lastMousePos = position;
+        IsSizeBoxEnabled = true;
     }
 
     private VecD SnapAndHighlight(VecD position)

+ 3 - 7
src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -135,7 +135,6 @@ internal class SymmetryOverlay : Overlay
         checkerWhite.StrokeWidth = PenThickness;
         rulerPen.StrokeWidth = PenThickness;
 
-
         if (HorizontalAxisVisible)
         {
             if (capturedDirection == SymmetryAxisDirection.Horizontal || hoveredDirection == SymmetryAxisDirection.Horizontal)
@@ -221,9 +220,6 @@ internal class SymmetryOverlay : Overlay
 
         string text = upper ? $"{start - horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({(start - horizontalAxisY) / Size.Y * 100:F1}%)" : $"{horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({horizontalAxisY / Size.Y * 100:F1}%)";
 
-        /*var formattedText = new FormattedText(text, CultureInfo.GetCultureInfo("en-us"),
-            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomScale, Brushes.White);*/
-
         using Font font = Font.CreateDefault(14f / (float)ZoomScale);
         
         if (Size.Y < font.FontSize * 2.5 || horizontalAxisY == (int)Size.Y && upper || horizontalAxisY == 0 && !upper)
@@ -253,9 +249,6 @@ internal class SymmetryOverlay : Overlay
 
         string text = right ? $"{start - verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({(start - verticalAxisX) / Size.X * 100:F1}%)" : $"{verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({verticalAxisX / Size.X * 100:F1}%)";
 
-        /*var formattedText = new FormattedText(text, CultureInfo.GetCultureInfo("en-us"),
-            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomScale, Brushes.White);*/
-
         using Font font = Font.CreateDefault(14f / (float)ZoomScale);
         
         if (Size.X < font.MeasureText(text) * 2.5 || verticalAxisX == (int)Size.X && right || verticalAxisX == 0 && !right)
@@ -320,9 +313,12 @@ internal class SymmetryOverlay : Overlay
         var dir = IsTouchingHandle(args.Point);
         if (dir is null)
             return;
+        
         capturedDirection = dir.Value;
         args.Pointer.Capture(this);
         CallSymmetryDragStartCommand(dir.Value);
+
+        args.Handled = TestHit(args.Point);
     }
 
     protected override void OnOverlayPointerEntered(OverlayPointerArgs args)

+ 81 - 46
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -140,6 +140,25 @@ internal class TransformOverlay : Overlay
         set => SetValue(SnappingControllerProperty, value);
     }
 
+    public static readonly StyledProperty<bool> ShowHandlesProperty = AvaloniaProperty.Register<TransformOverlay, bool>(
+        nameof(ShowHandles));
+
+    public bool ShowHandles
+    {
+        get => GetValue(ShowHandlesProperty);
+        set => SetValue(ShowHandlesProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> IsSizeBoxEnabledProperty =
+        AvaloniaProperty.Register<TransformOverlay, bool>(
+            nameof(IsSizeBoxEnabled));
+
+    public bool IsSizeBoxEnabled
+    {
+        get => GetValue(IsSizeBoxEnabledProperty);
+        set => SetValue(IsSizeBoxEnabledProperty, value);
+    }
+
     static TransformOverlay()
     {
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
@@ -294,7 +313,7 @@ internal class TransformOverlay : Overlay
 
         moveHandle.OnPress += OnMoveHandlePressed;
         moveHandle.OnRelease += OnMoveHandleReleased;
-
+        
         infoBox = new InfoBox();
     }
 
@@ -352,37 +371,40 @@ internal class TransformOverlay : Overlay
 
         // corner anchors
 
-        centerHandle.Position = VecD.Zero;
-        topLeftHandle.Position = topLeft;
-        topRightHandle.Position = topRight;
-        bottomLeftHandle.Position = bottomLeft;
-        bottomRightHandle.Position = bottomRight;
-        topHandle.Position = top;
-        bottomHandle.Position = bottom;
-        leftHandle.Position = left;
-        rightHandle.Position = right;
-        originHandle.Position = InternalState.Origin;
-        moveHandle.Position = TransformHelper.GetHandlePos(Corners, ZoomScale, moveHandle.Size);
-
-        topLeftHandle.Draw(context);
-        topRightHandle.Draw(context);
-        bottomLeftHandle.Draw(context);
-        bottomRightHandle.Draw(context);
-        topHandle.Draw(context);
-        bottomHandle.Draw(context);
-        leftHandle.Draw(context);
-        rightHandle.Draw(context);
-        originHandle.Draw(context);
-        moveHandle.Draw(context);
-
-        if (capturedAnchor == Anchor.Origin)
-        {
-            centerHandle.Position = Corners.RectCenter;
-            centerHandle.Draw(context);
+        if (ShowHandles)
+        {
+            centerHandle.Position = VecD.Zero;
+            topLeftHandle.Position = topLeft;
+            topRightHandle.Position = topRight;
+            bottomLeftHandle.Position = bottomLeft;
+            bottomRightHandle.Position = bottomRight;
+            topHandle.Position = top;
+            bottomHandle.Position = bottom;
+            leftHandle.Position = left;
+            rightHandle.Position = right;
+            originHandle.Position = InternalState.Origin;
+            moveHandle.Position = TransformHelper.GetHandlePos(Corners, ZoomScale, moveHandle.Size);
+
+            topLeftHandle.Draw(context);
+            topRightHandle.Draw(context);
+            bottomLeftHandle.Draw(context);
+            bottomRightHandle.Draw(context);
+            topHandle.Draw(context);
+            bottomHandle.Draw(context);
+            leftHandle.Draw(context);
+            rightHandle.Draw(context);
+            originHandle.Draw(context);
+            moveHandle.Draw(context);
+
+            if (capturedAnchor == Anchor.Origin)
+            {
+                centerHandle.Position = Corners.RectCenter;
+                centerHandle.Draw(context);
+            }
         }
 
         int saved = context.Save();
-        if (rotationCursorActive)
+        if (ShowHandles && rotationCursorActive)
         {
             var matrix = Matrix3X3.CreateTranslation((float)lastPointerPos.X, (float)lastPointerPos.Y);
             double angle = (lastPointerPos - InternalState.Origin).Angle * 180 / Math.PI - 90;
@@ -399,18 +421,23 @@ internal class TransformOverlay : Overlay
         context.RestoreToCount(saved);
 
         infoBox.ZoomScale = ZoomScale;
-        if (capturedAnchor is not null && capturedAnchor != Anchor.Origin)
-        {
-            VecD rectSize = Corners.RectSize;
-            string sizeText = $"W: {rectSize.X:0.#} H: {rectSize.Y:0.#} px";
-            infoBox.DrawInfo(context, sizeText, lastPointerPos);
-        }
-        else if (isRotating)
+
+        if (IsSizeBoxEnabled)
         {
-            infoBox.DrawInfo(context, $"{(RadiansToDegreesNormalized(corners.RectRotation)):0.#}\u00b0", lastPointerPos);
+            if (isRotating)
+            {
+                infoBox.DrawInfo(context, $"{(RadiansToDegreesNormalized(corners.RectRotation)):0.#}\u00b0",
+                    lastPointerPos);
+            }
+            else
+            {
+                VecD rectSize = Corners.RectSize;
+                string sizeText = $"W: {rectSize.X:0.#} H: {rectSize.Y:0.#} px";
+                infoBox.DrawInfo(context, sizeText, lastPointerPos);
+            }
         }
     }
-    
+
     private double RadiansToDegreesNormalized(double radians)
     {
         double degrees = double.RadiansToDegrees(radians);
@@ -424,9 +451,11 @@ internal class TransformOverlay : Overlay
         cornersOnStartAnchorDrag = Corners;
         originOnStartAnchorDrag = InternalState.Origin;
         mousePosOnStartAnchorDrag = lastPointerPos;
+        IsSizeBoxEnabled = true;
 
         if (source == originHandle)
         {
+            IsSizeBoxEnabled = false;
             snapHandleOfOrigin = null;
         }
     }
@@ -511,6 +540,11 @@ internal class TransformOverlay : Overlay
                 finalCursor = TransformHelper.GetResizeCursor((Anchor)anchor, Corners, ZoomboxAngle);
         }
 
+        if (!ShowHandles)
+        {
+            finalCursor = new Cursor(StandardCursorType.Arrow);
+        }
+
         if (Cursor != finalCursor)
             Cursor = finalCursor;
 
@@ -559,6 +593,7 @@ internal class TransformOverlay : Overlay
 
         SnappingController.HighlightedXAxis = string.Empty;
         SnappingController.HighlightedYAxis = string.Empty;
+        IsSizeBoxEnabled = false;
     }
 
     private void StartMoving(VecD position)
@@ -708,7 +743,7 @@ internal class TransformOverlay : Overlay
             {
                 projected = targetPos;
             }
-            
+
             VecD anchorRelativeDelta = projected - originalAnchorPos;
 
             var adjacentAnchors = TransformHelper.GetAdjacentAnchors((Anchor)capturedAnchor);
@@ -747,9 +782,9 @@ internal class TransformOverlay : Overlay
                     snapped = TrySnapAnchor(targetPos);
                 }
             }
-            
+
             VecD potentialPos = targetPos + snapped.Delta;
-            if(potentialPos.X < 0 || potentialPos.Y < 0 || potentialPos.X > lastSize.X || potentialPos.Y > lastSize.Y)
+            if (potentialPos.X < 0 || potentialPos.Y < 0 || potentialPos.X > lastSize.X || potentialPos.Y > lastSize.Y)
             {
                 snapped = new SnapData();
             }
@@ -793,12 +828,12 @@ internal class TransformOverlay : Overlay
         VecD snapOrigin = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, adjacent) + anchorRelativeDelta;
         var snapped = TrySnapAnchorAlongLine(adjacentAnchorPos, snapOrigin);
         double maxDistance = GetSizeToOppositeSide(cornersOnStartAnchorDrag, capturedAnchor.Value) / 8f;
-        
-        if(snapped.Delta.Length > maxDistance)
+
+        if (snapped.Delta.Length > maxDistance)
         {
             snapped = new SnapData();
         }
-        
+
         if (snapped.Delta == VecI.Zero)
         {
             adjacentAnchorPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, adjacentAnchors.Item2) +
@@ -808,7 +843,7 @@ internal class TransformOverlay : Overlay
             snapOrigin = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, adjacent) + anchorRelativeDelta;
 
             snapped = TrySnapAnchorAlongLine(adjacentAnchorPos, snapOrigin);
-            if(snapped.Delta.Length > maxDistance)
+            if (snapped.Delta.Length > maxDistance)
             {
                 snapped = new SnapData();
             }
@@ -816,7 +851,7 @@ internal class TransformOverlay : Overlay
 
         return snapped;
     }
-    
+
     private double GetSizeToOppositeSide(ShapeCorners corners, Anchor anchor1)
     {
         Anchor opposite = TransformHelper.GetOppositeAnchor(anchor1);

+ 4 - 2
src/PixiEditor/Views/Rendering/Scene.cs

@@ -143,11 +143,13 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         CheckerImagePathProperty.Changed.AddClassHandler<Scene>(CheckerImagePathChanged);
         AllOverlaysProperty.Changed.AddClassHandler<Scene>(ActiveOverlaysChanged);
         DefaultCursorProperty.Changed.AddClassHandler<Scene>(DefaultCursorChanged);
-        ChannelsProperty.Changed.AddClassHandler<Scene>(ChannelsChanged);
+        ChannelsProperty.Changed.AddClassHandler<Scene>(Refresh);
         DocumentProperty.Changed.AddClassHandler<Scene>(DocumentChanged);
+        FlipXProperty.Changed.AddClassHandler<Scene>(Refresh);
+        FlipYProperty.Changed.AddClassHandler<Scene>(Refresh);
     }
 
-    private static void ChannelsChanged(Scene scene, AvaloniaPropertyChangedEventArgs args)
+    private static void Refresh(Scene scene, AvaloniaPropertyChangedEventArgs args)
     {
         scene.InvalidateVisual();
     }