Browse Source

Merge branch 'master' into hsv-hsl-support

CPKreuz 11 months ago
parent
commit
ee676ae1e9
100 changed files with 605 additions and 441 deletions
  1. 2 12
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodeMetadata.cs
  2. 2 2
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  3. 2 2
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  4. 5 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs
  5. 1 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs
  6. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs
  7. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  8. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs
  9. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecDNode.cs
  10. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecINode.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs
  15. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  16. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs
  17. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EllipseNode.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  19. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs
  21. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  23. 4 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  24. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs
  25. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  26. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  27. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  29. 0 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  30. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  31. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  32. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  33. 4 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseData.cs
  34. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs
  35. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  36. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  37. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs
  38. 36 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  39. 8 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs
  40. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  41. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/CreateStructureMemberMask_Change.cs
  42. 7 7
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/DeleteStructureMemberMask_Change.cs
  43. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/StructureMemberClipToMemberBelow_Change.cs
  44. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  45. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs
  46. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  47. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  48. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs
  49. 10 10
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  50. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  51. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs
  52. 15 1
      src/PixiEditor.ChangeableDocument/Enums/MathNodeMode.cs
  53. 43 0
      src/PixiEditor.Linux/LinuxOperatingSystem.cs
  54. 3 0
      src/PixiEditor.MacOs/MacOperatingSystem.cs
  55. 2 0
      src/PixiEditor.Numerics/VecI.cs
  56. 3 0
      src/PixiEditor.OperatingSystem/IOperatingSystem.cs
  57. 3 0
      src/PixiEditor.Windows/WindowsOperatingSystem.cs
  58. 33 0
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  59. 10 1
      src/PixiEditor/Data/Localization/Languages/en.json
  60. 7 0
      src/PixiEditor/Exceptions/CommandInvocationException.cs
  61. 1 42
      src/PixiEditor/Fonts/NodeIcons.cs
  62. 12 3
      src/PixiEditor/Helpers/CrashHelper.cs
  63. 0 10
      src/PixiEditor/Helpers/IFolderHandlerFactory.cs
  64. 0 10
      src/PixiEditor/Helpers/ILayerHandlerFactory.cs
  65. 28 27
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  66. 1 0
      src/PixiEditor/Models/AnalyticsAPI/AnalyticEventTypes.cs
  67. 8 2
      src/PixiEditor/Models/AnalyticsAPI/AnalyticSessionInfo.cs
  68. 4 2
      src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs
  69. 25 7
      src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs
  70. 12 4
      src/PixiEditor/Models/Commands/CommandController.cs
  71. 20 0
      src/PixiEditor/Models/Config/ConfigManager.cs
  72. 13 0
      src/PixiEditor/Models/Config/ToolSetConfig.cs
  73. 35 25
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  74. 7 0
      src/PixiEditor/Models/Events/NodePropertyValueChanged.cs
  75. 15 11
      src/PixiEditor/Models/ExceptionHandling/CrashReport.cs
  76. 16 0
      src/PixiEditor/Models/ExceptionHandling/CrashedFileInfo.cs
  77. 18 0
      src/PixiEditor/Models/ExceptionHandling/CrashedSessionInfo.cs
  78. 0 2
      src/PixiEditor/Models/Handlers/IDocument.cs
  79. 3 0
      src/PixiEditor/Models/Handlers/INodePropertyHandler.cs
  80. 7 0
      src/PixiEditor/Models/Handlers/IToolSetHandler.cs
  81. 4 2
      src/PixiEditor/Models/Handlers/IToolsHandler.cs
  82. 5 5
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  83. 8 8
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  84. 1 37
      src/PixiEditor/PixiEditor.csproj
  85. 8 1
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  86. 1 1
      src/PixiEditor/Styles/Templates/NodePropertyViewTemplate.axaml
  87. 17 23
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  88. 0 21
      src/PixiEditor/ViewModels/Document/FolderHandlerFactory.cs
  89. 0 16
      src/PixiEditor/ViewModels/Document/FolderViewModel.cs
  90. 1 1
      src/PixiEditor/ViewModels/Document/KeyFrameGroupViewModel.cs
  91. 0 21
      src/PixiEditor/ViewModels/Document/LayerHandlerFactory.cs
  92. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/Animable/TimeNodeViewModel.cs
  93. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineChannelsNodeViewModel.cs
  94. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineColorNodeViewModel.cs
  95. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecDNodeViewModel.cs
  96. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecINodeViewModel.cs
  97. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateChannelsNodeViewModel.cs
  98. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateColorNodeViewModel.cs
  99. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecDNodeViewModel.cs
  100. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecINodeViewModel.cs

+ 2 - 12
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodeMetadata.cs

@@ -13,10 +13,11 @@ public class NodeMetadata
     public Guid? PairNodeGuid { get; set; }
     public string? ZoneUniqueName { get; private set; }
     
-    public string? Category { get; private set; }
+    public Type NodeType { get; private set; }
 
     public NodeMetadata(Type type)
     {
+        NodeType = type;
         AddAttributes(type);
     }
     
@@ -24,20 +25,9 @@ public class NodeMetadata
 
     private void AddAttributes(Type type)
     {
-        AddNodeInfoAttribute(type);
         AddPairAttributes(type);
     }
 
-    private void AddNodeInfoAttribute(Type type)
-    {
-        var attribute = type.GetCustomAttribute<NodeInfoAttribute>();
-
-        if (attribute == null)
-            return;
-
-        Category = attribute.Category;
-    }
-
     private void AddPairAttributes(Type type)
     {
         var attribute = type.GetCustomAttribute<PairNodeAttribute>();

+ 2 - 2
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs

@@ -31,11 +31,11 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
             folder.GetNodeTypeUniqueName(),
             folder.Opacity.Value,
             folder.IsVisible.Value,
-            folder.ClipToPreviousMember.Value,
+            folder.ClipToPreviousMember,
             folder.MemberName,
             folder.BlendMode.Value,
             folder.Id,
-            folder.Mask.Value is not null,
+            folder.EmbeddedMask is not null,
             folder.MaskIsVisible.Value, CreatePropertyInfos(folder.InputProperties, true, folder.Id),
             CreatePropertyInfos(folder.OutputProperties, false, folder.Id),
             new NodeMetadata(folder));

+ 2 - 2
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs

@@ -37,11 +37,11 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
             layer.GetNodeTypeUniqueName(),
             layer.Opacity.Value,
             layer.IsVisible.Value,
-            layer.ClipToPreviousMember.Value,
+            layer.ClipToPreviousMember,
             layer.MemberName,
             layer.BlendMode.Value,
             layer.Id,
-            layer.Mask.Value is not null,
+            layer.EmbeddedMask is not null,
             layer.MaskIsVisible.Value,
             layer is ITransparencyLockable { LockTransparency: true },
             CreatePropertyInfos(layer.InputProperties, true, layer.Id),

+ 5 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs

@@ -1,5 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -8,10 +9,12 @@ public interface IReadOnlyStructureNode : IReadOnlyNode
 {
     public InputProperty<float> Opacity { get; }
     public InputProperty<bool> IsVisible { get; }
-    public InputProperty<bool> ClipToPreviousMember { get; }
+    public bool ClipToPreviousMember { get; }
     public InputProperty<BlendMode> BlendMode { get; }
-    public InputProperty<ChunkyImage?> Mask { get; }
+    public InputProperty<Texture?> CustomMask { get; }
     public InputProperty<bool> MaskIsVisible { get; }
     public string MemberName { get; set; }
     public RectI? GetTightBounds(KeyFrameTime frameTime);
+    
+    public ChunkyImage? EmbeddedMask { get; }
 }

+ 1 - 9
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs

