• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pcb2gcode / pcb2gcode / 19811714133

24 Nov 2025 02:34PM UTC coverage: 59.007% (-15.0%) from 74.006%
19811714133

push

github

web-flow
Merge pull request #730 from mar0x/master

Enable windows build in CI

2199 of 4409 branches covered (49.88%)

Branch coverage included in aggregate %.

1902 of 2541 relevant lines covered (74.85%)

117318.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.17
/path_finding.cpp
1
#include <vector>
2
using std::vector;
3

4
#include <unordered_map>
5
using std::unordered_map;
6

7
#include <unordered_set>
8
using std::unordered_set;
9

10
#include <queue>
11
using std::priority_queue;
12

13
#include <utility>
14
using std::pair;
15
using std::make_pair;
16

17
#include <boost/graph/adjacency_list.hpp>
18
#include <boost/graph/astar_search.hpp>
19
#include <boost/optional.hpp>
20

21
#include "path_finding.hpp"
22
#include "options.hpp"
23
#include "geometry.hpp"
24
#include "bg_operators.hpp"
25
#include "bg_helpers.hpp"
26
#include "segment_tree.hpp"
27

28
namespace path_finding {
29

30
using boost::optional;
31
using boost::make_optional;
32

33
Neighbors::iterator Neighbors::iterator::operator++() {
194✔
34
  const auto& all_vertices_size = neighbors->vertices.size();
194✔
35
  do {
36
    // Move to a new valid point, even if it isn't a neighbor.
37
    point_index++;
332✔
38
  } while (point_index < all_vertices_size + 2 &&
689✔
39
           !neighbors->is_neighbor(**this));
303✔
40
  return *this;
193✔
41
}
42

43
bool Neighbors::iterator::operator!=(const Neighbors::iterator& other) const {
×
44
  return (point_index != other.point_index);
211✔
45
}
46

47
bool Neighbors::iterator::operator==(const Neighbors::iterator& other) const {
×
48
  return !(*this != other);
×
49
}
50

51
const point_type_fp& Neighbors::iterator::operator*() const {
515✔
52
  if (point_index == 0) {
515✔
53
    return neighbors->start;
48✔
54
  } else if (point_index == 1) {
467✔
55
    return neighbors->goal;
37✔
56
  } else {
57
    return neighbors->vertices[point_index-2];
430✔
58
  }
59
}
60

61
Neighbors::Neighbors(const point_type_fp& start, const point_type_fp& goal,
×
62
                     const point_type_fp& current,
63
                     const coordinate_type_fp& max_path_length,
64
                     const std::vector<point_type_fp>& vertices,
65
                     const PathFindingSurface* pfs) :
30✔
66
    start(start),
30✔
67
    goal(goal),
30✔
68
    current(current),
30✔
69
    max_path_length_squared(max_path_length),
30✔
70
    vertices(vertices),
30✔
71
    pfs(pfs) {}
30✔
72

73
// Returns a valid neighbor index that is either the one provided or
74
// the next higher valid one.
75
inline bool Neighbors::is_neighbor(const point_type_fp p) const {
333✔
76
  if (p == current) {
333✔
77
    return false;
78
  }
79
  pfs->decrement_tries();
297✔
80
  if (bg::distance(current, p) + bg::distance(p, goal) > max_path_length_squared) {
296!
81
    return false;
82
  }
83
  if (!pfs->in_surface(current, p)) {
296✔
84
    return false;
85
  }
86
  return true;
87
}
88

89
Neighbors::iterator Neighbors::begin() const {
30!
90
  auto ret = iterator(this, 0);
91
  if (ret == end()) {
30!
92
    // Can't dereferfence the end.
93
    return ret;
×
94
  }
95
  if (is_neighbor(*ret)) {
30✔
96
    // This is a valid begin.
97
    return ret;
18✔
98
  } else {
99
    // This position is invalid but we can increment to get to a valid one.
100
    ++ret;
12✔
101
    return ret;
12✔
102
  }
103
}
104

105
Neighbors::iterator Neighbors::end() const {
×
106
  return iterator(this, vertices.size()+2);
60!
107
}
108

109
vector<pair<point_type_fp, point_type_fp>> get_all_segments(
20✔
110
    const vector<vector<std::reference_wrapper<const ring_type_fp>>>& all_rings) {
111
  vector<pair<point_type_fp, point_type_fp>> ret;
112
  for (const auto& x0 : all_rings) {
43✔
113
    for (auto x1 : x0) {
51✔
114
      for (size_t i = 0; i+1 < x1.get().size(); i++) {
257✔
115
        ret.emplace_back(x1.get()[i], x1.get()[i+1]);
229!
116
      }
117
    }
118
  }
119
  return ret;
20✔
120
}
×
121

122
vector<std::reference_wrapper<const ring_type_fp>> get_all_rings(const multi_polygon_type_fp& mpolys) {
23✔
123
  vector<std::reference_wrapper<const ring_type_fp>> ret;
124
  for (const auto& poly : mpolys) {
49✔
125
    ret.push_back(poly.outer());
26✔
126
    for (const auto& inner : poly.inners()) {
28✔
127
      ret.push_back(inner);
2✔
128
    }
129
  }
130
  return ret;
23✔
131
}
×
132

133
vector<vector<std::reference_wrapper<const ring_type_fp>>> get_all_rings(
20✔
134
    const nested_multipolygon_type_fp& mpolys) {
135
  vector<vector<std::reference_wrapper<const ring_type_fp>>> ret;
136
  for (const auto& poly : mpolys) {
38✔
137
    ret.push_back(get_all_rings(poly.outer()));
36!
138
    for (const auto& inner: poly.inners()) {
23✔
139
      ret.push_back(get_all_rings(inner));
10!
140
    }
141
  }
142
  return ret;
20✔
143
}
×
144

145
PathFindingSurface::PathFindingSurface(const optional<multi_polygon_type_fp>& keep_in,
20✔
146
                                       const multi_polygon_type_fp& keep_out,
147
                                       const coordinate_type_fp tolerance) {
20!
148
  if (keep_in) {
20✔
149
    multi_polygon_type_fp total_keep_in = *keep_in - keep_out;
11!
150

151
    total_keep_in_grown.emplace();
152
    for (const auto& poly : total_keep_in) {
22✔
153
      all_vertices.emplace_back();
11!
154
      all_vertices.back().emplace_back(poly.outer().cbegin(), poly.outer().cend());
11!
155
      total_keep_in_grown->emplace_back(bg_helpers::buffer_miter(poly.outer(), tolerance));
22!
156
      for (const auto& inner : poly.inners()) {
14✔
157
        all_vertices.back().emplace_back(inner.cbegin(), inner.cend());
3!
158
        // Because the inner is reversed, we need to reverse it so
159
        // that the buffer algorithm won't get confused.
160
        auto temp_inner = inner;
161
        bg::reverse(temp_inner);
162
        // tolerance needs to be inverted because growing a shape
163
        // shrinks the holes in it.
164
        total_keep_in_grown->back().inners().push_back(bg_helpers::buffer_miter(temp_inner, -tolerance));
6!
165
      }
166
    }
167
  } else {
168
    for (const auto& poly : keep_out){
16✔
169
      all_vertices.emplace_back();
7!
170
      all_vertices.back().emplace_back(poly.outer().cbegin(), poly.outer().cend());
7!
171
      keep_out_shrunk.emplace_back(bg_helpers::buffer_miter(poly.outer(), -tolerance));
14!
172
      for (const auto& inner : poly.inners()) {
9✔
173
        all_vertices.back().emplace_back(inner.cbegin(), inner.cend());
2!
174
        // Because the inner is reversed, we need to reverse it so
175
        // that the buffer algorithm won't get confused.
176
        auto temp_inner = inner;
177
        bg::reverse(temp_inner);
178
        // tolerance needs to be inverted because shrinking a shape
179
        // grows the holes in it.
180
        keep_out_shrunk.back().inners().push_back(bg_helpers::buffer_miter(temp_inner, tolerance));
4!
181
      }
182
    }
183
  }
184
  const nested_multipolygon_type_fp& poly_to_search = total_keep_in_grown ? *total_keep_in_grown : keep_out_shrunk;
20✔
185
  const vector<vector<std::reference_wrapper<const ring_type_fp>>>& all_rings = get_all_rings(poly_to_search);
20!
186
  const auto& all_segments = get_all_segments(all_rings);
20!
187
  segment_tree::SegmentTree x(all_segments);
20!
188
  tree = std::move(x);
189

190
  for (auto av : all_vertices) {
38!
191
    sort(av.begin(), av.end());
18✔
192
    av.erase(std::unique(av.begin(), av.end()), av.end());
18✔
193
  }
18✔
194
}
20✔
195

196
boost::optional<MPRingIndices> inside_multipolygon(const point_type_fp& p,
40✔
197
                                                   const multi_polygon_type_fp& mp) {
198
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
55✔
199
    const auto& poly = mp[poly_index];
200
    if (point_in_ring(p, poly.outer())) {
46✔
201
      // Might be part of this shape but only if the point isn't in
202
      // the inners.
203
      MPRingIndices ring_indices{{poly_index, {0}}};
76!
204
      for (size_t inner_index = 0; inner_index < poly.inners().size(); inner_index++) {
49✔
205
        const auto& inner = poly.inners()[inner_index];
206
        if (!point_in_ring(p, inner)) {
18✔
207
          // We'll have to make sure not to cross this inner.
208
          ring_indices.back().second.emplace_back(inner_index+1);
11!
209
        } else {
210
          break; // We're inside one of the inners so give up.
211
        }
212
      }
213
      if (ring_indices.back().second.size() == poly.inners().size() + 1) {
38✔
214
        // We never hit the break so we're inside this shape so we're
215
        // done.
216
        return {ring_indices};
217
      }
218
      // We're inside the outer but also inside an inner!  There might
219
      // be another shape inside this hold so we'll ignore this one
220
      // and keep searching.
221
    }
38✔
222
  }
223
  return boost::none;
9✔
224
}
76!
225

