/*                      O B J _ R E A D . C
 * BRL-CAD
 *
 * Copyright (c) 2010-2024 United States Government as represented by
 * the U.S. Army Research Laboratory.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * version 2.1 as published by the Free Software Foundation.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this file; see the file named COPYING for more
 * information.
 */
/**
 *
 * Recommended Improvements (not already listed in function comments):
 * - Support obj file texture vertices and materials
 * - Optionally allow face grouping to be processed in parallel
 * - Allow user to select which obj file face groupings to convert
 * - Option to provide analysis of obj file edge lengths to help user
 *   estimate a reasonable distance tolerance or help user determine
 *   if the model should be re-tessellated in the source system using
 *   a different tessellation tolerance.
 * - The ability to parse the obj file group names with user
 *   selectable delimiters(s), to recreate a group/assembly structure
 *   in BRL-CAD. For example, group names which contain a '/' may
 *   indicate a assembly structure. This capability could be extended
 *   to create a group/assembly structure based on faces being
 *   contained in multiple groups in the obj file. This would correct
 *   the current behavior of when a face resides, for example, in two
 *   groups, there will be two copies of the face in the resulting
 *   model.
 */


#include "common.h"

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <time.h>

#include "bu/getopt.h"
#include "gcv/api.h"
#include "wdb.h"
#include "bu/sort.h"
#include "bu/units.h"
#include "bv/plot3.h"
#include "obj_parser.h"
#include "tri_face.h"


/* grouping type */
#define GRP_NONE     0 /* perform no grouping */
#define GRP_GROUP    1 /* create bot for each obj file 'g' grouping */
#define GRP_OBJECT   2 /* create bot for each obj file 'o' grouping */
#define GRP_MATERIAL 3 /* create bot for each obj file 'usemtl' grouping */
#define GRP_TEXTURE  4 /* create bot for each obj file 'usemap' grouping */

/* face type */
#define FACE_V    1 /* polygonal faces only identified by vertices */
#define FACE_TV   2 /* textured polygonal faces */
#define FACE_NV   3 /* oriented polygonal faces */
#define FACE_TNV  4 /* textured oriented polygonal faces */

/* face test type */
#define TEST_ALL      0 /* perform all face tests */
#define TEST_NUM_VERT 1 /* test only number of vertices */

/* type of vertex to fuse */
#define FUSE_VERT     0
#define FUSE_TEX_VERT 1

/* type of vertex compare during vertex fuse */
#define FUSE_WI_TOL   0
#define FUSE_EQUAL    1

/* closure status */
#define SURF_UNTESTED 0
#define SURF_CLOSED   1
#define SURF_OPEN     2

/* normal vector processing */
#define PROC_NORM 0
#define IGNR_NORM 1

/* texture vertex processing */
#define PROC_TEX 0
#define IGNR_TEX 1

/* primitive output mode */
#define OUT_PBOT 0 /* output plate mode bot */
#define OUT_VBOT 1 /* output volume mode bot */
#define OUT_NMG  2 /* output nmg */

/* plot open edges, turn-on, turn-off */
#define PLOT_OFF 0
#define PLOT_ON  1

typedef const size_t (**arr_1D_t);    /* 1 dimensional array type */
typedef const size_t (**arr_2D_t)[2]; /* 2 dimensional array type */
typedef const size_t (**arr_3D_t)[3]; /* 3 dimensional array type */

typedef size_t (*tri_arr_1D_t)[3];    /* triangle index array 1 dimensional type (v) */
typedef size_t (*tri_arr_2D_t)[3][2]; /* triangle index array 2 dimensional type (tv) or (nv) */
typedef size_t (*tri_arr_3D_t)[3][3]; /* triangle index array 3 dimensional type (tnv) */

typedef size_t (*edge_arr_2D_t)[2]; /* edge array type */

/* grouping face indices type */
struct gfi_t {
    void *index_arr_faces;            /* face indices into vertex, normal , texture vertex lists */
    size_t *num_vertices_arr;         /* number of vertices for each face within index_arr_faces */
    size_t *obj_file_face_idx_arr;    /* corresponds to the index of the face within the obj file. this
				       * value is useful to trace face errors back to the obj file.
				       */
    struct bu_vls *raw_grouping_name; /* raw name of grouping from obj file; group/object/material/texture */
    struct bu_vls *primitive_name;    /* name of primitive to be created */
    size_t num_faces;                 /* number of faces represented by index_arr_faces and num_vertices_arr */
    size_t max_faces;                 /* maximum number of faces based on current memory allocation */
    short int *face_status;           /* test_face result, 0 = untested, 1 = valid, >1 = degenerate */
    short int closure_status;         /* i.e. SURF_UNTESTED, SURF_CLOSED, SURF_OPEN */
    size_t tot_vertices;              /* sum of contents of num_vertices_arr. note: if the face_type
				       * includes normals and/or texture vertices, each vertex must have
				       * an associated normal and/or texture vertices. therefore this
				       * total is also the total of the associated normals and/or texture
				       * vertices.
				       */
    int face_type;                    /* i.e. FACE_V, FACE_TV, FACE_NV or FACE_TNV */
    int grouping_type;                /* i.e. GRP_NONE, GRP_GROUP, GRP_OBJECT, GRP_MATERIAL or GRP_TEXTURE */
    size_t grouping_index;            /* corresponds to the index of the grouping name within the obj file.
				       * this value is useful to append to the raw_grouping_name to
				       * create the primitive_name and ensure the primitive_name is unique.
				       */
    size_t *vertex_fuse_map;          /* maps the vertex index of duplicate (i.e. within tolerance) vertices
				       * int
				       * together to a single vertex
				       */
    short int *vertex_fuse_flag;      /* used during creation of vertex_fuse_map, indicates if the vertex
				       * has already been tested for duplicates or the vertex is a duplicate
				       * of an already processed vertex and does not need to be processed again
				       */
    size_t vertex_fuse_offset;        /* subtract this value from libobj vertex index to index into the fuse
				       * array to find the libobj vertex index this vertex was fused to
				       */
    size_t num_vertex_fuse;           /* number of elements in vertex_fuse_map and vertex_fuse_flag arrays */
    size_t *texture_vertex_fuse_map;  /* same as vertex_fuse_map but for texture vertex */
    short int *texture_vertex_fuse_flag; /* same as vertex_fuse_flag but for texture vertex */
    size_t texture_vertex_fuse_offset;/* same as vertex_fuse_offset but for texture vertex */
    size_t num_texture_vertex_fuse;   /* same as num_vertex_fuse but for texture vertex */
};


/* triangle indices type */
struct ti_t {
    void *index_arr_tri; /* triangle indices into vertex, normal, texture vertex lists */
    size_t num_tri;      /* number of triangles represented by index_arr_triangles */
    size_t max_tri;      /* maximum number of triangles based on current memory allocation */
    int tri_type;        /* i.e. FACE_V, FACE_TV, FACE_NV or FACE_TNV */
    size_t *vsi;         /* triangle vertex sort index array */
    size_t *vnsi;        /* triangle vertex normal sort index array */
    size_t *tvsi;        /* triangle texture vertex sort index array */
    size_t *uvi;         /* unique triangle vertex index array */
    size_t *uvni;        /* unique triangle vertex normal index array */
    size_t *utvi;        /* unique triangle texture vertex index array */
    size_t num_uvi;      /* number of unique triangle vertex indexes in uvi array */
    size_t num_uvni;     /* number of unique triangle vertex normal index in uvni array */
    size_t num_utvi;     /* number of unique triangle texture vertex index in utvi array */
    unsigned char bot_mode;        /* bot mode RT_BOT_PLATE, RT_BOT_PLATE_NOCOS, RT_BOT_SOLID, RT_BOT_SURFACE */
    fastf_t *bot_vertices;         /* array of floats for bot vertices [bot_num_vertices*3] */
    fastf_t *bot_thickness;        /* array of floats for face thickness [bot_num_faces] */
    fastf_t *bot_normals;          /* array of floats for bot normals [bot_num_normals*3] */
    fastf_t *bot_texture_vertices; /* array of floats for texture vertices [bot_num_texture_vertices*3] */
    int *bot_faces;                /* array of indices into bot_vertices array [bot_num_faces*3] */
    struct bu_bitv *bot_face_mode; /* bu_list of face modes for plate-mode-bot [bot_num_faces] */
    int *bot_face_normals;         /* array of indices into bot_normals array [bot_num_faces*3] */
    int *bot_textures;             /* array indices into bot_texture_vertices array */
    size_t bot_num_vertices;          /* number of vertices in bot_vertices array */
    size_t bot_num_faces;             /* number of faces in bot_faces array */
    size_t bot_num_normals;           /* number of normals in bot_normals array */
    size_t bot_num_texture_vertices;  /* number of textures in bot_texture_vertices array */
};


/* obj file global attributes type */
struct ga_t {
    const struct gcv_opts *gcv_options; /* gcv options */
    int nmg_debug;                      /* saved for longjmp handling */

    const obj_polygonal_attributes_t *polyattr_list; /* obj_polygonal_attributes */
    obj_parser_t parser;     /* obj_parser_create */
    obj_contents_t contents; /* obj_fparse */
    size_t numPolyAttr;      /* obj_polygonal_attributes */
    size_t numGroups;        /* obj_groups */
    size_t numObjects;       /* obj_objects */
    size_t numMaterials;     /* obj_materials */
    size_t numTexmaps;       /* obj_texmaps */
    size_t numVerts;         /* obj_vertices */
    size_t numNorms;         /* obj_normals */
    size_t numTexCoords;     /* obj_texture_coord */
    size_t numNorFaces;      /* obj_polygonal_nv_faces */
    size_t numFaces;         /* obj_polygonal_v_faces */
    size_t numTexFaces;      /* obj_polygonal_tv_faces */
    size_t numTexNorFaces;   /* obj_polygonal_tnv_faces */
    const char * const *str_arr_obj_groups;    /* obj_groups */
    const char * const *str_arr_obj_objects;   /* obj_objects */
    const char * const *str_arr_obj_materials; /* obj_materials */
    const char * const *str_arr_obj_texmaps;   /* obj_texmaps */
    const double (*vert_list)[4];          /* obj_vertices */
    const double (*norm_list)[3];          /* obj_normals */
    const double (*texture_coord_list)[3]; /* obj_texture_coord */
    const size_t *attindex_arr_nv_faces;  /* obj_polygonal_nv_faces */
    const size_t *attindex_arr_v_faces;   /* obj_polygonal_v_faces */
    const size_t *attindex_arr_tv_faces;  /* obj_polygonal_tv_faces */
    const size_t *attindex_arr_tnv_faces; /* obj_polygonal_tnv_faces */
};


/*
 * Collects object file attributes from the libobj library for use by
 * functions called later. Examples of collected attributes are the
 * quantity of each face type, the quantity of each grouping type, the
 * quantity of vertices, texture vertices, normals etc.
 */
void
collect_global_obj_file_attributes(struct ga_t *ga)
{
    size_t i = 0;
    size_t count = 0;
    const char * const *lib_arr = NULL;

    ga->numPolyAttr = obj_polygonal_attributes(ga->contents, &ga->polyattr_list);

    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	bu_log("OBJ FILE CONTENTS:\n");
    } else {
	bu_log("OBJ FILE CONTENT SUMMARY:\n");
    }

    /* release unused */
    count = obj_materiallibs(ga->contents, &lib_arr);
    bu_free_args(count, (char **)lib_arr, "materiallibs");

    count = obj_texmaplibs(ga->contents, &lib_arr);
    bu_free_args(count, (char **)lib_arr, "texmaplibs");

    count = obj_shadow_objs(ga->contents, &lib_arr);
    bu_free_args(count, (char **)lib_arr, "shadow_objs");

    count = obj_trace_objs(ga->contents, &lib_arr);
    bu_free_args(count, (char **)lib_arr, "trace_objs");

    ga->numGroups = obj_groups(ga->contents, &ga->str_arr_obj_groups);
    bu_log("\tTotal number of groups in OBJ file; numGroups = (%zu)\n", ga->numGroups);

    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	bu_log("\tList of all groups i.e. 'g' in OBJ file\n");
	for (i = 0 ; i < ga->numGroups ; i++) {
	    bu_log("\t\t(%zu)(%s)\n", i + 1, ga->str_arr_obj_groups[i]);
	}
    }

    ga->numObjects = obj_objects(ga->contents, &ga->str_arr_obj_objects);
    bu_log("\tTotal number of object groups in OBJ file; numObjects = (%zu)\n", ga->numObjects);

    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	bu_log("\tList of all object groups i.e. 'o' in OBJ file\n");
	for (i = 0 ; i < ga->numObjects ; i++) {
	    bu_log("\t\t(%zu)(%s)\n", i + 1, ga->str_arr_obj_objects[i]);
	}
    }

    ga->numMaterials = obj_materials(ga->contents, &ga->str_arr_obj_materials);
    bu_log("\tTotal number of material names in OBJ file; numMaterials = (%zu)\n", ga->numMaterials);

    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	bu_log("\tList of all material names i.e. 'usemtl' in OBJ file\n");
	for (i = 0 ; i < ga->numMaterials ; i++) {
	    bu_log("\t\t(%zu)(%s)\n", i + 1, ga->str_arr_obj_materials[i]);
	}
    }

    ga->numTexmaps = obj_texmaps(ga->contents, &ga->str_arr_obj_texmaps);
    bu_log("\tTotal number of texture map names in OBJ file; numTexmaps = (%zu)\n", ga->numTexmaps);

    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	bu_log("\tList of all texture map names i.e. 'usemap' in OBJ file\n");
	for (i = 0 ; i < ga->numTexmaps ; i++) {
	    bu_log("\t\t(%zu)(%s)\n", i + 1, ga->str_arr_obj_texmaps[i]);
	}
    }

    ga->numVerts = obj_vertices(ga->contents, &ga->vert_list);
    bu_log("\tTotal number of vertices in OBJ file; numVerts = (%zu)\n", ga->numVerts);

    ga->numNorms = obj_normals(ga->contents, &ga->norm_list);
    bu_log("\tTotal number of normals in OBJ file; numNorms = (%zu)\n", ga->numNorms);

    ga->numTexCoords = obj_texture_coord(ga->contents, &ga->texture_coord_list);
    bu_log("\tTotal number of texture coordinates in OBJ file; numTexCoords = (%zu)\n", ga->numTexCoords);

    ga->numNorFaces = obj_polygonal_nv_faces(ga->contents, &ga->attindex_arr_nv_faces);
    bu_log("\tNumber of oriented polygonal faces; numNorFaces = (%zu)\n", ga->numNorFaces);

    ga->numFaces = obj_polygonal_v_faces(ga->contents, &ga->attindex_arr_v_faces);
    bu_log("\tNumber of polygonal faces only identified by vertices; numFaces = (%zu)\n", ga->numFaces);

    ga->numTexFaces = obj_polygonal_tv_faces(ga->contents, &ga->attindex_arr_tv_faces);
    bu_log("\tNumber of textured polygonal faces; numTexFaces = (%zu)\n", ga->numTexFaces);

    ga->numTexNorFaces = obj_polygonal_tnv_faces(ga->contents, &ga->attindex_arr_tnv_faces);
    bu_log("\tNumber of oriented textured polygonal faces; numTexNorFaces = (%zu)\n\n", ga->numTexNorFaces);

    return;
}


/*
 * Replaces with underscore characters which are invalid for BRL-CAD
 * primitive names and file names.
 */
void
cleanup_name(struct bu_vls *outputObjectName_ptr)
{
    char *temp_str;
    size_t outputObjectName_length;
    size_t i;

    temp_str = bu_vls_addr(outputObjectName_ptr);

    /* length does not include null */
    outputObjectName_length = bu_vls_strlen(outputObjectName_ptr);

    for (i = 0 ; i < outputObjectName_length ; i++) {
	if (strchr("/\\=", (int)temp_str[i]) != (char *)NULL) {
	    temp_str[i] = '_';
	}
    }

    return;
}


/*
 * Compare function used by the function bsearch for
 * sorting and searching an array of numbers.
 */
static int
comp_b(const void *p1, const void *p2)
{
    size_t i = * (size_t *) p1;
    size_t j = * (size_t *) p2;

    return (int)(i - j);
}


/*
 * Compare function used by the function bu_sort for
 * sorting an array of numbers.
 */
static int
comp_b_sort(const void *p1, const void *p2, void *UNUSED(arg))
{
    return comp_b(p1, p2);
}


/*
 * Compare function used by the function bu_sort for sorting an index
 * into a multi-dimensional array.
 */
static int
comp(const void *p1, const void *p2, void *arg)
{
    size_t i = * (size_t *) p1;
    size_t j = * (size_t *) p2;
    size_t *array = (size_t *) arg;

    return (int)(array[i] - array[j]);
}


/*
 * Compare function used by the function bu_sort for sorting a 2D array
 * of numbers.
 */
static int
comp_c(const void *p1, const void *p2, void *UNUSED(arg))
{
    edge_arr_2D_t i = (edge_arr_2D_t) p1;
    edge_arr_2D_t j = (edge_arr_2D_t) p2;

    if ((i[0][0] - j[0][0]) != 0) {
	return (int)(i[0][0] - j[0][0]);
    } else {
	return (int)(i[0][1] - j[0][1]);
    }
}


/*
 * For a grouping of faces, retrieve the coordinates and obj file
 * indexes of a specific vertex within a specific face in this
 * grouping. If the face type indicates a type where some return
 * information is non-applicable, then this non-applicable information
 * will be undefined when this function returns.
 */
