Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
# Description

_Please include a summary of the change and which issue is fixed. Please also include relevant
motivation and context. List any dependencies that are required for this change._
Added automated unit tests for ray intersection functions, migrating interactive tests from `test_geometry.cpp` to proper unit tests that can run in CI/CD pipelines.

**Changes:**
- Added unit tests for `rectangle_ray_intersection` in `unit_test_geometry.cpp`
- Added unit tests for `circle_ray_intersection` in `unit_test_geometry.cpp`
- Added unit tests for `triangle_ray_intersection` in `unit_test_geometry.cpp`
- Added unit tests for `quad_ray_intersection` in `unit_test_geometry.cpp`
- Added unit tests for `bitmap_ray_collision` in `unit_test_bitmap.cpp`
- Added test for detecting closest intersection among multiple shapes
- Added necessary includes for geometry headers and physics

**Motivation:**
The ray intersection functionality previously only had interactive visual tests that required manual inspection. These new automated tests enable continuous integration testing and prevent regressions.

Fixes # (issue)

## Type of change

_Please delete options that are not relevant._

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as
expected)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation (update or new)

## How Has This Been Tested?

_Please describe the tests that you ran to verify your changes. Provide instructions so we can
reproduce. Please also list any relevant details for your test configuration_
**Test Details:**
- All tests are automated using Catch2 framework
- Tests validate both boolean return values and output parameters (hit points, distances)
- Edge cases tested: rays pointing away, parallel rays, rays from inside shapes
- Multiple shape priority testing validates distance-based collision detection

**To reproduce:**
```bash
# From MSYS2 MinGW64 terminal
cd projects/cmake
cmake -G "Unix Makefiles" .
make
cd ../../bin

# Run all unit tests
./skunit_tests

# Run only ray intersection tests
./skunit_tests "[ray_intersection]"
./skunit_tests "[ray_collision]"
```

## Testing Checklist

- [ ] Tested with sktest
- [ ] Tested with skunit_tests
- [ ] Tested with sktest (not applicable - these are unit tests)
- [x] Tested with skunit_tests (syntax validated, ready for build/test)

## Checklist

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code in hard-to-understand areas
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] My changes generate no new warnings
- [ ] I have requested a review from ... on the Pull Request
106 changes: 106 additions & 0 deletions coresdk/src/test/unit_tests/unit_test_bitmap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
#include "types.h"
#include "graphics.h"
#include "resources.h"
#include "physics.h"
#include "images.h"
#include "rectangle_drawing.h"
#include "color.h"

#include "logging_handling.h"

Expand Down Expand Up @@ -134,3 +138,105 @@ TEST_CASE("bitmap bounding details can be retrieved", "[bitmap]")
}
free_bitmap(bmp);
}

