• 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

54.24
/voronoi.cpp
1
/*
2
 * This file is part of pcb2gcode.
3
 *
4
 * Copyright (C) 2016 Nicola Corna <nicola@corna.info>
5
 *
6
 * pcb2gcode is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * pcb2gcode 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 pcb2gcode.  If not, see <http://www.gnu.org/licenses/>.
18
 */
19

20
 #include <iostream>
21

22
#include "voronoi.hpp"
23
#include "voronoi_visual_utils.hpp"
24
#include <list>
25
#include <map>
26
#include <algorithm>
27
using std::list;
28
using std::map;
29

30
#include <vector>
31
using std::vector;
32

33
// For use when we have to convert from float to long and back.
34
const double SCALE = 1000000.0;
35

36
multi_polygon_type_fp Voronoi::build_voronoi(
×
37
    const multi_polygon_type_fp& input,
38
    const box_type_fp& mask_bounding_box, coordinate_type_fp max_dist) {
39
  // We need to scale all the inputs and call the integer version.
40
  multi_polygon_type_fp scaled_input;
41
  bg::transform(input, scaled_input,
42
                bg::strategy::transform::scale_transformer<coordinate_type_fp, 2, 2>(
×
43
                    SCALE, SCALE));
44

45
  box_type_fp scaled_mask_bounding_box;
46
  bg::transform(mask_bounding_box, scaled_mask_bounding_box,
47
                bg::strategy::transform::scale_transformer<coordinate_type_fp, 2, 2>(
×
48
                    SCALE, SCALE));
49

50
  multi_polygon_type voronoi_input;
51
  bg::convert(scaled_input, voronoi_input);
52
  box_type voronoi_bounding_box;
53
  bg::convert(scaled_mask_bounding_box, voronoi_bounding_box);
54

55
  const multi_polygon_type_fp scaled_voronoi = build_voronoi(voronoi_input, voronoi_bounding_box, max_dist * SCALE);
×
56
  // Scale the result back down.
57
  multi_polygon_type_fp voronoi;
58
  bg::transform(scaled_voronoi, voronoi,
59
                bg::strategy::transform::scale_transformer<coordinate_type_fp, 2, 2>(
×
60
                    1.0/SCALE, 1.0/SCALE));
61
  return voronoi;
×
62
}
63