void                                    /* inputs: */
retrieve_coord_index(struct ga_t *ga,   /* obj file global attributes */
		     struct gfi_t *gfi, /* grouping face indices */
		     size_t fi,         /* face index number of grouping */
		     size_t vi,         /* vertex index number of face */
					/* outputs: */
		     fastf_t *vc,       /* vertex coordinates */
		     fastf_t *nc,       /* normal coordinates */
		     fastf_t *tc,       /* texture vertex coordinates */
		     fastf_t *w,        /* vertex weight */
		     size_t *vofi,      /* vertex obj file index */
		     size_t *nofi,      /* normal obj file index */
		     size_t *tofi)      /* texture vertex obj file index */
{
    arr_1D_t index_arr_faces_1D = NULL;
    arr_2D_t index_arr_faces_2D = NULL;
    arr_3D_t index_arr_faces_3D = NULL;

    size_t fofi = gfi->obj_file_face_idx_arr[fi]; /* face obj file index */

    switch (gfi->face_type) {
	case FACE_V:
	    index_arr_faces_1D = (arr_1D_t)(gfi->index_arr_faces);

	    /* copy current vertex obj file index into vofi */
	    *vofi = index_arr_faces_1D[fi][vi];

	    /* used fused vertex index if available */
	    if (gfi->vertex_fuse_map != NULL) {
		*vofi = gfi->vertex_fuse_map[*vofi - gfi->vertex_fuse_offset];
	    }

	    /* copy current vertex coordinates into vc */
	    VMOVE(vc, ga->vert_list[*vofi]);
	    /* copy current vertex weight into w */
	    *w = ga->vert_list[*vofi][3];

	    if (ga->gcv_options->debug_mode) {
		bu_log("fi=(%zu)vi=(%zu)fofi=(%zu)vofi=(%zu)v=(%f)(%f)(%f)w=(%f)\n",
		       fi, vi, fofi+1, *vofi+1, vc[0], vc[1], vc[2], *w);
	    }
	    break;
	case FACE_TV:
	    index_arr_faces_2D = (arr_2D_t)(gfi->index_arr_faces);
	    /* copy current vertex coordinates into vc */
	    VMOVE(vc, ga->vert_list[index_arr_faces_2D[fi][vi][0]]);
	    /* copy current vertex weight into w */
	    *w = ga->vert_list[index_arr_faces_2D[fi][vi][0]][3];
	    /* copy current texture coordinate into tc */
	    VMOVE(tc, ga->texture_coord_list[index_arr_faces_2D[fi][vi][1]]);
	    /* copy current vertex obj file index into vofi */
	    *vofi = index_arr_faces_2D[fi][vi][0];
	    /* copy current texture coordinate obj file index into tofi */
	    *tofi = index_arr_faces_2D[fi][vi][1];
	    if (ga->gcv_options->debug_mode) {
		bu_log("fi=(%zu)vi=(%zu)fofi=(%zu)vofi=(%zu)tofi=(%zu)v=(%f)(%f)(%f)w=(%f)t=(%f)(%f)(%f)\n",
		       fi, vi, fofi+1, *vofi+1, *tofi+1, vc[0], vc[1], vc[2], *w, tc[0], tc[1], tc[2]);
	    }
	    break;
	case FACE_NV:
	    index_arr_faces_2D = (arr_2D_t)(gfi->index_arr_faces);

	    /* copy current vertex obj file index into vofi */
	    *vofi = index_arr_faces_2D[fi][vi][0];
	    /* copy current normal obj file index into nofi */
	    *nofi = index_arr_faces_2D[fi][vi][1];

	    /* use fused vertex index if available */
	    if (gfi->vertex_fuse_map != NULL) {
		*vofi = gfi->vertex_fuse_map[*vofi - gfi->vertex_fuse_offset];
	    }

	    /* copy current vertex coordinates into vc */
	    VMOVE(vc, ga->vert_list[*vofi]);
	    /* copy current vertex weight into w */
	    *w = ga->vert_list[*vofi][3];
	    /* copy current normal into nc */
	    VMOVE(nc, ga->norm_list[*nofi]);

	    if (ga->gcv_options->debug_mode) {
		bu_log("fi=(%zu)vi=(%zu)fofi=(%zu)vofi=(%zu)nofi=(%zu)v=(%f)(%f)(%f)w=(%f)n=(%f)(%f)(%f)\n",
		       fi, vi, fofi+1, *vofi+1, *nofi+1, vc[0], vc[1], vc[2], *w, nc[0], nc[1], nc[2]);
	    }
	    break;
	case FACE_TNV:
	    index_arr_faces_3D = (arr_3D_t)(gfi->index_arr_faces);
	    /* copy current vertex coordinates into vc */
	    VMOVE(vc, ga->vert_list[index_arr_faces_3D[fi][vi][0]]);
	    /* copy current vertex weight into w */
	    *w = ga->vert_list[index_arr_faces_3D[fi][vi][0]][3];
	    /* copy current texture coordinate into tc */
	    VMOVE(tc, ga->texture_coord_list[index_arr_faces_3D[fi][vi][1]]);
	    /* copy current normal into nc */
	    VMOVE(nc, ga->norm_list[index_arr_faces_3D[fi][vi][2]]);
	    /* copy current vertex obj file index into vofi */
	    *vofi = index_arr_faces_3D[fi][vi][0];
	    /* copy current texture coordinate obj file index into tofi */
	    *tofi = index_arr_faces_3D[fi][vi][1];
	    /* copy current normal obj file index into nofi */
	    *nofi = index_arr_faces_3D[fi][vi][2];
	    if (ga->gcv_options->debug_mode) {
		bu_log("fi=(%zu)vi=(%zu)fofi=(%zu)vofi=(%zu)tofi=(%zu)nofi=(%zu)v=(%f)(%f)(%f)w=(%f)t=(%f)(%f)(%f)n=(%f)(%f)(%f)\n",
		       fi, vi, fofi+1, *vofi+1, *tofi+1, *nofi+1, vc[0], vc[1], vc[2], *w,
		       tc[0], tc[1], tc[2], nc[0], nc[1], nc[2]);
	    }
	    break;
    }
    return;
}


/*
 * Returns the number of vertices in a face where the last vertex in
 * the vertex list is not the same as the first vertex. Essentially
 * this function finds the last unique vertex of the face. If the
 * returned count is used to indicate the number of vertices in the
 * face, the result is trailing vertices in the face which are
 * identical to the first vertex will be removed. This function
 * is a support function for the test_face function and allows some
 * cases of zero length edges to be removed from a face.
 */
size_t
find_last_unique_vertex(struct ga_t *ga,   /* obj file global attributes */
			struct gfi_t *gfi, /* grouping face indices */
			size_t face_idx)   /* face index number of grouping */
{
    fastf_t tmp_v[3] = {0.0, 0.0, 0.0}; /* temporary vertex */
    fastf_t tmp_w = 0.0;                /* temporary weight */
    fastf_t tmp_n[3] = {0.0, 0.0, 0.0}; /* temporary normal */
    fastf_t tmp_t[3] = {0.0, 0.0, 0.0}; /* temporary texture vertex */
    size_t vofi = 0;                    /* vertex obj file index */
    size_t nofi = 0;                    /* normal obj file index */
    size_t tofi = 0;                    /* texture vertex obj file index */
    size_t num_vertex = 0;
    int done = 0;
    fastf_t first_vertex[3] = {0.0, 0.0, 0.0};
    fastf_t last_vertex[3] = {0.0, 0.0, 0.0};
    size_t first_vertex_index = 0;
    size_t last_vertex_index = 0;

    num_vertex = gfi->num_vertices_arr[face_idx];

    if (num_vertex >= 3) {
	retrieve_coord_index(ga, gfi, face_idx, 0, tmp_v, tmp_n, tmp_t,
			     &tmp_w, &vofi, &nofi, &tofi);
	VMOVE(first_vertex, tmp_v);
	first_vertex_index = vofi;

	while ((num_vertex >= 3) && !done) {
	    retrieve_coord_index(ga, gfi, face_idx, num_vertex - 1, tmp_v, tmp_n, tmp_t,
				 &tmp_w, &vofi, &nofi, &tofi);
	    VMOVE(last_vertex, tmp_v);
	    last_vertex_index = vofi;

	    if ((first_vertex_index == last_vertex_index) || VEQUAL(first_vertex, last_vertex)) {
		num_vertex--;
	    } else {
		done = 1;
	    }
	}
    }

    return num_vertex;
}


/*
 * Within a given grouping of faces, test an individual face for
 * degenerate conditions such as duplicate vertex indexes or the
 * distance between any pair of vertices of a individual face are
 * equal to or less than the distance tolerance. The test result is
 * returned by this function and is recorded so it can be used later.
 * If a face was previously tested, this function will not retest the
 * face unless the force_retest flag is set. If the face test was
 * TEST_NUM_VERT, the stored result is untested since this is not an
 * extensive test. Return codes for tests are below. Note: the stored
 * test result value is the return code plus one, since 0 in the
 * results list indicates an untested face and one indicates a valid
 * face. This function also removes trailing vertices from a face
 * which are identical to the first vertex in the face resulting in
 * zero length edges being removed when this condition occurs.
 *
 * Returns:
 * 0 valid face
 * 1 degenerate, <3 vertices
 * 2 degenerate, duplicate vertex indexes
 * 3 degenerate, vertices too close
 *
 * Recommended function improvements:
 * - Optionally remove all zero length edges from a face
 */
int
test_face(struct ga_t *ga,
	  struct gfi_t *gfi,
	  size_t face_idx,
	  fastf_t conv_factor,  /* conversion factor from obj file units to mm */
	  const struct bn_tol *tol,
	  int face_test_type,   /* TEST_ALL, TEST_NUM_VERT */
	  int force_retest)     /* force the retest of this face if it was already
				 * tested, this is useful when we know a retest is
				 * required such as when the previous test was performed
				 * without fused vertices and now vertices are fused
				 */
{
    fastf_t tmp_v_o[3] = {0.0, 0.0, 0.0}; /* temporary vertex, referenced from outer loop */
    fastf_t tmp_v_i[3] = {0.0, 0.0, 0.0}; /* temporary vertex, referenced from inner loop */
    fastf_t tmp_w = 0.0;                  /* temporary weight */
    fastf_t tmp_n[3] = {0.0, 0.0, 0.0};   /* temporary normal */
    fastf_t tmp_t[3] = {0.0, 0.0, 0.0};   /* temporary texture vertex */
    size_t nofi = 0;                      /* normal obj file index */
    size_t tofi = 0;                      /* texture vertex obj file index */
    fastf_t distance_between_vertices = 0.0;
    size_t vofi_o = 0; /* vertex obj file index, referenced from outer loop */
    size_t vofi_i = 0; /* vertex obj file index, referenced from inner loop */

    size_t vert = 0;
    size_t vert2 = 0;
    int degenerate_face = 0;

    /* if this face has already been tested, return the previous
     * results unless the force_retest flag is set
     */
    if (!force_retest) {
	if (gfi->face_status[face_idx]) {
	    /* subtract 1 since face_status value of 1 indicates
	     * success and 0 indicates untested
	     */
	    return (gfi->face_status[face_idx] - 1);
	}
    }

    /* removes trailing vertices identical to the first vertex */
    gfi->num_vertices_arr[face_idx] = find_last_unique_vertex(ga, gfi, face_idx);

    /* added 1 to internal index values so warning message index
     * values matches obj file index numbers. this is because obj file
     * indexes start at 1, internally indexes start at 0.  changed the
     * warning message if grouping_type is GRP_NONE because the group
     * name and grouping index has no meaning to the user if grouping
     * type is GRP_NONE
     */
    if (gfi->num_vertices_arr[face_idx] < 3) {
	degenerate_face = 1;
	if (gfi->grouping_type != GRP_NONE) {
	    if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
		bu_log("WARNING: removed degenerate face (reason: < 3 vertices); obj file face group name = (%s) obj file face grouping index = (%zu) obj file face index = (%zu)\n",
		       bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1,
		       gfi->obj_file_face_idx_arr[face_idx] + 1);
	    }
	} else {
	    if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
		bu_log("WARNING: removed degenerate face (reason: < 3 vertices); obj file face index = (%zu)\n",
		       gfi->obj_file_face_idx_arr[face_idx] + 1);
	    }
	}
    }

    if (face_test_type == TEST_NUM_VERT) {
	/* set face test status to zero (i.e. untested) since just
	 * counting the number of vertices is not a true test
	 */
	gfi->face_status[face_idx] = 0;
	return degenerate_face;
    }

    while ((vert < gfi->num_vertices_arr[face_idx]) && !degenerate_face) {
	vert2 = vert+1;
	while ((vert2 < gfi->num_vertices_arr[face_idx]) && !degenerate_face) {
	    retrieve_coord_index(ga, gfi, face_idx, vert, tmp_v_o,
				 tmp_n, tmp_t, &tmp_w, &vofi_o, &nofi, &tofi);
	    retrieve_coord_index(ga, gfi, face_idx, vert2, tmp_v_i,
				 tmp_n, tmp_t, &tmp_w, &vofi_i, &nofi, &tofi);
	    if (vofi_o == vofi_i) {
		/* test for duplicate vertex indexes in face */
		degenerate_face = 2;
		if (gfi->grouping_type != GRP_NONE) {
		    if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
			bu_log("WARNING: removed degenerate face (reason: duplicate vertex index); obj file face group name = (%s) obj file face grouping index = (%zu) obj file face index = (%zu) obj file vertex index = (%zu)\n",
			       bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1,
			       gfi->obj_file_face_idx_arr[face_idx] + 1, vofi_o + 1);
		    }
		} else {
		    if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
			bu_log("WARNING: removed degenerate face (reason: duplicate vertex index); obj file face index = (%zu) obj file vertex index = (%zu)\n",
			       gfi->obj_file_face_idx_arr[face_idx] + 1, vofi_o + 1);
		    }
		}
	    } else {
		/* test for vertices closer than tol.dist
		 * tol.dist is assumed to be mm
		 */
		VSCALE(tmp_v_o, tmp_v_o, conv_factor);
		VSCALE(tmp_v_i, tmp_v_i, conv_factor);
		if (bg_pnt3_pnt3_equal(tmp_v_o, tmp_v_i, tol)) {
		    distance_between_vertices = DIST_PNT_PNT(tmp_v_o, tmp_v_i);
		    degenerate_face = 3;
		    if (gfi->grouping_type != GRP_NONE) {
			if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
			    bu_log("WARNING: removed degenerate face (reason: vertices too close); obj file face group name = (%s) obj file face grouping index = (%zu) obj file face index = (%zu) obj file vertice indexes (%zu) vs (%zu) tol.dist = (%lfmm) dist = (%fmm)\n",
				   bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1,
				   gfi->obj_file_face_idx_arr[face_idx] + 1, vofi_o + 1,
				   vofi_i + 1, tol->dist, distance_between_vertices);
			}
		    } else {
			if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
			    bu_log("WARNING: removed degenerate face (reason: vertices too close); obj file face index = (%zu) obj file vertice indexes (%zu) vs (%zu) tol.dist = (%lfmm) dist = (%fmm)\n",
				   gfi->obj_file_face_idx_arr[face_idx] + 1, vofi_o + 1, vofi_i + 1,
				   tol->dist, distance_between_vertices);
			}
		    }
		}
	    }
	    vert2++;
	}
	vert++;
    }

    /* add 1 since zero within face_status indicates untested */
    gfi->face_status[face_idx] = degenerate_face + 1;

    return degenerate_face;
}


/*
 * Within a given grouping of faces, test all the faces for degenerate
 * conditions such as duplicate vertex indexes or the distance between
 * any pair of vertices of a individual face are equal to or less than
 * the distance tolerance. Test results for each face of the grouping
 * is recorded to be used later. If a face was previously tested, this
 * function will retest the face.  Retesting is useful if a vertex
 * fuse was performed after the last testing of the faces.
 */
size_t
retest_grouping_faces(struct ga_t *ga,
		      struct gfi_t *gfi,
		      fastf_t conv_factor,  /* conversion factor from obj file units to mm */
		      int face_test_type,
		      const struct bn_tol *tol)
{
    size_t face_idx = 0;
    size_t failed_face_count = 0;

    /* face_status is populated within test_face function */
    for (face_idx = 0 ; face_idx < gfi->num_faces ; face_idx++) {
	/* 1 passed into test_face indicates forced retest */
	if (test_face(ga, gfi, face_idx, conv_factor, tol, face_test_type, 1)) {
	    failed_face_count++;
	}
    }

    return failed_face_count;
}


/*
 * Releases the memory allocated for the contents of the gfi structure
 * and the gfi structure itself. The gfi structure contains the
 * 'grouping face indices' into the libobj structures (and supporting
 * attributes and indexes) for all the faces of a grouping of faces. A
 * grouping of faces corresponds to an individual BRL-CAD primitive.
 */
void
free_gfi(struct gfi_t **gfi)
{
    if (*gfi != NULL) {
	bu_vls_free((*gfi)->raw_grouping_name);
	bu_vls_free((*gfi)->primitive_name);
	bu_free((*gfi)->raw_grouping_name, "(*gfi)->raw_grouping_name");
	bu_free((*gfi)->primitive_name, "(*gfi)->primitive_name");
	bu_free((*gfi)->index_arr_faces, "(*gfi)->index_arr_faces");
	bu_free((*gfi)->num_vertices_arr, "(*gfi)->num_vertices_arr");
	bu_free((*gfi)->obj_file_face_idx_arr, "(*gfi)->obj_file_face_idx_arr");
	bu_free((*gfi)->face_status, "(*gfi)->face_status");
	bu_free((*gfi)->vertex_fuse_map, "(*gfi)->vertex_fuse_map");
	bu_free((*gfi)->texture_vertex_fuse_map, "(*gfi)->texture_vertex_fuse_map");
	bu_free(*gfi, "*gfi");
	*gfi = NULL;
    }
    return;
}


/*
 * Collects the face indexes into the libobj structures for a specific
 * grouping of faces. The grouping_index identifies the grouping to be
 * collected and corresponds to the index of the grouping defined in the obj
 * file. It is ignored if grouping_type is GROUP_NONE. This function allocates
 * all memory needed for the gfi structure and its contents. Gfi is expected to
 * be a null pointer when passed to this function. The gfi structure and its
 * contents is expected to be freed outside this function. It is not possible
 * to directly retrieve a list of faces for a specific grouping from the libobj
 * structures, therefore all faces in the obj file must be traversed and tested
 * if they are a member of a grouping, if a face is a member, the index of the
 * found face is stored in a list to be processed later. This function creates
 * this list of indexes to the face indexes in the libobj structures.
 */
