Skip to content

Commit 6a9b52b

Browse files
committed
Унификация логики построения дуг и секторов Arc/Pie
Уточнена документация и примеры для Arc и Pie. Дуги и сектора теперь всегда строятся по часовой стрелке между нормализованными углами. Исправлены алгоритмы построения: добавлена нормализация углов, корректно обрабатываются полные окружности и малые дуги/сектора, устранена поддержка обратного направления. Исправлены ошибки построения внутренней дуги в Pie. Улучшена предсказуемость и соответствие документации.
1 parent 291e271 commit 6a9b52b

3 files changed

Lines changed: 75 additions & 70 deletions

File tree

MathCore.WPF/Shapes/Arc.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ static Arc()
6060
/// <summary>Начальный угол дуги в градусах</summary>
6161
/// <remarks>
6262
/// Отсчёт ведётся по часовой стрелке, 0 градусов направлен вправо, 90 градусов вниз
63-
/// Дуга рисуется от <see cref="StartAngle"/> к <see cref="StopAngle"/>, знак разности задаёт направление обхода
63+
/// Дуга всегда рисуется по часовой стрелке от нормализованного <see cref="StartAngle"/> к нормализованному <see cref="StopAngle"/>
64+
/// Для задания больших дуг (более 180°) или дуг против часовой используйте углы вне диапазона [0;360)
6465
/// </remarks>
6566
public double StartAngle { get => (double)GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); }
6667

@@ -74,8 +75,11 @@ static Arc()
7475

7576
/// <summary>Конечный угол дуги в градусах</summary>
7677
/// <remarks>
77-
/// Если разница между <see cref="StartAngle"/> и <see cref="StopAngle"/> по модулю близка к 360 градусам,
78-
/// будет отрисована полная окружность вместо дуги
78+
/// После нормализации обоих углов к диапазону [0;360) дуга рисуется по часовой стрелке
79+
/// Примеры:
80+
/// - StartAngle=270, StopAngle=60 → дуга 150° по часовой (270→360→60)
81+
/// - StartAngle=60, StopAngle=270 → дуга 210° по часовой (60→270)
82+
/// - StartAngle=0, StopAngle=360 → полная окружность (разность исходных углов = 360°)
7983
/// </remarks>
8084
public double StopAngle { get => (double)GetValue(StopAngleProperty); set => SetValue(StopAngleProperty, value); }
8185

