Browse Source

Basic brushes loading

Krzysztof Krysiński 2 days ago
parent
commit
615195151f
22 changed files with 373 additions and 35 deletions
  1. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  2. 16 0
      src/PixiEditor.ChangeableDocument/Changeables/DocumentGraphPipe.cs
  3. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  4. 11 18
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  5. BIN
      src/PixiEditor/Data/Brushes/Basic.pixi
  6. BIN
      src/PixiEditor/Data/Brushes/Claws.pixi
  7. 20 0
      src/PixiEditor/Models/BrushEngine/Brush.cs
  8. 76 0
      src/PixiEditor/Models/Controllers/BrushLibrary.cs
  9. 3 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs
  10. 16 13
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs
  11. 1 0
      src/PixiEditor/Models/Handlers/IDocument.cs
  12. 3 1
      src/PixiEditor/Models/Handlers/Toolbars/IPenToolbar.cs
  13. 4 0
      src/PixiEditor/Models/IO/Paths.cs
  14. 7 0
      src/PixiEditor/PixiEditor.csproj
  15. 5 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  16. 20 0
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  17. 30 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/BrushSettingViewModel.cs
  18. 10 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/PenToolbar.cs
  19. 15 0
      src/PixiEditor/Views/Input/BrushPicker.axaml
  20. 95 0
      src/PixiEditor/Views/Input/BrushPicker.axaml.cs
  21. 20 0
      src/PixiEditor/Views/Tools/ToolSettings/Settings/BrushSettingView.axaml
  22. 15 0
      src/PixiEditor/Views/Tools/ToolSettings/Settings/BrushSettingView.axaml.cs

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

@@ -173,6 +173,11 @@ internal class Document : IChangeable, IReadOnlyDocument
         return new DocumentNodePipe<T>(this, layerId);
         return new DocumentNodePipe<T>(this, layerId);
     }
     }
 
 
+    public ICrossDocumentPipe<IReadOnlyNodeGraph> CreateGraphPipe()
+    {
+        return new DocumentGraphPipe(this);
+    }
+
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
     {
         graph.TryTraverse((node) =>
         graph.TryTraverse((node) =>

+ 16 - 0
src/PixiEditor.ChangeableDocument/Changeables/DocumentGraphPipe.cs

@@ -0,0 +1,16 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables;
+
+internal class DocumentGraphPipe : DocumentMemoryPipe<IReadOnlyNodeGraph>
+{
+    public DocumentGraphPipe(Document document) : base(document)
+    {
+    }
+
+    protected override IReadOnlyNodeGraph? GetData()
+    {
+        return Document.NodeGraph;
+    }
+}

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

@@ -106,4 +106,5 @@ public interface IReadOnlyDocument : IDisposable
     public void InitProcessingColorSpace(ColorSpace processingColorSpace);
     public void InitProcessingColorSpace(ColorSpace processingColorSpace);
     public List<IReadOnlyStructureNode> GetParents(Guid memberGuid);
     public List<IReadOnlyStructureNode> GetParents(Guid memberGuid);
     public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode;
     public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode;
+    public ICrossDocumentPipe<IReadOnlyNodeGraph> CreateGraphPipe();
 }
 }

+ 11 - 18
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -27,7 +27,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private readonly bool erasing;
     private readonly bool erasing;
     private readonly bool drawOnMask;
     private readonly bool drawOnMask;
     private readonly bool antiAliasing;
     private readonly bool antiAliasing;
-    private Guid brushOutputGuid;
     private BrushData brushData;
     private BrushData brushData;
     private BrushEngine engine = new BrushEngine();
     private BrushEngine engine = new BrushEngine();
     private float spacing = 1;
     private float spacing = 1;
@@ -44,7 +43,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, float strokeWidth, bool erasing,
     public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, float strokeWidth, bool erasing,
         bool antiAliasing,
         bool antiAliasing,
         float spacing,
         float spacing,
-        Guid brushOutputGuid,
+        BrushData brushData,
         bool drawOnMask, int frame, PointerInfo pointerInfo, EditorData editorData)
         bool drawOnMask, int frame, PointerInfo pointerInfo, EditorData editorData)
     {
     {
         this.memberGuid = memberGuid;
         this.memberGuid = memberGuid;
@@ -53,8 +52,8 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         this.erasing = erasing;
         this.erasing = erasing;
         this.antiAliasing = antiAliasing;
         this.antiAliasing = antiAliasing;
         this.drawOnMask = drawOnMask;
         this.drawOnMask = drawOnMask;
-        this.brushOutputGuid = brushOutputGuid;
         this.spacing = spacing;
         this.spacing = spacing;
+        this.brushData = brushData;
         points.Add(pos);
         points.Add(pos);
         this.frame = frame;
         this.frame = frame;
         this.pointerInfo = pointerInfo;
         this.pointerInfo = pointerInfo;
@@ -104,16 +103,10 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
         DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
         srcPaint.IsAntiAliased = antiAliasing;
         srcPaint.IsAntiAliased = antiAliasing;
 
 
-        if (brushOutputGuid != Guid.Empty)
-        {
-            brushOutputNode = target.FindNode<BrushOutputNode>(brushOutputGuid);
-            brushData = new BrushData(target.NodeGraph);
-            UpdateBrushData();
-
-            return brushOutputNode != null;
-        }
+        brushOutputNode = brushData.BrushGraph?.AllNodes.FirstOrDefault(x => x is BrushOutputNode) as BrushOutputNode;
+        UpdateBrushData();
 
 
-        return true;
+        return brushOutputNode != null;
     }
     }
 
 
     private void UpdateBrushData()
     private void UpdateBrushData()
