/** * TSM Generic Thread Generator 0.0.2, Tyler Montbriand, 2019 * * Fast and flexible generator of structurally-sound * Known bug: Top and bottom faces are slightly off-angle fot thread profiles * not containing four points. However there aren't any of those right now. */ // Generate a thread of major diameter 20mm, length 20mm, pitch 2.5, profile UTS. // tsmthread(20, 20, 2.5); /** * Generate a thread of major diameter 9mm minus 0.25mm tolerance, * of 25mm length, 2mm pitch, trapezoid thread profile, with 4 starts. */ // tsmthread(9-0.25, 25, 2, PR=THREAD_EXTERNAL_TRAP, STARTS=4); /** * Differential screw made out of 3-start trapezoidal threads at an unreasonable * number of triangles subtracted from each other because fuck CPU's. */ /*difference() { tsmthread(42, 20, 4.5, PR=THREAD_EXTERNAL_TRAP, STARTS=3, POINTS=360); translate([0,0,-5]) mirror([1,0,0]) tsmthread(36,30,4,PR=THREAD_EXTERNAL_TRAP, STARTS=3, POINTS=360); }*/ /** * Generates truncated, symmetrical thread profiles like UTS or ACME * A is pitch angle * MIN is distance below centerline in pitch units * MAX is distance above centerline in pitch units * TOL makes tooth narrower/wider and deeper/shallower than perfect by TOL amount, * again in units of pitch * * Resuls biased to zero and inverted -- just like tsmthread() likes them. */ function prof(A,MIN,MAX)=let( M=tan(90-(A/2)), // Slope of tooth X1=((M/2)-(MIN*2))/M, // Given a Y of MIN, calculate X X2=((M/2)+(MAX*2))/M, // Given a Y of MAX, calculate X OFF=-X1*M) [ [0, OFF + M*X1], // Starting point, always [X1+X1,OFF + M*X1], [X1+X2,OFF+M*X2], [X1+2-X2,OFF+M*X2], // Profile wraps here ]/2; /** * Profile Interpolation * * prof() generates, and tsmthread expects, a profile of [X,Y] values like * [ [0,0.25], [0.25, 1], [0.5,0.25] ] * The first X must be 0. The last X cannot be 1. All values are in pitch units. * * Get a value out with interpolate(PR, X). * interpolate(PR, 0.25) would return 1, * interpolate(PR, 0) would give 0.25, * interpolate(PR, 0.125) would give someting between. * interpolate(PR, 0.5) would give 0.25. * interpolate(PR,0.75) would wrap, interpolating between P[2]-p[0]. * * Should wrap cleanly for any positive or negative value. */ /** * Helper function for interpolate(). Allows * a thread profile to repeat cleanly for increasing * values of N, with growing X accordingly. * * P=[ [0,0], [0.5,0.5] ]; * echo(wrap(P,0)); // should be [0,0] * echo(wrap(P,1)); // should be [0.5,0.5] * echo(wrap(P,2)); // should be [0,0]+[1,0] * echo(wrap(P,3)); // should be [0.5,0.5]+[1,0] * echo(wrap(P,4)); // should be [0,0]+[2,0] * etc. */ function wrap(V, N)=let(M=floor(N/len(V))) V[N%len(V)] + M*[1,0]; /* Very basic interpolation. mix(A,B,0)=A, mix(A,B,1)=B, etc. */ function mix(A,B,X)=(A*(1-X)) + B*X; // 0 <= X <= 1 /** * Line-matching. V1-V2 are a pair of XY coordinates describing a line. * Returns [X,Y] along that line for the given X. */ function mixv(V1,V2,X)=let(XP=X-V1[0]) mix(V1,V2,XP/(V2[0]-V1[0])); /** * Returns Y for given X along an interpolated Y. * V must be a set of points [ [0,Y1], [X2,Y2], ..., [XN,YN] ] X<1 * X can be any value, even negative. */ function interpolate(V, X, P=0, N=0)= (X<0) ? interpolate(V, 1+(X%1),P,N) : (X>wrap(V,N+1)[0]) ? interpolate(V,X,P,N+1) : mixv(wrap(V,N),wrap(V,N+1),X)[1]; // [X,Y,Z] point along a circle of radius R, angle A, at position Z. function traj(R,A,Z)=R*[sin(A),cos(A),0]+[0,0,Z]; module tsmthread(DMAJ=20 // Major diameter , L=50, // Length of thread in mm. Accuracy depends on pitch. , PITCH=2.5, // Scale of the thread itself. , PR=THREAD_EXTERNAL_UTS// Thread profile, i.e. ACME, UTS, other , STARTS=1, // Want a crazy 37-start thread? You're crazy. but I'll try. , POINTS=16 // Radial resolution of threading , OFF=0 // Increase or decrease diameter a tiny amount. ) { /** * The top and bottom are cut off so more height than 0 is needed * to generate a thread. */ RING_MIN=2+len(PR) + STARTS*len(PR); // Calculated number of rings per height. RINGS=RING_MIN + floor((L*len(PR)/PITCH)); // Debug value. Offsets top and bottom faces to better show any flaws in mesh SHOW=0; /** * How this works: Take PR to be the outside edge of a cylinder of radius DMAJ. * Generate points for 360 degrees. N is angle along this circle, RING is height. * * Now, to turn this circle into a spiral, a little Z is added for each value of N. * The join between the first and last vertex of each circle must jump to meet. */ function zoff(RING,N)=(wrap(PR, RING)[0] + STARTS*(N/POINTS)) * PITCH; FLAT_B=zoff(len(PR)*STARTS,0); // Z coordinates of bottom flat FLAT_T=zoff(RINGS-len(PR),0); // Z coordinates of top flat /** * Deliminate what spiral coordinates exist between the top and bottom flats. * Used for loops, so that only those polygons are joined and nothing outside it. */ function ringmin(N,RING=0)=zoff(RING,(N%POINTS))<=FLAT_B?ringmin(N,RING+1):RING; function ringmax(N,RING=0)=zoff(RING+1,(N%POINTS))ringmax2(N))? DATA[1]+(N%POINTS) // Top flat :DATA[2]+RING*POINTS + (N%POINTS); // Inbetween // Like above but takes a vector to transform into a triangle function pointv(V)=[ for(N=V) point(N[0],N[1]) ]; /** * List of points, organized in sections. * 0 - RINGS-1 Bottom cap * RINGS - (2*RINGS)-1 Top cap * (2*RINGS) - end Spiral * Do not change this arrangement without updating DATA to match! */ POLY=concat( // Bottom cap, top cap cap(len(PR)*STARTS,-SHOW),cap(RINGS-len(PR),SHOW), // Main spiral [ for(RING=[0:RINGS-1], N=[0:POINTS-1], A=-360*(N/POINTS), // Radial coodinates //R=(OFF/2)+(DMAJ/2)-PITCH*wrap(PR, RING)[1], R=(OFF/2)+radius_spiral(RING,N)//, /***/ /*Z=(wrap(PR, RING)[0] + STARTS*(N/POINTS)) * PITCH*/) traj(R,A,zoff(RING,N)) ]); PLIST=concat( // Main spiral A [ for(N=[0:POINTS-2], RING=[ringmin2(N)-1:ringmax2(N)]) pointv([ [RING,N+1],[RING,N],[RING+1,N] ]) ], // Main spiral B [ for(N=[0:POINTS-2], RING=[ringmin2(N+1)-1:ringmax2(N+1)]) pointv([[RING+1,N+1],[RING,N+1],[RING+1,N]]) ], // stitch A [ for(N=POINTS-1, RING=[ringmin2(N)-1:ringmax2(0)]) let(P=pointv([ [RING,N],[RING+1,N],[RING+len(PR)*STARTS,0] ])) if((P[0] != P[1]) && (P[0] != P[2]) && (P[1] != P[2])) P ], // Stitch B [ for(N=0, RING=[ringmin2(N)-1:ringmax2(N)]) let(P=pointv([[RING+1,N],[RING,N],[RING+1-len(PR)*STARTS,POINTS-1]])) if((P[0] != P[1]) && (P[0] != P[2]) && (P[1] != P[2])) P ], // Bottom flat [ [ for(N=[0:POINTS-1]) N+DATA[0] ] ], // top flat. Note reverse direction to mirror the normal. [ [ for(N=[0:POINTS-1], N2=POINTS-(N+1)) N2+DATA[1] ] ] ); // Have to translate it down after chopping off the bottom to // make it flat translate(-FLAT_B*[0,0,1]) polyhedron(POLY, PLIST, convexity=3); } /* "Garden Hose" thread profile. Very close to UTS but marginally deeper */ THREAD_EXTERNAL_NH=let(H=0.8660) prof(60, H*(3/8), H*(3/8)); //THREAD_EXTERNAL_NPSH=let(H=0.8660/2) prof(60,(H-.1), H-0.1); THREAD_EXTERNAL_ACME=let(H=0.5/2) prof(29,H,H); THREAD_EXTERNAL_TRAP=let(H=0.5/2) prof(30,H,H); THREAD_EXTERNAL_UTS =let(H=0.8660) prof(60,H*(7/16), H*(3/8)); // Just for the heck of it, buttress threads. function buttress(A,FLAT)=let(M=tan(A)) [ [0,0],[FLAT,0],[FLAT,M*(1-(2*FLAT))], [2*FLAT,M*(1-(2*FLAT))] ]; THREAD_EXTERNAL_BUTT=buttress(30,0.2); /* Test: Trapezoidal threads. */ if(0) { color("lightgreen") intersection() { /* Looks like 9mm, not 8mm, from trivial measurement */ import("nut_8mm4start2mm.stl", convexity=3); cube([50,50,50]); } difference() { rotate([0,0,80+(360/16)]) translate([0,0,0.5]+[0,0,4]) tsmthread(9-0.25, 6, 2, THREAD_EXTERNAL_TRAP, STARTS=4, POINTS=32); cylinder(d=5, h=50, $fn=32); } } /* Test: UTS metric threads, 20mm. */ if(0) { color("lightblue") import("Teil_1.stl"); difference() { translate([0,0,-2.5]) rotate([0,0,-80]) tsmthread(20,20,2.5,STARTS=1,POINTS=32); cylinder(d=14, h=100, center=true); } } module show_profile(V=THREAD_EXTERNAL_ACME, title="ACME", l=4, s=1) { POLY=concat([[0,-1]], [for(N=[0:(len(V)*l)]) [wrap(V,N)[0], 1-wrap(V,N)[1]] ], [[l,-1]]); color("lightblue") linear_extrude(0.1) difference() { polygon(POLY); translate([0.5,-0.75]) text(title,0.75); } translate([4,-1]) rotate([0,90]) tsmthread(4,3,1,PR=V, STARTS=s, POINTS=64); } /* thread profile circus */ if(1) { for(X=[ ["ACME",[0,0],THREAD_EXTERNAL_ACME,2 ], ["NH",[0,-5], THREAD_EXTERNAL_NH,1 ], ["UTS", [8,0],THREAD_EXTERNAL_UTS,1 ], ["BUTT",[8,-5],THREAD_EXTERNAL_BUTT,1], ]) translate(X[1]) show_profile(X[2], X[0]); }