226
boost::optional<MPRingIndices> outside_multipolygon(const point_type_fp& p,
35✔
227
                                                    const multi_polygon_type_fp& mp) {
228
  MPRingIndices ring_indices;
229
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
70✔
230
    const auto& poly = mp[poly_index];
231
    if (point_in_ring(p, poly.outer())) {
47✔
232
      // We're inside the outer, maybe we're in an inner?  If not, we
233
      // aren't outside at all and we'll just give up.
234
      bool in_any_inner = false;
235
      for (size_t i = 0; i < poly.inners().size(); i++) {
23✔
236
        const auto& inner = poly.inners()[i];
237
        if (point_in_ring(p, inner)) {
11✔
238
          in_any_inner = true;
239
          ring_indices.emplace_back(poly_index, vector<size_t>{i+1});
6!
240
          break;
241
        }
242
      }
243
      if (!in_any_inner) {
244
        // We're inside the outer but not in any of the inners, so
245
        // we're in the shape, but we want to be outside the shape, so
246
        // we've failed.
247
        return boost::none;
12✔
248
      }
249
    } else {
250
      // We need to keep out of this outer.
251
      ring_indices.emplace_back(poly_index, vector<size_t>{0});
29!
252
      // No need to examine the inners which we can't possibly be inside.
253
    }
254
  }
