Skip to content

Commit 912335e

Browse files
fredroybakpaul
andauthored
[Geometry] Bugfixes and add unit tests (sofa-framework#5987)
* fix test for intersectionWithPlane (if zero with tolerance) if (denominator < EQUALITY_THRESHOLD) the denominator is a signed dot product. When negative (edge crosses plane from the "back"), this is always true, so the function rejects ~half of all valid intersections. * fix two bugs with 2d case for Edge::intersectionWithEdge 1-The 2D path computes alpha (parameter on edge AB) and checks 0 <= alpha <= 1, but never computes or checks beta (parameter on edge CD). Reports false intersections when the infinite line CD crosses segment AB but outside segment CD. 2-if (alphaDenom < std::numeric_limits<T>::epsilon()) same pattern as Bug 1. The cross product alphaDenom is signed. When negative (non-collinear edges in one orientation), incorrectly reports them as collinear. * fix equality for 2d case of ispointonedge * Mat_solve_LCP: fix when reaching max nb loop solveLCP returns true when solver fails to converge (max iterations reached) * fix get bary coord for triangle While isPointInTriangle still works (it only checks sign pattern), any code that uses the returned coordinates for interpolation, projection to the nearest point on the triangle, or distance computation gets wrong values. This is a public utility function likely consumed by many parts of the codebase. * isPointInTriangle(3d) was not checking the plane, and add a boolean to bypass the plane check (as before) * add more unit tests * Update Sofa/framework/Geometry/src/sofa/geometry/Edge.h (fix sign error for beta) Co-authored-by: Paul Baksic <30337881+bakpaul@users.noreply.github.com> * add isPointOnPlane function and add its unit tests * compute coeffs only if in plane; set -1,-1,-1 if not * update unit tests --------- Co-authored-by: Paul Baksic <30337881+bakpaul@users.noreply.github.com>
1 parent cdfd69c commit 912335e

9 files changed

Lines changed: 674 additions & 34 deletions

File tree

Sofa/framework/Geometry/src/sofa/geometry/Edge.h

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ struct Edge
165165
N2 = sofa::type::cross(AB, AC).norm2();
166166
}
167167

168-
if (N2 > EQUALITY_THRESHOLD || N2 < 0)
168+
if (std::abs(N2) > EQUALITY_THRESHOLD)
169169
return false;
170170

171171
return true;
@@ -197,7 +197,7 @@ struct Edge
197197

198198
//compute intersection between line and plane equation
199199
const T denominator = planeNorm * (n1 - n0);
200-
if (denominator < EQUALITY_THRESHOLD)
200+
if (std::abs(denominator) < EQUALITY_THRESHOLD)
201201
{
202202
return false;
203203
}
@@ -307,15 +307,19 @@ struct Edge
307307
const T alphaNom = AC[1] * CD[0] - AC[0] * CD[1];
308308
const T alphaDenom = AB[1] * CD[0] - AB[0] * CD[1];
309309

310-
if (alphaDenom < std::numeric_limits<T>::epsilon()) // collinear
310+
if (std::abs(alphaDenom) < std::numeric_limits<T>::epsilon()) // collinear
311311
{
312312
intersectionBaryCoord = sofa::type::Vec<2, T>(0, 0);
313313
return false;
314314
}
315315

316316
const T alpha = alphaNom / alphaDenom;
317+
// beta: pC + beta * CD = pA + alpha * AB
318+
const T beta = (std::abs(CD[0]) > std::abs(CD[1]))
319+
? (-AC[0] + alpha * AB[0]) / CD[0]
320+
: (-AC[1] + alpha * AB[1]) / CD[1];
317321

