Skip to content

Commit

Permalink
Rewrite of the arc class. Added SweepDirection as property, viewbox t…
Browse files Browse the repository at this point in the history
…o easier scale the arc aswell as a new startpoint at 12am and refactoring of the code.
  • Loading branch information
m0lDaViA committed Apr 30, 2024
1 parent d33ecf3 commit 5525c2b
Showing 1 changed file with 147 additions and 54 deletions.
201 changes: 147 additions & 54 deletions src/Wpf.Ui/Controls/Arc/Arc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,24 @@
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.

using System.Windows.Controls;
using System.Windows.Shapes;
using Point = System.Windows.Point;
using Size = System.Windows.Size;
#pragma warning disable SA1124

// ReSharper disable once CheckNamespace
namespace Wpf.Ui.Controls;

/// <summary>
/// Control that draws a symmetrical arc with rounded edges.
/// </summary>
/// <example>
/// <code lang="xml">
/// &lt;ui:Arc
/// EndAngle="359"
/// StartAngle="0"
/// Stroke="{ui:ThemeResource SystemAccentColorSecondaryBrush}"
/// StrokeThickness="2"
/// Visibility="Visible" /&gt;
/// </code>
/// </example>
public class Arc : System.Windows.Shapes.Shape
public class Arc : Shape
{
#region Declarations

private Viewbox? _rootLayout;

#endregion

#region Static Properties

/// <summary>Identifies the <see cref="StartAngle"/> dependency property.</summary>
public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register(
nameof(StartAngle),
Expand All @@ -40,6 +37,28 @@ public class Arc : System.Windows.Shapes.Shape
new PropertyMetadata(0.0d, PropertyChangedCallback)
);

/// <summary>Identifies the <see cref="SweepDirection"/> dependency property.</summary>
public static readonly DependencyProperty SweepDirectionProperty =
DependencyProperty.Register(
nameof(SweepDirection),
typeof(SweepDirection),
typeof(Arc),
new PropertyMetadata(SweepDirection.Clockwise, PropertyChangedCallback)
);

/// <summary>Identifies the <see cref="StrokeStartLineCap"/> dependency property.</summary>
public static new readonly DependencyProperty StrokeStartLineCapProperty =
DependencyProperty.Register(
nameof(StrokeStartLineCap),
typeof(PenLineCap),
typeof(Arc),
new PropertyMetadata(PenLineCap.Round, PropertyChangedCallback)
);

#endregion

#region Public Properties

/// <summary>
/// Gets or sets the initial angle from which the arc will be drawn.
/// </summary>
Expand All @@ -59,58 +78,71 @@ public double EndAngle
}

/// <summary>
/// Gets a value indicating whether one of the two larger arc sweeps is chosen; otherwise, if is <see langword="false"/>, one of the smaller arc sweeps is chosen.
/// Gets or sets the direction to where the arc will be drawn.
/// </summary>
public bool IsLargeArc { get; internal set; } = false;
public SweepDirection SweepDirection
{
get => (SweepDirection)GetValue(SweepDirectionProperty);
set => SetValue(SweepDirectionProperty, value);
}

/// <inheritdoc />
protected override Geometry DefiningGeometry => GetDefiningGeometry();
public new PenLineCap StrokeStartLineCap
{
get { return (PenLineCap)GetValue(StrokeStartLineCapProperty); }
set { SetValue(StrokeStartLineCapProperty, value); }
}

/// <summary>
/// Initializes static members of the <see cref="Arc"/> class.
/// Gets a value indicating whether one of the two larger arc sweeps is chosen; otherwise, if is <see langword="false"/>, one of the smaller arc sweeps is chosen.
/// </summary>
/// <remarks>
/// Overrides default properties.
/// </remarks>
static Arc()
public bool IsLargeArc { get; internal set; } = false;

#endregion

#region Private Methods

private void EnsureRootLayout()
{
StrokeStartLineCapProperty.OverrideMetadata(
typeof(Arc),
new FrameworkPropertyMetadata(PenLineCap.Round)
);
if (_rootLayout != null)
{
return;
}

StrokeEndLineCapProperty.OverrideMetadata(
typeof(Arc),
new FrameworkPropertyMetadata(PenLineCap.Round)
);
_rootLayout = new Viewbox { SnapsToDevicePixels = true };
AddVisualChild(_rootLayout);
}

