diff --git a/samples/AvalonDraw/MainWindow.axaml b/samples/AvalonDraw/MainWindow.axaml
index d2405f1132..aa75dc97d6 100644
--- a/samples/AvalonDraw/MainWindow.axaml
+++ b/samples/AvalonDraw/MainWindow.axaml
@@ -90,6 +90,7 @@
+
@@ -132,11 +133,14 @@
+
+
+
diff --git a/samples/AvalonDraw/MainWindow.axaml.cs b/samples/AvalonDraw/MainWindow.axaml.cs
index 69290f5c5b..8e755dc77e 100644
--- a/samples/AvalonDraw/MainWindow.axaml.cs
+++ b/samples/AvalonDraw/MainWindow.axaml.cs
@@ -186,6 +186,8 @@ private enum DropPosition { None, Inside, Before, After }
private readonly List _multiSelected = new();
private readonly List _multiDrawables = new();
private SK.SKRect _multiBounds = SK.SKRect.Empty;
+ private readonly List _freehandPoints = new();
+ private TextBox? _strokeWidthBox;
public MainWindow()
{
@@ -347,6 +349,9 @@ public MainWindow()
_layerTree = this.FindControl("LayerTree");
_swatchList = this.FindControl("SwatchList");
_brushList = this.FindControl("BrushList");
+ _strokeWidthBox = this.FindControl("StrokeWidthBox");
+ if (_strokeWidthBox is { })
+ _strokeWidthBox.Text = _toolService.CurrentStrokeWidth.ToString(System.Globalization.CultureInfo.InvariantCulture);
_wireframeEnabled = false;
_filtersDisabled = false;
_snapToGrid = false;
@@ -873,13 +878,28 @@ private void SvgView_OnPointerMoved(object? sender, PointerEventArgs e)
if (_creating && SvgView.TryGetPicturePoint(point, out var cp) && _newElement is { })
{
var cur = new Shim.SKPoint(cp.X, cp.Y);
- _toolService.UpdateElement(_newElement, _toolService.CurrentTool,
- _newStart, cur, _snapToGrid, _selectionService.Snap);
- SvgView.SkSvg!.FromSvgDocument(_document);
- UpdateSelectedDrawable();
- if (_pathService.IsEditing)
- _pathService.EditDrawable = _selectedDrawable;
- SvgView.InvalidateVisual();
+ if (_toolService.CurrentTool == Tool.Freehand)
+ {
+ var last = _freehandPoints[^1];
+ if (Math.Abs(last.X - cur.X) > DragThreshold || Math.Abs(last.Y - cur.Y) > DragThreshold)
+ {
+ _freehandPoints.Add(cur);
+ _toolService.AddFreehandPoint(_newElement, cur, _snapToGrid, _selectionService.Snap);
+ SvgView.SkSvg!.FromSvgDocument(_document);
+ UpdateSelectedDrawable();
+ SvgView.InvalidateVisual();
+ }
+ }
+ else
+ {
+ _toolService.UpdateElement(_newElement, _toolService.CurrentTool,
+ _newStart, cur, _snapToGrid, _selectionService.Snap);
+ SvgView.SkSvg!.FromSvgDocument(_document);
+ UpdateSelectedDrawable();
+ if (_pathService.IsEditing)
+ _pathService.EditDrawable = _selectedDrawable;
+ SvgView.InvalidateVisual();
+ }
return;
}
@@ -1217,12 +1237,20 @@ private void SvgView_OnPointerReleased(object? sender, PointerReleasedEventArgs
_creating = false;
if (_newElement is { })
{
+ if (_toolService.CurrentTool == Tool.Freehand && _newElement is SvgPath fp && _freehandPoints.Count > 1)
+ {
+ fp.PathData = PathService.MakeSmooth(_freehandPoints);
+ if (_selectedBrush is { })
+ fp.CustomAttributes["stroke-profile"] = _selectedBrush.Profile.ToString();
+ SvgView.SkSvg!.FromSvgDocument(_document);
+ }
LoadProperties(_newElement);
_selectedElement = _newElement;
_selectedSvgElement = _newElement;
UpdateSelectedDrawable();
}
_newElement = null;
+ _freehandPoints.Clear();
}
}
else if (_isResizing)
@@ -2408,6 +2436,13 @@ private void PathMoveToolButton_Click(object? sender, RoutedEventArgs e)
_pathService.CurrentSegmentTool = PathService.SegmentTool.Move;
}
+ private void FreehandToolButton_Click(object? sender, RoutedEventArgs e)
+ {
+ if (_pathService.IsEditing)
+ _pathService.Stop();
+ _toolService.SetTool(Tool.Freehand);
+ }
+
private async void SymbolToolButton_Click(object? sender, RoutedEventArgs e)
{
if (_pathService.IsEditing)
@@ -2446,6 +2481,7 @@ private async void SymbolToolButton_Click(object? sender, RoutedEventArgs e)
private void PathArcToolMenuItem_Click(object? sender, RoutedEventArgs e) => PathArcToolButton_Click(sender, e);
private void PathMoveToolMenuItem_Click(object? sender, RoutedEventArgs e) => PathMoveToolButton_Click(sender, e);
private void SymbolToolMenuItem_Click(object? sender, RoutedEventArgs e) => SymbolToolButton_Click(sender, e);
+ private void FreehandToolMenuItem_Click(object? sender, RoutedEventArgs e) => FreehandToolButton_Click(sender, e);
private async void SettingsMenuItem_Click(object? sender, RoutedEventArgs e)
{
@@ -2696,6 +2732,15 @@ private void PropertyFilterBox_OnKeyUp(object? sender, KeyEventArgs e)
}
}
+ private void StrokeWidthBox_OnKeyUp(object? sender, KeyEventArgs e)
+ {
+ if (sender is TextBox tb &&
+ float.TryParse(tb.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var w))
+ {
+ _toolService.CurrentStrokeWidth = w;
+ }
+ }
+
private void DocumentTree_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
var pos = e.GetPosition(DocumentTree);
diff --git a/samples/AvalonDraw/Services/PathService.cs b/samples/AvalonDraw/Services/PathService.cs
index 3087aecd68..fc6bddd60a 100644
--- a/samples/AvalonDraw/Services/PathService.cs
+++ b/samples/AvalonDraw/Services/PathService.cs
@@ -358,6 +358,30 @@ public static SK.SKPoint[] ConvertPoints(SvgPointCollection pc)
return pts;
}
+ public static SvgPathSegmentList MakeSmooth(IList points)
+ {
+ var list = new SvgPathSegmentList();
+ if (points.Count == 0)
+ return list;
+ list.Add(new SvgMoveToSegment(false, new System.Drawing.PointF(points[0].X, points[0].Y)));
+ if (points.Count == 1)
+ return list;
+ for (int i = 0; i < points.Count - 1; i++)
+ {
+ var p0 = i == 0 ? points[i] : points[i - 1];
+ var p1 = points[i];
+ var p2 = points[i + 1];
+ var p3 = i + 2 < points.Count ? points[i + 2] : p2;
+ var c1 = new Shim.SKPoint(p1.X + (p2.X - p0.X) / 6f, p1.Y + (p2.Y - p0.Y) / 6f);
+ var c2 = new Shim.SKPoint(p2.X - (p3.X - p1.X) / 6f, p2.Y - (p3.Y - p1.Y) / 6f);
+ list.Add(new SvgCubicCurveSegment(false,
+ new System.Drawing.PointF(c1.X, c1.Y),
+ new System.Drawing.PointF(c2.X, c2.Y),
+ new System.Drawing.PointF(p2.X, p2.Y)));
+ }
+ return list;
+ }
+
public static void AddPathSegments(SK.SKPath path, SvgPathSegmentList segments)
{
var cur = new SK.SKPoint();
diff --git a/samples/AvalonDraw/Services/ToolService.cs b/samples/AvalonDraw/Services/ToolService.cs
index dcc9e0f429..d5438bd869 100644
--- a/samples/AvalonDraw/Services/ToolService.cs
+++ b/samples/AvalonDraw/Services/ToolService.cs
@@ -29,7 +29,8 @@ public enum Tool
PathArc,
PathMove,
Symbol,
- Image
+ Image,
+ Freehand
}
public Tool CurrentTool { get; private set; } = Tool.Select;
@@ -142,6 +143,7 @@ public void SetTool(Tool tool)
Height = new SvgUnit(SvgUnitType.User, 0),
Href = ImageHref
},
+ Tool.Freehand => CreateFreehand(start),
_ => null!
};
}
@@ -176,6 +178,19 @@ private SvgPath CreatePath(ShimSkiaSharp.SKPoint start, Tool tool)
return path;
}
+ private SvgPath CreateFreehand(ShimSkiaSharp.SKPoint start)
+ {
+ return new SvgPath
+ {
+ Stroke = new SvgColourServer(System.Drawing.Color.Black),
+ StrokeWidth = new SvgUnit(CurrentStrokeWidth),
+ PathData = new SvgPathSegmentList
+ {
+ new SvgMoveToSegment(false, new System.Drawing.PointF(start.X, start.Y))
+ }
+ };
+ }
+
public void UpdateElement(SvgVisualElement element, Tool tool, ShimSkiaSharp.SKPoint start, ShimSkiaSharp.SKPoint current, bool snapToGrid, Func snap)
{
switch (tool)
@@ -367,4 +382,16 @@ public void FinalizePolygon(SvgVisualElement element, Tool tool, ShimSkiaSharp.S
pts[pts.Count - 1] = new SvgUnit(pts[1].Type, y);
}
}
+
+ public void AddFreehandPoint(SvgVisualElement element, ShimSkiaSharp.SKPoint point, bool snapToGrid, Func snap)
+ {
+ if (element is not SvgPath path)
+ return;
+ var x = snapToGrid ? snap(point.X) : point.X;
+ var y = snapToGrid ? snap(point.Y) : point.Y;
+ if (path.PathData.Count == 0)
+ path.PathData.Add(new SvgMoveToSegment(false, new System.Drawing.PointF(x, y)));
+ else
+ path.PathData.Add(new SvgLineSegment(false, new System.Drawing.PointF(x, y)));
+ }
}