diff --git a/Sources/UntoldEngine/Physics/PhysicsQuery.swift b/Sources/UntoldEngine/Physics/PhysicsQuery.swift index f0d8954a..093e9c55 100644 --- a/Sources/UntoldEngine/Physics/PhysicsQuery.swift +++ b/Sources/UntoldEngine/Physics/PhysicsQuery.swift @@ -73,11 +73,19 @@ public enum PhysicsQuery { // farther candidates even though its reported hit distance is 0 — so // inside-origin candidates are always examined and the scan never // breaks out of the sorted order early. + // + // The query itself is asked for the unbounded distance, not the + // caller's maxDistance: its own internal cull compares maxDistance + // against that same entry-or-exit value, so an inside-origin box + // whose EXIT point lies beyond a tight maxDistance would otherwise be + // dropped before the narrow phase ever sees it — even though its true + // hit distance (0, at the ray origin) is well within range. The real + // cap is enforced below, against the recomputed entry distance. var best: PhysicsRayHit? for (entity, sortedDistance) in OctreeSystem.shared.query( rayOrigin: ray.origin, rayDirection: direction, - maxDistance: maxDistance + maxDistance: fallbackUnboundedDistance ) { guard passesFilter(entity, filter), let bounds = OctreeSystem.shared.getBounds(for: entity) diff --git a/Tests/UntoldEngineTests/PhysicsQueryTests.swift b/Tests/UntoldEngineTests/PhysicsQueryTests.swift index d67fc946..8f8599d1 100644 --- a/Tests/UntoldEngineTests/PhysicsQueryTests.swift +++ b/Tests/UntoldEngineTests/PhysicsQueryTests.swift @@ -189,6 +189,29 @@ final class PhysicsQueryTests: XCTestCase { XCTAssertEqual(hit.normal, -direction) } + func testInsideBoxHitSurvivesTightMaxDistance() throws { + // Regression: the broad-phase cull inside OctreeSystem.query compares + // maxDistance against the sorted distance, which for an inside-origin + // box is its far EXIT distance, not the true hit distance (0). Passing + // the caller's maxDistance straight through to that query would drop + // this box before the narrow phase ever saw it, even though the real + // hit (distance 0, at the ray origin) is well inside a tight budget. + let bigBox = makeObstacle( + center: simd_float3(0, 0, -50), + halfExtents: simd_float3(50, 50, 100) + ) + + let direction = simd_float3(0, 0, -1) + let hit = try XCTUnwrap(PhysicsQuery.raycast( + PhysicsRay(origin: .zero, direction: direction, maxDistance: 5.0) + )) + + XCTAssertEqual(hit.entity, bigBox) + XCTAssertEqual(hit.distance, 0.0) + XCTAssertEqual(hit.position, .zero) + XCTAssertEqual(hit.normal, -direction) + } + func testFallbackMissReturnsNil() { _ = makeObstacle(center: simd_float3(0.0, 10.0, -5.0))