@@ -146,8 +139,8 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         List<IChangeInfo> changes = new()
         List<IChangeInfo> changes = new()
         {
         {
             changeInfo.AsT1,
             changeInfo.AsT1,
-            new ComputedPropertyValue_ChangeInfo(brushOutputGuid, "VectorShape", true,
-                brushOutputNode.VectorShape.Value)
+            /*new ComputedPropertyValue_ChangeInfo(brushOutputGuid, "VectorShape", true,
+                brushOutputNode.VectorShape.Value)*/
         };
         };
 
 
         return changes;
         return changes;
@@ -189,8 +182,8 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             List<IChangeInfo> changes = new()
             List<IChangeInfo> changes = new()
             {
             {
                 change,
                 change,
-                new ComputedPropertyValue_ChangeInfo(brushOutputGuid, "VectorShape", true,
-                    brushOutputNode?.VectorShape.Value)
+                /*new ComputedPropertyValue_ChangeInfo(brushOutputGuid, "VectorShape", true,
+                    brushOutputNode?.VectorShape.Value)*/
             };
             };
             return changes;
             return changes;
         }
         }
@@ -210,8 +203,8 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             List<IChangeInfo> changes = new()
             List<IChangeInfo> changes = new()
             {
             {
                 info,
                 info,
-                new ComputedPropertyValue_ChangeInfo(brushOutputGuid, "VectorShape", true,
-                    brushOutputNode?.VectorShape.Value)
+                /*new ComputedPropertyValue_ChangeInfo(brushOutputGuid, "VectorShape", true,
+                    brushOutputNode?.VectorShape.Value)*/
             };
             };
 
 
             return changes;
             return changes;

BIN
src/PixiEditor/Data/Brushes/Basic.pixi


BIN
src/PixiEditor/Data/Brushes/Claws.pixi


+ 20 - 0
src/PixiEditor/Models/BrushEngine/Brush.cs

@@ -0,0 +1,20 @@
+using PixiEditor.Models.Handlers;
+
+namespace PixiEditor.Models.BrushEngine;
+
+internal class Brush
+{
+    public IDocument Document { get; set; }
+    public string Name { get; set; }
+
+    public Brush(string name, IDocument brushDocument)
+    {
+        Name = name;
+        Document = brushDocument;
+    }
+
+    public override string ToString()
+    {
+        return Name;
+    }
+}

+ 76 - 0
src/PixiEditor/Models/Controllers/BrushLibrary.cs

