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