508 lines
23 KiB
C#
508 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade;
|
|
using MultiWheelC.TrajectoryPlanning.Mapping;
|
|
|
|
namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test;
|
|
|
|
/// <summary>Clumsy 粗路径手动测试可选择的固定场景。</summary>
|
|
public enum CoarsePathTestScenario
|
|
{
|
|
/// <summary>明确允许的空地图直达场景。</summary>
|
|
ExplicitEmpty,
|
|
|
|
/// <summary>由中央矩形阻断直线的绕行场景。</summary>
|
|
RectangleDetour,
|
|
|
|
/// <summary>同时包含手工圆形、矩形与 TwoLeg 快照的多来源场景。</summary>
|
|
ManualAndTwoLeg,
|
|
|
|
/// <summary>与矩形绕行输入完全一致,用于在同一服务中验证输入缓存命中。</summary>
|
|
CacheHit,
|
|
|
|
/// <summary>起步前进、终点倒车进入的换向场景。</summary>
|
|
ReverseGearSwitch,
|
|
|
|
/// <summary>由贯穿边界的障碍带分隔起终点的无解场景。</summary>
|
|
NoFeasiblePath,
|
|
}
|
|
|
|
/// <summary>手动障碍物输入支持的世界几何类型。</summary>
|
|
public enum ManualCoarsePathObstacleKind
|
|
{
|
|
/// <summary>由世界中心和半径定义的圆形障碍物。</summary>
|
|
Circle,
|
|
|
|
/// <summary>由世界中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。</summary>
|
|
AxisAlignedRectangle,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 手动粗路径测试的不可变障碍物输入。
|
|
/// 所有中心和尺寸均使用世界 mm;矩形始终与世界坐标轴平行,不包含旋转角。
|
|
/// </summary>
|
|
public sealed class ManualCoarsePathObstacle
|
|
{
|
|
private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters,
|
|
double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters)
|
|
{
|
|
Kind = kind;
|
|
CenterXMillimeters = centerXMillimeters;
|
|
CenterYMillimeters = centerYMillimeters;
|
|
SizeXMillimeters = sizeXMillimeters;
|
|
SizeYMillimeters = sizeYMillimeters;
|
|
}
|
|
|
|
/// <summary>障碍物的支持几何类型。</summary>
|
|
public ManualCoarsePathObstacleKind Kind { get; }
|
|
|
|
/// <summary>几何中心世界 X 坐标,单位 mm。</summary>
|
|
public double CenterXMillimeters { get; }
|
|
|
|
/// <summary>几何中心世界 Y 坐标,单位 mm。</summary>
|
|
public double CenterYMillimeters { get; }
|
|
|
|
/// <summary>圆形时为半径,矩形时为 X 方向长度;单位 mm。</summary>
|
|
public double SizeXMillimeters { get; }
|
|
|
|
/// <summary>圆形时为半径,矩形时为 Y 方向宽度;单位 mm。</summary>
|
|
public double SizeYMillimeters { get; }
|
|
|
|
/// <summary>创建圆形障碍物。参数:圆心和半径均使用世界 mm,半径必须为有限正数。</summary>
|
|
public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters,
|
|
double radiusMillimeters)
|
|
{
|
|
EnsureFinite(centerXMillimeters, nameof(centerXMillimeters));
|
|
EnsureFinite(centerYMillimeters, nameof(centerYMillimeters));
|
|
EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters));
|
|
return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters,
|
|
centerYMillimeters, radiusMillimeters, radiusMillimeters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建轴对齐矩形障碍物。
|
|
/// 参数:中心、X 方向长度和 Y 方向宽度均使用世界 mm;两个尺寸必须为有限正数。
|
|
/// </summary>
|
|
public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters,
|
|
double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters)
|
|
{
|
|
EnsureFinite(centerXMillimeters, nameof(centerXMillimeters));
|
|
EnsureFinite(centerYMillimeters, nameof(centerYMillimeters));
|
|
EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters));
|
|
EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters));
|
|
return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle,
|
|
centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters);
|
|
}
|
|
|
|
private static void EnsureFinite(double value, string parameterName)
|
|
{
|
|
if (double.IsNaN(value) || double.IsInfinity(value))
|
|
throw new ArgumentOutOfRangeException(parameterName, "Value must be finite.");
|
|
}
|
|
|
|
private static void EnsurePositiveFinite(double value, string parameterName)
|
|
{
|
|
EnsureFinite(value, parameterName);
|
|
if (value <= 0d)
|
|
throw new ArgumentOutOfRangeException(parameterName, "Value must be positive.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clumsy 粗路径测试的纯输入工厂。
|
|
/// 固定场景不读取 UI、传感器、定位或时钟;传入 AMR 位姿的手动入口仅在此处完成世界 mm/deg 到核心 m/rad 的转换。
|
|
/// </summary>
|
|
public static class CoarsePathScenarioFactory
|
|
{
|
|
private const float MapXMinMillimeters = 0f;
|
|
private const float MapXMaxMillimeters = 6000f;
|
|
private const float MapYMinMillimeters = 0f;
|
|
private const float MapYMaxMillimeters = 4000f;
|
|
private const float ResolutionMillimeters = 50f;
|
|
private const double MillimetersPerMeter = 1000d;
|
|
private const double DegreesToRadians = Math.PI / 180d;
|
|
private const double ManualMapPaddingMillimeters = 8000d;
|
|
private const int MaximumManualObstacleCount = 20;
|
|
|
|
/// <summary>
|
|
/// 创建一个新的固定测试业务请求。
|
|
/// 返回:每次调用都返回独立的可变请求对象,供调用方安全地传入同一个长期存活的规划服务。
|
|
/// </summary>
|
|
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario)
|
|
{
|
|
return CreateCore(scenario, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建以当前 AMR 世界位姿为起点的固定测试业务请求。
|
|
/// 参数:X/Y 使用世界 mm,航向使用 deg;地图、目标和障碍物仅随 AMR 坐标平移,TwoLeg 朝向保持不变。
|
|
/// </summary>
|
|
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario,
|
|
double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)
|
|
{
|
|
return CreateCore(scenario,
|
|
new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees));
|
|
}
|
|
|
|
private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario, FixedScenarioAnchor anchor)
|
|
{
|
|
switch (scenario)
|
|
{
|
|
case CoarsePathTestScenario.ExplicitEmpty:
|
|
return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
|
case CoarsePathTestScenario.RectangleDetour:
|
|
case CoarsePathTestScenario.CacheHit:
|
|
return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
|
case CoarsePathTestScenario.ManualAndTwoLeg:
|
|
return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor));
|
|
case CoarsePathTestScenario.ReverseGearSwitch:
|
|
return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
|
case CoarsePathTestScenario.NoFeasiblePath:
|
|
return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(scenario));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建传入 AMR 世界位姿和手动世界终点的空图演示请求。
|
|
/// 参数:X/Y 使用世界 mm,航向使用 deg;返回请求中的 <see cref="Pose2D"/> 使用世界 m/rad。
|
|
/// 注意:这是坐标、路径和取消流程的演示空图,不能表示现场不存在障碍物。
|
|
/// </summary>
|
|
public static CoarsePathPlanningJob CreateManualGoalDemo(
|
|
double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
|
|
double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees)
|
|
{
|
|
return CreateManualObstacleDemo(startXMillimeters, startYMillimeters, startHeadingDegrees,
|
|
goalXMillimeters, goalYMillimeters, goalHeadingDegrees,
|
|
Array.Empty<ManualCoarsePathObstacle>(), 0L);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建传入 AMR 世界位姿、手动世界终点和手动障碍物快照的测试请求。
|
|
/// 参数:位姿 X/Y、障碍物中心和尺寸使用世界 mm,航向使用 deg;返回的 <see cref="Pose2D"/> 使用 m/rad。
|
|
/// 障碍物非空时 obstacleSnapshotVersion 必须为正数,以避免长期服务错误复用旧地图;零障碍物才创建显式空图演示。
|
|
/// </summary>
|
|
public static CoarsePathPlanningJob CreateManualObstacleDemo(
|
|
double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
|
|
double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees,
|
|
IReadOnlyList<ManualCoarsePathObstacle> obstacles, long obstacleSnapshotVersion)
|
|
{
|
|
EnsureFinite(startXMillimeters, nameof(startXMillimeters));
|
|
EnsureFinite(startYMillimeters, nameof(startYMillimeters));
|
|
EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees));
|
|
EnsureFinite(goalXMillimeters, nameof(goalXMillimeters));
|
|
EnsureFinite(goalYMillimeters, nameof(goalYMillimeters));
|
|
EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees));
|
|
|
|
if (obstacles == null) throw new ArgumentNullException(nameof(obstacles));
|
|
if (obstacles.Count > MaximumManualObstacleCount)
|
|
throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle count exceeds the supported limit.");
|
|
if (obstacles.Count != 0 && obstacleSnapshotVersion <= 0L)
|
|
throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion), "Obstacle snapshots require a positive version.");
|
|
|
|
return CreateJob(
|
|
CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters,
|
|
obstacles, obstacleSnapshotVersion),
|
|
ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees),
|
|
ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees),
|
|
null,
|
|
GoalDirectionConstraint.Any);
|
|
}
|
|
|
|
private static CoarsePathPlanningJob CreateExplicitEmpty(FixedScenarioTransform transform)
|
|
{
|
|
return CreateJob(
|
|
CreateMap(true, Array.Empty<IMapObstacleSource>(), transform),
|
|
transform.Pose(1d, 2d, 0d),
|
|
transform.Pose(5d, 2d, 0d),
|
|
null,
|
|
GoalDirectionConstraint.Forward);
|
|
}
|
|
|
|
private static CoarsePathPlanningJob CreateRectangleDetour(FixedScenarioTransform transform)
|
|
{
|
|
IMapObstacleSource[] sources =
|
|
{
|
|
new ManualObstacleSource("manual", 1L, true, new IMapObstacle[]
|
|
{
|
|
new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f),
|
|
transform.Y(1200f), transform.Y(2800f)),
|
|
}),
|
|
};
|
|
CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d),
|
|
transform.Pose(5d, 2d, 0d), null, GoalDirectionConstraint.Forward);
|
|
// 固定绕行场景保留最优启发式;30 秒覆盖较慢测试环境,UI 仍可随时取消。
|
|
job.Configuration.SearchTimeout = TimeSpan.FromSeconds(30d);
|
|
return job;
|
|
}
|
|
|
|
private static CoarsePathPlanningJob CreateManualAndTwoLeg(FixedScenarioTransform transform)
|
|
{
|
|
IMapObstacleSource[] sources =
|
|
{
|
|
new ManualObstacleSource("manual", 2L, true, new IMapObstacle[]
|
|
{
|
|
new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f),
|
|
new AxisAlignedRectangleObstacle(transform.X(3000f), transform.X(3600f),
|
|
transform.Y(2000f), transform.Y(2600f)),
|
|
}),
|
|
new TwoLegObstacleSource("two-leg", 1L, true, new TwoLegProjectionInput(true,
|
|
transform.X(3900f), transform.Y(2500f), 0d,
|
|
-180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")),
|
|
};
|
|
CoarsePathPlanningJob job = CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 1d, 0d),
|
|
transform.Pose(5d, 3d, 0d), null, GoalDirectionConstraint.Forward);
|
|
// 多来源场景的最优绕行会受机器负载影响;放宽演示总预算但保留全部碰撞与目标判定。
|
|
job.Configuration.SearchTimeout = TimeSpan.FromSeconds(15d);
|
|
return job;
|
|
}
|
|
|
|
private static CoarsePathPlanningJob CreateReverseGearSwitch(FixedScenarioTransform transform)
|
|
{
|
|
return CreateJob(
|
|
CreateMap(true, Array.Empty<IMapObstacleSource>(), transform),
|
|
transform.Pose(1d, 2d, 0d),
|
|
transform.Pose(4d, 2d, 0d),
|
|
TravelDirection.Forward,
|
|
GoalDirectionConstraint.Reverse);
|
|
}
|
|
|
|
private static CoarsePathPlanningJob CreateNoFeasiblePath(FixedScenarioTransform transform)
|
|
{
|
|
IMapObstacleSource[] sources =
|
|
{
|
|
new ManualObstacleSource("manual", 3L, true, new IMapObstacle[]
|
|
{
|
|
new AxisAlignedRectangleObstacle(transform.X(2900f), transform.X(3100f),
|
|
transform.Y(0f), transform.Y(4000f)),
|
|
}),
|
|
};
|
|
return CreateJob(CreateMap(false, sources, transform), transform.Pose(1d, 2d, 0d),
|
|
transform.Pose(5d, 2d, 0d),
|
|
null, GoalDirectionConstraint.Forward);
|
|
}
|
|
|
|
private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal,
|
|
TravelDirection? startDirection, GoalDirectionConstraint goalDirection)
|
|
{
|
|
return new CoarsePathPlanningJob
|
|
{
|
|
MapRequest = mapRequest,
|
|
Start = start,
|
|
Goal = goal,
|
|
Vehicle = new VehicleParameters
|
|
{
|
|
LengthMeters = 0.80d,
|
|
WidthMeters = 0.60d,
|
|
SafetyMarginMeters = 0.05d,
|
|
MaximumCurvaturePerMeter = 1d / 1.20d,
|
|
},
|
|
Configuration = new HybridAStarConfiguration(),
|
|
StartDirection = startDirection,
|
|
GoalDirection = goalDirection,
|
|
};
|
|
}
|
|
|
|
private static PlanningMapRequest CreateMap(bool allowExplicitEmptyMap, IReadOnlyList<IMapObstacleSource> sources,
|
|
FixedScenarioTransform transform)
|
|
{
|
|
return new PlanningMapRequest
|
|
{
|
|
Bounds = new MapBoundsMm(transform.X(MapXMinMillimeters), transform.X(MapXMaxMillimeters),
|
|
transform.Y(MapYMinMillimeters), transform.Y(MapYMaxMillimeters)),
|
|
ResolutionMm = ResolutionMillimeters,
|
|
ObstacleSources = sources,
|
|
AllowExplicitEmptyMap = allowExplicitEmptyMap,
|
|
};
|
|
}
|
|
|
|
private sealed class FixedScenarioAnchor
|
|
{
|
|
public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees)
|
|
{
|
|
EnsureFinite(xMillimeters, nameof(xMillimeters));
|
|
EnsureFinite(yMillimeters, nameof(yMillimeters));
|
|
EnsureFinite(headingDegrees, nameof(headingDegrees));
|
|
XMillimeters = xMillimeters;
|
|
YMillimeters = yMillimeters;
|
|
HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians);
|
|
}
|
|
|
|
public double XMillimeters { get; }
|
|
public double YMillimeters { get; }
|
|
public double HeadingRadians { get; }
|
|
}
|
|
|
|
private sealed class FixedScenarioTransform
|
|
{
|
|
private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians)
|
|
{
|
|
DeltaXMillimeters = deltaXMillimeters;
|
|
DeltaYMillimeters = deltaYMillimeters;
|
|
HeadingDeltaRadians = headingDeltaRadians;
|
|
}
|
|
|
|
public double DeltaXMillimeters { get; }
|
|
public double DeltaYMillimeters { get; }
|
|
public double HeadingDeltaRadians { get; }
|
|
|
|
public static FixedScenarioTransform From(double baselineStartXMillimeters,
|
|
double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor)
|
|
{
|
|
if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d);
|
|
return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters,
|
|
anchor.YMillimeters - baselineStartYMillimeters,
|
|
NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians));
|
|
}
|
|
|
|
public float X(float value)
|
|
{
|
|
return ToFiniteFloat(value + DeltaXMillimeters, nameof(value));
|
|
}
|
|
|
|
public float Y(float value)
|
|
{
|
|
return ToFiniteFloat(value + DeltaYMillimeters, nameof(value));
|
|
}
|
|
|
|
public Pose2D Pose(double xMeters, double yMeters, double headingRadians)
|
|
{
|
|
return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter,
|
|
(yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter,
|
|
NormalizeRadians(headingRadians + HeadingDeltaRadians));
|
|
}
|
|
}
|
|
|
|
private static PlanningMapRequest CreateManualDemoMap(double startXMillimeters, double startYMillimeters,
|
|
double goalXMillimeters, double goalYMillimeters, IReadOnlyList<ManualCoarsePathObstacle> obstacles,
|
|
long obstacleSnapshotVersion)
|
|
{
|
|
double minimumX = Math.Min(startXMillimeters, goalXMillimeters);
|
|
double maximumX = Math.Max(startXMillimeters, goalXMillimeters);
|
|
double minimumY = Math.Min(startYMillimeters, goalYMillimeters);
|
|
double maximumY = Math.Max(startYMillimeters, goalYMillimeters);
|
|
for (int index = 0; index < obstacles.Count; index++)
|
|
{
|
|
ManualCoarsePathObstacle obstacle = obstacles[index] ??
|
|
throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles));
|
|
double halfX;
|
|
double halfY;
|
|
switch (obstacle.Kind)
|
|
{
|
|
case ManualCoarsePathObstacleKind.Circle:
|
|
EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles));
|
|
halfX = obstacle.SizeXMillimeters;
|
|
halfY = obstacle.SizeYMillimeters;
|
|
break;
|
|
case ManualCoarsePathObstacleKind.AxisAlignedRectangle:
|
|
EnsurePositiveFinite(obstacle.SizeXMillimeters, nameof(obstacles));
|
|
EnsurePositiveFinite(obstacle.SizeYMillimeters, nameof(obstacles));
|
|
halfX = obstacle.SizeXMillimeters / 2d;
|
|
halfY = obstacle.SizeYMillimeters / 2d;
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported.");
|
|
}
|
|
|
|
minimumX = Math.Min(minimumX, obstacle.CenterXMillimeters - halfX);
|
|
maximumX = Math.Max(maximumX, obstacle.CenterXMillimeters + halfX);
|
|
minimumY = Math.Min(minimumY, obstacle.CenterYMillimeters - halfY);
|
|
maximumY = Math.Max(maximumY, obstacle.CenterYMillimeters + halfY);
|
|
}
|
|
|
|
float xMin = ToGridLowerBound(minimumX - ManualMapPaddingMillimeters);
|
|
float xMax = ToGridUpperBound(maximumX + ManualMapPaddingMillimeters);
|
|
float yMin = ToGridLowerBound(minimumY - ManualMapPaddingMillimeters);
|
|
float yMax = ToGridUpperBound(maximumY + ManualMapPaddingMillimeters);
|
|
bool isExplicitEmptyMap = obstacles.Count == 0;
|
|
return new PlanningMapRequest
|
|
{
|
|
Bounds = new MapBoundsMm(xMin, xMax, yMin, yMax),
|
|
ResolutionMm = ResolutionMillimeters,
|
|
ObstacleSources = isExplicitEmptyMap ? Array.Empty<IMapObstacleSource>() :
|
|
new IMapObstacleSource[]
|
|
{
|
|
new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true,
|
|
ConvertManualObstacles(obstacles)),
|
|
},
|
|
AllowExplicitEmptyMap = isExplicitEmptyMap,
|
|
};
|
|
}
|
|
|
|
private static IMapObstacle[] ConvertManualObstacles(IReadOnlyList<ManualCoarsePathObstacle> obstacles)
|
|
{
|
|
var result = new IMapObstacle[obstacles.Count];
|
|
for (int index = 0; index < obstacles.Count; index++)
|
|
{
|
|
ManualCoarsePathObstacle obstacle = obstacles[index] ??
|
|
throw new ArgumentException("Manual obstacle entries cannot be null.", nameof(obstacles));
|
|
switch (obstacle.Kind)
|
|
{
|
|
case ManualCoarsePathObstacleKind.Circle:
|
|
result[index] = new CircleObstacle(ToFiniteFloat(obstacle.CenterXMillimeters, nameof(obstacles)),
|
|
ToFiniteFloat(obstacle.CenterYMillimeters, nameof(obstacles)),
|
|
ToFiniteFloat(obstacle.SizeXMillimeters, nameof(obstacles)));
|
|
break;
|
|
case ManualCoarsePathObstacleKind.AxisAlignedRectangle:
|
|
double halfX = obstacle.SizeXMillimeters / 2d;
|
|
double halfY = obstacle.SizeYMillimeters / 2d;
|
|
result[index] = new AxisAlignedRectangleObstacle(
|
|
ToFiniteFloat(obstacle.CenterXMillimeters - halfX, nameof(obstacles)),
|
|
ToFiniteFloat(obstacle.CenterXMillimeters + halfX, nameof(obstacles)),
|
|
ToFiniteFloat(obstacle.CenterYMillimeters - halfY, nameof(obstacles)),
|
|
ToFiniteFloat(obstacle.CenterYMillimeters + halfY, nameof(obstacles)));
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(obstacles), "Manual obstacle kind is not supported.");
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees)
|
|
{
|
|
return new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter,
|
|
headingDegrees * DegreesToRadians);
|
|
}
|
|
|
|
private static double NormalizeRadians(double angle)
|
|
{
|
|
double normalized = angle % (2d * Math.PI);
|
|
if (normalized <= -Math.PI) return normalized + 2d * Math.PI;
|
|
return normalized > Math.PI ? normalized - 2d * Math.PI : normalized;
|
|
}
|
|
|
|
private static float ToGridLowerBound(double millimeters)
|
|
{
|
|
double rounded = Math.Floor(millimeters / ResolutionMillimeters) * ResolutionMillimeters;
|
|
return ToFiniteFloat(rounded, nameof(millimeters));
|
|
}
|
|
|
|
private static float ToGridUpperBound(double millimeters)
|
|
{
|
|
double rounded = Math.Ceiling(millimeters / ResolutionMillimeters) * ResolutionMillimeters;
|
|
return ToFiniteFloat(rounded, nameof(millimeters));
|
|
}
|
|
|
|
private static float ToFiniteFloat(double value, string parameterName)
|
|
{
|
|
if (double.IsNaN(value) || double.IsInfinity(value) || value < float.MinValue || value > float.MaxValue)
|
|
throw new ArgumentOutOfRangeException(parameterName, "Value cannot be represented as a finite millimeter coordinate.");
|
|
return (float)value;
|
|
}
|
|
|
|
private static void EnsureFinite(double value, string parameterName)
|
|
{
|
|
if (double.IsNaN(value) || double.IsInfinity(value))
|
|
throw new ArgumentOutOfRangeException(parameterName, "Value must be finite.");
|
|
}
|
|
|
|
private static void EnsurePositiveFinite(double value, string parameterName)
|
|
{
|
|
EnsureFinite(value, parameterName);
|
|
if (value <= 0d)
|
|
throw new ArgumentOutOfRangeException(parameterName, "Value must be positive.");
|
|
}
|
|
}
|