255
  return {ring_indices};
256
}
35✔
257

258
boost::optional<RingIndices> inside_multipolygons(
25✔
259
    const point_type_fp& p,
260
    const nested_multipolygon_type_fp& mp) {
261
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
29✔
262
    const auto& poly = mp[poly_index];
263
    boost::optional<MPRingIndices> inside_mp = inside_multipolygon(p, poly.outer());
25✔
264
    if (inside_mp) {
25✔
265
      // Might be part of this shape but only if the point isn't in
266
      // the inners.
267
      RingIndices ring_indices{{poly_index, {{0, *inside_mp}}}};
69!
268
      for (size_t inner_index = 0; inner_index < poly.inners().size(); inner_index++) {
28✔
269
        const auto& inner = poly.inners()[inner_index];
270
        auto outside_mp = outside_multipolygon(p, inner);
7!
271
        if (outside_mp) {
7✔
272
          // We'll have to make sure not to cross this inner.
273
          ring_indices.back().second.emplace_back(inner_index+1, *outside_mp);
5!
274
        } else {
275
          break; // We're inside one of the inners so give up.
276
        }
277
      }
278
      if (ring_indices.back().second.size() == poly.inners().size() + 1) {
23✔
279
        // We never hit the break so we're inside this shape so we're
280
        // done.
281
        return {ring_indices};
282
      }
283
      // We're inside the outer but also inside an inner!  It might
284
      // be an outer in an inner so we'll ignore this one and keep
285
      // searching.
286
    }
23✔
287
  }
