| | | 1 | | //======================================================================= |
| | | 2 | | // GridCellGeometry.NavigationBodySegment.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 | | |
| | | 8 | | using System; |
| | | 9 | | using FixedMathSharp; |
| | | 10 | | using FixedMathSharp.Geometry; |
| | | 11 | | |
| | | 12 | | namespace GridForge.Grids.Topology; |
| | | 13 | | |
| | | 14 | | public static partial class GridCellGeometry |
| | | 15 | | { |
| | | 16 | | /// <summary> |
| | | 17 | | /// Attempts to certify where one straight body-foot segment traverses an exact directed portal. |
| | | 18 | | /// </summary> |
| | | 19 | | /// <remarks> |
| | | 20 | | /// Vertical portals return a directed source/target enclosure around the exact crossing. |
| | | 21 | | /// Horizontal portals return the ordered parameters of the exact profile anchors. Both forms |
| | | 22 | | /// certify the body against the authored source/target prism pair. |
| | | 23 | | /// </remarks> |
| | | 24 | | public static bool TryGetNavigationPortalTraversalParameters( |
| | | 25 | | in GridCellPrism sourcePrism, |
| | | 26 | | in GridCellPrism targetPrism, |
| | | 27 | | in GridNavigationPortal portal, |
| | | 28 | | Vector3d footStart, |
| | | 29 | | Vector3d footEnd, |
| | | 30 | | Fixed64 horizontalRadius, |
| | | 31 | | Fixed64 bodyHeight, |
| | | 32 | | out Fixed64 sourceParameter, |
| | | 33 | | out Fixed64 targetParameter) |
| | | 34 | | { |
| | 287 | 35 | | SwiftThrowHelper.ThrowIfArgument( |
| | 287 | 36 | | horizontalRadius < Fixed64.Zero, |
| | 287 | 37 | | nameof(horizontalRadius), |
| | 287 | 38 | | "Horizontal radius must be nonnegative."); |
| | 287 | 39 | | SwiftThrowHelper.ThrowIfArgument( |
| | 287 | 40 | | bodyHeight <= Fixed64.Zero, |
| | 287 | 41 | | nameof(bodyHeight), |
| | 287 | 42 | | "Body height must be positive."); |
| | | 43 | | |
| | 287 | 44 | | if (!TryCreateNavigationPortal( |
| | 287 | 45 | | sourcePrism, |
| | 287 | 46 | | targetPrism, |
| | 287 | 47 | | out GridNavigationPortal expectedPortal) |
| | 287 | 48 | | || !AreSamePortal(portal, expectedPortal) |
| | 287 | 49 | | || !portal.TryResolveProfile( |
| | 287 | 50 | | horizontalRadius, |
| | 287 | 51 | | bodyHeight, |
| | 287 | 52 | | out Vector3d sourceAnchor, |
| | 287 | 53 | | out Vector3d targetAnchor)) |
| | | 54 | | { |
| | 9 | 55 | | sourceParameter = default; |
| | 9 | 56 | | targetParameter = default; |
| | 9 | 57 | | return false; |
| | | 58 | | } |
| | | 59 | | |
| | 278 | 60 | | return TryGetCompiledNavigationPortalTraversalParameters( |
| | 278 | 61 | | sourcePrism, |
| | 278 | 62 | | targetPrism, |
| | 278 | 63 | | portal, |
| | 278 | 64 | | sourceAnchor, |
| | 278 | 65 | | targetAnchor, |
| | 278 | 66 | | footStart, |
| | 278 | 67 | | footEnd, |
| | 278 | 68 | | horizontalRadius, |
| | 278 | 69 | | bodyHeight, |
| | 278 | 70 | | out sourceParameter, |
| | 278 | 71 | | out targetParameter); |
| | | 72 | | } |
| | | 73 | | |
| | | 74 | | internal static bool TryGetCompiledNavigationPortalTraversalParameters( |
| | | 75 | | in GridCellPrism sourcePrism, |
| | | 76 | | in GridCellPrism targetPrism, |
| | | 77 | | in GridNavigationPortal portal, |
| | | 78 | | Vector3d sourceAnchor, |
| | | 79 | | Vector3d targetAnchor, |
| | | 80 | | Vector3d footStart, |
| | | 81 | | Vector3d footEnd, |
| | | 82 | | Fixed64 horizontalRadius, |
| | | 83 | | Fixed64 bodyHeight, |
| | | 84 | | out Fixed64 sourceParameter, |
| | | 85 | | out Fixed64 targetParameter) |
| | | 86 | | { |
| | 278 | 87 | | sourceParameter = default; |
| | 278 | 88 | | targetParameter = default; |
| | 278 | 89 | | if (portal.FaceKind == VoxelContactFaceKind.Vertical) |
| | | 90 | | { |
| | 273 | 91 | | FixedSegment2d path = new( |
| | 273 | 92 | | new Vector2d(footStart.X, footStart.Z), |
| | 273 | 93 | | new Vector2d(footEnd.X, footEnd.Z)); |
| | 273 | 94 | | FixedSegment2d opening = new( |
| | 273 | 95 | | portal.VerticalFaceSegmentStart, |
| | 273 | 96 | | portal.VerticalFaceSegmentEnd); |
| | 273 | 97 | | if (!IsDirectedPortalCrossing(sourcePrism, path, opening) |
| | 273 | 98 | | || !path.TryGetUniqueIntersectionParameterEnclosure( |
| | 273 | 99 | | opening, |
| | 273 | 100 | | out _, |
| | 273 | 101 | | out sourceParameter, |
| | 273 | 102 | | out targetParameter)) |
| | | 103 | | { |
| | 4 | 104 | | sourceParameter = default; |
| | 4 | 105 | | targetParameter = default; |
| | 4 | 106 | | return false; |
| | | 107 | | } |
| | | 108 | | |
| | 269 | 109 | | Vector3d sourcePoint = Vector3d.Lerp(footStart, footEnd, sourceParameter); |
| | 269 | 110 | | Vector3d targetPoint = Vector3d.Lerp(footStart, footEnd, targetParameter); |
| | 269 | 111 | | FixedSegment2d traversalGap = new( |
| | 269 | 112 | | new Vector2d(sourcePoint.X, sourcePoint.Z), |
| | 269 | 113 | | new Vector2d(targetPoint.X, targetPoint.Z)); |
| | 269 | 114 | | path.TryGetCapsuleIntersectionParameterEnclosure( |
| | 269 | 115 | | opening, |
| | 269 | 116 | | horizontalRadius, |
| | 269 | 117 | | out Fixed64 overlapEntry, |
| | 269 | 118 | | out Fixed64 overlapExit); |
| | 269 | 119 | | if (!IsPortalTraversalGapPlanarValid( |
| | 269 | 120 | | sourcePrism, |
| | 269 | 121 | | traversalGap, |
| | 269 | 122 | | horizontalRadius, |
| | 269 | 123 | | portal) |
| | 269 | 124 | | || !IsPortalTraversalGapPlanarValid( |
| | 269 | 125 | | targetPrism, |
| | 269 | 126 | | traversalGap, |
| | 269 | 127 | | horizontalRadius, |
| | 269 | 128 | | portal) |
| | 269 | 129 | | || !IsPortalHeightValidOverInterval( |
| | 269 | 130 | | footStart.Y, |
| | 269 | 131 | | footEnd.Y, |
| | 269 | 132 | | overlapEntry, |
| | 269 | 133 | | overlapExit, |
| | 269 | 134 | | bodyHeight, |
| | 269 | 135 | | portal)) |
| | | 136 | | { |
| | 7 | 137 | | sourceParameter = default; |
| | 7 | 138 | | targetParameter = default; |
| | 7 | 139 | | return false; |
| | | 140 | | } |
| | | 141 | | |
| | 262 | 142 | | return true; |
| | | 143 | | } |
| | | 144 | | |
| | 5 | 145 | | if (!TryGetPointParameter(footStart, footEnd, sourceAnchor, out sourceParameter) |
| | 5 | 146 | | || !TryGetPointParameter(footStart, footEnd, targetAnchor, out targetParameter) |
| | 5 | 147 | | || sourceParameter >= targetParameter) |
| | | 148 | | { |
| | 3 | 149 | | sourceParameter = default; |
| | 3 | 150 | | targetParameter = default; |
| | 3 | 151 | | return false; |
| | | 152 | | } |
| | | 153 | | |
| | 2 | 154 | | return true; |
| | | 155 | | } |
| | | 156 | | |
| | | 157 | | /// <summary> |
| | | 158 | | /// Determines whether a cylindrical body can sweep one straight foot segment through an exact |
| | | 159 | | /// cell prism, optionally approaching an incoming and outgoing vertical portal. |
| | | 160 | | /// </summary> |
| | | 161 | | /// <remarks> |
| | | 162 | | /// The horizontal capsule is compared exactly with every blocked wall span. Selected portal |
| | | 163 | | /// openings retain their own vertical authority and apply only while the sweep overlaps that |
| | | 164 | | /// opening. Each selected portal must cover its complete possible overlap; a segment that would |
| | | 165 | | /// need to switch height authority between two same-wall openings is rejected. A non-default |
| | | 166 | | /// endpoint allowance clips one exact directed footprint-edge crossing inside GridForge before |
| | | 167 | | /// validating the retained in-prism segment. The method retains no state and allocates nothing. |
| | | 168 | | /// </remarks> |
| | | 169 | | public static bool IsNavigationBodySegmentValid( |
| | | 170 | | in GridCellPrism prism, |
| | | 171 | | Vector3d footStart, |
| | | 172 | | Vector3d footEnd, |
| | | 173 | | Fixed64 horizontalRadius, |
| | | 174 | | Fixed64 bodyHeight, |
| | | 175 | | in GridNavigationPortal incomingPortal, |
| | | 176 | | in GridNavigationPortal outgoingPortal, |
| | | 177 | | GridNavigationBodySegmentEndpointAllowance endpointAllowance) |
| | | 178 | | { |
| | 4446 | 179 | | SwiftThrowHelper.ThrowIfArgument( |
| | 4446 | 180 | | horizontalRadius < Fixed64.Zero, |
| | 4446 | 181 | | nameof(horizontalRadius), |
| | 4446 | 182 | | "Horizontal radius must be nonnegative."); |
| | 4446 | 183 | | SwiftThrowHelper.ThrowIfArgument( |
| | 4446 | 184 | | bodyHeight <= Fixed64.Zero, |
| | 4446 | 185 | | nameof(bodyHeight), |
| | 4446 | 186 | | "Body height must be positive."); |
| | | 187 | | |
| | 4446 | 188 | | if (endpointAllowance != GridNavigationBodySegmentEndpointAllowance.None |
| | 4446 | 189 | | && endpointAllowance != GridNavigationBodySegmentEndpointAllowance.StartFootprintEdge |
| | 4446 | 190 | | && endpointAllowance != GridNavigationBodySegmentEndpointAllowance.EndFootprintEdge) |
| | | 191 | | { |
| | 1 | 192 | | throw new System.ArgumentOutOfRangeException(nameof(endpointAllowance)); |
| | | 193 | | } |
| | | 194 | | |
| | 4445 | 195 | | if (!IsNavigationPrismValid(prism)) |
| | 1 | 196 | | return false; |
| | | 197 | | |
| | 4444 | 198 | | int allowedEdgeIndex = -1; |
| | 4444 | 199 | | if (endpointAllowance == GridNavigationBodySegmentEndpointAllowance.StartFootprintEdge) |
| | | 200 | | { |
| | 7 | 201 | | if (incomingPortal.IsValid |
| | 7 | 202 | | || !TryClipNavigationBodySegmentEndpoint( |
| | 7 | 203 | | prism, |
| | 7 | 204 | | footStart, |
| | 7 | 205 | | footEnd, |
| | 7 | 206 | | bodyHeight, |
| | 7 | 207 | | clipStart: true, |
| | 7 | 208 | | out footStart, |
| | 7 | 209 | | out allowedEdgeIndex)) |
| | | 210 | | { |
| | 5 | 211 | | return false; |
| | | 212 | | } |
| | | 213 | | } |
| | 4437 | 214 | | else if (endpointAllowance == GridNavigationBodySegmentEndpointAllowance.EndFootprintEdge) |
| | | 215 | | { |
| | 3 | 216 | | if (outgoingPortal.IsValid |
| | 3 | 217 | | || !TryClipNavigationBodySegmentEndpoint( |
| | 3 | 218 | | prism, |
| | 3 | 219 | | footStart, |
| | 3 | 220 | | footEnd, |
| | 3 | 221 | | bodyHeight, |
| | 3 | 222 | | clipStart: false, |
| | 3 | 223 | | out footEnd, |
| | 3 | 224 | | out allowedEdgeIndex)) |
| | | 225 | | { |
| | 2 | 226 | | return false; |
| | | 227 | | } |
| | | 228 | | } |
| | | 229 | | |
| | 4437 | 230 | | return IsNavigationBodySegmentValidCore( |
| | 4437 | 231 | | prism, |
| | 4437 | 232 | | footStart, |
| | 4437 | 233 | | footEnd, |
| | 4437 | 234 | | horizontalRadius, |
| | 4437 | 235 | | bodyHeight, |
| | 4437 | 236 | | incomingPortal, |
| | 4437 | 237 | | outgoingPortal, |
| | 4437 | 238 | | allowedEdgeIndex); |
| | | 239 | | } |
| | | 240 | | |
| | | 241 | | private static bool IsNavigationBodySegmentValidCore( |
| | | 242 | | in GridCellPrism prism, |
| | | 243 | | Vector3d footStart, |
| | | 244 | | Vector3d footEnd, |
| | | 245 | | Fixed64 horizontalRadius, |
| | | 246 | | Fixed64 bodyHeight, |
| | | 247 | | in GridNavigationPortal incomingPortal, |
| | | 248 | | in GridNavigationPortal outgoingPortal, |
| | | 249 | | int allowedEdgeIndex) |
| | | 250 | | { |
| | 4437 | 251 | | if (!prism.Contains(footStart) |
| | 4437 | 252 | | || !prism.Contains(footEnd) |
| | 4437 | 253 | | || !Fixed64.TryAdd(footStart.Y, bodyHeight, out Fixed64 startTop) |
| | 4437 | 254 | | || !Fixed64.TryAdd(footEnd.Y, bodyHeight, out Fixed64 endTop) |
| | 4437 | 255 | | || startTop > prism.VerticalMax |
| | 4437 | 256 | | || endTop > prism.VerticalMax) |
| | | 257 | | { |
| | 9 | 258 | | return false; |
| | | 259 | | } |
| | | 260 | | |
| | 4428 | 261 | | FixedSegment2d path = new( |
| | 4428 | 262 | | new Vector2d(footStart.X, footStart.Z), |
| | 4428 | 263 | | new Vector2d(footEnd.X, footEnd.Z)); |
| | 44272 | 264 | | for (int edgeIndex = 0; edgeIndex < prism.FootprintVertexCount; edgeIndex++) |
| | | 265 | | { |
| | 17739 | 266 | | if (edgeIndex == allowedEdgeIndex) |
| | | 267 | | continue; |
| | 17736 | 268 | | Vector2d edgeStart = prism.GetFootprintVertex(edgeIndex); |
| | 17736 | 269 | | Vector2d edgeEnd = prism.GetFootprintVertex( |
| | 17736 | 270 | | (edgeIndex + 1) % prism.FootprintVertexCount); |
| | 17736 | 271 | | FixedSegment2d edge = new(edgeStart, edgeEnd); |
| | 17736 | 272 | | if (path.IsDistanceAtLeast(edge, horizontalRadius)) |
| | | 273 | | continue; |
| | | 274 | | |
| | 5353 | 275 | | bool hasFirst = TryGetActiveOpening( |
| | 5353 | 276 | | edge, |
| | 5353 | 277 | | path, |
| | 5353 | 278 | | footStart.Y, |
| | 5353 | 279 | | footEnd.Y, |
| | 5353 | 280 | | horizontalRadius, |
| | 5353 | 281 | | bodyHeight, |
| | 5353 | 282 | | incomingPortal, |
| | 5353 | 283 | | edgeStart, |
| | 5353 | 284 | | out Vector2d firstStart, |
| | 5353 | 285 | | out Vector2d firstEnd); |
| | 5353 | 286 | | bool hasSecond = TryGetActiveOpening( |
| | 5353 | 287 | | edge, |
| | 5353 | 288 | | path, |
| | 5353 | 289 | | footStart.Y, |
| | 5353 | 290 | | footEnd.Y, |
| | 5353 | 291 | | horizontalRadius, |
| | 5353 | 292 | | bodyHeight, |
| | 5353 | 293 | | outgoingPortal, |
| | 5353 | 294 | | edgeStart, |
| | 5353 | 295 | | out Vector2d secondStart, |
| | 5353 | 296 | | out Vector2d secondEnd); |
| | 5353 | 297 | | if (!hasFirst && !hasSecond) |
| | 21 | 298 | | return false; |
| | 5332 | 299 | | if (!hasFirst) |
| | | 300 | | { |
| | 2279 | 301 | | firstStart = secondStart; |
| | 2279 | 302 | | firstEnd = secondEnd; |
| | 2279 | 303 | | hasSecond = false; |
| | | 304 | | } |
| | 3053 | 305 | | else if (hasSecond |
| | 3053 | 306 | | && CompareDistanceFrom(edgeStart, secondStart, firstStart) < 0) |
| | | 307 | | { |
| | 2 | 308 | | Swap(ref firstStart, ref secondStart); |
| | 2 | 309 | | Swap(ref firstEnd, ref secondEnd); |
| | | 310 | | } |
| | | 311 | | |
| | 5332 | 312 | | if (!path.IsDistanceAtLeast( |
| | 5332 | 313 | | new FixedSegment2d(edgeStart, firstStart), |
| | 5332 | 314 | | horizontalRadius)) |
| | | 315 | | { |
| | 4 | 316 | | return false; |
| | | 317 | | } |
| | | 318 | | |
| | 5328 | 319 | | if (!hasSecond |
| | 5328 | 320 | | || CompareDistanceFrom(edgeStart, secondStart, firstEnd) <= 0) |
| | | 321 | | { |
| | 5326 | 322 | | if (hasSecond |
| | 5326 | 323 | | && CompareDistanceFrom(edgeStart, secondEnd, firstEnd) > 0) |
| | | 324 | | { |
| | 2 | 325 | | firstEnd = secondEnd; |
| | | 326 | | } |
| | | 327 | | |
| | 5326 | 328 | | if (!path.IsDistanceAtLeast( |
| | 5326 | 329 | | new FixedSegment2d(firstEnd, edgeEnd), |
| | 5326 | 330 | | horizontalRadius)) |
| | | 331 | | { |
| | 4 | 332 | | return false; |
| | | 333 | | } |
| | | 334 | | } |
| | | 335 | | else |
| | | 336 | | { |
| | 2 | 337 | | return false; |
| | | 338 | | } |
| | | 339 | | } |
| | | 340 | | |
| | 4397 | 341 | | return true; |
| | | 342 | | } |
| | | 343 | | |
| | | 344 | | private static bool TryClipNavigationBodySegmentEndpoint( |
| | | 345 | | in GridCellPrism prism, |
| | | 346 | | Vector3d footStart, |
| | | 347 | | Vector3d footEnd, |
| | | 348 | | Fixed64 bodyHeight, |
| | | 349 | | bool clipStart, |
| | | 350 | | out Vector3d clippedEndpoint, |
| | | 351 | | out int allowedEdgeIndex) |
| | | 352 | | { |
| | 8 | 353 | | clippedEndpoint = default; |
| | 8 | 354 | | allowedEdgeIndex = -1; |
| | 8 | 355 | | FixedSegment2d path = new( |
| | 8 | 356 | | new Vector2d(footStart.X, footStart.Z), |
| | 8 | 357 | | new Vector2d(footEnd.X, footEnd.Z)); |
| | 8 | 358 | | if (path.Start == path.End) |
| | 1 | 359 | | return false; |
| | | 360 | | |
| | 7 | 361 | | Vector2d center = new(prism.Center.X, prism.Center.Z); |
| | 7 | 362 | | Fixed64 lowerParameter = default; |
| | 7 | 363 | | Fixed64 upperParameter = default; |
| | 60 | 364 | | for (int edgeIndex = 0; edgeIndex < prism.FootprintVertexCount; edgeIndex++) |
| | | 365 | | { |
| | 25 | 366 | | Vector2d edgeStart = prism.GetFootprintVertex(edgeIndex); |
| | 25 | 367 | | Vector2d edgeEnd = prism.GetFootprintVertex( |
| | 25 | 368 | | (edgeIndex + 1) % prism.FootprintVertexCount); |
| | 25 | 369 | | FixedSegment2d edge = new(edgeStart, edgeEnd); |
| | 25 | 370 | | int centerSide = Vector2d.OrientationSign(edgeStart, edgeEnd, center); |
| | 25 | 371 | | int startSide = Vector2d.OrientationSign(edgeStart, edgeEnd, path.Start); |
| | 25 | 372 | | int endSide = Vector2d.OrientationSign(edgeStart, edgeEnd, path.End); |
| | 25 | 373 | | bool directed = clipStart |
| | 25 | 374 | | ? endSide == centerSide && startSide != centerSide |
| | 25 | 375 | | : startSide == centerSide && endSide != centerSide; |
| | 25 | 376 | | if (!directed) |
| | | 377 | | continue; |
| | 6 | 378 | | if (!path.TryGetUniqueIntersectionParameterEnclosure( |
| | 6 | 379 | | edge, |
| | 6 | 380 | | out _, |
| | 6 | 381 | | out Fixed64 candidateLower, |
| | 6 | 382 | | out Fixed64 candidateUpper)) |
| | | 383 | | { |
| | | 384 | | continue; |
| | | 385 | | } |
| | 5 | 386 | | if (new FixedSegment2d(edgeStart, edgeStart).TryGetUniqueIntersection(path, out _) |
| | 5 | 387 | | || new FixedSegment2d(edgeEnd, edgeEnd).TryGetUniqueIntersection(path, out _)) |
| | | 388 | | { |
| | 2 | 389 | | return false; |
| | | 390 | | } |
| | 3 | 391 | | allowedEdgeIndex = edgeIndex; |
| | 3 | 392 | | lowerParameter = candidateLower; |
| | 3 | 393 | | upperParameter = candidateUpper; |
| | | 394 | | } |
| | | 395 | | |
| | 5 | 396 | | if (allowedEdgeIndex < 0 |
| | 5 | 397 | | || !IsBodyHeightValidOverInterval( |
| | 5 | 398 | | footStart.Y, |
| | 5 | 399 | | footEnd.Y, |
| | 5 | 400 | | lowerParameter, |
| | 5 | 401 | | upperParameter, |
| | 5 | 402 | | bodyHeight, |
| | 5 | 403 | | prism.VerticalMin, |
| | 5 | 404 | | prism.VerticalMax)) |
| | | 405 | | { |
| | 2 | 406 | | return false; |
| | | 407 | | } |
| | | 408 | | |
| | 3 | 409 | | Fixed64 containedParameter = clipStart ? upperParameter : lowerParameter; |
| | 3 | 410 | | clippedEndpoint = Vector3d.Lerp(footStart, footEnd, containedParameter); |
| | 3 | 411 | | return prism.Contains(clippedEndpoint); |
| | | 412 | | } |
| | | 413 | | |
| | | 414 | | private static bool TryGetActiveOpening( |
| | | 415 | | FixedSegment2d edge, |
| | | 416 | | FixedSegment2d path, |
| | | 417 | | Fixed64 footStartY, |
| | | 418 | | Fixed64 footEndY, |
| | | 419 | | Fixed64 horizontalRadius, |
| | | 420 | | Fixed64 bodyHeight, |
| | | 421 | | in GridNavigationPortal portal, |
| | | 422 | | Vector2d edgeStart, |
| | | 423 | | out Vector2d openingStart, |
| | | 424 | | out Vector2d openingEnd) |
| | | 425 | | { |
| | 10706 | 426 | | openingStart = default; |
| | 10706 | 427 | | openingEnd = default; |
| | 10706 | 428 | | if (!portal.IsValid |
| | 10706 | 429 | | || portal.FaceKind != VoxelContactFaceKind.Vertical |
| | 10706 | 430 | | || horizontalRadius > portal.MaximumHorizontalRadius |
| | 10706 | 431 | | || bodyHeight > portal.MaximumBodyHeight |
| | 10706 | 432 | | || !IsPortalCertifiedOnEdge(edge, portal)) |
| | | 433 | | { |
| | 5359 | 434 | | return false; |
| | | 435 | | } |
| | | 436 | | |
| | 5347 | 437 | | openingStart = portal.VerticalFaceSegmentStart; |
| | 5347 | 438 | | openingEnd = portal.VerticalFaceSegmentEnd; |
| | 5347 | 439 | | if (CompareDistanceFrom(edgeStart, openingEnd, openingStart) < 0) |
| | 2028 | 440 | | Swap(ref openingStart, ref openingEnd); |
| | | 441 | | |
| | 5347 | 442 | | FixedSegment2d opening = new(openingStart, openingEnd); |
| | 5347 | 443 | | if (!path.TryGetCapsuleIntersectionParameterEnclosure( |
| | 5347 | 444 | | opening, |
| | 5347 | 445 | | horizontalRadius, |
| | 5347 | 446 | | out Fixed64 entry, |
| | 5347 | 447 | | out Fixed64 exit)) |
| | | 448 | | { |
| | 2 | 449 | | return false; |
| | | 450 | | } |
| | | 451 | | |
| | 5345 | 452 | | return IsPortalHeightValidOverInterval( |
| | 5345 | 453 | | footStartY, |
| | 5345 | 454 | | footEndY, |
| | 5345 | 455 | | entry, |
| | 5345 | 456 | | exit, |
| | 5345 | 457 | | bodyHeight, |
| | 5345 | 458 | | portal); |
| | | 459 | | } |
| | | 460 | | |
| | | 461 | | private static bool IsDirectedPortalCrossing( |
| | | 462 | | in GridCellPrism sourcePrism, |
| | | 463 | | FixedSegment2d path, |
| | | 464 | | FixedSegment2d opening) |
| | | 465 | | { |
| | 273 | 466 | | if (path.Start == path.End) |
| | 1 | 467 | | return false; |
| | | 468 | | |
| | 272 | 469 | | Vector2d sourceCenter = new(sourcePrism.Center.X, sourcePrism.Center.Z); |
| | 272 | 470 | | int sourceSide = Vector2d.OrientationSign(opening.Start, opening.End, sourceCenter); |
| | 272 | 471 | | int startSide = Vector2d.OrientationSign(opening.Start, opening.End, path.Start); |
| | 272 | 472 | | int endSide = Vector2d.OrientationSign(opening.Start, opening.End, path.End); |
| | 272 | 473 | | return (startSide == 0 || startSide == sourceSide) |
| | 272 | 474 | | && (endSide == 0 || endSide == -sourceSide) |
| | 272 | 475 | | && (startSide != 0 || endSide != 0); |
| | | 476 | | } |
| | | 477 | | |
| | | 478 | | private static bool IsPortalTraversalGapPlanarValid( |
| | | 479 | | in GridCellPrism prism, |
| | | 480 | | FixedSegment2d traversalGap, |
| | | 481 | | Fixed64 horizontalRadius, |
| | | 482 | | in GridNavigationPortal portal) |
| | | 483 | | { |
| | 5324 | 484 | | for (int edgeIndex = 0; edgeIndex < prism.FootprintVertexCount; edgeIndex++) |
| | | 485 | | { |
| | 2131 | 486 | | Vector2d edgeStart = prism.GetFootprintVertex(edgeIndex); |
| | 2131 | 487 | | Vector2d edgeEnd = prism.GetFootprintVertex( |
| | 2131 | 488 | | (edgeIndex + 1) % prism.FootprintVertexCount); |
| | 2131 | 489 | | FixedSegment2d edge = new(edgeStart, edgeEnd); |
| | 2131 | 490 | | if (!IsPortalCertifiedOnEdge(edge, portal)) |
| | | 491 | | { |
| | 1598 | 492 | | if (!traversalGap.IsDistanceAtLeast( |
| | 1598 | 493 | | edge, |
| | 1598 | 494 | | horizontalRadius)) |
| | | 495 | | { |
| | 2 | 496 | | return false; |
| | | 497 | | } |
| | | 498 | | continue; |
| | | 499 | | } |
| | | 500 | | |
| | 533 | 501 | | Vector2d openingStart = portal.VerticalFaceSegmentStart; |
| | 533 | 502 | | Vector2d openingEnd = portal.VerticalFaceSegmentEnd; |
| | 533 | 503 | | if (CompareDistanceFrom(edgeStart, openingEnd, openingStart) < 0) |
| | 265 | 504 | | Swap(ref openingStart, ref openingEnd); |
| | 533 | 505 | | if (!traversalGap.IsDistanceAtLeast( |
| | 533 | 506 | | new FixedSegment2d(edgeStart, openingStart), |
| | 533 | 507 | | horizontalRadius) |
| | 533 | 508 | | || !traversalGap.IsDistanceAtLeast( |
| | 533 | 509 | | new FixedSegment2d(openingEnd, edgeEnd), |
| | 533 | 510 | | horizontalRadius)) |
| | | 511 | | { |
| | 2 | 512 | | return false; |
| | | 513 | | } |
| | | 514 | | } |
| | | 515 | | |
| | 531 | 516 | | return true; |
| | | 517 | | } |
| | | 518 | | |
| | | 519 | | private static bool IsPortalHeightValidOverInterval( |
| | | 520 | | Fixed64 footStartY, |
| | | 521 | | Fixed64 footEndY, |
| | | 522 | | Fixed64 entryParameter, |
| | | 523 | | Fixed64 exitParameter, |
| | | 524 | | Fixed64 bodyHeight, |
| | | 525 | | in GridNavigationPortal portal) |
| | | 526 | | { |
| | 5610 | 527 | | Fixed64 portalTop = portal.CanonicalFacePoint.Y + portal.MaximumBodyHeight; |
| | 5610 | 528 | | return IsBodyHeightValidOverInterval( |
| | 5610 | 529 | | footStartY, |
| | 5610 | 530 | | footEndY, |
| | 5610 | 531 | | entryParameter, |
| | 5610 | 532 | | exitParameter, |
| | 5610 | 533 | | bodyHeight, |
| | 5610 | 534 | | portal.CanonicalFacePoint.Y, |
| | 5610 | 535 | | portalTop); |
| | | 536 | | } |
| | | 537 | | |
| | | 538 | | private static bool IsBodyHeightValidOverInterval( |
| | | 539 | | Fixed64 footStartY, |
| | | 540 | | Fixed64 footEndY, |
| | | 541 | | Fixed64 entryParameter, |
| | | 542 | | Fixed64 exitParameter, |
| | | 543 | | Fixed64 bodyHeight, |
| | | 544 | | Fixed64 verticalMin, |
| | | 545 | | Fixed64 verticalMax) |
| | | 546 | | { |
| | 5613 | 547 | | GetConservativeLerpBounds( |
| | 5613 | 548 | | footStartY, |
| | 5613 | 549 | | footEndY, |
| | 5613 | 550 | | entryParameter, |
| | 5613 | 551 | | out Fixed64 lowerStartY, |
| | 5613 | 552 | | out Fixed64 upperStartY); |
| | 5613 | 553 | | GetConservativeLerpBounds( |
| | 5613 | 554 | | footStartY, |
| | 5613 | 555 | | footEndY, |
| | 5613 | 556 | | exitParameter, |
| | 5613 | 557 | | out Fixed64 lowerEndY, |
| | 5613 | 558 | | out Fixed64 upperEndY); |
| | 5613 | 559 | | Fixed64 minimumFootY = FixedMath.Min(lowerStartY, lowerEndY); |
| | 5613 | 560 | | Fixed64 maximumFootY = FixedMath.Max(upperStartY, upperEndY); |
| | 5613 | 561 | | return Fixed64.TryAdd(maximumFootY, bodyHeight, out Fixed64 maximumTop) |
| | 5613 | 562 | | && minimumFootY >= verticalMin |
| | 5613 | 563 | | && maximumTop <= verticalMax; |
| | | 564 | | } |
| | | 565 | | |
| | | 566 | | private static void GetConservativeLerpBounds( |
| | | 567 | | Fixed64 start, |
| | | 568 | | Fixed64 end, |
| | | 569 | | Fixed64 parameter, |
| | | 570 | | out Fixed64 lower, |
| | | 571 | | out Fixed64 upper) |
| | | 572 | | { |
| | 11226 | 573 | | lower = FixedMath.Lerp(start, end, parameter); |
| | 11226 | 574 | | upper = lower; |
| | 11226 | 575 | | if (start == end || parameter == Fixed64.Zero || parameter == Fixed64.One) |
| | 11218 | 576 | | return; |
| | 8 | 577 | | if (lower > Fixed64.MinValue) |
| | 7 | 578 | | lower = Fixed64.FromRaw(lower.m_rawValue - 1L); |
| | 8 | 579 | | if (upper < Fixed64.MaxValue) |
| | 7 | 580 | | upper = Fixed64.FromRaw(upper.m_rawValue + 1L); |
| | 8 | 581 | | } |
| | | 582 | | |
| | | 583 | | private static bool TryGetPointParameter( |
| | | 584 | | Vector3d segmentStart, |
| | | 585 | | Vector3d segmentEnd, |
| | | 586 | | Vector3d point, |
| | | 587 | | out Fixed64 parameter) |
| | | 588 | | { |
| | 9 | 589 | | parameter = default; |
| | 9 | 590 | | FixedSegment segment = new(segmentStart, segmentEnd); |
| | 9 | 591 | | if (!segment.Contains(point) || segmentStart.Y == segmentEnd.Y) |
| | 2 | 592 | | return false; |
| | | 593 | | |
| | 7 | 594 | | FixedSegment2d vertical = new( |
| | 7 | 595 | | new Vector2d(segmentStart.Y, Fixed64.Zero), |
| | 7 | 596 | | new Vector2d(segmentEnd.Y, Fixed64.Zero)); |
| | 7 | 597 | | FixedSegment2d verticalPoint = new( |
| | 7 | 598 | | new Vector2d(point.Y, Fixed64.Zero), |
| | 7 | 599 | | new Vector2d(point.Y, Fixed64.Zero)); |
| | 7 | 600 | | return vertical.TryGetUniqueIntersection(verticalPoint, out parameter); |
| | | 601 | | } |
| | | 602 | | |
| | | 603 | | private static bool AreSamePortal( |
| | | 604 | | in GridNavigationPortal first, |
| | | 605 | | in GridNavigationPortal second) |
| | | 606 | | { |
| | 286 | 607 | | return first.FaceKind == second.FaceKind |
| | 286 | 608 | | && first.SourceToTarget == second.SourceToTarget |
| | 286 | 609 | | && first.CanonicalFacePoint == second.CanonicalFacePoint |
| | 286 | 610 | | && first.MaximumHorizontalRadius == second.MaximumHorizontalRadius |
| | 286 | 611 | | && first.MaximumBodyHeight == second.MaximumBodyHeight |
| | 286 | 612 | | && first.VerticalFaceSegmentStart == second.VerticalFaceSegmentStart |
| | 286 | 613 | | && first.VerticalFaceSegmentEnd == second.VerticalFaceSegmentEnd; |
| | | 614 | | } |
| | | 615 | | |
| | | 616 | | private static int CompareDistanceFrom( |
| | | 617 | | Vector2d origin, |
| | | 618 | | Vector2d first, |
| | | 619 | | Vector2d second) |
| | | 620 | | { |
| | 5893 | 621 | | return Vector2d.CompareDistanceSquared(origin, first, origin, second); |
| | | 622 | | } |
| | | 623 | | |
| | | 624 | | private static void Swap(ref Vector2d first, ref Vector2d second) |
| | | 625 | | { |
| | 2297 | 626 | | Vector2d value = first; |
| | 2297 | 627 | | first = second; |
| | 2297 | 628 | | second = value; |
| | 2297 | 629 | | } |
| | | 630 | | |
| | | 631 | | internal static bool HasPositiveNavigationBodyPrismOverlap( |
| | | 632 | | in GridCellPrism prism, |
| | | 633 | | Vector3d footStart, |
| | | 634 | | Vector3d footEnd, |
| | | 635 | | Fixed64 horizontalRadius, |
| | | 636 | | Fixed64 bodyHeight) |
| | | 637 | | { |
| | 3231 | 638 | | Fixed64 prismHalfThickness = prism.VerticalMax - prism.Center.Y; |
| | | 639 | | |
| | 3231 | 640 | | Span<Vector2d> offsets = stackalloc Vector2d[6]; |
| | 3231 | 641 | | Vector2d planarOrigin = new(prism.Center.X, prism.Center.Z); |
| | 32890 | 642 | | for (int i = 0; i < prism.FootprintVertexCount; i++) |
| | 13214 | 643 | | offsets[i] = prism.GetFootprintVertex(i) - planarOrigin; |
| | | 644 | | |
| | 3231 | 645 | | return FixedConvexPrismRelations.IntersectsSweptUprightCylinderStrict( |
| | 3231 | 646 | | footStart, |
| | 3231 | 647 | | footEnd, |
| | 3231 | 648 | | horizontalRadius, |
| | 3231 | 649 | | bodyHeight, |
| | 3231 | 650 | | prism.Center, |
| | 3231 | 651 | | Fixed64.Zero, |
| | 3231 | 652 | | offsets[..prism.FootprintVertexCount], |
| | 3231 | 653 | | prismHalfThickness); |
| | | 654 | | } |
| | | 655 | | |
| | | 656 | | internal static bool TryGetPlanarSegmentInterval( |
| | | 657 | | in GridCellPrism prism, |
| | | 658 | | Vector2d start, |
| | | 659 | | Vector2d end, |
| | | 660 | | out Fixed64 overlapEnter, |
| | | 661 | | out Fixed64 overlapExit) |
| | | 662 | | { |
| | 471 | 663 | | Vector2d origin = new(prism.Center.X, prism.Center.Z); |
| | 471 | 664 | | Span<Vector2d> vertices = stackalloc Vector2d[6]; |
| | 471 | 665 | | Span<Vector2d> offsets = stackalloc Vector2d[6]; |
| | 471 | 666 | | prism.CopyFootprintTo(vertices); |
| | 5414 | 667 | | for (int i = 0; i < prism.FootprintVertexCount; i++) |
| | 2236 | 668 | | offsets[i] = vertices[i] - origin; |
| | 471 | 669 | | ReadOnlySpan<Vector2d> footprint = offsets[..prism.FootprintVertexCount]; |
| | | 670 | | |
| | 471 | 671 | | bool startContained = FixedConvex2dRelations.ContainsPoint(start, origin, footprint); |
| | 471 | 672 | | if (start == end) |
| | | 673 | | { |
| | 60 | 674 | | overlapEnter = Fixed64.Zero; |
| | 60 | 675 | | overlapExit = Fixed64.One; |
| | 60 | 676 | | return startContained; |
| | | 677 | | } |
| | | 678 | | |
| | 411 | 679 | | Span<Fixed64> parameters = stackalloc Fixed64[16]; |
| | 411 | 680 | | int count = 0; |
| | 411 | 681 | | if (startContained) |
| | 89 | 682 | | parameters[count++] = Fixed64.Zero; |
| | 411 | 683 | | if (FixedConvex2dRelations.ContainsPoint(end, origin, footprint)) |
| | 92 | 684 | | parameters[count++] = Fixed64.One; |
| | | 685 | | |
| | 411 | 686 | | FixedSegment2d path = new(start, end); |
| | 4790 | 687 | | for (int i = 0; i < prism.FootprintVertexCount; i++) |
| | | 688 | | { |
| | 1984 | 689 | | FixedSegment2d edge = new( |
| | 1984 | 690 | | vertices[i], |
| | 1984 | 691 | | vertices[(i + 1) % prism.FootprintVertexCount]); |
| | 1984 | 692 | | if (path.TryGetUniqueIntersection(edge, out Fixed64 parameter)) |
| | 449 | 693 | | AddNavigationBodyParameter(parameters, ref count, parameter); |
| | 1984 | 694 | | if (Vector2d.OrientationSign(start, end, edge.Start) == 0 |
| | 1984 | 695 | | && Vector2d.OrientationSign(start, end, edge.End) == 0) |
| | | 696 | | { |
| | 45 | 697 | | AddNavigationBodyParameter(parameters, ref count, GetNavigationBodyParameter(path, edge.Start)); |
| | 45 | 698 | | AddNavigationBodyParameter(parameters, ref count, GetNavigationBodyParameter(path, edge.End)); |
| | | 699 | | } |
| | | 700 | | } |
| | | 701 | | |
| | 411 | 702 | | if (count == 0) |
| | | 703 | | { |
| | 156 | 704 | | overlapEnter = default; |
| | 156 | 705 | | overlapExit = default; |
| | 156 | 706 | | return false; |
| | | 707 | | } |
| | | 708 | | |
| | 255 | 709 | | overlapEnter = parameters[0]; |
| | 255 | 710 | | overlapExit = parameters[0]; |
| | 936 | 711 | | for (int i = 1; i < count; i++) |
| | | 712 | | { |
| | 213 | 713 | | overlapEnter = FixedMath.Min(overlapEnter, parameters[i]); |
| | 213 | 714 | | overlapExit = FixedMath.Max(overlapExit, parameters[i]); |
| | | 715 | | } |
| | 255 | 716 | | return true; |
| | | 717 | | } |
| | | 718 | | |
| | | 719 | | private static void AddNavigationBodyParameter( |
| | | 720 | | Span<Fixed64> parameters, |
| | | 721 | | ref int count, |
| | | 722 | | Fixed64 parameter) |
| | | 723 | | { |
| | 539 | 724 | | if ((ulong)parameter.m_rawValue > (ulong)Fixed64.One.m_rawValue) |
| | 6 | 725 | | return; |
| | 1530 | 726 | | for (int i = 0; i < count; i++) |
| | | 727 | | { |
| | 478 | 728 | | if (parameters[i] == parameter) |
| | 246 | 729 | | return; |
| | | 730 | | } |
| | 287 | 731 | | parameters[count++] = parameter; |
| | 287 | 732 | | } |
| | | 733 | | |
| | | 734 | | private static Fixed64 GetNavigationBodyParameter(FixedSegment2d path, Vector2d point) |
| | | 735 | | { |
| | 90 | 736 | | Vector2d delta = path.Delta; |
| | 90 | 737 | | return FixedMath.Abs(delta.X) >= FixedMath.Abs(delta.Y) |
| | 90 | 738 | | ? (point.X - path.Start.X) / delta.X |
| | 90 | 739 | | : (point.Y - path.Start.Y) / delta.Y; |
| | | 740 | | } |
| | | 741 | | |
| | | 742 | | } |