KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_shape_line_chain_collision.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 (C) 2017 CERN
5 * @author Alejandro GarcĂ­a Montoro <[email protected]>
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
22
23#include <geometry/shape.h>
24#include <geometry/shape_arc.h>
28
29#include "fixtures_geometry.h"
30
31#include <algorithm>
32#include <fstream>
33#include <limits>
34#include <sstream>
35#include <utility>
36#include <vector>
37
38
39BOOST_AUTO_TEST_SUITE( SHAPE_LINE_CHAIN_COLLIDE_TEST )
40
41
42// Sentinels far outside the fixtures' range, so an untouched output cannot look like a real one
43static constexpr int NO_ACTUAL = 123456789;
44static const VECTOR2I NO_LOCATION( 123456789, -123456789 );
45
46
54
55
56static SHAPE_POLY_SET loadZone( const std::string& aFilename, int aZone )
57{
58 const std::string path = KI_TEST::GetTestDataRootDir() + "triangulation/" + aFilename;
59 std::ifstream stream( path );
60 BOOST_REQUIRE_MESSAGE( stream, "Unable to open " << path );
61
62 const std::string data( ( std::istreambuf_iterator<char>( stream ) ), {} );
63 size_t pos = 0;
64
65 for( int zone = 0; zone <= aZone; ++zone )
66 {
67 pos = data.find( "(zone (layer \"", pos );
68 BOOST_REQUIRE_MESSAGE( pos != std::string::npos, "Missing zone " << aZone << " in " << path );
69 ++pos;
70 }
71
72 pos = data.find( "polyset ", pos );
73 BOOST_REQUIRE_MESSAGE( pos != std::string::npos, "Missing polyset for zone " << aZone << " in " << path );
74
75 // Parse consumes only the polyset expression, so handing it the rest of the file is harmless
76 std::stringstream serialized( data.substr( pos ) );
78 BOOST_REQUIRE_MESSAGE( result.Parse( serialized ), "Unable to parse zone " << aZone << " in " << path );
79
80 return result;
81}
82
83
84static SHAPE_LINE_CHAIN extractSubchain( const SHAPE_LINE_CHAIN& aSource, int aStart, int aSegmentCount )
85{
87 result.Append( aSource.CSegment( aStart ).A );
88
89 for( int i = 0; i < aSegmentCount; ++i )
90 result.Append( aSource.CSegment( aStart + i ).B, true );
91
92 return result;
93}
94
95
102
103
104// The pre-index algorithm, which walked every segment pair in sorted order
105// Valid only for arc-free chains, since it skips the arc refinement pass
106static COLLISION_OUTPUT referenceCollision( const SHAPE_LINE_CHAIN& aA, const SHAPE_LINE_CHAIN& aB, int aClearance,
107 int aOutputs )
108{
109 int closest = std::numeric_limits<int>::max();
110 VECTOR2I nearest;
111
112 if( aB.IsClosed() && aA.GetPointCount() > 0 && aB.PointInside( aA.GetPoint( 0 ) ) )
113 {
114 closest = 0;
115 nearest = aA.GetPoint( 0 );
116 }
117 else if( aA.IsClosed() && aB.GetPointCount() > 0 && aA.PointInside( aB.GetPoint( 0 ) ) )
118 {
119 closest = 0;
120 nearest = aB.GetPoint( 0 );
121 }
122 else if( aClearance >= 0 )
123 {
124 std::vector<SEG> aSegments;
125 std::vector<SEG> bSegments;
126
127 for( int i = 0; i < aA.SegmentCount(); ++i )
128 aSegments.push_back( aA.CSegment( i ) );
129
130 for( int i = 0; i < aB.SegmentCount(); ++i )
131 bSegments.push_back( aB.CSegment( i ) );
132
133 auto segmentSort = []( const SEG& aFirst, const SEG& aSecond )
134 {
135 return aFirst.A.x < aSecond.A.x || ( aFirst.A.x == aSecond.A.x && aFirst.A.y < aSecond.A.y );
136 };
137
138 std::sort( aSegments.begin(), aSegments.end(), segmentSort );
139 std::sort( bSegments.begin(), bSegments.end(), segmentSort );
140
141 for( const SEG& a : aSegments )
142 {
143 for( const SEG& b : bSegments )
144 {
145 int distance = 0;
146
147 if( a.Collide( b, aClearance, &distance ) )
148 {
149 if( distance < closest )
150 {
151 nearest = a.NearestPoint( b );
152 closest = distance;
153 }
154
155 if( closest == 0 || !( aOutputs & WANT_ACTUAL ) )
156 break;
157 }
158 }
159 }
160 }
161
163
164 if( closest == 0 || closest < aClearance )
165 {
166 result.result = true;
167
168 if( aOutputs & WANT_ACTUAL )
169 result.actual = closest;
170
171 if( aOutputs & WANT_LOCATION )
172 result.location = nearest;
173 }
174
175 return result;
176}
177
178
179static COLLISION_OUTPUT indexedCollision( const SHAPE_LINE_CHAIN& aA, const SHAPE_LINE_CHAIN& aB, int aClearance,
180 int aOutputs )
181{
183 int* actual = aOutputs & WANT_ACTUAL ? &result.actual : nullptr;
184 VECTOR2I* location = aOutputs & WANT_LOCATION ? &result.location : nullptr;
185
186 result.result = static_cast<const SHAPE&>( aA ).Collide( &aB, aClearance, actual, location );
187
188 return result;
189}
190
191
192static void checkCollisionParity( const SHAPE_LINE_CHAIN& aA, const SHAPE_LINE_CHAIN& aB, int aClearance )
193{
194 for( int outputs = WANT_NOTHING; outputs <= WANT_BOTH; ++outputs )
195 {
196 const COLLISION_OUTPUT expected = referenceCollision( aA, aB, aClearance, outputs );
197 const COLLISION_OUTPUT actual = indexedCollision( aA, aB, aClearance, outputs );
198
199 BOOST_CHECK_EQUAL( actual.result, expected.result );
200 BOOST_CHECK_EQUAL( actual.actual, expected.actual );
201 BOOST_CHECK( actual.location == expected.location );
202 }
203}
204
205
207{
208 const char* file;
209 int zone;
212};
213
214
215BOOST_AUTO_TEST_CASE( Collide_RealContourParity )
216{
217 const std::vector<CORPUS_CASE> cases = { { "One-Air-Max.kicad_polys", 18, 0, 3 },
218 { "issue5093.kicad_polys", 3, 0, 1 },
219 { "bad_triangulation_case.kicad_polys", 47, 0, 3 } };
220
221 for( const CORPUS_CASE& test : cases )
222 {
223 const SHAPE_POLY_SET poly = loadZone( test.file, test.zone );
224 BOOST_REQUIRE_GT( poly.OutlineCount(), std::max( test.outlineA, test.outlineB ) );
225
226 const SHAPE_LINE_CHAIN& a = poly.COutline( test.outlineA );
227 const SHAPE_LINE_CHAIN& b = poly.COutline( test.outlineB );
228 BOOST_REQUIRE_EQUAL( a.ArcCount(), 0 );
229 BOOST_REQUIRE_EQUAL( b.ArcCount(), 0 );
230
231 for( int clearance : { -1, 0, 200000, 2000000 } )
232 {
235 }
236 }
237}
238
239
240BOOST_AUTO_TEST_CASE( Collide_RealContourGateBoundaries )
241{
242 const SHAPE_POLY_SET poly = loadZone( "issue5093.kicad_polys", 3 );
243 const SHAPE_LINE_CHAIN& sourceA = poly.COutline( 0 );
244 const SHAPE_LINE_CHAIN& sourceB = poly.COutline( 1 );
245 // Sizes straddle the output gate of A>=32, B>=64 and 4096 pairs, so 31x133 stays direct while
246 // 66x63 and 63x65 cross it in one argument order only
247 const std::vector<std::pair<int, int>> sizes = { { 31, 133 }, { 32, 128 }, { 66, 63 }, { 63, 65 }, { 64, 64 } };
248 BOOST_REQUIRE_EQUAL( sourceA.SegmentCount(), 265 );
249 BOOST_REQUIRE_EQUAL( sourceB.SegmentCount(), 307 );
250
251 for( const auto& [countA, countB] : sizes )
252 {
253 for( int startA : { 0, sourceA.SegmentCount() - countA } )
254 {
255 const SHAPE_LINE_CHAIN a = extractSubchain( sourceA, startA, countA );
256 const SHAPE_LINE_CHAIN b = extractSubchain( sourceB, 0, countB );
257
258 for( int clearance : { -1, 0, 200000, 2000000 } )
259 {
262 }
263 }
264 }
265}
266
267
268BOOST_AUTO_TEST_CASE( Collide_RealContourSharedSegments )
269{
270 const SHAPE_POLY_SET poly = loadZone( "issue5093.kicad_polys", 3 );
271 const SHAPE_LINE_CHAIN& source = poly.COutline( 0 );
272
273 // Overlapping subchains share 32 segments, which drives the zero-distance exact-hit exit
274 const SHAPE_LINE_CHAIN a = extractSubchain( source, 0, 64 );
275 const SHAPE_LINE_CHAIN b = extractSubchain( source, 32, 128 );
276
277 checkCollisionParity( a, b, 0 );
278 checkCollisionParity( b, a, 0 );
279}
280
281
282BOOST_AUTO_TEST_CASE( SegmentIndex_RealContourCandidateSet )
283{
284 const SHAPE_POLY_SET poly = loadZone( "One-Air-Max.kicad_polys", 18 );
285 const SHAPE_LINE_CHAIN& contour = poly.COutline( 0 );
286 std::vector<SEG> segments;
287
288 for( int i = 0; i < contour.SegmentCount(); ++i )
289 segments.push_back( contour.CSegment( i ) );
290
291 // Enough segments to force a multi-level tree rather than a single leaf
292 SEGMENT_INDEX index( segments );
293 BOOST_REQUIRE_GT( index.size(), 256 );
294
295 for( size_t queryIndex = 0; queryIndex < segments.size(); queryIndex += 17 )
296 {
297 const SEG& query = segments[queryIndex];
298
299 for( int padding : { 0, 200000, 2000000 } )
300 {
301 std::vector<int> expected;
302 const int64_t minX = static_cast<int64_t>( std::min( query.A.x, query.B.x ) ) - padding;
303 const int64_t minY = static_cast<int64_t>( std::min( query.A.y, query.B.y ) ) - padding;
304 const int64_t maxX = static_cast<int64_t>( std::max( query.A.x, query.B.x ) ) + padding;
305 const int64_t maxY = static_cast<int64_t>( std::max( query.A.y, query.B.y ) ) + padding;
306
307 for( size_t i = 0; i < segments.size(); ++i )
308 {
309 const SEG& segment = segments[i];
310
311 if( std::max( segment.A.x, segment.B.x ) >= minX && std::min( segment.A.x, segment.B.x ) <= maxX
312 && std::max( segment.A.y, segment.B.y ) >= minY && std::min( segment.A.y, segment.B.y ) <= maxY )
313 {
314 expected.push_back( static_cast<int>( i ) );
315 }
316 }
317
318 std::vector<int> actual;
319 auto visitor = [&]( int aItem )
320 {
321 actual.push_back( aItem );
322 return true;
323 };
324
325 index.VisitCandidates( query, padding, visitor );
326 std::sort( actual.begin(), actual.end() );
327 BOOST_CHECK( actual == expected );
328 }
329 }
330
331 int calls = 0;
332 auto stopVisitor = [&]( int )
333 {
334 ++calls;
335 return false;
336 };
337
338 index.VisitCandidates( segments.front(), std::numeric_limits<int>::max(), stopVisitor );
339 BOOST_CHECK_EQUAL( calls, 1 );
340}
341
342BOOST_AUTO_TEST_CASE( Collide_LineToLine )
343{
344 SHAPE_LINE_CHAIN lineA;
345 lineA.Append( VECTOR2I( 0, 0 ) );
346 lineA.Append( VECTOR2I( 10, 0 ) );
347
348 SHAPE_LINE_CHAIN lineB;
349 lineB.Append( VECTOR2I( 5, 5 ) );
350 lineB.Append( VECTOR2I( 5, -5 ) );
351
353 int actual = 0;
354 bool collided = static_cast<SHAPE*>( &lineA )->Collide( &lineB, 0, &actual, &location );
355
356 BOOST_CHECK( collided );
357 BOOST_TEST( actual == 0 );
358 BOOST_CHECK_MESSAGE( location == VECTOR2I( 5, 0 ), "Expected: " << VECTOR2I( 5, 0 ) << " Actual: " << location );
359}
360
361BOOST_AUTO_TEST_CASE( Collide_LineToArc )
362{
363 SHAPE_LINE_CHAIN lineA;
364 lineA.Append( VECTOR2I( 0, 0 ) );
365 lineA.Append( VECTOR2I( 10, 0 ) );
366
367 SHAPE_LINE_CHAIN arcB;
368 arcB.Append( SHAPE_ARC( VECTOR2I( 5, 5 ), VECTOR2I( 6, 4 ), VECTOR2I( 7, 0 ), 0 ) );
369
371 int actual = 0;
372 bool collided = static_cast<SHAPE*>( &lineA )->Collide( &arcB, 0, &actual, &location );
373
374 BOOST_CHECK( collided );
375 BOOST_TEST( actual == 0 );
376 BOOST_CHECK_MESSAGE( location == VECTOR2I( 7, 0 ), "Expected: " << VECTOR2I( 7, 0 ) << " Actual: " << location );
377}
378
379BOOST_AUTO_TEST_CASE( Collide_ArcToArc )
380{
381 SHAPE_LINE_CHAIN arcA;
382 arcA.Append( SHAPE_ARC( VECTOR2I( 0, 0 ), VECTOR2I( 10, 0 ), VECTOR2I( 5, 5 ), 0 ) );
383
384 SHAPE_LINE_CHAIN arcB;
385 arcB.Append( SHAPE_ARC( VECTOR2I( 5, 5 ), VECTOR2I( 5, -5 ), VECTOR2I( 10, 0 ), 0 ) );
386
388 int actual = 0;
389 bool collided = static_cast<SHAPE*>( &arcA )->Collide( &arcB, 0, &actual, &location );
390
391 BOOST_CHECK( collided );
392 BOOST_TEST( actual == 0 );
393 BOOST_CHECK_MESSAGE( location == VECTOR2I( 5, 5 ), "Expected: " << VECTOR2I( 5, 5 ) << " Actual: " << location );
394}
395
396BOOST_AUTO_TEST_CASE( Collide_WithClearance )
397{
398 SHAPE_LINE_CHAIN lineA;
399 lineA.Append( VECTOR2I( 0, 0 ) );
400 lineA.Append( VECTOR2I( 10, 0 ) );
401
402 SHAPE_LINE_CHAIN lineB;
403 lineB.Append( VECTOR2I( 5, 6 ) );
404 lineB.Append( VECTOR2I( -5, 6 ) );
405
407 int actual = 0;
408 bool collided = static_cast<SHAPE*>( &lineA )->Collide( &lineB, 7, &actual, &location );
409
410 BOOST_CHECK( collided );
411 BOOST_CHECK_MESSAGE( actual == 6, "Expected: " << 6 << " Actual: " << actual );
412 BOOST_CHECK_MESSAGE( location == VECTOR2I( 0, 0 ), "Expected: " << VECTOR2I( 0, 0 ) << " Actual: " << location );
413}
414
415BOOST_AUTO_TEST_CASE( Collide_NoClearance )
416{
417 SHAPE_LINE_CHAIN lineA;
418 lineA.Append( VECTOR2I( 0, 0 ) );
419 lineA.Append( VECTOR2I( 10, 0 ) );
420
421 SHAPE_LINE_CHAIN lineB;
422 lineB.Append( VECTOR2I( 5, 6 ) );
423 lineB.Append( VECTOR2I( -5, 6 ) );
424
426 int actual = 0;
427 bool collided = static_cast<SHAPE*>( &lineA )->Collide( &lineB, 0, &actual, &location );
428
429 BOOST_CHECK( !collided );
430 BOOST_CHECK_MESSAGE( actual == 0, "Expected: " << 0 << " Actual: " << actual );
431}
432
int index
Immutable owning spatial snapshot of straight segments.
Definition seg.h:38
VECTOR2I A
Definition seg.h:45
VECTOR2I B
Definition seg.h:46
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
bool IsClosed() const override
virtual const VECTOR2I GetPoint(int aIndex) const override
virtual size_t GetPointCount() const override
void Append(int aX, int aY, bool aAllowDuplication=false)
Append a new point at the end of the line chain.
int SegmentCount() const
Return the number of segments in this line chain.
size_t ArcCount() const
const SEG CSegment(int aIndex) const
Return a constant copy of the aIndex segment in the line chain.
bool PointInside(const VECTOR2I &aPt, int aAccuracy=0, bool aUseBBoxCache=false) const override
Check if point aP lies inside a closed shape.
Represent a set of closed polygons.
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
An abstract shape on 2D plane.
Definition shape.h:124
std::string source
std::string GetTestDataRootDir()
static float distance(const SFVEC2UI &a, const SFVEC2UI &b)
static bool Collide(const SHAPE_CIRCLE &aA, const SHAPE_CIRCLE &aB, int aClearance, int *aActual, VECTOR2I *aLocation, VECTOR2I *aMTV)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST(netlist.find("R_G1 ARM_OUT1 DIE_B R='0.001 / ((SW_STATE)") !=std::string::npos)
std::string path
VECTOR3I expected(15, 30, 45)
int clearance
VECTOR2I location
int actual
static const VECTOR2I NO_LOCATION(123456789, -123456789)
static constexpr int NO_ACTUAL
BOOST_AUTO_TEST_CASE(Collide_RealContourParity)
static SHAPE_POLY_SET loadZone(const std::string &aFilename, int aZone)
static COLLISION_OUTPUT indexedCollision(const SHAPE_LINE_CHAIN &aA, const SHAPE_LINE_CHAIN &aB, int aClearance, int aOutputs)
static COLLISION_OUTPUT referenceCollision(const SHAPE_LINE_CHAIN &aA, const SHAPE_LINE_CHAIN &aB, int aClearance, int aOutputs)
static SHAPE_LINE_CHAIN extractSubchain(const SHAPE_LINE_CHAIN &aSource, int aStart, int aSegmentCount)
static void checkCollisionParity(const SHAPE_LINE_CHAIN &aA, const SHAPE_LINE_CHAIN &aB, int aClearance)
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708