feat: 初始提交——一键运行快捷轮盘工具(WPF + C#)

This commit is contained in:
2026-08-30 15:44:15 +08:00
commit 2081a938bf
63 changed files with 4214 additions and 0 deletions
@@ -0,0 +1,123 @@
using System.Windows;
using OneClickRun.Helpers;
using Xunit;
namespace OneClickRun.Tests;
public class WheelGeometryTests
{
private const double InnerR = 70;
private const double OuterR = 220;
private const double Gap = 10;
private static readonly Point Center = new(300, 300);
/// <summary>
/// 需求核心:“不论距离中心多远,扇形之间的间距都相等”。
/// 相邻扇形在任意半径处,右边界点与左边界点间的距离应恒等于 Gap。
/// </summary>
[Fact]
public void AdjacentSectorGapIsConstantAtEveryRadius()
{
var radii = new[] { InnerR, 80, 100, 145, 200, OuterR };
for (var count = 2; count <= 12; count++)
{
for (var k = 0; k < count; k++)
{
var rightEdgeAngle = WheelGeometry.SectorStartAngle(k, count) + WheelGeometry.TwoPi / count;
var leftEdgeAngle = WheelGeometry.SectorStartAngle((k + 1) % count, count);
// 共享同一条原始径向射线(允许相差 2π 整周期)
Assert.Equal(0.0, (rightEdgeAngle - leftEdgeAngle) % WheelGeometry.TwoPi, 12);
foreach (var rho in radii)
{
// 扇形 k 的右边界点:角度 - ε(ρ);扇形 k+1 的左边界点:角度 + ε(ρ)
var right = WheelGeometry.PointOnCircle(Center, rho, rightEdgeAngle - WheelGeometry.EdgeOffset(rho, Gap));
var left = WheelGeometry.PointOnCircle(Center, rho, leftEdgeAngle + WheelGeometry.EdgeOffset(rho, Gap));
var distance = (right - left).Length;
Assert.True(Math.Abs(distance - Gap) < 1e-9,
$"count={count}, sector={k}, rho={rho}: gap={distance}, expected={Gap}");
}
}
}
}
/// <summary>两个相邻边界点是同一条偏移直线上的点(平行边 ⇒ 处处等距)</summary>
[Fact]
public void AdjacentEdgePointsLieOnParallelOffsetLines()
{
const int count = 6;
const int sector = 2;
var edgeAngle = WheelGeometry.SectorStartAngle(sector, count) + WheelGeometry.TwoPi / count;
// 同一偏移线在不同半径上的点应共线:取三点验证叉积为 0
var p1 = WheelGeometry.PointOnCircle(Center, 80, edgeAngle + WheelGeometry.EdgeOffset(80, Gap));
var p2 = WheelGeometry.PointOnCircle(Center, 145, edgeAngle + WheelGeometry.EdgeOffset(145, Gap));
var p3 = WheelGeometry.PointOnCircle(Center, 200, edgeAngle + WheelGeometry.EdgeOffset(200, Gap));
var cross = (p2.X - p1.X) * (p3.Y - p1.Y) - (p2.Y - p1.Y) * (p3.X - p1.X);
Assert.True(Math.Abs(cross) < 1e-9, $"边界点不共线,叉积={cross}");
}
[Fact]
public void SectorCornersHaveExactRadii()
{
for (var count = 1; count <= 12; count++)
{
for (var k = 0; k < count; k++)
{
var (p1, p2, p3, p4) = WheelGeometry.SectorCorners(Center, InnerR, OuterR, Gap, k, count);
Assert.Equal(InnerR, (p1 - Center).Length, 9);
Assert.Equal(OuterR, (p2 - Center).Length, 9);
Assert.Equal(OuterR, (p3 - Center).Length, 9);
Assert.Equal(InnerR, (p4 - Center).Length, 9);
}
}
}
[Theory]
[InlineData(1)]
[InlineData(4)]
[InlineData(6)]
[InlineData(12)]
public void HitTestMapsSectorMidPointsToExpectedSector(int count)
{
for (var k = 0; k < count; k++)
{
var mid = WheelGeometry.SectorMidPoint(Center, InnerR, OuterR, k, count);
var hit = WheelGeometry.HitTest(mid, Center, InnerR, OuterR, Gap, count);
Assert.Equal(k, hit);
}
}
[Fact]
public void HitTestReturnsNullForCenterOutsideAndGap()
{
// 中心区域
Assert.Null(WheelGeometry.HitTest(Center, Center, InnerR, OuterR, Gap, 4));
// 圆环外
Assert.Null(WheelGeometry.HitTest(new Point(Center.X + OuterR + 5, Center.Y), Center, InnerR, OuterR, Gap, 4));
// 间隙正中间:原始径向边界角度处
var midRadius = (InnerR + OuterR) / 2;
var edge = WheelGeometry.SectorStartAngle(1, 4);
var inGap = WheelGeometry.PointOnCircle(Center, midRadius, edge);
Assert.Null(WheelGeometry.HitTest(inGap, Center, InnerR, OuterR, Gap, 4));
}
[Fact]
public void SingleSectorIsFullRing()
{
var angles = new[] { -Math.PI / 2, 0.0, Math.PI / 2, Math.PI, Math.PI * 1.5 };
foreach (var angle in angles)
{
var point = WheelGeometry.PointOnCircle(Center, (InnerR + OuterR) / 2, angle);
Assert.Equal(0, WheelGeometry.HitTest(point, Center, InnerR, OuterR, Gap, 1));
}
Assert.NotNull(WheelGeometry.CreateSectorGeometry(Center, InnerR, OuterR, Gap, 0, 1));
}
[Fact]
public void SectorGeometryIsFrozen()
{
var geometry = WheelGeometry.CreateSectorGeometry(Center, InnerR, OuterR, Gap, 0, 4);
Assert.True(geometry.IsFrozen);
Assert.False(geometry.IsEmpty());
}
}