void
collect_grouping_faces_indexes(struct ga_t *ga,
			       struct gfi_t **gfi,
			       int face_type,
			       int grouping_type,
			       size_t grouping_index)
{
    /* number of faces of the current face_type in the entire obj file */
    size_t numFaces = 0;
    size_t i = 0;
    const size_t *attindex_arr_faces = (const size_t *)NULL;
    int found = 0;
    const char *name_str = (char *)NULL;
    size_t setsize = 0;
    const size_t *indexset_arr;
    size_t groupid = 0;
    size_t *num_vertices_arr_tmp = NULL;
    size_t *obj_file_face_idx_arr_tmp = NULL;

    const size_t (*index_arr_v_faces) = NULL;      /* used by v_faces */
    const size_t (*index_arr_tv_faces)[2] = NULL;  /* used by tv_faces */
    const size_t (*index_arr_nv_faces)[2] = NULL;  /* used by nv_faces */
    const size_t (*index_arr_tnv_faces)[3] = NULL; /* used by tnv_faces */

    arr_1D_t index_arr_faces_1D = NULL;
    arr_2D_t index_arr_faces_2D = NULL;
    arr_3D_t index_arr_faces_3D = NULL;

    /* number of faces of the current face_type from the entire obj
     * file which is found in the current grouping_type and current
     * group
     */
    size_t numFacesFound = 0;
    size_t faceIndex = 0;

    /* number of additional elements to allocate memory for when the
     * currently allocated memory is exhausted
     */
    const size_t max_faces_increment = 128;

    if (*gfi != NULL) {
	bu_bomb("function collect_grouping_faces_indexes passed non-null for gfi\n");

	return;
    }

    switch (face_type) {
	case FACE_V:
	    numFaces = ga->numFaces;
	    attindex_arr_faces = ga->attindex_arr_v_faces;
	    break;
	case FACE_TV:
	    numFaces = ga->numTexFaces;
	    attindex_arr_faces = ga->attindex_arr_tv_faces;
	    break;
	case FACE_NV:
	    numFaces = ga->numNorFaces;
	    attindex_arr_faces = ga->attindex_arr_nv_faces;
	    break;
	case FACE_TNV:
	    numFaces = ga->numTexNorFaces;
	    attindex_arr_faces = ga->attindex_arr_tnv_faces;
	    break;
    }

    /* traverse list of all faces in OBJ file of current face_type */
    for (i = 0; i < numFaces; i++) {

	const obj_polygonal_attributes_t *face_attr;
	face_attr = ga->polyattr_list + attindex_arr_faces[i];

	/* reset for next face */
	found = 0;

	/* for each type of grouping, check if current face is in
	 * current grouping.
	 */
	switch (grouping_type) {
	    case GRP_NONE:
		found = 1;
		/* since there is no grouping, still need a somewhat
		 * useful name for the brlcad primitive and region,
		 * set the name to the face_type which is the inherent
		 * grouping
		 */
		switch (face_type) {
		    case FACE_V:
			name_str = "v";
			break;
		    case FACE_TV:
			name_str = "tv";
			break;
		    case FACE_NV:
			name_str = "nv";
			break;
		    case FACE_TNV:
			name_str = "tnv";
			break;
		}
		break;
	    case GRP_GROUP:
		/* setsize is the number of groups the current nv_face
		 * belongs to.
		 */
		setsize =
		    obj_groupset(ga->contents, face_attr->groupset_index,
				 &indexset_arr);

		/* loop through each group this face is in */
		for (groupid = 0 ; groupid < setsize ; groupid++) {
		    /* if true, current face is in current group grouping */
		    if (grouping_index == indexset_arr[groupid]) {
			size_t groups_index = indexset_arr[groupid];
			name_str = ga->str_arr_obj_groups[groups_index];
			found = 1;
		    }
		}
		break;
	    case GRP_OBJECT:
		/* if true, current face is in current object grouping */
		if (grouping_index == face_attr->object_index) {
		    found = 1;
		    name_str =
			ga->str_arr_obj_objects[face_attr->object_index];
		}
		break;
	    case GRP_MATERIAL:
		/* if true, current face is in current material grouping */
		if (grouping_index == face_attr->material_index) {
		    found = 1;
		    name_str =
			ga->str_arr_obj_materials[face_attr->material_index];
		}
		break;
	    case GRP_TEXTURE:
		/* if true, current face is in current texture map grouping */
		if (grouping_index == face_attr->texmap_index) {
		    found = 1;
		    name_str = ga->str_arr_obj_texmaps[face_attr->texmap_index];
		}
		break;
	} /* switch (grouping_type) */

	/* if found the first face allocate the output structure and
	 * initial allocation of the index_arr_faces, num_vertices_arr
	 * and obj_file_face_idx_arr arrays
	 */
	if (found && (numFacesFound == 0)) {
	    /* allocate memory for gfi structure */
	    BU_ALLOC((*gfi), struct gfi_t);

	    /* initialize gfi structure */
	    (*gfi)->index_arr_faces = (void *)NULL;
	    (*gfi)->num_vertices_arr = (size_t *)NULL;
	    (*gfi)->obj_file_face_idx_arr = (size_t *)NULL;
	    (*gfi)->raw_grouping_name = (struct bu_vls *)NULL;
	    (*gfi)->primitive_name = (struct bu_vls *)NULL;
	    (*gfi)->num_faces = 0;
	    (*gfi)->max_faces = 0;
	    (*gfi)->face_status = (short int *)NULL;
	    (*gfi)->closure_status = SURF_UNTESTED;
	    (*gfi)->tot_vertices = 0;
	    (*gfi)->face_type = 0;
	    (*gfi)->grouping_type = 0;
	    (*gfi)->grouping_index = 0;
	    (*gfi)->vertex_fuse_map = (size_t *)NULL;
	    (*gfi)->vertex_fuse_flag = (short int *)NULL;
	    (*gfi)->vertex_fuse_offset = 0;
	    (*gfi)->num_vertex_fuse = 0;
	    (*gfi)->texture_vertex_fuse_map = (size_t *)NULL;
	    (*gfi)->texture_vertex_fuse_flag = (short int *)NULL;
	    (*gfi)->texture_vertex_fuse_offset = 0;
	    (*gfi)->num_texture_vertex_fuse = 0;

	    /* set face_type, grouping_type, grouping_index inside gfi
	     * structure, the purpose of this is so functions called
	     * later do not need to pass this in separately
	     */
	    (*gfi)->face_type = face_type;
	    (*gfi)->grouping_type = grouping_type;
	    if (grouping_type != GRP_NONE) {
		(*gfi)->grouping_index = grouping_index;
	    } else {
		/* set grouping_index to face_type since if grouping_type is
		 * GRP_NONE, inherently we are grouping by face_type and we
		 * need a number for unique naming of the brlcad objects.
		 */
		(*gfi)->grouping_index = (size_t)abs(face_type);

	    }
	    /* allocate and initialize variable length string (vls) for
	     * raw_grouping_name
	     */
	    (*gfi)->raw_grouping_name = bu_vls_vlsinit();

	    /* allocate and initialize variable length string (vls) for
	     * primitive_name
	     */
	    (*gfi)->primitive_name = bu_vls_vlsinit();

	    /* only need to copy in the grouping name for the first
	     * face found within the grouping since all the faces in
	     * the grouping will have the same grouping name
	     */
	    bu_vls_strcpy((*gfi)->raw_grouping_name, name_str);

	    /* sets initial number of elements to allocate memory for */
	    (*gfi)->max_faces = max_faces_increment;

	    (*gfi)->num_vertices_arr = (size_t *)bu_calloc((*gfi)->max_faces, sizeof(size_t), "num_vertices_arr");
	    (*gfi)->obj_file_face_idx_arr = (size_t *)bu_calloc((*gfi)->max_faces, sizeof(size_t), "obj_file_face_idx_arr");

	    /* allocate initial memory for (*gfi)->index_arr_faces based on
	     * face_type
	     */
	    switch (face_type) {
		case FACE_V:
		    (*gfi)->index_arr_faces = bu_calloc((*gfi)->max_faces, sizeof(size_t *), "index_arr_faces");
		    index_arr_faces_1D = (arr_1D_t)((*gfi)->index_arr_faces);
		    break;
		case FACE_TV:
		case FACE_NV:
		    (*gfi)->index_arr_faces = bu_calloc((*gfi)->max_faces, sizeof(size_t (*)[2]), "index_arr_faces");
		    index_arr_faces_2D = (arr_2D_t)((*gfi)->index_arr_faces);
		    break;
		case FACE_TNV:
		    (*gfi)->index_arr_faces = bu_calloc((*gfi)->max_faces, sizeof(size_t (*)[3]), "index_arr_faces");
		    index_arr_faces_3D = (arr_3D_t)((*gfi)->index_arr_faces);
		    break;
	    } /* switch (face_type) */
	} /* if (found && (numFacesFound == 0)) */

	if (found) {
	    numFacesFound++;
	    faceIndex = numFacesFound - 1;

	    /* if needed, increase size of (*gfi)->num_vertices_arr and
	     * (*gfi)->index_arr_faces
	     */
	    if (faceIndex >= (*gfi)->max_faces) {
		(*gfi)->max_faces += max_faces_increment;

		num_vertices_arr_tmp = (size_t *)bu_realloc((*gfi)->num_vertices_arr, sizeof(size_t) * (*gfi)->max_faces, "num_vertices_arr_tmp");
		(*gfi)->num_vertices_arr = num_vertices_arr_tmp;

		obj_file_face_idx_arr_tmp = (size_t *)bu_realloc((*gfi)->obj_file_face_idx_arr, sizeof(size_t) * (*gfi)->max_faces, "obj_file_face_idx_arr_tmp");
		(*gfi)->obj_file_face_idx_arr = obj_file_face_idx_arr_tmp;

		switch (face_type) {
		    case FACE_V:
			(*gfi)->index_arr_faces = bu_realloc((void *)index_arr_faces_1D, sizeof(size_t *) * (*gfi)->max_faces, "index_arr_faces");
			index_arr_faces_1D = (arr_1D_t)((*gfi)->index_arr_faces);

			break;
		    case FACE_TV:
		    case FACE_NV:
			(*gfi)->index_arr_faces = bu_realloc((void *)index_arr_faces_2D, sizeof(size_t (*)[2]) * (*gfi)->max_faces, "index_arr_faces");
			index_arr_faces_2D = (arr_2D_t)((*gfi)->index_arr_faces);

			break;
		    case FACE_TNV:
			(*gfi)->index_arr_faces = bu_realloc((void *)index_arr_faces_3D, sizeof(size_t (*)[3]) * (*gfi)->max_faces, "index_arr_faces");
			index_arr_faces_3D = (arr_3D_t)((*gfi)->index_arr_faces);

			break;
		}
	    }

	    /* assign obj file face index into array for tracking
	     * errors back to the face within the obj file
	     */
	    (*gfi)->obj_file_face_idx_arr[faceIndex] = attindex_arr_faces[i];

	    switch (face_type) {
		case FACE_V:
		    (*gfi)->num_vertices_arr[faceIndex] =
			obj_polygonal_v_face_vertices(ga->contents, i,
						      &index_arr_v_faces);

		    index_arr_faces_1D[faceIndex] = index_arr_v_faces;
		    break;
		case FACE_TV:
		    (*gfi)->num_vertices_arr[faceIndex] =
			obj_polygonal_tv_face_vertices(ga->contents, i,
						       &index_arr_tv_faces);

		    index_arr_faces_2D[faceIndex] = index_arr_tv_faces;
		    break;
		case FACE_NV:
		    (*gfi)->num_vertices_arr[faceIndex] =
			obj_polygonal_nv_face_vertices(ga->contents, i,
						       &index_arr_nv_faces);

		    index_arr_faces_2D[faceIndex] = index_arr_nv_faces;
		    break;
		case FACE_TNV:
		    (*gfi)->num_vertices_arr[faceIndex] =
			obj_polygonal_tnv_face_vertices(ga->contents, i,
							&index_arr_tnv_faces);

		    index_arr_faces_3D[faceIndex] = index_arr_tnv_faces;
		    break;
	    }
	    (*gfi)->tot_vertices += (*gfi)->num_vertices_arr[faceIndex];
	} /* if (found) */
    } /* for (i = 0; i < numFaces; i++) */

    name_str = NULL;

    if (numFacesFound) {
	(*gfi)->num_faces = numFacesFound;
	(*gfi)->face_status = (short int *)bu_calloc((*gfi)->num_faces, sizeof(short int), "face_status");

	/* initialize array */
	for (i = 0; i < (*gfi)->num_faces; i++) {
	    (*gfi)->face_status[i] = 0;
	}
    } else {
	*gfi = NULL; /* this should already be null if no faces were found */
    }

    return;
} /* collect_grouping_faces_indexes */


/*
 * Populate the triangle index structure with the vertex indexes of
 * one or more triangles which represent a single face. This function
 * triangulates a face and places these triangles into the triangle
 * indexes (ti) structure to be processed later. Only convex
 * faces/polygons with coplanar vertices can be accurately
 * triangulated. It is assumed that when this function is called, the
 * number of face vertices is >= 3. This function allocates the memory
 * for the contents of the triangle indexes (ti) structure and this
 * memory is expected to be freed outside this function.
 *
 * Recommended function improvements:
 * - Correctly triangulate more than convex faces
 * - Warn of faces which may not be triangulated correctly
 */
void
populate_triangle_indexes(struct ga_t *ga,
			  struct gfi_t *gfi,
			  struct ti_t *ti,
			  size_t face_idx,
			  int texture_mode,  /* PROC_TEX, IGNR_TEX */
			  int normal_mode,   /* PROC_NORM, IGNR_NORM */
			  const struct bn_tol *tol)
{
    size_t vert_idx = 0;                 /* index into vertices within for-loop */
    size_t idx = 0;                      /* sub index */
    size_t num_new_tri = 0;              /* number of new triangles to create */
    fastf_t tmp_v[3] = {0.0, 0.0, 0.0};  /* temporary vertex */
    fastf_t tmp_n[3] = {0.0, 0.0, 0.0};  /* temporary normal */
    fastf_t tmp_t[3] = {0.0, 0.0, 0.0};  /* temporary texture vertex */
    fastf_t tmp_w = 0.0; /* temporary weight */
    size_t vofi = 0;     /* vertex obj file index */
    size_t nofi = 0;     /* normal obj file index */
    size_t tofi = 0;     /* texture vertex obj file index */
    size_t svofi = 0;    /* start vertex obj file index */
    size_t snofi = 0;    /* start normal obj file index */
    size_t stofi = 0;    /* start texture vertex obj file index */
    size_t max_tri_increment = 128;

    tri_arr_1D_t index_arr_tri_1D = NULL;
    tri_arr_2D_t index_arr_tri_2D = NULL;
    tri_arr_3D_t index_arr_tri_3D = NULL;

    double *facePoints = NULL;
    int *triFaces = NULL;
    size_t i = 0;
    size_t numFacePoints = 0;
    struct faceuse *fu = NULL;
    const int POINTS_PER_FACE = 3;

    if (ti->index_arr_tri == (void *)NULL) {

	/* adjust ti->tri_type for texture_mode and normal_mode */
	switch (gfi->face_type) {
	    case FACE_V:
		ti->tri_type = gfi->face_type;
		break;
	    case FACE_TV:
		if (texture_mode == PROC_TEX) {
		    ti->tri_type = gfi->face_type;
		} else {
		    ti->tri_type = FACE_V;
		}
		break;
	    case FACE_NV:
		if (normal_mode == PROC_NORM) {
		    ti->tri_type = gfi->face_type;
		} else {
		    ti->tri_type = FACE_V;
		}
		break;
	    case FACE_TNV:
		if ((texture_mode == PROC_TEX) && (normal_mode == PROC_NORM)) {
		    ti->tri_type = gfi->face_type;
		} else if ((texture_mode == IGNR_TEX) && (normal_mode == IGNR_NORM)) {
		    ti->tri_type = FACE_V;
		} else if ((texture_mode == PROC_TEX) && (normal_mode == IGNR_NORM)) {
		    ti->tri_type = FACE_TV;
		} else {
		    ti->tri_type = FACE_NV;
		}
		break;
	}

	/* the initial size will be the number of faces in the grouping */
	ti->max_tri = gfi->num_faces;

	/* allocate memory for initial 'ti->index_arr_tri' array */
	switch (ti->tri_type) {
	    case FACE_V:
		/* 3 vertice indexes */
		ti->index_arr_tri = (void *)bu_calloc(ti->max_tri * 3, sizeof(size_t), "triangle_indexes");
		break;
	    case FACE_TV:
		/* 3 vertice indexes + 3 texture vertice indexes */
	    case FACE_NV:
		/* 3 vertice indexes + 3 normal indexes */
		ti->index_arr_tri = (void *)bu_calloc(ti->max_tri * 6, sizeof(size_t), "triangle_indexes");
		break;
	    case FACE_TNV:
		/* 3 vertice indexes + 3 normal indexes + 3 texture vertice indexes */
		ti->index_arr_tri = (void *)bu_calloc(ti->max_tri * 9, sizeof(size_t), "triangle_indexes");
		break;
	}
    } /* ti->index_arr_tri == NULL */

    switch (ti->tri_type) {
	case FACE_V:
	    index_arr_tri_1D = (tri_arr_1D_t)ti->index_arr_tri;
	    break;
	case FACE_TV:
	case FACE_NV:
	    index_arr_tri_2D = (tri_arr_2D_t)ti->index_arr_tri;
	    break;
	case FACE_TNV:
	    index_arr_tri_3D = (tri_arr_3D_t)ti->index_arr_tri;
	    break;
    }

    /* copy face vertices into facePoints array */
    numFacePoints = gfi->num_vertices_arr[face_idx];
    facePoints = (double*)bu_malloc(numFacePoints * ELEMENTS_PER_POINT * sizeof(double), "facePoints");

    for (i = 0; i < numFacePoints; ++i) {
	retrieve_coord_index(ga, gfi, face_idx, i, tmp_v, tmp_n, tmp_t, &tmp_w,
			     &svofi, &snofi, &stofi);
	VMOVE(&facePoints[i * ELEMENTS_PER_POINT], tmp_v);
    }

    fu = make_faceuse_from_face(facePoints, numFacePoints);

    /* only try triangulation if face is well-formed enough to yield a faceuse */
    if (fu != NULL) {

	/* do simple or robust triangulation based on whether face is concave or convex */
	if (nmg_lu_is_convex(BU_LIST_FIRST(loopuse, &fu->lu_hd), &RTG.rtg_vlfree, tol)) {

	    /* compute number of new triangles to create */
	    if (gfi->num_vertices_arr[face_idx] > 3) {
		num_new_tri = gfi->num_vertices_arr[face_idx] - 2;
	    } else {
		num_new_tri = 1;
	    }

	    /* create triangles that all start at the first face vertex */
	    triFaces = (int*)bu_malloc(num_new_tri * POINTS_PER_FACE * sizeof(int), "triFaces");

	    for (vert_idx = 0; vert_idx < num_new_tri; vert_idx++) {
		triFaces[vert_idx * ELEMENTS_PER_POINT + X] = 0;
		triFaces[vert_idx * ELEMENTS_PER_POINT + Y] = (int)vert_idx + 1;
		triFaces[vert_idx * ELEMENTS_PER_POINT + Z] = (int)vert_idx + 2;
	    }

	    nmg_km(fu->s_p->r_p->m_p);
	} else {
	    triangulateFace(&triFaces, &num_new_tri, facePoints, numFacePoints, *tol);
	}
    } else {
	num_new_tri = 0;
    }

    bu_free(facePoints, "facePoints");

    /* if needed, increase size of 'ti->index_arr_tri' array */
    if ((ti->num_tri + num_new_tri) >= ti->max_tri) {
	ti->max_tri += max_tri_increment;
	switch (ti->tri_type) {
	    case FACE_V:
		ti->index_arr_tri = (void *)bu_realloc(index_arr_tri_1D, sizeof(size_t) * ti->max_tri * 3, "index_arr_tri");
		index_arr_tri_1D = (tri_arr_1D_t)(ti->index_arr_tri);
		break;
	    case FACE_TV:
	    case FACE_NV:
		ti->index_arr_tri = (void *)bu_realloc(index_arr_tri_2D, sizeof(size_t) * ti->max_tri * 6, "index_arr_tri");
		index_arr_tri_2D = (tri_arr_2D_t)(ti->index_arr_tri);
		break;
	    case FACE_TNV:
		ti->index_arr_tri = (void *)bu_realloc(index_arr_tri_3D, sizeof(size_t) * ti->max_tri * 9, "index_arr_tri");
		index_arr_tri_3D = (tri_arr_3D_t)(ti->index_arr_tri);
		break;
	}
    }

    /* populate triangle indexes array */
    for (vert_idx = 0; vert_idx < num_new_tri; vert_idx++) {
	for (idx = 0; idx < 3; idx++) {

	    retrieve_coord_index(ga, gfi, face_idx, triFaces[vert_idx * ELEMENTS_PER_POINT + idx], tmp_v,
				 tmp_n, tmp_t, &tmp_w, &vofi, &nofi, &tofi);

	    switch (ti->tri_type) {
		case FACE_V:
		    index_arr_tri_1D[ti->num_tri][idx] = vofi;
		    break;
		case FACE_TV:
		    index_arr_tri_2D[ti->num_tri][idx][0] = vofi;
		    index_arr_tri_2D[ti->num_tri][idx][1] = tofi;
		    break;
		case FACE_NV:
		    index_arr_tri_2D[ti->num_tri][idx][0] = vofi;
		    index_arr_tri_2D[ti->num_tri][idx][1] = nofi;
		    break;
		case FACE_TNV:
		    index_arr_tri_3D[ti->num_tri][idx][0] = vofi;
		    index_arr_tri_3D[ti->num_tri][idx][1] = tofi;
		    index_arr_tri_3D[ti->num_tri][idx][2] = nofi;
		    break;
	    }
	}
	ti->num_tri++;
    }

    bu_free(triFaces, "triFaces");

    return;
}


