Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ import QtTest
++ import QtQuick3D
++ import QtQuick3D.Helpers
++ import QtQuick3D.Physics
++ import QtQuick
++ import QtQuick3D.Physics.TestUtils
++
++ Item {
++ width: 640
++ height: 480
++ visible: true
++
++ PhysicsWorld {
++ id: caster
++ gravity: Qt.vector3d(0, -9.81, 0)
++ running: true
++ forceDebugDraw: true
++ typicalLength: 1
++ typicalSpeed: 10
++ minimumTimestep: 15
++ maximumTimestep: 15
++ scene: viewport.scene
++ }
++
++ // Helper shapes initialized for query parameters
++ BoxShape {
++ id: queryBoxShape
++ extents: Qt.vector3d(0.5, 0.5, 0.5)
++ position: Qt.vector3d(0, 5, 0)
++ }
++
++ SphereShape {
++ id: querySphereShape
++ diameter: 1.0
++ position: Qt.vector3d(0, 5, 0)
++ }
++
++ BoxShape {
++ id: queryOverlapBoxShape
++ extents: Qt.vector3d(1, 1, 1)
++ position: Qt.vector3d(0, -0.5, 0)
++ }
++
++ SphereShape {
++ id: queryOverlapSphereShape
++ diameter: 20.0
++ position: Qt.vector3d(0, -0.5, 0)
++ }
++
++ CapsuleShape {
++ id: queryCapsuleShape
++ diameter: 1.0
++ height: 1.0
++ position: Qt.vector3d(0, 5, 0)
++ }
++
++ ConvexMeshShape {
++ id: queryConvexShape
++ geometry: CuboidGeometry {}
++ position: Qt.vector3d(0, 5, 0)
++ scale: Qt.vector3d(0.01, 0.01, 0.01)
++ }
++
++ // Not valid as query geometry: PhysX only accepts sphere, capsule, box and convex mesh
++ PlaneShape {
++ id: queryPlaneShape
++ position: Qt.vector3d(0, 5, 0)
++ }
++
++ TriangleMeshShape {
++ id: queryTriangleMeshShape
++ geometry: CuboidGeometry {}
++ position: Qt.vector3d(0, 5, 0)
++ scale: Qt.vector3d(0.01, 0.01, 0.01)
++ }
++
++ // --- Test Flags ---
++ property bool testRaycastHit: false
++ property bool testRaycastMiss: false
++ property bool singleRaycastHit: false
++ property bool singleRaycastHitDetails: false
++ property bool singleRaycastMiss: false
++ property bool singleRaycastStaticOnly: false
++ property bool singleRaycastDynamicOnly: false
++ property bool multiRaycastHit: false
++
++ property bool testSweepBoxHit: false
++ property bool testSweepSphereHit: false
++ property bool testSweepMiss: false
++ property bool singleSweepBoxHit: false
++ property bool singleSweepSphereHit: false
++ property bool multiSweepBoxHit: false
++ property bool multiSweepSphereHit: false
++
++ property bool testOverlapBoxHit: false
++ property bool testOverlapSphereHit: false
++ property bool testOverlapMiss: false
++ property bool multiOverlapBoxHit: false
++ property bool multiOverlapSphereHit: false
++
++ property bool testInvalidInputs: false
++
++ // --- Query shape type coverage ---
++ property bool sweepCapsuleQueryShape: false
++ property bool sweepConvexQueryShape: false
++ property bool overlapCapsuleQueryShape: false
++ property bool overlapConvexQueryShape: false
++ property bool unsupportedQueryShapesRejected: false
++
++ // --- Target type coverage ---
++ property bool raycastHitsTriangleMesh: false
++ property bool raycastHitsCharacterController: false
++
++ // --- Destroyed-object safety ---
++ property bool queryAfterDestroyIsSafe: false
++ property var spawnedBodyRef: null
++ property int spawnedBodyDestroyed: 0
++
++ // --- New Flags for MTD and Trigger Exclusion ---
++ property bool testSweepMTD: false
++ property bool testTriggerExclusion: false
++
++ // --- Shape Validation Flags ---
++ property bool singleRaycastShapeCheck: false
++ property bool multiRaycastShapesCheck: false
++ property bool singleSweepBoxShapeCheck: false
++ property bool singleSweepSphereShapeCheck: false
++ property bool multiSweepBoxShapesCheck: false
++ property bool multiOverlapBoxShapesCheck: false
++
++ View3D {
++ id: viewport
++ anchors.fill: parent
++
++ environment: SceneEnvironment {
++ clearColor: "#151a3f"
++ backgroundMode: SceneEnvironment.Color
++ }
++
++ Node {
++ id: scene
++ PerspectiveCamera {
++ id: camera1
++ position: Qt.vector3d(0, 5, 10)
++ eulerRotation: Qt.vector3d(-20, 0, 0)
++ clipFar: 50
++ clipNear: 0.01
++ }
++
++ DirectionalLight {
++ eulerRotation.x: -45
++ eulerRotation.y: 45
++ }
++
++ Node {
++ id: fallingBoxNode
++ StaticRigidBody {
++ id: staticFloor
++ position: Qt.vector3d(0, -1, 0)
++ collisionShapes: BoxShape {
++ id: floorShape
++ extents: Qt.vector3d(20, 1, 20)
++ }
++ }
++
++ DynamicRigidBody {
++ id: dynamicBox
++ position: Qt.vector3d(0, 0, 0)
++ collisionShapes: BoxShape {
++ id: dynamicBoxShape
++ extents: Qt.vector3d(1, 1, 1)
++ }
++ }
++
++ // Trigger body placed directly in the ray/sweep path (Y = 2.5)
++ TriggerBody {
++ id: triggerZone
++ position: Qt.vector3d(0, 2.5, 0)
++ collisionShapes: BoxShape {
++ id: triggerShape
++ extents: Qt.vector3d(2, 2, 2)
++ }
++ }
++
++ // Triangle mesh target, off to the side at X = 8
++ StaticRigidBody {
++ id: meshBody
++ position: Qt.vector3d(8, 0, 0)
++ collisionShapes: TriangleMeshShape {
++ id: meshBodyShape
++ source: "#Cube"
++ scale: Qt.vector3d(0.02, 0.02, 0.02)
++ }
++ }
++
++ // Character controller target, off to the side at X = -8
++ CharacterController {
++ id: characterTarget
++ position: Qt.vector3d(-8, 0, 0)
++ gravity: Qt.vector3d(0, 0, 0)
++ collisionShapes: CapsuleShape {
++ id: characterTargetShape
++ diameter: 1.0
++ height: 1.0
++ }
++ }
++ }
++ }
++ }
++
++ // Helper for floating-point comparisons
++ function fuzzyCompareVec3(v1, v2, eps = 0.001) {
++ return Math.abs(v1.x - v2.x) < eps &&
++ Math.abs(v1.y - v2.y) < eps &&
++ Math.abs(v1.z - v2.z) < eps;
++ }
++
++ Timer {
++ interval: 1000
++ running: true
++ repeat: false
++ onTriggered: {
++ // --- 1. Invalid Input Validation Tests ---
++ const zeroDir = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, 0, 0), 10, true, true
++ );
++ const negDist = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), -5, true, true
++ );
++ const nanDist = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), NaN, true, true
++ );
++ const bothFalse = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 10, false, false
++ );
++ const nullSweep = caster.singleSweepQuery(null, Qt.vector3d(0, -1, 0), 10, true, true);
++
++ testInvalidInputs = (zeroDir.body === null) &&
++ (negDist.body === null) &&
++ (nanDist.body === null) &&
++ (bothFalse.body === null) &&
++ (nullSweep.body === null);
++
++ // --- 2. Raycast Tests ---
++ testRaycastHit = caster.testRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 100, true, true
++ );
++ testRaycastMiss = !caster.testRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, 1, 0), 10, true, true
++ );
++
++ const rayHitS = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 100, true, true
++ );
++ const rayHitD = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 100, false, true
++ );
++ singleRaycastHit = (rayHitD.body === dynamicBox && rayHitS.body === dynamicBox);
++
++ singleRaycastShapeCheck = (rayHitS.shape === dynamicBoxShape) &&
++ (rayHitD.shape === dynamicBoxShape);
++
++ if (rayHitS.body === dynamicBox) {
++ const expectedPos = Qt.vector3d(0, 0.5, 0);
++ const expectedNormal = Qt.vector3d(0, 1, 0);
++ const expectedDist = 4.5;
++
++ singleRaycastHitDetails = fuzzyCompareVec3(rayHitS.position, expectedPos) &&
++ fuzzyCompareVec3(rayHitS.normal, expectedNormal) &&
++ Math.abs(rayHitS.distance - expectedDist) < 0.001 &&
++ rayHitS.shape === dynamicBoxShape;
++ }
++
++ const rayMiss = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, 1, 0), 10, true, true
++ );
++ singleRaycastMiss = (rayMiss.body === null);
++
++ const rayStatic = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 10, true, false
++ );
++ singleRaycastStaticOnly = (rayStatic.body === staticFloor &&
++ rayStatic.shape === floorShape);
++
++ const rayDynamic = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 10, false, true
++ );
++ singleRaycastDynamicOnly = (rayDynamic.body === dynamicBox &&
++ rayDynamic.shape === dynamicBoxShape);
++
++ const multiRayHits = caster.multiRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ multiRaycastHit = (multiRayHits.length >= 2 &&
++ multiRayHits.some(h => h.body === dynamicBox) &&
++ multiRayHits.some(h => h.body === staticFloor));
++
++ const dynRayHit = multiRayHits.find(h => h.body === dynamicBox);
++ const statRayHit = multiRayHits.find(h => h.body === staticFloor);
++ multiRaycastShapesCheck = (dynRayHit && dynRayHit.shape === dynamicBoxShape) &&
++ (statRayHit && statRayHit.shape === floorShape);
++
++ // --- 3. Sweep Tests ---
++ queryBoxShape.position = Qt.vector3d(0, 5, 0);
++ querySphereShape.position = Qt.vector3d(0, 5, 0);
++
++ testSweepBoxHit = caster.testSweepQuery(
++ queryBoxShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ testSweepSphereHit = caster.testSweepQuery(
++ querySphereShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ testSweepMiss = !caster.testSweepQuery(
++ queryBoxShape, Qt.vector3d(0, 1, 0), 10, true, true
++ );
++
++ const sweepBoxS = caster.singleSweepQuery(
++ queryBoxShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ const sweepBoxD = caster.singleSweepQuery(
++ queryBoxShape, Qt.vector3d(0, -1, 0), 10, false, true
++ );
++ singleSweepBoxHit = (sweepBoxD.body === dynamicBox && sweepBoxS.body === dynamicBox);
++ singleSweepBoxShapeCheck = (sweepBoxS.shape === dynamicBoxShape) &&
++ (sweepBoxD.shape === dynamicBoxShape);
++
++ const sweepSphereS = caster.singleSweepQuery(
++ querySphereShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ const sweepSphereD = caster.singleSweepQuery(
++ querySphereShape, Qt.vector3d(0, -1, 0), 10, false, true
++ );
++ singleSweepSphereHit = (sweepSphereD.body === dynamicBox &&
++ sweepSphereS.body === dynamicBox);
++ singleSweepSphereShapeCheck = (sweepSphereS.shape === dynamicBoxShape) &&
++ (sweepSphereD.shape === dynamicBoxShape);
++
++ const multiSweepBox = caster.multiSweepQuery(
++ queryBoxShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ multiSweepBoxHit = (multiSweepBox.length >= 2 &&
++ multiSweepBox.some(h => h.body === dynamicBox) &&
++ multiSweepBox.some(h => h.body === staticFloor));
++
++ const dynSweepHit = multiSweepBox.find(h => h.body === dynamicBox);
++ const statSweepHit = multiSweepBox.find(h => h.body === staticFloor);
++ multiSweepBoxShapesCheck = (dynSweepHit && dynSweepHit.shape === dynamicBoxShape) &&
++ (statSweepHit && statSweepHit.shape === floorShape);
++
++ const multiSweepSphere = caster.multiSweepQuery(
++ querySphereShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ multiSweepSphereHit = (multiSweepSphere.length >= 2 &&
++ multiSweepSphere.some(h => h.body === dynamicBox) &&
++ multiSweepSphere.some(h => h.body === staticFloor));
++
++ // --- 4. Overlap Tests ---
++ queryOverlapBoxShape.position = Qt.vector3d(0, -0.5, 0);
++ queryOverlapSphereShape.position = Qt.vector3d(0, -0.5, 0);
++
++ testOverlapBoxHit = caster.testOverlapQuery(queryOverlapBoxShape, true, true);
++ testOverlapSphereHit = caster.testOverlapQuery(queryOverlapSphereShape, true, true);
++
++ queryOverlapBoxShape.position = Qt.vector3d(0, 50, 0);
++ testOverlapMiss = !caster.testOverlapQuery(queryOverlapBoxShape, true, true);
++ queryOverlapBoxShape.position = Qt.vector3d(0, -0.5, 0);
++
++ const multiOverlapBox = caster.multiOverlapQuery(queryOverlapBoxShape, true, true);
++ multiOverlapBoxHit = (multiOverlapBox.length >= 2 &&
++ multiOverlapBox.some(h => h.body === dynamicBox) &&
++ multiOverlapBox.some(h => h.body === staticFloor));
++
++ const dynOverlapHit = multiOverlapBox.find(h => h.body === dynamicBox);
++ const statOverlapHit = multiOverlapBox.find(h => h.body === staticFloor);
++ multiOverlapBoxShapesCheck = (dynOverlapHit && dynOverlapHit.shape === dynamicBoxShape) &&
++ (statOverlapHit && statOverlapHit.shape === floorShape);
++
++ const multiOverlapSphere = caster.multiOverlapQuery(
++ queryOverlapSphereShape, true, true
++ );
++ multiOverlapSphereHit = (multiOverlapSphere.length >= 2 &&
++ multiOverlapSphere.some(h => h.body === dynamicBox) &&
++ multiOverlapSphere.some(h => h.body === staticFloor));
++
++ // --- 5. MTD Sweep Test (Initial Overlap) ---
++ queryBoxShape.position = Qt.vector3d(0, 0, 0); // Position inside dynamicBox
++ const mtdHit = caster.singleSweepQuery(
++ queryBoxShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ testSweepMTD = (mtdHit.body === dynamicBox) && (mtdHit.distance < 0);
++ queryBoxShape.position = Qt.vector3d(0, 5, 0); // Restore position
++
++ // --- 6. Trigger Exclusion Test ---
++ const triggerRayHit = caster.singleRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ const triggerMultiRay = caster.multiRaycastQuery(
++ Qt.vector3d(0, 5, 0), Qt.vector3d(0, -1, 0), 10, true, true
++ );
++
++ queryBoxShape.position = Qt.vector3d(0, 5, 0);
++ const triggerSweepHit = caster.singleSweepQuery(
++ queryBoxShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++
++ queryOverlapBoxShape.position = Qt.vector3d(0, 2.5, 0);
++ const triggerOverlap = caster.multiOverlapQuery(queryOverlapBoxShape, true, true);
++ queryOverlapBoxShape.position = Qt.vector3d(0, -0.5, 0);
++
++ testTriggerExclusion = (triggerRayHit.body !== triggerZone) &&
++ !triggerMultiRay.some(h => h.body === triggerZone) &&
++ (triggerSweepHit.body !== triggerZone) &&
++ !triggerOverlap.some(h => h.body === triggerZone);
++
++ // --- 7. Query shape type coverage ---
++ // PhysX accepts sphere, capsule, box and convex mesh as query geometry.
++ // Box and sphere are covered above; cover the other two here.
++ queryCapsuleShape.position = Qt.vector3d(0, 5, 0);
++ sweepCapsuleQueryShape = caster.testSweepQuery(
++ queryCapsuleShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++
++ queryConvexShape.position = Qt.vector3d(0, 5, 0);
++ sweepConvexQueryShape = caster.testSweepQuery(
++ queryConvexShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++
++ queryCapsuleShape.position = Qt.vector3d(0, -0.5, 0);
++ overlapCapsuleQueryShape = caster.testOverlapQuery(queryCapsuleShape, true, true);
++ queryCapsuleShape.position = Qt.vector3d(0, 5, 0);
++
++ queryConvexShape.position = Qt.vector3d(0, -0.5, 0);
++ overlapConvexQueryShape = caster.testOverlapQuery(queryConvexShape, true, true);
++ queryConvexShape.position = Qt.vector3d(0, 5, 0);
++
++ // Everything else must be rejected rather than silently producing garbage.
++ const planeSweep = caster.testSweepQuery(
++ queryPlaneShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ const planeOverlap = caster.multiOverlapQuery(queryPlaneShape, true, true);
++ const meshSweep = caster.testSweepQuery(
++ queryTriangleMeshShape, Qt.vector3d(0, -1, 0), 10, true, true
++ );
++ const meshOverlap = caster.multiOverlapQuery(queryTriangleMeshShape, true, true);
++ unsupportedQueryShapesRejected = !planeSweep && planeOverlap.length === 0 &&
++ !meshSweep && meshOverlap.length === 0;
++
++ // --- 8. Target type coverage ---
++ // Triangle mesh bodies are valid query *targets* even though they are not
++ // valid query *shapes*.
++ const meshHit = caster.singleRaycastQuery(
++ Qt.vector3d(8, 5, 0), Qt.vector3d(0, -1, 0), 100, true, true
++ );
++ raycastHitsTriangleMesh = (meshHit.body === meshBody) &&
++ (meshHit.shape === meshBodyShape);
++
++ // A CharacterController reports a body but no shape, as documented.
++ const charHit = caster.singleRaycastQuery(
++ Qt.vector3d(-8, 5, 0), Qt.vector3d(0, -1, 0), 100, true, true
++ );
++ raycastHitsCharacterController = (charHit.body === characterTarget) &&
++ (charHit.shape === null);
++
++ // --- 9. Destroyed-object safety (must run last: it pauses the world) ---
++ spawnedBodyRef = spawnedBodyComponent.createObject(scene);
++ destroySpawnedBody.start();
++ }
++ }
++
++ // A body whose PhysX actor outlives its QML object: the actor is only released on
++ // the next physics frame, and while the world is paused that frame never comes.
++ Component {
++ id: spawnedBodyComponent
++ StaticRigidBody {
++ position: Qt.vector3d(0, 30, 0)
++ collisionShapes: BoxShape { extents: Qt.vector3d(2, 2, 2) }
++ Component.onDestruction: spawnedBodyDestroyed++
++ }
++ }
++
++ Timer {
++ id: destroySpawnedBody
++ interval: 100
++ running: false
++ repeat: false
++ onTriggered: {
++ caster.running = false;
++ spawnedBodyRef.destroy();
++ queryAfterDestroy.start();
++ }
++ }
++
++ Timer {
++ id: queryAfterDestroy
++ interval: 50
++ running: false
++ repeat: true
++ property int tries: 0
++ onTriggered: {
++ gc();
++ tries++;
++ if (spawnedBodyDestroyed === 0) {
++ if (tries > 40)
++ queryAfterDestroy.stop();
++ return;
++ }
++ queryAfterDestroy.stop();
++ // Must not crash, and must not report the destroyed body.
++ const hits = caster.multiRaycastQuery(
++ Qt.vector3d(0, 40, 0), Qt.vector3d(0, -1, 0), 100, true, true
++ );
++ queryAfterDestroyIsSafe = !hits.some(h => h.body !== null && h.body === spawnedBodyRef);
++ }
++ }
++
++ // --- Physics Test Cases ---
++
++ PhysicsTestCase {
++ name: "TestInvalidInputs"
++ goalReached: testInvalidInputs
++ }
++
++ PhysicsTestCase {
++ name: "TestRayCastHit"
++ goalReached: testRaycastHit
++ }
++
++ PhysicsTestCase {
++ name: "TestRayCastMiss"
++ goalReached: testRaycastMiss
++ }
++
++ PhysicsTestCase {
++ name: "SingleRayCast"
++ goalReached: singleRaycastHit && singleRaycastShapeCheck
++ }
++
++ PhysicsTestCase {
++ name: "SingleRayCastHitDetails"
++ goalReached: singleRaycastHitDetails
++ }
++
++ PhysicsTestCase {
++ name: "SingleRayCastMiss"
++ goalReached: singleRaycastMiss
++ }
++
++ PhysicsTestCase {
++ name: "SingleRayCastStaticOnly"
++ goalReached: singleRaycastStaticOnly
++ }
++
++ PhysicsTestCase {
++ name: "SingleRayCastDynamicOnly"
++ goalReached: singleRaycastDynamicOnly
++ }
++
++ PhysicsTestCase {
++ name: "MultiRayCast"
++ goalReached: multiRaycastHit && multiRaycastShapesCheck
++ }
++
++ PhysicsTestCase {
++ name: "TestSweepBoxHit"
++ goalReached: testSweepBoxHit
++ }
++
++ PhysicsTestCase {
++ name: "TestSweepSphereHit"
++ goalReached: testSweepSphereHit
++ }
++
++ PhysicsTestCase {
++ name: "TestSweepMiss"
++ goalReached: testSweepMiss
++ }
++
++ PhysicsTestCase {
++ name: "SingleSweepBox"
++ goalReached: singleSweepBoxHit && singleSweepBoxShapeCheck
++ }
++
++ PhysicsTestCase {
++ name: "SingleSweepSphere"
++ goalReached: singleSweepSphereHit && singleSweepSphereShapeCheck
++ }
++
++ PhysicsTestCase {
++ name: "MultiSweepBox"
++ goalReached: multiSweepBoxHit && multiSweepBoxShapesCheck
++ }
++
++ PhysicsTestCase {
++ name: "MultiSweepSphere"
++ goalReached: multiSweepSphereHit
++ }
++
++ PhysicsTestCase {
++ name: "TestSweepMTD"
++ goalReached: testSweepMTD
++ }
++
++ PhysicsTestCase {
++ name: "TestOverlapBoxHit"
++ goalReached: testOverlapBoxHit
++ }
++
++ PhysicsTestCase {
++ name: "TestOverlapSphereHit"
++ goalReached: testOverlapSphereHit
++ }
++
++ PhysicsTestCase {
++ name: "TestOverlapMiss"
++ goalReached: testOverlapMiss
++ }
++
++ PhysicsTestCase {
++ name: "MultiOverlapBox"
++ goalReached: multiOverlapBoxHit && multiOverlapBoxShapesCheck
++ }
++
++ PhysicsTestCase {
++ name: "MultiOverlapSphere"
++ goalReached: multiOverlapSphereHit
++ }
++
++ PhysicsTestCase {
++ name: "TestTriggerExclusion"
++ goalReached: testTriggerExclusion
++ }
++
++ PhysicsTestCase {
++ name: "CapsuleQueryShape"
++ goalReached: sweepCapsuleQueryShape && overlapCapsuleQueryShape
++ }
++
++ PhysicsTestCase {
++ name: "ConvexMeshQueryShape"
++ goalReached: sweepConvexQueryShape && overlapConvexQueryShape
++ }
++
++ PhysicsTestCase {
++ name: "UnsupportedQueryShapesRejected"
++ goalReached: unsupportedQueryShapesRejected
++ }
++
++ PhysicsTestCase {
++ name: "RaycastHitsTriangleMesh"
++ goalReached: raycastHitsTriangleMesh
++ }
++
++ PhysicsTestCase {
++ name: "RaycastHitsCharacterController"
++ goalReached: raycastHitsCharacterController
++ }
++
++ PhysicsTestCase {
++ name: "QueryAfterDestroyIsSafe"
++ goalReached: queryAfterDestroyIsSafe
++ }
++ }