SweptAreaGeometryTests.cs 6.26 KB
using Rcs.Application.Services.PathFind.Models;
using Rcs.Domain.Enums;
using Rcs.Infrastructure.PathFinding.Services;

namespace Rcs.Infrastructure.Tests;

public class SweptAreaGeometryTests
{
    [Fact]
    public void BuildFootprintGeometry_UsesSafetyDistanceOnce()
    {
        var dimensions = new RobotSweepDimensions(
            Width: 2d,
            Length: 4d,
            SafetyDistance: 0.5d,
            UseDefaultWidth: false,
            UseDefaultLength: false,
            UseDefaultSafetyDistance: false);

        var geometry = SweptAreaCoverageResolver.BuildFootprintGeometry(
            new SweepPoint(0d, 0d),
            0d,
            dimensions);

        var envelope = geometry.EnvelopeInternal;

        Assert.Equal(-2.25d, envelope.MinX, 6);
        Assert.Equal(2.25d, envelope.MaxX, 6);
        Assert.Equal(-1.25d, envelope.MinY, 6);
        Assert.Equal(1.25d, envelope.MaxY, 6);
    }

    [Fact]
    public void BuildSweptGeometryFromSegments_SameCenterLineDifferentPoseCreatesDifferentGeometry()
    {
        var graph = BuildStraightGraph();
        var dimensions = new RobotSweepDimensions(
            Width: 1d,
            Length: 3d,
            SafetyDistance: 0d,
            UseDefaultWidth: false,
            UseDefaultLength: false,
            UseDefaultSafetyDistance: false);

        var forward = SweptAreaCoverageResolver.BuildSweptGeometryFromSegments(
            new[] { BuildSegment(0d, 0d) },
            graph,
            dimensions);
        var sideway = SweptAreaCoverageResolver.BuildSweptGeometryFromSegments(
            new[] { BuildSegment(Math.PI / 2d, Math.PI / 2d) },
            graph,
            dimensions);

        Assert.False(forward.EqualsTopologically(sideway));
        Assert.NotEqual(forward.Area, sideway.Area, precision: 6);
    }

    [Fact]
    public void CoveredResourceOverlap_DoesNotImplyGeometryIntersection()
    {
        var dimensions = new RobotSweepDimensions(
            Width: 1d,
            Length: 1d,
            SafetyDistance: 0d,
            UseDefaultWidth: false,
            UseDefaultLength: false,
            UseDefaultSafetyDistance: false);

        var left = SweptAreaCoverageResolver.BuildFootprintGeometry(
            new SweepPoint(0d, 0d),
            0d,
            dimensions);
        var right = SweptAreaCoverageResolver.BuildFootprintGeometry(
            new SweepPoint(5d, 0d),
            0d,
            dimensions);

        var leftDescriptor = new SweptGeometryDescriptor
        {
            GeometryWkbBase64 = Convert.ToBase64String(new NetTopologySuite.IO.WKBWriter().Write(left)),
            MinX = left.EnvelopeInternal.MinX,
            MinY = left.EnvelopeInternal.MinY,
            MaxX = left.EnvelopeInternal.MaxX,
            MaxY = left.EnvelopeInternal.MaxY,
            CoveredNodeCodes = new List<string> { "N1" },
            CoveredEdges = new List<VdaLockCoveredEdge> { new() { EdgeCode = "E1" } },
            Kind = SweptGeometryKind.PlannedDispatch
        };
        var rightDescriptor = new SweptGeometryDescriptor
        {
            GeometryWkbBase64 = Convert.ToBase64String(new NetTopologySuite.IO.WKBWriter().Write(right)),
            MinX = right.EnvelopeInternal.MinX,
            MinY = right.EnvelopeInternal.MinY,
            MaxX = right.EnvelopeInternal.MaxX,
            MaxY = right.EnvelopeInternal.MaxY,
            CoveredNodeCodes = new List<string> { "N1" },
            CoveredEdges = new List<VdaLockCoveredEdge> { new() { EdgeCode = "E1" } },
            Kind = SweptGeometryKind.PlannedDispatch
        };

        var leftGeometry = SweptAreaCoverageResolver.ReadGeometryFromWkbBase64(leftDescriptor.GeometryWkbBase64);
        var rightGeometry = SweptAreaCoverageResolver.ReadGeometryFromWkbBase64(rightDescriptor.GeometryWkbBase64);

        Assert.True(leftDescriptor.CoveredNodeCodes.Intersect(rightDescriptor.CoveredNodeCodes).Any());
        Assert.False(leftGeometry.Intersects(rightGeometry));
    }

    [Fact]
    public void BuildRotationSweptGeometry_CoversIntermediateFootprint()
    {
        var dimensions = new RobotSweepDimensions(
            Width: 1d,
            Length: 3d,
            SafetyDistance: 0d,
            UseDefaultWidth: false,
            UseDefaultLength: false,
            UseDefaultSafetyDistance: false);

        var rotation = SweptAreaCoverageResolver.BuildRotationSweptGeometry(
            new SweepPoint(0d, 0d),
            0d,
            Math.PI / 2d,
            dimensions);
        var intermediate = SweptAreaCoverageResolver.BuildFootprintGeometry(
            new SweepPoint(0d, 0d),
            Math.PI / 4d,
            dimensions);

        Assert.True(rotation.Covers(intermediate));
    }

    private static PathGraph BuildStraightGraph()
    {
        var fromId = Guid.NewGuid();
        var toId = Guid.NewGuid();
        var edgeId = Guid.NewGuid();

        return new PathGraph
        {
            MapId = Guid.NewGuid(),
            MapCode = "M1",
            Nodes = new Dictionary<Guid, PathNode>
            {
                [fromId] = new()
                {
                    NodeId = fromId,
                    NodeCode = "N1",
                    X = 0d,
                    Y = 0d,
                    Active = true
                },
                [toId] = new()
                {
                    NodeId = toId,
                    NodeCode = "N2",
                    X = 5d,
                    Y = 0d,
                    Active = true
                }
            },
            Edges = new Dictionary<Guid, PathEdge>
            {
                [edgeId] = new()
                {
                    EdgeId = edgeId,
                    EdgeCode = "E12",
                    FromNodeId = fromId,
                    ToNodeId = toId,
                    FromNodeCode = "N1",
                    ToNodeCode = "N2",
                    Active = true,
                    CurveType = MapEdgeCurveType.Straight
                }
            }
        };
    }

    private static PathSegmentWithCode BuildSegment(double startTheta, double endTheta)
    {
        return new PathSegmentWithCode
        {
            FromNodeCode = "N1",
            ToNodeCode = "N2",
            EdgeCode = "E12",
            StartTheta = startTheta,
            EndTheta = endTheta,
            Angle = 0d
        };
    }
}