@@ -146,18 +150,23 @@ private static Geometry GetGeometry(Rect rect, double Start, double End, double
146150
var h = rect.Height;
147151
if (w == 0 || h == 0) return Geometry.Empty; // Если хотя бы одна из сторон прямоугольника равна нулю, возвращаем пустую геометрию
148152

153+
// Вычисляем разность исходных углов для определения полной окружности
154+
var d_raw_abs = Math.Abs(End - Start);
155+
156+
// Если длина дуги по модулю близка к полному кругу или превышает его, рисуем полную окружность
157+
if (d_raw_abs >= FullCircleDegrees - MinArcDegrees)
158+
return new EllipseGeometry(rect);
159+
160+
// Нормализуем углы к диапазону [0;360)
149161
var start_angle = NormalizeAngle(Start);
150162
var end_angle = NormalizeAngle(End);
151163

152-
var d_raw = end_angle - start_angle;
153-
var d_abs = Math.Abs(d_raw);
154-
155-
// Если длина дуги по модулю близка к полному кругу, рисуем полную окружность
156-
if (d_abs >= FullCircleDegrees - MinArcDegrees)
157-
return new EllipseGeometry(rect);
164+
// Вычисляем угловое расстояние по часовой стрелке от start_angle до end_angle
165+
var delta_clockwise = end_angle - start_angle;
166+
if (delta_clockwise < 0) delta_clockwise += FullCircleDegrees; // Приводим к диапазону [0;360)
158167

159168
// Слишком маленькая дуга считается нулевой
160-
if (d_abs < MinArcDegrees)
169+
if (delta_clockwise < MinArcDegrees)
161170
return Geometry.Empty;
162171

163172
var p1 = GetPoint(start_angle, Radius, rect); // Вычисляем координаты начальной точки дуги
@@ -170,8 +179,9 @@ private static Geometry GetGeometry(Rect rect, double Start, double End, double
170179
var radius_y = Math.Max(0, half_height * Radius);
171180
var arc = new Size(radius_x, radius_y); // Размеры дуги (радиусы эллипса), гарантируем неотрицательность
172181

173-
var is_large = d_abs > 180; // Определяем, является ли дуга большой (более 180 градусов)
174-
var sweep_direction = d_raw >= 0 ? SweepDirection.Clockwise : SweepDirection.Counterclockwise; // Учитываем направление дуги
182+
// Дуга всегда рисуется по часовой стрелке между нормализованными углами
183+
var is_large = delta_clockwise > 180; // Большая дуга, если угловое расстояние больше 180 градусов
184+
var sweep_direction = SweepDirection.Clockwise;
175185

176186
var geometry = new StreamGeometry(); // Создаём потоковую геометрию для описания дуги
177187
using var context = geometry.Open(); // Открываем контекст для построения фигуры

MathCore.WPF/Shapes/Pie.cs

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public class Pie : Shape
1717
private const FrameworkPropertyMetadataOptions __DependendPropertyMetadataOptions =
1818
FrameworkPropertyMetadataOptions.AffectsRender;
1919

20+
private const double FullCircleDegrees = 360d;
21+
private const double MinArcDegrees = 1e-6; // минимальная длина дуги в градусах
22+
2023
static Pie()
2124
{
2225
//StretchProperty.OverrideMetadata(typeof(Pie), new FrameworkPropertyMetadata(Stretch.None));
@@ -82,6 +85,10 @@ static Pie()
8285
coerceValueCallback: null));
8386

8487
/// <summary>Получает или устанавливает начальный угол сектора в градусах</summary>
88+
/// <remarks>
89+
/// Отсчёт ведётся по часовой стрелке, 0 градусов направлен вправо, 90 градусов вниз
90+
/// Сектор всегда рисуется по часовой стрелке от нормализованного <see cref="StartAngle"/> к нормализованному <see cref="StopAngle"/>
91+
/// </remarks>
8592
public double StartAngle { get => (double)GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); }
8693

8794
/// <summary>Определяет зависимое свойство для конечного угла сектора</summary>
@@ -98,6 +105,13 @@ private static void OnStopAngleChanged(DependencyObject o, DependencyPropertyCha
98105
o.SetValue(AngleProperty, (double)e.NewValue - ((Pie)o).StartAngle);
99106

100107
/// <summary>Получает или устанавливает конечный угол сектора в градусах</summary>
108+
/// <remarks>
109+
/// После нормализации обоих углов к диапазону [0;360) сектор рисуется по часовой стрелке
110+
/// Примеры:
111+
/// - StartAngle=270, StopAngle=60 → сектор 150° по часовой (270→360→60)
112+
/// - StartAngle=60, StopAngle=270 → сектор 210° по часовой (60→270)
113+
/// - StartAngle=0, StopAngle=360 → полный круг (разность исходных углов = 360°)
114+
/// </remarks>
101115
public double StopAngle { get => (double)GetValue(StopAngleProperty); set => SetValue(StopAngleProperty, value); }
102116

103117
/// <summary>Определяет зависимое свойство для угла раствора сектора</summary>
@@ -177,7 +191,7 @@ protected override Size ArrangeOverride(Size FinalSize)
177191
return size;
178192
}
179193

180-
/// <summary>Вычисляетсектора на основе заданных параметров</summary>
194+
/// <summary>Вычисляет геометрию сектора на основе заданных параметров</summary>
181195
/// <param name="rect">Прямоугольник ограничивающей области</param>
182196
/// <param name="start">Начальный угол в градусах</param>
183197
/// <param name="stop">Конечный угол в градусах</param>
@@ -205,7 +219,7 @@ private Geometry GetGeometry(Rect rect, double start, double stop, double R, dou
205219
return _Pie;
206220
}
207221