64
multi_polygon_type_fp Voronoi::build_voronoi(
4✔
65
    const multi_polygon_type& input,
66
    const box_type& mask_bounding_box, coordinate_type max_dist) {
67
    if (input.empty()) {
4✔
68
        return multi_polygon_type_fp();
69
    }
70

71
    // Bounding_box is a box that is big enough to hold all milling.
72
    box_type_fp bounding_box = bg::return_envelope<box_type_fp>(input);
73
    // Expand that bounding box by the provided bounding_box.
74
    bg::expand(bounding_box, mask_bounding_box);
75
    // Make it large enough so that any voronoi edges between it and
76
    // the input will surely line outside mask_bounding_box.
77
    const auto bounding_box_width = bounding_box.max_corner().x() - bounding_box.min_corner().x();
3✔
78
    const auto bounding_box_height = bounding_box.max_corner().y() - bounding_box.min_corner().y();
3✔
79
    bounding_box.max_corner().x(bounding_box.max_corner().x() + 2*bounding_box_width);
3✔
80
    bounding_box.min_corner().x(bounding_box.min_corner().x() - 2*bounding_box_width);
3✔
81
    bounding_box.max_corner().y(bounding_box.max_corner().y() + 2*bounding_box_height);
3✔
82
    bounding_box.min_corner().y(bounding_box.min_corner().y() - 2*bounding_box_height);
3✔
83
    // Each line segment from all the inputs.
84
    vector<segment_type_p> segments;
85
    // From which polygon each segment is sourced.
86
    std::vector<size_t> segments_to_poly;
87
    for (const auto& polygon : input) {
8✔
88
        copy_ring(polygon.outer(), segments);
5!
89
        for (const ring_type& ring : polygon.inners()) {
6✔
90
            copy_ring(ring, segments);
1!
91
        }
92
        segments_to_poly.push_back(segments.size());
5!
93
    }
94

95
    // Add the bounding box but without putting it in segments_to_poly
96
    // so that we won't put voronoi cells from it into the output.
97
    ring_type bounding_box_ring;
98
    bg::convert(bounding_box, bounding_box_ring);
99
    copy_ring(bounding_box_ring, segments);
3!
100

101
    voronoi_builder_type voronoi_builder;
3✔
102
    boost::polygon::insert(segments.begin(), segments.end(), &voronoi_builder);
103
    voronoi_diagram_type voronoi_diagram;
104
    voronoi_builder.construct(&voronoi_diagram);
3!
105

106
    // The output polygons which are voronoi shapes.  The outputs
107
    // match the inputs in number and position but the number of inner
108
    // rings for each output polygon might not match.
109
    multi_polygon_type_fp output;
110
    // Pre-allocate because we are going to add elements out of order.
111
    output.resize(input.size());
3!
112

113
    // To keep it simply, we first mark each edge as used if it won't
114
    // be part of the output.
115
    const int VISITED = 1;
116
    for (const edge_type& edge : voronoi_diagram.edges()) {
435✔
117
        if (!edge.is_primary() || // Not a real voronoi edge.
264✔
118
            same_poly(edge, *edge.twin(), segments_to_poly) || // This edge doesn't divide different shapes.
432✔
119
            // The edge belongs to the boundary.
120
            std::upper_bound(segments_to_poly.cbegin(), segments_to_poly.cend(), edge.twin()->cell()->source_index()) == segments_to_poly.cend()) {
432✔
121
            edge.color(edge.color() | VISITED);
122
        }
123
    }
124
    for (const edge_type& edge : voronoi_diagram.edges()) {
435✔
125
        if ((edge.color() & VISITED) == VISITED) {
432✔
126
            continue;  // Used already.
425✔
127
        }
128

129
        const edge_type* current_edge = &edge;
130
        const edge_type* const start_edge = current_edge;
131
        ring_type_fp ring;
132
        do {
133
            linestring_type_fp discrete_edge = edge_to_linestring(*current_edge, segments, bounding_box, max_dist);
87!
134
            // Don't push the last one because we'll put it at the end.
135
            for(std::size_t i=0; i<discrete_edge.size()-1; ++i)
174✔
136
            {
137
              bg::append(ring, discrete_edge.at(i));
138
            }
139

140
            current_edge->color(current_edge->color() | VISITED);  // Mark used
141
            current_edge = current_edge->next();
142

143
            // Check that we are still circling the same polygon for
144
            // our ring, and that we are on edge that is between
145
            // different traces, and that it's a primary edge.
146
            while (current_edge != start_edge &&
159✔
147
                   ((current_edge->color() & VISITED) == VISITED ||
88✔
148
                    // Still circling the same twin.  We look at twin because we need clockwise outer loops.
149
                    !same_poly(*current_edge->twin(), *start_edge->twin(), segments_to_poly))) {
88✔
150
                current_edge = current_edge->rot_next();
151
            }
152
        } while (current_edge != start_edge);
87✔
153

154
        if(!ring.empty())
7!
155
        {
156
          ring.push_back(ring.front());  // Close the ring.
7!
157
          size_t poly_index = std::distance(segments_to_poly.cbegin(), std::upper_bound(segments_to_poly.cbegin(), segments_to_poly.cend(), edge.twin()->cell()->source_index()));
7✔
158
          if (bg::area(ring) > 0) {
7✔
159
              // This is the outer ring of the poly.
160
              output[poly_index].outer().swap(ring);
161
          } else {
162
              // This has negative area, it must be a hole in the outer.
163
              output[poly_index].inners().push_back(ring);
2!
164
          }
165
        }
166
    }
167

168
    return output;
169
}
6✔
170

171
bool Voronoi::same_poly(const edge_type& edge0, const edge_type& edge1, const std::vector<size_t>& segments_to_poly) {
352✔
172
    return (std::upper_bound(segments_to_poly.cbegin(), segments_to_poly.cend(), edge0.cell()->source_index()) ==
352✔
173
            std::upper_bound(segments_to_poly.cbegin(), segments_to_poly.cend(), edge1.cell()->source_index()));
352✔
174
}
175

176
linestring_type_fp Voronoi::edge_to_linestring(const edge_type& edge, const vector<segment_type_p>& segments, const box_type_fp& bounding_box, coordinate_type max_dist) {
87!
177
    linestring_type_fp new_voronoi_edge;
178
    if (edge.is_finite()) {
179
        if (edge.is_linear()) {
87✔
180
            new_voronoi_edge.push_back(point_type_fp(edge.vertex0()->x(),
45!
181
                                                     edge.vertex0()->y()));
182
            new_voronoi_edge.push_back(point_type_fp(edge.vertex1()->x(),
45✔
183
                                                     edge.vertex1()->y()));
184
        } else {
185
            // It's a curve, it needs sampling.
186
            vector<point_type_fp_p> sampled_edge;
187
            sample_curved_edge(&edge, segments, sampled_edge, max_dist);
42!
188
            for (const auto& point : sampled_edge) {
126✔
189
                new_voronoi_edge.push_back(point_type_fp(point.x(), point.y()));
84!
190
            }
191
        }
42✔
192
    } else {
193
        // Infinite edge, only make it if it is inside the bounding_box.
194
        if ((edge.vertex0() == NULL ||
×
195
             bg::covered_by(point_type(edge.vertex0()->x(), edge.vertex0()->y()),
×
196
                            bounding_box)) &&
×
197
            (edge.vertex1() == NULL ||
×
198
             bg::covered_by(point_type(edge.vertex1()->x(), edge.vertex1()->y()),
×
199
                            bounding_box))) {
200
            vector<point_type_fp_p> clipped_edge;
201
            clip_infinite_edge(
×
202
                edge, segments, &clipped_edge, bounding_box);
203
            for (const auto& point : clipped_edge) {
×
204
                new_voronoi_edge.push_back(point_type_fp(point.x(), point.y()));
×
205
            }
206
        }
×
207
    }
208
    return new_voronoi_edge;
87✔
209
}
210

