diff --git a/TEST_Geometry2D.cpp b/TEST_Geometry2D.cpp index fbdb6ee..3d39c64 100644 --- a/TEST_Geometry2D.cpp +++ b/TEST_Geometry2D.cpp @@ -8,6 +8,7 @@ #include "third_party/olcPGEX_QuickGUI.h" #include +#include using namespace olc::utils::geom2d; @@ -97,7 +98,10 @@ class Test_Geometry2D : public olc::PixelGameEngine // The clever bit (and a bit new to me - jx9) using ShapeWrap = std::variant; - + enum Mode + { + CircleProject, LineProject, TriangleProject, NoProject + }; bool CheckOverlaps(const ShapeWrap& s1, const ShapeWrap& s2) { @@ -156,6 +160,55 @@ class Test_Geometry2D : public olc::PixelGameEngine return std::visit(dispatch, s1, s2); } + std::optional> CheckProject(const ShapeWrap& s1, const ShapeWrap& s2, const ShapeWrap& s3, + const double& end_length = 0.5) + { + const auto dispatch = overloads{ + + [](const auto& s1, const auto& s2, const auto& s3) + { + return std::optional>{}; + }, + + [](const Circle& s1, const Rect& s2, const Ray& s3) + { + return project(make_internal(s1), make_internal(s2), make_internal(s3)); + }, + + [](const Circle& s1, const Triangle& s2, const Ray& s3) + { + return project(make_internal(s1), make_internal(s2), make_internal(s3)); + }, + + [&](const Line& s1, const Circle& s2, const Ray& s3) + { + return project(make_internal(s1), make_internal(s2), make_internal(s3), end_length); + }, + + }; + + return std::visit(dispatch, s1, s2, s3); + } + + std::optional> CheckProjectTriangle(const ShapeWrap& s1, const ShapeWrap& s2, const ShapeWrap& s3) + { + const auto dispatch = overloads{ + + [](const auto& s1, const auto& s2, const auto& s3) + { + return std::optional>{}; + }, + + [](const Triangle& s1, const Circle& s2, const Ray& s3) + { + return project(make_internal(s1), make_internal(s2), make_internal(s3)); + } + }; + + return std::visit(dispatch, s1, s2, s3); + } + + std::optional> CheckReflect(const olc::utils::geom2d::ray& s1, const ShapeWrap& s2) { const auto dispatch = overloads{ @@ -228,6 +281,7 @@ class Test_Geometry2D : public olc::PixelGameEngine size_t nSelectedShapeIndex = -1; olc::vi2d vOldMousePos; + Mode mode = Mode::NoProject; public: bool OnUserCreate() override @@ -259,6 +313,11 @@ class Test_Geometry2D : public olc::PixelGameEngine if (GetMouse(0).bReleased) nSelectedShapeIndex = -1; + if (GetKey(olc::Key::C).bPressed) mode = Mode::CircleProject; + if (GetKey(olc::Key::L).bPressed) mode = Mode::LineProject; + if (GetKey(olc::Key::T).bPressed) mode = Mode::TriangleProject; + if (GetKey(olc::Key::N).bPressed) mode = Mode::NoProject; + // Check for mouse hovered shapes ShapeWrap mouse{ Point{olc::vf2d(GetMousePos())} }; @@ -319,6 +378,17 @@ class Test_Geometry2D : public olc::PixelGameEngine bool bRayMode = false; + std::vector>> projected_circle_left_ray; + std::vector>> projected_circle_right_ray; + std::vector>> projected_line_left_ray; + const double left_line_end_length = 0.1; + std::vector>> projected_line_right_ray; + std::vector>> projected_triangle_left_ray; + std::vector>> projected_triangle_right_ray; + const Line line_to_project{ { { 100.0f, 100.0f }, {130.0f, 100.0f} } }; + const Triangle triangle_to_project{ { {65.0f, 107.0f}, {18.0f, 100.0f}, {80.0f, 170.0f}} }; + + if (GetMouse(1).bHeld) { // Enable Ray Mode @@ -344,6 +414,27 @@ class Test_Geometry2D : public olc::PixelGameEngine vIntersections.insert(vIntersections.end(), vPoints3.begin(), vPoints3.end()); + for (const auto& shape : vecShapes) + { + if(mode == Mode::CircleProject && (std::holds_alternative(shape) || std::holds_alternative(shape))) + { + projected_circle_left_ray.push_back(CheckProject(Circle{ { { 130.0f, 20.0f }, {150.0f, 20.0f} } }, shape, ray1)); + projected_circle_right_ray.push_back(CheckProject(Circle{ { { 130.0f, 20.0f }, {150.0f, 20.0f} } }, shape, ray2)); + } + + else if (mode == Mode::LineProject && (std::holds_alternative(shape))) + { + projected_line_left_ray.push_back(CheckProject(line_to_project, shape, ray1, left_line_end_length)); + projected_line_right_ray.push_back(CheckProject(line_to_project, shape, ray2)); + } + + else if (mode == Mode::TriangleProject && (std::holds_alternative(shape))) + { + projected_triangle_left_ray.push_back(CheckProjectTriangle(triangle_to_project, shape, ray1)); + projected_triangle_right_ray.push_back(CheckProjectTriangle(triangle_to_project, shape, ray2)); + } + } + } // Draw All Shapes @@ -373,6 +464,74 @@ class Test_Geometry2D : public olc::PixelGameEngine { DrawShape(ray1, olc::CYAN); DrawShape(ray2, olc::CYAN); + + for(const auto& projection : projected_circle_left_ray) + { + if (mode == Mode::CircleProject && projection.has_value()) + { + DrawCircle(projection.value(), 20.0f, olc::CYAN); + } + } + + for (const auto& projection : projected_circle_right_ray) + { + if (mode == Mode::CircleProject && projection.has_value()) + { + DrawCircle(projection.value(), 20.0f, olc::RED); + } + } + + for (const auto& projection : projected_line_left_ray) + { + if (mode == Mode::LineProject && projection.has_value()) + { + const auto& vec = make_internal(line_to_project).vector().norm(); + const auto& end_length = left_line_end_length * make_internal(line_to_project).vector().mag(); + const auto& start_length = (left_line_end_length - 1) * make_internal(line_to_project).vector().mag(); + const olc::vf2d start = projection.value() + vec * end_length; + const olc::vf2d end = projection.value() + vec * start_length; + Line line_to_draw{ {{start}, {end}} }; + DrawShape(line_to_draw, olc::CYAN); + } + } + + for (const auto& projection : projected_line_right_ray) + { + if (mode == Mode::LineProject && projection.has_value()) + { + const auto& vec = make_internal(line_to_project).vector().norm(); + const auto& half_length = 0.5 * make_internal(line_to_project).vector().mag(); + const olc::vf2d start = projection.value() - vec * half_length; + const olc::vf2d end = projection.value() + vec * half_length; + Line line_to_draw{ {{start}, {end}} }; + DrawShape(line_to_draw, olc::RED); + } + } + + for (const auto& projection : projected_triangle_left_ray) + { + if (mode == Mode::TriangleProject && projection.has_value()) + { + const auto p0 = projection.value().pos[0], + p1 = projection.value().pos[1], + p2 = projection.value().pos[2]; + + DrawTriangle(p0, p1, p2, olc::CYAN); + } + } + + for (const auto& projection : projected_triangle_right_ray) + { + if (mode == Mode::TriangleProject && projection.has_value()) + { + const auto p0 = projection.value().pos[0], + p1 = projection.value().pos[1], + p2 = projection.value().pos[2]; + + DrawTriangle(p0, p1, p2, olc::RED); + } + } + } // Laser beam diff --git a/olcUTIL_Geometry2D.h b/olcUTIL_Geometry2D.h index 7e3be8a..0652db3 100644 --- a/olcUTIL_Geometry2D.h +++ b/olcUTIL_Geometry2D.h @@ -164,7 +164,7 @@ | closest | closest | | | | | | overlaps | overlaps | overlaps | overlaps | overlaps | | | intersects | intersects | intersects | intersects | intersects | | - | project | project | | project | | | + | project | project | project | project | project | | ---------+--------------+--------------+--------------+--------------+--------------+--------------+ TRIANGLE | contains | contains | contains | contains | contains | | | closest | | | | | | @@ -1406,14 +1406,7 @@ namespace olc::utils::geom2d } - - - - - - - - + // ================================================================================================================ @@ -1501,7 +1494,8 @@ namespace olc::utils::geom2d // Inspired by this (very clever btw) // https://stackoverflow.com/questions/45370692/circle-rectangle-collision-response // But modified to work :P - double overlap = (olc::v_2d{ std::clamp(c.pos.x, r.pos.x, r.pos.x + r.size.x), std::clamp(c.pos.y, r.pos.y, r.pos.y + r.size.y) } - c.pos).mag2(); + double overlap = (olc::v_2d{ std::clamp(c.pos.x, r.pos.x, r.pos.x + r.size.x), + std::clamp(c.pos.y, r.pos.y, r.pos.y + r.size.y) } - c.pos).mag2(); if (std::isnan(overlap)) overlap = 0; return (overlap - (c.radius * c.radius)) < 0; } @@ -1924,6 +1918,7 @@ namespace olc::utils::geom2d } + // envelope_c(c) // Return circle that fully encapsulates a point template @@ -2099,6 +2094,11 @@ namespace olc::utils::geom2d template inline std::optional> project(const circle& c, const line& l, const ray& q) { + + // There should be no projection if the shapes overlap + // (The circle) is positioned at the origin of the ray + if (overlaps(circle{q.origin, c.radius}, l)) return std::nullopt; + // Treat line segment as capsule with radius that of the circle // and treat the circle as a point @@ -2125,18 +2125,11 @@ namespace olc::utils::geom2d return std::nullopt; } - // Find closest - double dClosest = std::numeric_limits::max(); - olc::v_2d vClosest; - for (const auto& vContact : vAllIntersections) - { - double dDistance = (vContact - q.origin).mag2(); - if (dDistance < dClosest) + const auto& vClosest = *std::min_element(vAllIntersections.begin(), vAllIntersections.end(), + [&q](const auto& lhs, const auto& rhs) { - dClosest = dDistance; - vClosest = vContact; - } - } + return (lhs - q.origin).mag2() < (rhs - q.origin).mag2(); + }); return vClosest; } @@ -2146,6 +2139,12 @@ namespace olc::utils::geom2d template inline std::optional> project(const circle& c, const rect& r, const ray& q) { + + const auto& closest_to_origin = closest(r, q.origin); + // Since the projected circle lies ON the rectangle, the closest point + // is allowed to be on the rectangle, but not inside + if ((q.origin - closest_to_origin).mag() < c.radius) return std::nullopt; + const auto s1 = project(c, r.top(), q); const auto s2 = project(c, r.bottom(), q); const auto s3 = project(c, r.left(), q); @@ -2164,17 +2163,11 @@ namespace olc::utils::geom2d } // Find closest - double dClosest = std::numeric_limits::max(); - olc::v_2d vClosest; - for (const auto& vContact : vAllIntersections) - { - double dDistance = (vContact - q.origin).mag2(); - if (dDistance < dClosest) + const auto& vClosest = *std::min_element(vAllIntersections.begin(), vAllIntersections.end(), + [&q](const auto& lhs, const auto& rhs) { - dClosest = dDistance; - vClosest = vContact; - } - } + return (lhs - q.origin).mag2() < (rhs - q.origin).mag2(); + }); return vClosest; } @@ -2184,8 +2177,102 @@ namespace olc::utils::geom2d template inline std::optional> project(const circle& c, const triangle& t, const ray& q) { - // TODO: - return std::nullopt; + const auto& closest_to_origin = closest(t, q.origin); + if ((q.origin - closest_to_origin).mag() < c.radius) return std::nullopt; + + const auto s1 = project(c, t.side(0), q); + const auto s2 = project(c, t.side(1), q); + const auto s3 = project(c, t.side(2), q); + + std::vector> vAllIntersections; + if (s1.has_value()) vAllIntersections.push_back(s1.value()); + if (s2.has_value()) vAllIntersections.push_back(s2.value()); + if (s3.has_value()) vAllIntersections.push_back(s3.value()); + + if (vAllIntersections.size() == 0) + { + // No intersections at all, so + return std::nullopt; + } + + // Find closest + const auto& vClosest = *std::min_element(vAllIntersections.begin(), vAllIntersections.end(), + [&q](const auto& lhs, const auto& rhs) + { + return (lhs - q.origin).mag2() < (rhs - q.origin).mag2(); + }); + + return vClosest; + } + + // project(l,c) + // project a line, onto a circle, via a ray (i.e. how far along the ray can the line travel until it contacts the circle?) + template + inline std::optional> project(const line& l, const circle& c, const ray& q, const double& end_length = 0.5) + { + // The ray intersects the line in the point p + // |------p--------------| + // ^ ^ + // | | + // start end + // |<-end length->| + // The function returns a projected point + + if (contains(c, q.origin)) return std::nullopt; + + const auto& length = l.vector().mag(); + const auto& vec = l.vector().norm(); + const auto& start_length = end_length - 1; + + // Projecting the circle on the line that is going through the origin of the ray + const line line_on_the_origin{ q.origin + end_length * length * vec, q.origin + start_length * length * vec }; + const ray q_reverse{ c.pos, -q.direction }; + + const auto& circle_on_line = project(c, line_on_the_origin, q_reverse); + + if (!circle_on_line.has_value()) return std::nullopt; + + const double distance = (circle_on_line.value() - c.pos).mag(); + + // Move the projection back so that the centre of the circle is in it's original position + return q.origin + distance * q.direction; + + } + + + // project(t,c) + // project a triangle, onto a circle, via a ray (i.e. how far along the ray can the triangle travel until it contacts the circle?) + template + inline std::optional> project(const triangle& t, const circle& c, const ray q) + { + // The ray is going through the point of median inersections + + if (contains(c, q.origin)) return std::nullopt; + + const auto side_0_mid = t.side(0).rpoint(0.5 * t.side(0).length()); + const auto side_1_mid = t.side(1).rpoint(0.5 * t.side(1).length()); + + const auto& median_point = intersects(line{side_0_mid, t.pos[2]}, line{side_1_mid, t.pos[0]}); + if (median_point.empty()) return std::nullopt; + + const auto displace = (q.origin - median_point[0]); + const triangle triangle_on_the_origin{ t.pos[0] + displace, t.pos[1] + displace, t.pos[2] + displace }; + + // Projecting the circle on the line that is going through the origin of the ray + const ray q_reverse{ c.pos, -q.direction }; + + const auto& circle_on_triangle = project(c, triangle_on_the_origin, q_reverse); + + if (!circle_on_triangle.has_value()) return std::nullopt; + + const double distance = (circle_on_triangle.value() - c.pos).mag(); + + const triangle projected{ triangle_on_the_origin.pos[0] + distance * q.direction.norm(), + triangle_on_the_origin.pos[1] + distance * q.direction.norm(), + triangle_on_the_origin.pos[2] + distance * q.direction.norm() }; + + // Move the projection back so that the centre of the circle is in it's original position + return projected; } @@ -2457,10 +2544,11 @@ namespace olc::utils::geom2d if (s2 < 0) return { q.origin + q.direction * s1 }; - return { q.origin + q.direction * std::min(s1, s2) }; + const auto& [min_dist, max_dist] = std::minmax(s1, s2); + return { q.origin + q.direction * min_dist, q.origin + q.direction * max_dist }; } } - + // intersects(q,r) // Get intersection points where a ray intersects a rectangle template