/*
 * Allocate the memory for and populate the sort-indexes used to sort
 * the necessary libobj vertex indexes. The sort-indexes created are
 * determined by the current face_type. Sorting of these indexes is
 * necessary as part of the process of creating a unique set of
 * vertices to build a bot primitive.
 */
void
populate_sort_indexes(struct ti_t *ti)
{
    size_t idx = 0;
    size_t num_indexes = ti->num_tri * 3;

    switch (ti->tri_type) {
	case FACE_V:
	    /* vsi ... vertex sort index */
	    ti->vsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->vsi");
	    for (idx = 0 ; idx < num_indexes ; idx++) {
		ti->vsi[idx] = idx;
	    }
	    break;
	case FACE_TV:
	    /* vsi ... vertex sort index */
	    ti->vsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->vsi");
	    /* tvsi ... texture vertex sort index */
	    ti->tvsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->tvsi");
	    for (idx = 0 ; idx < num_indexes ; idx++) {
		ti->vsi[idx] = idx * 2;
		ti->tvsi[idx] = (idx * 2) + 1;
	    }
	    break;
	case FACE_NV:
	    /* vsi ... vertex sort index */
	    ti->vsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->vsi");
	    /* vnsi ... vertex normal sort index */
	    ti->vnsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->vnsi");
	    for (idx = 0 ; idx < num_indexes ; idx++) {
		ti->vsi[idx] = idx * 2;
		ti->vnsi[idx] = (idx * 2) + 1;
	    }
	    break;
	case FACE_TNV:
	    /* vsi ... vertex sort index */
	    ti->vsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->vsi");
	    /* tvsi ... texture vertex sort index */
	    ti->tvsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->tvsi");
	    /* vnsi ... vertex normal sort index */
	    ti->vnsi = (size_t *)bu_calloc(num_indexes, sizeof(size_t), "ti->vnsi");
	    for (idx = 0 ; idx < num_indexes ; idx++) {
		ti->vsi[idx] = idx * 3;
		ti->tvsi[idx] = (idx * 3) + 1;
		ti->vnsi[idx] = (idx * 3) + 2;
	    }
	    break;
    }
    return;
}


/*
 * Sort the sort-indexes used to sort the necessary libobj vertex
 * indexes. Sorting of these indexes is necessary as part of the
 * process of creating a unique set of vertices to build a bot
 * primitive.
 */
void
sort_indexes(struct ti_t *ti)
{
    size_t num_indexes = ti->num_tri * 3;

    /* process vertex indexes */
    bu_sort(ti->vsi, num_indexes, sizeof ti->vsi[0], comp, ti->index_arr_tri);

    /* process vertex normal indexes */
    if (ti->tri_type == FACE_NV || ti->tri_type == FACE_TNV) {
	bu_sort(ti->vnsi, num_indexes, sizeof ti->vnsi[0], comp, ti->index_arr_tri);
    }

    /* process texture vertex indexes */
    if (ti->tri_type == FACE_TV || ti->tri_type == FACE_TNV) {
	bu_sort(ti->tvsi, num_indexes, sizeof ti->tvsi[0], comp, ti->index_arr_tri);
    }

    return;
}


/*
 * Create a unique sort-index from a provided non-unique sorted
 * sort-index by removing duplicates in the index. The non-unique
 * sort-indexes are freed when they are no longer needed.
 */
void
create_unique_indexes(struct ti_t *ti)
{
    size_t last = 0;
    size_t idx = 0;
    size_t counter = 0;
    size_t num_indexes = ti->num_tri * 3;
    size_t *tmp_arr = (size_t *)ti->index_arr_tri;

    /* process vertex indexes, count sorted
     * and unique libobj vertex indexes
     */
    last = tmp_arr[ti->vsi[0]];
    ti->num_uvi = 1;
    for (idx = 1; idx < num_indexes ; ++idx) {
	if (tmp_arr[ti->vsi[idx]] != last) {
	    last = tmp_arr[ti->vsi[idx]];
	    ti->num_uvi++;
	}
    }

    /* allocate unique triangle vertex index array */
    ti->uvi = (size_t *)bu_calloc(ti->num_uvi, sizeof(size_t), "ti->uvi");

    /* store unique triangle vertex index array */
    counter = 0;
    last = tmp_arr[ti->vsi[0]];
    ti->uvi[counter] = last;
    for (idx = 1; idx < num_indexes ; ++idx) {
	if (tmp_arr[ti->vsi[idx]] != last) {
	    last = tmp_arr[ti->vsi[idx]];
	    counter++;
	    ti->uvi[counter] = last;
	}
    }

    /* free 'triangle vertex sort index array' (ti->vsi) since it is
     * no longer needed since 'unique triangle vertex index array' has
     * been created.
     */
    bu_free(ti->vsi, "ti->vsi");
    /* end of process vertex indexes */

    /* process vertex normal indexes */
    if (ti->tri_type == FACE_NV || ti->tri_type == FACE_TNV) {
	/* count sorted and unique libobj vertex normal indexes */
	last = tmp_arr[ti->vnsi[0]];
	ti->num_uvni = 1;
	for (idx = 1; idx < num_indexes ; ++idx) {
	    if (tmp_arr[ti->vnsi[idx]] != last) {
		last = tmp_arr[ti->vnsi[idx]];
		ti->num_uvni++;
	    }
	}

	/* allocate unique triangle vertex normal index array */
	ti->uvni = (size_t *)bu_calloc(ti->num_uvni, sizeof(size_t), "ti->uvni");

	/* store unique triangle vertex normal index array */
	counter = 0;
	last = tmp_arr[ti->vnsi[0]];
	ti->uvni[counter] = last;
	for (idx = 1; idx < num_indexes ; ++idx) {
	    if (tmp_arr[ti->vnsi[idx]] != last) {
		last = tmp_arr[ti->vnsi[idx]];
		counter++;
		ti->uvni[counter] = last;
	    }
	}

	/* free 'triangle vertex normal sort index array' (ti->vnsi)
	 * since it is no longer needed since 'unique triangle vertex
	 * normal index array' has been created.
	 */
	bu_free(ti->vnsi, "ti->vnsi");
    } /* end of process vertex normal indexes */

    /* process texture vertex indexes */
    if (ti->tri_type == FACE_TV || ti->tri_type == FACE_TNV) {
	/* count sorted and unique libobj texture vertex indexes */
	last = tmp_arr[ti->tvsi[0]];
	ti->num_utvi = 1;
	for (idx = 1; idx < num_indexes ; ++idx) {
	    if (tmp_arr[ti->tvsi[idx]] != last) {
		last = tmp_arr[ti->tvsi[idx]];
		ti->num_utvi++;
	    }
	}

	/* allocate unique triangle texture vertex index array */
	ti->utvi = (size_t *)bu_calloc(ti->num_utvi, sizeof(size_t), "ti->utvi");

	/* store unique triangle texture vertex index array */
	counter = 0;
	last = tmp_arr[ti->tvsi[0]];
	ti->utvi[counter] = last;
	for (idx = 1; idx < num_indexes ; ++idx) {
	    if (tmp_arr[ti->tvsi[idx]] != last) {
		last = tmp_arr[ti->tvsi[idx]];
		counter++;
		ti->utvi[counter] = last;
	    }
	}

	/* free 'triangle texture vertex sort index array' (ti->tvsi)
	 * since it is no longer needed since 'unique triangle texture
	 * vertex index array' has been created.
	 */
	bu_free(ti->tvsi, "ti->tvsi");
    } /* end of process texture vertex indexes */
    return;
}


/*
 * Free memory allocated for the contents of the triangle index
 * structure.
 */
void
free_ti(struct ti_t *ti)
{
    if (ti == (struct ti_t *)NULL) {
	bu_log("function free_ti was passed a null pointer\n");
	return;
    }

    bu_free(ti->bot_faces, "ti->bot_faces");

    /* free 'triangle indices into vertex, normal, texture vertex lists' */
    bu_free(ti->index_arr_tri, "ti->index_arr_tri");

    /* free 'array of floats for bot vertices' */
    bu_free(ti->bot_vertices, "ti->bot_vertices");

    /* free 'array of floats for bot normals' */
    if (ti->tri_type == FACE_NV || ti->tri_type == FACE_TNV) {
	bu_free(ti->bot_normals, "ti->bot_normals");
	bu_free(ti->bot_face_normals, "ti->bot_face_normals");
    }

    /* free 'array of floats for texture vertices' */
    if (ti->tri_type == FACE_TV || ti->tri_type == FACE_TNV) {
	bu_free(ti->bot_texture_vertices, "ti->bot_texture_vertices");
    }

    if ((ti->bot_mode == RT_BOT_PLATE) || (ti->bot_mode == RT_BOT_PLATE_NOCOS)) {
	/* free array of bot_face_mode */
	bu_free((char *)ti->bot_face_mode, "ti->bot_face_mode");

	/* free 'array of floats for face thickness' */
	bu_free(ti->bot_thickness, "ti->bot_thickness");
    }

    return;
}


/*
 * Create the arrays used by the bot primitive which contain
 * floating-point values. The unique sorted-indexes are used to
 * retrieve the values from the libobj structures.
 *
 * Recommended function improvements:
 * - If unable to unitize a normal, deal with this.
 */
void
create_bot_float_arrays(struct ga_t *ga,
			struct ti_t *ti,
			fastf_t bot_thickness, /* plate-mode-bot thickness in mm units */
			fastf_t conv_factor)
{
    size_t i = 0;
    size_t j = 0;
    fastf_t tmp_norm[3] = {0.0, 0.0, 0.0};

    ti->bot_num_vertices = ti->num_uvi;
    ti->bot_num_faces = ti->num_tri;
    ti->bot_num_normals = ti->num_uvni;
    ti->bot_num_texture_vertices = ti->num_utvi;

    /* allocate bot_vertices array */
    ti->bot_vertices = (fastf_t *)bu_calloc(ti->bot_num_vertices * 3, sizeof(fastf_t), "ti->bot_vertices");

    /* populate bot_vertices array */
    for (i = 0 ; i < ti->bot_num_vertices ; i++) {
	for (j = 0 ; j < 3 ; j++) {
	    ti->bot_vertices[(i*3)+j] = ga->vert_list[ti->uvi[i]][j] * conv_factor;
	}
    }

    if ((ti->bot_mode == RT_BOT_PLATE) || (ti->bot_mode == RT_BOT_PLATE_NOCOS)) {
	/* allocate and populate bot_thickness array */
	ti->bot_thickness = (fastf_t *)bu_calloc(ti->bot_num_faces, sizeof(fastf_t), "ti->bot_thickness");
	for (i = 0 ; i < ti->bot_num_faces ; i++) {
	    ti->bot_thickness[i] = bot_thickness;
	}

	/* allocate and populate bot_face_mode array */
	ti->bot_face_mode = bu_bitv_new(ti->bot_num_faces);
	BU_BITSET(ti->bot_face_mode, 1); /* 1 indicates thickness is appended to hit
					  * point in ray direction, 0 indicates thickness
					  * is centered about hit point
					  */
    }

    /* normals */
    if (ti->tri_type == FACE_NV || ti->tri_type == FACE_TNV) {
	/* allocate bot_normals array */
	ti->bot_normals = (fastf_t *)bu_calloc(ti->bot_num_normals * 3, sizeof(fastf_t), "ti->bot_normals");

	/* populate bot_normals array */
	for (i = 0 ; i < ti->bot_num_normals ; i++) {
	    for (j = 0 ; j < 3 ; j++) {
		tmp_norm[j] = ga->norm_list[ti->uvni[i]][j];
	    }

	    if (MAGNITUDE(tmp_norm) < VDIVIDE_TOL) {
		bu_log("ERROR: unable to unitize normal (%f)(%f)(%f)\n", tmp_norm[0], tmp_norm[1], tmp_norm[2]);
		VMOVE(&(ti->bot_normals[i*3]), tmp_norm);
	    } else {
		VUNITIZE(tmp_norm);
		VMOVE(&(ti->bot_normals[i*3]), tmp_norm);
	    }
	}
    }

    /* textures */
    if (ti->tri_type == FACE_TV || ti->tri_type == FACE_TNV) {
	/* allocate bot_texture_vertices array */
	ti->bot_texture_vertices = (fastf_t *)bu_calloc(ti->bot_num_texture_vertices * 3,
							sizeof(fastf_t), "ti->bot_texture_vertices");

	/* populate bot_texture_vertices array */
	for (i = 0 ; i < ti->bot_num_texture_vertices ; i++) {
	    for (j = 0 ; j < 3 ; j++) {
		ti->bot_texture_vertices[(i*3)+j] = ga->texture_coord_list[ti->utvi[i]][j] * conv_factor;
	    }
	}
    }
    return;
}


/*
 * Create the arrays used by the bot primitive which contain integer
 * values. These integer values are the index values of the triangle
 * vertices and normals within the bot primitive. The unique
 * sorted-indexes are searched to convert the libobj index values of
 * each vertex to the appropriate index value in the bot primitive.
 * The memory for the bot integer arrays is allocated within this
 * function and is expected to be freed once the bot primitive is
 * created. The unique sorted-indexes are freed by this function once
 * they are no longer needed. This function returns a non-zero value
 * (i.e. fails) if bsearch is unable to find a value. This function
 * should never fail unless there is a logic bug in this code.
 */