@@ -5,14 +5,7 @@ public class NodeInfoAttribute : Attribute
 {
     public string UniqueName { get; }
     
-    public string DisplayName { get; }
-    
-    public string? PickerName { get; set; }
-    
-    public string? Category { get; set; }
-    public string? Icon { get; set; }
-
-    public NodeInfoAttribute(string uniqueName, string displayName)
+    public NodeInfoAttribute(string uniqueName)
     {
         if (!uniqueName.StartsWith("PixiEditor"))
         {
@@ -20,6 +13,5 @@ public class NodeInfoAttribute : Attribute
         }
         
         UniqueName = uniqueName;
-        DisplayName = displayName;
     }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs

@@ -3,7 +3,7 @@ using PixiEditor.DrawingApi.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
 
-[NodeInfo("Time", "TIME_NODE", Category = "ANIMATION")]
+[NodeInfo("Time")]
 public class TimeNode : Node
 {
     public OutputProperty<int> ActiveFrame { get; set; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineChannels", "COMBINE_CHANNELS_NODE", Category = "IMAGE")]
+[NodeInfo("CombineChannels")]
 public class CombineChannelsNode : Node
 {
     private readonly Paint _screenPaint = new() { BlendMode = BlendMode.Screen };

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineColor", "COMBINE_COLOR_NODE", Category = "COLOR")]
+[NodeInfo("CombineColor")]
 public class CombineColorNode : Node
 {
     public FuncOutputProperty<Half4> Color { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecDNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecD", "COMBINE_VECD_NODE", Category = "NUMBERS")]
+[NodeInfo("CombineVecD")]
 public class CombineVecDNode : Node
 {
     public FuncOutputProperty<Float2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecINode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecI", "COMBINE_VECI_NODE", Category = "NUMBERS")]
+[NodeInfo("CombineVecI")]
 public class CombineVecINode : Node
 {
     public FuncOutputProperty<Int2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateChannels", "SEPARATE_CHANNELS_NODE", Category = "IMAGE")]
+[NodeInfo("SeparateChannels")]
 public class SeparateChannelsNode : Node
 {
     private readonly Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateColor", "SEPARATE_COLOR_NODE", Category = "COLOR")]
+[NodeInfo("SeparateColor")]
 public class SeparateColorNode : Node
 {
     private readonly NodeVariableAttachments contextVariables = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecD", "SEPARATE_VECD_NODE", Category = "NUMBERS")]
+[NodeInfo("SeparateVecD")]
 public class SeparateVecDNode : Node
 {
     public FuncInputProperty<Float2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecI", "SEPARATE_VECI_NODE", Category = "NUMBERS")]
+[NodeInfo("SeparateVecI")]
 public class SeparateVecINode : Node
 {
     public FuncInputProperty<Int2> Vector { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("CreateImage", "CREATE_IMAGE_NODE", Category = "IMAGE")]
+[NodeInfo("CreateImage")]
 public class CreateImageNode : Node
 {
     private Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 // TODO: Add based on debug mode, not debug build.
-[NodeInfo("DebugBlendMode", "Debug Blend Mode")]
+[NodeInfo("DebugBlendMode")]
 public class DebugBlendModeNode : Node
 {
     private Paint _paint = new();

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EllipseNode.cs

@@ -0,0 +1 @@
+

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ApplyFilter", "APPLY_FILTER_NODE", Category = "FILTERS")]
+[NodeInfo("ApplyFilter")]
 public class ApplyFilterNode : Node
 {
     private Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs

@@ -3,7 +3,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ColorMatrixFilter", "COLOR_MATRIX_TRANSFORM_FILTER_NODE", Category = "FILTERS")]
+[NodeInfo("ColorMatrixFilter")]
 public class ColorMatrixFilterNode : FilterNode
 {
     public InputProperty<ColorMatrix> Matrix { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs

@@ -3,7 +3,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("GrayscaleFilter", "GRAYSCALE_FILTER_NODE", Category = "FILTERS")]
+[NodeInfo("GrayscaleFilter")]
 public class GrayscaleNode : FilterNode
 {
     private static readonly ColorMatrix WeightedMatrix = ColorMatrix.WeightedWavelengthGrayscale + ColorMatrix.UseAlpha;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs

@@ -4,7 +4,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("KernelFilter", "KERNEL_FILTER_NODE", Category = "FILTERS")]
+[NodeInfo("KernelFilter")]
 public class KernelFilterNode : FilterNode
 {
     private readonly Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Folder", "FOLDER_NODE", Category = "STRUCTURE")]
+[NodeInfo("Folder")]
 public class FolderNode : StructureNode, IReadOnlyFolderNode
 {
     public InputProperty<Texture?> Content { get; }

+ 4 - 12
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -10,18 +10,18 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ImageLayer", "IMAGE_LAYER_NODE", Category = "STRUCTURE")]
+[NodeInfo("ImageLayer")]
 public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 {
     public const string ImageFramesKey = "Frames";
     public const string ImageLayerKey = "LayerImage";
-
     public OutputProperty<Texture> RawOutput { get; }
 
-    public InputProperty<bool> LockTransparency { get; }
+    public bool LockTransparency { get; set; }
 
     private VecI size;
     private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
+    
 
     protected Dictionary<(ChunkResolution, int), Texture> workingSurfaces =
         new Dictionary<(ChunkResolution, int), Texture>();
@@ -43,8 +43,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     {
         RawOutput = CreateOutput<Texture>(nameof(RawOutput), "RAW_LAYER_OUTPUT", null);
 
-        LockTransparency = CreateInput<bool>("LockTransparency", "LOCK_TRANSPARENCY", false);
-
         if (keyFrames.Count == 0)
         {
             keyFrames.Add(new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size) });
@@ -315,6 +313,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     protected override bool CacheChanged(RenderingContext context)
     {
         var frame = GetFrameWithImage(context.FrameTime);
+        
         return base.CacheChanged(context) || frame?.RequiresUpdate == true;
     }
 
@@ -358,13 +357,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     void IReadOnlyImageNode.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
 
-    bool ITransparencyLockable.LockTransparency
-    {
-        get => LockTransparency.Value; // TODO: I wonder if it should be NonOverridenValue
-        set => LockTransparency.NonOverridenValue = value;
-    }
-
-
     public void ForEveryFrame(Action<ChunkyImage> action)
     {
         foreach (var frame in keyFrames)

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Lerp", "LERP_NODE", Category = "NUMBERS")]
+[NodeInfo("Lerp")]
 public class LerpColorNode : Node // TODO: ILerpable as inputs? 
 {
     public FuncOutputProperty<Half4> Result { get; } 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Math", "MATH_NODE", Category = "NUMBERS")]
+[NodeInfo("Math")]
 public class MathNode : Node
 {
     public FuncOutputProperty<Float1> Result { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Merge", "MERGE_NODE", Category = "IMAGE")]
+[NodeInfo("Merge")]
 public class MergeNode : Node, IBackgroundInput
 {
     private Paint _paint = new();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -13,7 +13,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ModifyImageLeft", "MODIFY_IMAGE_LEFT_NODE", PickerName = "MODIFY_IMAGE_PAIR_NODE", Category = "IMAGE")]
+[NodeInfo("ModifyImageLeft")]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
 public class ModifyImageLeftNode : Node, IPairNode
 {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -14,7 +14,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ModifyImageRight", "MODIFY_IMAGE_RIGHT_NODE", PickerName = "", Category = "IMAGE")]
+[NodeInfo("ModifyImageRight")]
 [PairNode(typeof(ModifyImageLeftNode), "ModifyImageZone")]
 public class ModifyImageRightNode : Node, IPairNode, ICustomShaderNode
 {

+ 0 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -45,11 +45,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected virtual bool AffectedByChunkToUpdate { get; }
 
-    protected Node()
-    {
-        displayName = GetType().GetCustomAttribute<NodeInfoAttribute>().DisplayName;
-    }
-
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     IReadOnlyList<IReadOnlyKeyFrameData> IReadOnlyNode.KeyFrames => keyFrames;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -9,7 +9,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Noise", "NOISE_NODE", Category = "IMAGE")]
+[NodeInfo("Noise")]
 public class NoiseNode : Node
 {
     private double previousScale = double.NaN;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Output", "OUTPUT_NODE", PickerName = "")]
+[NodeInfo("Output")]
 public class OutputNode : Node, IBackgroundInput
 {
     public const string InputPropertyName = "Background";

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("SampleImage", "SAMPLE_IMAGE", Category = "IMAGE")]
+[NodeInfo("SampleImage")]
 public class SampleImageNode : Node
 {
     public InputProperty<Texture?> Image { get; }

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseData.cs

@@ -16,8 +16,10 @@ public class EllipseData : ShapeData
     
     public override void Rasterize(DrawingSurface drawingSurface)
     {
-        using ChunkyImage img = new ChunkyImage(new VecI((int)Radius.X * 2, (int)Radius.Y * 2));
-        RectI rect = new RectI((int)Center.X - (int)Radius.X, (int)Center.Y - (int)Radius.Y, (int)Radius.X * 2, (int)Radius.Y * 2);
+        var imageSize = new VecI((int)Radius.X * 2, (int)Radius.Y * 2);
+
+        using ChunkyImage img = new ChunkyImage(imageSize);
+        RectI rect = new RectI(0, 0, (int)Radius.X * 2, (int)Radius.Y * 2);
         
         img.EnqueueDrawEllipse(rect, StrokeColor, FillColor, StrokeWidth);
         img.CommitChanges();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs

@@ -5,7 +5,7 @@ using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.D
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
-[NodeInfo("DistributePoints", "DISTRIBUTE_POINTS", Category = "SHAPE")]
+[NodeInfo("DistributePoints")]
 public class DistributePointsNode : ShapeNode<PointsData>
 {
     public InputProperty<int> MaxPointCount { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs

@@ -6,7 +6,7 @@ using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.D
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
-[NodeInfo("Ellipse", "ELLIPSE_NODE", Category = "SHAPE")]
+[NodeInfo("Ellipse")]
 public class EllipseNode : ShapeNode<EllipseData>
 {
     public InputProperty<VecD> Position { get; }

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShape.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs

@@ -8,15 +8,15 @@ using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.D
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
-[NodeInfo("RasterizeShape", "RASTERIZE_SHAPE", Category = "SHAPE")]
-public class RasterizeShape : Node
+[NodeInfo("RasterizeShape")]
+public class RasterizeShapeNode : Node
 {
     public OutputProperty<Texture> Image { get; }
 
     public InputProperty<ShapeData> Data { get; }
 
 
-    public RasterizeShape()
+    public RasterizeShapeNode()
     {
         Image = CreateOutput<Texture>("Image", "IMAGE", null);
         Data = CreateInput<ShapeData>("Points", "SHAPE", null);
@@ -39,5 +39,5 @@ public class RasterizeShape : Node
         return image;
     }
 
-    public override Node CreateCopy() => new RasterizeShape();
+    public override Node CreateCopy() => new RasterizeShapeNode();
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs

@@ -6,7 +6,7 @@ using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.D
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
-[NodeInfo("RemoveClosePoints", "REMOVE_CLOSE_POINTS", Category = "SHAPE")]
+[NodeInfo("RemoveClosePoints")]
 public class RemoveClosePointsNode : ShapeNode<PointsData>
 {
     public InputProperty<PointsData> Input { get; }

+ 36 - 16
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -17,16 +17,17 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
     public InputProperty<Texture?> Background { get; }
     public InputProperty<float> Opacity { get; }
     public InputProperty<bool> IsVisible { get; }
-    public InputProperty<bool> ClipToPreviousMember { get; }
+    public bool ClipToPreviousMember { get; set; }
     public InputProperty<BlendMode> BlendMode { get; }
-    public InputProperty<ChunkyImage?> Mask { get; }
+    public InputProperty<Texture?> CustomMask { get; }
     public InputProperty<bool> MaskIsVisible { get; }
     public InputProperty<Filter> Filters { get; }
-
     public OutputProperty<Texture?> Output { get; }
 
     public OutputProperty<Texture?> FilterlessOutput { get; }
 
+    public ChunkyImage? EmbeddedMask { get; set; }
+
     public string MemberName
     {
         get => DisplayName;
@@ -35,15 +36,16 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
     
     private Paint maskPaint = new Paint() { BlendMode = DrawingApi.Core.Surfaces.BlendMode.DstIn };
     protected Paint blendPaint = new Paint();
+    
+    private int maskCacheHash = 0;
 
     protected StructureNode()
     {
         Background = CreateInput<Texture?>("Background", "BACKGROUND", null);
         Opacity = CreateInput<float>("Opacity", "OPACITY", 1);
         IsVisible = CreateInput<bool>("IsVisible", "IS_VISIBLE", true);
-        ClipToPreviousMember = CreateInput<bool>("ClipToMemberBelow", "CLIP_TO_MEMBER_BELOW", false);
         BlendMode = CreateInput<BlendMode>("BlendMode", "BLEND_MODE", Enums.BlendMode.Normal);
-        Mask = CreateInput<ChunkyImage?>("Mask", "MASK", null);
+        CustomMask = CreateInput<Texture?>("Mask", "MASK", null);
         MaskIsVisible = CreateInput<bool>("MaskIsVisible", "MASK_IS_VISIBLE", true);
         Filters = CreateInput<Filter>(nameof(Filters), "FILTERS", null);
 
@@ -60,34 +62,52 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
 
     protected void ApplyMaskIfPresent(Texture surface, RenderingContext context)
     {
-        if (Mask.Value != null && MaskIsVisible.Value)
+        if (MaskIsVisible.Value)
         {
-            Mask.Value.DrawMostUpToDateChunkOn(
-                context.ChunkToUpdate,
-                context.ChunkResolution,
-                surface.DrawingSurface,
-                context.ChunkToUpdate * context.ChunkResolution.PixelSize(),
-                maskPaint);
+            if (CustomMask.Value != null)
+            {
+                surface.DrawingSurface.Canvas.DrawSurface(CustomMask.Value.DrawingSurface, 0, 0, maskPaint); 
+            }
+            else if (EmbeddedMask != null)
+            {
+                EmbeddedMask.DrawMostUpToDateChunkOn(
+                    context.ChunkToUpdate,
+                    context.ChunkResolution,
+                    surface.DrawingSurface,
+                    context.ChunkToUpdate * context.ChunkResolution.PixelSize(),
+                    maskPaint);
+            }
         }
     }
 
+    protected override bool CacheChanged(RenderingContext context)
+    {
+        int cacheHash = EmbeddedMask?.GetCacheHash() ?? 0;
+        return base.CacheChanged(context) || maskCacheHash != cacheHash;
+    }
+
+    protected override void UpdateCache(RenderingContext context)
+    {
+        base.UpdateCache(context);
+        maskCacheHash = EmbeddedMask?.GetCacheHash() ?? 0;
+    }
+
     protected void ApplyRasterClip(Texture toClip, Texture clipSource)
     {
-        if (ClipToPreviousMember.Value && Background.Value != null)
+        if (ClipToPreviousMember && Background.Value != null)
         {
              toClip.DrawingSurface.Canvas.DrawSurface(clipSource.DrawingSurface, 0, 0, maskPaint);
         }
     }
 
-
     protected bool IsEmptyMask()
     {
-        return Mask.Value != null && MaskIsVisible.Value && !Mask.Value.LatestOrCommittedChunkExists();
+        return EmbeddedMask != null && MaskIsVisible.Value && !EmbeddedMask.LatestOrCommittedChunkExists();
     }
 
     protected bool HasOperations()
     {
-        return (MaskIsVisible.Value && Mask.Value != null) || ClipToPreviousMember.Value;
+        return (MaskIsVisible.Value && (EmbeddedMask != null || CustomMask.Value != null)) || ClipToPreviousMember;
     }
 
     protected void DrawBackground(Texture workingSurface, RenderingContext context)

+ 8 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs

@@ -21,24 +21,24 @@ internal class ApplyLayerMask_Change : Change
     public override bool InitializeAndValidate(Document target)
     {
         //TODO: Check if support for different Layer types is needed here.
-        if (!target.TryFindMember<ImageLayerNode>(layerGuid, out var layer) || layer.Mask.Value is null)
+        if (!target.TryFindMember<ImageLayerNode>(layerGuid, out var layer) || layer.EmbeddedMask is null)
             return false;
 
         var layerImage = layer.GetLayerImageAtFrame(frame);
         savedLayer = new CommittedChunkStorage(layerImage, layerImage.FindCommittedChunks());
-        savedMask = new CommittedChunkStorage(layer.Mask.Value, layer.Mask.Value.FindCommittedChunks());
+        savedMask = new CommittedChunkStorage(layer.EmbeddedMask, layer.EmbeddedMask.FindCommittedChunks());
         return true;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
         var layer = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
-        if (layer.Mask is null)
+        if (layer.EmbeddedMask is null)
             throw new InvalidOperationException("Cannot apply layer mask, no mask");
 
         var layerImage = layer.GetLayerImageAtFrame(frame);
         ChunkyImage newLayerImage = new ChunkyImage(target.Size);
-        newLayerImage.AddRasterClip(layer.Mask.Value);
+        newLayerImage.AddRasterClip(layer.EmbeddedMask);
         newLayerImage.EnqueueDrawCommitedChunkyImage(VecI.Zero, layerImage);
         newLayerImage.CommitChanges();
 
@@ -48,8 +48,8 @@ internal class ApplyLayerMask_Change : Change
         layer.SetLayerImageAtFrame(frame, newLayerImage);
         toDispose.Dispose();
 
-        var toDisposeMask = layer.Mask.NonOverridenValue;
-        layer.Mask.NonOverridenValue = null;
+        var toDisposeMask = layer.EmbeddedMask;
+        layer.EmbeddedMask = null;
         toDisposeMask.Dispose();
 
         ignoreInUndo = false;
@@ -63,7 +63,7 @@ internal class ApplyLayerMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var layer = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
-        if (layer.Mask.Value is not null)
+        if (layer.EmbeddedMask is not null)
             throw new InvalidOperationException("Cannot restore layer mask, it already has one");
         if (savedLayer is null || savedMask is null)
             throw new InvalidOperationException("Cannot restore layer mask, no saved data");
@@ -72,7 +72,7 @@ internal class ApplyLayerMask_Change : Change
         savedMask.ApplyChunksToImage(newMask);
         var affectedChunksMask = newMask.FindAffectedArea();
         newMask.CommitChanges();
-        layer.Mask.NonOverridenValue = newMask;
+        layer.EmbeddedMask = newMask;
 
         var layerImage = layer.GetLayerImageAtFrame(frame);
         

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs

@@ -40,9 +40,9 @@ internal static class DrawingChangeHelper
 
         if (drawOnMask)
         {
-            if (member.Mask is null)
+            if (member.EmbeddedMask is null)
                 throw new InvalidOperationException("Trying to draw on a mask that doesn't exist");
-            return member.Mask.Value;
+            return member.EmbeddedMask;
         }
 
         if (member is FolderNode)
@@ -65,9 +65,9 @@ internal static class DrawingChangeHelper
 
         if (drawOnMask)
         {
-            if (member.Mask.NonOverridenValue is null)
+            if (member.EmbeddedMask is null)
                 throw new InvalidOperationException("Trying to draw on a mask that doesn't exist");
-            return member.Mask.NonOverridenValue;
+            return member.EmbeddedMask;
         }
 
         if (member is FolderNode)
@@ -109,7 +109,7 @@ internal static class DrawingChangeHelper
         return drawOnMask switch
         {
             // If it should draw on the mask, the mask can't be null
-            true when member.Mask.NonOverridenValue is null => false,
+            true when member.EmbeddedMask is null => false,
             // If it should not draw on the mask, the member can't be a folder
             false when member is FolderNode => false,
             _ => true

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/CreateStructureMemberMask_Change.cs

@@ -14,15 +14,15 @@ internal class CreateStructureMemberMask_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        return target.TryFindMember(targetMember, out var member) && member.Mask.NonOverridenValue is null;
+        return target.TryFindMember(targetMember, out var member) && member.EmbeddedMask is null;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
         var member = target.FindMemberOrThrow(targetMember);
-        if (member.Mask.NonOverridenValue is not null)
+        if (member.EmbeddedMask is not null)
             throw new InvalidOperationException("Cannot create a mask; the target member already has one");
-        member.Mask.NonOverridenValue = new ChunkyImage(target.Size);
+        member.EmbeddedMask = new ChunkyImage(target.Size);
 
         ignoreInUndo = false;
         return new StructureMemberMask_ChangeInfo(targetMember, true);
@@ -31,10 +31,10 @@ internal class CreateStructureMemberMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var member = target.FindMemberOrThrow(targetMember);
-        if (member.Mask.NonOverridenValue is null)
+        if (member.EmbeddedMask is null)
             throw new InvalidOperationException("Cannot delete the mask; the target member has no mask");
-        member.Mask.NonOverridenValue.Dispose();
-        member.Mask.NonOverridenValue = null;
+        member.EmbeddedMask.Dispose();
+        member.EmbeddedMask = null;
         return new StructureMemberMask_ChangeInfo(targetMember, false);
     }
 }

+ 7 - 7
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/DeleteStructureMemberMask_Change.cs

@@ -15,20 +15,20 @@ internal class DeleteStructureMemberMask_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if (!target.TryFindMember(memberGuid, out var member) || member.Mask.NonOverridenValue is null)
+        if (!target.TryFindMember(memberGuid, out var member) || member.EmbeddedMask is null)
             return false;
         
-        storedMask = member.Mask.NonOverridenValue.CloneFromCommitted();
+        storedMask = member.EmbeddedMask.CloneFromCommitted();
         return true;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
         var member = target.FindMemberOrThrow(memberGuid);
-        if (member.Mask.NonOverridenValue is null)
+        if (member.EmbeddedMask is null)
             throw new InvalidOperationException("Cannot delete the mask; Target member has no mask");
-        member.Mask.NonOverridenValue.Dispose();
-        member.Mask.NonOverridenValue = null;
+        member.EmbeddedMask.Dispose();
+        member.EmbeddedMask = null;
 
         ignoreInUndo = false;
         return new StructureMemberMask_ChangeInfo(memberGuid, false);
@@ -37,9 +37,9 @@ internal class DeleteStructureMemberMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var member = target.FindMemberOrThrow(memberGuid);
-        if (member.Mask.NonOverridenValue is not null)
+        if (member.EmbeddedMask is not null)
             throw new InvalidOperationException("Cannot revert mask deletion; The target member already has a mask");
-        member.Mask.NonOverridenValue = storedMask!.CloneFromCommitted();
+        member.EmbeddedMask = storedMask!.CloneFromCommitted();
 
         return new StructureMemberMask_ChangeInfo(memberGuid, true);
     }

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/StructureMemberClipToMemberBelow_Change.cs

@@ -16,16 +16,16 @@ internal class StructureMemberClipToMemberBelow_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if (!target.TryFindMember(memberGuid, out var member) || member.ClipToPreviousMember.NonOverridenValue == newValue)
+        if (!target.TryFindMember(memberGuid, out var member) || member.ClipToPreviousMember == newValue)
             return false;
-        originalValue = member.ClipToPreviousMember.NonOverridenValue;
+        originalValue = member.ClipToPreviousMember;
         return true;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
         var member = target.FindMemberOrThrow(memberGuid);
-        member.ClipToPreviousMember.NonOverridenValue = newValue;
+        member.ClipToPreviousMember = newValue;
         ignoreInUndo = false;
         return new StructureMemberClipToMemberBelow_ChangeInfo(memberGuid, newValue);
     }
@@ -33,7 +33,7 @@ internal class StructureMemberClipToMemberBelow_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var member = target.FindMemberOrThrow(memberGuid);
-        member.ClipToPreviousMember.NonOverridenValue = originalValue;
+        member.ClipToPreviousMember = originalValue;
         return new StructureMemberClipToMemberBelow_ChangeInfo(memberGuid, originalValue);
     }
 

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs

@@ -52,10 +52,10 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
                 });
             }
             
-            if (member.Mask.NonOverridenValue is null)
+            if (member.EmbeddedMask is null)
                 return;
             
-            Resize(member.Mask.NonOverridenValue, member.Id, newBounds.Size, -newBounds.Pos, deletedMaskChunks);
+            Resize(member.EmbeddedMask, member.Id, newBounds.Size, -newBounds.Pos, deletedMaskChunks);
         });
 
         ignoreInUndo = false;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs

@@ -42,10 +42,10 @@ internal class Crop_Change : ResizeBasedChangeBase
                     Resize(frame, layer.Id, rect.Size, rect.Pos * -1, deletedChunks);
                 });
             }
-            if (member.Mask.NonOverridenValue is null)
+            if (member.EmbeddedMask is null)
                 return;
 
-            Resize(member.Mask.NonOverridenValue, member.Id, rect.Size, rect.Pos * -1, deletedMaskChunks);
+            Resize(member.EmbeddedMask, member.Id, rect.Size, rect.Pos * -1, deletedMaskChunks);
         });
         
         ignoreInUndo = false;

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs

@@ -113,12 +113,12 @@ internal sealed class FlipImage_Change : Change
                 }
                 // TODO: Add support for non-raster layers
 
-                if (member.Mask.NonOverridenValue is not null)
+                if (member.EmbeddedMask is not null)
                 {
-                    FlipImage(member.Mask.NonOverridenValue);
+                    FlipImage(member.EmbeddedMask);
                     changes.Add(
-                        new MaskArea_ChangeInfo(member.Id, member.Mask.NonOverridenValue.FindAffectedArea()));
-                    member.Mask.NonOverridenValue.CommitChanges();
+                        new MaskArea_ChangeInfo(member.Id, member.EmbeddedMask.FindAffectedArea()));
+                    member.EmbeddedMask.CommitChanges();
                 }
             }
         });

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -60,11 +60,11 @@ internal abstract class ResizeBasedChangeBase : Change
 
             // TODO: Add support for different Layer types?
 
-            if (member.Mask.NonOverridenValue is null)
+            if (member.EmbeddedMask is null)
                 return;
-            member.Mask.NonOverridenValue.EnqueueResize(_originalSize);
-            deletedMaskChunks[member.Id][0].ApplyChunksToImage(member.Mask.NonOverridenValue);
-            member.Mask.NonOverridenValue.CommitChanges();
+            member.EmbeddedMask.EnqueueResize(_originalSize);
+            deletedMaskChunks[member.Id][0].ApplyChunksToImage(member.EmbeddedMask);
+            member.EmbeddedMask.CommitChanges();
         });
 
         target.HorizontalSymmetryAxisY = _originalHorAxisY;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -57,10 +57,10 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
 
             // TODO: Check if adding support for different Layer types is necessary
 
-            if (member.Mask.Value is null)
+            if (member.EmbeddedMask is null)
                 return;
 
-            Resize(member.Mask.Value, member.Id, newSize, offset, deletedMaskChunks);
+            Resize(member.EmbeddedMask, member.Id, newSize, offset, deletedMaskChunks);
         });
 
         ignoreInUndo = false;

+ 10 - 10
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -101,12 +101,12 @@ internal class ResizeImage_Change : Change
 
             // Add support for different Layer types
 
-            if (member.Mask.Value is not null)
+            if (member.EmbeddedMask is not null)
             {
-                ScaleChunkyImage(member.Mask.Value);
-                var affected = member.Mask.Value.FindAffectedArea();
-                savedMaskChunks[member.Id] = new CommittedChunkStorage(member.Mask.Value, affected.Chunks);
-                member.Mask.Value.CommitChanges();
+                ScaleChunkyImage(member.EmbeddedMask);
+                var affected = member.EmbeddedMask.FindAffectedArea();
+                savedMaskChunks[member.Id] = new CommittedChunkStorage(member.EmbeddedMask, affected.Chunks);
+                member.EmbeddedMask.CommitChanges();
             }
         });
 
@@ -130,12 +130,12 @@ internal class ResizeImage_Change : Change
                 });
             }
 
-            if (member.Mask.Value is not null)
+            if (member.EmbeddedMask is not null)
             {
-                member.Mask.Value.EnqueueResize(originalSize);
-                member.Mask.Value.EnqueueClear();
-                savedMaskChunks[member.Id].ApplyChunksToImage(member.Mask.Value);
-                member.Mask.Value.CommitChanges();
+                member.EmbeddedMask.EnqueueResize(originalSize);
+                member.EmbeddedMask.EnqueueClear();
+                savedMaskChunks[member.Id].ApplyChunksToImage(member.EmbeddedMask);
+                member.EmbeddedMask.CommitChanges();
             }
         });
 

+ 9 - 9
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -158,10 +158,10 @@ internal sealed class RotateImage_Change : Change
 
                 // TODO: Add support for different Layer types
 
-                if (member.Mask.NonOverridenValue is null)
+                if (member.EmbeddedMask is null)
                     return;
 
-                Resize(member.Mask.NonOverridenValue, member.Id, deletedMaskChunks, null);
+                Resize(member.EmbeddedMask, member.Id, deletedMaskChunks, null);
             }
         });
 
@@ -199,10 +199,10 @@ internal sealed class RotateImage_Change : Change
                 }
             }
 
-            if (member.Mask.NonOverridenValue is null)
+            if (member.EmbeddedMask is null)
                 return;
 
-            Resize(member.Mask.NonOverridenValue, member.Id, deletedMaskChunks, null);
+            Resize(member.EmbeddedMask, member.Id, deletedMaskChunks, null);
         });
 
         return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