288
  return boost::none;
4✔
289
}
69!
290

291
boost::optional<RingIndices> outside_multipolygons(
19✔
292
    const point_type_fp& p,
293
    const nested_multipolygon_type_fp& mp) {
294
  RingIndices ring_indices;
295
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
33✔
296
    const auto& poly = mp[poly_index];
297
    auto outside_mp = outside_multipolygon(p, poly.outer());
16!
298
    if (!outside_mp) {
16✔
299
      // We're inside the outer, maybe we're in an inner?  If not, we
300
      // aren't outside at all and we'll just give up.
301
      bool in_any_inner = false;
302
      for (size_t inner_index = 0; inner_index < poly.inners().size(); inner_index++) {
5✔
303
        const auto& inner = poly.inners()[inner_index];
304
        auto inside_mp = inside_multipolygon(p, inner);
3!
305
        if (inside_mp) {
3!
306
          in_any_inner = true;
307
          ring_indices.emplace_back(poly_index, vector<pair<size_t, MPRingIndices>>{{inner_index + 1, *inside_mp}});
9!
308
          break;
309
        }
310
      }
311
      if (!in_any_inner) {
312
        // We're inside the outer but not in any of the inners, so
313
        // we're in the shape, but we want to be outside the shape, so
314
        // we've failed.
315
        return boost::none;
316
      }
317
    } else {
318
      // We need to keep out of this outer.
319
      ring_indices.emplace_back(poly_index, vector<pair<size_t, MPRingIndices>>{{0, *outside_mp}});
22!
320
    }
321
  }
322
  return {ring_indices};
323
}
19✔
324

325
/* Given a point, determine if the point is in the search surface.  If
326
   so, return a non-default value, otherwise default value.  If two
327
   points return the same value, there is a path between them in the
328
   surface.  If not then there cannot be a path between them.  The
329
   value will actually be a vector of size_t that indicates which
330
   rings in the stored polygon should be used for the generated points
331
   in the path and also for the collision detection. */
332
const boost::optional<SearchKey>& PathFindingSurface::in_surface(point_type_fp p) const {
45✔
333
  auto memoized_result = point_in_surface_memo.find(p);
334
  if (memoized_result != point_in_surface_memo.cend()) {
45✔
335
    return memoized_result->second;
1✔
336
  }
337
  boost::optional<RingIndices> maybe_ring_indices;
338
  if (total_keep_in_grown) {
44✔
339
    maybe_ring_indices = inside_multipolygons(p, *total_keep_in_grown);
50!
340
  } else {
341
    maybe_ring_indices = outside_multipolygons(p, keep_out_shrunk);
38!
342
  }
343
  if (!maybe_ring_indices) {
44✔
344
    return point_in_surface_memo.emplace(p, boost::none).first->second;
6✔
345
  }
346
  const auto& ring_indices = *maybe_ring_indices;
347
  // Check if this one is already in the cache.
348
  const auto& find_result = ring_indices_lookup.find(std::cref(ring_indices));
349
  if (find_result != ring_indices_lookup.cend()) {
38✔
350
    // Found in the cache so we can use that.
351
    return point_in_surface_memo.emplace(p, find_result->second).first->second;
36!
352
  }
353
  // Not found so we need to add it to the cache.
354
  ring_indices_cache.push_back(ring_indices);
20!
355
  ring_indices_lookup.emplace(ring_indices_cache.back(), ring_indices_cache.size()-1);
20!
356
  return point_in_surface_memo.emplace(p, ring_indices_cache.size()-1).first->second;
40!
357
}
358