318-
if (alpha < 0 || alpha > 1)
322+
if (alpha < 0 || alpha > 1 || beta < 0 || beta > 1)
319323
{
320324
intersectionBaryCoord = sofa::type::Vec<2, T>(0, 0);
321325
return false;

Sofa/framework/Geometry/src/sofa/geometry/Triangle.h

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,54 @@ struct Triangle
8888
static constexpr auto getBarycentricCoordinates(const Node& p0, const Node& n0, const Node& n1, const Node& n2)
8989
{
9090
// Point can be written: p0 = a*n0 + b*n1 + c*n2
91-
// with a = area(n1n2p0)/area(n0n1n2), b = area(n0n2p0)/area(n0n1n2) and c = area(n0n1p0)/area(n0n1n2)
92-
const auto area = Triangle::area(n0, n1, n2);
93-
if (fabs(area) < std::numeric_limits<T>::epsilon()) // triangle is flat
91+
// with a = area(n1n2p0)/area(n0n1n2), b = area(n0n2p0)/area(n0n1n2) and c = area(n0n1p0)/area(n0n1n2)
92+
if constexpr (std::is_same_v<Node, sofa::type::Vec<3, T>>)
9493
{
95-
return sofa::type::Vec<3, T>(-1, -1, -1);
94+
// In 3D, use signed areas via dot product with triangle normal
95+
// to get correct sign for outside-triangle points
96+
const auto N = sofa::type::cross(n1 - n0, n2 - n0);
97+
const auto NdotN = sofa::type::dot(N, N);
98+
99+
if (NdotN < std::numeric_limits<T>::epsilon() * std::numeric_limits<T>::epsilon()) // triangle is flat
100+
{
101+
return sofa::type::Vec<3, T>(-1, -1, -1);
102+
}
103+
104+
sofa::type::Vec<3, T> baryCoefs(type::NOINIT);
105+
baryCoefs[0] = sofa::type::dot(N, sofa::type::cross(n2 - n1, p0 - n1)) / NdotN;
106+
baryCoefs[1] = sofa::type::dot(N, sofa::type::cross(n0 - n2, p0 - n2)) / NdotN;
107+
baryCoefs[2] = 1 - baryCoefs[0] - baryCoefs[1];
108+
109+
if (fabs(baryCoefs[2]) <= std::numeric_limits<T>::epsilon()){
110+
baryCoefs[2] = 0;
111+
}
112+
113+
return baryCoefs;
96114
}
97-
98-
const auto A0 = Triangle::area(n1, n2, p0);
99-
const auto A1 = Triangle::area(n0, p0, n2);
100-
101-
sofa::type::Vec<3, T> baryCoefs(type::NOINIT);
102-
baryCoefs[0] = A0 / area;
103-
baryCoefs[1] = A1 / area;
104-
baryCoefs[2] = 1 - baryCoefs[0] - baryCoefs[1];
105-
106-
if (fabs(baryCoefs[2]) <= std::numeric_limits<T>::epsilon()){
107-
baryCoefs[2] = 0;
115+
else
116+
{
117+
// In 2D, Triangle::area() returns a signed value (shoelace formula),
118+
// so the ratio of sub-areas to total area preserves correct signs
119+
const auto area = Triangle::area(n0, n1, n2);
120+
if (fabs(area) < std::numeric_limits<T>::epsilon()) // triangle is flat
121+
{
122+
return sofa::type::Vec<3, T>(-1, -1, -1);
123+
}
124+
125+
const auto A0 = Triangle::area(n1, n2, p0);
126+
const auto A1 = Triangle::area(n0, p0, n2);
127+
128+
sofa::type::Vec<3, T> baryCoefs(type::NOINIT);
129+
baryCoefs[0] = A0 / area;
130+
baryCoefs[1] = A1 / area;
131+
baryCoefs[2] = 1 - baryCoefs[0] - baryCoefs[1];
132+
133+
if (fabs(baryCoefs[2]) <= std::numeric_limits<T>::epsilon()){
134+
baryCoefs[2] = 0;
135+
}
136+
137+
return baryCoefs;
108138
}
109-
110-
return baryCoefs;
111139
}
112140

113141

@@ -148,23 +176,76 @@ struct Triangle
148176
}
149177

150178
}
151-
152-
179+
180+
/**
181+
* @brief Test if input point is on the plane defined by the Triangle (n0, n1, n2)
182+
* @tparam Node iterable container
183+
* @tparam T scalar
184+
* @param p0: position of the point to test
185+
* @param n0, n1, n2: nodes of the triangle
186+
* @return bool result if point is on the plane of the triangle.
187+
*/
188+
template<typename Node,
189+
typename T = std::decay_t<decltype(*std::begin(std::declval<Node>()))>,
190+
typename = std::enable_if_t<std::is_scalar_v<T>>
191+
>
192+
[[nodiscard]]
193+
static constexpr bool isPointOnPlane(const Node& p0, const Node& n0, const Node& n1, const Node& n2)
194+
{
195+
if constexpr (std::is_same_v<Node, sofa::type::Vec<3, T>>)
196+
{
197+
const auto normal = Triangle::normal(n0, n1, n2);
198+
const auto normalNorm2 = sofa::type::dot(normal, normal);
199+
if (normalNorm2 > std::numeric_limits<T>::epsilon())
200+
{
201+
const auto d = sofa::type::dot(p0 - n0, normal);
202+
if (d * d / normalNorm2 > std::numeric_limits<T>::epsilon())
203+
return false;
204+
}
205+
206+
return true;
207+
}
208+
else
209+
{
210+
// all points are trivially in the same plane
211+
return true;
212+
}
213+
}
214+
153215
/**
154216
* @brief Test if input point is inside Triangle (n0, n1, n2) using Triangle @sa getBarycentricCoordinates . The point is inside the Triangle if and only if Those coordinates are all positive.
155217
* @tparam Node iterable container
156218
* @tparam T scalar
157219
* @param p0: position of the point to test
158220
* @param n0, n1, n2: nodes of the triangle
159221
* @param output parameter: sofa::type::Vec<3, T> barycentric coordinates of the input point in Triangle
222+
* @param assumePointIsOnPlane: optional bool to avoid testing if the point is on the plane defined by the triangle
160223
* @return bool result if point is inside Triangle.
161224
*/
162225
template<typename Node,
163226
typename T = std::decay_t<decltype(*std::begin(std::declval<Node>()))>,
164227
typename = std::enable_if_t<std::is_scalar_v<T>>
165228
>
166-
static constexpr bool isPointInTriangle(const Node& p0, const Node& n0, const Node& n1, const Node& n2, sofa::type::Vec<3, T>& baryCoefs)
229+
[[nodiscard]]
230+
static constexpr bool isPointInTriangle(const Node& p0, const Node& n0, const Node& n1, const Node& n2, sofa::type::Vec<3, T>& baryCoefs, bool assumePointIsOnPlane = true)
167231
{
232+
// In 3D, check if the point is in the plane of the triangle
233+
if constexpr (std::is_same_v<Node, sofa::type::Vec<3, T>>)
234+
{
235+
if(!assumePointIsOnPlane)
236+
{
237+
if(!isPointOnPlane(p0, n0, n1, n2))
238+
{
239+
baryCoefs = {static_cast<T>(-1), static_cast<T>(-1), static_cast<T>(-1)};
240+
return false;
241+
}
242+
}
243+
}
244+
else
245+
{
246+
SOFA_UNUSED(assumePointIsOnPlane);
247+
}
248+
168249
baryCoefs = Triangle::getBarycentricCoordinates(p0, n0, n1, n2);
169250

170251
for (int i = 0; i < 3; ++i)

Sofa/framework/Geometry/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ set(SOURCE_FILES
88
Quad_test.cpp
99
Tetrahedron_test.cpp
1010
Hexahedron_test.cpp
11+
Proximity_test.cpp
1112
)
1213

1314
add_executable(${PROJECT_NAME} ${SOURCE_FILES})

Sofa/framework/Geometry/test/Edge_test.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,4 +492,67 @@ TEST(GeometryEdge_test, intersectionWithPlane3f)
492492
EXPECT_FLOAT_EQ(inter[2], 0.0f);
493493
}
494494