@@ -257,12 +257,12 @@ internal sealed class RotateImage_Change : Change
                 }
             }
 
-            if (member.Mask.NonOverridenValue is null)
+            if (member.EmbeddedMask is null)
                 return;
-            member.Mask.NonOverridenValue.EnqueueResize(originalSize);
-            deletedMaskChunks[member.Id].ApplyChunksToImage(member.Mask.Value);
-            revertChanges.Add(new LayerImageArea_ChangeInfo(member.Id, member.Mask.Value.FindAffectedArea()));
-            member.Mask.NonOverridenValue.CommitChanges();
+            member.EmbeddedMask.EnqueueResize(originalSize);
+            deletedMaskChunks[member.Id].ApplyChunksToImage(member.EmbeddedMask);
+            revertChanges.Add(new LayerImageArea_ChangeInfo(member.Id, member.EmbeddedMask.FindAffectedArea()));
+            member.EmbeddedMask.CommitChanges();
         });
 
         DisposeDeletedChunks();

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs

@@ -21,7 +21,7 @@ internal sealed class ApplyMask_Change : Change
     public override bool InitializeAndValidate(Document target)
     {
         var member = target.FindMember(structureMemberGuid);
-        bool isValid = member is not (null or FolderNode) && member.Mask.Value is not null;
+        bool isValid = member is not (null or FolderNode) && member.EmbeddedMask is not null;
 
         return isValid;
     }
