/* * main.c -- ez3d: grayscale PNG -> printable STL heightmap. * * ez3d [options] input.png output.stl */ #include #include #include #include #include #include "ez3d.h" #include "decimate.h" #include "image.h" #include "mesh.h" #include "stl_write.h" #include "trimesh.h" #include "verify.h" #define EZ3D_VERSION "1.0" /* --decimate levels, indexed level-1: quadric error budgets in mm. */ static const double DECIMATE_MM[5] = { 0.05, 0.1, 0.2, 0.4, 0.8 }; static void usage(FILE *out) { fputs( "Usage: ez3d [options] input.png output.stl\n" "\n" "Turn a grayscale PNG into a watertight STL heightmap for 3D printing.\n" "1 pixel = 1 unit (mm); gray 0 is the lowest point, 255 the highest.\n" "\n" "Options:\n" " -H, --height F height of white/gray-255 in mm (default 10)\n" " negative height inverts shape, see note below.\n" " -b, --base F solid base thickness under gray 0 in mm (default 0)\n" " negative base is allowed, see note below.\n" " -e, --edge F height assigned to edge-of-world cells (default -Inf)\n" " also assigned to transparent cells, use small negative\n" " for softer cuts. Positive is unlikely to be useful.\n" " -a, --alpha-thresh N alpha >= N is solid, transparent pixels are cut\n" " away entirely (0-255, default 128; 256 = keep all)\n" " -f, --alt-fold Flag to change folding preference in mesh\n" " -s, --simplify N flat-patch simplification,\n" " 0=disable, 1=minimal .. up to 5=aggressive\n" " -d, --decimate N quadric-error mesh simplification,\n" " 0=disable, 1=finest .. up to 5=coarsest\n" " --ascii write ASCII STL instead of binary (debugging)\n" " --verify re-read the STL and check it is manifold\n" " -h, --help show this help\n" " -V, --version show version\n" "\n" " NOTE REGARDING NEGATIVE HEIGHT: Any pixel with final height below zero\n" " will be cut away. Similarly, if an alpha channel is available then\n" " transparent pixels get cut. Set height and base with suitable values\n" " to ensure the correct group of pixels are delivered.\n", out); } static double parse_double(const char *opt, const char *arg) { char *end; double v = strtod(arg, &end); if (end == arg || *end != '\0') { fprintf(stderr, "ez3d: %s expects a number, got '%s'\n", opt, arg); exit(2); } return v; } static int parse_int(const char *opt, const char *arg) { char *end; long v = strtol(arg, &end, 10); if (end == arg || *end != '\0') { fprintf(stderr, "ez3d: %s expects an integer, got '%s'\n", opt, arg); exit(2); } return (int)v; } static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; } void print_summary(ez3d_config_t *cfg, long facets, long bytes, double timer) { printf("ez3d: wrote %s: %ld facets, %.1f MB, %.0f ms\n", cfg->out_path, facets, bytes / (1024.0 * 1024.0), timer); } /* * Skip the decimation buffering and just pump it out directly. * This is faster to process and involves less memory, * but it will slow the slicer during loading of the STL. * * return 1 for error, return 0 if it all worked. */ int mesh_direct_write(ez3d_image_t *img, ez3d_config_t *cfg) { stl_writer_t w; long facets, bytes; double t0, t1; t0 = now_ms(); if (stl_open(&w, cfg->out_path, cfg->ascii) != 0) { return 1; } facets = ez3d_build_mesh(img, cfg, &w, NULL); bytes = ftell(w.f); if (facets < 0 || stl_close(&w) < 0) { fprintf(stderr, "ez3d: writing '%s' failed\n", cfg->out_path); return 1; } t1 = now_ms(); print_summary(cfg, facets, bytes, t1 - t0); return 0; } /* * Mesh into a buffer (large memory) and then feed through decimation stage. * Deliver the buffered results to the writer as a second step. */ int mesh_and_decimate(ez3d_image_t *img, ez3d_config_t *cfg) { stl_writer_t w; trimesh_t buf; long before; long facets, bytes; double t0, t1; t0 = now_ms(); tm_init(&buf); facets = ez3d_build_mesh(img, cfg, NULL, &buf); if (facets < 0) { tm_free(&buf); return 1; } before = facets; facets = (long)ez3d_decimate(&buf, DECIMATE_MM[cfg->decimate - 1]); printf("ez3d: decimated: %ld -> %ld triangles (level %d, %.3g mm " "budget)\n", before, facets, cfg->decimate, DECIMATE_MM[cfg->decimate - 1]); if (stl_open(&w, cfg->out_path, cfg->ascii) != 0) { tm_free(&buf); return 1; } for (size_t i = 0; i < buf.ntri; i++) { triangle_t *t = &buf.tri[i]; stl_tri(&w, &buf.v[t->v[0]], &buf.v[t->v[1]], &buf.v[t->v[2]]); } bytes = ftell(w.f); if (stl_close(&w) < 0) { fprintf(stderr, "ez3d: writing '%s' failed\n", cfg->out_path); tm_free(&buf); return 1; } tm_free(&buf); t1 = now_ms(); print_summary(cfg, facets, bytes, t1 - t0); return 0; } int main(int argc, char **argv) { ez3d_config_t cfg; ez3d_image_t img; int rc = 0; cfg.in_path = cfg.out_path = NULL; cfg.height = 10.0; cfg.base = 0.0; cfg.edge = -INFINITY; cfg.fold = 0; cfg.alpha_thresh = 128; cfg.decimate = 0; cfg.ascii = 0; cfg.verify = 0; cfg.simplify = 0; cfg.test_dump_cells = 0; cfg.test_dump_groups = 0; static const struct option lopts[] = { { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { "height", required_argument, NULL, 'H' }, { "simplify", required_argument, NULL, 's' }, { "base", required_argument, NULL, 'b' }, { "edge", required_argument, NULL, 'e' }, { "fold", required_argument, NULL, 'f' }, { "alpha-thresh", required_argument, NULL, 'a' }, { "decimate", required_argument, NULL, 'd' }, { "ascii", no_argument, NULL, 'A' }, { "verify", no_argument, NULL, 'r' }, // Test options, not for production use. { "test-dump-cells", no_argument, NULL, '1' }, { "test-dump-groups", no_argument, NULL, '2' }, { NULL, 0, NULL, 0 }, }; int c; while ((c = getopt_long(argc, argv, "hVH:b:a:d:t:e:f", lopts, NULL)) != -1) { switch (c) { // Real options. case 'h': usage(stdout); return 0; case 'V': printf("ez3d %s\n", EZ3D_VERSION); return 0; case 'H': cfg.height = parse_double("--height", optarg); break; case 'b': cfg.base = parse_double("--base", optarg); break; case 'e': cfg.edge = parse_double("--edge", optarg); break; case 'f': cfg.fold = 1; break; case 'a': cfg.alpha_thresh = parse_int("--alpha-thresh", optarg); break; case 's': cfg.simplify = parse_int("--simplify", optarg); break; case 'd': cfg.decimate = parse_int("--decimate", optarg); break; case 'A': cfg.ascii = 1; break; case 'r': cfg.verify = 1; break; // Test options case '1': cfg.test_dump_cells = 1; break; case '2': cfg.test_dump_groups = 1; break; // Errors default: usage(stderr); return 2; } } if (argc - optind != 2) { usage(stderr); return 2; } cfg.in_path = argv[optind]; cfg.out_path = argv[optind + 1]; if (cfg.alpha_thresh < 0 || cfg.alpha_thresh > 256) { fprintf(stderr, "ez3d: --alpha-thresh must be 0..256\n"); return 2; } if (cfg.decimate < 0 || cfg.decimate > 5) { fprintf(stderr, "ez3d: --decimate must be 0 (off), 1 (finest) .. 5 " "(coarsest)\n"); return 2; } if (cfg.simplify < 0 || cfg.simplify > 5) { fprintf(stderr, "ez3d: --simplify must be 0 (off), 1 (minimal) .. 5 " "(aggressive)\n"); return 2; } switch(cfg.simplify) { case 0: cfg.tolerance = 1e-6; break; case 1: cfg.tolerance = cfg.height / 100.0; break; case 2: cfg.tolerance = cfg.height / 50.0; break; case 3: cfg.tolerance = cfg.height / 30.0; break; case 4: cfg.tolerance = cfg.height / 20.0; break; case 5: cfg.tolerance = cfg.height / 10.0; break; } if (ez3d_load_image(cfg.in_path, &img) != 0) return 1; printf("ez3d: %s: %dx%d pixels, %s\n", cfg.in_path, img.w, img.h, img.has_alpha ? "gray+alpha" : "no alpha channel"); if (img.has_alpha && cfg.alpha_thresh < 256) { long transparent = 0; for (size_t i = 0; i < (size_t)img.w * img.h; i++) if (img.alpha[i] < cfg.alpha_thresh) transparent++; printf("ez3d: cutting %ld of %ld pixels (alpha < %d)\n", transparent, (long)img.w * img.h, cfg.alpha_thresh); } if (!img.has_alpha && cfg.alpha_thresh == 0) printf("ez3d: note: image has no alpha channel, --alpha-thresh has " "no effect\n"); int mesh_error; if (cfg.decimate > 0) { /* Build indexed, collapse, then write the simplified mesh. */ mesh_error = mesh_and_decimate(&img, &cfg); } else { /* Unbuffered: skip the decimation and write directly. */ mesh_error = mesh_direct_write(&img, &cfg); } ez3d_free_image(&img); // We are finished with the image. if (mesh_error != 0) { return mesh_error; } if (cfg.verify) { double tol = cfg.simplify > 0.0 ? cfg.simplify : 1e-3; if (cfg.ascii) { fprintf(stderr, "ez3d: --verify supports binary STL only, " "skipping\n"); } else if (ez3d_verify_stl(cfg.out_path, tol) != 0) { rc = 1; } } return rc; }