@@ -0,0 +1,76 @@
+using Avalonia.Platform;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Helpers;
+using PixiEditor.Models.BrushEngine;
+using PixiEditor.Models.IO;
+
+namespace PixiEditor.Models.Controllers;
+
+internal class BrushLibrary
+{
+    private List<Brush> brushes = new List<Brush>();
+    public IReadOnlyList<Brush> Brushes => brushes;
+
+    private string pathToBrushes;
+    public string PathToBrushes => pathToBrushes;
+
+    public BrushLibrary(string pathToBrushes)
+    {
+        this.pathToBrushes = pathToBrushes;
+    }
+
+    private void LoadBuiltIn()
+    {
+        Uri brushesUri = new Uri("avares://PixiEditor/Data/Brushes/");
+        var assets = AssetLoader.GetAssets(brushesUri, null);
+
+        foreach (var asset in assets)
+        {
+            string localPath = asset.LocalPath;
+            if (localPath.EndsWith(".pixi", StringComparison.OrdinalIgnoreCase))
+            {
+                try
+                {
+                    var fullUri = new Uri(brushesUri, asset);
+                    using var stream = AssetLoader.Open(fullUri);
+                    byte[] buffer = new byte[stream.Length];
+                    stream.ReadExactly(buffer, 0, buffer.Length);
+                    var doc = Importer.ImportDocument(buffer, null);
+                    var brush = new Brush(Path.GetFileNameWithoutExtension(localPath), doc);
+                    brushes.Add(brush);
+                }
+                catch (Exception ex)
+                {
+                    Console.WriteLine($"Failed to load built-in brush from {asset}: {ex.Message}");
+                }
+            }
+        }
+    }
+
+    private void LoadBrushesFromPath(string path)
+    {
+        if (!Directory.Exists(path))
+            return;
+
+        var brushFiles = Directory.GetFiles(path, "*.pixi", SearchOption.AllDirectories);
+        foreach (var file in brushFiles)
+        {
+            try
+            {
+                var doc = Importer.ImportDocument(file, false);
+                var brush = new Brush(Path.GetFileNameWithoutExtension(file), doc);
+                brushes.Add(brush);
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"Failed to load brush from {file}: {ex.Message}");
+            }
+        }
+    }
+
+    public void LoadBrushes()
+    {
+        LoadBuiltIn();
+        LoadBrushesFromPath(pathToBrushes);
+    }
+}

+ 3 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -9,6 +9,7 @@ using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Brushes;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
@@ -51,7 +52,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)eraserTool.ToolSize, true,
         IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)eraserTool.ToolSize, true,
-            antiAliasing, spacing, Guid.Empty, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
+            antiAliasing, spacing, new BrushData(), drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
         internals!.ActionAccumulator.AddActions(action);
         internals!.ActionAccumulator.AddActions(action);
 
 
         return ExecutionState.Success;
         return ExecutionState.Success;
@@ -59,7 +60,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
 
     public override void OnPixelPositionChange(VecI pos, MouseOnCanvasEventArgs args)
     public override void OnPixelPositionChange(VecI pos, MouseOnCanvasEventArgs args)
     {
     {
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, pos, (float)toolSize, true, antiAliasing, spacing, Guid.Empty, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, pos, (float)toolSize, true, antiAliasing, spacing, new BrushData(), drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
         internals!.ActionAccumulator.AddActions(action);
         internals!.ActionAccumulator.AddActions(action);
     }
     }
 
 

+ 16 - 13
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -33,8 +33,8 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     private bool pixelPerfect;
     private bool pixelPerfect;
     private bool antiAliasing;
     private bool antiAliasing;
     private bool transparentErase;
     private bool transparentErase;
+    private BrushData brushData;
 
 
-    private Guid brushOutputGuid = Guid.Empty;
     private INodePropertyHandler vectorShapeInput;
     private INodePropertyHandler vectorShapeInput;
 
 
     private IPenToolbar penToolbar;
     private IPenToolbar penToolbar;
@@ -66,24 +66,27 @@ internal class PenToolExecutor : UpdateableChangeExecutor
             colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
             colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         }
         }
 
 
-        brushOutputGuid =
-            document.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.InternalName == "PixiEditor.BrushOutput")?.Id ??
-            Guid.Empty;
-        if (brushOutputGuid == Guid.Empty)
+        if(toolbar.Brush == null)
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
+        var pipe = toolbar.Brush.Document.ShareGraph();
+        brushData = new BrushData(pipe.TryAccessData());
+        pipe.Dispose();
+
+        /*
         vectorShapeInput =
         vectorShapeInput =
             (document.NodeGraphHandler.NodeLookup[brushOutputGuid] as BrushOutputNodeViewModel).Inputs
             (document.NodeGraphHandler.NodeLookup[brushOutputGuid] as BrushOutputNodeViewModel).Inputs
             .FirstOrDefault(x => x.PropertyName == "VectorShape");
             .FirstOrDefault(x => x.PropertyName == "VectorShape");
+            */
 
 
         transparentErase = color.A == 0;
         transparentErase = color.A == 0;