211
// Make segments from the ring and put them in segments.
212
void Voronoi::copy_ring(const ring_type& ring, vector<segment_type_p> &segments)
9✔
213
{
214
  for (auto iter = ring.begin(); iter + 1 != ring.end(); iter++) {
51✔
215
    segments.push_back(segment_type_p(point_type_p(iter->x(), iter->y()),
42✔
216
                                      point_type_p((iter + 1)->x(), (iter + 1)->y())));
84✔
217
  }
218
}
9✔
219

220
point_type_p Voronoi::retrieve_point(const cell_type& cell, const vector<segment_type_p> &segments) {
42✔
221
    source_index_type index = cell.source_index();
222
    source_category_type category = cell.source_category();
223

224
    if (category == boost::polygon::SOURCE_CATEGORY_SEGMENT_START_POINT)
42✔
225
        return boost::polygon::low(segments[index]);
226
    else
227
       return boost::polygon::high(segments[index]);
228
}
229

230
const segment_type_p& Voronoi::retrieve_segment(const cell_type& cell, const vector<segment_type_p> &segments) {
×
231
    source_index_type index = cell.source_index();
232
    return segments[index];
×
233
}
234

235
void Voronoi::sample_curved_edge(const edge_type *edge, const vector<segment_type_p> &segments,
42✔
236
                         vector<point_type_fp_p>& sampled_edge, coordinate_type_fp max_dist) {
237

238
    point_type_p point = edge->cell()->contains_point() ?
42✔
239
        retrieve_point(*(edge->cell()), segments) :
240
        retrieve_point(*(edge->twin()->cell()), segments);
42✔
241

242
    const segment_type_p& segment = edge->cell()->contains_point() ?
42✔
243
        retrieve_segment(*(edge->twin()->cell()), segments) :
244
        retrieve_segment(*(edge->cell()), segments);
245

246
    sampled_edge.push_back(point_type_fp_p(edge->vertex0()->x(), edge->vertex0()->y()));
42✔
247
    sampled_edge.push_back(point_type_fp_p(edge->vertex1()->x(), edge->vertex1()->y()));
42✔
248
    boost::polygon::voronoi_visual_utils<coordinate_type_fp>::discretize(point, segment, max_dist, &sampled_edge);
42✔
249
}
42✔
250

251
void Voronoi::clip_infinite_edge(
×
252
    const edge_type& edge, const vector<segment_type_p>& segments, std::vector<point_type_fp_p>* clipped_edge, const box_type_fp& bounding_box) {
253
    const cell_type& cell1 = *edge.cell();
254
    const cell_type& cell2 = *edge.twin()->cell();
255
    point_type_p origin, direction;
256
    // Infinite edges could not be created by two segment sites.
257
    if (cell1.contains_point() && cell2.contains_point()) {
×
258
        point_type_p p1 = retrieve_point(cell1, segments);
×
259
        point_type_p p2 = retrieve_point(cell2, segments);
×
260
        origin.x((p1.x() + p2.x()) * 0.5);
×
261
        origin.y((p1.y() + p2.y()) * 0.5);
×
262
        direction.x(p1.y() - p2.y());
×
263
        direction.y(p2.x() - p1.x());
×
264
    } else {
265
        origin = cell1.contains_segment() ?
×
266
            retrieve_point(cell2, segments) :
267
            retrieve_point(cell1, segments);
268
        segment_type_p segment = cell1.contains_segment() ?
269
            retrieve_segment(cell1, segments) :
270
            retrieve_segment(cell2, segments);
×
271
        coordinate_type dx = high(segment).x() - low(segment).x();
×
272
        coordinate_type dy = high(segment).y() - low(segment).y();
×
273
        if ((low(segment) == origin) ^ cell1.contains_point()) {
×
274
            direction.x(dy);
275
            direction.y(-dx);
×
276
        } else {
277
            direction.x(-dy);
×
278
            direction.y(dx);
279
        }
280
    }
281
    coordinate_type side = bounding_box.max_corner().x() - bounding_box.min_corner().x();
×
282
    coordinate_type koef =
283
        side / std::max(std::abs(direction.x()), std::abs(direction.y()));
×
284
    if (edge.vertex0() == NULL) {
×
285
        clipped_edge->push_back(point_type_fp_p(
×
286
                                    origin.x() - direction.x() * koef,
×
287
                                    origin.y() - direction.y() * koef));
×
288
    } else {
289
        clipped_edge->push_back(
290
            point_type_fp_p(edge.vertex0()->x(), edge.vertex0()->y()));
×
291
    }
292
    if (edge.vertex1() == NULL) {
×
293
        clipped_edge->push_back(point_type_fp_p(
×
294
                                    origin.x() + direction.x() * koef,
×
295
                                    origin.y() + direction.y() * koef));
×
296
    } else {
297
        clipped_edge->push_back(
298
            point_type_fp_p(edge.vertex1()->x(), edge.vertex1()->y()));
×
299
    }
300
}
×
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