359
void PathFindingSurface::decrement_tries() const {
303✔
360
  if (tries) {
303✔
361
    if (*tries == 0) {
4✔
362
      throw GiveUp();
1✔
363
    }
364
    (*tries)--;
3✔
365
  }
366
}
302✔
367

368
// Return true if this edge from a to b is part of the path finding surface.
369
bool PathFindingSurface::in_surface(
430✔
370
    const point_type_fp& a, const point_type_fp& b) const {
371
  if (b < a) {
430✔
372
    return in_surface(b, a);
120✔
373
  }
374
  const auto key = make_pair(a, b);
375
  auto memoized_result = edge_in_surface_memo.find(key);
376
  if (memoized_result != edge_in_surface_memo.cend()) {
310✔
377
    return memoized_result->second;
87✔
378
  }
379
  auto found_intersection = tree.intersects(a, b);
223✔
380
  edge_in_surface_memo.emplace(key, !found_intersection);
223✔
381
  return !found_intersection;
223✔
382
}
383

384
// Return all possible neighbors of current.  A neighbor can be
385
// start, end, or any of the points in all_vertices.  But only the
386
// ones that are in_surface are returned.
387
Neighbors PathFindingSurface::neighbors(const point_type_fp& start, const point_type_fp& goal,
×
388
                                        const coordinate_type_fp& max_path_length,
389
                                        SearchKey search_key,
390
                                        const point_type_fp& current) const {
391
  return Neighbors(start, goal, current, max_path_length, vertices(search_key), this);
×
392
}
393

394
// Return a path from the start to the current.  Always return at
395
// least two points.
396
linestring_type_fp build_path(
7✔
397
    point_type_fp current,
398
    const unordered_map<point_type_fp, point_type_fp>& came_from) {
399
  linestring_type_fp result;
400
  while (came_from.count(current)) {
401
    result.push_back(current);
18!
402
    current = came_from.at(current);
18✔
403
  }
404
  result.push_back(current);
7!
405
  bg::reverse(result);
406
  return result;
7✔
407
}
408
optional<linestring_type_fp> PathFindingSurface::find_path(
14✔
409
    const point_type_fp& start, const point_type_fp& goal,
410
    const coordinate_type_fp& max_path_length,
411
    SearchKey search_key) const {
412
  // Connect if a direct connection is possible.  This also takes care
413
  // of the case where start == goal.
414
  try {
415
    if (in_surface(start, goal)) {
14!
416
      decrement_tries();
6!
417
      if (bg::comparable_distance(start, goal) < max_path_length * max_path_length) {
6✔
418
        // in_surface builds up some structures that are only efficient if
419
        // we're doing many tries.
420
        return {{start, goal}};
10!
421
      } else {
422
        // If the straight line was too long then there is no way to connect.
423
        return boost::none;
1✔
424
      }
425
    }
426
  } catch (GiveUp g) {
×
427
    return boost::none;
428
  }
×
429
  // Do astar.
430
  priority_queue<pair<coordinate_type_fp, point_type_fp>,
431
                 vector<pair<coordinate_type_fp, point_type_fp>>,
432
                 std::greater<pair<coordinate_type_fp, point_type_fp>>> open_set;
433
  open_set.emplace(bg::distance(start, goal), start);
8!
434
  unordered_set<point_type_fp> closed_set;
435
  unordered_map<point_type_fp, point_type_fp> came_from;
436
  unordered_map<point_type_fp, coordinate_type_fp> g_score; // Empty should be considered infinity.
437
  g_score[start] = 0;
8✔
438
  while (!open_set.empty()) {
37!
439
    const auto current = open_set.top().second;
37✔
440
    open_set.pop();
37✔
441
    if (current == goal) {
442
      // We're done.
443
      return boost::make_optional(build_path(current, came_from));
14!
444
    }
445
    if (closed_set.count(current) > 0) {
×
446
      // Skip this because we already "removed it", sort of.
447
      continue;
×
448
    }
449
    try {
450
      const auto current_neighbors = neighbors(
451
          start, goal,
452
          max_path_length - g_score.at(current),
30!
453
          search_key,
454
          current);
455
      for (const auto& neighbor : current_neighbors) {
212!
456
        const auto tentative_g_score = g_score.at(current) + bg::distance(current, neighbor);
182✔
457
        if (g_score.count(neighbor) == 0 || tentative_g_score < g_score.at(neighbor)) {
119✔
458
          // This path to neighbor is better than any previous one.
459
          came_from[neighbor] = current;
64!
460
          g_score[neighbor] = tentative_g_score;
64✔
461
          open_set.emplace(tentative_g_score + bg::distance(neighbor, goal), neighbor);
64!
462
        }
463
      }
464
    } catch (GiveUp g) {
1!
465
      return boost::none;
466
    }
1✔
467
    // Because we can't delete from the open_set, we'll just marked
468
    // items as closed and ignore them later.
469
    closed_set.insert(current);
470
  }
471
  return boost::none;
×
472
}
473