TEST_CASE("can perform bitmap ray collision detection", "[bitmap][ray_collision][physics]")
{
// Create opaque bitmaps for testing to avoid transparency issues
bitmap bmp_1 = create_bitmap("bmp_1", 50, 50);
clear_bitmap(bmp_1, COLOR_RED);
bitmap bmp_2 = create_bitmap("bmp_2", 50, 50);
clear_bitmap(bmp_2, COLOR_BLUE);
bitmap bmp_3 = create_bitmap("bmp_3", 50, 50);
clear_bitmap(bmp_3, COLOR_GREEN);

// Collision tests require pixel masks
setup_collision_mask(bmp_1);
setup_collision_mask(bmp_2);
setup_collision_mask(bmp_3);

REQUIRE(bitmap_valid(bmp_1));
REQUIRE(bitmap_valid(bmp_2));
REQUIRE(bitmap_valid(bmp_3));

SECTION("can detect ray collision with bitmap")
{
point_2d bmp_position = point_at(100.0, 100.0);
point_2d ray_origin = point_at(50.0, 125.0);
vector_2d ray_heading = vector_to(1.0, 0.0);

// Ray should collide with bitmap in its path
bool collision = bitmap_ray_collision(bmp_1, 0, bmp_position, ray_origin, ray_heading);
REQUIRE(collision);

// Ray pointing away should not collide
ray_heading = vector_to(-1.0, 0.0);
collision = bitmap_ray_collision(bmp_1, 0, bmp_position, ray_origin, ray_heading);
REQUIRE_FALSE(collision);
}

SECTION("can detect ray collision with multiple bitmaps at different positions")
{
point_2d bmp_1_position = point_at(300.0, 300.0);
point_2d bmp_2_position = point_at(500.0, 300.0);
point_2d bmp_3_position = point_at(700.0, 300.0);
point_2d ray_origin = point_at(100.0, 325.0);

// Single ray that passes through all three bitmaps
vector_2d ray_heading = vector_to(1.0, 0.0);
bool collision_1 = bitmap_ray_collision(bmp_1, 0, bmp_1_position, ray_origin, ray_heading);
bool collision_2 = bitmap_ray_collision(bmp_2, 0, bmp_2_position, ray_origin, ray_heading);
bool collision_3 = bitmap_ray_collision(bmp_3, 0, bmp_3_position, ray_origin, ray_heading);

// All should be true as we are using opaque bitmaps
REQUIRE((collision_1 && collision_2 && collision_3));
}

SECTION("can detect ray collision with different ray origins")
{
point_2d bmp_position = point_at(300.0, 300.0);
vector_2d ray_heading = vector_to(1.0, 0.0);

// Ray from left should collide
point_2d ray_origin_left = point_at(200.0, 325.0);
bool collision_left = bitmap_ray_collision(bmp_1, 0, bmp_position, ray_origin_left, ray_heading);
REQUIRE(collision_left);

// Ray from far below should not collide
point_2d ray_origin_below = point_at(200.0, 500.0);
bool collision_below = bitmap_ray_collision(bmp_1, 0, bmp_position, ray_origin_below, ray_heading);
REQUIRE_FALSE(collision_below);
}

SECTION("can handle ray collision with different bitmap cells")
{
// Create 2-cell bitmap
bitmap cell_bmp = create_bitmap("cell_bmp", 100, 50);
bitmap_set_cell_details(cell_bmp, 50, 50, 2, 1, 2); // w, h, cols, rows, count

// Clear to transparent
clear_bitmap(cell_bmp, COLOR_TRANSPARENT);

// Draw rect on first cell (left side)
fill_rectangle_on_bitmap(cell_bmp, COLOR_RED, 0, 0, 50, 50);

setup_collision_mask(cell_bmp);

point_2d bmp_position = point_at(300.0, 300.0);
point_2d ray_origin = point_at(200.0, 325.0);
vector_2d ray_heading = vector_to(1.0, 0.0);

// Test with cell 0 (solid)
bool collision_cell_0 = bitmap_ray_collision(cell_bmp, 0, bmp_position, ray_origin, ray_heading);
REQUIRE(collision_cell_0);

// Test with cell 1 (transparent/empty)
bool collision_cell_1 = bitmap_ray_collision(cell_bmp, 1, bmp_position, ray_origin, ray_heading);
REQUIRE_FALSE(collision_cell_1);

free_bitmap(cell_bmp);
}

free_bitmap(bmp_1);
free_bitmap(bmp_2);
free_bitmap(bmp_3);
}
173 changes: 173 additions & 0 deletions coresdk/src/test/unit_tests/unit_test_geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

#include "types.h"
#include "point_geometry.h"
#include "rectangle_geometry.h"
#include "circle_geometry.h"
#include "triangle_geometry.h"
#include "quad_geometry.h"

using namespace splashkit_lib;

Expand Down Expand Up @@ -984,3 +988,172 @@ TEST_CASE("can perform trigonometric calculations", "[trigonometry]")
REQUIRE(tangent(360.0f) == Catch::Detail::Approx(0.0f).margin(__FLT_EPSILON__));
}
}