@@ -31,7 +31,7 @@ internal sealed class ApplyMask_Change : Change
     {
         var layer = target.FindMemberOrThrow<ImageLayerNode>(structureMemberGuid)!;
         var layerImage = layer.GetLayerImageAtFrame(frame);
-        layerImage.EnqueueApplyMask(layer.Mask.Value!);
+        layerImage.EnqueueApplyMask(layer.EmbeddedMask);
         ignoreInUndo = false;
         var layerInfo = new LayerImageArea_ChangeInfo(structureMemberGuid, layerImage.FindAffectedArea());
         savedChunks = new CommittedChunkStorage(layerImage, layerInfo.Area.Chunks);

+ 15 - 1
src/PixiEditor.ChangeableDocument/Enums/MathNodeMode.cs

@@ -1,12 +1,26 @@
-namespace PixiEditor.ChangeableDocument.Enums;
+using System.ComponentModel;
+
+namespace PixiEditor.ChangeableDocument.Enums;
 
 public enum MathNodeMode
 {
+    [Description("MATH_ADD")]
     Add,
+    [Description("MATH_SUBTRACT")]
     Subtract,
+    [Description("MULTIPLY")]
     Multiply,
+    [Description("DIVIDE")]
     Divide,
+    [Description("SIN")]
     Sin,
+    [Description("COS")]
     Cos,
+    [Description("TAN")]
     Tan,
 }
+
+public static class MathNodeModeExtensions
+{
+    public static bool UsesYValue(this MathNodeMode mode) => !(mode is >= MathNodeMode.Sin and <= MathNodeMode.Tan);
+}

+ 43 - 0
src/PixiEditor.Linux/LinuxOperatingSystem.cs

@@ -1,4 +1,5 @@
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Input;
 using Avalonia.Threading;
 using PixiEditor.OperatingSystem;
 
@@ -7,6 +8,8 @@ namespace PixiEditor.Linux;
 public sealed class LinuxOperatingSystem : IOperatingSystem
 {
     public string Name { get; } = "Linux";
+    public string AnalyticsId => "Linux";
+    public string AnalyticsName => LinuxOSInformation.FromReleaseFile().ToString();
     public IInputKeys InputKeys { get; }
     public IProcessUtility ProcessUtility { get; }
     
@@ -24,4 +27,44 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
     {
         return true;
     }
+
+    class LinuxOSInformation
+    {
+        const string FilePath = "/etc/os-release";
+        
+        private LinuxOSInformation(string? name, string? version, bool available)
+        {
+            Name = name;
+            Version = version;
+            Available = available;
+        }
+
+        public static LinuxOSInformation FromReleaseFile()
+        {
+            if (!File.Exists(FilePath))
+            {
+                return new LinuxOSInformation(null, null, false);
+            }
+            
+            // Parse /etc/os-release file (e.g. 'NAME="Ubuntu"')
+            var lines = File.ReadAllLines(FilePath).Select<string, (string Key, string Value)>(x =>
+            {
+                var separatorIndex = x.IndexOf('=');
+                return (x[..separatorIndex], x[(separatorIndex + 1)..]);
+            }).ToList();
+            
+            var name = lines.FirstOrDefault(x => x.Key == "NAME").Value.Trim('"');
+            var version = lines.FirstOrDefault(x => x.Key == "VERSION").Value.Trim('"');
+            
+            return new LinuxOSInformation(name, version, true);
+        }
+        
+        public bool Available { get; }
+        
+        public string? Name { get; private set; }
+        
+        public string? Version { get; private set; }
+
+        public override string ToString() => $"{Name} {Version}";
+    }
 }

+ 3 - 0
src/PixiEditor.MacOs/MacOperatingSystem.cs

@@ -7,6 +7,9 @@ namespace PixiEditor.MacOs;
 public sealed class MacOperatingSystem : IOperatingSystem
 {
     public string Name { get; } = "MacOS";
+
+    public string AnalyticsId => "macOS";
+    
     public IInputKeys InputKeys { get; }
     public IProcessUtility ProcessUtility { get; }
     public void OpenUri(string uri)

+ 2 - 0
src/PixiEditor.Numerics/VecI.cs

@@ -86,6 +86,8 @@ public struct VecI : IEquatable<VecI>, IComparable<VecI>
         return new VecI(Math.Clamp(X, rect.Left, rect.Right), Math.Clamp(Y, rect.Top, rect.Bottom));
     }
 
+    public bool HasNegativeComponent() => X < 0 || Y < 0;
+
     public byte[] ToByteArray()
     {
         var data = new byte[sizeof(int) * 2];

+ 3 - 0
src/PixiEditor.OperatingSystem/IOperatingSystem.cs

@@ -8,6 +8,9 @@ public interface IOperatingSystem
     public static IOperatingSystem Current { get; protected set; }
     public string Name { get; }
 
+    public virtual string AnalyticsName => Environment.OSVersion.ToString();
+    public string AnalyticsId { get; }
+
     public IInputKeys InputKeys { get; }
     public IProcessUtility ProcessUtility { get; }
 

+ 3 - 0
src/PixiEditor.Windows/WindowsOperatingSystem.cs

@@ -9,6 +9,9 @@ namespace PixiEditor.Windows;
 public sealed class WindowsOperatingSystem : IOperatingSystem
 {
     public string Name => "Windows";
+    
+    public string AnalyticsId => "Windows";
+    
     public IInputKeys InputKeys { get; } = new WindowsInputKeys();
     public IProcessUtility ProcessUtility { get; } = new WindowsProcessUtility();
     

+ 33 - 0
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -0,0 +1,33 @@
+[
+  {
+    "Name": "PIXEL_ART_TOOLSET",
+    "Tools": [
+      "MoveViewport",
+      "RotateViewport",
+      "Move",
+      "Pen",
+      "Select",
+      "MagicWand",
+      "Lasso",
+      "FloodFill",
+      "Line",
+      "Ellipse",
+      "Rectangle",
+      "Eraser",
+      "ColorPicker",
+      "Brightness",
+      "Zoom"
+    ]
+  },
+  {
+    "Name": "VECTOR_TOOLSET",
+    "Tools": [
+      "MoveViewport",
+      "RotateViewport",
+      "Move",
+      "Line",
+      "Ellipse",
+      "Rectangle"
+    ]
+  }
+]

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

@@ -738,5 +738,14 @@
   "NUMBER": "Number",
   "ANIMATION": "Animation",
   "SAMPLE_IMAGE": "Sample Image",
-  "POSITION": "Position"
+  "POSITION": "Position",
+  "MATH_ADD": "Add",
+  "MATH_SUBTRACT": "Subtract",
+  "MULTIPLY": "Multiply",
+  "DIVIDE": "Divide",
+  "SIN": "Sin",
+  "COS": "Cos",
+  "TAN": "Tan",
+  "PIXEL_ART_TOOLSET": "Pixel Art",
+  "VECTOR_TOOLSET": "Vector"
 }

+ 7 - 0
src/PixiEditor/Exceptions/CommandInvocationException.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Exceptions;
+
+public class CommandInvocationException : Exception
+{
+    public CommandInvocationException(string commandName, Exception? innerException = null) : 
+        base($"Command '{commandName}' threw an exception", innerException) { }
+}

+ 1 - 42
src/PixiEditor/Fonts/NodeIcons.cs

@@ -1,42 +1 @@
-using System.Collections.ObjectModel;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
-
-namespace PixiEditor.Fonts;
-
-public static class NodeIcons
-{
-    public static ReadOnlyDictionary<Type, string> IconMap { get; } = new ReadOnlyDictionary<Type, string>(
-        new Dictionary<Type, string>
-        {
-            { typeof(TimeNode), "\uE900" },
-            { typeof(FolderNode), "\ue901" },
-            { typeof(CreateImageNode), "\ue902" },
-            { typeof(MergeNode), "\ue903" },
-            { typeof(ModifyImageLeftNode), "\ue904" },
-            { typeof(ImageLayerNode), "\ue905" },
-            { typeof(RasterizeShape), "\ue906" },
-            { typeof(SampleImageNode), "\ue907" },
-            { typeof(CombineColorNode), "\ue908" },
-            { typeof(ApplyFilterNode), "\ue909" },
-            { typeof(DistributePointsNode), "\ue90a" },
-            { typeof(LerpColorNode), "\ue90b" },
-            { typeof(NoiseNode), "\ue90c" },
-            { typeof(EllipseNode), "\ue90d" },
-            { typeof(MathNode), "\ue90e" },
-            { typeof(KernelFilterNode), "\ue90f" },
-            { typeof(CombineChannelsNode), "\ue915" },
-            { typeof(ColorMatrixFilterNode), "\ue911" },
-            { typeof(GrayscaleNode), "\ue912" },
-            { typeof(SeparateColorNode), "\ue913" },
-            { typeof(RemoveClosePointsNode), "\ue914" },
-            { typeof(SeparateChannelsNode), "\ue910" },
-            { typeof(CombineVecDNode), "\ue916" },
-            { typeof(CombineVecINode), "\ue917" },
-            { typeof(SeparateVecDNode), "\ue918" },
-            { typeof(SeparateVecINode), "\ue919" }
-        });
-}
+

+ 12 - 3
src/PixiEditor/Helpers/CrashHelper.cs

@@ -4,6 +4,7 @@ using System.IO;
 using System.Net.Http;
 using System.Runtime.CompilerServices;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using ByteSizeLib;
 using Hardware.Info;
@@ -12,7 +13,7 @@ using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.Helpers;
 
-internal class CrashHelper
+internal partial class CrashHelper
 {
     private readonly IHardwareInfo hwInfo;
 
@@ -84,7 +85,7 @@ internal class CrashHelper
             .AppendLine("\n-------Crash message-------")
             .Append(e.GetType().ToString())
             .Append(": ")
-            .AppendLine(e.Message);
+            .AppendLine(TrimFilePaths(e.Message));
         {
             var innerException = e.InnerException;
             while (innerException != null)
@@ -93,7 +94,7 @@ internal class CrashHelper
                     .Append("\n-----Inner exception-----\n")
                     .Append(innerException.GetType().ToString())
                     .Append(": ")
-                    .Append(innerException.Message);
+                    .Append(TrimFilePaths(innerException.Message));
                 innerException = innerException.InnerException;
             }
         }
@@ -112,6 +113,8 @@ internal class CrashHelper
             }
         }
     }