474
optional<linestring_type_fp> PathFindingSurface::find_path(
×
475
    const point_type_fp& start, const point_type_fp& goal,
476
    const coordinate_type_fp& max_path_length,
477
    const boost::optional<size_t>& max_tries,
478
    SearchKey search_key) const {
479
  if (max_tries) {
×
480
    if (*max_tries == 0) {
×
481
      return boost::none;
×
482
    }
483
    tries.emplace(*max_tries);
484
  } else {
485
    tries = boost::none;
486
  }
487
  return find_path(start, goal, max_path_length, search_key);
×
488
}
489

490
optional<linestring_type_fp> PathFindingSurface::find_path(
18✔
491
    const point_type_fp& start, const point_type_fp& goal,
492
    const coordinate_type_fp& max_path_length,
493
    const boost::optional<size_t>& max_tries) const {
494
  if (max_tries) {
18✔
495
    if (*max_tries == 0) {
3✔
496
      return boost::none;
1✔
497
    }
498
    tries.emplace(*max_tries);
499
  } else {
500
    tries = boost::none;
501
  }
502

503
  auto ring_indices = in_surface(start);
17!
504
  if (!ring_indices) {
17!
505
    // Start is not in the surface.
506
    return boost::none;
×
507
  }
508
  if (ring_indices != in_surface(goal)) {
34✔
509
    // Either goal is not in the surface or it's in a region unreachable by start.
510
    return boost::none;
3✔
511
  }
512
  return find_path(start, goal, max_path_length, *ring_indices);
14✔
513
}
514

515
const std::vector<point_type_fp>&
516
PathFindingSurface::vertices(SearchKey search_key) const {
30✔
517
  auto memoized_result = vertices_memo.find(search_key);
518
  if (memoized_result != vertices_memo.cend()) {
30✔
519
    return memoized_result->second;
22✔
520
  }
521
  std::vector<point_type_fp> ret;
522
  const auto& vertices = all_vertices;
523
  const auto& ring_indices = ring_indices_cache.at(search_key);
8!
524
  for (size_t poly_index = 0; poly_index < ring_indices.size() ; poly_index++) {
16✔
525
    // This is the poly to look at.
526
    const auto& poly_ring_index = ring_indices[poly_index];
527
    // These are the vertices for that poly.
528
    const auto& poly_vertices = vertices[poly_ring_index.first];
8✔
529
    for (size_t ring_index = 0; ring_index < poly_ring_index.second.size(); ring_index++) {
18✔
530
      const auto& ring_ring_index = poly_ring_index.second[ring_index];
531
      const auto& ring_vertices = poly_vertices[ring_ring_index.first];
10!
532
      ret.insert(ret.cend(), ring_vertices.cbegin(), ring_vertices.cend());
10!
533
    }
534
  }
535
  return vertices_memo.emplace(search_key, ret).first->second;
8✔
536
}
8✔
537

538
} //namespace path_finding
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc