/*
* Utility to convert overlapping Excellon drill hits into drill slots.
* Copyright © 2018, 2021, 2023 Nick Bowler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "options.h"
#include "help.h"
#include "xtra.h"
#if !ENABLE_NLS
# undef ENABLE_NLS
# define ENABLE_NLS 0
#endif
#define _(x) (gettext(x))
static const char *progname = "slotifier";
static unsigned verbose;
static void print_version(void)
{
const char *copysign = "(C)";
void *convsign = NULL;
puts(PACKAGE_STRING);
if (ENABLE_NLS) {
convsign = str_iconv("\xc2\xa9", "UTF-8", locale_charset());
if (convsign)
copysign = convsign;
}
printf("Copyright %s 2023 Nick Bowler.\n", copysign);
puts("License GPLv3+: GNU GPL version 3 or any later version.");
puts("This is free software: you are free to change and redistribute it.");
puts("There is NO WARRANTY, to the extent permitted by law.");
free(convsign);
}
static void print_usage(FILE *f)
{
fprintf(f, _("Usage: %s [options] [-o filename] filename\n"), progname);
if (f != stdout)
fprintf(f, _("Try %s --help for more information.\n"),
progname);
}
static void print_help(const struct option *lopts)
{
const struct option *opt;
print_usage(stdout);
puts(_("This is \"slotifier\": a tool to convert overlapping drill hits in Excellon\n"
"drill files to G85 drill slots."));
putchar('\n');
puts(_("Options:"));
for (opt = lopts; opt->val; opt++) {
struct lopt_help help;
if (!lopt_get_help(opt, &help))
continue;
help_print_option(opt, help.arg, help.desc, 20);
}
putchar('\n');
puts(_("For more information, see the slotifier(1) man page."));
putchar('\n');
/*
* TRANSLATORS: Please add *another line* indicating where users should
* report translation bugs.
*/
printf(_("Report bugs to <%s>.\n"), PACKAGE_BUGREPORT);
}
static void init_i18n(void)
{
setlocale(LC_ALL, "");
bindtextdomain(PACKAGE, LOCALEDIR);
textdomain(PACKAGE);
}
static CNearTreeHandle build_search_tree(gerbv_image_t *drill)
{
CNearTreeHandle t;
gerbv_net_t *x;
if (CNearTreeCreate(&t, 2, CNEARTREE_TYPE_DOUBLE|CNEARTREE_NORM_L2))
return 0;
/* Build a search tree from all the holes. */
for (x = drill->netlist; x; x = x->next) {
double xy[2] = { x->start_x, x->start_y };
assert(x->aperture >= 0 && x->aperture < APERTURE_MAX);
/* Skip things that aren't drill hits */
if (!drill->aperture[x->aperture])
continue;
/* Holes are marked as "flashing"; otherwise it's an existing
* slot; skip it. */
if (x->aperture_state != GERBV_APERTURE_STATE_FLASH)
continue;
if (CNearTreeInsert(t, xy, x)) {
CNearTreeFree(&t);
return 0;
}
}
if (CNearTreeCompleteDelayedInsert(t)) {
CNearTreeFree(&t);
return 0;
}
return t;
}
static gerbv_aperture_type_t tool_type(gerbv_image_t *drill, int aperture)
{
gerbv_aperture_t *tool = drill->aperture[abs(aperture)];
return tool->type;
}
static double tool_radius(gerbv_image_t *drill, int aperture)
{
gerbv_aperture_t *tool = drill->aperture[abs(aperture)];
/* Half a mil slop to decisively include points on boundary. */
return tool->parameter[0] / 2.0 + 0.0005;
}
static int holes_overlap(gerbv_image_t *drill, gerbv_net_t *a, gerbv_net_t *b)
{
double d = hypot(a->start_x - b->start_x, a->start_y - b->start_y);
return tool_radius(drill, a->aperture) >= d
|| tool_radius(drill, b->aperture) >= d;
}
static int combine_holes(gerbv_image_t *drill, gerbv_net_t *hole,
CNearTreeHandle t)
{
CVectorHandle group, tmp;
int biggest_tool, ret = -1;
double biggest_r;
size_t i, j;
/*
* Since we consider holes in order of decreasing size, the initial hole
* considered is by definition the biggest one we will find in a group.
*/
biggest_r = tool_radius(drill, (biggest_tool = hole->aperture));
if (CVectorCreate(&group, sizeof (gerbv_net_t *), 10)) {
fprintf(stderr, _("%s: failed to allocate memory\n"), progname);
return -1;
}
if (CVectorCreate(&tmp, sizeof (void *), 10)) {
fprintf(stderr, _("%s: failed to allocate memory\n"), progname);
CVectorFree(&group);
return -1;
}
/*
* Breadth-first nearest neighbour search of holes. We negate the
* aperture to indicate which holes have been previously visited.
*/
CVectorAddElement(group, &hole);
hole->aperture = -hole->aperture;
for (i = 0; i < CVectorSize(group); i++) {
double xy[2];
CVectorGetElement(group, &hole, i);
assert(tool_type(drill, hole->aperture) == GERBV_APTYPE_CIRCLE);
assert(tool_radius(drill, hole->aperture) <= biggest_r);
xy[0] = hole->start_x; xy[1] = hole->start_y;
if (CNearTreeFindInSphere(t, biggest_r, 0, tmp, xy, 1) != 0) {
/* We should always should find at least one hole! */
fprintf(stderr, _("%s: fatal error searching holes\n"),
progname);
goto err;
}
for (j = 0; j < CVectorSize(tmp); j++) {
/* I don't know why, but CNearTree returns a list
* of pointers to its internal copies of pointers
* to the objects in the tree. So we need this
* double indirection to get the actual hole. */
gerbv_net_t *newhole;
void *p;
CVectorGetElement(tmp, &p, j);
newhole = *(void **)p;
if (newhole->aperture < 0)
continue; /* already visited */
if (holes_overlap(drill, hole, newhole)) {
CVectorAddElement(group, &newhole);
newhole->aperture = -newhole->aperture;
}
}
}
/* Compare each pair of matched points to find the longest slot. */
if (CVectorSize(group) > 1) {
int biggest_slot = 0;
double bestlen = 0;
for (i = 0; i < CVectorSize(group) - 1; i++) {
CVectorGetElement(group, &hole, i);
for (j = i+1; j < CVectorSize(group); j++) {
gerbv_net_t *h2;
double newlen;
CVectorGetElement(group, &h2, j);
newlen = hypot(hole->start_x - h2->start_x,
hole->start_y - h2->start_y);
if (newlen > bestlen) {
hole->stop_x = h2->start_x;
hole->stop_y = h2->stop_y;
bestlen = newlen;
biggest_slot = i;
}
}
}
CVectorGetElement(group, &hole, biggest_slot);
hole->aperture = biggest_tool;
hole->aperture_state = GERBV_APERTURE_STATE_ON;
if (verbose) {
const char *fmt = ngettext(
"%s: merged %zu hole into slot (%.4f,%.4f)-(%.4f,%.4f)\n",
"%s: merged %zu holes into slot (%.4f,%.4f)-(%.4f,%.4f)\n",
CVectorSize(group));
fprintf(stderr, fmt, progname, CVectorSize(group),
hole->start_x, hole->start_y,
hole->stop_x, hole->stop_y);
}
} else {
/* The only hole we found was our original one, restore
* initial state. */
CVectorGetElement(group, &hole, 0);
assert(hole->aperture < 0);
hole->aperture = -hole->aperture;
if (verbose > 1) {
fprintf(stderr, _("%s: hole at (%.4f,%.4f) not merged\n"),
progname, hole->start_x, hole->start_y);
}
}
ret = 0;
err:
CVectorFree(&tmp);
CVectorFree(&group);
return ret;
}
/*
* Order two holes by hole diameter.
*/
static gerbv_image_t *hsc_drill_data;
static int hole_size_cmp(const void *a_, const void *b_)
{
gerbv_net_t * const *a = a_, * const *b = b_;
gerbv_aperture_t *ta, *tb;
ta = hsc_drill_data->aperture[abs(a[0]->aperture)];
assert(ta->type == GERBV_APTYPE_CIRCLE);
tb = hsc_drill_data->aperture[abs(b[0]->aperture)];
assert(tb->type == GERBV_APTYPE_CIRCLE);
if (ta->parameter[0] > tb->parameter[0])
return -1;
if (ta->parameter[0] < tb->parameter[0])
return 1;
return 0;
}
static void sort_holes_by_size(gerbv_image_t *drill, CVectorHandle work)
{
hsc_drill_data = drill;
qsort(work->array, work->size, work->elementsize, hole_size_cmp);
}
static int slotify(gerbv_image_t *drill)
{
CVectorHandle holes, work;
CNearTreeHandle t;
int ret = 0;
size_t i;
t = build_search_tree(drill);
if (!t) {
fprintf(stderr, _("%s: failed to build search tree\n"),
progname);
return -1;
}
CNearTreeObjects(t, &holes);
if (!holes)
goto err_free_tree;
if (CVectorCreate(&work, sizeof (gerbv_net_t *), CVectorSize(holes))) {
fprintf(stderr, _("%s: failed to allocate memory\n"), progname);
goto err_free_tree;
}
memcpy(work->array, holes->array, holes->size * holes->elementsize);
work->size = holes->size;
sort_holes_by_size(drill, work);
for (i = 0; i < CVectorSize(work); i++) {
gerbv_net_t *hole;
CVectorGetElement(work, &hole, i);
/* Skip holes we've already looked at */
if (hole->aperture < 0)
continue;
if (hole->aperture_state == GERBV_APERTURE_STATE_ON)
continue;
if (verbose > 1) {
fprintf(stderr, _("%s: checking hole at (%.4f,%.4f) for overlaps\n"),
progname, hole->start_x, hole->start_y);
}
if (combine_holes(drill, hole, t) < 0) {
ret = -1;
break;
}
}
/* Clean out any holes we don't need anymore */
for (i = 0; i < CVectorSize(holes); i++) {
gerbv_net_t *hole;
CVectorGetElement(holes, &hole, i);
if (hole->aperture < 0)
gerbv_image_delete_net(hole);
}
CVectorFree(&work);
err_free_tree:
CNearTreeFree(&t);
return ret;
}
static int do_cmdline(int argc, char **argv, const char **outfile)
{
const char *sopts = SOPT_STRING;
int opt;
XTRA_PACKED_LOPTS(lopts);
if (argc > 0)
progname = argv[0];
while ((opt = getopt_long(argc, argv, sopts, lopts, NULL)) != -1) {
switch (opt) {
case 'o':
*outfile = optarg;
break;
case 'v':
verbose++;
break;
case 'V':
print_version();
return 1;
case 'H':
print_help(lopts);
return 1;
default:
print_usage(stderr);
return -1;
}
}
if (!argv[optind]) {
fprintf(stderr, _("%s: error: must specify a filename\n"),
progname);
print_usage(stderr);
return -1;
}
if (optind + 1 < argc) {
fprintf(stderr, _("%s: error: excess command-line arguments\n"),
progname);
print_usage(stderr);
return -1;
}
return 0;
}
int main(int argc, char **argv)
{
const char *outfile = "/dev/stdout";
gerbv_project_t *gp;
gerbv_image_t *drill;
int ret = 0;
init_i18n();
switch (do_cmdline(argc, argv, &outfile)) {
case -1: return EXIT_FAILURE;
case 1: return EXIT_SUCCESS;
}
gp = gerbv_create_project();
if (!gp)
return EXIT_FAILURE;
gerbv_open_layer_from_filename(gp, argv[optind]);
if (!gp->file[0]) {
ret = EXIT_FAILURE;
goto out;
}
drill = gp->file[0]->image;
if (drill->layertype != GERBV_LAYERTYPE_DRILL) {
fprintf(stderr, _("%s: %s: error: not a drill file\n"),
progname, argv[optind]);
ret = EXIT_FAILURE;
goto out;
}
if (slotify(drill) != 0) {
ret = EXIT_FAILURE;
goto out;
}
if (!gerbv_export_drill_file_from_image(outfile, drill, NULL))
ret = EXIT_FAILURE;
out:
gerbv_destroy_project(gp);
return ret;
}