]> git.draconx.ca Git - slotifier.git/blob - src/slotifier.c
Trivial manual fixes.
[slotifier.git] / src / slotifier.c
1 /*
2  * Utility to convert overlapping Excellon drill hits into drill slots.
3  * Copyright © 2018, 2021, 2023 Nick Bowler
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  */
18
19 #include <config.h>
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <stdint.h>
24 #include <assert.h>
25 #include <locale.h>
26
27 #include <getopt.h>
28 #include <gettext.h>
29 #include <localcharset.h>
30 #include <mbswidth.h>
31
32 #include <CNearTree.h>
33 #include <gerbv.h>
34
35 #include "options.h"
36 #include "help.h"
37 #include "xtra.h"
38 #include "copysym.h"
39
40 #if !ENABLE_NLS
41 #  undef ENABLE_NLS
42 #  define ENABLE_NLS 0
43 #endif
44
45 #define _(x) (gettext(x))
46
47 static const char *progname = "slotifier";
48 static unsigned verbose;
49
50 static void print_version(void)
51 {
52         const char *copysign = copyright_symbol(locale_charset());
53
54         puts(PACKAGE_STRING);
55         printf("Copyright %s 2023 Nick Bowler.\n", copysign);
56         puts("License GPLv3+: GNU GPL version 3 or any later version.");
57         puts("This is free software: you are free to change and redistribute it.");
58         puts("There is NO WARRANTY, to the extent permitted by law.");
59 }
60
61 static void print_usage(FILE *f)
62 {
63         fprintf(f, _("Usage: %s [options] [-o filename] filename\n"), progname);
64         if (f != stdout)
65                 fprintf(f, _("Try %s --help for more information.\n"),
66                            progname);
67 }
68
69 static void print_help(const struct option *lopts)
70 {
71         const struct option *opt;
72
73         print_usage(stdout);
74
75         puts(_("This is \"slotifier\": a tool to convert overlapping drill hits in Excellon\n"
76                "drill files to G85 drill slots."));
77         putchar('\n');
78
79         puts(_("Options:"));
80         for (opt = lopts; opt->val; opt++) {
81                 struct lopt_help help;
82
83                 if (!lopt_get_help(opt, &help))
84                         continue;
85
86                 help_print_option(opt, help.arg, help.desc, 20);
87         }
88         putchar('\n');
89
90         puts(_("For more information, see the slotifier(1) man page."));
91         putchar('\n');
92
93         /*
94          * TRANSLATORS: Please add *another line* indicating where users should
95          * report translation bugs.
96          */
97         printf(_("Report bugs to <%s>.\n"), PACKAGE_BUGREPORT);
98 }
99
100 static void init_i18n(void)
101 {
102         setlocale(LC_ALL, "");
103         bindtextdomain(PACKAGE, LOCALEDIR);
104         textdomain(PACKAGE);
105 }
106
107 static CNearTreeHandle build_search_tree(gerbv_image_t *drill)
108 {
109         CNearTreeHandle t;
110         gerbv_net_t *x;
111
112         if (CNearTreeCreate(&t, 2, CNEARTREE_TYPE_DOUBLE|CNEARTREE_NORM_L2))
113                 return 0;
114
115         /* Build a search tree from all the holes. */
116         for (x = drill->netlist; x; x = x->next) {
117                 double xy[2] = { x->start_x, x->start_y };
118
119                 assert(x->aperture >= 0 && x->aperture < APERTURE_MAX);
120
121                 /* Skip things that aren't drill hits */
122                 if (!drill->aperture[x->aperture])
123                         continue;
124
125                 /* Holes are marked as "flashing"; otherwise it's an existing
126                  * slot; skip it. */
127                 if (x->aperture_state != GERBV_APERTURE_STATE_FLASH)
128                         continue;
129
130                 if (CNearTreeInsert(t, xy, x)) {
131                         CNearTreeFree(&t);
132                         return 0;
133                 }
134         }
135
136         if (CNearTreeCompleteDelayedInsert(t)) {
137                 CNearTreeFree(&t);
138                 return 0;
139         }
140
141         return t;
142 }
143
144 static gerbv_aperture_type_t tool_type(gerbv_image_t *drill, int aperture)
145 {
146         gerbv_aperture_t *tool = drill->aperture[abs(aperture)];
147
148         return tool->type;
149 }
150
151 static double tool_radius(gerbv_image_t *drill, int aperture)
152 {
153         gerbv_aperture_t *tool = drill->aperture[abs(aperture)];
154
155         /* Half a mil slop to decisively include points on boundary. */
156         return tool->parameter[0] / 2.0 + 0.0005;
157 }
158
159 static int holes_overlap(gerbv_image_t *drill, gerbv_net_t *a, gerbv_net_t *b)
160 {
161         double d = hypot(a->start_x - b->start_x, a->start_y - b->start_y);
162
163         return tool_radius(drill, a->aperture) >= d
164             || tool_radius(drill, b->aperture) >= d;
165 }
166
167 static int combine_holes(gerbv_image_t *drill, gerbv_net_t *hole,
168                          CNearTreeHandle t)
169 {
170         CVectorHandle group, tmp;
171         int biggest_tool, ret = -1;
172         double biggest_r;
173         size_t i, j;
174
175         /*
176          * Since we consider holes in order of decreasing size, the initial hole
177          * considered is by definition the biggest one we will find in a group.
178          */
179         biggest_r = tool_radius(drill, (biggest_tool = hole->aperture));
180
181         if (CVectorCreate(&group, sizeof (gerbv_net_t *), 10)) {
182                 fprintf(stderr, _("%s: failed to allocate memory\n"), progname);
183                 return -1;
184         }
185
186         if (CVectorCreate(&tmp, sizeof (void *), 10)) {
187                 fprintf(stderr, _("%s: failed to allocate memory\n"), progname);
188                 CVectorFree(&group);
189                 return -1;
190         }
191
192         /*
193          * Breadth-first nearest neighbour search of holes.  We negate the
194          * aperture to indicate which holes have been previously visited.
195          */
196         CVectorAddElement(group, &hole);
197         hole->aperture = -hole->aperture;
198
199         for (i = 0; i < CVectorSize(group); i++) {
200                 double xy[2];
201
202                 CVectorGetElement(group, &hole, i);
203
204                 assert(tool_type(drill, hole->aperture) == GERBV_APTYPE_CIRCLE);
205                 assert(tool_radius(drill, hole->aperture) <= biggest_r);
206
207                 xy[0] = hole->start_x; xy[1] = hole->start_y;
208                 if (CNearTreeFindInSphere(t, biggest_r, 0, tmp, xy, 1) != 0) {
209                         /* We should always should find at least one hole! */
210                         fprintf(stderr, _("%s: fatal error searching holes\n"),
211                                         progname);
212                         goto err;
213                 }
214
215                 for (j = 0; j < CVectorSize(tmp); j++) {
216                         /* I don't know why, but CNearTree returns a list
217                          * of pointers to its internal copies of pointers
218                          * to the objects in the tree.  So we need this
219                          * double indirection to get the actual hole. */
220                         gerbv_net_t *newhole;
221                         void *p;
222
223                         CVectorGetElement(tmp, &p, j);
224                         newhole = *(void **)p;
225
226                         if (newhole->aperture < 0)
227                                 continue; /* already visited */
228
229                         if (holes_overlap(drill, hole, newhole)) {
230                                 CVectorAddElement(group, &newhole);
231                                 newhole->aperture = -newhole->aperture;
232                         }
233                 }
234         }
235
236         /* Compare each pair of matched points to find the longest slot. */
237         if (CVectorSize(group) > 1) {
238                 int biggest_slot = 0;
239                 double bestlen = 0;
240
241                 for (i = 0; i < CVectorSize(group) - 1; i++) {
242                         CVectorGetElement(group, &hole, i);
243                         for (j = i+1; j < CVectorSize(group); j++) {
244                                 gerbv_net_t *h2;
245                                 double newlen;
246
247                                 CVectorGetElement(group, &h2, j);
248                                 newlen = hypot(hole->start_x - h2->start_x,
249                                                hole->start_y - h2->start_y);
250
251                                 if (newlen > bestlen) {
252                                         hole->stop_x = h2->start_x;
253                                         hole->stop_y = h2->stop_y;
254                                         bestlen = newlen;
255                                         biggest_slot = i;
256                                 }
257                         }
258                 }
259
260                 CVectorGetElement(group, &hole, biggest_slot);
261                 hole->aperture = biggest_tool;
262                 hole->aperture_state = GERBV_APERTURE_STATE_ON;
263
264                 if (verbose) {
265                         const char *fmt = ngettext(
266                                 "%s: merged %zu hole into slot (%.4f,%.4f)-(%.4f,%.4f)\n",
267                                 "%s: merged %zu holes into slot (%.4f,%.4f)-(%.4f,%.4f)\n",
268                                 CVectorSize(group));
269
270                         fprintf(stderr, fmt, progname, CVectorSize(group),
271                                              hole->start_x, hole->start_y,
272                                              hole->stop_x, hole->stop_y);
273                 }
274         } else {
275                 /* The only hole we found was our original one, restore
276                  * initial state. */
277                 CVectorGetElement(group, &hole, 0);
278
279                 assert(hole->aperture < 0);
280                 hole->aperture = -hole->aperture;
281
282                 if (verbose > 1) {
283                         fprintf(stderr, _("%s: hole at (%.4f,%.4f) not merged\n"),
284                                         progname, hole->start_x, hole->start_y);
285                 }
286         }
287
288         ret = 0;
289 err:
290         CVectorFree(&tmp);
291         CVectorFree(&group);
292         return ret;
293 }
294
295 /*
296  * Order two holes by hole diameter.
297  */
298 static gerbv_image_t *hsc_drill_data;
299 static int hole_size_cmp(const void *a_, const void *b_)
300 {
301         gerbv_net_t * const *a = a_, * const *b = b_;
302         gerbv_aperture_t *ta, *tb;
303
304         ta = hsc_drill_data->aperture[abs(a[0]->aperture)];
305         assert(ta->type == GERBV_APTYPE_CIRCLE);
306
307         tb = hsc_drill_data->aperture[abs(b[0]->aperture)];
308         assert(tb->type == GERBV_APTYPE_CIRCLE);
309
310         if (ta->parameter[0] > tb->parameter[0])
311                 return -1;
312         if (ta->parameter[0] < tb->parameter[0])
313                 return 1;
314         return 0;
315 }
316
317 static void sort_holes_by_size(gerbv_image_t *drill, CVectorHandle work)
318 {
319         hsc_drill_data = drill;
320         qsort(work->array, work->size, work->elementsize, hole_size_cmp);
321 }
322
323 static int slotify(gerbv_image_t *drill)
324 {
325         CVectorHandle holes, work;
326         CNearTreeHandle t;
327         int ret = 0;
328         size_t i;
329
330         t = build_search_tree(drill);
331         if (!t) {
332                 fprintf(stderr, _("%s: failed to build search tree\n"),
333                                 progname);
334                 return -1;
335         }
336
337         CNearTreeObjects(t, &holes);
338         if (!holes)
339                 goto err_free_tree;
340
341         if (CVectorCreate(&work, sizeof (gerbv_net_t *), CVectorSize(holes))) {
342                 fprintf(stderr, _("%s: failed to allocate memory\n"), progname);
343                 goto err_free_tree;
344         }
345
346         memcpy(work->array, holes->array, holes->size * holes->elementsize);
347         work->size = holes->size;
348         sort_holes_by_size(drill, work);
349
350         for (i = 0; i < CVectorSize(work); i++) {
351                 gerbv_net_t *hole;
352
353                 CVectorGetElement(work, &hole, i);
354                 /* Skip holes we've already looked at */
355                 if (hole->aperture < 0)
356                         continue;
357                 if (hole->aperture_state == GERBV_APERTURE_STATE_ON)
358                         continue;
359
360                 if (verbose > 1) {
361                         fprintf(stderr, _("%s: checking hole at (%.4f,%.4f) for overlaps\n"),
362                                         progname, hole->start_x, hole->start_y);
363                 }
364
365                 if (combine_holes(drill, hole, t) < 0) {
366                         ret = -1;
367                         break;
368                 }
369         }
370
371         /* Clean out any holes we don't need anymore */
372         for (i = 0; i < CVectorSize(holes); i++) {
373                 gerbv_net_t *hole;
374
375                 CVectorGetElement(holes, &hole, i);
376                 if (hole->aperture < 0)
377                         gerbv_image_delete_net(hole);
378         }
379
380         CVectorFree(&work);
381 err_free_tree:
382         CNearTreeFree(&t);
383         return ret;
384 }
385
386 static int do_cmdline(int argc, char **argv, const char **outfile)
387 {
388         const char *sopts = SOPT_STRING;
389         int opt;
390
391         XTRA_PACKED_LOPTS(lopts);
392
393         if (argc > 0)
394                 progname = argv[0];
395
396         while ((opt = getopt_long(argc, argv, sopts, lopts, NULL)) != -1) {
397                 switch (opt) {
398                 case 'o':
399                         *outfile = optarg;
400                         break;
401                 case 'v':
402                         verbose++;
403                         break;
404                 case 'V':
405                         print_version();
406                         return 1;
407                 case 'H':
408                         print_help(lopts);
409                         return 1;
410                 default:
411                         print_usage(stderr);
412                         return -1;
413                 }
414         }
415
416         if (!argv[optind]) {
417                 fprintf(stderr, _("%s: error: must specify a filename\n"),
418                                 progname);
419                 print_usage(stderr);
420                 return -1;
421         }
422
423         if (optind + 1 < argc) {
424                 fprintf(stderr, _("%s: error: excess command-line arguments\n"),
425                                 progname);
426                 print_usage(stderr);
427                 return -1;
428         }
429
430         return 0;
431 }
432
433 int main(int argc, char **argv)
434 {
435         const char *outfile = "/dev/stdout";
436         gerbv_project_t *gp;
437         gerbv_image_t *drill;
438         int ret = 0;
439
440         init_i18n();
441         switch (do_cmdline(argc, argv, &outfile)) {
442         case -1: return EXIT_FAILURE;
443         case 1: return EXIT_SUCCESS;
444         }
445
446         gp = gerbv_create_project();
447         if (!gp)
448                 return EXIT_FAILURE;
449
450         gerbv_open_layer_from_filename(gp, argv[optind]);
451         if (!gp->file[0]) {
452                 ret = EXIT_FAILURE;
453                 goto out;
454         }
455
456         drill = gp->file[0]->image;
457         if (drill->layertype != GERBV_LAYERTYPE_DRILL) {
458                 fprintf(stderr, _("%s: %s: error: not a drill file\n"),
459                                 progname, argv[optind]);
460                 ret = EXIT_FAILURE;
461                 goto out;
462         }
463
464         if (slotify(drill) != 0) {
465                 ret = EXIT_FAILURE;
466                 goto out;
467         }
468
469         if (!gerbv_export_drill_file_from_image(outfile, drill, NULL))
470                 ret = EXIT_FAILURE;
471 out:
472         gerbv_destroy_project(gp);
473         return ret;
474 }