Skip to content

Commit aaebf43

Browse files
author
Illia
committed
Non-center line projection and edge cases
1 parent 6cdaad9 commit aaebf43

File tree

2 files changed

+138
-49
lines changed

2 files changed

+138
-49
lines changed

TEST_Geometry2D.cpp

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ class Test_Geometry2D : public olc::PixelGameEngine
160160
return std::visit(dispatch, s1, s2);
161161
}
162162

163-
std::optional<olc::v_2d<float>> CheckProject(const ShapeWrap& s1, const ShapeWrap& s2, const ShapeWrap& s3)
163+
std::optional<olc::v_2d<float>> CheckProject(const ShapeWrap& s1, const ShapeWrap& s2, const ShapeWrap& s3, const double& end_length = 0.5)
164164
{
165165
const auto dispatch = overloads{
166166

@@ -179,10 +179,10 @@ class Test_Geometry2D : public olc::PixelGameEngine
179179
return project(make_internal(s1), make_internal(s2), make_internal(s3));
180180
},
181181

182-
[](const Line& s1, const Circle& s2, const Ray& s3)
182+
[&](const Line& s1, const Circle& s2, const Ray& s3)
183183
{
184-
return project(make_internal(s1), make_internal(s2), make_internal(s3));
185-
}
184+
return project(make_internal(s1), make_internal(s2), make_internal(s3), end_length);
185+
},
186186

187187
};
188188

@@ -361,8 +361,9 @@ class Test_Geometry2D : public olc::PixelGameEngine
361361
std::vector<std::optional<olc::v_2d<float>>> projected_circle_left_ray;
362362
std::vector<std::optional<olc::v_2d<float>>> projected_circle_right_ray;
363363
std::vector<std::optional<olc::v_2d<float>>> projected_line_left_ray;
364+
const double left_line_end_length = 0.1;
364365
std::vector<std::optional<olc::v_2d<float>>> projected_line_right_ray;
365-
const Line line_to_project{ { { 100.0f, 100.0f }, {140.0f, 70.0f} } };
366+
const Line line_to_project{ { { 100.0f, 100.0f }, {130.0f, 150.0f} } };
366367

367368

368369
if (GetMouse(1).bHeld)
@@ -400,7 +401,7 @@ class Test_Geometry2D : public olc::PixelGameEngine
400401

401402
if (mode == Mode::LineProject && (std::holds_alternative<Circle>(shape)))
402403
{
403-
projected_line_left_ray.push_back(CheckProject(line_to_project, shape, ray1));
404+
projected_line_left_ray.push_back(CheckProject(line_to_project, shape, ray1, left_line_end_length));
404405
projected_line_right_ray.push_back(CheckProject(line_to_project, shape, ray2));
405406
}
406407
}
@@ -456,9 +457,10 @@ class Test_Geometry2D : public olc::PixelGameEngine
456457
if (mode == Mode::LineProject && projection.has_value())
457458
{
458459
const auto& vec = make_internal(line_to_project).vector().norm();
459-
const auto& half_length = 0.5 * make_internal(line_to_project).vector().mag();
460-
const olc::vf2d start = projection.value() - vec * half_length;
461-
const olc::vf2d end = projection.value() + vec * half_length;
460+
const auto& end_length = left_line_end_length * make_internal(line_to_project).vector().mag();
461+
const auto& start_length = (left_line_end_length - 1) * make_internal(line_to_project).vector().mag();
462+
const olc::vf2d start = projection.value() + vec * end_length;
463+
const olc::vf2d end = projection.value() + vec * start_length;
462464
Line line_to_draw{ {{start}, {end}} };
463465
DrawShape(line_to_draw, olc::CYAN);
464466
}

olcUTIL_Geometry2D.h