int
create_bot_int_arrays(struct ti_t *ti)
{
    size_t i = 0;
    size_t j = 0;
    size_t *res_v = 0;
    size_t *res_n = 0;
    size_t *res_t = 0;

    tri_arr_1D_t index_arr_tri_1D = NULL;
    tri_arr_2D_t index_arr_tri_2D = NULL;
    tri_arr_3D_t index_arr_tri_3D = NULL;

    /* allocate memory for 'array of indices into bot_vertices array' */
    ti->bot_faces = (int *)bu_calloc(ti->bot_num_faces * 3, sizeof(int), "ti->bot_faces");

    /* allocate memory for 'array of indices into bot_normals array' */
    if (ti->tri_type == FACE_NV || ti->tri_type == FACE_TNV) {
	ti->bot_face_normals = (int *)bu_calloc(ti->bot_num_faces * 3, sizeof(int), "ti->bot_face_normals");
    }

    /* allocate memory for 'array indices into bot_texture_vertices array' */
    if (ti->tri_type == FACE_TV || ti->tri_type == FACE_TNV) {
	ti->bot_textures = (int *)bu_calloc(ti->bot_num_faces * 3, sizeof(int), "ti->bot_textures");
    }

    if (ti->tri_type == FACE_V) {
	index_arr_tri_1D = (tri_arr_1D_t)ti->index_arr_tri;
	for (i = 0 ; i < ti->bot_num_faces ; i++) {
	    for (j = 0 ; j < 3 ; j++) {
		if ((res_v = (size_t *)bsearch(&(index_arr_tri_1D[i][j]),
					       ti->uvi, ti->num_uvi, sizeof(size_t),
					       (int (*)(const void *a, const void *b))comp_b)) ==
		    (size_t *)NULL)
		{
		    bu_log("ERROR: FACE_V bsearch returned null, "
			   "face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_faces[(i*3)+j] = (int)(res_v - ti->uvi);
		}
	    }
	}
	bu_free(ti->uvi, "ti->uvi");
    }

    if (ti->tri_type == FACE_TV) {
	index_arr_tri_2D = (tri_arr_2D_t)ti->index_arr_tri;
	for (i = 0 ; i < ti->bot_num_faces ; i++) {
	    for (j = 0 ; j < 3 ; j++) {
		if ((res_v = (size_t *)bsearch(&(index_arr_tri_2D[i][j][0]),
					       ti->uvi, ti->num_uvi, sizeof(size_t),
					       (int (*)(const void *a, const void *b))comp_b)) ==
		    (size_t *)NULL)
		{
		    bu_log("ERROR: FACE_TV bsearch returned vertex null, "
			   "face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_faces[(i*3)+j] = (int)(res_v - ti->uvi);
		}
		if ((res_t = (size_t*)bsearch(&(index_arr_tri_2D[i][j][1]), ti->utvi, ti->num_utvi, sizeof(size_t),
					      (int (*)(const void *a, const void *b))comp_b)) == (size_t *)NULL) {
		    bu_log("ERROR: FACE_TV bsearch returned texture null, face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_textures[(i*3)+j] = (int)(res_t - ti->utvi);
		}
	    }
	}
	bu_free(ti->uvi, "ti->uvi");
	bu_free(ti->utvi, "ti->utvi");
    }

    if (ti->tri_type == FACE_NV) {
	index_arr_tri_2D = (tri_arr_2D_t)ti->index_arr_tri;
	for (i = 0 ; i < ti->bot_num_faces ; i++) {
	    for (j = 0 ; j < 3 ; j++) {
		if ((res_v = (size_t*)bsearch(&(index_arr_tri_2D[i][j][0]), ti->uvi, ti->num_uvi, sizeof(size_t),
					      (int (*)(const void *a, const void *b))comp_b)) == (size_t *)NULL) {
		    bu_log("ERROR: FACE_NV bsearch returned vertex null, face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_faces[(i*3)+j] = (int)(res_v - ti->uvi);
		}
		if ((res_n = (size_t*)bsearch(&(index_arr_tri_2D[i][j][1]), ti->uvni, ti->num_uvni, sizeof(size_t),
					      (int (*)(const void *a, const void *b))comp_b)) == (size_t *)NULL) {
		    bu_log("ERROR: FACE_NV bsearch returned normal null, face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_face_normals[(i*3)+j] = (int)(res_n - ti->uvni);
		}
	    }
	}
	bu_free(ti->uvi, "ti->uvi");
	bu_free(ti->uvni, "ti->uvni");
    }

    if (ti->tri_type == FACE_TNV) {
	index_arr_tri_3D = (tri_arr_3D_t)ti->index_arr_tri;
	for (i = 0 ; i < ti->bot_num_faces ; i++) {
	    for (j = 0 ; j < 3 ; j++) {
		if ((res_v = (size_t*)bsearch(&(index_arr_tri_3D[i][j][0]), ti->uvi, ti->num_uvi, sizeof(size_t),
					      (int (*)(const void *a, const void *b))comp_b)) == (size_t *)NULL) {
		    bu_log("ERROR: FACE_TNV bsearch returned vertex null, face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_faces[(i*3)+j] = (int)(res_v - ti->uvi);
		}
		if ((res_t = (size_t*)bsearch(&(index_arr_tri_3D[i][j][1]), ti->utvi, ti->num_utvi, sizeof(size_t),
					      (int (*)(const void *a, const void *b))comp_b)) == (size_t *)NULL) {
		    bu_log("ERROR: FACE_TNV bsearch returned texture null, face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_textures[(i*3)+j] = (int)(res_t - ti->utvi);
		}
		if ((res_n = (size_t*)bsearch(&(index_arr_tri_3D[i][j][2]), ti->uvni, ti->num_uvni, sizeof(size_t),
					      (int (*)(const void *a, const void *b))comp_b)) == (size_t *)NULL) {
		    bu_log("ERROR: FACE_TNV bsearch returned normal null, face=(%zu)idx=(%zu)\n", i, j);
		    return 1;
		} else {
		    ti->bot_face_normals[(i*3)+j] = (int)(res_n - ti->uvni);
		}
	    }
	}
	bu_free(ti->uvi, "ti->uvi");
	bu_free(ti->utvi, "ti->utvi");
	bu_free(ti->uvni, "ti->uvni");
    }
    return 0;
}


/*
 * Given a one-dimensional array of numbers of type size_t, the array
 * is sorted and duplicate entries removed, resulting in a sorted list
 * of unique values. For speed, a new list is allocated where the
 * unique sorted list is stored and the original list is freed.
 */
void
remove_duplicates_and_sort(size_t **list, size_t *count)
{
    size_t last = 0;
    size_t idx = 0;
    size_t idx2 = 0;
    size_t unique_count = 0;
    size_t *unique_arr = (size_t *)NULL;

    // Sanity
    if (!list || !*list || !count)
	return;

    bu_sort(*list, *count, sizeof(size_t), comp_b_sort, NULL);

    /* process list, count sorted and unique list elements */
    last = (*list)[0];
    unique_count = 1;
    for (idx = 1; idx < *count ; ++idx) {
	if ((*list)[idx] != last) {
	    last = (*list)[idx];
	    unique_count++;
	}
    }

    /* allocate unique array */
    unique_arr = (size_t *)bu_calloc(unique_count, sizeof(size_t), "unique_arr");

    /* store unique array */
    idx2 = 0;
    last = (*list)[0];
    unique_arr[idx2] = last;
    for (idx = 1; idx < *count ; ++idx) {
	if ((*list)[idx] != last) {
	    last = (*list)[idx];
	    idx2++;
	    unique_arr[idx2] = last;
	}
    }
    bu_free(*list, "*list");

    *list = unique_arr;
    *count = unique_count;

    return;
}


/*
 * Populate the fuse map array which maps vertex indexes to their
 * fused equivalent. This function is a support function for the
 * fuse_vertex function. This function performs the distance compare
 * operations between the vertices and populates the fuse map array.
 * The number of fused vertices is returned by this function.
 */
size_t
populate_fuse_map(struct ga_t *ga,
		  struct gfi_t *gfi,
		  fastf_t conv_factor,
		  const struct bn_tol *tol,
		  size_t *unique_index_list,
		  size_t num_unique_index_list,
		  int compare_type,  /* FUSE_WI_TOL or FUSE_EQUAL */
		  int vertex_type)   /* FUSE_VERT or FUSE_TEX_VERT */
{
    size_t idx1 = 0;
    size_t idx2 = 0;
    size_t fuse_count = 0;
    fastf_t tmp_v1[3];
    fastf_t tmp_v2[3];
    fastf_t distance_between_vertices = 0.0;
    size_t *fuse_map = (size_t *)NULL;
    short int *fuse_flag = (short int *)NULL;
    size_t fuse_offset = 0;

    if (vertex_type == FUSE_TEX_VERT) {
	fuse_map = gfi->texture_vertex_fuse_map;
	fuse_flag = gfi->texture_vertex_fuse_flag;
	fuse_offset = gfi->texture_vertex_fuse_offset;
    } else {
	fuse_map = gfi->vertex_fuse_map;
	fuse_flag = gfi->vertex_fuse_flag;
	fuse_offset = gfi->vertex_fuse_offset;
    }

    /* the loop below is the expensive part of the fusing operation. */

    /* only set the flag for duplicates */
    idx1 = 0;
    while (idx1 < num_unique_index_list) {
	if (fuse_flag[unique_index_list[idx1] - fuse_offset] == 0) {
	    /* true if current vertex has not already been identified as a duplicate */
	    idx2 = idx1 + 1;
	    VMOVE(tmp_v1, ga->vert_list[unique_index_list[idx1]]);
	    VSCALE(tmp_v1, tmp_v1, conv_factor);
	    fuse_map[unique_index_list[idx1] - fuse_offset] = unique_index_list[idx1];
	    while (idx2 < num_unique_index_list) {
		if (fuse_flag[unique_index_list[idx2] - fuse_offset] == 0) {
		    VMOVE(tmp_v2, ga->vert_list[unique_index_list[idx2]]);
		    VSCALE(tmp_v2, tmp_v2, conv_factor);
		    if ((compare_type == FUSE_EQUAL) ? VEQUAL(tmp_v1, tmp_v2) :
			bg_pnt3_pnt3_equal(tmp_v1, tmp_v2, tol)) {
			if (ga->gcv_options->debug_mode) {
			    distance_between_vertices = DIST_PNT_PNT(tmp_v1, tmp_v2);
			    bu_log("found equal i1=(%zu)vi1=(%zu)v1=(%f)(%f)(%f), i2=(%zu)vi2=(%zu)v2=(%f)(%f)(%f), dist = (%lu mm)\n",
				   idx1, unique_index_list[idx1], tmp_v1[0], tmp_v1[1], tmp_v1[2],
				   idx2, unique_index_list[idx2], tmp_v2[0], tmp_v2[1], tmp_v2[2],
				   (unsigned long)distance_between_vertices);
			}
			fuse_map[unique_index_list[idx2] - fuse_offset] = unique_index_list[idx1];
			fuse_flag[unique_index_list[idx2] - fuse_offset] = 1;
			fuse_count++;
		    }
		}
		idx2++;
	    }
	}
	idx1++;
    }

    if (ga->gcv_options->debug_mode) {
	for (idx1 = 0 ; idx1 < num_unique_index_list ; idx1++) {
	    bu_log("fused unique_index_list = (%zu)->(%zu)\n", unique_index_list[idx1],
		   fuse_map[unique_index_list[idx1] - fuse_offset]);
	}
    }

    return fuse_count;
}


/*
 * Perform a vertex fuse of the given face grouping. Vertices which
 * are close enough together to be considered the same vertex are
 * joined. A mapping is created to convert each vertex index to their
 * fused equivalent. The number of fused vertices is returned by this
 * function. Both vertices and texture vertices can be fused by this
 * function and the determination of equal vertices can be performed
 * using the distance tolerance or VEQUAL. Fusing the vertices is
 * useful when the obj file was formed improperly where identical or
 * virtually identical vertices have different indexes and it is
 * desired to identify faces sharing an edge by comparing the index
 * values of the edge vertices. For this same reason, vertex fusing is
 * necessary for testing surface closure where closure is defined by
 * no open edges. A closed edge is where the edge is shared by exactly
 * two faces, a open edge is where an edge is used by only one face.
 */
size_t
fuse_vertex(struct ga_t *ga,
	    struct gfi_t *gfi,
	    fastf_t conv_factor,
	    const struct bn_tol *tol,
	    int vertex_type,   /* FUSE_VERT or FUSE_TEX_VERT */
	    int compare_type)  /* FUSE_WI_TOL or FUSE_EQUAL */
{
    size_t face_idx = 0; /* index into faces within for-loop */
    size_t vert_idx = 0; /* index into vertices within for-loop */
    fastf_t tmp_v[3] = {0.0, 0.0, 0.0}; /* temporary vertex */
    fastf_t tmp_n[3] = {0.0, 0.0, 0.0}; /* temporary normal */
    fastf_t tmp_t[3] = {0.0, 0.0, 0.0}; /* temporary texture vertex */
    fastf_t tmp_w = 0.0; /* temporary weight */
    size_t vofi = 0;     /* vertex obj file index */
    size_t nofi = 0;     /* normal obj file index */
    size_t tofi = 0;     /* texture vertex obj file index */
    size_t min_vert_idx = 0;
    size_t max_vert_idx = 0;
    size_t min_tex_vert_idx = 0;
    size_t max_tex_vert_idx = 0;

    size_t *vertex_index_list = (size_t *)NULL;
    size_t *texture_vertex_index_list = (size_t *)NULL;
    size_t *index_list_tmp = (size_t *)NULL;
    size_t num_index_list = 0;
    size_t max_index_list = 0;
    size_t max_index_list_increment = 128;

    size_t num_unique_vertex_index_list = 0;
    size_t num_unique_texture_vertex_index_list = 0;

    size_t idx1 = 0;

    size_t fuse_count = 0;

    if ((gfi->face_type == FACE_V || gfi->face_type == FACE_NV) &&
	(vertex_type == FUSE_TEX_VERT))
    {
	return 0; /* nothing to process */
    }

    /* test if fuse vertices has already been run, if so free old arrays */
    if (vertex_type == FUSE_VERT) {
	bu_free(gfi->vertex_fuse_map, "gfi->vertex_fuse_map");
	gfi->num_vertex_fuse = 0;
	gfi->vertex_fuse_offset = 0;
    }

    /* test if fuse texture vertices has already been run, if so free
     * old arrays.
     */
    if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) && (vertex_type == FUSE_TEX_VERT)) {
	bu_free(gfi->texture_vertex_fuse_map, "gfi->texture_vertex_fuse_map");
	gfi->num_texture_vertex_fuse = 0;
	gfi->texture_vertex_fuse_offset = 0;
    }

    max_index_list += max_index_list_increment;
    /* initial allocation for vertex_index_list, and as applicable,
     * the normal_index_list, texture_vertex_index_list
     */

    if (vertex_type == FUSE_VERT) {
	vertex_index_list = (size_t *)bu_calloc(max_index_list, sizeof(size_t),	"vertex_index_list");
    }

    if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) &&
	(vertex_type == FUSE_TEX_VERT))
    {
	texture_vertex_index_list = (size_t *)bu_calloc(max_index_list, sizeof(size_t), "texture_vertex_index_list");
    }

    /* loop thru all polygons and collect max and min index values and
     * fill the vertex_index_list and as applicable, the
     * texture_vertex_index_list
     */
    for (face_idx = 0; face_idx < gfi->num_faces; face_idx++) {
	for (vert_idx = 0; vert_idx < gfi->num_vertices_arr[face_idx];
	     vert_idx++)
	{
	    retrieve_coord_index(ga, gfi, face_idx, vert_idx, tmp_v, tmp_n,
				 tmp_t, &tmp_w, &vofi, &nofi, &tofi);

	    if (face_idx == 0 && vert_idx == 0) {
		/* set initial max and min */
		if (vertex_type == FUSE_VERT) {
		    vertex_index_list[num_index_list] = vofi;
		    min_vert_idx = vofi;
		    max_vert_idx = vofi;
		}
		if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) &&
		    (vertex_type == FUSE_TEX_VERT))
		{
		    texture_vertex_index_list[num_index_list] = tofi;
		    min_tex_vert_idx = tofi;
		    max_tex_vert_idx = tofi;
		}
	    } else {
		/* set max and min */
		if (vertex_type == FUSE_VERT) {
		    vertex_index_list[num_index_list] = vofi;
		    V_MAX(max_vert_idx, vofi);
		    V_MIN(min_vert_idx, vofi);
		}
		if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) &&
		    (vertex_type == FUSE_TEX_VERT))
		{
		    texture_vertex_index_list[num_index_list] = tofi;
		    V_MAX(max_tex_vert_idx, tofi);
		    V_MIN(min_tex_vert_idx, tofi);
		}
	    }
	    num_index_list++;
	    if (num_index_list >= max_index_list) {
		max_index_list += max_index_list_increment;
		if (vertex_type == FUSE_VERT) {
		    index_list_tmp = (size_t *)bu_realloc(vertex_index_list, sizeof(size_t) * max_index_list, "index_list_tmp");
		    vertex_index_list = index_list_tmp;
		}
		if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) &&
		    (vertex_type == FUSE_TEX_VERT))
		{
		    index_list_tmp = (size_t *) bu_realloc(texture_vertex_index_list, sizeof(size_t) * max_index_list, "index_list_tmp");
		    texture_vertex_index_list = index_list_tmp;
		}
	    }
	} /* end of vert_idx for-loop */
    } /* end of face_idx for-loop */

    /* compute size of fuse_map and fuse_flag arrays and allocate
     * these arrays. set fuse_offset values
     */
    if (vertex_type == FUSE_VERT) {
	gfi->num_vertex_fuse = max_vert_idx - min_vert_idx + 1;
	gfi->vertex_fuse_offset = min_vert_idx;
	gfi->vertex_fuse_map = (size_t *)bu_calloc(gfi->num_vertex_fuse, sizeof(size_t), "gfi->vertex_fuse_map");
	gfi->vertex_fuse_flag = (short int *)bu_calloc(gfi->num_vertex_fuse, sizeof(short int), "gfi->vertex_fuse_flag");

	/* initialize arrays */
	for (idx1 = 0 ; idx1 < gfi->num_vertex_fuse ; idx1++) {
	    gfi->vertex_fuse_map[idx1] = 0;
	    gfi->vertex_fuse_flag[idx1] = 0;
	}
    }

    if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) &&
	(vertex_type == FUSE_TEX_VERT))
    {
	gfi->num_texture_vertex_fuse = max_tex_vert_idx - min_tex_vert_idx + 1;
	gfi->texture_vertex_fuse_offset = min_tex_vert_idx;
	gfi->texture_vertex_fuse_map = (size_t *)bu_calloc(gfi->num_texture_vertex_fuse, sizeof(size_t), "gfi->texture_vertex_fuse_map");
	gfi->texture_vertex_fuse_flag = (short int *)bu_calloc(gfi->num_texture_vertex_fuse, sizeof(short int), "gfi->texture_vertex_fuse_flag");

	/* initialize arrays */
	for (idx1 = 0 ; idx1 < gfi->num_texture_vertex_fuse ; idx1++) {
	    gfi->texture_vertex_fuse_map[idx1] = 0;
	    gfi->texture_vertex_fuse_flag[idx1] = 0;
	}
    }

    /* initialize unique counts, these will be reduced as duplicates
     * are removed from the lists
     */
    num_unique_vertex_index_list = num_index_list;
    num_unique_texture_vertex_index_list = num_index_list;

    if (ga->gcv_options->debug_mode) {
	if (vertex_index_list && (num_index_list > 0)) {
	    for (idx1 = 0 ; idx1 < num_index_list ; idx1++) {
		bu_log("non-unique sorted vertex_index_list[idx1] = (%zu)\n",
		       vertex_index_list[idx1]);
	    }
	    bu_log("num non-unique sorted vertex_index_list[idx1] = (%zu)\n",
		   num_index_list);
	}
    }

    /* remove duplicates from lists and return a sorted list, pass in
     * the number of elements in the non-unique array and receive the
     * number of element in the unique array, a new list is created
     * and passed back by this function. the new list will be returned
     * sorted
     */
    if (vertex_type == FUSE_VERT) {
	remove_duplicates_and_sort(&vertex_index_list,
				   &num_unique_vertex_index_list);
    }

    if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) &&
	(vertex_type == FUSE_TEX_VERT))
    {
	remove_duplicates_and_sort(&texture_vertex_index_list,
				   &num_unique_texture_vertex_index_list);
    }

    if (ga->gcv_options->debug_mode && vertex_index_list && (num_unique_vertex_index_list > 0)) {
	for (idx1 = 0 ; idx1 < num_unique_vertex_index_list ; idx1++) {
	    bu_log("unique sorted vertex_index_list[idx1] = (%zu)\n",
		   vertex_index_list[idx1]);
	}
	bu_log("num unique sorted vertex_index_list[idx1] = (%zu)\n",
	       num_unique_vertex_index_list);
    }

    if (vertex_type == FUSE_VERT) {
	fuse_count = populate_fuse_map(ga, gfi, conv_factor, tol,
				       vertex_index_list, num_unique_vertex_index_list, compare_type,
				       vertex_type);

	bu_free(vertex_index_list, "vertex_index_list");
	bu_free(gfi->vertex_fuse_flag, "gfi->vertex_fuse_flag");
    }

    if ((gfi->face_type == FACE_TV || gfi->face_type == FACE_TNV) && (vertex_type == FUSE_TEX_VERT))
    {
	fuse_count = populate_fuse_map(ga, gfi, conv_factor, tol,
				       texture_vertex_index_list, num_unique_texture_vertex_index_list,
				       compare_type, vertex_type);

	bu_free(texture_vertex_index_list, "texture_vertex_index_list");
	bu_free(gfi->texture_vertex_fuse_flag, "gfi->texture_vertex_fuse_flag");
    }

    if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
	bu_log("Fused (%zu) vertex for obj file face grouping name (%s), "
	       "obj file face grouping index (%zu)\n",
	       fuse_count, bu_vls_addr(gfi->raw_grouping_name),
	       gfi->grouping_index + 1);
    }

    return fuse_count;
}


