KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_shape_ellipse.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25
26#include <cmath>
27#include <limits>
28#include <chrono>
29#include <random>
30#include <stdexcept>
31
32#include <geometry/eda_angle.h>
36#include <geometry/shape_rect.h>
37#include <geometry/shape_arc.h>
38
39BOOST_AUTO_TEST_SUITE( ShapeEllipse )
40
41
42
46static BOX2I bruteForceEllipseBBox( const VECTOR2I& aCenter, int aMajorR, int aMinorR, const EDA_ANGLE& aRotation,
47 const EDA_ANGLE& aStartAngle, const EDA_ANGLE& aEndAngle, bool aIsArc,
48 int aNSamples = 10000 )
49{
50 const double a = aMajorR;
51 const double b = aMinorR;
52 const double cp = std::cos( aRotation.AsRadians() );
53 const double sp = std::sin( aRotation.AsRadians() );
54
55 double tStart = aIsArc ? aStartAngle.AsRadians() : 0.0;
56 double tEnd = aIsArc ? aEndAngle.AsRadians() : 2.0 * M_PI;
57
58 if( tEnd < tStart )
59 tEnd += 2.0 * M_PI;
60
61 double minX = std::numeric_limits<double>::max();
62 double maxX = std::numeric_limits<double>::lowest();
63 double minY = std::numeric_limits<double>::max();
64 double maxY = std::numeric_limits<double>::lowest();
65
66 for( int i = 0; i <= aNSamples; ++i )
67 {
68 const double t = tStart + ( tEnd - tStart ) * i / aNSamples;
69 const double ct = std::cos( t );
70 const double st = std::sin( t );
71 const double x = aCenter.x + a * ct * cp - b * st * sp;
72 const double y = aCenter.y + a * ct * sp + b * st * cp;
73
74 minX = std::min( minX, x );
75 maxX = std::max( maxX, x );
76 minY = std::min( minY, y );
77 maxY = std::max( maxY, y );
78 }
79
80 const int ix = static_cast<int>( std::floor( minX ) );
81 const int iy = static_cast<int>( std::floor( minY ) );
82 const int ex = static_cast<int>( std::ceil( maxX ) );
83 const int ey = static_cast<int>( std::ceil( maxY ) );
84
85 return BOX2I( VECTOR2I( ix, iy ), VECTOR2I( ex - ix, ey - iy ) );
86}
87
88
89BOOST_AUTO_TEST_CASE( ConstructorSwapsMajorMinor )
90{
91 // Pass major=100, minor=300 (wrong order). Constructor should swap them
92 // to major=300, minor=100 and add 90° to rotation
93 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 100, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
94
97 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), 90.0, 1e-6 );
98}
99
100
101BOOST_AUTO_TEST_CASE( ConstructorClampsDegenerate )
102{
103 // Zero/negative radii are clamped to 1 IU
104 SHAPE_ELLIPSE e1( VECTOR2I( 0, 0 ), 0, 100, EDA_ANGLE( 0, DEGREES_T ) );
105 BOOST_CHECK_GE( e1.GetMajorRadius(), 1 );
106 BOOST_CHECK_GE( e1.GetMinorRadius(), 1 );
107
108 SHAPE_ELLIPSE e2( VECTOR2I( 0, 0 ), 100, 0, EDA_ANGLE( 0, DEGREES_T ) );
109 BOOST_CHECK_GE( e2.GetMajorRadius(), 1 );
110 BOOST_CHECK_GE( e2.GetMinorRadius(), 1 );
111
112 SHAPE_ELLIPSE e3( VECTOR2I( 0, 0 ), -10, 100, EDA_ANGLE( 0, DEGREES_T ) );
113 BOOST_CHECK_GE( e3.GetMajorRadius(), 1 );
114 BOOST_CHECK_GE( e3.GetMinorRadius(), 1 );
115}
116
117
118BOOST_AUTO_TEST_CASE( AxisAlignedEllipseBBox )
119{
120 // Axis-aligned ellipse at (1000, 2000) with major=500 along X, minor=300 along Y.
121 // No rotation, so the bbox is center.x +/- 500 and center.y +/- 300.
122 SHAPE_ELLIPSE e( VECTOR2I( 1000, 2000 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
123 BOX2I bbox = e.BBox();
124
125 BOOST_CHECK_EQUAL( bbox.GetLeft(), 500 );
126 BOOST_CHECK_EQUAL( bbox.GetRight(), 1500 );
127 BOOST_CHECK_EQUAL( bbox.GetTop(), 1700 );
128 BOOST_CHECK_EQUAL( bbox.GetBottom(), 2300 );
129}
130
131
132BOOST_AUTO_TEST_CASE( RotatedEllipseBBoxAt45Degrees )
133{
134 // At 45 degree rotation, the ellipse extends equally in both directions. The
135 // half extent on each axis is sqrt((a^2 + b^2) / 2).
136 const int a = 500;
137 const int b = 300;
138 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), a, b, EDA_ANGLE( 45.0, DEGREES_T ) );
139 BOX2I bbox = e.BBox();
140
141 const double expected = std::sqrt( ( double( a ) * a + double( b ) * b ) / 2.0 );
142 BOOST_CHECK_LE( std::abs( bbox.GetWidth() / 2.0 - expected ), 2.0 );
143 BOOST_CHECK_LE( std::abs( bbox.GetHeight() / 2.0 - expected ), 2.0 );
144}
145
146
147BOOST_AUTO_TEST_CASE( RandomClosedEllipseBBoxVsBruteForce )
148{
149 // Generate 1000 random closed ellipses with varying centers, radii, and
150 // rotations. Compare the analytical BBox against the brute-force sampled
151 // bbox. They must agree within +/- 2 IU (integer rounding tolerance).
152 std::mt19937 rng( 42 );
153 std::uniform_int_distribution<int> centerDist( -10000, 10000 );
154 std::uniform_int_distribution<int> radiusDist( 50, 2000 );
155 std::uniform_real_distribution<double> angleDist( 0.0, 360.0 );
156
157 const int N = 1000;
158 for( int i = 0; i < N; ++i )
159 {
160 const VECTOR2I center( centerDist( rng ), centerDist( rng ) );
161 int r1 = radiusDist( rng );
162 int r2 = radiusDist( rng );
163
164 if( r1 < r2 )
165 std::swap( r1, r2 );
166
167 const EDA_ANGLE rot( angleDist( rng ), DEGREES_T );
168
169 SHAPE_ELLIPSE e( center, r1, r2, rot );
170 BOX2I analytic = e.BBox();
171 BOX2I brute = bruteForceEllipseBBox( center, r1, r2, rot, EDA_ANGLE( 0, DEGREES_T ),
172 EDA_ANGLE( 360, DEGREES_T ), false );
173
174 // Allow +/- 2 IU for integer rounding on either side.
175 BOOST_CHECK_LE( std::abs( analytic.GetLeft() - brute.GetLeft() ), 2 );
176 BOOST_CHECK_LE( std::abs( analytic.GetRight() - brute.GetRight() ), 2 );
177 BOOST_CHECK_LE( std::abs( analytic.GetTop() - brute.GetTop() ), 2 );
178 BOOST_CHECK_LE( std::abs( analytic.GetBottom() - brute.GetBottom() ), 2 );
179 }
180}
181
182
183BOOST_AUTO_TEST_CASE( RandomEllipticalArcBBoxVsBruteForce )
184{
185 // 1000 random elliptical arcs with varying start angles and sweep lengths.
186 // The arc bbox logic has to check whether each axis extremum falls inside
187 // the angular range
188 std::mt19937 rng( 1337 );
189 std::uniform_int_distribution<int> centerDist( -10000, 10000 );
190 std::uniform_int_distribution<int> radiusDist( 50, 2000 );
191 std::uniform_real_distribution<double> angleDist( 0.0, 360.0 );
192 std::uniform_real_distribution<double> sweepDist( 10.0, 350.0 );
193
194 const int N = 1000;
195 int failures = 0;
196
197 for( int i = 0; i < N; ++i )
198 {
199 const VECTOR2I center( centerDist( rng ), centerDist( rng ) );
200 int r1 = radiusDist( rng );
201 int r2 = radiusDist( rng );
202
203 if( r1 < r2 )
204 std::swap( r1, r2 );
205
206 const EDA_ANGLE rot( angleDist( rng ), DEGREES_T );
207 const EDA_ANGLE start( angleDist( rng ), DEGREES_T );
208 const EDA_ANGLE end( start.AsDegrees() + sweepDist( rng ), DEGREES_T );
209
210 SHAPE_ELLIPSE e( center, r1, r2, rot, start, end );
211 BOX2I analytic = e.BBox();
212 BOX2I brute = bruteForceEllipseBBox( center, r1, r2, rot, start, end, true );
213
214 if( std::abs( analytic.GetLeft() - brute.GetLeft() ) > 2
215 || std::abs( analytic.GetRight() - brute.GetRight() ) > 2
216 || std::abs( analytic.GetTop() - brute.GetTop() ) > 2
217 || std::abs( analytic.GetBottom() - brute.GetBottom() ) > 2 )
218 {
219 ++failures;
220 }
221 }
222
223 BOOST_CHECK_EQUAL( failures, 0 );
224}
225
226
227BOOST_AUTO_TEST_CASE( CirclePerimeterViaRamanujan )
228{
229 // When both radii are equal it's a circle. Ramanujan's formula should
230 // give exactly 2πr in that case.
231 const int r = 1000;
232 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), r, r, EDA_ANGLE( 0.0, DEGREES_T ) );
233 BOOST_CHECK_CLOSE( e.GetLength(), 2.0 * M_PI * r, 1e-9 );
234}
235
236
237BOOST_AUTO_TEST_CASE( EllipsePerimeterMatchesHighResIntegration )
238{
239 // Cross-check Ramanujan's approximation against a high-resolution numerical
240 // integration (200,000 steps) for a 2:1 ellipse. They should agree to
241 // within 10 parts per million
242 const double a = 1000.0;
243 const double b = 500.0;
244
245 const int N = 200000;
246 double sum = 0.0;
247 const double h = 2.0 * M_PI / N;
248
249 for( int i = 0; i < N; ++i )
250 {
251 const double t0 = i * h;
252 const double t1 = ( i + 1 ) * h;
253 auto f = [a, b]( double t )
254 {
255 const double s = std::sin( t ), c = std::cos( t );
256 return std::sqrt( a * a * s * s + b * b * c * c );
257 };
258 sum += ( t1 - t0 ) * ( f( t0 ) + 4 * f( 0.5 * ( t0 + t1 ) ) + f( t1 ) ) / 6.0;
259 }
260
261 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), static_cast<int>( a ), static_cast<int>( b ), EDA_ANGLE( 30.0, DEGREES_T ) );
262
263 BOOST_CHECK_CLOSE( e.GetLength(), sum, 1e-4 );
264}
265
266
267BOOST_AUTO_TEST_CASE( SemiCircleArcLength )
268{
269 // Semicircular arc of radius 1000. Length should be 1000 * pi.
270 const int r = 1000;
271 SHAPE_ELLIPSE arc( VECTOR2I( 0, 0 ), r, r, EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 0.0, DEGREES_T ),
272 EDA_ANGLE( 180.0, DEGREES_T ) );
273 BOOST_CHECK_CLOSE( arc.GetLength(), M_PI * r, 1e-6 );
274}
275
276
277BOOST_AUTO_TEST_CASE( QuarterCircleArcLength )
278{
279 // Quarter of a circle (equal radii, 0 degrees to 90 degrees). Arc length must be pi * r/2.
280 const int r = 1000;
281 SHAPE_ELLIPSE arc( VECTOR2I( 0, 0 ), r, r, EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 0.0, DEGREES_T ),
282 EDA_ANGLE( 90.0, DEGREES_T ) );
283 BOOST_CHECK_CLOSE( arc.GetLength(), 0.5 * M_PI * r, 1e-6 );
284}
285
286
287BOOST_AUTO_TEST_CASE( PointInsideClosedAxisAligned )
288{
289 // Check points inside, outside, and on the boundary of an unrotated ellipse.
290 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
291
292 BOOST_CHECK( e.PointInside( VECTOR2I( 0, 0 ) ) ); // center
293 BOOST_CHECK( e.PointInside( VECTOR2I( 400, 0 ) ) ); // inside on major
294 BOOST_CHECK( e.PointInside( VECTOR2I( 0, 200 ) ) ); // inside on minor
295 BOOST_CHECK( !e.PointInside( VECTOR2I( 501, 0 ) ) ); // just outside
296 BOOST_CHECK( !e.PointInside( VECTOR2I( 0, 301 ) ) ); // just outside
297 BOOST_CHECK( !e.PointInside( VECTOR2I( 500, 300 ) ) ); // bbox corner, outside
298}
299
300
301BOOST_AUTO_TEST_CASE( PointInsideRotatedEllipse )
302{
303 // Ellipse rotated 90 degrees. Major axis runs along Y.
304 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 90.0, DEGREES_T ) );
305
306 BOOST_CHECK( e.PointInside( VECTOR2I( 0, 499 ) ) ); // now tall on Y
307 BOOST_CHECK( e.PointInside( VECTOR2I( 299, 0 ) ) ); // now short on X
308 BOOST_CHECK( !e.PointInside( VECTOR2I( 0, 501 ) ) );
309 BOOST_CHECK( !e.PointInside( VECTOR2I( 301, 0 ) ) );
310}
311
312
313BOOST_AUTO_TEST_CASE( PointInsideArcAlwaysFalse )
314{
315 // Arcs are open curves with no interior. PointInside must always return false.
316 SHAPE_ELLIPSE arc( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 0.0, DEGREES_T ),
317 EDA_ANGLE( 180.0, DEGREES_T ) );
318 BOOST_CHECK( !arc.PointInside( VECTOR2I( 0, 0 ) ) );
319 BOOST_CHECK( !arc.PointInside( VECTOR2I( 100, 50 ) ) );
320}
321
322
323BOOST_AUTO_TEST_CASE( SquaredDistanceCircleAgreesWithRadialDistance )
324{
325 // For a circle (equal radii), distance from a point to the boundary is
326 // |distance_to_center - radius|. Verify SquaredDistance agrees.
327 const int r = 1000;
328 SHAPE_ELLIPSE c( VECTOR2I( 0, 0 ), r, r, EDA_ANGLE( 0.0, DEGREES_T ) );
329
330 const VECTOR2I p( 2000, 0 );
331 const double expected = 1000.0; // |2000−1000|
332 const double got = std::sqrt( static_cast<double>( c.SquaredDistance( p, true ) ) );
333 BOOST_CHECK_LE( std::abs( got - expected ), 1.0 );
334}
335
336
337BOOST_AUTO_TEST_CASE( SquaredDistanceZeroInsideClosedEllipse )
338{
339 // Point inside the center of a closed ellipse means SquaredDistance should be 0
340 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 30.0, DEGREES_T ) );
341 BOOST_CHECK_EQUAL( e.SquaredDistance( VECTOR2I( 0, 0 ), false ), 0 );
342}
343
344
345BOOST_AUTO_TEST_CASE( SquaredDistanceOutlineOnlyReturnsBoundaryDist )
346{
347 // From the center, outline-only distance should be the minor radius (the
348 // closest point on the boundary).
349 const int a = 500, b = 300;
350 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), a, b, EDA_ANGLE( 0.0, DEGREES_T ) );
351
352 const double d = std::sqrt( static_cast<double>( e.SquaredDistance( VECTOR2I( 0, 0 ), true ) ) );
353 BOOST_CHECK_LE( std::abs( d - b ), 2.0 );
354}
355
356
357BOOST_AUTO_TEST_CASE( ConvertToPolylineClosedEllipseIsClosed )
358{
359 // A closed ellipse's polyline must be marked closed and have enough points
360 // to approximate the curve.
361 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
363 BOOST_CHECK( chain.IsClosed() );
364 BOOST_CHECK_GT( chain.PointCount(), 8 ); // at least 8 segments
365}
366
367
368BOOST_AUTO_TEST_CASE( ConvertToPolylineArcIsOpenWithCorrectEndpoints )
369{
370 // An arc's polyline must be open. First point should be at
371 // the start angle (1000, 0) and last point at the end angle (-1000, 0).
372 SHAPE_ELLIPSE arc( VECTOR2I( 0, 0 ), 1000, 500, EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 0.0, DEGREES_T ),
373 EDA_ANGLE( 180.0, DEGREES_T ) );
375
376 BOOST_CHECK( !chain.IsClosed() );
377 BOOST_CHECK_LE( ( chain.CPoint( 0 ) - VECTOR2I( 1000, 0 ) ).EuclideanNorm(), 2 );
378 BOOST_CHECK_LE( ( chain.CPoint( -1 ) - VECTOR2I( -1000, 0 ) ).EuclideanNorm(), 2 );
379}
380
381
382BOOST_AUTO_TEST_CASE( ConvertToPolylineAllPointsWithinMaxError )
383{
384 // Every tessellated point must lie on the true ellipse.
385 // Verify no point deviates more than maxError + rounding tolerance.
386 const int maxErr = 10;
387 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 2000, 800, EDA_ANGLE( 37.5, DEGREES_T ) );
388
390
391 int maxObserved = 0;
392 for( int i = 0; i < chain.PointCount(); ++i )
393 {
394 const double d = std::sqrt( static_cast<double>( e.SquaredDistance( chain.CPoint( i ), true ) ) );
395 maxObserved = std::max( maxObserved, static_cast<int>( std::ceil( d ) ) );
396 }
397 // Rounding adds up to ~1 IU per axis, so permit maxErr + 2.
398 BOOST_CHECK_LE( maxObserved, maxErr + 2 );
399}
400
401
402BOOST_AUTO_TEST_CASE( ConvertToPolylineTighterErrorYieldsMorePoints )
403{
404 // Tighter error tolerance must produce more points.
405 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 1000, 600, EDA_ANGLE( 0.0, DEGREES_T ) );
406
407 const int coarse = e.ConvertToPolyline( 50 ).PointCount();
408 const int fine = e.ConvertToPolyline( 5 ).PointCount();
409
410 BOOST_CHECK_GT( fine, coarse );
411}
412
413
414BOOST_AUTO_TEST_CASE( CollideSegmentThroughClosedEllipse )
415{
416 // Segment passes straight through the ellipse along the major axis.
417 // Collision with distance 0 (the segment crosses the boundary).
418 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
419 SEG s( VECTOR2I( -1000, 0 ), VECTOR2I( 1000, 0 ) );
420
421 int actual = -1;
422 BOOST_CHECK( e.Collide( s, 0, &actual, nullptr ) );
424}
425
426
427BOOST_AUTO_TEST_CASE( CollideSegmentEndpointInsideClosedEllipse )
428{
429 // One endpoint is at the center (inside the ellipse). Collision with
430 // distance 0 expected
431 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
432 SEG s( VECTOR2I( 0, 0 ), VECTOR2I( 2000, 0 ) );
433
434 int actual = -1;
435 BOOST_CHECK( e.Collide( s, 0, &actual, nullptr ) );
437}
438
439
440BOOST_AUTO_TEST_CASE( CollideSegmentOutsideFar )
441{
442 // Segment is far away from the ellipse. No collision even with 100 IU clearance.
443 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
444 SEG s( VECTOR2I( 10000, 10000 ), VECTOR2I( 20000, 20000 ) );
445
446 BOOST_CHECK( !e.Collide( s, 100, nullptr, nullptr ) );
447}
448
449
450BOOST_AUTO_TEST_CASE( CollideSegmentNearMissWithinClearance )
451{
452 // Segment runs 50 IU above the ellipse's top. With clearance 40 it's a miss,
453 // with clearance 60 it's a hit. Reported distance should be close to 50.
454 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
455 SEG s( VECTOR2I( -1000, 350 ), VECTOR2I( 1000, 350 ) );
456
457 BOOST_CHECK( !e.Collide( s, 40, nullptr, nullptr ) ); // 50 > 40
458
459 int actual = -1;
460 BOOST_CHECK( e.Collide( s, 60, &actual, nullptr ) ); // 50 < 60
461 BOOST_CHECK_LE( std::abs( actual - 50 ), 2 );
462}
463
464
465BOOST_AUTO_TEST_CASE( CollideDegenerateSegmentAtCenter )
466{
467 // Zero length segment at the center is treated as a point. Inside the
468 // ellipse, so collision with distance 0.
469 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
470 SEG s( VECTOR2I( 0, 0 ), VECTOR2I( 0, 0 ) );
471
472 BOOST_CHECK( e.Collide( s, 0, nullptr, nullptr ) );
473}
474
475
476BOOST_AUTO_TEST_CASE( CollideDegenerateSegmentFarAway )
477{
478 // Zero length segment far away. No collision.
479 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
480 SEG s( VECTOR2I( 5000, 5000 ), VECTOR2I( 5000, 5000 ) );
481
482 BOOST_CHECK( !e.Collide( s, 100, nullptr, nullptr ) );
483}
484
485
486BOOST_AUTO_TEST_CASE( CollideSegmentVsRotatedEllipse )
487{
488 // Ellipse rotated 90 degrees. Major axis now runs along Y. A vertical
489 // segment through center and a horizontal segment at y=350
490 // should collide
491 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 90.0, DEGREES_T ) );
492
493 SEG yAxis( VECTOR2I( 0, -1000 ), VECTOR2I( 0, 1000 ) );
494 BOOST_CHECK( e.Collide( yAxis, 0, nullptr, nullptr ) );
495
496 SEG crossing( VECTOR2I( -1000, 350 ), VECTOR2I( 1000, 350 ) );
497 BOOST_CHECK( e.Collide( crossing, 0, nullptr, nullptr ) );
498}
499
500
501BOOST_AUTO_TEST_CASE( CollideSegmentVsArcUpperHalf )
502{
503 // Upper-half arc (0 to 180 degrees). A vertical segment crossing the arc
504 // should collide. A horizontal segment in the lower half should miss the
505 // arc even though it would hit the full ellipse
506 SHAPE_ELLIPSE arc( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 0.0, DEGREES_T ),
507 EDA_ANGLE( 180.0, DEGREES_T ) );
508
509 // Vertical segment at x=300 crosses the arc around (300, 240).
510 SEG crossing( VECTOR2I( 300, -500 ), VECTOR2I( 300, 500 ) );
511 BOOST_CHECK( arc.Collide( crossing, 0, nullptr, nullptr ) );
512
513 // Horizontal segment at y=−100 stays in the lower half.
514 // The full ellipse would intersect it, but the arc sweep does not.
515 SEG lower( VECTOR2I( -600, -100 ), VECTOR2I( 600, -100 ) );
516 BOOST_CHECK( !arc.Collide( lower, 50, nullptr, nullptr ) ); // 100 > 50
517 BOOST_CHECK( arc.Collide( lower, 150, nullptr, nullptr ) ); // 100 < 150
518}
519
520
521BOOST_AUTO_TEST_CASE( CollideCircleDegenerateAgreesWithShapeCircle )
522{
523 // Equal radii ellipse is a circle. Collide results must match
524
525 const int r = 1000;
526 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), r, r, EDA_ANGLE( 0.0, DEGREES_T ) );
527 SHAPE_CIRCLE c( VECTOR2I( 0, 0 ), r );
528
529 const SEG segments[] = {
530 SEG( VECTOR2I( 2000, 0 ), VECTOR2I( 2500, 0 ) ), // far right
531 SEG( VECTOR2I( -2000, 0 ), VECTOR2I( 2000, 0 ) ), // through center
532 SEG( VECTOR2I( 1100, 1100 ), VECTOR2I( 2000, 2000 ) ), // outside diagonal
533 SEG( VECTOR2I( 500, 500 ), VECTOR2I( 800, 800 ) ), // inside
534 };
535
536 for( const SEG& s : segments )
537 {
538 BOOST_CHECK_EQUAL( e.Collide( s, 0, nullptr, nullptr ), c.Collide( s, 0, nullptr, nullptr ) );
539 BOOST_CHECK_EQUAL( e.Collide( s, 100, nullptr, nullptr ), c.Collide( s, 100, nullptr, nullptr ) );
540 }
541}
542
543
544BOOST_AUTO_TEST_CASE( CollideEllipseVsCircleOverlap )
545{
546 // Ellipse and circle overlap. They should collide.
547 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
548 SHAPE_CIRCLE c( VECTOR2I( 400, 0 ), 200 );
549
550 BOOST_CHECK( e.Collide( &c, 0, nullptr, nullptr ) );
551}
552
553
554BOOST_AUTO_TEST_CASE( CollideEllipseVsCircleDisjoint )
555{
556 // Ellipse and circle far apart. There is no collision.
557 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
558 SHAPE_CIRCLE c( VECTOR2I( 2000, 2000 ), 100 );
559
560 BOOST_CHECK( !e.Collide( &c, 0, nullptr, nullptr ) );
561}
562
563
564BOOST_AUTO_TEST_CASE( CollideEllipseVsRectContainsEllipse )
565{
566 // Large rectangle entirely contains the ellipse. Collision detected
567 // because the ellipse center is inside the rect.
568 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
569 SHAPE_RECT r( VECTOR2I( -2000, -2000 ), 4000, 4000 );
570
571 BOOST_CHECK( e.Collide( &r, 0, nullptr, nullptr ) );
572}
573
574
575BOOST_AUTO_TEST_CASE( CollideEllipseVsRectDisjoint )
576{
577 // Small rectangle far from the ellipse. No collision.
578 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
579 SHAPE_RECT r( VECTOR2I( 2000, 2000 ), 100, 100 );
580
581 BOOST_CHECK( !e.Collide( &r, 0, nullptr, nullptr ) );
582}
583
584
585BOOST_AUTO_TEST_CASE( CollideEllipseVsRectEdgeIntersects )
586{
587 // Rectangle straddles the ellipse's right edge. We have partial overlap.
588 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
589 SHAPE_RECT r( VECTOR2I( 400, -100 ), 400, 200 );
590
591 BOOST_CHECK( e.Collide( &r, 0, nullptr, nullptr ) );
592}
593
594
595BOOST_AUTO_TEST_CASE( CollideEllipseVsLineChainIntersects )
596{
597 // Open polyline passes through the ellipse. It should collide.
598 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
600 chain.Append( VECTOR2I( -1000, 100 ) );
601 chain.Append( VECTOR2I( 1000, 100 ) ); // horizontal line piercing the ellipse
602 chain.Append( VECTOR2I( 1000, 500 ) );
603
604 BOOST_CHECK( e.Collide( &chain, 0, nullptr, nullptr ) );
605}
606
607
608BOOST_AUTO_TEST_CASE( CollideEllipseInsideClosedChain )
609{
610 // Closed square chain entirely contains the ellipse. Collision detected
612 chain.Append( VECTOR2I( -2000, -2000 ) );
613 chain.Append( VECTOR2I( 2000, -2000 ) );
614 chain.Append( VECTOR2I( 2000, 2000 ) );
615 chain.Append( VECTOR2I( -2000, 2000 ) );
616 chain.SetClosed( true );
617
618 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
619
620 BOOST_CHECK( e.Collide( &chain, 0, nullptr, nullptr ) );
621}
622
623
624BOOST_AUTO_TEST_CASE( CollideEllipseVsArcOverlap )
625{
626 // Circular arc overlaps the ellipse. It should collide.
627 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
628 SHAPE_ARC arc( VECTOR2I( 0, 0 ), VECTOR2I( 400, 0 ), EDA_ANGLE( 180.0, DEGREES_T ), 0 );
629
630 BOOST_CHECK( e.Collide( &arc, 0, nullptr, nullptr ) );
631}
632
633
634BOOST_AUTO_TEST_CASE( CollideEllipseVsEllipseOverlap )
635{
636 // Two overlapping ellipses. They should collide.
637 SHAPE_ELLIPSE a( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
638 SHAPE_ELLIPSE b( VECTOR2I( 400, 0 ), 400, 200, EDA_ANGLE( 0.0, DEGREES_T ) );
639
640 BOOST_CHECK( a.Collide( &b, 0, nullptr, nullptr ) );
641}
642
643
644BOOST_AUTO_TEST_CASE( CollideEllipseVsEllipseDisjoint )
645{
646 // Two ellipses far apart. There is no collision.
647 SHAPE_ELLIPSE a( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
648 SHAPE_ELLIPSE b( VECTOR2I( 3000, 3000 ), 400, 200, EDA_ANGLE( 0.0, DEGREES_T ) );
649
650 BOOST_CHECK( !a.Collide( &b, 0, nullptr, nullptr ) );
651}
652
653
654BOOST_AUTO_TEST_CASE( CollideEllipseInsideAnotherEllipse )
655{
656 // Small ellipse inside a larger one. Collision should be detected.
657 SHAPE_ELLIPSE big( VECTOR2I( 0, 0 ), 2000, 1500, EDA_ANGLE( 0.0, DEGREES_T ) );
658 SHAPE_ELLIPSE small( VECTOR2I( 0, 0 ), 200, 100, EDA_ANGLE( 0.0, DEGREES_T ) );
659
660 BOOST_CHECK( big.Collide( &small, 0, nullptr, nullptr ) );
661}
662
663
664BOOST_AUTO_TEST_CASE( CollideEllipseVsRect )
665{
666 SHAPE_ELLIPSE e( { 0, 0 }, 500, 300, ANGLE_0 );
667 SHAPE_RECT r( { 400, 0 }, 200, 200 ); // straddles the ellipse boundary
668 int actual = -1;
669 BOOST_CHECK( e.Collide( &r, 0, &actual ) );
671}
672
673
674BOOST_AUTO_TEST_CASE( RotateByFullTurnPreservesBBox )
675{
676 // Rotating 360 degrees about any point should leave the bbox unchanged
677 SHAPE_ELLIPSE e( VECTOR2I( 100, 200 ), 500, 300, EDA_ANGLE( 30.0, DEGREES_T ) );
678 const BOX2I before = e.BBox();
679
680 e.Rotate( EDA_ANGLE( 360.0, DEGREES_T ), VECTOR2I( 0, 0 ) );
681
682 const BOX2I after = e.BBox();
683
684 // Full 360° rotation: bbox position and size should match (integer rounding <= 2 IU).
685 BOOST_CHECK_LE( std::abs( before.GetLeft() - after.GetLeft() ), 2 );
686 BOOST_CHECK_LE( std::abs( before.GetRight() - after.GetRight() ), 2 );
687 BOOST_CHECK_LE( std::abs( before.GetTop() - after.GetTop() ), 2 );
688 BOOST_CHECK_LE( std::abs( before.GetBottom() - after.GetBottom() ), 2 );
689}
690
691
692BOOST_AUTO_TEST_CASE( RotateCircleAboutCenterIsBBoxInvariant )
693{
694 // A circle's bbox doesn't change under any rotation about its center.
695 SHAPE_ELLIPSE c( VECTOR2I( 0, 0 ), 1000, 1000, EDA_ANGLE( 0.0, DEGREES_T ) );
696 const BOX2I before = c.BBox();
697
698 c.Rotate( EDA_ANGLE( 47.5, DEGREES_T ), VECTOR2I( 0, 0 ) );
699
700 const BOX2I after = c.BBox();
701
702 BOOST_CHECK_LE( std::abs( before.GetLeft() - after.GetLeft() ), 2 );
703 BOOST_CHECK_LE( std::abs( before.GetRight() - after.GetRight() ), 2 );
704 BOOST_CHECK_LE( std::abs( before.GetTop() - after.GetTop() ), 2 );
705 BOOST_CHECK_LE( std::abs( before.GetBottom() - after.GetBottom() ), 2 );
706}
707
708
709BOOST_AUTO_TEST_CASE( RotateAboutNonCenterTranslatesAndRotates )
710{
711 // Rotating 180 degrees about (1000, 0) moves the center from (0, 0) to
712 // (2000, 0) and subtracts 180 from the ellipse's internal rotation.
713 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ) );
714 e.Rotate( EDA_ANGLE( 180.0, DEGREES_T ), VECTOR2I( 1000, 0 ) );
715
716 BOOST_CHECK_LE( std::abs( e.GetCenter().x - 2000 ), 1 );
717 BOOST_CHECK_LE( std::abs( e.GetCenter().y - 0 ), 1 );
718
719 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), -180.0, 1e-6 );
720}
721
722
723BOOST_AUTO_TEST_CASE( MirrorLeftRightFlipsCenter )
724{
725 // Left-right mirror across x=1000: center.x flips from 100 to 1900,
726 // rotation negates from 30 to -30 degrees.
727 SHAPE_ELLIPSE e( VECTOR2I( 100, 200 ), 500, 300, EDA_ANGLE( 30.0, DEGREES_T ) );
729
730 BOOST_CHECK_EQUAL( e.GetCenter().x, 1900 );
731 BOOST_CHECK_EQUAL( e.GetCenter().y, 200 );
732
733 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), -30.0, 1e-6 );
734}
735
736
737BOOST_AUTO_TEST_CASE( MirrorTopBottomFlipsCenter )
738{
739 // Top-bottom mirror across y=500: center.y flips from 200 to 800,
740 // rotation negates
741 SHAPE_ELLIPSE e( VECTOR2I( 100, 200 ), 500, 300, EDA_ANGLE( 30.0, DEGREES_T ) );
743
744 BOOST_CHECK_EQUAL( e.GetCenter().x, 100 );
745 BOOST_CHECK_EQUAL( e.GetCenter().y, 800 );
746
747 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), -30.0, 1e-6 );
748}
749
750
751BOOST_AUTO_TEST_CASE( MirrorLeftRightTwiceIsIdentity )
752{
753 // Mirroring left-right twice with the same axis returns to the original
754 // center and rotation.
755 SHAPE_ELLIPSE e( VECTOR2I( 100, 200 ), 500, 300, EDA_ANGLE( 45.0, DEGREES_T ) );
756 const VECTOR2I origCenter = e.GetCenter();
757 const double origRotation = e.GetRotation().AsDegrees();
758
759 e.Mirror( VECTOR2I( 1234, 5678 ), FLIP_DIRECTION::LEFT_RIGHT );
760 e.Mirror( VECTOR2I( 1234, 5678 ), FLIP_DIRECTION::LEFT_RIGHT );
761
762 BOOST_CHECK_EQUAL( e.GetCenter().x, origCenter.x );
763 BOOST_CHECK_EQUAL( e.GetCenter().y, origCenter.y );
764 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), origRotation, 1e-6 );
765}
766
767
768BOOST_AUTO_TEST_CASE( MirrorTopBottomTwiceIsIdentity )
769{
770 // Mirroring top-bottom twice returns to the original state.
771 SHAPE_ELLIPSE e( VECTOR2I( 100, 200 ), 500, 300, EDA_ANGLE( 45.0, DEGREES_T ) );
772 const VECTOR2I origCenter = e.GetCenter();
773 const double origRotation = e.GetRotation().AsDegrees();
774
775 e.Mirror( VECTOR2I( 1234, 5678 ), FLIP_DIRECTION::TOP_BOTTOM );
776 e.Mirror( VECTOR2I( 1234, 5678 ), FLIP_DIRECTION::TOP_BOTTOM );
777
778 BOOST_CHECK_EQUAL( e.GetCenter().x, origCenter.x );
779 BOOST_CHECK_EQUAL( e.GetCenter().y, origCenter.y );
780 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), origRotation, 1e-6 );
781}
782
783
784BOOST_AUTO_TEST_CASE( MirrorArcSwapsAndReflectsSweep )
785{
786 // Symmetric arc [30, 150] mirrored left-right. The sweep [30, 150] is
787 // symmetric about 90 degrees, so start and end angles stay the same.
788 SHAPE_ELLIPSE arc( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 30.0, DEGREES_T ),
789 EDA_ANGLE( 150.0, DEGREES_T ) );
790
792
793 BOOST_CHECK_CLOSE( arc.GetStartAngle().AsDegrees(), 30.0, 1e-6 );
794 BOOST_CHECK_CLOSE( arc.GetEndAngle().AsDegrees(), 150.0, 1e-6 );
795}
796
797
798BOOST_AUTO_TEST_CASE( MirrorArcAsymmetricSwapsSweep )
799{
800 // Asymmetric arc [20, 80] mirrored left-right. Start becomes 180-80=100,
801 // end becomes 180-20=160.
802 SHAPE_ELLIPSE arc( VECTOR2I( 0, 0 ), 500, 300, EDA_ANGLE( 0.0, DEGREES_T ), EDA_ANGLE( 20.0, DEGREES_T ),
803 EDA_ANGLE( 80.0, DEGREES_T ) );
804
806
807 BOOST_CHECK_CLOSE( arc.GetStartAngle().AsDegrees(), 100.0, 1e-6 ); // 180 - 80
808 BOOST_CHECK_CLOSE( arc.GetEndAngle().AsDegrees(), 160.0, 1e-6 ); // 180 - 20
809}
810
811
812BOOST_AUTO_TEST_CASE( FormatContainsExpectedFields )
813{
814 SHAPE_ELLIPSE e( VECTOR2I( 100, 200 ), 500, 300, EDA_ANGLE( 30.0, DEGREES_T ) );
815
816 const std::string cpp = e.Format( true );
817 // output mentions all the key numeric values.
818 BOOST_CHECK( cpp.find( "SHAPE_ELLIPSE" ) != std::string::npos );
819 BOOST_CHECK( cpp.find( "100" ) != std::string::npos );
820 BOOST_CHECK( cpp.find( "200" ) != std::string::npos );
821 BOOST_CHECK( cpp.find( "500" ) != std::string::npos );
822 BOOST_CHECK( cpp.find( "300" ) != std::string::npos );
823
824 const std::string plain = e.Format( false );
825 BOOST_CHECK( plain.find( "500" ) != std::string::npos );
826 BOOST_CHECK( plain.find( "300" ) != std::string::npos );
827}
828
829
830BOOST_AUTO_TEST_CASE( FuzzRandomEllipsesInvariantsAndDeterminism )
831{
832 // 10,000 random ellipses. Check that major >= minor after construction
833 // bbox is non-degenerate, SquaredDistance gives the same result when
834 // called multiple times and PointInside is consistent
835 // with SquaredDistance returning 0 for interior points.
836 std::mt19937 rng( 12345 );
837 std::uniform_int_distribution<int> centerDist( -10000, 10000 );
838 std::uniform_int_distribution<int> radiusDist( 50, 2000 );
839 std::uniform_real_distribution<double> angleDist( 0.0, 360.0 );
840 std::uniform_int_distribution<int> ptDist( -15000, 15000 );
841
842 const int N = 10000;
843 int determinismFailures = 0;
844
845 for( int i = 0; i < N; ++i )
846 {
847 const VECTOR2I center( centerDist( rng ), centerDist( rng ) );
848 const int r1 = radiusDist( rng );
849 const int r2 = radiusDist( rng );
850 const EDA_ANGLE rot( angleDist( rng ), DEGREES_T );
851
852 SHAPE_ELLIPSE e( center, r1, r2, rot );
853
854 // major >= minor > 0 after normalize
855 BOOST_CHECK_GE( e.GetMajorRadius(), e.GetMinorRadius() );
856 BOOST_CHECK_GT( e.GetMinorRadius(), 0 );
857
858 // BBox is non-degenerate
859 const BOX2I bbox = e.BBox();
860 BOOST_CHECK_GT( bbox.GetWidth(), 0 );
861 BOOST_CHECK_GT( bbox.GetHeight(), 0 );
862
863 // SquaredDistance and PointInside are functions of their inputs
864 const VECTOR2I testPt( ptDist( rng ), ptDist( rng ) );
865 const SEG::ecoord d1 = e.SquaredDistance( testPt, false );
866 const SEG::ecoord d2 = e.SquaredDistance( testPt, false );
867 const SEG::ecoord d3 = e.SquaredDistance( testPt, false );
868
869 if( d1 != d2 || d2 != d3 )
870 ++determinismFailures;
871
872 // If PointInside is true for a closed ellipse, non-outline
873 // SquaredDistance must be 0.
874 if( !e.IsArc() && e.PointInside( testPt ) )
875 BOOST_CHECK_EQUAL( e.SquaredDistance( testPt, false ), 0 );
876 }
877
878 BOOST_CHECK_EQUAL( determinismFailures, 0 );
879}
880
881
882BOOST_AUTO_TEST_CASE( CrossCheckSegmentCollideAgainstTessellation )
883{
884 std::mt19937 rng( 9999 );
885 std::uniform_int_distribution<int> centerDist( -5000, 5000 );
886 std::uniform_int_distribution<int> radiusDist( 100, 1500 );
887 std::uniform_real_distribution<double> angleDist( 0.0, 360.0 );
888 std::uniform_int_distribution<int> segDist( -8000, 8000 );
889
890 const int N = 5000;
891 const int clearance = 50;
892 int mismatches = 0;
893
894 for( int i = 0; i < N; ++i )
895 {
896 const VECTOR2I ec( centerDist( rng ), centerDist( rng ) );
897 const int r1 = radiusDist( rng );
898 const int r2 = radiusDist( rng );
899 const EDA_ANGLE rot( angleDist( rng ), DEGREES_T );
900
901 SHAPE_ELLIPSE e( ec, r1, r2, rot );
902 const SEG s( VECTOR2I( segDist( rng ), segDist( rng ) ), VECTOR2I( segDist( rng ), segDist( rng ) ) );
903
904 const bool analyticCollide = e.Collide( s, clearance, nullptr, nullptr );
905
906 // Brute force: very fine tessellation + SHAPE_LINE_CHAIN::Collide.
907 // Tessellation error 2 IU << clearance 50 IU, so boundary ambiguity is small.
909 bool bruteCollide = chain.Collide( s, clearance, nullptr, nullptr );
910
911 // The tessellated chain's Collide does not model the closed ellipse interior.
912 // Compensate: for a closed ellipse, a segment endpoint inside the interior is
913 // a collision too.
914 if( !bruteCollide && !e.IsArc() )
915 {
916 if( e.PointInside( s.A ) || e.PointInside( s.B ) )
917 bruteCollide = true;
918 }
919
920 if( analyticCollide != bruteCollide )
921 ++mismatches;
922 }
923
924 BOOST_TEST_MESSAGE( "cross-check mismatches: " << mismatches << " / " << N );
925
926 // Near-threshold cases may disagree within the 2 IU tessellation budget.
927 // Allow up to 1% boundary mismatches.
928 BOOST_CHECK_LE( mismatches, N / 100 );
929}
930
931
937BOOST_AUTO_TEST_CASE( NormalizeArcAnglesOnSwap )
938{
939 // Construct with minor > major to force a swap in normalize().
940 // Major=20, Minor=50 → after normalize: Major=50, Minor=20, Rotation += 90
941 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 20, 50, EDA_ANGLE( 0, DEGREES_T ), EDA_ANGLE( 30.0, DEGREES_T ),
942 EDA_ANGLE( 120.0, DEGREES_T ) );
943
944 // After swap: major=50, minor=20
947
948 // Rotation should be 0 + 90 = 90
949 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), 90.0, 1e-6 );
950
951 // Angles should shift by -90: 30-90=-60, 120-90=30
952 BOOST_CHECK_CLOSE( e.GetStartAngle().AsDegrees(), -60.0, 1e-6 );
953 BOOST_CHECK_CLOSE( e.GetEndAngle().AsDegrees(), 30.0, 1e-6 );
954
955 // Sweep should be preserved: was 90, still 90
956 double sweep = e.GetEndAngle().AsDegrees() - e.GetStartAngle().AsDegrees();
957 BOOST_CHECK_CLOSE( sweep, 90.0, 1e-6 );
958}
959
960
964BOOST_AUTO_TEST_CASE( NormalizeClosedEllipseNoAngleShift )
965{
966 // Closed ellipse with minor > major.
967 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 20, 50, EDA_ANGLE( 0, DEGREES_T ) );
968
971 BOOST_CHECK_CLOSE( e.GetRotation().AsDegrees(), 90.0, 1e-6 );
972 BOOST_CHECK( !e.IsArc() );
973}
974
975
979BOOST_AUTO_TEST_CASE( FullSweepAngleInSweep )
980{
981 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 100, 50, EDA_ANGLE( 0, DEGREES_T ), EDA_ANGLE( 0, DEGREES_T ),
982 EDA_ANGLE( 360.0, DEGREES_T ) );
983
984 // Every angle should be in the sweep for a full 360 arc.
985 BOOST_CHECK( e.Collide( SEG( VECTOR2I( 100, 0 ), VECTOR2I( 100, 0 ) ), 1 ) ); // 0 degrees
986 BOOST_CHECK( e.Collide( SEG( VECTOR2I( 0, 50 ), VECTOR2I( 0, 50 ) ), 1 ) ); // 90 degrees
987 BOOST_CHECK( e.Collide( SEG( VECTOR2I( -100, 0 ), VECTOR2I( -100, 0 ) ), 1 ) ); // 180 degrees
988 BOOST_CHECK( e.Collide( SEG( VECTOR2I( 0, -50 ), VECTOR2I( 0, -50 ) ), 1 ) ); // 270 degrees
989}
990
991
995BOOST_AUTO_TEST_CASE( CacheConsistencyAfterSetRotation )
996{
997 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 100, 50, EDA_ANGLE( 0, DEGREES_T ) );
998
999 // Get BBox at rotation=0
1000 BOX2I box0 = e.BBox( 0 );
1001
1002 // Rotate 90 degrees so major and minor visual extents swap.
1003 e.SetRotation( EDA_ANGLE( 90.0, DEGREES_T ) );
1004 BOX2I box90 = e.BBox( 0 );
1005
1006 // At 0 degrees: width dominated by major (100), height by minor (50)
1007 // At 90 degrees: width dominated by minor (50), height by major (100)
1008 BOOST_CHECK_EQUAL( box0.GetWidth(), box90.GetHeight() );
1009 BOOST_CHECK_EQUAL( box0.GetHeight(), box90.GetWidth() );
1010}
1011
1012
1016BOOST_AUTO_TEST_CASE( CacheConsistencyAfterSetMajorRadius )
1017{
1018 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 100, 50, EDA_ANGLE( 0, DEGREES_T ) );
1019
1020 BOX2I box1 = e.BBox( 0 );
1021
1022 e.SetMajorRadius( 200 );
1023 BOX2I box2 = e.BBox( 0 );
1024
1025 // Width should double (major axis is along X at rotation=0).
1026 BOOST_CHECK_EQUAL( box2.GetWidth(), box1.GetWidth() * 2 );
1027 // Height should stay the same (minor unchanged).
1028 BOOST_CHECK_EQUAL( box2.GetHeight(), box1.GetHeight() );
1029}
1030
1031
1035BOOST_AUTO_TEST_CASE( PointInsideAfterRotationChange )
1036{
1037 SHAPE_ELLIPSE e( VECTOR2I( 0, 0 ), 100, 30, EDA_ANGLE( 0, DEGREES_T ) );
1038
1039 // Point at (80, 0) is inside when major axis is along X.
1040 BOOST_CHECK( e.PointInside( VECTOR2I( 80, 0 ) ) );
1041 // Point at (0, 80) is outside (minor = 30).
1042 BOOST_CHECK( !e.PointInside( VECTOR2I( 0, 80 ) ) );
1043
1044 // Rotate 90, major axis now along Y.
1045 e.SetRotation( EDA_ANGLE( 90.0, DEGREES_T ) );
1046
1047 // Now (80, 0) should be outside and (0, 80) inside.
1048 BOOST_CHECK( !e.PointInside( VECTOR2I( 80, 0 ) ) );
1049 BOOST_CHECK( e.PointInside( VECTOR2I( 0, 80 ) ) );
1050}
1051
1052
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr size_type GetWidth() const
Definition box2.h:214
constexpr size_type GetHeight() const
Definition box2.h:215
constexpr coord_type GetLeft() const
Definition box2.h:228
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr coord_type GetTop() const
Definition box2.h:229
constexpr coord_type GetBottom() const
Definition box2.h:222
double AsDegrees() const
Definition eda_angle.h:116
Definition seg.h:42
VECTOR2I A
Definition seg.h:49
VECTOR2I::extended_type ecoord
Definition seg.h:44
VECTOR2I B
Definition seg.h:50
bool Collide(const SEG &aSeg, int aClearance=0, int *aActual=nullptr, VECTOR2I *aLocation=nullptr) const override
Check if the boundary of shape (this) lies closer to the segment aSeg than aClearance,...
void SetRotation(const EDA_ANGLE &aAngle)
int GetMajorRadius() const
SHAPE_LINE_CHAIN ConvertToPolyline(int aMaxError) const
Build a polyline approximation of the ellipse or arc.
const VECTOR2I & GetCenter() const
void SetMajorRadius(int aRadius)
SEG::ecoord SquaredDistance(const VECTOR2I &aP, bool aOutlineOnly=false) const override
const EDA_ANGLE & GetStartAngle() const
const EDA_ANGLE & GetEndAngle() const
bool PointInside(const VECTOR2I &aPt, int aAccuracy=0, bool aUseBBoxCache=false) const override
Check if point aP lies inside a closed shape.
const std::string Format(bool aCplusPlus=true) const override
Serialize the ellipse.
double GetLength() const
void Rotate(const EDA_ANGLE &aAngle, const VECTOR2I &aCenter={ 0, 0 }) override
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
void Mirror(const VECTOR2I &aRef, FLIP_DIRECTION aFlipDirection)
Mirror the ellipse across a horizontal or vertical axis passing through aRef.
const EDA_ANGLE & GetRotation() const
bool IsArc() const
bool Collide(const SEG &aSeg, int aClearance=0, int *aActual=nullptr, VECTOR2I *aLocation=nullptr) const override
Check if the boundary of shape (this) lies closer to the segment aSeg than aClearance,...
int GetMinorRadius() const
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
int PointCount() const
Return the number of points (vertices) in this line chain.
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:411
@ DEGREES_T
Definition eda_angle.h:31
static thread_local boost::mt19937 rng
Definition kiid.cpp:50
@ LEFT_RIGHT
Flip left to right (around the Y axis)
Definition mirror.h:28
@ TOP_BOTTOM
Flip top to bottom (around the X axis)
Definition mirror.h:29
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
VECTOR2I center
const SHAPE_LINE_CHAIN chain
VECTOR2I end
int clearance
int actual
BOOST_AUTO_TEST_CASE(ConstructorSwapsMajorMinor)
static BOX2I bruteForceEllipseBBox(const VECTOR2I &aCenter, int aMajorR, int aMinorR, const EDA_ANGLE &aRotation, const EDA_ANGLE &aStartAngle, const EDA_ANGLE &aEndAngle, bool aIsArc, int aNSamples=10000)
Compute a bounding box by sampling 10,000 points around the ellipse Used as ground truth to verify BB...
BOOST_CHECK_EQUAL(result, "25.4")
#define M_PI
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687