#endregion

#region Protected Methods

/// <inheritdoc />
protected override Geometry DefiningGeometry => DefinedGeometry();

/// <summary>
/// Get the geometry that defines this shape.
/// <para><see href="https://stackoverflow.com/a/36756365/13224348">Based on Mark Feldman implementation.</see></para>
/// </summary>
protected Geometry GetDefiningGeometry()
protected Geometry DefinedGeometry()
{
var geometryStream = new StreamGeometry();
var arcSize = new Size(
Math.Max(0, (RenderSize.Width - StrokeThickness) / 2),
Math.Max(0, (RenderSize.Height - StrokeThickness) / 2)
);

using (StreamGeometryContext context = geometryStream.Open())
{
context.BeginFigure(PointAtAngle(Math.Min(StartAngle, EndAngle)), false, false);

context.ArcTo(
PointAtAngle(Math.Max(StartAngle, EndAngle)),
arcSize,
0,
IsLargeArc,
SweepDirection.Counterclockwise,
true,
false
);
}
using StreamGeometryContext context = geometryStream.Open();
context.BeginFigure(PointAtAngle(Math.Min(StartAngle, EndAngle)), false, false);

context.ArcTo(
PointAtAngle(Math.Max(StartAngle, EndAngle)),
arcSize,
0,
IsLargeArc,
SweepDirection,
true,
false
);

geometryStream.Transform = new TranslateTransform(StrokeThickness / 2, StrokeThickness / 2);

Expand All @@ -124,11 +156,36 @@ protected Geometry GetDefiningGeometry()
/// <param name="angle">The angle at which to create the point.</param>
protected Point PointAtAngle(double angle)
{
var radAngle = angle * (Math.PI / 180);
var xRadius = (RenderSize.Width - StrokeThickness) / 2;
var yRadius = (RenderSize.Height - StrokeThickness) / 2;

return new Point(xRadius + (xRadius * Math.Cos(radAngle)), yRadius - (yRadius * Math.Sin(radAngle)));
if (SweepDirection == SweepDirection.Counterclockwise)
{
angle += 90;
angle %= 360;
if (angle < 0)
{
angle += 360;
}

var radAngle = angle * (Math.PI / 180);
var xRadius = (RenderSize.Width - StrokeThickness) / 2;
var yRadius = (RenderSize.Height - StrokeThickness) / 2;

return new Point(xRadius + (xRadius * Math.Cos(radAngle)), yRadius - (yRadius * Math.Sin(radAngle)));
}
else
{
angle -= 90;
angle %= 360;
if (angle < 0)
{
angle += 360;
}

var radAngle = angle * (Math.PI / 180);
var xRadius = (RenderSize.Width - StrokeThickness) / 2;
var yRadius = (RenderSize.Height - StrokeThickness) / 2;

return new Point(xRadius + (xRadius * Math.Cos(-radAngle)), yRadius - (yRadius * Math.Sin(-radAngle)));
}
}

/// <summary>
Expand All @@ -142,8 +199,44 @@ protected static void PropertyChangedCallback(DependencyObject d, DependencyProp
}

control.IsLargeArc = Math.Abs(control.EndAngle - control.StartAngle) > 180;

// Force complete new layout pass
control.InvalidateVisual();
}

protected override Visual? GetVisualChild(int index)
{
if (index != 0)
{
throw new ArgumentOutOfRangeException(nameof(index), "Arc should have only 1 child");
}

EnsureRootLayout();

return _rootLayout;
}

protected override Size MeasureOverride(Size availableSize)
{
EnsureRootLayout();

_rootLayout!.Measure(availableSize);
return _rootLayout.DesiredSize;
}

protected override Size ArrangeOverride(Size finalSize)
{
EnsureRootLayout();

_rootLayout!.Arrange(new Rect(default, finalSize));
return finalSize;
}

/// <summary>Overrides the default OnRender method to draw the <see cref="Arc" /> element.</summary>
/// <param name="drawingContext">A <see cref="DrawingContext" /> object that is drawn during the rendering pass of this <see cref="System.Windows.Shapes.Shape" />.</param>
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
drawingContext.DrawGeometry(Stroke, new Pen(Stroke, StrokeThickness), DefinedGeometry());
}

#endregion
}

0 comments on commit 5525c2b

Please sign in to comment.