/*
 * For a grouping of faces, test if the surface is closed. This
 * function returns the number of open edges. Zero open edges
 * indicates a closed surface. This function traverses all the faces
 * in the face grouping and generates a list of edges using the libobj
 * index values of the edge vertices to identify the edges.  For a
 * closed surface, there must be two of each edge in the list.  A
 * BRL-CAD overlay file can be generated containing the open edges
 * when the plot_mode parameter is set to PLOT_ON. The overlay file
 * will be of the same name as the BRL-CAD primitive except the file
 * extension will be ".pl" instead of ".s". The overlay files will be
 * placed in the same directory as where the "obj-g" command was
 * executed. It is not required but highly recommended that the
 * fuse_vertex function be run on the face grouping before
 * test_closure otherwise some shared edges may be missed. If this
 * function returns zero open edges, this surface should be closed.
 * If this function indicates the surface is open, further testing is
 * necessary, not done by this function, to verify the surface is
 * open. The overlay of open edges may be useful for analysis of
 * surfaces reported as open.
 *
 * Recommended function improvements:
 * - Improve algorithm for identifying shared edges.
 *
 * For example the right edge of triangle 1 would be identified by the
 * current algorithm as an open edge since there is no single matching
 * edge to pair with it. An improved algorithm should identify that
 * the left edges of triangles 2 and 3 should be paired with the right
 * edge of triangle 1 to determine this is a closed edge. Note: In the
 * diagram below, the top vertex of triangle 1 is the same vertex as
 * the top left vertex of triangle 2, the same goes for the bottom
 * right vertex of triangle 1 and the bottom vertex of triangle 3. The
 * bottom vertex of triangle 2 (i.e. top left vertex of triangle 3)
 * does not exist in triangle 1.
 *
 *            * *___*
 *           / \ \2/
 *          /   \ *___*
 *         /  1  \ \3/
 *        *_______* *
 *
 */
size_t
test_closure(struct ga_t *ga,
	     struct gfi_t *gfi,
	     fastf_t conv_factor,
	     int plot_mode,        /* PLOT_OFF, PLOT_ON */
	     int face_test_type,
	     const struct bn_tol *tol)
{
    size_t face_idx = 0; /* index into faces within for-loop */
    size_t vert_idx = 0; /* index into vertices within for-loop */
    fastf_t tmp_v[3] = {0.0, 0.0, 0.0}; /* temporary vertex */
    fastf_t tmp_n[3] = {0.0, 0.0, 0.0}; /* temporary normal */
    fastf_t tmp_t[3] = {0.0, 0.0, 0.0}; /* temporary texture vertex */
    fastf_t tmp_w = 0.0;  /* temporary weight */
    size_t vofi0 = 0;    /* vertex obj file index */
    size_t vofi1 = 0;    /* vertex obj file index */
    size_t nofi = 0;     /* normal obj file index */
    size_t tofi = 0;     /* texture vertex obj file index */

    size_t first_idx = 0;
    edge_arr_2D_t edges = (edge_arr_2D_t)NULL;
    edge_arr_2D_t edges_tmp = (edge_arr_2D_t)NULL;
    size_t previous_edge[2] = {0, 0};
    size_t max_edges = 0;
    size_t max_edges_increment = 128;
    size_t edge_count = 0;
    size_t idx = 0;
    size_t match = 0;
    size_t open_edges = 0;

    FILE *plotfp = NULL;
    vect_t pnt1;
    vect_t pnt2;

    struct bu_vls plot_file_name = BU_VLS_INIT_ZERO;

    max_edges += max_edges_increment;
    edges = (edge_arr_2D_t)bu_calloc(max_edges * 2, sizeof(size_t), "edges");

    /* loop thru all polygons */
    for (face_idx = 0 ; face_idx < gfi->num_faces ; face_idx++) {
	if (!test_face(ga, gfi, face_idx, conv_factor, tol, face_test_type, 0)) {
	    for (vert_idx = 0 ; vert_idx < gfi->num_vertices_arr[face_idx] ; vert_idx++) {
		retrieve_coord_index(ga, gfi, face_idx, vert_idx,
				     tmp_v, tmp_n, tmp_t, &tmp_w, &vofi0, &nofi, &tofi);
		if (vert_idx == 0) {
		    first_idx = vofi0;
		}
		if ((vert_idx + 1) < gfi->num_vertices_arr[face_idx]) {
		    retrieve_coord_index(ga, gfi, face_idx, vert_idx+1,
					 tmp_v, tmp_n, tmp_t, &tmp_w, &vofi1, &nofi, &tofi);
		} else {
		    vofi1 = first_idx;
		}

		if (vofi0 <= vofi1) {
		    edges[edge_count][0] = vofi0;
		    edges[edge_count][1] = vofi1;
		} else {
		    edges[edge_count][0] = vofi1;
		    edges[edge_count][1] = vofi0;
		}

		if (ga->gcv_options->debug_mode) {
		    bu_log("edges (%zu)(%zu)(%zu)(%zu)\n", face_idx, edge_count,
			   edges[edge_count][0], edges[edge_count][1]);
		}
		edge_count++;

		if (edge_count >= max_edges) {
		    max_edges += max_edges_increment;
		    edges_tmp = (edge_arr_2D_t)bu_realloc(edges, sizeof(size_t) * max_edges * 2, "edges_tmp");
		    edges = edges_tmp;
		}
	    }
	}
    } /* ends when edges list is complete */

    bu_sort(edges, edge_count, sizeof(size_t) * 2, comp_c, NULL);

    if (ga->gcv_options->debug_mode) {
	for (idx = 0 ; idx < edge_count ; idx++) {
	    bu_log("sorted edges (%zu)(%zu)(%zu)\n", idx, edges[idx][0], edges[idx][1]);
	}
    }

    previous_edge[0] = edges[0][0];
    previous_edge[1] = edges[0][1];
    for (idx = 1 ; idx < edge_count ; idx++) {
	if ((previous_edge[0] == edges[idx][0]) && (previous_edge[1] == edges[idx][1])) {
	    match++;
	} else {
	    if (match == 0) {
		if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		    bu_log("open edge (%zu)= %f %f %f (%zu)= %f %f %f \n",
			   previous_edge[0],
			   ga->vert_list[previous_edge[0]][0] * conv_factor,
			   ga->vert_list[previous_edge[0]][1] * conv_factor,
			   ga->vert_list[previous_edge[0]][2] * conv_factor,
			   previous_edge[1],
			   ga->vert_list[previous_edge[1]][0] * conv_factor,
			   ga->vert_list[previous_edge[1]][1] * conv_factor,
			   ga->vert_list[previous_edge[1]][2] * conv_factor);
		}
		if ((plot_mode == PLOT_ON) && (open_edges == 0)) {
		    bu_vls_sprintf(&plot_file_name, "%s.%zu.%d.o.pl",
				   bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1,
				   gfi->face_type);
		    cleanup_name(&plot_file_name);
		    if ((plotfp = fopen(bu_vls_addr(&plot_file_name), "wb")) == (FILE *)NULL) {
			bu_log("ERROR: unable to create plot file (%s)\n", bu_vls_addr(&plot_file_name));
			bu_vls_free(&plot_file_name);
			plot_mode = PLOT_OFF;
		    }
		}
		if (plot_mode == PLOT_ON) {
		    VMOVE(pnt1, ga->vert_list[previous_edge[0]]);
		    VMOVE(pnt2, ga->vert_list[previous_edge[1]]);
		    VSCALE(pnt1, pnt1, conv_factor);
		    VSCALE(pnt2, pnt2, conv_factor);
		    pdv_3line(plotfp, pnt1, pnt2);
		}
		open_edges++;
	    } else {
		match = 0;
	    }
	}
	previous_edge[0] = edges[idx][0];
	previous_edge[1] = edges[idx][1];
    }

    bu_free(edges, "edges");

    if (open_edges) {
	gfi->closure_status = SURF_OPEN;
	if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
	    bu_log("Surface closure failed for obj file face grouping name (%s), obj file face grouping index (%zu), (%zu) open edges\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1, open_edges);
	}
    } else {
	gfi->closure_status = SURF_CLOSED;
	if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
	    bu_log("Surface closure success for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
    }

    if ((plot_mode == PLOT_ON) && (open_edges > 0)) {
	bu_vls_free(&plot_file_name);
	(void)fclose(plotfp);
    }

    return open_edges;
}


/*
 * For a grouping of faces, write a bot primitive to a BRL-CAD
 * database file (i.e. ".g" file). Texture_mode should be set to
 * IGNR_TEX since the ability to use the obj file texture vertices is
 * not implemented. Texture_mode can be set to PROC_TEX but extra
 * processing will be performed and will provide no benefit.  If
 * normal_mode is set to PROC_NORM, any normals provided in the obj
 * file for the current grouping of faces will be included in the bot
 * primitive. If the provided normals are smoothing normals, the
 * resulting raytrace of the bot will be smooth. The type of bot
 * created is chosen by the bot_output_mode. If the type of bot
 * selected is RT_BOT_PLATE or RT_BOT_PLATE_NOCOS the thickness of the
 * bot plates is defined by the parameter bot_thickness. This function
 * will return a non-zero value if an error occurred. Bot primitives
 * will be named according to the grouping name in the obj file with
 * the following exceptions. Characters invalid for BRL-CAD primitive
 * names and operating system file names are converted to
 * underscore. The remaining portions of the name indicate the decimal
 * index number of the grouping in the obj file, face_type number of
 * the grouping {1=v|2=tv|3=nv|4=tnv}, primitive type {b=bot} and
 * closure status {u=untested|c=closed|o=open}.
 */
