Planner no longer writes local physics formulas. All kinematics/geometry/damage go through shared utility classes (Kinematics/RouteGeometry/CloudExpansionModel/DamageAssessment). Core: RouteGeometry (route geometry), PlannerConfig + planner_config.json (config externalization). Planner route-aware layout (offset along tangent), cloud overlap 20pct. DroneEntity arc-length driven. Fixes: PathInSphere cloud-frame correction (root cause), GaussianPuffDispersion hardcoded Sunny, ComputeEffectiveRadius cloud age, planner wind offset, remove drone wind bias. Tests 167 to 191, 41s. Windy scenarios pass.
188 lines
7.0 KiB
C#
188 lines
7.0 KiB
C#
using System.Collections.Generic;
|
||
using CounterDrone.Core.Algorithms;
|
||
using CounterDrone.Core.Models;
|
||
using Xunit;
|
||
|
||
namespace CounterDrone.Core.Tests
|
||
{
|
||
public class RouteGeometryTests
|
||
{
|
||
private static List<Waypoint> Line(float x0, float z0, float x1, float z1, float y = 500f)
|
||
=> new()
|
||
{
|
||
new Waypoint { PosX = x0, PosY = y, PosZ = z0 },
|
||
new Waypoint { PosX = x1, PosY = y, PosZ = z1 },
|
||
};
|
||
|
||
private static List<Waypoint> LShape()
|
||
=> new()
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0 }, // 起点
|
||
new Waypoint { PosX = 100, PosY = 500, PosZ = 0 }, // 拐点(X 段 100m)
|
||
new Waypoint { PosX = 100, PosY = 500, PosZ = 100 }, // 终点(Z 段 100m)
|
||
};
|
||
|
||
// ═══════════════════════════════════════
|
||
// TotalLength
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void TotalLength_StraightLine()
|
||
{
|
||
var wps = Line(0, 0, 3000, 4000); // 3-4-5 → 5000m
|
||
Assert.Equal(5000f, RouteGeometry.TotalLength(wps), 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void TotalLength_LShape()
|
||
{
|
||
var wps = LShape(); // 100 + 100 = 200m
|
||
Assert.Equal(200f, RouteGeometry.TotalLength(wps), 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void TotalLength_SingleWaypoint_Zero()
|
||
{
|
||
var wps = new List<Waypoint> { new Waypoint { PosX = 5, PosY = 5, PosZ = 5 } };
|
||
Assert.Equal(0f, RouteGeometry.TotalLength(wps));
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// PositionAt
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void PositionAt_Start_ReturnsFirstWaypoint()
|
||
{
|
||
var wps = Line(0, 0, 1000, 0);
|
||
var (x, y, z) = RouteGeometry.PositionAt(wps, 0f);
|
||
Assert.Equal(0f, x, 1);
|
||
Assert.Equal(0f, z, 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void PositionAt_Midpoint_LinearInterp()
|
||
{
|
||
var wps = Line(0, 0, 1000, 0);
|
||
var (x, _, _) = RouteGeometry.PositionAt(wps, 500f); // 中点
|
||
Assert.Equal(500f, x, 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void PositionAt_LShape_Corner()
|
||
{
|
||
var wps = LShape(); // 0→(100,0)→(100,100)
|
||
// 弧长 100 = 拐点
|
||
var (x, _, z) = RouteGeometry.PositionAt(wps, 100f);
|
||
Assert.Equal(100f, x, 1);
|
||
Assert.Equal(0f, z, 1);
|
||
// 弧长 150 = Z 段中点
|
||
var (x2, _, z2) = RouteGeometry.PositionAt(wps, 150f);
|
||
Assert.Equal(100f, x2, 1);
|
||
Assert.Equal(50f, z2, 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void PositionAt_BeyondEnd_ReturnsLastWaypoint()
|
||
{
|
||
var wps = Line(0, 0, 1000, 0);
|
||
var (x, _, _) = RouteGeometry.PositionAt(wps, 99999f);
|
||
Assert.Equal(1000f, x, 1);
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// ArcLengthNearestTo
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void ArcLengthNearestTo_PointOnLine()
|
||
{
|
||
var wps = Line(0, 0, 1000, 0); // X 轴上
|
||
// 点 (300, 0) 在航路上,弧长应=300
|
||
Assert.Equal(300f, RouteGeometry.ArcLengthNearestTo(wps, 300f, 0f), 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void ArcLengthNearestTo_PointOffLine_Projects()
|
||
{
|
||
var wps = Line(0, 0, 1000, 0); // X 轴
|
||
// 点 (300, 50) 投影回 X 轴弧长=300
|
||
Assert.Equal(300f, RouteGeometry.ArcLengthNearestTo(wps, 300f, 50f), 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void ArcLengthNearestTo_LShape_CorrectSegment()
|
||
{
|
||
var wps = LShape(); // 0→(100,0)→(100,100)
|
||
// 点 (50, 0) 在第一段,弧长=50
|
||
Assert.Equal(50f, RouteGeometry.ArcLengthNearestTo(wps, 50f, 0f), 1);
|
||
// 点 (100, 50) 在第二段(X 段 100 + Z 段 50),弧长=150
|
||
Assert.Equal(150f, RouteGeometry.ArcLengthNearestTo(wps, 100f, 50f), 1);
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// TravelTimeTo
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void TravelTimeTo_UniformSpeed()
|
||
{
|
||
// 弧长 1000m,速度 36km/h=10m/s → 100s
|
||
Assert.Equal(100f, RouteGeometry.TravelTimeTo(Line(0, 0, 1000, 0), 1000f, 36f), 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void TravelTimeTo_ZeroSpeed_Throws()
|
||
{
|
||
Assert.Throws<System.ArgumentException>(
|
||
() => RouteGeometry.TravelTimeTo(Line(0, 0, 1000, 0), 1000f, 0f));
|
||
}
|
||
|
||
// ═══════════════════════════════════════
|
||
// TangentAt
|
||
// ═══════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void TangentAt_XDirection_IsUnitX()
|
||
{
|
||
var wps = Line(0, 0, 1000, 0); // +X 方向
|
||
var (dx, dz) = RouteGeometry.TangentAt(wps, 500f);
|
||
Assert.Equal(1f, dx, 3);
|
||
Assert.Equal(0f, dz, 3);
|
||
}
|
||
|
||
[Fact]
|
||
public void TangentAt_ZDirection_IsUnitZ()
|
||
{
|
||
var wps = Line(0, 0, 0, 1000); // +Z 方向
|
||
var (dx, dz) = RouteGeometry.TangentAt(wps, 500f);
|
||
Assert.Equal(0f, dx, 3);
|
||
Assert.Equal(1f, dz, 3);
|
||
}
|
||
|
||
[Fact]
|
||
public void TangentAt_LShape_CornerTransition()
|
||
{
|
||
var wps = LShape();
|
||
// 第一段(弧长 50):+X
|
||
var (dx1, dz1) = RouteGeometry.TangentAt(wps, 50f);
|
||
Assert.Equal(1f, dx1, 3);
|
||
Assert.Equal(0f, dz1, 3);
|
||
// 第二段(弧长 150):+Z
|
||
var (dx2, dz2) = RouteGeometry.TangentAt(wps, 150f);
|
||
Assert.Equal(0f, dx2, 3);
|
||
Assert.Equal(1f, dz2, 3);
|
||
}
|
||
|
||
[Fact]
|
||
public void TangentAt_AlwaysUnitLength()
|
||
{
|
||
// 任意方向直线,切向量必须是单位向量
|
||
var wps = Line(0, 0, 3000, 4000); // 3-4-5 方向
|
||
var (dx, dz) = RouteGeometry.TangentAt(wps, 100f);
|
||
float mag = (float)System.Math.Sqrt(dx * dx + dz * dz);
|
||
Assert.Equal(1f, mag, 3);
|
||
}
|
||
}
|
||
}
|