LCOV - code coverage report
Current view: top level - test - quadtree.cpp (source / functions) Coverage Total Hit
Test: coverage.info Lines: 99.5 % 184 183
Test Date: 2026-04-03 02:26:39 Functions: 100.0 % 9 9

            Line data    Source code
       1              : #include "catch.hpp"
       2              : #include <cmath>
       3              : #include "QuadTree.h"
       4              : #include "QuadTreeNode.h"
       5              : #include "DisplayList.h"
       6              : 
       7              : namespace {
       8              : 
       9              : // QuadTree's destructor does not delete the tree; callers must free nodes.
      10          230 : void freeQuadTreeNode(QuadTreeNode *n) {
      11          230 :   if (n == NULL)
      12          176 :     return;
      13          270 :   for (int i = 0; i < 4; ++i) {
      14          216 :     freeQuadTreeNode(n->child[i]);
      15          216 :   }
      16           54 :   delete n;
      17          230 : }
      18              : 
      19            3 : int countDisplayList(DisplayList *list) {
      20            3 :   int n = 0;
      21           11 :   for (QuadTreeNode *p = list->head; p != NULL; p = p->next) {
      22            8 :     ++n;
      23            8 :   }
      24            3 :   return n;
      25              : }
      26              : 
      27            9 : bool isLeaf(QuadTreeNode *n) {
      28            9 :   if (n == NULL)
      29            0 :     return false;
      30           18 :   return n->child[0] == NULL && n->child[1] == NULL && n->child[2] == NULL &&
      31            9 :          n->child[3] == NULL;
      32            9 : }
      33              : 
      34              : }  // namespace
      35              : 
      36           14 : SCENARIO( "QuadTree spatial partition", "[QuadTree]" ) {
      37           15 :   GIVEN( "A tree that is already a leaf at root" ) {
      38            2 :     QuadTree qt;
      39            2 :     REQUIRE(qt.root == NULL);
      40              : 
      41            2 :     qt.buildTree(qt.root, 0.0f, 10.0f, 0.0f, 10.0f, 10.0f);
      42            2 :     REQUIRE(qt.root != NULL);
      43              : 
      44            3 :     THEN( "bounds and bounding sphere match implementation formulas" ) {
      45            1 :       REQUIRE(qt.root->min[0] == Approx(0.0f));
      46            1 :       REQUIRE(qt.root->max[0] == Approx(10.0f));
      47            1 :       REQUIRE(qt.root->min[1] == Approx(0.0f));
      48            1 :       REQUIRE(qt.root->max[1] == Approx(10.0f));
      49              : 
      50            1 :       Vector o;
      51            1 :       qt.root->boundingSphere.getOriginVector(o);
      52            1 :       REQUIRE(o.x == Approx(5.0f));
      53            1 :       REQUIRE(o.y == Approx(0.0f));
      54            1 :       REQUIRE(o.z == Approx(-5.0f));
      55              :       // NOTE: the radius formula uses the full diagonal (sqrt(w²+h²)) rather
      56              :       // than the circumradius (half-diagonal). The sphere is 2x oversized,
      57              :       // causing conservative over-rendering. See known bugs for the fix.
      58            1 :       const float expectedR = std::sqrt(200.0f);
      59            1 :       REQUIRE(qt.root->boundingSphere.getRadius() == Approx(expectedR));
      60            1 :     }
      61              : 
      62            3 :     THEN( "root has no children" ) {
      63            1 :       REQUIRE(isLeaf(qt.root));
      64            1 :     }
      65              : 
      66            2 :     freeQuadTreeNode(qt.root);
      67            2 :     qt.root = NULL;
      68            2 :   }
      69              : 
      70           20 :   GIVEN( "A 20x20 region with scale 10 (splits into four leaves)" ) {
      71            7 :     QuadTree qt;
      72            7 :     qt.buildTree(qt.root, 0.0f, 20.0f, 0.0f, 20.0f, 10.0f);
      73              : 
      74            8 :     THEN( "root has four children and each is a leaf" ) {
      75            1 :       REQUIRE(qt.root->child[0] != NULL);
      76            1 :       REQUIRE(qt.root->child[1] != NULL);
      77            1 :       REQUIRE(qt.root->child[2] != NULL);
      78            1 :       REQUIRE(qt.root->child[3] != NULL);
      79            5 :       for (int i = 0; i < 4; ++i) {
      80            4 :         REQUIRE(isLeaf(qt.root->child[i]));
      81            4 :       }
      82            1 :     }
      83              : 
      84            8 :     THEN( "child[0] covers xMin-midX, zMin-midZ" ) {
      85            1 :       QuadTreeNode *c = qt.root->child[0];
      86            1 :       REQUIRE(c->min[0] == Approx(0.0f));
      87            1 :       REQUIRE(c->max[0] == Approx(10.0f));
      88            1 :       REQUIRE(c->min[1] == Approx(0.0f));
      89            1 :       REQUIRE(c->max[1] == Approx(10.0f));
      90            1 :     }
      91              : 
      92              :     // Verify the XZ bounding sphere convention holds for child nodes:
      93              :     // y=0 (ground plane), z=-centerZ (negated), radius is the full diagonal.
      94            8 :     THEN( "child[0] bounding sphere follows XZ convention" ) {
      95            1 :       Vector o;
      96            1 :       qt.root->child[0]->boundingSphere.getOriginVector(o);
      97            1 :       REQUIRE(o.x == Approx(5.0f));
      98            1 :       REQUIRE(o.y == Approx(0.0f));
      99            1 :       REQUIRE(o.z == Approx(-5.0f));
     100            1 :       REQUIRE(qt.root->child[0]->boundingSphere.getRadius() == Approx(std::sqrt(200.0f)));
     101            1 :     }
     102              : 
     103            8 :     THEN( "child[1] covers xMin-midX, midZ-zMax" ) {
     104            1 :       QuadTreeNode *c = qt.root->child[1];
     105            1 :       REQUIRE(c->min[0] == Approx(0.0f));
     106            1 :       REQUIRE(c->max[0] == Approx(10.0f));
     107            1 :       REQUIRE(c->min[1] == Approx(10.0f));
     108            1 :       REQUIRE(c->max[1] == Approx(20.0f));
     109            1 :     }
     110              : 
     111            8 :     THEN( "child[2] covers midX-xMax, zMin-midZ" ) {
     112            1 :       QuadTreeNode *c = qt.root->child[2];
     113            1 :       REQUIRE(c->min[0] == Approx(10.0f));
     114            1 :       REQUIRE(c->max[0] == Approx(20.0f));
     115            1 :       REQUIRE(c->min[1] == Approx(0.0f));
     116            1 :       REQUIRE(c->max[1] == Approx(10.0f));
     117            1 :     }
     118              : 
     119            8 :     THEN( "child[3] covers midX-xMax, midZ-zMax" ) {
     120            1 :       QuadTreeNode *c = qt.root->child[3];
     121            1 :       REQUIRE(c->min[0] == Approx(10.0f));
     122            1 :       REQUIRE(c->max[0] == Approx(20.0f));
     123            1 :       REQUIRE(c->min[1] == Approx(10.0f));
     124            1 :       REQUIRE(c->max[1] == Approx(20.0f));
     125            1 :     }
     126              : 
     127            8 :     THEN( "buildLeafList collects four nodes" ) {
     128            1 :       DisplayList list;
     129            1 :       qt.buildLeafList(&list);
     130            1 :       REQUIRE(countDisplayList(&list) == 4);
     131            1 :     }
     132              : 
     133            7 :     freeQuadTreeNode(qt.root);
     134            7 :     qt.root = NULL;
     135            7 :   }
     136              : 
     137           15 :   GIVEN( "A 20x10 strip (split along X only)" ) {
     138            2 :     QuadTree qt;
     139            2 :     qt.buildTree(qt.root, 0.0f, 20.0f, 0.0f, 10.0f, 10.0f);
     140              : 
     141            3 :     THEN( "two children along X, both leaves" ) {
     142            1 :       REQUIRE(qt.root->child[0] != NULL);
     143            1 :       REQUIRE(qt.root->child[1] != NULL);
     144            1 :       REQUIRE(qt.root->child[2] == NULL);
     145            1 :       REQUIRE(qt.root->child[3] == NULL);
     146            1 :       REQUIRE(isLeaf(qt.root->child[0]));
     147            1 :       REQUIRE(isLeaf(qt.root->child[1]));
     148            1 :       REQUIRE(qt.root->child[0]->min[0] == Approx(10.0f));
     149            1 :       REQUIRE(qt.root->child[0]->max[0] == Approx(20.0f));
     150            1 :       REQUIRE(qt.root->child[1]->min[0] == Approx(0.0f));
     151            1 :       REQUIRE(qt.root->child[1]->max[0] == Approx(10.0f));
     152            1 :     }
     153              : 
     154            3 :     THEN( "buildLeafList collects two nodes" ) {
     155            1 :       DisplayList list;
     156            1 :       qt.buildLeafList(&list);
     157            1 :       REQUIRE(countDisplayList(&list) == 2);
     158            1 :     }
     159              : 
     160            2 :     freeQuadTreeNode(qt.root);
     161            2 :     qt.root = NULL;
     162            2 :   }
     163              : 
     164           15 :   GIVEN( "A 10x20 strip (split along Z only)" ) {
     165            2 :     QuadTree qt;
     166            2 :     qt.buildTree(qt.root, 0.0f, 10.0f, 0.0f, 20.0f, 10.0f);
     167              : 
     168            3 :     THEN( "two children along Z, both leaves" ) {
     169            1 :       REQUIRE(qt.root->child[0] == NULL);
     170            1 :       REQUIRE(qt.root->child[1] == NULL);
     171            1 :       REQUIRE(qt.root->child[2] != NULL);
     172            1 :       REQUIRE(qt.root->child[3] != NULL);
     173            1 :       REQUIRE(isLeaf(qt.root->child[2]));
     174            1 :       REQUIRE(isLeaf(qt.root->child[3]));
     175            1 :       REQUIRE(qt.root->child[2]->min[1] == Approx(0.0f));
     176            1 :       REQUIRE(qt.root->child[2]->max[1] == Approx(10.0f));
     177            1 :       REQUIRE(qt.root->child[3]->min[1] == Approx(10.0f));
     178            1 :       REQUIRE(qt.root->child[3]->max[1] == Approx(20.0f));
     179            1 :     }
     180              : 
     181            3 :     THEN( "buildLeafList collects two nodes" ) {
     182            1 :       DisplayList list;
     183            1 :       qt.buildLeafList(&list);
     184            1 :       REQUIRE(countDisplayList(&list) == 2);
     185            1 :     }
     186              : 
     187            2 :     freeQuadTreeNode(qt.root);
     188            2 :     qt.root = NULL;
     189            2 :   }
     190           13 : }
     191              : 
     192            2 : SCENARIO( "QuadTree traverseTree", "[QuadTree]" ) {
     193            2 :   GIVEN( "A built tree" ) {
     194            1 :     QuadTree qt;
     195            1 :     qt.buildTree(qt.root, 0.0f, 20.0f, 0.0f, 20.0f, 10.0f);
     196              : 
     197              :     // traverseTree is a debug-only utility (PRINT_DEBUG) and a no-op in
     198              :     // production builds (DEBUG=0). Smoke-tested here to confirm the
     199              :     // recursive traversal terminates without crashing.
     200            2 :     WHEN( "traverseTree is called" ) {
     201            2 :       THEN( "it does not crash" ) {
     202            1 :         qt.traverseTree();
     203            1 :       }
     204            1 :     }
     205              : 
     206            1 :     freeQuadTreeNode(qt.root);
     207            1 :     qt.root = NULL;
     208            1 :   }
     209            1 : }
     210              : 
     211            3 : SCENARIO( "QuadTreeNode default construction", "[QuadTreeNode]" ) {
     212            4 :   GIVEN( "A default node" ) {
     213            2 :     QuadTreeNode n;
     214              : 
     215            3 :     THEN( "children are null, bounds are zero, and next is null" ) {
     216            5 :       for (int i = 0; i < 4; ++i) {
     217            4 :         REQUIRE(n.child[i] == NULL);
     218            4 :       }
     219            1 :       REQUIRE(n.min[0] == Approx(0.0f));
     220            1 :       REQUIRE(n.max[0] == Approx(0.0f));
     221            1 :       REQUIRE(n.min[1] == Approx(0.0f));
     222            1 :       REQUIRE(n.max[1] == Approx(0.0f));
     223            1 :       REQUIRE(n.next == NULL);
     224            1 :     }
     225              : 
     226            3 :     THEN( "bounding sphere defaults to zero origin and zero radius" ) {
     227            1 :       Vector o;
     228            1 :       n.boundingSphere.getOriginVector(o);
     229            1 :       REQUIRE(o.x == Approx(0.0f));
     230            1 :       REQUIRE(o.y == Approx(0.0f));
     231            1 :       REQUIRE(o.z == Approx(0.0f));
     232            1 :       REQUIRE(n.boundingSphere.getRadius() == Approx(0.0f));
     233            1 :     }
     234            2 :   }
     235            2 : }
        

Generated by: LCOV version 2.4-0