+
+    private static string TrimFilePaths(string text) => FilePathRegex().Replace(text, "{{ FILE PATH }}");
     
     public static void SendExceptionInfoToWebhook(Exception e, bool wait = false,
         [CallerFilePath] string filePath = "<unknown>", [CallerMemberName] string memberName = "<unknown>")
@@ -156,4 +159,10 @@ internal class CrashHelper
         }
         catch { }
     }
+
+    /// <summary>
+    /// Matches file paths with spaces when in quotes, otherwise not
+    /// </summary>
+    [GeneratedRegex(@"'([^']*[\/\\][^']*)'|(\S*[\/\\]\S*)")]
+    private static partial Regex FilePathRegex();
 }

+ 0 - 10
src/PixiEditor/Helpers/IFolderHandlerFactory.cs

@@ -1,10 +0,0 @@
-using PixiEditor.Models.DocumentModels;
-using PixiEditor.Models.Handlers;
-
-namespace PixiEditor.Helpers;
-
-internal interface IFolderHandlerFactory
-{
-    public IDocument Document { get; }
-    public IFolderHandler CreateFolderHandler(DocumentInternalParts helper, Guid infoGuidValue);
-}

+ 0 - 10
src/PixiEditor/Helpers/ILayerHandlerFactory.cs

@@ -1,10 +0,0 @@
-using PixiEditor.Models.DocumentModels;
-using PixiEditor.Models.Handlers;
-
-namespace PixiEditor.Helpers;
-
-internal interface ILayerHandlerFactory
-{
-    public IDocument Document { get; }
-    public ILayerHandler CreateLayerHandler(DocumentInternalParts helper, Guid infoGuidValue);
-}

+ 28 - 27
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -81,33 +81,21 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<CommandController>()
             .AddSingleton<DocumentManagerViewModel>()
             // Tools
-            .AddSingleton<IToolHandler, MoveViewportToolViewModel>()
-            .AddSingleton<IToolHandler, RotateViewportToolViewModel>()
-            .AddSingleton<IMoveToolHandler, MoveToolViewModel>()
-            .AddSingleton<IToolHandler, MoveToolViewModel>(x => (MoveToolViewModel)x.GetService<IMoveToolHandler>())
-            .AddSingleton<IPenToolHandler, PenToolViewModel>()
-            .AddSingleton<IToolHandler, PenToolViewModel>(x => (PenToolViewModel)x.GetService<IPenToolHandler>())
-            .AddSingleton<ISelectToolHandler, SelectToolViewModel>()
-            .AddSingleton<IToolHandler, SelectToolViewModel>(x => (SelectToolViewModel)x.GetService<ISelectToolHandler>())
-            .AddSingleton<IMagicWandToolHandler, MagicWandToolViewModel>()
-            .AddSingleton<IToolHandler, MagicWandToolViewModel>(x => (MagicWandToolViewModel)x.GetService<IMagicWandToolHandler>())
-            .AddSingleton<ILassoToolHandler, LassoToolViewModel>()
-            .AddSingleton<IToolHandler, LassoToolViewModel>(x => (LassoToolViewModel)x.GetService<ILassoToolHandler>())
-            .AddSingleton<IFloodFillToolHandler, FloodFillToolViewModel>()
-            .AddSingleton<IToolHandler, FloodFillToolViewModel>(x => (FloodFillToolViewModel)x.GetService<IFloodFillToolHandler>())
-            .AddSingleton<ILineToolHandler, LineToolViewModel>()
-            .AddSingleton<IToolHandler, LineToolViewModel>(x => (LineToolViewModel)x.GetService<ILineToolHandler>())
-            .AddSingleton<IEllipseToolHandler, EllipseToolViewModel>()
-            .AddSingleton<IToolHandler, EllipseToolViewModel>(x => (EllipseToolViewModel)x.GetService<IEllipseToolHandler>())
-            .AddSingleton<IRectangleToolHandler, RectangleToolViewModel>()
-            .AddSingleton<IToolHandler, RectangleToolViewModel>(x => (RectangleToolViewModel)x.GetService<IRectangleToolHandler>())
-            .AddSingleton<IEraserToolHandler, EraserToolViewModel>()
-            .AddSingleton<IToolHandler, EraserToolViewModel>(x => (EraserToolViewModel)x.GetService<IEraserToolHandler>())
-            .AddSingleton<IColorPickerHandler, ColorPickerToolViewModel>()
-            .AddSingleton<IToolHandler, ColorPickerToolViewModel>(x => (ColorPickerToolViewModel)x.GetService<IColorPickerHandler>())
-            .AddSingleton<IBrightnessToolHandler, BrightnessToolViewModel>()
-            .AddSingleton<IToolHandler, BrightnessToolViewModel>(x => (BrightnessToolViewModel)x.GetService<IBrightnessToolHandler>())
-            .AddSingleton<IToolHandler, ZoomToolViewModel>()
+            .AddTool<MoveViewportToolViewModel>()
+            .AddTool<RotateViewportToolViewModel>()
+            .AddTool<IMoveToolHandler, MoveToolViewModel>()
+            .AddTool<IPenToolHandler, PenToolViewModel>()
+            .AddTool<ISelectToolHandler, SelectToolViewModel>()
+            .AddTool<IMagicWandToolHandler, MagicWandToolViewModel>()
+            .AddTool<ILassoToolHandler, LassoToolViewModel>()
+            .AddTool<IFloodFillToolHandler, FloodFillToolViewModel>()
+            .AddTool<ILineToolHandler, LineToolViewModel>()
+            .AddTool<IEllipseToolHandler, EllipseToolViewModel>()
+            .AddTool<IRectangleToolHandler, RectangleToolViewModel>()
+            .AddTool<IEraserToolHandler, EraserToolViewModel>()
+            .AddTool<IColorPickerHandler, ColorPickerToolViewModel>()
+            .AddTool<IBrightnessToolHandler, BrightnessToolViewModel>()
+            .AddTool<ZoomToolViewModel>()
             // File types
             .AddSingleton<IoFileType, PixiFileType>()
             .AddSingleton<IoFileType, PngFileType>()
@@ -167,6 +155,19 @@ internal static class ServiceCollectionHelpers
             url = Environment.GetEnvironmentVariable("PixiEditorAnalytics");
         }
     }
+    
+    private static IServiceCollection AddTool<T, T1>(this IServiceCollection collection)
+        where T : class, IToolHandler where T1 : class, T
+    {
+        return collection.AddSingleton<T, T1>()
+            .AddSingleton<IToolHandler, T1>(x => (T1)x.GetRequiredService<T>());
+    }
+    
+    private static IServiceCollection AddTool<T>(this IServiceCollection collection)
+        where T : class, IToolHandler
+    {
+        return collection.AddSingleton<IToolHandler, T>();
+    }
 
     private static IServiceCollection AddMenuBuilders(this IServiceCollection collection)
     {

+ 1 - 0
src/PixiEditor/Models/AnalyticsAPI/AnalyticEventTypes.cs

@@ -12,6 +12,7 @@ public class AnalyticEventTypes
     public static string GeneralCommand { get; } = GetEventType("GeneralCommand");
     public static string SwitchTool { get; } = GetEventType("SwitchTool");
     public static string UseTool { get; } = GetEventType("UseTool");
+    public static string ResumeSession { get; } = GetEventType("ResumeSession");
 
     private static string GetEventType(string value) => $"PixiEditor.{value}";
 }

+ 8 - 2
src/PixiEditor/Models/AnalyticsAPI/AnalyticSessionInfo.cs

@@ -1,8 +1,14 @@
-namespace PixiEditor.Models.AnalyticsAPI;
+using PixiEditor.OperatingSystem;
 
-public class AnalyticSessionInfo
+namespace PixiEditor.Models.AnalyticsAPI;
+
+public class AnalyticSessionInfo(IOperatingSystem os)
 {
     public Version Version { get; set; }
 
     public string BuildId { get; set; }
+
+    public string? PlatformId { get; set; } = os.AnalyticsId;
+
+    public string? PlatformName { get; set; } = os.AnalyticsName;
 }

+ 4 - 2
src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs

@@ -6,6 +6,7 @@ using System.Text.Json.Serialization;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Input;
 using PixiEditor.Numerics;
+using PixiEditor.OperatingSystem;
 
 namespace PixiEditor.Models.AnalyticsAPI;
 