-        vectorShapeInput.UpdateComputedValue();
+        //vectorShapeInput.UpdateComputedValue();
         if (controller.LeftMousePressed)
         if (controller.LeftMousePressed)
         {
         {
             IAction? action = pixelPerfect switch
             IAction? action = pixelPerfect switch
             {
             {
                 false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
                 false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
-                    transparentErase, antiAliasing, Spacing, brushOutputGuid, drawOnMask,
+                    transparentErase, antiAliasing, Spacing, brushData, drawOnMask,
                     document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData),
                     document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData),
                 true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask,
                 true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask,
                     document!.AnimationHandler.ActiveFrameBindable)
                     document!.AnimationHandler.ActiveFrameBindable)
@@ -91,7 +94,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
             internals!.ActionAccumulator.AddActions(action);
             internals!.ActionAccumulator.AddActions(action);
         }
         }
 
 
-        handler.FinalBrushShape = (vectorShapeInput.ComputedValue as IReadOnlyShapeVectorData)?.ToPath(true);
+        //handler.FinalBrushShape = (vectorShapeInput.ComputedValue as IReadOnlyShapeVectorData)?.ToPath(true);
 
 
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }
@@ -102,7 +105,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         IAction? action = pixelPerfect switch
         IAction? action = pixelPerfect switch
         {
         {
             false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
             false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
-                transparentErase, antiAliasing, Spacing, brushOutputGuid, drawOnMask,
+                transparentErase, antiAliasing, Spacing, brushData, drawOnMask,
                 document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData),
                 document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData),
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask,
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask,
                 document!.AnimationHandler.ActiveFrameBindable)
                 document!.AnimationHandler.ActiveFrameBindable)
@@ -115,10 +118,10 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         base.OnPrecisePositionChange(args);
         base.OnPrecisePositionChange(args);
         if (!controller.LeftMousePressed)
         if (!controller.LeftMousePressed)
         {
         {
-            vectorShapeInput.UpdateComputedValue();
+            //vectorShapeInput.UpdateComputedValue();
         }
         }
 
 
-        handler.FinalBrushShape = (vectorShapeInput.ComputedValue as IReadOnlyShapeVectorData)?.ToPath(true);
+        //handler.FinalBrushShape = (vectorShapeInput.ComputedValue as IReadOnlyShapeVectorData)?.ToPath(true);
     }
     }
 
 
     public override void OnPixelPositionChange(VecI pos, MouseOnCanvasEventArgs args)
     public override void OnPixelPositionChange(VecI pos, MouseOnCanvasEventArgs args)
@@ -128,7 +131,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
             IAction? action = pixelPerfect switch
             IAction? action = pixelPerfect switch
             {
             {
                 false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, transparentErase, antiAliasing,
                 false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, transparentErase, antiAliasing,
-                    Spacing, brushOutputGuid, drawOnMask, document!.AnimationHandler.ActiveFrameBindable,
+                    Spacing, brushData, drawOnMask, document!.AnimationHandler.ActiveFrameBindable,
                     controller.LastPointerInfo, controller.EditorData),
                     controller.LastPointerInfo, controller.EditorData),
                 true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask,
                 true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask,
                     document!.AnimationHandler.ActiveFrameBindable)
                     document!.AnimationHandler.ActiveFrameBindable)
@@ -140,7 +143,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     public override void OnConvertedKeyDown(Key key)
     public override void OnConvertedKeyDown(Key key)
     {
     {
         base.OnConvertedKeyDown(key);
         base.OnConvertedKeyDown(key);
-        handler.FinalBrushShape = (vectorShapeInput.ComputedValue as IReadOnlyShapeVectorData)?.ToPath(true);
+        //handler.FinalBrushShape = (vectorShapeInput.ComputedValue as IReadOnlyShapeVectorData)?.ToPath(true);
     }
     }
 
 
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)

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

