Browse Source

Added vector line

flabbet 11 months ago
parent
commit
adea032b7b

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyLineData.cs

@@ -0,0 +1,9 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+public interface IReadOnlyLineData : IReadOnlyShapeVectorData
+{
+    public VecD Start { get; }
+    public VecD End { get; }
+}

+ 92 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -0,0 +1,92 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnlyLineData
+{
+    public VecD Start { get; set; } = startPos;
+    public VecD End { get; set; } = pos;
+    
+    public override RectD GeometryAABB
+    {
+        get
+        {
+            if (StrokeWidth == 1)
+            {
+                return RectD.FromTwoPoints(Start, End);
+            }
+            
+            VecD halfStroke = new(StrokeWidth / 2f);
+            VecD min = new VecD(Math.Min(Start.X, End.X), Math.Min(Start.Y, End.Y)) - halfStroke;
+            VecD max = new VecD(Math.Max(Start.X, End.X), Math.Max(Start.Y, End.Y)) + halfStroke;
+            
+            return new RectD(min, max - min);
+        }
+    }
+
+    public override ShapeCorners TransformationCorners => new ShapeCorners(GeometryAABB)
+        .WithMatrix(TransformationMatrix);
+
+    public override void Rasterize(DrawingSurface drawingSurface, ChunkResolution resolution)
+    {
+        RectD adjustedAABB = GeometryAABB.RoundOutwards().Inflate(1);
+        var imageSize = (VecI)adjustedAABB.Size;
+        
+        using ChunkyImage img = new ChunkyImage(imageSize);
+
+        if (StrokeWidth == 1)
+        {
+            img.EnqueueDrawBresenhamLine(
+                (VecI)Start - (VecI)adjustedAABB.TopLeft,
+                (VecI)End - (VecI)adjustedAABB.TopLeft, StrokeColor, BlendMode.SrcOver);
+        }
+        else
+        {
+            img.EnqueueDrawSkiaLine(
+                (VecI)Start - (VecI)adjustedAABB.TopLeft,
+                (VecI)End - (VecI)adjustedAABB.TopLeft, StrokeCap.Butt, StrokeWidth, StrokeColor, BlendMode.SrcOver);
+        }
+
+        img.CommitChanges();
+        
+        VecI topLeft = (VecI)adjustedAABB.TopLeft; 
+        
+        RectI region = new(VecI.Zero, imageSize);
+        
+        int num = drawingSurface.Canvas.Save();
+        drawingSurface.Canvas.SetMatrix(TransformationMatrix);
+        
+        img.DrawMostUpToDateRegionOn(region, resolution, drawingSurface, topLeft);
+        
+        drawingSurface.Canvas.RestoreToCount(num);
+    }
+
+    public override bool IsValid()
+    {
+        return Start != End;
+    }
+
+    public override int GetCacheHash()
+    {
+        return HashCode.Combine(Start, End, StrokeColor, StrokeWidth, TransformationMatrix);
+    }
+
+    public override int CalculateHash()
+    {
+        return GetCacheHash();
+    }
+
+    public override object Clone()
+    {
+        return new LineVectorData(Start, End)
+        {
+            StrokeColor = StrokeColor,
+            StrokeWidth = StrokeWidth,
+            TransformationMatrix = TransformationMatrix
+        };
+    }
+}

+ 1 - 1
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -25,7 +25,7 @@
       "MoveViewport",
       "RotateViewport",
       "Move",
-      "RasterLine",
+      "VectorLine",
       "VectorEllipse",
       "VectorRectangle"
     ]

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

@@ -89,7 +89,7 @@ internal static class ServiceCollectionHelpers
             .AddTool<IMagicWandToolHandler, MagicWandToolViewModel>()
             .AddTool<ILassoToolHandler, LassoToolViewModel>()
             .AddTool<IFloodFillToolHandler, FloodFillToolViewModel>()
-            .AddTool<IRasterLineToolHandler, RasterLineToolViewModel>()
+            .AddTool<ILineToolHandler, RasterLineToolViewModel>()
             .AddTool<IRasterEllipseToolHandler, RasterEllipseToolViewModel>()
             .AddTool<IRasterRectangleToolHandler, RasterRectangleToolViewModel>()
             .AddTool<IEraserToolHandler, EraserToolViewModel>()
