< Summary

Information
Class: Trailblazer.Pathing.TraversalEdgeEvidence
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Graph/TraversalEvaluator.cs
Line coverage
100%
Covered lines: 15
Uncovered lines: 0
Coverable lines: 15
Total lines: 645
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Graph/TraversalEvaluator.cs

#LineLine coverage
 1//=======================================================================
 2// TraversalEvaluator.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using FixedMathSharp;
 9using GridForge.Grids.Topology;
 10
 11namespace Trailblazer.Pathing;
 12
 13/// <summary>Reports the allocation-free result of one traversal evaluation.</summary>
 14internal enum TraversalEvaluationStatus : byte
 15{
 16    Passable = 0,
 17    Impassable = 1,
 18    CostOverflow = 2
 19}
 20
 21/// <summary>Reports progress through one bounded explicit-connection evaluation.</summary>
 22internal enum TraversalExplicitEdgeStatus : byte
 23{
 24    Pending = 0,
 25    Passable = 1,
 26    Impassable = 2,
 27    CostOverflow = 3
 28}
 29
 30/// <summary>Returns one evaluated edge's exact cost and resolved portal evidence.</summary>
 31internal readonly struct TraversalEdgeEvidence
 32{
 33    internal TraversalEdgeEvidence(NavigationNodeRef dependencyNode)
 34    {
 2055735        this = default;
 2055736        DependencyNode = dependencyNode;
 2055737    }
 38
 39    internal TraversalEdgeEvidence(
 40        Fixed64 cost,
 41        GridNavigationPortal portal,
 42        Vector3d sourcePortalAnchor,
 43        Vector3d targetPortalAnchor,
 44        NavigationCellAddress sourceAddress,
 45        NavigationCellAddress targetAddress,
 46        Vector3d sourceFootAnchor,
 47        Vector3d targetFootAnchor,
 48        in GridCellPrism sourcePrism,
 49        in GridCellPrism targetPrism,
 50        NavigationNodeRef dependencyNode)
 51    {
 12338852        Cost = cost;
 12338853        Portal = portal;
 12338854        SourcePortalAnchor = sourcePortalAnchor;
 12338855        TargetPortalAnchor = targetPortalAnchor;
 12338856        SourceAddress = sourceAddress;
 12338857        TargetAddress = targetAddress;
 12338858        SourceFootAnchor = sourceFootAnchor;
 12338859        TargetFootAnchor = targetFootAnchor;
 12338860        SourcePrism = sourcePrism;
 12338861        TargetPrism = targetPrism;
 12338862        DependencyNode = dependencyNode;
 12338863    }
 64
 65    internal Fixed64 Cost { get; }
 66
 67    internal GridNavigationPortal Portal { get; }
 68
 69    internal Vector3d SourcePortalAnchor { get; }
 70
 71    internal Vector3d TargetPortalAnchor { get; }
 72
 73    internal NavigationCellAddress SourceAddress { get; }
 74
 75    internal NavigationCellAddress TargetAddress { get; }
 76
 77    internal Vector3d SourceFootAnchor { get; }
 78
 79    internal Vector3d TargetFootAnchor { get; }
 80
 81    internal GridCellPrism SourcePrism { get; }
 82
 83    internal GridCellPrism TargetPrism { get; }
 84
 85    internal NavigationNodeRef DependencyNode { get; }
 86}
 87
 88/// <summary>Retains scalar state while explicit corridor legs are evaluated.</summary>
 89internal struct TraversalExplicitEdgeWork
 90{
 91    internal NavigationExplicitConnectionRecord Record;
 92    internal NavigationNodeRef Target;
 93    internal NavigationPagedSequence<GridNavigationPortal>.Enumerator Portals;
 94    internal NavigationCellAddress PreviousAddress;
 95    internal GridCellPrism PreviousPrism;
 96    internal Vector3d SourceFootAnchor;
 97    internal Vector3d TargetFootAnchor;
 98    internal Vector3d PreviousFootAnchor;
 99    internal Fixed64 TargetEnterCost;
 100    internal Fixed64 TargetAreaEnterCost;
 101    internal int WitnessIndex;
 102}
 103
 104/// <summary>Evaluates nodes and native edges against one resolved immutable query profile.</summary>
 105internal readonly struct TraversalEvaluator
 106{
 107    private readonly NavigationWorldGraph _graph;
 108    private readonly NavigationAgentProfile _profile;
 109    private readonly NavigationAreaPolicy _areaPolicy;
 110    private readonly TraversalMedia _medium;
 111    private readonly TraversalMedium _exactMedium;
 112
 113    internal TraversalEvaluator(
 114        NavigationWorldGraph graph,
 115        NavigationAgentProfile profile,
 116        NavigationAreaPolicy areaPolicy,
 117        TraversalMedium medium)
 118    {
 119        SwiftThrowHelper.ThrowIfNull(graph, nameof(graph));
 120        SwiftThrowHelper.ThrowIfNull(areaPolicy, nameof(areaPolicy));
 121        profile.Validate(nameof(profile));
 122        TraversalMedia resolvedMedium = NavigationCell.ToMedia(medium);
 123        SwiftThrowHelper.ThrowIfArgument(
 124            resolvedMedium == TraversalMedia.None,
 125            nameof(medium),
 126            "Traversal medium must resolve to one exact authored medium.");
 127
 128        _graph = graph;
 129        _profile = profile;
 130        _areaPolicy = areaPolicy;
 131        _medium = resolvedMedium;
 132        _exactMedium = medium;
 133    }
 134
 135    internal NavigationAgentProfile Profile => _profile;
 136
 137    internal NavigationAreaPolicy AreaPolicy => _areaPolicy;
 138
 139    internal NavigationWorldGraph Graph => _graph;
 140
 141    internal bool TryGetPassableNodeState(
 142        NavigationNodeRef node,
 143        out NavigationNodeState state) =>
 144        TryGetPassableNode(node, out state, out _);
 145
 146    internal TraversalEvaluationStatus EvaluateEdge(
 147        NavigationNodeRef source,
 148        in NavigationGraphEdge edge,
 149        out TraversalEdgeEvidence evidence)
 150    {
 151        return edge.Kind switch
 152        {
 153            NavigationGraphEdgeKind.Explicit => EvaluateExplicitEdge(
 154                source,
 155                edge,
 156                out evidence),
 157            NavigationGraphEdgeKind.Seam => EvaluateAutomaticSeam(
 158                source,
 159                edge,
 160                out evidence),
 161            _ => EvaluateNative(source, edge, out evidence)
 162        };
 163    }
 164
 165    internal TraversalExplicitEdgeStatus BeginExplicitEdge(
 166        NavigationNodeRef source,
 167        in NavigationGraphEdge edge,
 168        out TraversalExplicitEdgeWork work)
 169    {
 170        work = default;
 171        NavigationExplicitConnectionRecord record = edge.ExplicitConnection;
 172        bool hasSourceAddress = _graph.TryGetNodeAddress(
 173            source,
 174            out NavigationCellAddress sourceAddress);
 175        bool hasTargetAddress = _graph.TryGetNodeAddress(
 176            edge.Target,
 177            out NavigationCellAddress targetAddress);
 178        bool hasSourcePrism = _graph.TryGetSeamPrism(
 179            sourceAddress,
 180            out GridCellPrism sourcePrism);
 181        System.Diagnostics.Debug.Assert(
 182            record != null
 183            && record.IsActive
 184            && edge.Kind == NavigationGraphEdgeKind.Explicit
 185            && hasSourceAddress
 186            && hasTargetAddress
 187            && record.NavigationPortals.Count == record.Definition.Witnesses.Count + 1
 188            && hasSourcePrism
 189            && sourceAddress.Equals(record.Source)
 190            && targetAddress.Equals(record.Destination));
 191        if (!TryGetPassableNode(source, out NavigationNodeState sourceState, out _)
 192            || !TryGetPassableNode(
 193                edge.Target,
 194                out NavigationNodeState targetState,
 195                out NavigationAreaRule targetRule))
 196        {
 197            return TraversalExplicitEdgeStatus.Impassable;
 198        }
 199
 200        KinematicBodyShape shape = _profile.Shape;
 201        NavigationConnection connection = record.Definition;
 202        if (shape.Radius > connection.PortalRadiusClearance
 203            || shape.Height > connection.PortalHeightClearance)
 204        {
 205            return TraversalExplicitEdgeStatus.Impassable;
 206        }
 207
 208        work = new TraversalExplicitEdgeWork
 209        {
 210            Record = record,
 211            Target = edge.Target,
 212            Portals = record.NavigationPortals.GetEnumerator(),
 213            PreviousAddress = sourceAddress,
 214            PreviousPrism = sourcePrism,
 215            SourceFootAnchor = sourceState.FootAnchor,
 216            TargetFootAnchor = targetState.FootAnchor,
 217            PreviousFootAnchor = sourceState.FootAnchor,
 218            TargetEnterCost = targetState.Cell.EnterCost,
 219            TargetAreaEnterCost = targetRule.AdditionalEnterCost
 220        };
 221        return TraversalExplicitEdgeStatus.Pending;
 222    }
 223
 224    internal TraversalExplicitEdgeStatus AdvanceExplicitEdge(
 225        ref TraversalExplicitEdgeWork work,
 226        out TraversalEdgeEvidence evidence)
 227    {
 228        evidence = default;
 229        NavigationExplicitConnectionRecord record = work.Record;
 230        NavigationConnection connection = record.Definition;
 231        bool final = work.WitnessIndex >= connection.Witnesses.Count;
 232        NavigationCellAddress nextAddress = final
 233            ? record.Destination
 234            : connection.Witnesses[work.WitnessIndex];
 235        NavigationNodeRef dependencyNode;
 236        Vector3d nextFootAnchor;
 237        if (final)
 238        {
 239            dependencyNode = work.Target;
 240            evidence = new TraversalEdgeEvidence(dependencyNode);
 241            nextFootAnchor = work.TargetFootAnchor;
 242        }
 243        else
 244        {
 245            bool hasDependencyNode = _graph.TryGetNodeRef(nextAddress, out dependencyNode);
 246            System.Diagnostics.Debug.Assert(hasDependencyNode);
 247            evidence = new TraversalEdgeEvidence(dependencyNode);
 248            if (!TryGetPassableNode(
 249                    dependencyNode,
 250                    out NavigationNodeState nextState,
 251                    out _))
 252            {
 253                return TraversalExplicitEdgeStatus.Impassable;
 254            }
 255            nextFootAnchor = nextState.FootAnchor;
 256        }
 257
 258        bool hasNextPrism = _graph.TryGetSeamPrism(nextAddress, out GridCellPrism nextPrism);
 259        bool hasPortal = work.Portals.MoveNext();
 260        System.Diagnostics.Debug.Assert(hasNextPrism && hasPortal);
 261        GridNavigationPortal portal = work.Portals.Current;
 262        KinematicBodyShape shape = _profile.Shape;
 263        System.Diagnostics.Debug.Assert(portal.IsValid);
 264        bool resolvedProfile = portal.TryResolveProfile(
 265            shape.Radius,
 266            shape.Height,
 267            out Vector3d sourcePortalAnchor,
 268            out Vector3d targetPortalAnchor);
 269        System.Diagnostics.Debug.Assert(resolvedProfile);
 270        System.Diagnostics.Debug.Assert(
 271            portal.FaceKind != VoxelContactFaceKind.Horizontal
 272            || (GridCellGeometry.TryGetNavigationPortalTraversalParameters(
 273                    work.PreviousPrism,
 274                    nextPrism,
 275                    portal,
 276                    sourcePortalAnchor,
 277                    targetPortalAnchor,
 278                    shape.Radius,
 279                    shape.Height,
 280                    out Fixed64 sourceParameter,
 281                    out Fixed64 targetParameter)
 282                && sourceParameter == Fixed64.Zero
 283                && targetParameter == Fixed64.One),
 284            "published explicit corridors retain the exact validated profile crossing");
 285
 286        TraversalEvaluationStatus vertical = EvaluateVerticalDelta(
 287            work.PreviousFootAnchor,
 288            nextFootAnchor);
 289        if (vertical != TraversalEvaluationStatus.Passable)
 290            return TraversalExplicitEdgeStatus.Impassable;
 291
 292        Fixed64 cost = Fixed64.Zero;
 293        if (final
 294            && !TryGetExplicitConnectionCost(
 295                work.SourceFootAnchor,
 296                connection.EntryAnchor,
 297                record.CorridorCost,
 298                connection.ExitAnchor,
 299                work.TargetFootAnchor,
 300                connection.AdditionalCost,
 301                work.TargetEnterCost,
 302                work.TargetAreaEnterCost,
 303                out cost))
 304        {
 305            return TraversalExplicitEdgeStatus.CostOverflow;
 306        }
 307
 308        evidence = new TraversalEdgeEvidence(
 309            cost,
 310            portal,
 311            sourcePortalAnchor,
 312            targetPortalAnchor,
 313            work.PreviousAddress,
 314            nextAddress,
 315            work.PreviousFootAnchor,
 316            nextFootAnchor,
 317            work.PreviousPrism,
 318            nextPrism,
 319            dependencyNode);
 320        work.PreviousAddress = nextAddress;
 321        work.PreviousPrism = nextPrism;
 322        work.PreviousFootAnchor = nextFootAnchor;
 323        if (!final)
 324            work.WitnessIndex++;
 325        return final
 326            ? TraversalExplicitEdgeStatus.Passable
 327            : TraversalExplicitEdgeStatus.Pending;
 328    }
 329
 330    private TraversalEvaluationStatus EvaluateNative(
 331        NavigationNodeRef source,
 332        in NavigationGraphEdge edge,
 333        out TraversalEdgeEvidence evidence)
 334    {
 335        evidence = default;
 336        if (!TryGetPassableNode(source, out NavigationNodeState sourceState, out _)
 337            || !TryGetPassableNode(
 338                edge.Target,
 339                out NavigationNodeState targetState,
 340                out NavigationAreaRule targetRule))
 341        {
 342            return TraversalEvaluationStatus.Impassable;
 343        }
 344
 345        KinematicBodyShape shape = _profile.Shape;
 346        GridNavigationPortal template = edge.NativePortal;
 347        System.Diagnostics.Debug.Assert(template.IsValid);
 348        if (shape.Radius > template.MaximumHorizontalRadius
 349            || shape.Height > template.MaximumBodyHeight)
 350        {
 351            return TraversalEvaluationStatus.Impassable;
 352        }
 353
 354        bool hasSourceAddress = _graph.TryGetNodeAddress(
 355            source,
 356            out NavigationCellAddress sourceAddress);
 357        bool hasTargetAddress = _graph.TryGetNodeAddress(
 358            edge.Target,
 359            out NavigationCellAddress targetAddress);
 360        bool hasSourcePrism = _graph.TryGetSeamPrism(
 361            sourceAddress,
 362            out GridCellPrism sourcePrism);
 363        bool hasTargetPrism = _graph.TryGetSeamPrism(
 364            targetAddress,
 365            out GridCellPrism targetPrism);
 366        System.Diagnostics.Debug.Assert(
 367            hasSourceAddress && hasTargetAddress && hasSourcePrism && hasTargetPrism);
 368        bool translated = template.TryTranslate(
 369            sourceState.Center,
 370            out GridNavigationPortal retainedPortal);
 371        System.Diagnostics.Debug.Assert(translated);
 372        bool createdPortal = GridCellGeometry.TryCreateNavigationPortal(
 373            sourcePrism,
 374            targetPrism,
 375            out GridNavigationPortal portal);
 376        System.Diagnostics.Debug.Assert(createdPortal && retainedPortal.IsValid);
 377        bool resolvedProfile = portal.TryResolveProfile(
 378            shape.Radius,
 379            shape.Height,
 380            out Vector3d sourcePortalFoot,
 381            out Vector3d targetPortalFoot);
 382        System.Diagnostics.Debug.Assert(resolvedProfile);
 383
 384        if (!TryGetPortalTraversalCost(
 385                useCeilingDistance: true,
 386                sourceState.FootAnchor,
 387                sourcePortalFoot,
 388                targetPortalFoot,
 389                targetState.FootAnchor,
 390                targetState.Cell.EnterCost,
 391                targetRule.AdditionalEnterCost,
 392                out Fixed64 total))
 393        {
 394            return TraversalEvaluationStatus.CostOverflow;
 395        }
 396
 397        evidence = new TraversalEdgeEvidence(
 398            total,
 399            portal,
 400            sourcePortalFoot,
 401            targetPortalFoot,
 402            sourceAddress,
 403            targetAddress,
 404            sourceState.FootAnchor,
 405            targetState.FootAnchor,
 406            sourcePrism,
 407            targetPrism,
 408            default);
 409        return TraversalEvaluationStatus.Passable;
 410    }
 411
 412    private TraversalEvaluationStatus EvaluateExplicitEdge(
 413        NavigationNodeRef source,
 414        in NavigationGraphEdge edge,
 415        out TraversalEdgeEvidence evidence)
 416    {
 417        evidence = default;
 418        Fixed64 cost = Fixed64.Zero;
 419        TraversalExplicitEdgeStatus status = BeginExplicitEdge(source, edge, out TraversalExplicitEdgeWork work);
 420        while (status == TraversalExplicitEdgeStatus.Pending)
 421        {
 422            status = AdvanceExplicitEdge(ref work, out TraversalEdgeEvidence step);
 423            cost = step.Cost;
 424        }
 425        TraversalEvaluationStatus result = status switch
 426        {
 427            TraversalExplicitEdgeStatus.Passable => TraversalEvaluationStatus.Passable,
 428            TraversalExplicitEdgeStatus.CostOverflow => TraversalEvaluationStatus.CostOverflow,
 429            _ => TraversalEvaluationStatus.Impassable
 430        };
 431        if (result == TraversalEvaluationStatus.Passable)
 432            evidence = new TraversalEdgeEvidence(
 433                cost,
 434                default,
 435                default,
 436                default,
 437                default,
 438                default,
 439                default,
 440                default,
 441                default,
 442                default,
 443                default);
 444        return result;
 445    }
 446
 447    private TraversalEvaluationStatus EvaluateAutomaticSeam(
 448        NavigationNodeRef source,
 449        in NavigationGraphEdge edge,
 450        out TraversalEdgeEvidence evidence)
 451    {
 452        evidence = default;
 453        NavigationAutomaticSeamRef seam = edge.AutomaticSeam;
 454        bool hasSourceAddress = _graph.TryGetNodeAddress(
 455            source,
 456            out NavigationCellAddress sourceAddress);
 457        bool hasTargetAddress = _graph.TryGetNodeAddress(
 458            edge.Target,
 459            out NavigationCellAddress targetAddress);
 460        System.Diagnostics.Debug.Assert(
 461            edge.Kind == NavigationGraphEdgeKind.Seam
 462            && seam.Pair != null
 463            && _graph.AutomaticSeams.IsActive(seam)
 464            && hasSourceAddress
 465            && sourceAddress.Equals(seam.Source)
 466            && hasTargetAddress
 467            && targetAddress.Equals(seam.Destination));
 468        if (!TryGetPassableNode(source, out NavigationNodeState sourceState, out _)
 469            || !TryGetPassableNode(
 470                edge.Target,
 471                out NavigationNodeState targetState,
 472                out NavigationAreaRule targetRule))
 473        {
 474            return TraversalEvaluationStatus.Impassable;
 475        }
 476
 477        KinematicBodyShape shape = _profile.Shape;
 478        GridNavigationPortal retainedPortal = seam.Portal;
 479        System.Diagnostics.Debug.Assert(retainedPortal.IsValid);
 480        if (shape.Radius > retainedPortal.MaximumHorizontalRadius
 481            || shape.Height > retainedPortal.MaximumBodyHeight)
 482        {
 483            return TraversalEvaluationStatus.Impassable;
 484        }
 485        bool hasSourcePrism = _graph.TryGetSeamPrism(
 486            sourceAddress,
 487            out GridCellPrism sourcePrism);
 488        bool hasTargetPrism = _graph.TryGetSeamPrism(
 489            targetAddress,
 490            out GridCellPrism targetPrism);
 491        bool createdPortal = GridCellGeometry.TryCreateNavigationPortal(
 492            sourcePrism,
 493            targetPrism,
 494            out GridNavigationPortal portal);
 495        System.Diagnostics.Debug.Assert(
 496            hasSourcePrism
 497            && hasTargetPrism
 498            && createdPortal
 499            && retainedPortal.IsValid);
 500        bool resolvedProfile = portal.TryResolveProfile(
 501            shape.Radius,
 502            shape.Height,
 503            out Vector3d sourcePortalFoot,
 504            out Vector3d targetPortalFoot);
 505        System.Diagnostics.Debug.Assert(resolvedProfile);
 506        TraversalEvaluationStatus vertical = EvaluateVerticalDelta(
 507            sourceState.FootAnchor,
 508            targetState.FootAnchor);
 509        if (vertical != TraversalEvaluationStatus.Passable)
 510            return vertical;
 511        if (!TryGetPortalTraversalCost(
 512                portal.FaceKind == VoxelContactFaceKind.Vertical,
 513                sourceState.FootAnchor,
 514                sourcePortalFoot,
 515                targetPortalFoot,
 516                targetState.FootAnchor,
 517                targetState.Cell.EnterCost,
 518                targetRule.AdditionalEnterCost,
 519                out Fixed64 total))
 520        {
 521            return TraversalEvaluationStatus.CostOverflow;
 522        }
 523
 524        evidence = new TraversalEdgeEvidence(
 525            total,
 526            portal,
 527            sourcePortalFoot,
 528            targetPortalFoot,
 529            sourceAddress,
 530            targetAddress,
 531            sourceState.FootAnchor,
 532            targetState.FootAnchor,
 533            sourcePrism,
 534            targetPrism,
 535            default);
 536        return TraversalEvaluationStatus.Passable;
 537    }
 538
 539    internal static bool TryGetExplicitConnectionCost(
 540        Vector3d sourceFootAnchor,
 541        Vector3d entryAnchor,
 542        Fixed64 corridorCost,
 543        Vector3d exitAnchor,
 544        Vector3d targetFootAnchor,
 545        Fixed64 additionalCost,
 546        Fixed64 targetEnterCost,
 547        Fixed64 targetAreaEnterCost,
 548        out Fixed64 total)
 549    {
 550        total = Fixed64.Zero;
 551        return Vector3d.TryGetDistance(sourceFootAnchor, entryAnchor, out Fixed64 sourceDistance)
 552            && Vector3d.TryGetDistance(exitAnchor, targetFootAnchor, out Fixed64 targetDistance)
 553            && Fixed64.TryAdd(sourceDistance, corridorCost, out total)
 554            && Fixed64.TryAdd(total, targetDistance, out total)
 555            && Fixed64.TryAdd(total, additionalCost, out total)
 556            && Fixed64.TryAdd(total, targetEnterCost, out total)
 557            && Fixed64.TryAdd(total, targetAreaEnterCost, out total);
 558    }
 559
 560    internal static bool TryGetPortalTraversalCost(
 561        bool useCeilingDistance,
 562        Vector3d sourceFootAnchor,
 563        Vector3d sourcePortalFoot,
 564        Vector3d targetPortalFoot,
 565        Vector3d targetFootAnchor,
 566        Fixed64 targetEnterCost,
 567        Fixed64 targetAreaEnterCost,
 568        out Fixed64 total)
 569    {
 570        total = Fixed64.Zero;
 571        bool hasSourceDistance = useCeilingDistance
 572            ? NavigationDistanceMath.TryCeiling(
 573                sourceFootAnchor,
 574                sourcePortalFoot,
 575                out Fixed64 sourceDistance)
 576            : Vector3d.TryGetDistance(
 577                sourceFootAnchor,
 578                sourcePortalFoot,
 579                out sourceDistance);
 580        bool hasTargetDistance = useCeilingDistance
 581            ? NavigationDistanceMath.TryCeiling(
 582                targetPortalFoot,
 583                targetFootAnchor,
 584                out Fixed64 targetDistance)
 585            : Vector3d.TryGetDistance(
 586                targetPortalFoot,
 587                targetFootAnchor,
 588                out targetDistance);
 589        return hasSourceDistance
 590            && hasTargetDistance
 591            && Fixed64.TryAdd(sourceDistance, targetDistance, out total)
 592            && Fixed64.TryAdd(total, targetEnterCost, out total)
 593            && Fixed64.TryAdd(total, targetAreaEnterCost, out total);
 594    }
 595
 596    private TraversalEvaluationStatus EvaluateVerticalDelta(
 597        Vector3d sourceFootAnchor,
 598        Vector3d targetFootAnchor)
 599    {
 600        bool hasVerticalDelta = Fixed64.TrySubtract(
 601            targetFootAnchor.Y,
 602            sourceFootAnchor.Y,
 603            out Fixed64 verticalDelta);
 604        System.Diagnostics.Debug.Assert(hasVerticalDelta);
 605        if (verticalDelta > Fixed64.Zero)
 606        {
 607            return verticalDelta <= _profile.MaxStepUp
 608                ? TraversalEvaluationStatus.Passable
 609                : TraversalEvaluationStatus.Impassable;
 610        }
 611        if (verticalDelta >= Fixed64.Zero)
 612            return TraversalEvaluationStatus.Passable;
 613        bool hasDrop = Fixed64.TrySubtract(Fixed64.Zero, verticalDelta, out Fixed64 drop);
 614        System.Diagnostics.Debug.Assert(hasDrop);
 615        return drop <= _profile.MaxDropDown
 616            ? TraversalEvaluationStatus.Passable
 617            : TraversalEvaluationStatus.Impassable;
 618    }
 619
 620    internal bool TryGetPassableNode(
 621        NavigationNodeRef node,
 622        out NavigationNodeState state,
 623        out NavigationAreaRule areaRule)
 624    {
 625        areaRule = default;
 626        if (!_graph.TryGetNodeState(node, _exactMedium, out state)
 627            || !state.IsPresent
 628            || state.ObstacleCount != 0
 629            || (_profile.AllowedMedia & _medium) != _medium
 630            || (state.Cell.Media & _medium) != _medium
 631            || (_profile.Capabilities & state.Cell.RequiredCapabilities)
 632                != state.Cell.RequiredCapabilities
 633            || !_areaPolicy.TryGetRule(state.Cell.Area, out areaRule)
 634            || !areaRule.IsAllowed
 635            || _profile.Shape.Radius > state.Cell.RadiusClearance
 636            || _profile.Shape.Height > state.Cell.HeightClearance)
 637        {
 638            state = default;
 639            areaRule = default;
 640            return false;
 641        }
 642
 643        return true;
 644    }
 645}