208-
/// <summary>Обновляетэллипсов внешнего и внутреннего радиусов</summary>
222+
/// <summary>Обновляет геометрию эллипсов внешнего и внутреннего радиусов</summary>
209223
/// <param name="rect">Прямоугольник ограничивающей области</param>
210224
/// <param name="R">Внешний радиус</param>
211225
/// <param name="r">Внутренний радиус</param>
@@ -227,15 +241,7 @@ private void ChangeGeometry(Rect rect, double R, double r, bool aligned)
227241
_InnerEllipse.RadiusY = h * r;
228242
}
229243

230-
//private static Point GetPoint(Point p0, Rect rect, double a, double r, double w, double h)
231-
//{
232-
// const double to_rad = Math.PI / 180.0;
233-
// a -= 90;
234-
// a *= to_rad;
235-
// return new Point(p0.X + r * Math.Cos(a) * w / 2, p0.Y + r * Math.Sin(a) * h / 2);
236-
//}
237-
238-
/// <summary>Вычисляетна эллипсе по углу и радиусу</summary>
244+
/// <summary>Вычисляет координата точки на эллипсе по углу и радиусу</summary>
239245
/// <param name="rect">Прямоугольник ограничивающей области</param>
240246
/// <param name="a">Угол в градусах</param>
241247
/// <param name="r">Радиус (от 0 до 1)</param>
@@ -251,7 +257,14 @@ private static Point GetPoint(Rect rect, double a, double r)
251257
return new(x, y);
252258
}
253259