@@ -97,6 +97,7 @@ internal static class ServiceCollectionHelpers
             .AddTool<IBrightnessToolHandler, BrightnessToolViewModel>()
             .AddTool<IVectorEllipseToolHandler, VectorEllipseToolViewModel>()
             .AddTool<IVectorRectangleToolHandler, VectorRectangleToolViewModel>()
+            .AddTool<IVectorLineToolHandler, VectorLineToolViewModel>()
             .AddTool<ZoomToolViewModel>()
             // File types
             .AddSingleton<IoFileType, PixiFileType>()

+ 14 - 6
src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs

@@ -4,6 +4,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Tools;
 
 namespace PixiEditor.Models.DocumentModels.Public;
+
 internal class DocumentToolsModule
 {
     private IDocument Document { get; set; }
@@ -15,7 +16,8 @@ internal class DocumentToolsModule
         this.Internals = internals;
     }
 
-    public void UseSymmetry(SymmetryAxisDirection dir) => Internals.ChangeController.TryStartExecutor(new SymmetryExecutor(dir));
+    public void UseSymmetry(SymmetryAxisDirection dir) =>
+        Internals.ChangeController.TryStartExecutor(new SymmetryExecutor(dir));
 
     public void UseOpacitySlider() => Internals.ChangeController.TryStartExecutor<StructureMemberOpacityExecutor>();
 
@@ -38,23 +40,29 @@ internal class DocumentToolsModule
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
         Internals.ChangeController.TryStartExecutor<RasterEllipseToolExecutor>(force);
     }
-    
+
+    public void UseRasterLineTool()
+    {
+        bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
+        Internals.ChangeController.TryStartExecutor<RasterLineToolExecutor>(force);
+    }
+
     public void UseVectorEllipseTool()
     {
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
         Internals.ChangeController.TryStartExecutor<VectorEllipseToolExecutor>(force);
     }
-    
+
     public void UseVectorRectangleTool()
     {
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
         Internals.ChangeController.TryStartExecutor<VectorRectangleToolExecutor>(force);
     }
-
-    public void UseLineTool()
+    
+    public void UseVectorLineTool()
     {
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
-        Internals.ChangeController.TryStartExecutor<RasterLineToolExecutor>(force);
+        Internals.ChangeController.TryStartExecutor<VectorLineToolExecutor>(force);
     }
 
     public void UseSelectTool() => Internals.ChangeController.TryStartExecutor<SelectToolExecutor>();

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

