Skip to content

Commit fca3e28

Browse files
committed
Ellipse: areaLineIntegral() added unit tests
1 parent 1cb791d commit fca3e28

2 files changed

Lines changed: 322 additions & 34 deletions

File tree

librecad/src/lib/engine/document/entities/rs_ellipse.cpp

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,24 +1732,26 @@ LC_Quadratic RS_Ellipse::getQuadratic() const
17321732
* \oint x dy = Cx y + \frac{1}{4}((a^{2}+b^{2})sin(2a)cos^{2}(t)-ab(2sin^{2}(a)sin(2t)-2t-sin(2t)))
17331733
*@author Dongxu Li
17341734
*/
1735-
double RS_Ellipse::areaLineIntegral() const{
1736-
const double a=getMajorRadius();
1737-
const double b=getMinorRadius();
1738-
if(!isEllipticArc())
1739-
return M_PI*a*b;
1740-
const double ab=a*b;
1741-
const double r2=a*a+b*b;
1742-
const double& cx=data.center.x;
1743-
const double aE=getAngle();
1744-
const double& a0=data.angle1;
1745-
const double& a1=data.angle2;
1746-
const double fStart=cx*getStartpoint().y+0.25*r2*sin(2.*aE)*cos(a0)*cos(a0)-0.25*ab*(2.*sin(aE)*sin(aE)*sin(2.*a0)-sin(2.*a0));
1747-
const double fEnd=cx*getEndpoint().y+0.25*r2*sin(2.*aE)*cos(a1)*cos(a1)-0.25*ab*(2.*sin(aE)*sin(aE)*sin(2.*a1)-sin(2.*a1));
1748-
if (isReversed()) {
1749-
return fEnd-fStart - 0.5 * a * b * getAngleLength();
1750-
} else {
1751-
return fEnd-fStart + 0.5 * a * b * getAngleLength();
1752-
}
1735+
double RS_Ellipse::areaLineIntegral() const {
1736+
const double a = getMajorRadius();
1737+
const double b = getMinorRadius();
1738+
if (!isEllipticArc())
1739+
return M_PI * a * b;
1740+
const double ab = a * b;
1741+
const double r2 = a * a + b * b;
1742+
const double& cx = data.center.x;
1743+
const double aE = getAngle();
1744+
const double y_start = getStartpoint().y;
1745+
const double y_end = getEndpoint().y;
1746+
const double start_angle = data.angle1;
1747+
const double end_angle = isReversed() ? start_angle - getAngleLength() : start_angle + getAngleLength();
1748+
const double aE2 = aE + aE;
1749+
const auto antiDerivative = [&cx, c1 = r2 * std::sin(aE2), c2 = std::cos(aE2), &ab](double t, double y) {
1750+
const double t2 = t + t;
1751+
return cx * y + (c1 + c1 * std::cos(t2) + 2. * ab * ( t2 + c2 * std::sin(t2))) / 8.;
1752+
};
1753+
1754+
return antiDerivative(end_angle, y_end) - antiDerivative(start_angle, y_start);
17531755
}
17541756

17551757
bool RS_Ellipse::isReversed() const {
@@ -1761,7 +1763,7 @@ void RS_Ellipse::setReversed(bool r) {
17611763
}
17621764

17631765
double RS_Ellipse::getAngle() const {
1764-
return data.majorP.angle();
1766+
return data.majorP.angle();
17651767
}
17661768

17671769
double RS_Ellipse::getAngle1() const {

librecad/src/lib/engine/document/entities/tests/rs_ellipse_tests.cpp

Lines changed: 301 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
/****************************************************************************
42
**
53
** This file is part of the LibreCAD project, a 2D CAD program
@@ -30,6 +28,7 @@
3028
#include <catch2/matchers/catch_matchers_floating_point.hpp>
3129

3230
#include "rs_ellipse.h"
31+
#include "rs_line.h"
3332
#include "rs_vector.h"
3433
#include "rs_math.h" // For M_PI if needed
3534

@@ -127,21 +126,308 @@ TEST_CASE("RS_Ellipse::getNearestPointOnEntity") {
127126
REQUIRE(nearest.valid == true);
128127
}
129128

130-
TEST_CASE("RS_Ellipse::areaLineIntegral") {
131-
RS_Ellipse ellipse(nullptr, {RS_Vector(0,0), RS_Vector(5,0), 0.5, 0, 2*M_PI, false}); // Full ellipse
132-
REQUIRE(std::abs(ellipse.areaLineIntegral() - (M_PI * 5.0 * 2.5)) < EPS); // Area = pi * a * b
133129

134-
RS_Ellipse halfEllipse(nullptr, {RS_Vector(0,0), RS_Vector(5,0), 0.5, 0, M_PI, false}); // Half ellipse
135-
REQUIRE(std::abs(halfEllipse.areaLineIntegral() - (M_PI * 5.0 * 2.5 / 2)) < EPS);
130+
131+
using namespace Catch;
132+
133+
TEST_CASE("RS_Ellipse::areaLineIntegral()", "[rs_ellipse]") {
134+
double tol = 1e-8;
135+
136+
SECTION("Full ellipse, center (0,0), ratio 1.0, rotation 0") {
137+
RS_Vector center(0.0, 0.0);
138+
RS_Vector majorP(5.0, 0.0);
139+
double ratio = 1.0;
140+
double angle1 = 0.0;
141+
double angle2 = 0.0;
142+
bool reversed = false;
143+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
144+
RS_Ellipse ellipse(nullptr, data);
145+
double result = ellipse.areaLineIntegral();
146+
double expected = M_PI * 5.0 * 5.0;
147+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
148+
}
149+
150+
SECTION("Full ellipse, center (3,4), ratio 0.5, rotation pi/4") {
151+
RS_Vector center(3.0, 4.0);
152+
double a = 2.0;
153+
double alpha = M_PI / 4.0;
154+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
155+
double ratio = 0.5;
156+
double angle1 = 0.0;
157+
double angle2 = 0.0;
158+
bool reversed = false;
159+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
160+
RS_Ellipse ellipse(nullptr, data);
161+
double result = ellipse.areaLineIntegral();
162+
double expected = M_PI * 2.0 * 1.0;
163+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
164+
}
165+
166+
SECTION("Arc non-reversed, ratio 0.5, angles 0 to pi/2, rotation pi/4") {
167+
RS_Vector center(3.0, 4.0);
168+
double a = 2.0;
169+
double alpha = M_PI / 4.0;
170+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
171+
double ratio = 0.5;
172+
double angle1 = 0.0;
173+
double angle2 = M_PI / 2.0;
174+
bool reversed = false;
175+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
176+
RS_Ellipse ellipse(nullptr, data);
177+
double result = ellipse.areaLineIntegral();
178+
double expected = -1.8005240167647454;
179+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
180+
}
181+
182+
SECTION("Arc reversed, ratio 0.5, angles 0 to pi/2, rotation pi/4") {
183+
RS_Vector center(3.0, 4.0);
184+
double a = 2.0;
185+
double alpha = M_PI / 4.0;
186+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
187+
double ratio = 0.5;
188+
double angle1 = 0.0;
189+
double angle2 = M_PI / 2.0;
190+
bool reversed = true;
191+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
192+
RS_Ellipse ellipse(nullptr, data);
193+
double result = ellipse.areaLineIntegral();
194+
double expected = -8.083709323944333;
195+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
196+
}
197+
198+
SECTION("Arc non-reversed, ratio 0.5, angles pi/4 to 3pi/4, rotation pi/4") {
199+
RS_Vector center(3.0, 4.0);
200+
double a = 2.0;
201+
double alpha = M_PI / 4.0;
202+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
203+
double ratio = 0.5;
204+
double angle1 = M_PI / 4.0;
205+
double angle2 = 3.0 * M_PI / 4.0;
206+
bool reversed = false;
207+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
208+
RS_Ellipse ellipse(nullptr, data);
209+
double result = ellipse.areaLineIntegral();
210+
double expected = -4.4292036732051026;
211+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
212+
}
213+
214+
SECTION("Arc non-reversed, ratio 2.0, angles 0 to pi/2, rotation pi/4") {
215+
RS_Vector center(3.0, 4.0);
216+
double a = 2.0;
217+
double alpha = M_PI / 4.0;
218+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
219+
double ratio = 2.0;
220+
double angle1 = 0.0;
221+
double angle2 = M_PI / 2.0;
222+
bool reversed = false;
223+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
224+
RS_Ellipse ellipse(nullptr, data);
225+
double result = ellipse.areaLineIntegral();
226+
double expected = 5.525825994298873;
227+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
228+
}
229+
230+
SECTION("Arc reversed, ratio 2.0, angles pi/4 to 3pi/4, rotation pi/6") {
231+
RS_Vector center(3.0, 4.0);
232+
double a = 2.0;
233+
double alpha = M_PI / 6.0;
234+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
235+
double ratio = 2.0;
236+
double angle1 = M_PI / 4.0;
237+
double angle2 = 3.0 * M_PI / 4.0;
238+
bool reversed = true;
239+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
240+
RS_Ellipse ellipse(nullptr, data);
241+
double result = ellipse.areaLineIntegral();
242+
double expected = -25.092196608658043;
243+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
244+
}
245+
246+
SECTION("Arc non-reversed, ratio 0.75, angles 0 to pi, rotation 0, center (0,0)") {
247+
RS_Vector center(0.0, 0.0);
248+
double a = 2.0;
249+
double alpha = 0.0;
250+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
251+
double ratio = 0.75;
252+
double angle1 = 0.0;
253+
double angle2 = M_PI;
254+
bool reversed = false;
255+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
256+
RS_Ellipse ellipse(nullptr, data);
257+
double result = ellipse.areaLineIntegral();
258+
double expected = 4.71238898038469;
259+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
260+
}
261+
262+
SECTION("Arc reversed, ratio 0.75, angles 0 to pi, rotation 0, center (0,0)") {
263+
RS_Vector center(0.0, 0.0);
264+
double a = 2.0;
265+
double alpha = 0.0;
266+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
267+
double ratio = 0.75;
268+
double angle1 = 0.0;
269+
double angle2 = M_PI;
270+
bool reversed = true;
271+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
272+
RS_Ellipse ellipse(nullptr, data);
273+
double result = ellipse.areaLineIntegral();
274+
double expected = -4.71238898038469;
275+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
276+
}
277+
278+
SECTION("Arc non-reversed, ratio 1.5, angles pi/2 to 3pi/2, rotation pi/3, center (3,4)") {
279+
RS_Vector center(3.0, 4.0);
280+
double a = 2.0;
281+
double alpha = M_PI / 3.0;
282+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
283+
double ratio = 1.5;
284+
double angle1 = M_PI / 2.0;
285+
double angle2 = 3.0 * M_PI / 2.0;
286+
bool reversed = false;
287+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
288+
RS_Ellipse ellipse(nullptr, data);
289+
double result = ellipse.areaLineIntegral();
290+
double expected = 0.4247779607693788;
291+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
292+
}
293+
294+
SECTION("Arc reversed, ratio 1.5, angles pi/2 to 3pi/2, rotation pi/3, center (3,4)") {
295+
RS_Vector center(3.0, 4.0);
296+
double a = 2.0;
297+
double alpha = M_PI / 3.0;
298+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
299+
double ratio = 1.5;
300+
double angle1 = M_PI / 2.0;
301+
double angle2 = 3.0 * M_PI / 2.0;
302+
bool reversed = true;
303+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
304+
RS_Ellipse ellipse(nullptr, data);
305+
double result = ellipse.areaLineIntegral();
306+
double expected = -18.42477796076938;
307+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
308+
}
309+
310+
SECTION("Arc non-reversed, ratio 0.5, angles 7pi/4 to pi/4 (crossing 0), rotation pi/2, center (0,0)") {
311+
RS_Vector center(0.0, 0.0);
312+
double a = 2.0;
313+
double alpha = M_PI / 2.0;
314+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
315+
double ratio = 0.5;
316+
double angle1 = 7.0 * M_PI / 4.0;
317+
double angle2 = M_PI / 4.0;
318+
bool reversed = false;
319+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
320+
RS_Ellipse ellipse(nullptr, data);
321+
double result = ellipse.areaLineIntegral();
322+
double expected = 0.5707963267948966;
323+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
324+
}
325+
326+
SECTION("Arc reversed, ratio 0.5, angles 7pi/4 to pi/4 (crossing 0), rotation pi/2, center (0,0)") {
327+
RS_Vector center(0.0, 0.0);
328+
double a = 2.0;
329+
double alpha = M_PI / 2.0;
330+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
331+
double ratio = 0.5;
332+
double angle1 = 7.0 * M_PI / 4.0;
333+
double angle2 = M_PI / 4.0;
334+
bool reversed = true;
335+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
336+
RS_Ellipse ellipse(nullptr, data);
337+
double result = ellipse.areaLineIntegral();
338+
double expected = -5.71238898038469;
339+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
340+
}
341+
342+
SECTION("Arc non-reversed, ratio 1.0, angles 0 to 3pi/2, rotation 0, center (3,4)") {
343+
RS_Vector center(3.0, 4.0);
344+
double a = 2.0;
345+
double alpha = 0.0;
346+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
347+
double ratio = 1.0;
348+
double angle1 = 0.0;
349+
double angle2 = 3.0 * M_PI / 2.0;
350+
bool reversed = false;
351+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
352+
RS_Ellipse ellipse(nullptr, data);
353+
double result = ellipse.areaLineIntegral();
354+
double expected = 3.4247779607693793;
355+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
356+
}
357+
358+
SECTION("Arc reversed, ratio 1.0, angles 0 to 3pi/2, rotation 0, center (3,4)") {
359+
RS_Vector center(3.0, 4.0);
360+
double a = 2.0;
361+
double alpha = 0.0;
362+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
363+
double ratio = 1.0;
364+
double angle1 = 0.0;
365+
double angle2 = 3.0 * M_PI / 2.0;
366+
bool reversed = true;
367+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
368+
RS_Ellipse ellipse(nullptr, data);
369+
double result = ellipse.areaLineIntegral();
370+
double expected = -9.14159265358979312;
371+
REQUIRE_THAT(result, Matchers::WithinAbs(expected, tol));
372+
}
136373
}
137374

138-
TEST_CASE("RS_Ellipse::switchMajorMinor") {
139-
RS_Ellipse ellipse(nullptr, {RS_Vector(0,0), RS_Vector(5,0), 0.5, 0, 2*M_PI, false});
140-
REQUIRE(ellipse.switchMajorMinor() == true);
141-
REQUIRE(ellipse.getMajorP() == RS_Vector(0.0, 2.5)); // Switched to vertical major
142-
REQUIRE(std::abs(ellipse.getRatio() - 2.0) < EPS); // Inverse ratio 1/0.5 = 2
143-
REQUIRE(ellipse.switchMajorMinor()); // Switch back
144-
REQUIRE(ellipse.getMajorP().distanceTo(RS_Vector(-5.0, 0.0)) < EPS);
145-
REQUIRE(std::abs(ellipse.getRatio() - 0.5) < EPS);
375+
TEST_CASE("Elliptic arc segment area", "[rs_ellipse]") {
376+
double tol = 1e-8;
377+
378+
SECTION("Arc non-reversed, ratio 0.5, angles 0 to pi/2, rotation 0, center 0") {
379+
RS_Vector center(0.0, 0.0);
380+
double a = 2.0;
381+
double alpha = 0.0;
382+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
383+
double ratio = 0.5;
384+
double angle1 = 0.0;
385+
double angle2 = M_PI / 2.0;
386+
bool reversed = false;
387+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
388+
RS_Ellipse ellipse(nullptr, data);
389+
390+
double theta = ellipse.getAngleLength();
391+
double sector = 0.5 * a * (a * ratio) * theta; // 0.5 a b theta
392+
RS_Vector p1 = ellipse.getStartpoint(); // (2,0)
393+
RS_Vector p2 = ellipse.getEndpoint(); // (0,1)
394+
double triangle = 0.5 * (p1.x * p2.y - p2.x * p1.y); // Signed area
395+
double segment = sector - triangle;
396+
double expected = M_PI / 2.0 - 1.0; // ~0.5708
397+
REQUIRE_THAT(segment, Matchers::WithinAbs(expected, tol));
398+
}
399+
400+
SECTION("Arc reversed, ratio 2.5, angles pi/6 to 3pi/7, rotation pi/5, center (3,4)") {
401+
RS_Vector center(3.0, 4.0);
402+
double a = 2.0;
403+
double alpha = M_PI / 5.0;
404+
RS_Vector majorP(a * cos(alpha), a * sin(alpha));
405+
double ratio = 2.5;
406+
double angle1 = M_PI / 6.0;
407+
double angle2 = 3.0 * M_PI / 7.0;
408+
bool reversed = true;
409+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
410+
RS_Ellipse ellipse(nullptr, data);
411+
412+
double segment = ellipse.areaLineIntegral();
413+
double expected = -28.9718193637963;
414+
REQUIRE_THAT(segment, Matchers::WithinAbs(expected, tol));
415+
}
416+
417+
SECTION("Arc non-reversed, center (3,4), majorP=(2,1), ratio 0.75, angles pi/6 to 0.8pi, rotation from majorP; line connecting begin/end") {
418+
RS_Vector center(3.0, 4.0);
419+
RS_Vector majorP(2.0, 1.0);
420+
double ratio = 0.75;
421+
double angle1 = M_PI / 6.0;
422+
double angle2 = 0.8 * M_PI;
423+
bool reversed = false;
424+
RS_EllipseData data{center, majorP, ratio, angle1, angle2, reversed};
425+
RS_Ellipse ellipse(nullptr, data);
426+
427+
RS_Line line{nullptr, RS_LineData{ellipse.getEndpoint(), ellipse.getStartpoint()}};
146428

429+
double area = ellipse.areaLineIntegral() + line.areaLineIntegral();
430+
double expected = 2.0177435430580033;
431+
REQUIRE_THAT(area, Matchers::WithinAbs(expected, tol));
432+
}
147433
}

0 commit comments

Comments
 (0)