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