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))); + } }