@@ -0,0 +1,149 @@
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.Models.Tools;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+
+internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILineToolHandler
+{
+    public override ExecutorType Type => ExecutorType.ToolLinked;
+
+    protected VecI startPos;
+    protected Color StrokeColor => colorsVM!.PrimaryColor;
+    protected int StrokeWidth => toolViewModel!.ToolSize;
+    protected Guid memberGuid;
+    protected bool drawOnMask;
+
+    protected VecI curPos;
+    private bool started = false;
+    private bool transforming = false;
+    private T? toolViewModel;
+    private IColorsHandler? colorsVM;
+
+    public override ExecutionState Start()
+    {
+        colorsVM = GetHandler<IColorsHandler>();
+        toolViewModel = GetHandler<T>();
+        IStructureMemberHandler? member = document?.SelectedStructureMember;
+        if (colorsVM is null || toolViewModel is null || member is null)
+            return ExecutionState.Error;
+
+        drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
+        if (drawOnMask && !member.HasMaskBindable)
+            return ExecutionState.Error;
+        if (!drawOnMask && member is not ILayerHandler)
+            return ExecutionState.Error;
+
+        startPos = controller!.LastPixelPosition;
+        memberGuid = member.Id;
+
+        return ExecutionState.Success;
+    }
+
+    protected abstract IAction DrawLine(VecI pos);
+    protected abstract IAction TransformOverlayMoved(VecD start, VecD end);
+    protected abstract IAction SettingsChange();
+    protected abstract IAction EndDraw();
+
+    public override void OnPixelPositionChange(VecI pos)
+    {
+        if (transforming)
+            return;
+        started = true;
+
+        if (toolViewModel!.Snap)
+            pos = ShapeToolExecutor<IShapeToolHandler>.Get45IncrementedPosition(startPos, pos);
+        curPos = pos;
+        var drawLineAction = DrawLine(pos);
+        internals!.ActionAccumulator.AddActions(drawLineAction);
+    }
+
+    public override void OnLeftMouseButtonUp()
+    {
+        if (!started)
+        {
+            onEnded!(this);
+            return;
+        }
+
+        document!.LineToolOverlayHandler.Show(startPos + new VecD(0.5), curPos + new VecD(0.5));
+        transforming = true;
+    }
+
+    public override void OnLineOverlayMoved(VecD start, VecD end)
+    {
+        if (!transforming)
+            return;
+
+
+        var moveOverlayAction = TransformOverlayMoved(start, end);
+        internals!.ActionAccumulator.AddActions(moveOverlayAction);
+
+        startPos = (VecI)start;
+        curPos = (VecI)end;
+    }
+
+    public override void OnColorChanged(Color color, bool primary)
+    {
+        if (!primary)
+            return;
+
+        var colorChangedAction = SettingsChange();
+        internals!.ActionAccumulator.AddActions(colorChangedAction);
+    }
+
+    public override void OnSelectedObjectNudged(VecI distance)
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayHandler.Nudge(distance);
+    }
+
+    public override void OnSettingsChanged(string name, object value)
+    {
+        var colorChangedAction = SettingsChange();
+        internals!.ActionAccumulator.AddActions(colorChangedAction);
+    }
+
+    public override void OnMidChangeUndo()
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayHandler.Undo();
+    }
+
+    public override void OnMidChangeRedo()
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayHandler.Redo();
+    }
+
+    public override void OnTransformApplied()
+    {
+        if (!transforming)
+            return;
+
+        document!.LineToolOverlayHandler.Hide();
+        var endDrawAction = EndDraw();
+        internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
+        onEnded!(this);
+
+        colorsVM.AddSwatch(new PaletteColor(StrokeColor.R, StrokeColor.G, StrokeColor.B));
+    }
+
+    public override void ForceStop()
+    {
+        if (transforming)
+            document!.LineToolOverlayHandler.Hide();
+
+        var endDrawAction = EndDraw();
+        internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
+    }
+}

+ 14 - 119
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs

@@ -1,138 +1,33 @@
-using PixiEditor.ChangeableDocument.Actions.Generated;
-using PixiEditor.DrawingApi.Core.ColorsImpl;
-using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
-using PixiEditor.Extensions.CommonApi.Palettes;
-using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
-using PixiEditor.Models.Tools;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
-internal class RasterLineToolExecutor : UpdateableChangeExecutor
+internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
 {
-    public override ExecutorType Type => ExecutorType.ToolLinked;
-
-    private VecI startPos;
-    private Color strokeColor;
-    private int StrokeWidth => toolViewModel!.ToolSize;
-    private Guid memberGuid;
-    private bool drawOnMask;
-
-    private VecI curPos;
-    private bool started = false;
-    private bool transforming = false;
-    private IRasterLineToolHandler? toolViewModel;
-    private IColorsHandler? colorsVM;
-
-    public override ExecutionState Start()
-    {
-        colorsVM = GetHandler<IColorsHandler>();
-        toolViewModel = GetHandler<IRasterLineToolHandler>();
-        IStructureMemberHandler? member = document?.SelectedStructureMember;
-        if (colorsVM is null || toolViewModel is null || member is null)
-            return ExecutionState.Error;
-
-        drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
-        if (drawOnMask && !member.HasMaskBindable)
-            return ExecutionState.Error;
-        if (!drawOnMask && member is not ILayerHandler)
-            return ExecutionState.Error;
-
-        startPos = controller!.LastPixelPosition;
-        strokeColor = colorsVM.PrimaryColor;
-        memberGuid = member.Id;
-        
-        return ExecutionState.Success;
-    }
-
-    public override void OnPixelPositionChange(VecI pos)
-    {
-        if (transforming)
-            return;
-        started = true;
-
-        if (toolViewModel!.Snap)
-            pos = ShapeToolExecutor<IShapeToolHandler>.Get45IncrementedPosition(startPos, pos);
-        curPos = pos;
-        internals!.ActionAccumulator.AddActions(new DrawRasterLine_Action(memberGuid, startPos, pos, StrokeWidth, strokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
-    }
-
-    public override void OnLeftMouseButtonUp()
-    {
-        if (!started)
-        {
-            onEnded!(this);
-            return;
-        }
-
-        document!.LineToolOverlayHandler.Show(startPos + new VecD(0.5), curPos + new VecD(0.5));
-        transforming = true;
-    }
-
-    public override void OnLineOverlayMoved(VecD start, VecD end)
+    protected override IAction DrawLine(VecI pos)
     {
-        if (!transforming)
-            return;
-        internals!.ActionAccumulator.AddActions(new DrawRasterLine_Action(memberGuid, (VecI)start, (VecI)end, StrokeWidth, strokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
-        
-        startPos = (VecI)start;
-        curPos = (VecI)end;
+        return new DrawRasterLine_Action(memberGuid, startPos, pos, StrokeWidth,
+            StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
-    public override void OnColorChanged(Color color, bool primary)
+    protected override IAction TransformOverlayMoved(VecD start, VecD end)
     {
-        if (!primary)
-            return;
-        
-        strokeColor = color;
-        internals!.ActionAccumulator.AddActions(new DrawRasterLine_Action(memberGuid, startPos, curPos, StrokeWidth, strokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
+        return new DrawRasterLine_Action(memberGuid, (VecI)start, (VecI)end,
+            StrokeWidth, StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
-    public override void OnSelectedObjectNudged(VecI distance)
+    protected override IAction SettingsChange()
     {
-        if (!transforming)
-            return;
-        document!.LineToolOverlayHandler.Nudge(distance);
+        return new DrawRasterLine_Action(memberGuid, startPos, curPos, StrokeWidth,
+            StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
-    public override void OnSettingsChanged(string name, object value)
+    protected override IAction EndDraw()
     {
-        internals!.ActionAccumulator.AddActions(new DrawRasterLine_Action(memberGuid, startPos, curPos, StrokeWidth, strokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
-    }
-
-    public override void OnMidChangeUndo()
-    {
-        if (!transforming)
-            return;
-        document!.LineToolOverlayHandler.Undo();
-    }
-
-    public override void OnMidChangeRedo()
-    {
-        if (!transforming)
-            return;
-        document!.LineToolOverlayHandler.Redo();
-    }
-
-    public override void OnTransformApplied()
-    {
-        if (!transforming)
-            return;
-
-        document!.LineToolOverlayHandler.Hide();
-        internals!.ActionAccumulator.AddFinishedActions(new EndDrawRasterLine_Action());
-        onEnded!(this);
-
-        colorsVM.AddSwatch(new PaletteColor(strokeColor.R, strokeColor.G, strokeColor.B));
-    }
-
-    public override void ForceStop()
-    {
-        if (transforming)
-            document!.LineToolOverlayHandler.Hide();
-
-        internals!.ActionAccumulator.AddFinishedActions(new EndDrawRasterLine_Action());
+        return new EndDrawRasterLine_Action();
     }
 }

+ 51 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs

@@ -0,0 +1,51 @@
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+
+internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler> 
+{
+    private VecD startPoint;
+    private VecD endPoint;
+    protected override IAction DrawLine(VecI pos)
+    {
+        LineVectorData data = new LineVectorData(startPos, pos)
+        {
+            StrokeColor = StrokeColor,
+            StrokeWidth = StrokeWidth,
+        };
+        
+        startPoint = startPos;
+        endPoint = pos;
+
+        return new SetShapeGeometry_Action(memberGuid, data);
+    }
+
+    protected override IAction TransformOverlayMoved(VecD start, VecD end)
+    {
+        LineVectorData data = new LineVectorData((VecD)start, (VecD)end)
+        {
+            StrokeColor = StrokeColor,
+            StrokeWidth = StrokeWidth,
+        };
+        
+        startPoint = start;
+        endPoint = end;
+
+        return new SetShapeGeometry_Action(memberGuid, data);
+    }
+
+    protected override IAction SettingsChange()
+    {
+        return TransformOverlayMoved(startPoint, endPoint);
+    }
+
+    protected override IAction EndDraw()
+    {
+        return new EndSetShapeGeometry_Action();
+    }
+}

+ 1 - 1
src/PixiEditor/Models/Handlers/Tools/IRasterLineToolHandler.cs → src/PixiEditor/Models/Handlers/Tools/ILineToolHandler.cs

@@ -1,6 +1,6 @@
 namespace PixiEditor.Models.Handlers.Tools;
 
-internal interface IRasterLineToolHandler : IToolHandler
+internal interface ILineToolHandler : IToolHandler
 {
     public int ToolSize { get; }
     public bool Snap { get; }

+ 7 - 0
src/PixiEditor/Models/Handlers/Tools/IVectorLineToolHandler.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Models.Handlers.Tools;
+
+internal interface IVectorLineToolHandler : ILineToolHandler 
+{
+    public int ToolSize { get; }
+    public bool Snap { get; }
+}

+ 21 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -132,7 +132,11 @@ internal partial class DocumentViewModel
         else if (vectorNode.ShapeData is IReadOnlyRectangleData rectangleData)
         {
             elementToAdd = AddRectangle(resizeFactor, rectangleData);
-        } 
+        }
+        else if (vectorNode.ShapeData is IReadOnlyLineData lineData)
+        {
+            elementToAdd = AddLine(resizeFactor, lineData);
+        }
 
         if (elementToAdd != null)
         {
@@ -140,6 +144,22 @@ internal partial class DocumentViewModel
         }
     }
 
+    private static SvgLine AddLine(VecD resizeFactor, IReadOnlyLineData lineData)
+    {
+        SvgLine line = new SvgLine();
+        line.X1.Unit = SvgNumericUnit.FromUserUnits(lineData.Start.X * resizeFactor.X);
+        line.Y1.Unit = SvgNumericUnit.FromUserUnits(lineData.Start.Y * resizeFactor.Y);
+        line.X2.Unit = SvgNumericUnit.FromUserUnits(lineData.End.X * resizeFactor.X);
+        line.Y2.Unit = SvgNumericUnit.FromUserUnits(lineData.End.Y * resizeFactor.Y);
+        
+        line.Stroke.Unit = SvgColorUnit.FromRgba(lineData.StrokeColor.R, lineData.StrokeColor.G,
+            lineData.StrokeColor.B, lineData.StrokeColor.A);
+        line.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(lineData.StrokeWidth * resizeFactor.X);
+        line.Transform.Unit = new SvgTransformUnit(lineData.TransformationMatrix);
+        
+        return line;
+    }
+
     private static SvgEllipse AddEllipse(VecD resizeFactor, IReadOnlyEllipseData ellipseData)
     {
         SvgEllipse ellipse = new SvgEllipse();

+ 2 - 2
src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs

@@ -11,7 +11,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 namespace PixiEditor.ViewModels.Tools.Tools;
 
 [Command.Tool(Key = Key.L)]
-internal class RasterLineToolViewModel : ShapeTool, IRasterLineToolHandler
+internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
 {
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
 
@@ -47,6 +47,6 @@ internal class RasterLineToolViewModel : ShapeTool, IRasterLineToolHandler
 
     public override void UseTool(VecD pos)
     {
-        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseLineTool();
+        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseRasterLineTool();
     }
 }

+ 52 - 0
src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs

@@ -0,0 +1,52 @@
+using Avalonia.Input;
+using PixiEditor.Views.Overlays.BrushShapeOverlay;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.Numerics;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
+
+namespace PixiEditor.ViewModels.Tools.Tools;
+
+[Command.Tool(Key = Key.L)]
+internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
+{
+    private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
+
+    public VectorLineToolViewModel()
+    {
+        ActionDisplay = defaultActionDisplay;
+        Toolbar = ToolbarFactory.Create<VectorLineToolViewModel, BasicToolbar>(this);
+    }
+
+    public override string ToolNameLocalizationKey => "LINE_TOOL";
+    public override LocalizedString Tooltip => new LocalizedString("LINE_TOOL_TOOLTIP", Shortcut);
+
+    public override string Icon => PixiPerfectIcons.Line;
+
+    [Settings.Inherited]
+    public int ToolSize => GetValue<int>();
+
+    public bool Snap { get; private set; }
+
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    {
+        if (shiftIsDown)
+        {
+            ActionDisplay = "LINE_TOOL_ACTION_DISPLAY_SHIFT";
+            Snap = true;
+        }
+        else
+        {
+            ActionDisplay = defaultActionDisplay;
+            Snap = false;
+        }
+    }
+
+    public override void UseTool(VecD pos)
+    {
+        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorLineTool();
+    }
+}