@@ -31,9 +32,10 @@ public class AnalyticsClient
 
     public async Task<Guid?> CreateSessionAsync(CancellationToken cancellationToken = default)
     {
-        var session = new AnalyticSessionInfo()
+        var session = new AnalyticSessionInfo(IOperatingSystem.Current)
         {
-            Version = VersionHelpers.GetCurrentAssemblyVersion(), BuildId = VersionHelpers.GetBuildId()
+            Version = VersionHelpers.GetCurrentAssemblyVersion(),
+            BuildId = VersionHelpers.GetBuildId(),
         };
         
         var response = await _client.PostAsJsonAsync("sessions/new", session, _options, cancellationToken);

+ 25 - 7
src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs

@@ -5,6 +5,7 @@ namespace PixiEditor.Models.AnalyticsAPI;
 public class AnalyticsPeriodicReporter
 {
     private int _sendExceptions = 0;
+    private bool _resumeSession;
     
     private readonly SemaphoreSlim _semaphore = new(1, 1);
     private readonly AnalyticsClient _client;
@@ -28,8 +29,16 @@ public class AnalyticsPeriodicReporter
         _client = client;
     }
 
-    public void Start()
+    public void Start(Guid? sessionId)
     {
+        if (sessionId != null)
+        {
+            SessionId = sessionId.Value;
+            _resumeSession = true;
+            
+            _backlog.Add(new AnalyticEvent { Time = DateTime.UtcNow, EventType = AnalyticEventTypes.ResumeSession });
+        }
+
         Task.Run(RunAsync);
     }
 
@@ -42,6 +51,12 @@ public class AnalyticsPeriodicReporter
 
     public void AddEvent(AnalyticEvent value)
     {
+        // Don't send startup as it gives invalid results for crash resumed sessions
+        if (value.EventType == AnalyticEventTypes.Startup && _resumeSession)
+        {
+            return;
+        }
+        
         Task.Run(() =>
         {
             _semaphore.Wait();
@@ -59,14 +74,17 @@ public class AnalyticsPeriodicReporter
 
     private async Task RunAsync()
     {
-        var createSession = await _client.CreateSessionAsync(_cancellationToken.Token);
-
-        if (!createSession.HasValue)
+        if (!_resumeSession)
         {
-            return;
-        }
+            var createSession = await _client.CreateSessionAsync(_cancellationToken.Token);
+
+            if (!createSession.HasValue)
+            {
+                return;
+            }
 
-        SessionId = createSession.Value;
+            SessionId = createSession.Value;
+        }
 
         Task.Run(RunHeartbeatAsync);
 

+ 12 - 4
src/PixiEditor/Models/Commands/CommandController.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using Avalonia.Media;
 using Microsoft.Extensions.DependencyInjection;
 using Newtonsoft.Json;
+using PixiEditor.Exceptions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.AnalyticsAPI;
@@ -376,10 +377,17 @@ internal class CommandController
         {
             Analytics.SendCommand(name, (parameter as CommandExecutionContext)?.SourceInfo);
         }
-                
-        object result = method.Invoke(instance, parameters);
-        if (result is Task task)
-            task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+
+        try
+        {
+            object result = method.Invoke(instance, parameters);
+            if (result is Task task)
+                task.ContinueWith(ActionOnException, TaskContinuationOptions.OnlyOnFaulted);
+        }
+        catch (TargetInvocationException e)
+        {
+            throw new CommandInvocationException(name, e);
+        }
 
         return;
 

+ 20 - 0
src/PixiEditor/Models/Config/ConfigManager.cs

@@ -0,0 +1,20 @@
+using System.Reflection;
+using Avalonia.Platform;
+using Newtonsoft.Json;
+using PixiEditor.Views;
+
+namespace PixiEditor.Models.Config;
+
+public class ConfigManager
+{
+    public T GetConfig<T>(string configName)
+    {
+        string path = $"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Data/Configs/{configName}.json";
+
+        using Stream config = AssetLoader.Open(new Uri(path));
+        using StreamReader reader = new(config);
+        
+        string json = reader.ReadToEnd();
+        return JsonConvert.DeserializeObject<T>(json);
+    }
+}

+ 13 - 0
src/PixiEditor/Models/Config/ToolSetConfig.cs

@@ -0,0 +1,13 @@
+using Newtonsoft.Json;
+
+namespace PixiEditor.Models.Config;
+
+public class ToolSetsConfig : List<ToolSetConfig>
+{
+}
+
+public class ToolSetConfig
+{
+    public string Name { get; set; }
+    public List<string> Tools { get; set; }
+}

+ 35 - 25
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -1,4 +1,5 @@
 using System.Collections.Immutable;
+using System.Reflection;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Exceptions;
@@ -19,6 +20,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Layers;
 using PixiEditor.Numerics;
 using PixiEditor.ViewModels.Document;
+using PixiEditor.ViewModels.Document.Nodes;
 using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Models.DocumentModels;
@@ -55,15 +57,7 @@ internal class DocumentUpdater
         switch (arbitraryInfo)
         {
             case CreateStructureMember_ChangeInfo info:
-                if (info is CreateLayer_ChangeInfo layerChangeInfo)
-                {
-                    ProcessCreateNode<LayerViewModel>(info);
-                }
-                else if (info is CreateFolder_ChangeInfo folderChangeInfo)
-                {
-                    ProcessCreateNode<FolderViewModel>(info);
-                }
-
+                ProcessCreateNode(info);
                 ProcessCreateStructureMember(info);
                 break;
             case DeleteStructureMember_ChangeInfo info:
@@ -170,7 +164,7 @@ internal class DocumentUpdater
                 ClearSelectedKeyFrames(info);
                 break;
             case CreateNode_ChangeInfo info:
-                ProcessCreateNode<NodeViewModel>(info);
+                ProcessCreateNode(info);
                 break;
             case DeleteNode_ChangeInfo info:
                 ProcessDeleteNode(info);
@@ -511,28 +505,44 @@ internal class DocumentUpdater
         doc.AnimationHandler.ClearSelectedKeyFrames();
     }
 
-    private void ProcessCreateNode<T>(CreateNode_ChangeInfo info) where T : NodeViewModel, new()
+    private void ProcessCreateNode(CreateNode_ChangeInfo info)
     {
-        T node = new T()
-        {
-            InternalName = info.InternalName, Id = info.Id, Document = (DocumentViewModel)doc, Internals = helper
-        };
+        var nodeType = info.Metadata.NodeType;
+        
+        var ns = nodeType.Namespace.Replace("ChangeableDocument.Changeables.Graph.", "ViewModels.Document.");
+        var name = nodeType.Name.Replace("Node", "NodeViewModel");
+        var fullViewModelName = $"{ns}.{name}";
+        var nodeViewModelType = Type.GetType(fullViewModelName);
+
+        if (nodeViewModelType == null)
+            throw new NullReferenceException($"No ViewModel found for {nodeType}. Looking for '{fullViewModelName}'");
+        
+        var viewModel = (NodeViewModel)Activator.CreateInstance(nodeViewModelType);
 
-        node.SetName(info.NodeName);
-        node.SetPosition(info.Position);
+        InitializeNodeViewModel(info, viewModel);
+    }
 
-        List<INodePropertyHandler> inputs = CreateProperties(info.Inputs, node, true);
-        List<INodePropertyHandler> outputs = CreateProperties(info.Outputs, node, false);
-        node.Inputs.AddRange(inputs);
-        node.Outputs.AddRange(outputs);
-        doc.NodeGraphHandler.AddNode(node);
+    private void InitializeNodeViewModel(CreateNode_ChangeInfo info, NodeViewModel viewModel)
+    {
+        viewModel.Initialize(info.Id, info.InternalName, (DocumentViewModel)doc, helper);
+        
+        viewModel.SetName(info.NodeName);
+        viewModel.SetPosition(info.Position);
+        
+        var inputs = CreateProperties(info.Inputs, viewModel, true);
+        var outputs = CreateProperties(info.Outputs, viewModel, false);
+        viewModel.Inputs.AddRange(inputs);
+        viewModel.Outputs.AddRange(outputs);
+        doc.NodeGraphHandler.AddNode(viewModel);
 
-        node.Metadata = info.Metadata;
+        viewModel.Metadata = info.Metadata;
 
-        AddZoneIfNeeded(info, node);
+        AddZoneIfNeeded(info, viewModel);
+        
+        viewModel.OnInitialized();
     }
 
-    private void AddZoneIfNeeded<T>(CreateNode_ChangeInfo info, T node) where T : NodeViewModel, new()
+    private void AddZoneIfNeeded(CreateNode_ChangeInfo info, NodeViewModel node)
     {
         if (node.Metadata?.PairNodeGuid != null)
         {

+ 7 - 0
src/PixiEditor/Models/Events/NodePropertyValueChanged.cs

@@ -0,0 +1,7 @@
+using PixiEditor.Models.Handlers;
+
+namespace PixiEditor.Models.Events;
+
+public delegate void NodePropertyValueChanged(INodePropertyHandler property, NodePropertyValueChangedArgs args);
+
+public record NodePropertyValueChangedArgs(object OldValue, object NewValue);

+ 15 - 11
src/PixiEditor/Models/ExceptionHandling/CrashReport.cs

@@ -12,6 +12,7 @@ using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Helpers;
+using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.Commands;
 using PixiEditor.Parser;
 using PixiEditor.ViewModels;
@@ -115,6 +116,8 @@ internal class CrashReport : IDisposable
         builder
             .AppendLine("Environment:")
             .AppendLine($"  Thread Count: {GetFormatted(() => Process.GetCurrentProcess().Threads.Count)}")
+            .AppendLine("Analytics:")
+            .AppendLine($"  Analytics Id: {GetFormatted(() => AnalyticsPeriodicReporter.Instance?.SessionId)}")
             .AppendLine("\nCulture:")
             .AppendLine($"  Selected language: {GetPreferenceFormatted("LanguageCode", true, "system")}")
             .AppendLine($"  Current Culture: {GetFormatted(() => CultureInfo.CurrentCulture)}")
@@ -267,15 +270,16 @@ internal class CrashReport : IDisposable
 
     public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
 
-    public bool TryRecoverDocuments(out List<RecoveredPixi> list)
+    public bool TryRecoverDocuments(out List<RecoveredPixi> list, out CrashedSessionInfo? sessionInfo)
     {
         try
         {
-            list = RecoverDocuments();
+            list = RecoverDocuments(out sessionInfo);
         }
         catch (Exception e)
         {
             list = null;
+            sessionInfo = null;
             CrashHelper.SendExceptionInfoToWebhook(e);
             return false;
         }
@@ -283,12 +287,12 @@ internal class CrashReport : IDisposable
         return true;
     }
 
-    public List<RecoveredPixi> RecoverDocuments()
+    public List<RecoveredPixi> RecoverDocuments(out CrashedSessionInfo? sessionInfo)
     {
         List<RecoveredPixi> recoveredDocuments = new();
 
-        var paths = TryGetOriginalPaths();
-        if (paths == null)
+        sessionInfo = TryGetSessionInfo();
+        if (sessionInfo?.OpenedDocuments == null)
         {
             recoveredDocuments.AddRange(
                 ZipFile.Entries
@@ -300,11 +304,11 @@ internal class CrashReport : IDisposable
             return recoveredDocuments;
         }
 
-        recoveredDocuments.AddRange(paths.Select(path => new RecoveredPixi(path.Value, ZipFile.GetEntry($"Documents/{path.Key}"))));
+        recoveredDocuments.AddRange(sessionInfo.OpenedDocuments.Select(path => new RecoveredPixi(path.OriginalPath, ZipFile.GetEntry($"Documents/{path.ZipName}"))));
 
         return recoveredDocuments;
 
-        Dictionary<string, string>? TryGetOriginalPaths()
+        CrashedSessionInfo? TryGetSessionInfo()
         {
             var originalPathsEntry = ZipFile.Entries.FirstOrDefault(entry => entry.FullName == "DocumentInfo.json");
 
@@ -317,7 +321,7 @@ internal class CrashReport : IDisposable
                 using var reader = new StreamReader(stream);
                 string json = reader.ReadToEnd();
 
-                return JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
+                return JsonConvert.DeserializeObject<CrashedSessionInfo>(json);
             }
             catch
             {
@@ -373,7 +377,7 @@ internal class CrashReport : IDisposable
 
         // Write the documents into zip
         int counter = 0;
-        var originalPaths = new Dictionary<string, string>();
+        var originalPaths = new List<CrashedFileInfo>();
         //TODO: Implement
         foreach (var document in documents)
         {
@@ -389,7 +393,7 @@ internal class CrashReport : IDisposable
                 using Stream documentStream = archive.CreateEntry($"Documents/{nameInZip}").Open();
                 documentStream.Write(serialized);
 
-                originalPaths.Add(nameInZip, document.FullFilePath);
+                originalPaths.Add(new CrashedFileInfo(nameInZip, document.FullFilePath));
             }
             catch { }
             counter++;
@@ -400,7 +404,7 @@ internal class CrashReport : IDisposable
             using Stream jsonStream = archive.CreateEntry("DocumentInfo.json").Open();
             using StreamWriter writer = new StreamWriter(jsonStream);
 
-            string originalPathsJson = JsonConvert.SerializeObject(originalPaths, Formatting.Indented);
+            string originalPathsJson = JsonConvert.SerializeObject(new CrashedSessionInfo(AnalyticsPeriodicReporter.Instance?.SessionId ?? Guid.Empty, originalPaths), Formatting.Indented);
             writer.Write(originalPathsJson);
         }
     }

+ 16 - 0
src/PixiEditor/Models/ExceptionHandling/CrashedFileInfo.cs

@@ -0,0 +1,16 @@
+namespace PixiEditor.Models.ExceptionHandling;
+
+public class CrashedFileInfo
+{
+    public string ZipName { get; set; }
+    
+    public string OriginalPath { get; set; }
+    
+    public CrashedFileInfo() { }
+
+    public CrashedFileInfo(string zipName, string originalPath)
+    {
+        ZipName = zipName;
+        OriginalPath = originalPath;
+    }
+}

+ 18 - 0
src/PixiEditor/Models/ExceptionHandling/CrashedSessionInfo.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.Models.ExceptionHandling;
+
+public class CrashedSessionInfo
+{
+    public Guid? AnalyticsSessionId { get; set; }
+    
+    public ICollection<CrashedFileInfo>? OpenedDocuments { get; set; }
+
+    public CrashedSessionInfo()
+    {
+    }
+
+    public CrashedSessionInfo(Guid? analyticsSessionId, ICollection<CrashedFileInfo> openedDocuments)
+    {
+        AnalyticsSessionId = analyticsSessionId;
+        OpenedDocuments = openedDocuments;
+    }
+}

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

@@ -31,8 +31,6 @@ internal interface IDocument : IHandler
     public bool AllChangesSaved { get; }
     public string CoordinatesString { get; set; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }
-    public ILayerHandlerFactory LayerHandlerFactory { get; }
-    public IFolderHandlerFactory FolderHandlerFactory { get; }
     public ITransformHandler TransformHandler { get; }
     public bool Busy { get; set; }
     public ILineOverlayHandler LineToolOverlayHandler { get; }

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

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using PixiEditor.Models.Events;
 
 namespace PixiEditor.Models.Handlers;
 
@@ -10,5 +11,7 @@ public interface INodePropertyHandler
     public bool IsInput { get; }
     public INodePropertyHandler? ConnectedOutput { get; set; }
     public ObservableCollection<INodePropertyHandler> ConnectedInputs { get; }
+
+    public event NodePropertyValueChanged ValueChanged;
     public INodeHandler Node { get; set; }
 }

+ 7 - 0
src/PixiEditor/Models/Handlers/IToolSetHandler.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Models.Handlers;
+
+internal interface IToolSetHandler : IHandler
+{
+    public string Name { get; }
+    public ICollection<IToolHandler> Tools { get; }
+}

+ 4 - 2
src/PixiEditor/Models/Handlers/IToolsHandler.cs

@@ -3,6 +3,7 @@ using Avalonia.Input;
 using PixiEditor.Models.Preferences;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
+using PixiEditor.Models.Config;
 using PixiEditor.Models.Events;
 using PixiEditor.Numerics;
 using PixiEditor.ViewModels.Tools;
@@ -14,11 +15,12 @@ internal interface IToolsHandler : IHandler
     public void SetTool(object parameter);
     public void RestorePreviousTool();
     public IToolHandler ActiveTool { get; }
-    public ICollection<IToolHandler> ToolSet { get; }
+    public IToolSetHandler ActiveToolSet { get; } 
+    public ICollection<IToolSetHandler> AllToolSets { get; }
     public RightClickMode RightClickMode { get; set; }
     public bool EnableSharedToolbar { get; set; }
     public event EventHandler<SelectedToolEventArgs> SelectedToolChanged;
-    public void SetupTools(IServiceProvider services);
+    public void SetupTools(IServiceProvider services, ToolSetsConfig toolSetConfig);
     public void SetupToolsTooltipShortcuts(IServiceProvider services);
     public void SetActiveTool<T>(bool transient) where T : IToolHandler;
     public void SetActiveTool(Type toolType, bool transient);

+ 5 - 5
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -196,8 +196,8 @@ internal class AffectedAreasGatherer
             }
 
             var chunks = result.FindAllChunks();
-            if (layer.Mask.Value is not null && layer.MaskIsVisible.Value && useMask)
-                chunks.IntersectWith(layer.Mask.Value.FindAllChunks());
+            if (layer.EmbeddedMask is not null && layer.MaskIsVisible.Value && useMask)
+                chunks.IntersectWith(layer.EmbeddedMask.FindAllChunks());
             AddToMainImage(new AffectedArea(chunks));
         }
         else
@@ -210,9 +210,9 @@ internal class AffectedAreasGatherer
     {
         if (!tracker.Document.TryFindMember(memberGuid, out var member))
             return;
-        if (member.Mask.Value is not null)
+        if (member.EmbeddedMask is not null)
         {
-            var chunks = member.Mask.Value.FindAllChunks();
+            var chunks = member.EmbeddedMask.FindAllChunks();
             AddToMaskPreview(memberGuid, new AffectedArea(chunks));
         }
 
@@ -305,7 +305,7 @@ internal class AffectedAreasGatherer
     {
         tracker.Document.ForEveryReadonlyMember((member) =>
         {
-            if (member.Mask.Value is not null)
+            if (member.EmbeddedMask is not null)
                 AddWholeCanvasToMaskPreview(member.Id);
         });
     }

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

@@ -193,7 +193,7 @@ internal class MemberPreviewUpdater
             if (member is null)
                 continue;
 
-            if (forMasks && member.Mask.NonOverridenValue is null)
+            if (forMasks && member.EmbeddedMask is null)
             {
                 newPreviewBitmapSizes.Add(guid, null);
                 continue;
@@ -274,7 +274,7 @@ internal class MemberPreviewUpdater
     private RectI? GetOrFindMemberTightBounds(IReadOnlyStructureNode member, int atFrame,
         AffectedArea currentlyAffectedArea, bool forMask)
     {
-        if (forMask && member.Mask.NonOverridenValue is null)
+        if (forMask && member.EmbeddedMask is null)
             throw new InvalidOperationException();
 
         RectI? prevTightBounds = null;
@@ -304,11 +304,11 @@ internal class MemberPreviewUpdater
     /// </summary>
     private RectI? FindLayerTightBounds(IReadOnlyLayerNode layer, int frame, bool forMask)
     {
-        if (layer.Mask.NonOverridenValue is null && forMask)
+        if (layer.EmbeddedMask is null && forMask)
             throw new InvalidOperationException();
 
-        if (layer.Mask.NonOverridenValue is not null && forMask)
-            return FindImageTightBoundsFast(layer.Mask.Value);
+        if (layer.EmbeddedMask is not null && forMask)
+            return FindImageTightBoundsFast(layer.EmbeddedMask);
 
         if (layer is IReadOnlyImageNode raster)
         {
@@ -325,9 +325,9 @@ internal class MemberPreviewUpdater
     {
         if (forMask)
         {
-            if (folder.Mask.Value is null)
+            if (folder.EmbeddedMask is null)
                 throw new InvalidOperationException();
-            return FindImageTightBoundsFast(folder.Mask.Value);
+            return FindImageTightBoundsFast(folder.EmbeddedMask);
         }
 
         /*RectI? combinedBounds = null;
@@ -717,7 +717,7 @@ internal class MemberPreviewUpdater
                 foreach (var chunk in affArea.Value.Chunks)
                 {
                     var pos = chunk * ChunkResolution.Full.PixelSize();
-                    member.Mask!.Value.DrawMostUpToDateChunkOn
+                    member.EmbeddedMask!.DrawMostUpToDateChunkOn
                     (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface.DrawingSurface, pos,
                         scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
                 }

+ 1 - 37
src/PixiEditor/PixiEditor.csproj

@@ -79,6 +79,7 @@
     <AvaloniaResource Include="Data\Languages\**"/>
     <AvaloniaResource Include="Data\ShortcutActionMaps\**"/>
     <AvaloniaResource Include="Data\BetaExampleFiles\**"/>
+    <AvaloniaResource Include="Data\Configs\**"/>
   </ItemGroup>
 
   <ItemGroup>
@@ -133,41 +134,4 @@
     </None>
   </ItemGroup>
 
-  <ItemGroup>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Clipboard\Copy.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Clipboard\Cut.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Clipboard\Paste.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Clipboard\PasteAsNewLayer.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Clipboard\PasteReferenceLayer.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\CenterContent.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\ResizeCanvas.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\ResizeDocument.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\Rotate180Deg.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\Rotate180DegLayers.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\Rotate270Deg.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\Rotate270DegLayers.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\Rotate90Deg.png"/>
-    <UpToDateCheckInput Remove="Images\Commands\PixiEditor\Document\Rotate90DegLayers.png"/>
-    <UpToDateCheckInput Remove="Images\News\Article.png"/>
-    <UpToDateCheckInput Remove="Images\News\Misc.png"/>
-    <UpToDateCheckInput Remove="Images\News\NewVersion.png"/>
-    <UpToDateCheckInput Remove="Images\News\OfficialAnnouncement.png"/>
-    <UpToDateCheckInput Remove="Images\News\YouTube.png"/>
-    <UpToDateCheckInput Remove="Styles\Templates\NodeProperties\ImageNodePropertyView.axaml"/>
-  </ItemGroup>
-
-  <ItemGroup>
-    <Compile Update="Views\Dock\TimelineDockView.axaml.cs">
-      <DependentUpon>TimelineDockView.axaml</DependentUpon>
-      <SubType>Code</SubType>
-    </Compile>
-    <Compile Update="Views\Nodes\Properties\NodeSocket.cs">
-      <DependentUpon>NodeSocket.axaml</DependentUpon>
-      <SubType>Code</SubType>
-    </Compile>
-    <Compile Update="Views\Nodes\ConnectionView.cs">
-      <SubType>Code</SubType>
-    </Compile>
-  </ItemGroup>
-
 </Project>

+ 8 - 1
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -87,7 +87,14 @@
                                         InputNodePosition="{Binding InputNode.PositionBindable}"
                                         OutputNodePosition="{Binding OutputNode.PositionBindable}"
                                         InputProperty="{Binding InputProperty}"
-                                        OutputProperty="{Binding OutputProperty}" />
+                                        OutputProperty="{Binding OutputProperty}">
+                                        <nodes:ConnectionView.IsVisible>
+                                            <MultiBinding Converter="{x:Static BoolConverters.And}">
+                                                <Binding Path="InputProperty.IsVisible" />
+                                                <Binding Path="OutputProperty.IsVisible" />
+                                            </MultiBinding>
+                                        </nodes:ConnectionView.IsVisible>
+                                    </nodes:ConnectionView>
                                 </DataTemplate>
                             </ItemsControl.ItemTemplate>
                         </ItemsControl>

+ 1 - 1
src/PixiEditor/Styles/Templates/NodePropertyViewTemplate.axaml

@@ -6,7 +6,7 @@
         <Setter Property="ClipToBounds" Value="False" />
         <Setter Property="Template">
             <ControlTemplate>
-                <Grid Margin="-5, 2" ColumnDefinitions="15, *, 15" MinHeight="18">
+                <Grid Margin="-5, 2" ColumnDefinitions="15, *, 15" MinHeight="18" IsVisible="{Binding DataContext.IsVisible, RelativeSource={RelativeSource TemplatedParent}}">
                     <properties:NodeSocket Name="PART_InputSocket"
                                            ClipToBounds="False"
                                            Node="{Binding DataContext.Node, RelativeSource={RelativeSource TemplatedParent}}"

+ 17 - 23
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -46,6 +46,7 @@ using PixiEditor.Models.Structures;
 using PixiEditor.Models.Tools;
 using PixiEditor.Numerics;
 using PixiEditor.Parser.Skia;
+using PixiEditor.ViewModels.Document.Nodes;
 using PixiEditor.ViewModels.Document.TransformOverlays;
 using PixiEditor.Views.Overlays.SymmetryOverlay;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
@@ -147,7 +148,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     private double verticalSymmetryAxisX;
     public double VerticalSymmetryAxisXBindable => verticalSymmetryAxisX;
 
-    private readonly HashSet<StructureMemberViewModel> softSelectedStructureMembers = new();
+    private readonly HashSet<IStructureMemberHandler> softSelectedStructureMembers = new();
 
     public bool UpdateableChangeActive => Internals.ChangeController.IsChangeActive;
 
@@ -209,8 +210,6 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     IDocumentOperations IDocument.Operations => Operations;
     ITransformHandler IDocument.TransformHandler => TransformViewModel;
     ILineOverlayHandler IDocument.LineToolOverlayHandler => LineToolOverlayViewModel;
-    public ILayerHandlerFactory LayerHandlerFactory { get; }
-    public IFolderHandlerFactory FolderHandlerFactory { get; }
     IReferenceLayerHandler IDocument.ReferenceLayerHandler => ReferenceLayerViewModel;
     IAnimationHandler IDocument.AnimationHandler => AnimationDataViewModel;
 
@@ -224,8 +223,6 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         EventInlet = new DocumentEventsModule(this, Internals);
         Operations = new DocumentOperationsModule(this, Internals);
 
-        LayerHandlerFactory = new LayerHandlerFactory(this);
-        FolderHandlerFactory = new FolderHandlerFactory(this);
         AnimationDataViewModel = new(this, Internals);
 
         NodeGraph = new NodeGraphViewModel(this, Internals);
@@ -537,7 +534,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         IStructureMemberHandler? layerToExtractFrom = null)
     {
         layerToExtractFrom ??= SelectedStructureMember;
-        if (layerToExtractFrom is not LayerViewModel layerVm)
+        if (layerToExtractFrom is not ILayerHandler layerVm)
             return new Error();
         if (SelectionPathBindable.IsEmpty)
             return new None();
@@ -667,7 +664,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                         _ => Colors.Transparent);
             }
 
-            if (SelectedStructureMember is not LayerViewModel layerVm)
+            if (SelectedStructureMember is not ILayerHandler layerVm)
                 return Colors.Transparent;
             IReadOnlyStructureNode? maybeMember = Internals.Tracker.Document.FindMember(layerVm.Id);
             if (maybeMember is not IReadOnlyImageNode layer)
@@ -714,8 +711,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         OnPropertyChanged(nameof(VerticalSymmetryAxisXBindable));
     }
 
-    public void SetSelectedMember(IStructureMemberHandler member) =>
-        SetSelectedMember((StructureMemberViewModel)member);
+    public void SetSelectedMember(IStructureMemberHandler member)
+    {
+        SelectedStructureMember = member;
+        OnPropertyChanged(nameof(SelectedStructureMember));
+    }
 
     public void SetHorizontalSymmetryAxisY(double horizontalSymmetryAxisY)
     {
@@ -742,25 +742,19 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         OnPropertyChanged(nameof(SelectionPathBindable));
     }
 
-    public void SetSelectedMember(StructureMemberViewModel? member)
+    public void AddSoftSelectedMember(IStructureMemberHandler member)
     {
-        SelectedStructureMember = member;
-        OnPropertyChanged(nameof(SelectedStructureMember));
+        softSelectedStructureMembers.Add(member);
     }
 
     public void RemoveSoftSelectedMember(IStructureMemberHandler member)
     {
         SelectedStructureMember = member;
+        softSelectedStructureMembers.Remove(member);
     }
 
     public void ClearSoftSelectedMembers() => softSelectedStructureMembers.Clear();
 
-    public void AddSoftSelectedMember(IStructureMemberHandler member) =>
-        softSelectedStructureMembers.Add((StructureMemberViewModel)member);
-
-    public void RemoveSoftSelectedMember(StructureMemberViewModel member) =>
-        softSelectedStructureMembers.Remove(member);
-
     #endregion
 
     /// <summary>
@@ -790,12 +784,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             var foundMember = StructureHelper.Find(member);
             if (foundMember != null)
             {
-                if (foundMember is LayerViewModel layer && selectedMembers.Contains(foundMember.Id) &&
+                if (foundMember is ImageLayerNodeViewModel layer && selectedMembers.Contains(foundMember.Id) &&
                     !result.Contains(layer.Id))
                 {
                     result.Add(layer.Id);
                 }
-                else if (foundMember is FolderViewModel folder && selectedMembers.Contains(foundMember.Id))
+                else if (foundMember is FolderNodeViewModel folder && selectedMembers.Contains(foundMember.Id))
                 {
                     if (includeFoldersWithMask && folder.HasMaskBindable && !result.Contains(folder.Id))
                         result.Add(folder.Id);
@@ -812,16 +806,16 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         OnPropertyChanged(nameof(AllChangesSaved));
     }
 
-    private void ExtractSelectedLayers(FolderViewModel folder, List<Guid> list,
+    private void ExtractSelectedLayers(FolderNodeViewModel folder, List<Guid> list,
         bool includeFoldersWithMask)
     {
         foreach (var member in folder.Children)
         {
-            if (member is LayerViewModel layer && !list.Contains(layer.Id))
+            if (member is ImageLayerNodeViewModel layer && !list.Contains(layer.Id))
             {
                 list.Add(layer.Id);
             }
-            else if (member is FolderViewModel childFolder)
+            else if (member is FolderNodeViewModel childFolder)
             {
                 if (includeFoldersWithMask && childFolder.HasMaskBindable && !list.Contains(childFolder.Id))
                     list.Add(childFolder.Id);

+ 0 - 21
src/PixiEditor/ViewModels/Document/FolderHandlerFactory.cs

@@ -1,21 +0,0 @@
-using PixiEditor.Helpers;
-using PixiEditor.Models.DocumentModels;
-using PixiEditor.Models.Handlers;
-
-namespace PixiEditor.ViewModels.Document;
-
-internal class FolderHandlerFactory : IFolderHandlerFactory
-{
-    public DocumentViewModel Document { get; }
-    IDocument IFolderHandlerFactory.Document => Document;
-
-    public FolderHandlerFactory(DocumentViewModel document)
-    {
-        Document = document;
-    }
-
-    public IFolderHandler CreateFolderHandler(DocumentInternalParts helper, Guid infoGuidValue)
-    {
-        return new FolderViewModel(Document, helper, infoGuidValue);
-    }
-}

+ 0 - 16
src/PixiEditor/ViewModels/Document/FolderViewModel.cs

@@ -1,16 +0,0 @@
-using System.Collections.ObjectModel;
-using PixiEditor.Models.DocumentModels;
-using PixiEditor.Models.Handlers;
-
-namespace PixiEditor.ViewModels.Document;
-#nullable enable
-internal class FolderViewModel : StructureMemberViewModel, IFolderHandler
-{
-    public FolderViewModel()
-    {
-        
-    }
-    
-    public ObservableCollection<IStructureMemberHandler> Children { get; } = new();
-    public FolderViewModel(DocumentViewModel doc, DocumentInternalParts internals, Guid id) : base(doc, internals, id) { }
-}

+ 1 - 1
src/PixiEditor/ViewModels/Document/KeyFrameGroupViewModel.cs

@@ -52,7 +52,7 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
         Children.CollectionChanged += ChildrenOnCollectionChanged;
         Document.StructureHelper.Find(LayerGuid).PropertyChanged += (sender, args) =>
         {
-            if (args.PropertyName == nameof(StructureMemberViewModel.NodeNameBindable))
+            if (args.PropertyName == nameof(IStructureMemberHandler.NodeNameBindable))
             {
                 OnPropertyChanged(nameof(LayerName));
             }

+ 0 - 21
src/PixiEditor/ViewModels/Document/LayerHandlerFactory.cs

@@ -1,21 +0,0 @@
-using PixiEditor.Helpers;
-using PixiEditor.Models.DocumentModels;
-using PixiEditor.Models.Handlers;
-
-namespace PixiEditor.ViewModels.Document;
-
-internal class LayerHandlerFactory : ILayerHandlerFactory
-{
-    public DocumentViewModel Document { get; }
-    IDocument ILayerHandlerFactory.Document => Document;
-
-    public LayerHandlerFactory(DocumentViewModel document)
-    {
-        Document = document;
-    }
-
-    public ILayerHandler CreateLayerHandler(DocumentInternalParts helper, Guid infoGuidValue)
-    {
-        return new LayerViewModel(Document, helper, infoGuidValue);
-    }
-}

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/Animable/TimeNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Animable;
+
+[NodeViewModel("TIME_NODE", "ANIMATION", "\uE900")]
+internal class TimeNodeViewModel : NodeViewModel<TimeNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineChannelsNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("COMBINE_CHANNELS_NODE", "IMAGE", "\ue915")]
+internal class CombineChannelsNodeViewModel : NodeViewModel<CombineChannelsNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineColorNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("COMBINE_COLOR_NODE", "COLOR", "\ue908")]
+internal class CombineColorNodeViewModel : NodeViewModel<CombineColorNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecDNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("COMBINE_VECD_NODE", "NUMBERS", "\ue916")]
+internal class CombineVecDNodeViewModel : NodeViewModel<CombineVecDNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecINodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("COMBINE_VECI_NODE", "NUMBERS", "\ue917")]
+internal class CombineVecINodeViewModel : NodeViewModel<CombineVecINode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateChannelsNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("SEPARATE_CHANNELS_NODE", "IMAGE", "\ue910")]
+internal class SeparateChannelsNodeViewModel : NodeViewModel<SeparateChannelsNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateColorNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("SEPARATE_COLOR_NODE", "COLOR", "\ue913")]
+internal class SeparateColorNodeViewModel : NodeViewModel<SeparateColorNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecDNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("SEPARATE_VECD_NODE", "NUMBERS", "\ue918")]
+internal class SeparateVecDNodeViewModel : NodeViewModel<SeparateVecDNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecINodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
+
+[NodeViewModel("SEPARATE_VECI_NODE", "NUMBERS", "\ue917")]
+internal class SeparateVecINodeViewModel : NodeViewModel<SeparateVecINode>;

Some files were not shown because too many files changed in this diff