254-
/// <summary>Рисует гев контекст потока</summary>
260+
/// <summary>Нормализует угол к диапазону [0;360)</summary>
261+
private static double NormalizeAngle(double angle)
262+
{
263+
angle %= FullCircleDegrees;
264+
return angle < 0 ? angle + FullCircleDegrees : angle;
265+
}
266+
267+
/// <summary>Рисует геометрию сектора в контекст потока</summary>
255268
/// <param name="g">Контекст потока геометрии</param>
256269
/// <param name="rect">Прямоугольник ограничивающей области</param>
257270
/// <param name="R">Внешний радиус</param>
@@ -269,11 +282,16 @@ private static void DrawGeometry(StreamGeometryContext g, Rect rect, double R, d
269282
// Вычисляем центральную точку прямоугольника
270283
var p0 = new Point(0.5 * rect.Width + rect.Left, 0.5 * rect.Height + rect.Top);
271284

272-
// Нормализуем углы: a - меньший угол, b - больший угол
273-
var a = Math.Min(start, stop);
274-
var b = Math.Max(start, stop);
275-
var d = b - a; // Разница углов (угол раствора сектора)
276-
if (d is 0d) return;
285+
// Нормализуем углы к диапазону [0;360)
286+
var start_angle = NormalizeAngle(start);
287+
var stop_angle = NormalizeAngle(stop);
288+
289+
// Вычисляем угловое расстояние по часовой стрелке от start_angle до stop_angle
290+
var delta_clockwise = stop_angle - start_angle;
291+
if (delta_clockwise < 0) delta_clockwise += FullCircleDegrees; // Приводим к диапазону [0;360)
292+
293+
// Слишком маленькая дуга считается нулевой
294+
if (delta_clockwise < MinArcDegrees) return;
277295

278296
// Если включено выравнивание, приводим к квадрату по меньшей стороне
279297
if (aligned)
@@ -283,13 +301,13 @@ private static void DrawGeometry(StreamGeometryContext g, Rect rect, double R, d
283301
}
284302

285303
// Вычисляем ключевые точки для построения сектора:
286-
var in_arc_stop = GetPoint(rect, a, r); // конечная точка внутренней дуги (начальный угол)
287-
var out_arc_start = GetPoint(rect, a, R); // начальная точка внешней дуги (начальный угол)
288-
var out_arc_stop = GetPoint(rect, b, R); // конечная точка внешней дуги (конечный угол)
289-
var in_arc_start = GetPoint(rect, b, r); // начальная точка внутренней дуги (конечный угол)
304+
var out_arc_start = GetPoint(rect, start_angle, R); // начальная точка внешней дуги
305+
var out_arc_stop = GetPoint(rect, stop_angle, R); // конечная точка внешней дуги
306+
var in_arc_start = GetPoint(rect, start_angle, r); // начальная точка внутренней дуги
307+
var in_arc_stop = GetPoint(rect, stop_angle, r); // конечная точка внутренней дуги
290308

291309
// Определяем тип дуги (большая дуга если угол > 180°)
292-
var arc_isout = d > 180.0;
310+
var arc_isout = delta_clockwise > 180.0;
293311

294312
// Вычисляем размеры эллипсов для внутренней и внешней дуг
295313
var in_arc_size = new Size(r * w / 2, r * h / 2);
@@ -304,7 +322,7 @@ private static void DrawGeometry(StreamGeometryContext g, Rect rect, double R, d
304322
else
305323
{
306324
// Начинаем с центра (если r = 0) или с точки на внутренней дуге
307-
g.BeginFigure(r is 0d ? p0 : in_arc_stop, true, true);
325+
g.BeginFigure(r is 0d ? p0 : in_arc_start, true, true);
308326
g.LineTo(out_arc_start, true, true); // Линия к началу внешней дуги
309327
}
310328

@@ -314,9 +332,9 @@ private static void DrawGeometry(StreamGeometryContext g, Rect rect, double R, d
314332
if (r is 0d || line_only) return; // Если внутренний радиус 0 или это линия, завершаем
315333

316334
// Рисуем линию к началу внутренней дуги
317-
g.LineTo(in_arc_start, true, true);
335+
g.LineTo(in_arc_stop, true, true);
318336

319337
// Рисуем внутреннюю дугу от конечного до начального угла против часовой стрелки
320-
g.ArcTo(in_arc_stop, in_arc_size, 0, arc_isout, SweepDirection.Counterclockwise, true, true);
338+
g.ArcTo(in_arc_start, in_arc_size, 0, arc_isout, SweepDirection.Counterclockwise, true, true);
321339
}
322340
}

Tests/MathCore.WPF.WindowTest/TestWindows8.xaml

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,20 @@
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" mc:Ignorable="d"
55
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
66
xmlns:vm="clr-namespace:MathCore.WPF.WindowTest.ViewModels"
7+
xmlns:sh="clr-namespace:MathCore.WPF.Shapes;assembly=MathCore.WPF"
8+
Title="{Binding Title}"
79
Width="800" Height="450">
810
<Window.Resources>
911
<vm:TestWindow8ViewModel x:Key="TestWindow8Model"/>
1012
</Window.Resources>
13+
<Window.DataContext>
14+
<!--<Binding Source="{StaticResource TestWindow8Model}"/>-->
15+
<StaticResource ResourceKey="TestWindow8Model"/>
16+
</Window.DataContext>
1117
<Grid>
12-
<!-- Эксперимент с 3D-графикой -->
13-
14-
<Viewport3D>
15-
<Viewport3D.Camera>
16-
<PerspectiveCamera Position="0,0,5"
17-
LookDirection="0,0,-1"
18-
UpDirection="0,1,0"
19-
FieldOfView="60"/>
20-
</Viewport3D.Camera>
21-
22-
<ModelVisual3D>
23-
<ModelVisual3D.Content>
24-
<DirectionalLight Color="White"
25-
Direction="-1,-1,-2"/>
26-
</ModelVisual3D.Content>
27-
</ModelVisual3D>
28-
29-
<ModelVisual3D>
30-
<ModelVisual3D.Content>
31-
<GeometryModel3D>
32-
<GeometryModel3D.Geometry>
33-
<MeshGeometry3D Positions="-1,-1,0 1,-1,0 1,1,0 -1,1,0"
34-
TriangleIndices="0 1 2 0 2 3"
35-
TextureCoordinates="0,1 1,1 1,0 0,0"/>
36-
</GeometryModel3D.Geometry>
37-
<GeometryModel3D.Material>
38-
<DiffuseMaterial Brush="LightBlue"/>
39-
</GeometryModel3D.Material>
40-
</GeometryModel3D>
41-
</ModelVisual3D.Content>
42-
</ModelVisual3D>
43-
44-
</Viewport3D>
18+
<sh:Arc VerticalAlignment="Top" HorizontalAlignment="Left"
19+
Width="200" Height="200" Margin="20"
20+
Stroke="Red" StrokeThickness="4"
21+
StartAngle="-30" StopAngle="185"/>
4522
</Grid>
4623
</Window>

0 commit comments

Comments
 (0)