int
output_to_bot(struct ga_t *ga,
	      struct gfi_t *gfi,
	      struct rt_wdb *outfp,
	      fastf_t conv_factor,
	      const struct bn_tol *tol,
	      fastf_t bot_thickness,         /* plate-mode-bot thickness in mm units */
	      int texture_mode,              /* PROC_TEX, IGNR_TEX */
	      int normal_mode,               /* PROC_NORM, IGNR_NORM */
	      unsigned char bot_output_mode, /* RT_BOT_PLATE, RT_BOT_PLATE_NOCOS, RT_BOT_SOLID, RT_BOT_SURFACE */
	      char bot_orientation,
	      int face_test_type)
{
    struct ti_t ti;              /* triangle indices structure */
    size_t num_faces_killed = 0; /* number of degenerate faces killed in the current bot */
    size_t face_idx = 0;         /* index into faces within for-loop */
    int ret = 0;                 /* 0=success !0=fail */

    /* initialize triangle indices structure */
    memset((void *)&ti, 0, sizeof(struct ti_t)); /* probably redundant */
    ti.index_arr_tri = (void *)NULL; /* triangle indices into vertex, normal, texture vertex lists */
    ti.num_tri = 0;                  /* number of triangles represented by index_arr_triangles */
    ti.max_tri = 0;                  /* maximum number of triangles based on current memory allocation */
    ti.tri_type = 0;                 /* i.e. FACE_V, FACE_TV, FACE_NV or FACE_TNV */
    ti.vsi = (size_t *)NULL;         /* triangle vertex sort index array */
    ti.vnsi = (size_t *)NULL;        /* triangle vertex normal sort index array */
    ti.tvsi = (size_t *)NULL;        /* triangle texture vertex sort index array */
    ti.uvi = (size_t *)NULL;         /* unique triangle vertex index array */
    ti.uvni = (size_t *)NULL;        /* unique triangle vertex normal index array */
    ti.utvi = (size_t *)NULL;        /* unique triangle texture vertex index array */
    ti.num_uvi = 0;                  /* number of unique triangle vertex indexes in uvi array */
    ti.num_uvni = 0;                 /* number of unique triangle vertex normal index in uvni array */
    ti.num_utvi = 0;                 /* number of unique triangle texture vertex index in utvi array */
    ti.bot_mode = 0;                 /* bot mode RT_BOT_PLATE, RT_BOT_PLATE_NOCOS, RT_BOT_SOLID,
				      * RT_BOT_SURFACE
				      */
    ti.bot_vertices = (fastf_t *)NULL;         /* array of float for bot vertices [bot_num_vertices*3] */
    ti.bot_thickness = (fastf_t *)NULL;        /* array of floats for face thickness [bot_num_faces] */
    ti.bot_normals = (fastf_t *)NULL;          /* array of floats for bot normals [bot_num_normals*3] */
    ti.bot_texture_vertices = (fastf_t *)NULL; /* array of floats for texture vertices [bot_num_texture_vertices*3] */
    ti.bot_faces = (int *)NULL;                /* array of indices into bot_vertices array [bot_num_faces*3] */
    ti.bot_face_mode = (struct bu_bitv *)NULL; /* bu_list of face modes for plate-mode-bot [bot_num_faces] */
    ti.bot_face_normals = (int *)NULL;         /* array of indices into bot_normals array [bot_num_faces*3] */
    ti.bot_textures  = (int *)NULL;            /* array of indices into bot_texture_vertices array */
    ti.bot_num_vertices = 0;                   /* number of vertices in bot_vertices array */
    ti.bot_num_faces = 0;                      /* number of faces in bot_faces array */
    ti.bot_num_normals = 0;                    /* number of normals in bot_normals array */
    ti.bot_num_texture_vertices = 0;           /* number of textures in bot_texture_vertices array */

    ti.bot_mode = bot_output_mode;

    /* loop thru all the polygons (i.e. faces) to be placed in the current bot */
    for (face_idx = 0 ; face_idx < gfi->num_faces ; face_idx++) {
	if (ga->gcv_options->debug_mode) {
	    bu_log("num vertices in current polygon = (%zu)\n", gfi->num_vertices_arr[face_idx]);
	}

	/* test for degenerate face */
	if (test_face(ga, gfi, face_idx, conv_factor, tol, face_test_type, 0)) {
	    num_faces_killed++;
	} else {
	    /* populates triangle indexes for the current face.  if
	     * necessary, allocates additional memory for the triangle
	     * indexes array. if necessary, triangulates the face.
	     */
	    populate_triangle_indexes(ga, gfi, &ti, face_idx, texture_mode, normal_mode, tol);
	}
    } /* loop exits when all polygons within the current grouping have
       * been triangulated (if necessary) and the vertex, normal and
       * texture vertices indexes have been placed in the array
       * 'ti.index_arr_tri'.
       */

    /* if no triangles exist to process, report this and return */
    if (ti.num_tri == 0) {
	bu_log("WARNING: No triangles to output, dropped (%zu) of (%zu) faces for obj file face grouping name (%s), obj file face grouping index (%zu)\n",
	       num_faces_killed, gfi->num_faces, bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	return 0;
    }

    populate_sort_indexes(&ti);

    sort_indexes(&ti);

    create_unique_indexes(&ti);

    create_bot_float_arrays(ga, &ti, bot_thickness, conv_factor);

    /* added 1 to grouping_index so the index number used in the
     * primitive name matches the obj file index since internal
     * indexes from libobj start at 0 but obj file indexes start at 1.
     */
    switch (gfi->closure_status) {
	case SURF_UNTESTED:
	    bu_vls_sprintf(gfi->primitive_name, "%s.%zu.%d.b.u.s", bu_vls_addr(gfi->raw_grouping_name),
			   gfi->grouping_index + 1, gfi->face_type);
	    break;
	case SURF_CLOSED:
	    bu_vls_sprintf(gfi->primitive_name, "%s.%zu.%d.b.c.s", bu_vls_addr(gfi->raw_grouping_name),
			   gfi->grouping_index + 1, gfi->face_type);
	    break;
	case SURF_OPEN:
	    bu_vls_sprintf(gfi->primitive_name, "%s.%zu.%d.b.o.s", bu_vls_addr(gfi->raw_grouping_name),
			   gfi->grouping_index + 1, gfi->face_type);
	    break;
    }

    cleanup_name(gfi->primitive_name);

    if (create_bot_int_arrays(&ti)) {
	bu_log("ERROR: function create_bot_int_arrays returned an error, aborting bot creation for object (%s)\n",
	       bu_vls_addr(gfi->raw_grouping_name));
	free_ti(&ti);
	return 1;
    }

    /* write bot to ".g" file */
    if (ti.tri_type == FACE_TNV) {
	ret = mk_bot_w_normals_and_uvs(outfp, bu_vls_addr(gfi->primitive_name),
				       ti.bot_mode, bot_orientation,
				       RT_BOT_HAS_SURFACE_NORMALS | RT_BOT_USE_NORMALS | RT_BOT_HAS_TEXTURE_UVS,
				       ti.bot_num_vertices, ti.bot_num_faces, ti.bot_vertices,
				       ti.bot_faces, ti.bot_thickness, ti.bot_face_mode,
				       ti.bot_num_normals, ti.bot_normals, ti.bot_face_normals,
				       ti.bot_num_texture_vertices, ti.bot_texture_vertices, ti.bot_textures);
    } else if (ti.tri_type == FACE_TV) {
	ret = mk_bot_w_normals_and_uvs(outfp, bu_vls_addr(gfi->primitive_name),
				       ti.bot_mode, bot_orientation,
				       RT_BOT_HAS_TEXTURE_UVS,
				       ti.bot_num_vertices, ti.bot_num_faces, ti.bot_vertices,
				       ti.bot_faces, ti.bot_thickness, ti.bot_face_mode,
				       0, NULL, NULL,
				       ti.bot_num_texture_vertices, ti.bot_texture_vertices, ti.bot_textures);
    } else if (ti.tri_type == FACE_NV) {
	ret = mk_bot_w_normals(outfp, bu_vls_addr(gfi->primitive_name),
			       ti.bot_mode, bot_orientation,
			       RT_BOT_HAS_SURFACE_NORMALS | RT_BOT_USE_NORMALS,
			       ti.bot_num_vertices, ti.bot_num_faces, ti.bot_vertices,
			       ti.bot_faces, ti.bot_thickness, ti.bot_face_mode,
			       ti.bot_num_normals, ti.bot_normals, ti.bot_face_normals);
    } else {
	ret = mk_bot(outfp, bu_vls_addr(gfi->primitive_name), ti.bot_mode,
		     bot_orientation, 0, ti.bot_num_vertices, ti.bot_num_faces,
		     ti.bot_vertices, ti.bot_faces, ti.bot_thickness,
		     ti.bot_face_mode);
    }

    if (ret) {
	bu_log("ERROR: Make BOT failed for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
    }

    free_ti(&ti);

    return ret;
}


/*
 * For a grouping of faces, write a nmg primitive or volume-mode-bot
 * primitive to a BRL-CAD database file (i.e. ".g" file). This
 * function will return a non-zero value if no primitive was output.
 * Only face groupings with closed surfaces will be output. Closure is
 * determined by the function nmg_check_closed_shell. A primitive will
 * not be output if the face grouping fails the closure test or an
 * error occurred during processing. A return value of 1 indicates no
 * primitive was output because the geometry was not closed. A return
 * value of 2 indicates no primitive was output because an nmg bomb
 * occurred. The output_mode option determines if an nmg or
 * volume-mode-bot will be output. The normal_mode will determine if
 * normals provided in the obj file will be inserted into the nmg
 * structure. Normal_mode should be set to IGNR_NORM since, inserted
 * normals are not used for computing face normals and appear to
 * provide no benefit. The tolerance values passed to this function
 * (within bn_tol) must be those which will be used when raytracing
 * (as defined in rtip->rti_tol) otherwise raytracing errors will
 * occur. Primitives will be named according to the grouping name in
 * the obj file with the following exceptions. Characters invalid for
 * BRL-CAD primitive names and operating system file names are
 * converted to underscore. The remaining portions of the name
 * indicate the decimal index number of the grouping in the obj file,
 * face_type number of the grouping {1=v|2=tv|3=nv|4=tnv}, primitive
 * type {n=nmg|v=bot-via-nmg} and closure status {c=closed}.
 *
 * Recommended function improvements:
 * - Profile the called nmg functions to determine where the
 *   performance bottlenecks are occurring.
 * - Provide cleaner status to the user while processing.
 * - Investigate usefulness of inserting obj file face normals into
 *   nmg structure.
 * - If unable to unitize a normal, determine how to deal with this.
 */
int
output_to_nmg(struct ga_t *ga,
	      struct gfi_t *gfi,
	      struct rt_wdb *outfp,
	      fastf_t conv_factor,
	      const struct bn_tol *tol,
	      int normal_mode,      /* PROC_NORM, IGNR_NORM */
	      int output_mode,      /* OUT_NMG, OUT_VBOT */
	      int face_test_type)
{
    struct model *m = (struct model *)NULL;
    struct nmgregion *r = (struct nmgregion *)NULL;
    struct shell *s = (struct shell *)NULL;
    struct bu_ptbl faces;  /* table of faces for one element */
    struct faceuse *fu = (struct faceuse *)NULL;
    struct vertexuse *vu = (struct vertexuse *)NULL;
    struct vertexuse *vu2 = (struct vertexuse *)NULL;
    struct loopuse *lu = (struct loopuse *)NULL;
    struct edgeuse *eu = (struct edgeuse *)NULL;
    struct loopuse *lu2 = (struct loopuse *)NULL;
    struct edgeuse *eu2 = (struct edgeuse *)NULL;
    size_t face_idx = 0;                 /* index into faces within for-loop */
    size_t vert_idx = 0;                 /* index into vertices within for-loop */
    size_t shell_vert_idx = 0;           /* index into vertices for entire nmg shell */
    size_t num_entities_fused = 0;
    int ret;                             /* function return value, default to failure */
    plane_t pl;                          /* plane equation for face */
    fastf_t tmp_v[3] = {0.0, 0.0, 0.0};  /* temporary vertex */
    fastf_t tmp_w = 0.0;                 /* temporary weight */
    fastf_t tmp_rn[3] = {0.0, 0.0, 0.0}; /* temporary reverse normal */
    fastf_t tmp_n[3] = {0.0, 0.0, 0.0};  /* temporary normal */
    fastf_t tmp_t[3] = {0.0, 0.0, 0.0};  /* temporary texture vertex */
    size_t vofi = 0;                     /* vertex obj file index */
    size_t nofi = 0;                     /* normal obj file index */
    size_t tofi = 0;                     /* texture vertex obj file index */

    struct vertex **verts = (struct vertex **)NULL;

    size_t num_faces_killed;             /* number of degenerate faces killed in the current shell */

    m = nmg_mm();
    if (m == NULL)
	return -1;
    r = nmg_mrsv(m);
    s = BU_LIST_FIRST(shell, &r->s_hd);
    NMG_CK_MODEL(m);
    NMG_CK_REGION(r);
    NMG_CK_SHELL(s);

    /* begin bomb protection */
    if (BU_SETJUMP) {
	BU_UNSETJUMP; /* relinquish bomb protection */

	/* Sometimes the NMG library adds debugging bits when it
	 * detects an internal error, before before bombing out.
	 */
	nmg_debug = ga->nmg_debug; /* restore mode */

	bu_free(verts, "verts");

	if (m != (struct model *)NULL) {
	    /* sanity check */
	    nmg_km(m);
	}

	bu_log("ERROR: caught nmg bomb for obj file face grouping name (%s), obj file face grouping index (%zu)\n",
	       bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);

	return 2; /* return code 2 indicates nmg bomb occurred */
    }

    ret = 0;
    num_faces_killed = 0;

    /* initialize tables */
    bu_ptbl_init(&faces, 64, " &faces ");

    verts = (struct vertex **)bu_calloc(gfi->tot_vertices, sizeof(struct vertex *), "verts");
    memset((void *)verts, 0, sizeof(struct vertex *) * gfi->tot_vertices);

    shell_vert_idx = 0;
    NMG_CK_SHELL(s);
    /* loop thru all the polygons (i.e. faces) to be placed in the current shell/region/model */
    for (face_idx = 0 ; face_idx < gfi->num_faces ; face_idx++) {

	/* test for degenerate face */
	if (test_face(ga, gfi, face_idx, conv_factor, tol, face_test_type, 0)) {
	    num_faces_killed++;
	} else {
	    fu = nmg_cface(s, (struct vertex **)&(verts[shell_vert_idx]),
			   (int)(gfi->num_vertices_arr[face_idx]));
	    lu = BU_LIST_FIRST(loopuse, &fu->lu_hd);
	    eu = BU_LIST_FIRST(edgeuse, &lu->down_hd);
	    lu2 = BU_LIST_FIRST(loopuse, &fu->fumate_p->lu_hd);
	    eu2 = BU_LIST_FIRST(edgeuse, &lu2->down_hd);


	    for (vert_idx = 0 ; vert_idx < gfi->num_vertices_arr[face_idx] ; vert_idx++) {
		retrieve_coord_index(ga, gfi, face_idx, vert_idx, tmp_v, tmp_n, tmp_t,
				     &tmp_w, &vofi, &nofi, &tofi);

		VSCALE(tmp_v, tmp_v, conv_factor);

		NMG_CK_VERTEX(eu->vu_p->v_p);
		nmg_vertex_gv(eu->vu_p->v_p, tmp_v);

		if ((gfi->face_type == FACE_NV || gfi->face_type == FACE_TNV) && (normal_mode == PROC_NORM)) {
		    if (MAGNITUDE(tmp_n) < VDIVIDE_TOL) {
			bu_log("ERROR: Unable to unitize normal (%f)(%f)(%f) for obj file face index (%zu), obj file face grouping name (%s), obj file face grouping index (%zu)\n",
			       tmp_n[0], tmp_n[1], tmp_n[2],
			       gfi->obj_file_face_idx_arr[face_idx] + 1,
			       bu_vls_addr(gfi->raw_grouping_name),
			       gfi->grouping_index + 1);
		    } else {
			VUNITIZE(tmp_n);
		    }

		    VREVERSE(tmp_rn, tmp_n);

		    /* assign this normal to all uses of this vertex */
		    for (BU_LIST_FOR(vu, vertexuse, &eu->vu_p->v_p->vu_hd)) {
			NMG_CK_VERTEXUSE(vu);
			nmg_vertexuse_nv(vu, tmp_n);
		    }
		    for (BU_LIST_FOR(vu2, vertexuse, &eu2->vu_p->v_p->vu_hd)) {
			NMG_CK_VERTEXUSE(vu2);
			nmg_vertexuse_nv(vu2, tmp_rn);
		    }
		}

		eu = BU_LIST_NEXT(edgeuse, &eu->l);
		eu2 = BU_LIST_NEXT(edgeuse, &eu2->l);
		shell_vert_idx++;

	    } /* this loop exits when all the vertices and their normals
	       * for the current polygon/faceuse has been inserted into
	       * their appropriate structures
	       */

	    NMG_CK_FACEUSE(fu);
	    if (nmg_loop_plane_area(BU_LIST_FIRST(loopuse, &fu->lu_hd), pl) < 0.0) {
		if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
		    bu_log("WARNING: Failed nmg_loop_plane_area for obj file face index (%zu), obj file face grouping name (%s), obj file face grouping index (%zu)\n",
			   gfi->obj_file_face_idx_arr[face_idx] + 1,
			   bu_vls_addr(gfi->raw_grouping_name),
			   gfi->grouping_index + 1);
		    nmg_pr_fu_briefly(fu, "");
		}
		(void)nmg_kfu(fu);
		fu = (struct faceuse *)NULL;
		num_faces_killed++;
	    } else {
		nmg_face_g(fu, pl);
		nmg_face_bb(fu->f_p, tol);
		bu_ptbl_ins(&faces, (long *)fu);
	    }

	} /* close of degenerate_face if test */

    } /* loop exits when all polygons within the current grouping
       * has been placed within one nmg shell, inside one nmg region
       * and inside one nmg model
       */

    if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
	bu_log("Killed (%zu) faces in obj file face grouping name (%s), obj file face grouping index (%zu)\n", num_faces_killed, bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
    }

    if (!BU_PTBL_LEN(&faces)) {
	if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
	    bu_log("No faces in obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
    } else {
	/* run nmg_model_fuse */
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Running nmg_model_fuse on (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
	num_entities_fused = nmg_model_fuse(m, &RTG.rtg_vlfree, tol);
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Completed nmg_model_fuse for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	    bu_log("Fused (%d) entities in obj file face grouping name (%s), obj file face grouping index (%zu)\n",
		   (int)num_entities_fused, bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}

	/* run nmg_gluefaces, run nmg_vertex_fuse before nmg_gluefaces */
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Running nmg_gluefaces on (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
	nmg_gluefaces((struct faceuse **)BU_PTBL_BASEADDR(&faces), BU_PTBL_LEN(&faces), &RTG.rtg_vlfree, tol);
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Completed nmg_gluefaces for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}

	/* mark edges as real */
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Running nmg_mark_edges_real with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
	(void)nmg_mark_edges_real(&s->l.magic,&RTG.rtg_vlfree);
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Completed nmg_mark_edges_real for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
	/* compute geometry for region and shell */
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Running nmg_region_a with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
	nmg_region_a(r, tol);
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Completed nmg_region_a for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
	if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
	    bu_log("Running nmg_kill_cracks with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	}
	if (nmg_kill_cracks(s)) {
	    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		bu_log("Completed nmg_kill_cracks for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	    }
	    bu_log("Object %s has no faces\n", bu_vls_addr(gfi->raw_grouping_name));
	} else {
	    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		bu_log("Completed nmg_kill_cracks for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	    }
	    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		bu_log("Running nmg_rebound with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	    }
	    nmg_rebound(m, tol);
	    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		bu_log("Completed nmg_rebound for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	    }
	    /* run nmg_vertex_fuse before nmg_check_closed_shell */
	    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		bu_log("Running nmg_check_closed_shell with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
	    }
	    if (!nmg_check_closed_shell(s, tol)) {
		/* true when surface is closed */
		if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		    bu_log("Completed nmg_check_closed_shell for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		}
		if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		    bu_log("Running nmg_fix_normals with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		}
		nmg_fix_normals(s, &RTG.rtg_vlfree, tol);
		if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
		    bu_log("Completed nmg_fix_normals for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		}
		if (output_mode == OUT_NMG) {
		    /* make nmg */
		    /* added 1 to grouping_index so the index number
		     * used in the primitive name matches the obj file
		     * index since internal indexes from libobj start
		     * at 0 but obj file indexes start at 1.
		     */
		    bu_vls_sprintf(gfi->primitive_name, "%s.%zu.%d.n.c.s",
				   bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1, gfi->face_type);
		    cleanup_name(gfi->primitive_name);
		    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
			bu_log("Running mk_nmg with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		    }
		    /* the model (m) is freed when mk_nmg completes */
		    if (mk_nmg(outfp, bu_vls_addr(gfi->primitive_name), m) < 0) {
			bu_log("ERROR: Completed mk_nmg but FAILED for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		    } else {
			if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
			    bu_log("Completed mk_nmg for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
			}
			ret = 0; /* set return success */
		    }
		} else {
		    /* make volume bot */
		    if (output_mode != OUT_VBOT) {
			bu_log("NOTE: Currently bot-via-nmg can only create volume bots, ignoring plate mode option and creating volume bot instead.\n");
		    }
		    /* added 1 to grouping_index so the index number
		     * used in the primitive name matches the obj file
		     * index since internal indexes from libobj start
		     * at 0 but obj file indexes start at 1.
		     */
		    bu_vls_sprintf(gfi->primitive_name, "%s.%zu.%d.v.c.s",
				   bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1, gfi->face_type);
		    cleanup_name(gfi->primitive_name);
		    if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
			bu_log("Running mk_bot_from_nmg with approx (%zu) faces from obj file face grouping name (%s), obj file face grouping index (%zu)\n", BU_PTBL_LEN(&faces), bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		    }
		    if (mk_bot_from_nmg(outfp, bu_vls_addr(gfi->primitive_name), s) < 0) {
			bu_log("ERROR: Completed mk_bot_from_nmg but FAILED for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		    } else {
			if ((ga->gcv_options->verbosity_level > 1) || ga->gcv_options->debug_mode) {
			    bu_log("Completed mk_bot_from_nmg for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
			}
			ret = 0; /* set return success */
			nmg_km(m); /* required for mk_bot_from_nmg but not mk_nmg */
		    }
		}
	    } else {
		if (ga->gcv_options->verbosity_level || ga->gcv_options->debug_mode) {
		    bu_log("WARNING: Function nmg_check_closed_shell found no surface closure for obj file face grouping name (%s), obj file face grouping index (%zu)\n", bu_vls_addr(gfi->raw_grouping_name), gfi->grouping_index + 1);
		}
	    }
	}
    }

    BU_UNSETJUMP; /* relinquish bomb catch protection */

    bu_ptbl_reset(&faces);
    bu_free(verts, "verts");

    if (ret) {
	/* if failure kill model */
	nmg_km(m);
    }

    return ret;
}


/*
 * Validate unit string and output conversion factor to millimeters.
 * If the string is not a standard units identifier, the function
 * assumes a custom conversion factor was specified. A valid null
 * terminated string is expected as input. Return of 0 is success,
 * return of 1 is failure.
 */
int
str2mm(const char *units_string, fastf_t *conv_factor)
{
    struct bu_vls str = BU_VLS_INIT_ZERO;
    fastf_t tmp_value = 0.0;
    char *endp = (char *)NULL;
    int ret = 0;

    if ((units_string == (char *)NULL) || (conv_factor == (fastf_t *)NULL)) {
	bu_log("NULL pointer(s) passed to function 'str2mm'.\n");
	ret = 1;
    } else {
	bu_vls_strcat(&str, units_string);
	bu_vls_trimspace(&str);

	tmp_value = (fastf_t)strtod(bu_vls_addr(&str), &endp);
	if ((endp != bu_vls_addr(&str)) && (*endp == '\0')) {
	    /* convert to double success */
	    *conv_factor = tmp_value;
	} else if ((tmp_value = (fastf_t)bu_mm_value(bu_vls_addr(&str))) > 0.0) {
	    *conv_factor = tmp_value;
	} else {
	    bu_log("Invalid units string '%s'\n", units_string);
	    ret = 1;
	}
    }
    bu_vls_free(&str);
    return ret;
}


/*
 * This function is executed from main when the user selects mode
 * option 'b' from the command line. (i.e. output to "native bot")
 */
void
process_b_mode_option(struct ga_t *ga,
		      struct gfi_t *gfi,
		      struct rt_wdb *outfp,
		      fastf_t conv_factor,
		      const struct bn_tol *tol,
		      int fuse_vertices,
		      fastf_t bot_thickness, /* bot plate thickness (mm) */
		      int normal_mode,       /* PROC_NORM, IGNR_NORM */
		      int plot_mode,         /* PLOT_OFF, PLOT_ON */
		      /* RT_BOT_SURFACE, RT_BOT_PLATE, RT_BOT_PLATE_NOCOS */
		      unsigned char open_bot_output_mode,
		      char bot_orientation,
		      int face_test_type)    /* TEST_ALL, TEST_NUM_VERT */
{
    if (fuse_vertices) {
	(void)fuse_vertex(ga, gfi, conv_factor, tol, FUSE_VERT, FUSE_EQUAL);
    }

    (void)retest_grouping_faces(ga, gfi, conv_factor, face_test_type, tol);
    if (!test_closure(ga, gfi, conv_factor, plot_mode, face_test_type, tol)) {
	(void)output_to_bot(ga, gfi, outfp, conv_factor, tol, bot_thickness,
			    IGNR_TEX, normal_mode, RT_BOT_SOLID,
			    bot_orientation, face_test_type);
    } else {
	(void)output_to_bot(ga, gfi, outfp, conv_factor, tol, bot_thickness,
			    IGNR_TEX, normal_mode, open_bot_output_mode,
			    bot_orientation, face_test_type);
    }
}


/*
 * This function is executed from main when the user selects mode
 * option 'n' or 'v' from the command line. The 'n' indicates output
 * to nmg and 'v' indicates output to volume-mode-bot via nmg. If
 * during nmg or volume-mode-bot creation a 'no closure' condition is
 * encountered, output of the grouping will be attempted to
 * 'native bot'. If during nmg or volume-mode-bot creation an nmg-bomb
 * occurs then no primitive will be output unless cont_on_nmg_bomb is
 * set to true which will then cause output of the grouping to be
 * attempted to 'native bot'. A return value of 0 indicates no
 * nmg-bomb occurred, a return of 1 indicates an nmg-bomb occurred
 * and cont_on_nmg_bomb_flag was set to false.
 */
int
process_nv_mode_option(struct ga_t *ga,
		       struct gfi_t *gfi,
		       struct rt_wdb *outfp,
		       fastf_t conv_factor,
		       const struct bn_tol *tol,
		       fastf_t bot_thickness, /* bot plate thickness (mm) */
		       int nmg_output_mode,   /* OUT_NMG, OUT_VBOT */
		       int plot_mode,         /* PLOT_OFF, PLOT_ON */
		       /* RT_BOT_SURFACE, RT_BOT_PLATE, RT_BOT_PLATE_NOCOS */
		       unsigned char open_bot_output_mode,
		       /* output_to_nmg (nmg or bot-via-nmg) face_test_type
			* TEST_ALL, TEST_NUM_VERT
			*/
		       char bot_orientation,
		       int nmg_face_test_type,
		       /* output_to_bot (i.e. native) face_test_type
			* TEST_ALL, TEST_NUM_VERT
			*/
		       int native_face_test_type,
		       int cont_on_nmg_bomb_flag)
{
    int ret = 0;
    ret = output_to_nmg(ga, gfi, outfp, conv_factor, tol, IGNR_NORM,
			nmg_output_mode, nmg_face_test_type);

    if ((ret == 1) || ((ret == 2) && cont_on_nmg_bomb_flag)) {
	(void)fuse_vertex(ga, gfi, conv_factor, tol, FUSE_VERT, FUSE_WI_TOL);
	(void)retest_grouping_faces(ga, gfi, conv_factor, native_face_test_type,
				    tol);
	if (!test_closure(ga, gfi, conv_factor, plot_mode,
			  native_face_test_type, tol))
	{
	    (void)output_to_bot(ga, gfi, outfp, conv_factor, tol, bot_thickness,
				IGNR_TEX, IGNR_NORM, RT_BOT_SOLID, bot_orientation,
				native_face_test_type);
	} else {
	    (void)output_to_bot(ga, gfi, outfp, conv_factor, tol, bot_thickness,
				IGNR_TEX, IGNR_NORM, open_bot_output_mode,
				bot_orientation,
				native_face_test_type);
	}
    }

    /* convert output_to_nmg return code to process_nv_mode_option
     * return code
     */
    if ((ret == 2) && !cont_on_nmg_bomb_flag) {
	ret = 1;
    } else {
	ret = 0;
    }

    return ret;
}


struct obj_read_options
{
    int cont_on_nmg_bomb_flag;
    int fuse_vertices;
    char grouping_option;
    char mode_option;
    int nmg_output_mode;
    fastf_t bot_thickness;
    int user_bot_thickness_flag;
    int normal_mode;
    int open_bot_output_mode;
    int plot_mode;
    char bot_orientation;
};


static int
parse_grouping_option(struct bu_vls *error_msg, size_t argc, const char **argv, void *set_var)
{
    char * const value = (char *)set_var;

    BU_OPT_CHECK_ARGV0(error_msg, argc, argv, "parse_grouping_option");

    if (!value) return -1;

    if (!bu_strcmp(argv[0], "group"))
	*value = 'g';
    else if (!bu_strcmp(argv[0], "material"))
	*value = 'm';
    else if (!bu_strcmp(argv[0], "none"))
	*value = 'n';
    else if (!bu_strcmp(argv[0], "object"))
	*value = 'o';
    else if (!bu_strcmp(argv[0], "texture"))
	*value = 't';
    else {
	bu_vls_printf(error_msg, "invalid grouping mode\n");
	return -1;
    }

    return 1;
}


static int
parse_mode_option(struct bu_vls *error_msg, size_t argc, const char **argv, void *set_var)
{
    struct obj_read_options * const options = (struct obj_read_options *)set_var;

    BU_OPT_CHECK_ARGV0(error_msg, argc, argv, "parse_mode_option");

    if (!options) return -1;

    if (!bu_strcmp(argv[0], "bot")) {
	options->mode_option = 'b';
    } else if (!bu_strcmp(argv[0], "nmg")) {
	options->nmg_output_mode = OUT_NMG;
	options->mode_option = 'n';
    } else if (!bu_strcmp(argv[0], "nmgbot")) {
	options->nmg_output_mode = OUT_VBOT;
	options->mode_option = 'v';
    } else {
	bu_vls_printf(error_msg, "invalid conversion mode\n");
	return -1;
    }

    return 1;
}


static int
parse_bot_thickness_option(struct bu_vls *error_msg, size_t argc, const char **argv, void *set_var)
{
    struct obj_read_options * const options = (struct obj_read_options *)set_var;

    BU_OPT_CHECK_ARGV0(error_msg, argc, argv, "parse_bot_thickness_option");

    if (!options) return -1;

    if (bu_opt_fastf_t(error_msg, argc, argv, &options->bot_thickness) != 1)
	return -1;

    if (options->bot_thickness <= 0.0) {
	bu_vls_printf(error_msg, "invalid bot plate thickness\n");
	return -1;
    }

    options->user_bot_thickness_flag = 1;

    return 1;
}


static int
parse_normal_mode_option(struct bu_vls *UNUSED(error_msg), size_t UNUSED(argc), const char **UNUSED(argv), void *set_var)
{
    int * const value = (int *)set_var;

    if (value) {
	*value = IGNR_NORM;
    }
    return 0;
}


static int
parse_open_bot_output_mode_option(struct bu_vls *error_msg, size_t argc, const char **argv, void *set_var)
{
    int * const value = (int *)set_var;

    BU_OPT_CHECK_ARGV0(error_msg, argc, argv, "parse_open_bot_output_mode_option");

    if (!value ) return -1;

    if (!bu_strcmp(argv[0], "nocos"))
	*value = RT_BOT_PLATE_NOCOS;
    else if (!bu_strcmp(argv[0], "plate"))
	*value = RT_BOT_PLATE;
    else if (!bu_strcmp(argv[0], "surface"))
	*value = RT_BOT_SURFACE;
    else {
	bu_vls_printf(error_msg, "invalid BoT open type\n");
	return -1;
    }

    return 1;
}


static int
parse_plot_mode_option(struct bu_vls *UNUSED(error_msg), size_t UNUSED(argc), const char **UNUSED(argv), void *set_var)
{
    int * const value = (int *)set_var;

    if (value) {
	*value = PLOT_ON;
    }
    return 0;
}


static int
parse_bot_orientation_option(struct bu_vls *error_msg, size_t argc, const char **argv, void *set_var)
{
    int have_orientation = 0;
    int * const value = (int *)set_var;

    BU_OPT_CHECK_ARGV0(error_msg, argc, argv, "parse_bot_orientation_option");

    if (!bu_strcmp(argv[0], "unoriented")) {
	*value = RT_BOT_UNORIENTED;
	have_orientation = 1;
    }
    if (!bu_strcmp(argv[0], "ccw")) {
	*value = RT_BOT_CCW;
	have_orientation = 1;
    }
    if (!bu_strcmp(argv[0], "cw")) {
	*value = RT_BOT_CW;
	have_orientation = 1;
    }
    if (!have_orientation) {
	bu_vls_printf(error_msg, "invalid BoT orientation mode\n");
	return -1;
    }

    return 1;
}


static void
obj_read_create_opts(struct bu_opt_desc **options_desc, void **dest_options_data)
{
    struct obj_read_options *options_data;

    BU_ALLOC(options_data, struct obj_read_options);
    *dest_options_data = options_data;
    *options_desc = (struct bu_opt_desc *)bu_malloc(10 * sizeof(struct bu_opt_desc), "options_desc");

    options_data->cont_on_nmg_bomb_flag = 0;
    options_data->fuse_vertices = 0;
    options_data->grouping_option = 'g';
    options_data->mode_option = 'b';
    options_data->nmg_output_mode = OUT_VBOT;
    options_data->bot_thickness = 0.0;
    options_data->user_bot_thickness_flag = 0;
    options_data->normal_mode = PROC_NORM;
    options_data->open_bot_output_mode = RT_BOT_SURFACE;
    options_data->plot_mode = PLOT_OFF;
    options_data->bot_orientation = RT_BOT_UNORIENTED;

    BU_OPT((*options_desc)[0], NULL, "continue", NULL,
	    NULL, &options_data->cont_on_nmg_bomb_flag,
	    "continue processing on nmg-bomb; will fall back to native BoT mode if a bu_bomb occurs when using the nmg or BoT via nmg modes");

    BU_OPT((*options_desc)[1], NULL, "fuse-vertices", NULL,
	    NULL, &options_data->fuse_vertices,
	    "fuse vertices that are near enough to be considered identical");

    BU_OPT((*options_desc)[2], NULL, "grouping", "mode",
	    parse_grouping_option, &options_data->grouping_option,
	    "select OBJ face grouping used to create primitives");

    BU_OPT((*options_desc)[3], NULL, "conversion-mode", "mode",
	    parse_mode_option, options_data,
	    "select conversion mode");

    BU_OPT((*options_desc)[4], NULL, "bot-plate-thickness", "thickness",
	    parse_bot_thickness_option, options_data,
	    "select conversion mode");

    BU_OPT((*options_desc)[5], NULL, "bot-ignore-normals", NULL,
	    parse_normal_mode_option, &options_data->normal_mode,
	    "ignore normals defined in the input file when using native BoT conversion mode");

    BU_OPT((*options_desc)[6], NULL, "bot-open-type", "type",
	    parse_open_bot_output_mode_option, &options_data->open_bot_output_mode,
	    "select type used for BoTs that aren't closed volumes");

    BU_OPT((*options_desc)[7], NULL, "bot-plot", NULL,
	    parse_plot_mode_option, &options_data->plot_mode,
	    "create a .plot3 file of open edges for BoTs that aren't closed volumes");

    BU_OPT((*options_desc)[8], NULL, "bot-orientation", "mode",
	    parse_bot_orientation_option, &options_data->bot_orientation,
	    "select BoT orientation mode");

    BU_OPT_NULL((*options_desc)[9]);
}


static void
obj_read_free_opts(void *options_data)
{
    bu_free(options_data, "options_data");
}


static void
do_grouping(struct rt_wdb *wdbp, const struct gcv_opts *gcv_options, const struct obj_read_options *obj_read_options, struct ga_t *ga)
{
    const int native_face_test_type = TEST_ALL;
    const int nmg_face_test_type = TEST_NUM_VERT;
    int face_type_idx = 0;
    int stop_processing_flag = 0;
    size_t i = 0;
    struct gfi_t *gfi = NULL;

    switch (obj_read_options->grouping_option) {
	case 'n':
	    face_type_idx = FACE_V;
	    while ((face_type_idx <= FACE_TNV) && !stop_processing_flag) {
		collect_grouping_faces_indexes(ga, &gfi, face_type_idx,
			GRP_NONE, 0);

		if (gfi != NULL) {
		    switch (obj_read_options->mode_option) {
			case 'b':
			    process_b_mode_option(ga, gfi, wdbp, gcv_options->scale_factor,
				    &gcv_options->calculational_tolerance, obj_read_options->fuse_vertices, obj_read_options->bot_thickness, obj_read_options->normal_mode, obj_read_options->plot_mode,
				    obj_read_options->open_bot_output_mode, obj_read_options->bot_orientation,
				    native_face_test_type);
			    break;
			case 'n':
			case 'v':
			    stop_processing_flag = process_nv_mode_option(ga,
				    gfi, wdbp, gcv_options->scale_factor, &gcv_options->calculational_tolerance,
				    obj_read_options->bot_thickness, obj_read_options->nmg_output_mode, obj_read_options->plot_mode,
				    obj_read_options->open_bot_output_mode, obj_read_options->bot_orientation,
				    nmg_face_test_type,
				    native_face_test_type,
				    obj_read_options->cont_on_nmg_bomb_flag);
			    break;
		    }

		    free_gfi(&gfi);
		}
		face_type_idx++;
	    }
	    break;
	case 'g':
	    i = 0;
	    while ((i < ga->numGroups) && !stop_processing_flag) {
		face_type_idx = FACE_V;
		while ((face_type_idx <= FACE_TNV) && !stop_processing_flag) {
		    collect_grouping_faces_indexes(ga, &gfi, face_type_idx,
			    GRP_GROUP, i);
		    if (gfi != NULL) {
			switch (obj_read_options->mode_option) {
			    case 'b':
				process_b_mode_option(ga, gfi, wdbp, gcv_options->scale_factor, &gcv_options->calculational_tolerance, obj_read_options->fuse_vertices, obj_read_options->bot_thickness,
					obj_read_options->normal_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode, obj_read_options->bot_orientation,
					native_face_test_type);
				break;
			    case 'n':
			    case 'v':
				stop_processing_flag = process_nv_mode_option(ga, gfi, wdbp, gcv_options->scale_factor,
					&gcv_options->calculational_tolerance, obj_read_options->bot_thickness, obj_read_options->nmg_output_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode,
					obj_read_options->bot_orientation, nmg_face_test_type, native_face_test_type,
					obj_read_options->cont_on_nmg_bomb_flag);
				break;
			}

			free_gfi(&gfi);
		    }
		    face_type_idx++;
		}
		i++;
	    }
	    break;
	case 'o':
	    i = 0;
	    while ((i < ga->numObjects) && !stop_processing_flag) {
		face_type_idx = FACE_V;
		while ((face_type_idx <= FACE_TNV) && !stop_processing_flag) {
		    collect_grouping_faces_indexes(ga, &gfi, face_type_idx, GRP_OBJECT, i);
		    if (gfi != NULL) {
			switch (obj_read_options->mode_option) {
			    case 'b':
				process_b_mode_option(ga, gfi, wdbp, gcv_options->scale_factor, &gcv_options->calculational_tolerance, obj_read_options->fuse_vertices, obj_read_options->bot_thickness,
					obj_read_options->normal_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode, obj_read_options->bot_orientation,
					native_face_test_type);
				break;
			    case 'n':
			    case 'v':
				stop_processing_flag = process_nv_mode_option(ga, gfi, wdbp, gcv_options->scale_factor,
					&gcv_options->calculational_tolerance, obj_read_options->bot_thickness, obj_read_options->nmg_output_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode,
					obj_read_options->bot_orientation, nmg_face_test_type, native_face_test_type,
					obj_read_options->cont_on_nmg_bomb_flag);
				break;
			}

			free_gfi(&gfi);
		    }
		    face_type_idx++;
		}
		i++;
	    }
	    break;
	case 'm':
	    i = 0;
	    while ((i < ga->numMaterials) && !stop_processing_flag) {
		face_type_idx = FACE_V;
		while ((face_type_idx <= FACE_TNV) && !stop_processing_flag) {
		    collect_grouping_faces_indexes(ga, &gfi, face_type_idx, GRP_MATERIAL, i);
		    if (gfi != NULL) {
			switch (obj_read_options->mode_option) {
			    case 'b':
				process_b_mode_option(ga, gfi, wdbp, gcv_options->scale_factor, &gcv_options->calculational_tolerance, obj_read_options->fuse_vertices, obj_read_options->bot_thickness,
					obj_read_options->normal_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode, obj_read_options->bot_orientation,
					native_face_test_type);
				break;
			    case 'n':
			    case 'v':
				stop_processing_flag = process_nv_mode_option(ga, gfi, wdbp, gcv_options->scale_factor,
					&gcv_options->calculational_tolerance, obj_read_options->bot_thickness, obj_read_options->nmg_output_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode,
					obj_read_options->bot_orientation,
					nmg_face_test_type, native_face_test_type, obj_read_options->cont_on_nmg_bomb_flag);
				break;
			}

			free_gfi(&gfi);
		    }
		    face_type_idx++;
		}
		i++;
	    }
	    break;
	case 't':
	    i = 0;
	    while ((i < ga->numTexmaps) && !stop_processing_flag) {
		face_type_idx = FACE_V;
		while ((face_type_idx <= FACE_TNV) && !stop_processing_flag) {
		    if (gfi != NULL) {
			switch (obj_read_options->mode_option) {
			    case 'b':
				process_b_mode_option(ga, gfi, wdbp, gcv_options->scale_factor, &gcv_options->calculational_tolerance, obj_read_options->fuse_vertices, obj_read_options->bot_thickness,
					obj_read_options->normal_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode, obj_read_options->bot_orientation,
					native_face_test_type);
				break;
			    case 'n':
			    case 'v':
				stop_processing_flag = process_nv_mode_option(ga, gfi, wdbp, gcv_options->scale_factor,
					&gcv_options->calculational_tolerance, obj_read_options->bot_thickness, obj_read_options->nmg_output_mode, obj_read_options->plot_mode, obj_read_options->open_bot_output_mode,
					obj_read_options->bot_orientation,
					nmg_face_test_type, native_face_test_type, obj_read_options->cont_on_nmg_bomb_flag);
				break;
			}

			free_gfi(&gfi);
		    }
		    face_type_idx++;
		}
		i++;
	    }
	    break;
    }
}


static int
obj_read(struct gcv_context *context, const struct gcv_opts *gcv_options, const void *options_data, const char *source_path)
{
    const struct obj_read_options * const obj_read_options = (struct obj_read_options *)options_data;
    struct ga_t ga;

    if (obj_read_options->open_bot_output_mode == RT_BOT_PLATE || obj_read_options->open_bot_output_mode == RT_BOT_PLATE_NOCOS) {
	if (!obj_read_options->user_bot_thickness_flag) {
	    bu_log("BoT plate thickness was not specified but is required\n");
	    return 0;
	}
    } else if (obj_read_options->user_bot_thickness_flag) {
	bu_log("BoT plate thickness was specified but does not apply to the BoT open type\n");
	return 0;
    }

    memset(&ga, 0, sizeof(ga));

    ga.gcv_options = gcv_options;
    ga.nmg_debug = nmg_debug;

    if (obj_parser_create(&ga.parser)) {
	bu_log("Cannot initialize an obj_parser_t object\n");
	obj_parser_destroy(ga.parser);
	return 0;
    }

    {
	FILE *my_stream;
	int parse_err;

	if (!(my_stream = fopen(source_path, "rb"))) {
	    perror("libgcv");
	    bu_log("Cannot open input file (%s)\n", source_path);
	    obj_parser_destroy(ga.parser);
	    return 0;
	}

	if ((parse_err = obj_fparse(my_stream, ga.parser, &ga.contents)) != 0) {
	    if (parse_err < 0) {
		bu_log("obj_fparse() failed: syntax error\n");
		bu_log("%s\n", obj_parse_error(ga.parser));
	    } else if (parse_err == ENOMEM)
		bu_log("obj_fparse() failed: out of memory");
	    else
		bu_log("obj_fparse() failed: other error\n");

	    obj_contents_destroy(ga.contents);
	    obj_parser_destroy(ga.parser);

	    if (fclose(my_stream))
		bu_log("unable to close file\n");

	    return 0;
	}

	if (fclose(my_stream)) {
	    bu_log("unable to close file\n");
	    return 0;
	}

	collect_global_obj_file_attributes(&ga);
    }

    struct rt_wdb *wdbp = wdb_dbopen(context->dbip, RT_WDB_TYPE_DB_INMEM);
    do_grouping(wdbp, gcv_options, obj_read_options, &ga);

    /* cleanup */

    bu_free_args(ga.numGroups, (char **)ga.str_arr_obj_groups, "str_arr_obj_groups");
    bu_free_args(ga.numObjects, (char **)ga.str_arr_obj_objects, "str_arr_obj_objects");
    bu_free_args(ga.numMaterials, (char **)ga.str_arr_obj_materials, "str_arr_obj_materials");
    bu_free_args(ga.numTexmaps, (char **)ga.str_arr_obj_texmaps, "str_arr_obj_texmaps");

    obj_contents_destroy(ga.contents);
    obj_parser_destroy(ga.parser);

    rt_clean_resource(NULL, &rt_uniresource);

    return 1;
}

static int
obj_can_read(const char *source_path)
{
    FILE *fp;
    int ret;
    obj_contents_t contents; /* obj_fparse */
    obj_parser_t parser;     /* obj_parser_create */

    if (obj_parser_create(&parser)) {
	obj_parser_destroy(parser);
	return 0;
    }
    if (!(fp = fopen(source_path, "rb"))) {
	obj_parser_destroy(parser);
	return 0;
    }

    ret = !obj_fparse(fp, parser, &contents);
    obj_contents_destroy(contents);
    obj_parser_destroy(parser);
    fclose(fp);
    return ret;
}



static const struct gcv_filter gcv_conv_obj_read = {
    "OBJ Reader", GCV_FILTER_READ, BU_MIME_MODEL_OBJ, obj_can_read,
    obj_read_create_opts, obj_read_free_opts, obj_read
};


extern const struct gcv_filter gcv_conv_obj_write;
static const struct gcv_filter * const filters[] = {&gcv_conv_obj_read, &gcv_conv_obj_write, NULL};

const struct gcv_plugin gcv_plugin_info_s = { filters };

COMPILER_DLLEXPORT const struct gcv_plugin *
gcv_plugin_info(void){ return &gcv_plugin_info_s; }

/*
 * Local Variables:
 * mode: C
 * tab-width: 8
 * indent-tabs-mode: t
 * c-file-style: "stroustrup"
 * End:
 * ex: shiftwidth=4 tabstop=8
 */