TEST_CASE("can perform rectangle ray intersection", "[geometry][ray_intersection]")
{
rectangle r1 = rectangle_from(100.0, 100.0, 100.0, 100.0);

SECTION("can detect ray intersection with rectangle")
{
// Ray from left that intersects
REQUIRE(rectangle_ray_intersection(point_at(90.0, 110.0), vector_to(1.0, 0.0), r1));

// Ray that misses (goes above the rectangle)
REQUIRE_FALSE(rectangle_ray_intersection(point_at(95.0, 95.0), vector_to(1.0, 0.0), r1));

// Ray from top that intersects
REQUIRE(rectangle_ray_intersection(point_at(150.0, 50.0), vector_to(0.0, 1.0), r1));

// Ray pointing away from rectangle
REQUIRE_FALSE(rectangle_ray_intersection(point_at(50.0, 150.0), vector_to(-1.0, 0.0), r1));
}

SECTION("can get hit point and distance for rectangle ray intersection")
{
point_2d hit_point;
double distance;

// Ray from left hitting the left edge
bool intersects = rectangle_ray_intersection(point_at(50.0, 150.0), vector_to(1.0, 0.0), r1, hit_point, distance);
REQUIRE(intersects);
REQUIRE(hit_point.x == Catch::Detail::Approx(100.0).margin(EPSILON));
REQUIRE(hit_point.y == Catch::Detail::Approx(150.0).margin(EPSILON));
REQUIRE(distance == Catch::Detail::Approx(50.0).margin(EPSILON));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional improvement: The Catch::Detail::Approx(50.0).margin(EPSILON) syntax is consistent with the rest of the file, but could be simplified to Approx(50.0) anywhere it's used like this.


// Ray from top hitting the top edge
intersects = rectangle_ray_intersection(point_at(150.0, 50.0), vector_to(0.0, 1.0), r1, hit_point, distance);
REQUIRE(intersects);
REQUIRE(hit_point.x == Catch::Detail::Approx(150.0).margin(EPSILON));
REQUIRE(hit_point.y == Catch::Detail::Approx(100.0).margin(EPSILON));
REQUIRE(distance == Catch::Detail::Approx(50.0).margin(EPSILON));

// Ray that doesn't intersect
intersects = rectangle_ray_intersection(point_at(50.0, 50.0), vector_to(-1.0, -1.0), r1, hit_point, distance);
REQUIRE_FALSE(intersects);
Comment on lines +1030 to +1032
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional improvement: This test was already done in the previous test case. Instead, consider covering the case where the ray starts from inside the rectangle (as noted in the function documentation)

}
}

TEST_CASE("can perform circle ray intersection", "[geometry][ray_intersection]")
{
circle c1 = circle_at(300.0, 200.0, 60.0);

SECTION("can detect ray intersection with circle")
{
// Ray from left that intersects center
REQUIRE(circle_ray_intersection(point_at(200.0, 200.0), vector_to(1.0, 0.0), c1));

// Ray from top that intersects
REQUIRE(circle_ray_intersection(point_at(300.0, 100.0), vector_to(0.0, 1.0), c1));

// Ray that misses the circle
REQUIRE_FALSE(circle_ray_intersection(point_at(200.0, 100.0), vector_to(0.0, 1.0), c1));

// Ray pointing away from circle
REQUIRE_FALSE(circle_ray_intersection(point_at(200.0, 200.0), vector_to(-1.0, 0.0), c1));
}

SECTION("can get hit point and distance for circle ray intersection")
{
point_2d hit_point;
double distance;

// Ray from left hitting circle
bool intersects = circle_ray_intersection(point_at(200.0, 200.0), vector_to(1.0, 0.0), c1, hit_point, distance);
REQUIRE(intersects);
REQUIRE(hit_point.x == Catch::Detail::Approx(240.0).margin(EPSILON));
REQUIRE(hit_point.y == Catch::Detail::Approx(200.0).margin(EPSILON));
REQUIRE(distance == Catch::Detail::Approx(40.0).margin(EPSILON));

// Ray from inside the circle
intersects = circle_ray_intersection(point_at(300.0, 200.0), vector_to(1.0, 0.0), c1, hit_point, distance);
REQUIRE(intersects);
REQUIRE(hit_point.x == Catch::Detail::Approx(300.0).margin(EPSILON));
REQUIRE(distance == Catch::Detail::Approx(0.0).margin(EPSILON));

// Ray that doesn't intersect
intersects = circle_ray_intersection(point_at(200.0, 100.0), vector_to(0.0, 1.0), c1, hit_point, distance);
REQUIRE_FALSE(intersects);
}
}