495+
TEST(GeometryEdge_test, closestPointWithEdge3f_perpendicular)
496+
{
497+
// Two perpendicular edges crossing at (1,1,1)
498+
const sofa::type::Vec3f pA{ 0.f, 0.f, 0.f };
499+
const sofa::type::Vec3f pB{ 2.f, 2.f, 2.f };
500+
const sofa::type::Vec3f pC{ 0.f, 0.f, 2.f };
501+
const sofa::type::Vec3f pD{ 2.f, 2.f, 0.f };
502+
503+
sofa::type::Vec<2, float> baryCoord(sofa::type::NOINIT);
504+
sofa::geometry::Edge::closestPointWithEdge(pA, pB, pC, pD, baryCoord);
505+
506+
// alpha and beta should both be 0.5 (midpoint of each edge)
507+
EXPECT_NEAR(baryCoord[0], 0.5f, 1e-4);
508+
EXPECT_NEAR(baryCoord[1], 0.5f, 1e-4);
509+
510+
// Verify closest points
511+
const auto pX = pA + baryCoord[0] * (pB - pA);
512+
const auto pY = pC + baryCoord[1] * (pD - pC);
513+
EXPECT_NEAR(pX[0], 1.f, 1e-4);
514+
EXPECT_NEAR(pX[1], 1.f, 1e-4);
515+
EXPECT_NEAR(pX[2], 1.f, 1e-4);
516+
EXPECT_NEAR(pY[0], 1.f, 1e-4);
517+
EXPECT_NEAR(pY[1], 1.f, 1e-4);
518+
EXPECT_NEAR(pY[2], 1.f, 1e-4);
519+
}
520+
521+
TEST(GeometryEdge_test, closestPointWithEdge3f_parallel)
522+
{
523+
// Two parallel edges offset in z
524+
const sofa::type::Vec3f pA{ 0.f, 0.f, 0.f };
525+
const sofa::type::Vec3f pB{ 2.f, 0.f, 0.f };
526+
const sofa::type::Vec3f pC{ 0.f, 0.f, 1.f };
527+
const sofa::type::Vec3f pD{ 2.f, 0.f, 1.f };
528+
529+
sofa::type::Vec<2, float> baryCoord(sofa::type::NOINIT);
530+
sofa::geometry::Edge::closestPointWithEdge(pA, pB, pC, pD, baryCoord);
531+
532+
// Parallel/collinear case: denominator is near zero, should return (0,0)
533+
EXPECT_FLOAT_EQ(baryCoord[0], 0.f);
534+
EXPECT_FLOAT_EQ(baryCoord[1], 0.f);
535+
}
536+
537+
TEST(GeometryEdge_test, closestPointWithEdge3f_skew)
538+
{
539+
// Two skew edges (not intersecting, not parallel)
540+
const sofa::type::Vec3f pA{ 0.f, 0.f, 0.f };
541+
const sofa::type::Vec3f pB{ 2.f, 0.f, 0.f };
542+
const sofa::type::Vec3f pC{ 1.f, 1.f, -1.f };
543+
const sofa::type::Vec3f pD{ 1.f, 1.f, 1.f };
544+
545+
sofa::type::Vec<2, float> baryCoord(sofa::type::NOINIT);
546+
sofa::geometry::Edge::closestPointWithEdge(pA, pB, pC, pD, baryCoord);
547+
548+
// Closest point on edge1 is at alpha=0.5 (x=1), on edge2 at beta=0.5 (z=0)
549+
EXPECT_NEAR(baryCoord[0], 0.5f, 1e-4);
550+
EXPECT_NEAR(baryCoord[1], 0.5f, 1e-4);
551+
552+
const auto pX = pA + baryCoord[0] * (pB - pA);
553+
const auto pY = pC + baryCoord[1] * (pD - pC);
554+
EXPECT_NEAR(pX[0], 1.f, 1e-4);
555+
EXPECT_NEAR(pY[2], 0.f, 1e-4);
556+
}
557+
495558
}// namespace sofa

0 commit comments

Comments
 (0)