Lines changed: 127 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,7 +1409,7 @@ namespace olc::utils::geom2d
14091409
// project(t,c)
14101410
// 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?)
14111411
template<typename T1, typename T2, typename T3>
1412-
inline std::optional<olc::v_2d<T1>> project(const line<T1>& l, const circle<T2>& c, const ray<T3>& q)
1412+
inline std::optional<olc::v_2d<T1>> project(const line<T1>& l, const circle<T2>& c, const ray<T3>& q, const double& end_length=0.5)
14131413
{
14141414
// The ray intersects the line in the midpoint at all times
14151415
// The function returns a projected midpoint
@@ -1420,80 +1420,167 @@ namespace olc::utils::geom2d
14201420
auto [rx, ry] = q.direction;
14211421
const auto& [ox, oy] = q.origin;
14221422
const auto& [slope, intercept] = l.coefficients();
1423+
1424+
bool is_vertical = false;
1425+
bool is_horizontal = false;
1426+
1427+
if (slope == 0) is_horizontal = true;
1428+
if (slope == std::numeric_limits<double>::infinity()) is_vertical = true;
1429+
14231430
const auto& length = l.vector().mag();
1431+
const auto& vec = l.vector().norm();
1432+
const auto& start_length = end_length - 1;
14241433

1425-
// First, we find two points on the cirlce that correspond to the tangent of the same slope
1426-
// as the given line
1427-
const double y_t1 = r / std::sqrt(slope * slope + 1) + b;
1428-
const double y_t2 = - r / std::sqrt(slope * slope + 1) + b;
1429-
std::vector<olc::v_2d<double>> tangent_points{ { {a - (y_t1 - b) * slope, y_t1}, {a - (y_t2 - b) * slope, y_t2} } };
1434+
// First, we find two points on the cirlce that correspond to
1435+
// the tangent of the same as the given line
1436+
std::vector<olc::v_2d<double>> tangent_points;
1437+
if (is_vertical)
1438+
{
1439+
const double y_t1 = b, y_t2 = b;
1440+
tangent_points.assign({{a - r, y_t1},{a + r, y_t2}});
1441+
}
1442+
else if (is_horizontal)
1443+
{
1444+
const double x_t1 = a, x_t2 = a;
1445+
tangent_points.assign({ {x_t1, b + r},{x_t2, b - r} });
1446+
}
1447+
else
1448+
{
1449+
const double y_t1 = r / std::sqrt(slope * slope + 1) + b;
1450+
const double y_t2 = -r / std::sqrt(slope * slope + 1) + b;
1451+
tangent_points.assign({ {a - (y_t1 - b) * slope, y_t1}, {a - (y_t2 - b) * slope, y_t2} });
1452+
}
14301453

14311454
// Check if the ray intersects the tangent line along its way and not from behind
1432-
const auto& v = (tangent_points[0] - q.origin).norm();
1433-
if (v.dot(q.direction) < 0) return{};
1455+
if ((tangent_points[0] - q.origin).norm().dot(q.direction) < 0) return{};
1456+
1457+
// If the ray is parallel to both tangent lines - special case
1458+
if ((is_vertical && std::abs(rx) < epsilon) ||
1459+
(is_horizontal && std::abs(ry) < epsilon) ||
1460+
std::abs(slope - ry / rx) < epsilon)
1461+
{
1462+
// The line is going through the origin of the ray
1463+
const std::vector<std::pair<olc::v_2d<double>, double>> sides{
1464+
{{q.origin + vec * end_length * length}, end_length},
1465+
{{q.origin + vec * start_length * length}, start_length} };
1466+
1467+
const auto& closest = *std::min_element(sides.begin(), sides.end(),
1468+
[&c](const auto& lhs, const auto& rhs)
1469+
{
1470+
return (lhs.first - c.pos).mag2() < (rhs.first - c.pos).mag2();
1471+
});
14341472

1435-
// The line is first being projected onto both tangent lines, the tangent point and the intersections between the ray and
1436-
// the tangent line are stored as well
1473+
// The closest end of the line is transported onto the circle,
1474+
// Return value is determined via the length of that side
1475+
const auto& intersections = intersects(q, c);
1476+
if (!intersections.empty()) return intersections[0] - closest.second * length * vec;
1477+
return {};
1478+
}
1479+
1480+
// The line is first being projected onto both tangent lines,
1481+
// the tangent point and the intersections between the and the tangent line are stored as well
14371482
std::vector<std::pair<olc::v_2d<double>, std::pair<olc::v_2d<double>, olc::v_2d<double>>>> side_points;
14381483
std::vector<olc::v_2d<T1>> possible_points;
14391484

14401485
for (const auto& tangent_point : tangent_points)
14411486
{
1487+
std::vector<std::pair<olc::v_2d<double>, double>> possible_closest_side;
1488+
1489+
double xl;
1490+
double yl;
1491+
14421492
const auto& [xt, yt] = tangent_point;
1443-
const double xl = (ry / rx * ox - xt * slope + yt - oy) / (ry / rx - slope);
1444-
const double yl = (xl - xt) * slope + yt;
1445-
// Creating two points on a tangent line that correspond to the start and the end of a projected line
1446-
const double x_side_1 = xl - 0.5 * length * l.vector().norm().x;
1447-
const double x_side_2 = xl + 0.5 * length * l.vector().norm().x;
1448-
const double y_side_1 = (x_side_1 - xl) * slope + yl;
1449-
const double y_side_2 = (x_side_2 - xl) * slope + yl;
1493+
if (is_vertical)
1494+
{
1495+
xl = xt;
1496+
yl = (xl - ox) * (ry / rx) + oy;
1497+
}
1498+
else if (is_horizontal)
1499+
{
1500+
yl = yt;
1501+
xl = (yl - oy) * (rx / ry) + ox;
1502+
}
1503+
else
1504+
{
1505+
xl = (ry / rx * ox - xt * slope + yt - oy) / (ry / rx - slope);
1506+
yl = (xl - xt) * slope + yt;
1507+
}
1508+
const v_2d<double> intersection_point{xl, yl};
1509+
// Creating two points on a tangent line that correspond to
1510+
// the start and the end of a projected line
1511+
1512+
const double x_side_end = xl + end_length * length * vec.x;
1513+
const double y_side_end = yl + end_length * length * vec.y;
1514+
const double x_side_start = xl + start_length * length * vec.x;
1515+
const double y_side_start = yl + start_length * length * vec.y;
1516+
1517+
possible_closest_side.push_back({ { x_side_end, y_side_end }, std::abs(end_length * length) });
1518+
possible_closest_side.push_back({ { x_side_start, y_side_start }, std::abs(start_length * length) });
1519+
1520+
const auto& closest_side_to_tangent = *std::min_element(possible_closest_side.begin(), possible_closest_side.end(),
1521+
[&tangent_point, &intersection_point](const auto& lhs, const auto& rhs)
1522+
{
1523+
return (lhs.first - intersection_point).dot(tangent_point - intersection_point) >
1524+
(rhs.first - intersection_point).dot(tangent_point - intersection_point);
1525+
});
1526+
1527+
// Only add a tangent-ray intersection point if the distance between
1528+
// it and the tangent point is less than or equal the length of side.
1529+
// It means the line can lie on the tangent
1530+
if ((tangent_point - intersection_point).mag() <= closest_side_to_tangent.second)
1531+
possible_points.push_back(intersection_point);
1532+
1533+
side_points.push_back({ {x_side_end, y_side_end}, {{xl, yl}, {xt, yt}} });
1534+
side_points.push_back({ {x_side_start, y_side_start}, {{xl, yl}, {xt, yt}} });
14501535

1451-
side_points.push_back({ {x_side_1, y_side_1}, {{xl, yl}, {xt, yt}} });
1452-
side_points.push_back({ {x_side_2, y_side_2}, {{xl, yl}, {xt, yt}} });
14531536
}
14541537

14551538

1456-
for (const auto& [side_point, pair] : side_points)
1539+
for (const auto& [side_point, pair_intersection_tangent] : side_points)
14571540
{
14581541
const auto& [x1, y1] = side_point;
1459-
const auto& tangent_intersection_point = pair.first;
1460-
const auto& tangent_point = pair.second;
1461-
1462-
// We search for a scalar s such that:
1463-
// x_new = x_side + s * rx
1464-
// y_new = y_side + s * ry
1465-
// (x_new - a)^2 + (y_new - b)^2 = r^2
1466-
//
1467-
// Where (x_new, y_new) is a point on the circle that is obtained by transporting a line projected onto the
1468-
// tangent via a ray q
1469-
//
1470-
// For each tangent line and for each side of the line being transported, there can be up to 4 transported lines
1542+
const auto& tangent_intersection_point = pair_intersection_tangent.first;
1543+
const auto& tangent_point = pair_intersection_tangent.second;
1544+
1545+
/* We search for a scalar s such that:
1546+
x_new = x_side + s * rx
1547+
y_new = y_side + s * ry
1548+
(x_new - a)^2 + (y_new - b)^2 = r^2
1549+
1550+
Where (x_new, y_new) is a point on the circle that is
1551+
obtained by transporting a line projected onto tangent via a ray q
1552+
1553+
In a regular (non-parallel to the tangent, non-vertical or horizontal) situation,
1554+
There will be up to 4 possible lines projected onto the circle.
1555+
Each end point of the line can be projected so that the line is either outside the circle
1556+
or, respectively, inside */
14711557

14721558
const double D = (2 * rx * (x1 - a) + 2 * ry * (y1 - b)) * (2 * rx * (x1 - a) + 2 * ry * (y1 - b)) -
14731559
4 * (rx * rx + ry * ry) * ((x1 - a) * (x1 - a) + (y1 - b) * (y1 - b) - r * r);
14741560
if (D > 0)
14751561
{
14761562
const double s1 = (-2 * rx * (x1 - a) - 2 * ry * (y1 - b) + std::sqrt(D)) / (2 * (rx * rx + ry * ry));
1477-
const double s2 = (-2 * rx * (x1 - a) - 2 * ry * (y1 - b) - std::sqrt(D)) / (2 * (rx * rx + ry * ry));
1563+
const double s2 = (-2 * rx * (x1 - a) - 2 * ry * (y1 - b) - std::sqrt(D)) / (2 * (rx * rx + ry * ry))
1564+
14781565
const olc::v_2d<T1> p1{ tangent_intersection_point + s1 * q.direction };
14791566
const olc::v_2d<T1> p2{ tangent_intersection_point + s2 * q.direction };
1480-
// Only add a point is it is not inside the circle
1481-
!contains(c, p1) ? possible_points.push_back(p1) : (void)0;
1482-
!contains(c, p2) ? possible_points.push_back(p2) : (void)0;
1483-
// Only add a tangent-ray intersection point if the distance between it and the tangent point is less than of equal
1484-
// half length of the line. It means the line can lie on the tangent
1485-
(tangent_intersection_point - tangent_point).mag() <= 0.5 * length ? possible_points.push_back(tangent_intersection_point) : (void)0;
1567+
1568+
possible_points.push_back(p1);
1569+
possible_points.push_back(p2);
14861570
}
14871571
else if (D == 0)
14881572
{
14891573
const double s = (-2 * rx * (x1 - a) - 2 * ry * (y1 - b)) / (2 * (rx * rx + ry * ry));
14901574
const olc::v_2d<T1> p{ tangent_intersection_point + s * q.direction };
1491-
!contains(c, p) ? possible_points.push_back(p): (void)0;
1575+
1576+
possible_points.push_back(p);
14921577
}
14931578
}
14941579

14951580
if (possible_points.empty()) return {};
14961581

1582+
1583+
14971584
// Compare by the distance to the origin
14981585
return *std::min_element(possible_points.begin(), possible_points.end(),
14991586
[&q](const auto& lhs, const auto& rhs)

0 commit comments

Comments
 (0)