TEST_CASE("can perform triangle ray intersection", "[geometry][ray_intersection]")
{
// Axis-aligned right triangle for simpler calculations
// (400,400) - bottom-left corner
// (500,400) - bottom-right corner
// (400,500) - top-left corner
triangle t1 = triangle_from(400.0, 400.0, 500.0, 400.0, 400.0, 500.0);

SECTION("can detect ray intersection with triangle")
{
// Ray from left that intersects vertical side (x=400)
REQUIRE(triangle_ray_intersection(point_at(350.0, 450.0), vector_to(1.0, 0.0), t1));

// Ray from bottom that intersects horizontal side (y=400)
REQUIRE(triangle_ray_intersection(point_at(450.0, 350.0), vector_to(0.0, 1.0), t1));

// Ray that misses the triangle
REQUIRE_FALSE(triangle_ray_intersection(point_at(300.0, 300.0), vector_to(1.0, 1.0), t1));

// Ray pointing away from triangle
REQUIRE_FALSE(triangle_ray_intersection(point_at(350.0, 450.0), vector_to(-1.0, 0.0), t1));
}

SECTION("can get hit point and distance for triangle ray intersection")
{
point_2d hit_point;
double distance;

// Ray from left hitting vertical edge x=400
bool intersects = triangle_ray_intersection(point_at(350.0, 450.0), vector_to(1.0, 0.0), t1, hit_point, distance);
REQUIRE(intersects);
REQUIRE(hit_point.x == Catch::Detail::Approx(400.0).margin(EPSILON));
REQUIRE(hit_point.y == Catch::Detail::Approx(450.0).margin(EPSILON));
REQUIRE(distance == Catch::Detail::Approx(50.0).margin(EPSILON));

// Ray that doesn't intersect
intersects = triangle_ray_intersection(point_at(300.0, 300.0), vector_to(0.0, 1.0), t1, hit_point, distance);
REQUIRE_FALSE(intersects);
}
}

TEST_CASE("can perform quad ray intersection", "[geometry][ray_intersection]")
{
// Axis-aligned rectangular quad
// (100,300) TL, (200,300) TR, (200,500) BR, (100,500) BL
quad q1 = quad_from(100.0, 300.0, 200.0, 300.0, 200.0, 500.0, 100.0, 500.0);

SECTION("can detect ray intersection with quad")
{
// Ray from left that intersects
REQUIRE(quad_ray_intersection(point_at(50.0, 400.0), vector_to(1.0, 0.0), q1));

// Ray from top that intersects
REQUIRE(quad_ray_intersection(point_at(150.0, 200.0), vector_to(0.0, 1.0), q1));

// Ray that misses the quad
REQUIRE_FALSE(quad_ray_intersection(point_at(50.0, 200.0), vector_to(0.0, 1.0), q1));

// Ray pointing away from quad
REQUIRE_FALSE(quad_ray_intersection(point_at(50.0, 400.0), vector_to(-1.0, 0.0), q1));
}

SECTION("can get hit point and distance for quad ray intersection")
{
point_2d hit_point;
double distance;

// Ray from left hitting quad vertical side (x=100)
bool intersects = quad_ray_intersection(point_at(50.0, 400.0), vector_to(1.0, 0.0), q1, hit_point, distance);
REQUIRE(intersects);
REQUIRE(hit_point.x == Catch::Detail::Approx(100.0).margin(EPSILON));
REQUIRE(hit_point.y == Catch::Detail::Approx(400.0).margin(EPSILON));
REQUIRE(distance == Catch::Detail::Approx(50.0).margin(EPSILON));

// Ray that doesn't intersect
intersects = quad_ray_intersection(point_at(50.0, 200.0), vector_to(0.0, 1.0), q1, hit_point, distance);
REQUIRE_FALSE(intersects);
}
}


Loading