@@ -73,4 +73,5 @@ internal interface IDocument : IHandler, Extensions.CommonApi.Documents.IDocumen
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);
     internal void InternalMarkSaveState(DocumentMarkType type);
     internal void InternalMarkSaveState(DocumentMarkType type);
     public ICrossDocumentPipe<T> ShareNode<T>(Guid layerId) where T : class, IReadOnlyNode;
     public ICrossDocumentPipe<T> ShareNode<T>(Guid layerId) where T : class, IReadOnlyNode;
+    public ICrossDocumentPipe<IReadOnlyNodeGraph> ShareGraph();
 }
 }

+ 3 - 1
src/PixiEditor/Models/Handlers/Toolbars/IPenToolbar.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Views.Overlays.BrushShapeOverlay;
+using PixiEditor.Models.BrushEngine;
+using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 
 namespace PixiEditor.Models.Handlers.Toolbars;
 namespace PixiEditor.Models.Handlers.Toolbars;
 
 
@@ -7,4 +8,5 @@ internal interface IPenToolbar : IToolbar, IToolSizeToolbar
     public bool AntiAliasing { get; set; }
     public bool AntiAliasing { get; set; }
     public float Spacing { get; set; }
     public float Spacing { get; set; }
     public PaintBrushShape PaintShape { get; set; }
     public PaintBrushShape PaintShape { get; set; }
+    public Brush Brush { get; set; }
 }
 }

+ 4 - 0
src/PixiEditor/Models/IO/Paths.cs

@@ -29,6 +29,10 @@ public static class Paths
         Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
         Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
         "PixiEditor", "Palettes");
         "PixiEditor", "Palettes");
 
 
+    public static string PathToBrushesFolder { get; } = Path.Join(
+        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+        "PixiEditor", "Brushes");
+
     public static string InternalResourceDataPath { get; } =
     public static string InternalResourceDataPath { get; } =
         $"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Data";
         $"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Data";
 
 

+ 7 - 0
src/PixiEditor/PixiEditor.csproj

@@ -156,5 +156,12 @@
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
     </None>
   </ItemGroup>
   </ItemGroup>
+
+  <ItemGroup>
+    <Compile Update="Views\Tools\ToolSettings\Settings\BrushSettingView.axaml.cs">
+      <DependentUpon>BrushSettingView.axaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+  </ItemGroup>
   
   
 </Project>
 </Project>

+ 5 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -731,6 +731,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         return Internals.Tracker.Document.CreateNodePipe<T>(layerId);
         return Internals.Tracker.Document.CreateNodePipe<T>(layerId);
     }
     }
 
 
