diff --git a/isosurface.scad b/isosurface.scad index 081805fe..78ddd81c 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -1232,7 +1232,7 @@ function _contour_vertices(pxlist, pxsize, isovalmin, isovalmax, segtablemin, se else if(f1<=isovalmin && isovalmin<=f0 && f0<=isovalmax) [p0, midptmin] else if(f0isovalmax) [midptmin, midptmax] else if(f0>isovalmax && f1=f1) || (f1=f0)) + else if((f0<=f1 && isovalmin<=f0 && isovalmax>=f1) || (f1<=f0 && isovalmin<=f1 && isovalmax>=f0)) [p0, p1] ] ]; diff --git a/joiners.scad b/joiners.scad index 39ef69e5..900a1c08 100644 --- a/joiners.scad +++ b/joiners.scad @@ -687,7 +687,7 @@ module dovetail(gender, width, height, slide, h, w, angle, slope, thickness, tap type = is_def(chamfer) && chamfer>0 ? "chamfer" : "circle"; - smallend_half = round_corners( + bigend_half = round_corners( move( [0,-slide/2-extra,0], p=[ @@ -700,13 +700,13 @@ module dovetail(gender, width, height, slide, h, w, angle, slope, thickness, tap method=type, cut = fullsize, closed=false ); - smallend_points = concat(select(smallend_half, 1, -2), [down(extra,p=select(smallend_half, -2))]); + bigend_points = concat(select(bigend_half, 1, -2), [down(extra,p=select(bigend_half, -2))]); offset = is_def(taper) ? -slide * tan(taper) : is_def(back_width) ? (back_width-width) / 2 : 0; - bigend_points = move([offset+2*extra_offset,slide+2*extra,0], p=smallend_points); + smallend_points = move([offset+2*extra_offset,slide+2*extra,0], p=bigend_points); - bigenough = all_nonnegative(column(smallend_half,0)) && all_nonnegative(column(bigend_points,0)); + bigenough = all_nonnegative(column(bigend_half,0)) && all_nonnegative(column(smallend_points,0)); assert(bigenough, "Width (or back_width) of dovetail is not large enough for its geometry (angle and taper"); @@ -715,7 +715,7 @@ module dovetail(gender, width, height, slide, h, w, angle, slope, thickness, tap // This code computes the true normal from which the exact width factor can be obtained // as the x component. Comparing to wfactor above shows that they agree. - // pts = [smallend_points[0], smallend_points[1], bigend_points[1],bigend_points[0]]; + // pts = [bigend_points[0], bigend_points[1], smallend_points[1],smallend_points[0]]; // n = -polygon_normal(pts); // echo(n=n); // echo(invwfactor = 1/wfactor, error = n.x-1/wfactor); @@ -726,8 +726,8 @@ module dovetail(gender, width, height, slide, h, w, angle, slope, thickness, tap skin( [ - reverse(concat(smallend_points, xflip(p=reverse(smallend_points)))), - reverse(concat(bigend_points, xflip(p=reverse(bigend_points)))) + reverse(concat(bigend_points, xflip(p=reverse(bigend_points)))), + reverse(concat(smallend_points, xflip(p=reverse(smallend_points)))) ], slices=0, convexity=4 ); diff --git a/scripts/3d2scad.py b/scripts/3d2scad.py index bd1b31d5..c8993872 100644 --- a/scripts/3d2scad.py +++ b/scripts/3d2scad.py @@ -1,18 +1,33 @@ # 3d2scad.py - convert STL or 3MF to OpenSCAD polyhedron arrays. # # This utility does these things (in this order): -# - removes invalid triangles -# - optionally simplifies mesh (reduces polygon count) using quadric decimation -# - quantizes coordinates to nearest 0.001 (or whatever you specify) for more compact output -# - removes zero-area triangles -# - removes duplicate vertices for significant size reduction (often a STL vertex is repeated six times) -# - removes shared edges from coplanar polygons +# - creates list of vertices and faces as the mesh is loaded +# - separates object into shells if multiple objects are detected +# - removes invalid triangles +# - optionally simplifies mesh (reduces polygon count) using quadric decimation (a robust method of simplification) +# - attempts repairs if a shell is detected as non-watertight (fill holes, remove unreferenced vertices, fix inversion and winding order, remove duplicate faces) +# - ensure normals are consistently pointing outward +# - quantizes coordinates to nearest 0.001 (or whatever you specify) for more compact output +# - removes zero-area triangles +# - removes duplicate vertices for significant size reduction (often a STL vertex is repeated six times) +# - another pass of removing unreferenced vertices +# - removes shared edges from coplanar polygons +# - outputs a text file with a raw list of polyhedron structures (NOT an .scad file); see below for usage. # # In some cases, the operations above can result in non-manifold shapes, such as when two objects # share an edge, the resulting edge may be shared by more than two faces. # # June 2025 +# TO USE IN OPENSCAD WITH BOLS2 LIBRARY: +# See VNF documentation at https://github.com/BelfrySCAD/BOSL2/wiki/vnf.scad +# If your output file is "model.txt" then use it this way: +# +# include +# vnf_list = include ; // end with semicolon +# // vnf_list now contains a list of VNF (OpenSCAD polyhedron) structures +# vnf_polyhedron(vnf_list); + import sys REQUIRED = ["numpy", "scipy", "trimesh", "open3d", "networkx", "lxml"] # required libraries not typically included in Python MISSING = [] @@ -216,20 +231,25 @@ def format_number(n, precision): s = "0" return s -def export_openscad_structure(vertices, polygons, name, shell_index, precision, f): - varname = f"{name}{shell_index}" - f.write(f"{varname}=[\n[") +def export_openscad_structure(vertices, polygons, nshells, shell_index, precision, f): + if shell_index == 0: + f.write("[ ") + f.write("\n[[") f.write(",".join("[" + ",".join(format_number(c, precision) for c in v) + "]" for v in vertices)) f.write("],\n[") f.write(",".join("[" + ",".join(str(i) for i in poly) + "]" for poly in polygons)) - f.write("]];\n") + f.write("]]") + if shell_index < nshells-1: + f.write(",\n") + else: + f.write(f"\n// shells: {nshells}\n]\n") print(f" Wrote shell {shell_index+1} with {len(vertices)} vertices and {len(polygons)} faces", flush=True) def main(): parser = argparse.ArgumentParser(description="3D model to OpenSCAD polyhedron converter", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("input", help="Input STL or 3MF file") - parser.add_argument("output", help="Output OpenSCAD file") - parser.add_argument("--tolerance", type=float, metavar="FRAC", default=0.0, + parser.add_argument("output", help="Output data file (list of VNF structures)") + parser.add_argument("--polycount", type=float, metavar="FRAC", default=0.0, help="Fraction of faces to remove via quadric decimation (0-0.9)") parser.add_argument("--quantize", type=float, metavar="GRIDUNIT", default=0.001, help="Grid size to quantize vertices") @@ -244,6 +264,7 @@ def main(): mesh = load_mesh(args.input) shells = split_into_shells(mesh) + nshells = len(shells) if args.merge_shells: merged = [] @@ -274,8 +295,8 @@ def main(): for i, shell in enumerate(shells): print(f"Processing shell {i + 1}:", flush=True) shell = remove_invalid_triangles(shell) - if args.tolerance > 0: - shell = decimate_mesh(shell, args.tolerance) + if args.polycount > 0: + shell = decimate_mesh(shell, args.polycount) if not shell.is_watertight: print(" Mesh is not watertight after simplification; attempting repair...", flush=True) @@ -296,6 +317,7 @@ def main(): if len(shell.faces) < args.min_faces: print(f" Skipping shell with only {len(shell.faces)} face{'s' if len(shell.faces) != 1 else ''}", flush=True) + nshells = nshells-1 continue print(f" Diagnostics:") @@ -306,7 +328,7 @@ def main(): print(f" - Genus: {int(genus)}") polygons = merge_coplanar_triangles(shell.vertices, shell.faces) - export_openscad_structure(shell.vertices.tolist(), polygons, name, i, precision, f) + export_openscad_structure(shell.vertices.tolist(), polygons, nshells, i, precision, f) if __name__ == "__main__": main()