+    public ICrossDocumentPipe<IReadOnlyNodeGraph> ShareGraph()
+    {
+        return Internals.Tracker.Document.CreateGraphPipe();
+    }
+
     public OneOf<Error, Surface> TryRenderWholeImage(KeyFrameTime frameTime, VecI renderSize)
     public OneOf<Error, Surface> TryRenderWholeImage(KeyFrameTime frameTime, VecI renderSize)
     {
     {
         return TryRenderWholeImage(frameTime, renderSize, SizeBindable);
         return TryRenderWholeImage(frameTime, renderSize, SizeBindable);

+ 20 - 0
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -20,6 +20,7 @@ using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.Models.IO;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Tools;
 using PixiEditor.ViewModels.Tools;
@@ -102,6 +103,8 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 
 
     public event EventHandler<SelectedToolEventArgs>? SelectedToolChanged;
     public event EventHandler<SelectedToolEventArgs>? SelectedToolChanged;
 
 
+    public BrushLibrary BrushLibrary { get; private set; }
+
     private bool shiftIsDown;
     private bool shiftIsDown;
     private bool ctrlIsDown;
     private bool ctrlIsDown;
     private bool altIsDown;
     private bool altIsDown;
@@ -118,6 +121,23 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
     {
         owner.DocumentManagerSubViewModel.ActiveDocumentChanged += ActiveDocumentChanged;
         owner.DocumentManagerSubViewModel.ActiveDocumentChanged += ActiveDocumentChanged;
         PixiEditorSettings.Tools.PrimaryToolset.ValueChanged += PrimaryToolsetOnValueChanged;
         PixiEditorSettings.Tools.PrimaryToolset.ValueChanged += PrimaryToolsetOnValueChanged;
+        BrushLibrary = new BrushLibrary(Paths.PathToBrushesFolder);
+        owner.AttachedToWindow += window =>
+        {
+            if (!window.IsLoaded)
+            {
+                window.Loaded += (_, _) => LoadBrushLibrary();
+            }
+            else
+            {
+                LoadBrushLibrary();
+            }
+        };
+    }
+
+    private void LoadBrushLibrary()
+    {
+        BrushLibrary.LoadBrushes();
     }
     }
 
 
     private void PrimaryToolsetOnValueChanged(Setting<string> setting, string? newPrimaryToolset)
     private void PrimaryToolsetOnValueChanged(Setting<string> setting, string? newPrimaryToolset)

+ 30 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/BrushSettingViewModel.cs

@@ -0,0 +1,30 @@
+using System.Collections.ObjectModel;
+using PixiEditor.Models.BrushEngine;
+using PixiEditor.Models.Controllers;
+using PixiEditor.ViewModels.SubViewModels;
+
+namespace PixiEditor.ViewModels.Tools.ToolSettings.Settings;
+
+internal class BrushSettingViewModel : Setting<Brush>
+{
+    private static BrushLibrary library;
+
+    private static BrushLibrary Library
+    {
+        get
+        {
+            if (library == null)
+            {
+                library = (ViewModelMain.Current.ToolsSubViewModel as ToolsViewModel).BrushLibrary;
+            }
+
+            return library;
+        }
+    }
+
+    public ObservableCollection<Brush> AllBrushes => new ObservableCollection<Brush>(Library.Brushes);
+    public BrushSettingViewModel(string name, string label) : base(name)
+    {
+        Label = label;
+    }
+}

+ 10 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/PenToolbar.cs

@@ -1,4 +1,6 @@
-using PixiEditor.Models.Handlers.Toolbars;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using PixiEditor.Models.BrushEngine;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 
@@ -30,6 +32,12 @@ internal class PenToolbar : Toolbar, IPenToolbar
         set => GetSetting<EnumSettingViewModel<PaintBrushShape>>(nameof(PaintShape)).Value = value;
         set => GetSetting<EnumSettingViewModel<PaintBrushShape>>(nameof(PaintShape)).Value = value;
     }
     }
 
 
+    public Brush Brush
+    {
+        get => GetSetting<BrushSettingViewModel>(nameof(Brush)).Value;
+        set => GetSetting<BrushSettingViewModel>(nameof(Brush)).Value = value;
+    }
+
     public override void OnLoadedSettings()
     public override void OnLoadedSettings()
     {
     {
         OnPropertyChanged(nameof(ToolSize));
         OnPropertyChanged(nameof(ToolSize));
@@ -43,5 +51,6 @@ internal class PenToolbar : Toolbar, IPenToolbar
         setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));
         setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));
         AddSetting(setting);
         AddSetting(setting);
         AddSetting(new EnumSettingViewModel<PaintBrushShape>(nameof(PaintShape), "PAINT_SHAPE_SETTING") { IsExposed = false });
         AddSetting(new EnumSettingViewModel<PaintBrushShape>(nameof(PaintShape), "PAINT_SHAPE_SETTING") { IsExposed = false });
+        AddSetting(new BrushSettingViewModel(nameof(Brush), "BRUSH_SETTING") { IsExposed = true });
     }
     }
 }
 }

+ 15 - 0
src/PixiEditor/Views/Input/BrushPicker.axaml

@@ -0,0 +1,15 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:input="clr-namespace:PixiEditor.Views.Input"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:ClassModifier="internal"
+             x:Class="PixiEditor.Views.Input.BrushPicker">
+    <ComboBox VerticalAlignment="Center"
+              MinWidth="85"
+              ItemsSource="{Binding Brushes, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type input:BrushPicker}}}"
+              SelectedIndex="{Binding BrushIndex,
+              RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type input:BrushPicker}}, Mode=TwoWay}">
+    </ComboBox>
+</UserControl>

+ 95 - 0
src/PixiEditor/Views/Input/BrushPicker.axaml.cs

@@ -0,0 +1,95 @@
+using System.Collections.ObjectModel;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.VisualTree;
+using Brush = PixiEditor.Models.BrushEngine.Brush;
+
+namespace PixiEditor.Views.Input;
+
+internal partial class BrushPicker : UserControl
+{
+    public static readonly StyledProperty<int> BrushIndexProperty = AvaloniaProperty.Register<BrushPicker, int>(
+        nameof(BrushIndex));
+
+    public int BrushIndex
+    {
+        get => GetValue(BrushIndexProperty);
+        set => SetValue(BrushIndexProperty, value);
+    }
+
+    public static readonly StyledProperty<ObservableCollection<Brush>> BrushesProperty =
+        AvaloniaProperty.Register<BrushPicker, ObservableCollection<Brush>>(
+            nameof(Brushes));
+
+    public ObservableCollection<Brush> Brushes
+    {
+        get => GetValue(BrushesProperty);
+        set => SetValue(BrushesProperty, value);
+    }
+
+    public static readonly StyledProperty<Brush?> SelectedBrushProperty =
+        AvaloniaProperty.Register<BrushPicker, Brush?>(
+            nameof(SelectedBrush));
+
+    public Brush? SelectedBrush
+    {
+        get => GetValue(SelectedBrushProperty);
+        set => SetValue(SelectedBrushProperty, value);
+    }
+
+    private bool suppressUpdate = false;
+
+    static BrushPicker()
+    {
+        BrushesProperty.Changed.AddClassHandler<BrushPicker>((x, e) => x.OnBrushesChanged(e));
+        BrushIndexProperty.Changed.AddClassHandler<BrushPicker>((x, e) => x.OnBrushIndexChanged(e));
+        SelectedBrushProperty.Changed.AddClassHandler<BrushPicker>((x, e) =>
+        {
+            if (e.NewValue is Brush newBrush && x.Brushes != null)
+            {
+                int index = x.Brushes.IndexOf(newBrush);
+                if (index != -1 && x.BrushIndex != index)
+                {
+                    x.suppressUpdate = true;
+                    x.BrushIndex = index;
+                    x.suppressUpdate = false;
+                }
+            }
+        });
+    }
+
+    public BrushPicker()
+    {
+        InitializeComponent();
+    }
+
+    private void OnBrushesChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.NewValue is ObservableCollection<Brush> brushes && brushes.Count > 0)
+        {
+            BrushIndex = brushes.IndexOf(SelectedBrush);
+            if (BrushIndex == -1)
+            {
+                BrushIndex = 0;
+                SelectedBrush = brushes[0];
+            }
+        }
+    }
+
+    private void OnBrushIndexChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (suppressUpdate || (e.Sender is Visual v && !v.IsAttachedToVisualTree()))
+            return;
+
+        if (e.NewValue is int index && Brushes != null && index >= 0 && index < Brushes.Count)
+        {
+            SelectedBrush = Brushes[index];
+        }
+        else
+        {
+            SelectedBrush = null;
+        }
+    }
+}

+ 20 - 0
src/PixiEditor/Views/Tools/ToolSettings/Settings/BrushSettingView.axaml

@@ -0,0 +1,20 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:settings="clr-namespace:PixiEditor.ViewModels.Tools.ToolSettings.Settings"
+             xmlns:enums="clr-namespace:PixiEditor.ChangeableDocument.Enums;assembly=PixiEditor.ChangeableDocument"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:helpers="clr-namespace:PixiEditor.Helpers"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:properties="clr-namespace:PixiEditor.Views.Nodes.Properties"
+             xmlns:input="clr-namespace:PixiEditor.Views.Input"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PixiEditor.Views.Tools.ToolSettings.Settings.BrushSettingView">
+    <Design.DataContext>
+        <settings:BrushSettingViewModel />
+    </Design.DataContext>
+
+    <input:BrushPicker SelectedBrush="{Binding Value, Mode=TwoWay}"
+                                 Brushes="{Binding AllBrushes}"/>
+</UserControl>

+ 15 - 0
src/PixiEditor/Views/Tools/ToolSettings/Settings/BrushSettingView.axaml.cs

@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.Views.Tools.ToolSettings.Settings;
+
+public partial class BrushSettingView : UserControl
+{
+    public BrushSettingView()
+    {
+        InitializeComponent();
+    }
+}
+