diff --git a/affine.scad b/affine.scad index 5696cc6..462ae1f 100644 --- a/affine.scad +++ b/affine.scad @@ -467,6 +467,48 @@ function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][ +// Function: rot_decode() +// Usage: +// [angle,axis,cp,translation] = rot_decode(rotation) +// Description: +// Given an input 3d rigid transformation operator (one composed of just rotations and translations) +// represented as a 4x4 matrix, compute the rotation and translation parameters of the operator. +// Returns a list of the four parameters, the angle, in the interval [0,180], the rotation axis +// as a unit vector, a centerpoint for the rotation, and a translation. If you set `parms=rot_decode(rotation)` +// then the transformation can be reconstructed from parms as `move(parms[3])*rot(a=parms[0],v=parms[1],cp=parms[2])`. +// This decomposition makes it possible to perform interpolation. If you construct a transformation using `rot` +// the decoding may flip the axis (if you gave an angle outside of [0,180]). The returned axis will be a unit vector, +// and the centerpoint lies on the plane through the origin that is perpendicular to the axis. It may be different +// than the centerpoint you used to construct the transformation. +// Example: +// rot_decode(rot(45)); // Returns [45,[0,0,1], [0,0,0], [0,0,0]] +// rot_decode(rot(a=37, v=[1,2,3], cp=[4,3,-7]))); // Returns [37, [0.26, 0.53, 0.80], [4.8, 4.6, -4.6], [0,0,0]] +// rot_decode(left(12)*xrot(-33)); // Returns [33, [-1,0,0], [0,0,0], [-12,0,0]] +// rot_decode(translate([3,4,5])); // Returns [0, [0,0,1], [0,0,0], [3,4,5]] +function rot_decode(M) = + assert(is_matrix(M,4,4) && M[3]==[0,0,0,1], "Input matrix must be a 4x4 matrix representing a 3d transformation") + let(R = submatrix(M,[0:2],[0:2])) + assert(approx(det3(R),1) && approx(norm_fro(R * transpose(R)-ident(3)),0),"Input matrix is not a rotation") + let( + translation = [for(row=[0:2]) M[row][3]], // translation vector + largest = max_index([R[0][0], R[1][1], R[2][2]]), + axis_matrix = R + transpose(R) - (matrix_trace(R)-1)*ident(3), // Each row is on the rotational axis + // Construct quaternion q = c * [x sin(theta/2), y sin(theta/2), z sin(theta/2), cos(theta/2)] + q_im = axis_matrix[largest], + q_re = R[(largest+2)%3][(largest+1)%3] - R[(largest+1)%3][(largest+2)%3], + c_sin = norm(q_im), // c * sin(theta/2) for some c + c_cos = abs(q_re) // c * cos(theta/2) + ) + approx(c_sin,0) ? [0,[0,0,1],[0,0,0],translation] : + let( + angle = 2*atan2(c_sin, c_cos), // This is supposed to be more accurate than acos or asin + axis = (q_re>=0 ? 1:-1)*q_im/c_sin, + tproj = translation - (translation*axis)*axis, // Translation perpendicular to axis determines centerpoint + cp = (tproj + cross(axis,tproj)*c_cos/c_sin)/2 + ) + [angle, axis, cp, (translation*axis)*axis]; + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/geometry.scad b/geometry.scad index 6a69e11..b18456f 100644 --- a/geometry.scad +++ b/geometry.scad @@ -472,6 +472,73 @@ function line_from_points(points, fast=false, eps=EPSILON) = // Section: 2D Triangles + +// Function: law_of_cosines() +// Usage: +// C = law_of_cosines(a, b, c); +// c = law_of_cosines(a, b, C); +// Description: +// Applies the Law of Cosines for an arbitrary triangle. +// Given three side lengths, returns the angle in degrees for the corner opposite of the third side. +// Given two side lengths, and the angle between them, returns the length of the third side. +// Figure(2D): +// stroke([[-50,0], [10,60], [50,0]], closed=true); +// color("black") { +// translate([ 33,35]) text(text="a", size=8, halign="center", valign="center"); +// translate([ 0,-6]) text(text="b", size=8, halign="center", valign="center"); +// translate([-22,35]) text(text="c", size=8, halign="center", valign="center"); +// } +// color("blue") { +// translate([-37, 6]) text(text="A", size=8, halign="center", valign="center"); +// translate([ 9,51]) text(text="B", size=8, halign="center", valign="center"); +// translate([ 38, 6]) text(text="C", size=8, halign="center", valign="center"); +// } +// Arguments: +// a = The length of the first side. +// b = The length of the second side. +// c = The length of the third side. +// C = The angle in degrees of the corner opposite of the third side. +function law_of_cosines(a, b, c, C) = + // Triangle Law of Cosines: + // c^2 = a^2 + b^2 - 2*a*b*cos(C) + assert(num_defined([c,C]) == 1, "Must give exactly one of c= or C=.") + is_undef(c) ? sqrt(a*a + b*b - 2*a*b*cos(C)) : + acos(constrain((a*a + b*b - c*c) / (2*a*b), -1, 1)); + + +// Function: law_of_sines() +// Usage: +// B = law_of_sines(a, A, b); +// b = law_of_sines(a, A, B); +// Description: +// Applies the Law of Sines for an arbitrary triangle. +// Given two triangle side lengths and the angle between them, returns the angle of the corner opposite of the second side. +// Given a side length, the opposing angle, and a second angle, returns the length of the side opposite of the second angle. +// Figure(2D): +// stroke([[-50,0], [10,60], [50,0]], closed=true); +// color("black") { +// translate([ 33,35]) text(text="a", size=8, halign="center", valign="center"); +// translate([ 0,-6]) text(text="b", size=8, halign="center", valign="center"); +// translate([-22,35]) text(text="c", size=8, halign="center", valign="center"); +// } +// color("blue") { +// translate([-37, 6]) text(text="A", size=8, halign="center", valign="center"); +// translate([ 9,51]) text(text="B", size=8, halign="center", valign="center"); +// translate([ 38, 6]) text(text="C", size=8, halign="center", valign="center"); +// } +// Arguments: +// a = The length of the first side. +// A = The angle in degrees of the corner opposite of the first side. +// b = The length of the second side. +// B = The angle in degrees of the corner opposite of the second side. +function law_of_sines(a, A, b, B) = + // Triangle Law of Sines: + // a/sin(A) = b/sin(B) = c/sin(C) + assert(num_defined([b,B]) == 1, "Must give exactly one of b= or B=.") + let( r = a/sin(A) ) + is_undef(b) ? r*sin(B) : asin(constrain(b/r, -1, 1)); + + // Function: tri_calc() // Usage: // tri_calc(ang,ang2,adj,opp,hyp); @@ -750,8 +817,8 @@ function adj_opp_to_ang(adj,opp) = function triangle_area(a,b,c) = assert( is_path([a,b,c]), "Invalid points or incompatible dimensions." ) len(a)==3 - ? 0.5*norm(cross(c-a,c-b)) - : 0.5*cross(c-a,c-b); + ? 0.5*norm(cross(c-a,c-b)) + : 0.5*cross(c-a,c-b); @@ -816,7 +883,7 @@ function plane3pt_indexed(points, i1, i2, i3) = function plane_from_normal(normal, pt=[0,0,0]) = assert( is_matrix([normal,pt],2,3) && !approx(norm(normal),0), "Inputs `normal` and `pt` should 3d vectors/points and `normal` cannot be zero." ) - concat(normal, normal*pt)/norm(normal); + concat(normal, normal*pt) / norm(normal); // Function: plane_from_points() diff --git a/involute_gears.scad b/involute_gears.scad index a424eac..86528b1 100644 --- a/involute_gears.scad +++ b/involute_gears.scad @@ -45,52 +45,112 @@ // Function: circular_pitch() -// Description: Get tooth density expressed as "circular pitch". +// Usage: +// circp = circular_pitch(pitch|mod); +// Description: +// Get tooth density expressed as "circular pitch". // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. -function circular_pitch(pitch=5) = pitch; +// mod = The metric module/modulus of the gear. +// Examples: +// circp = circular_pitch(pitch=5); +// circp = circular_pitch(mod=2); +function circular_pitch(pitch=5, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) + pitch; // Function: diametral_pitch() -// Description: Get tooth density expressed as "diametral pitch". +// Usage: +// dp = diametral_pitch(pitch|mod); +// Description: +// Get tooth density expressed as "diametral pitch". // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. -function diametral_pitch(pitch=5) = PI / pitch; +// mod = The metric module/modulus of the gear. +// Examples: +// dp = diametral_pitch(pitch=5); +// dp = diametral_pitch(mod=2); +function diametral_pitch(pitch=5, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) + PI / pitch; + + +// Function: pitch_value() +// Usage: +// pitch = pitch_value(mod); +// Description: +// Get circular pitch in mm from module/modulus. +// Arguments: +// mod = The module/modulus of the gear. +function pitch_value(mod) = mod * PI; // Function: module_value() -// Description: Get tooth density expressed as "module" or "modulus" in millimeters +// Usage: +// mod = module_value(pitch); +// Description: +// Get tooth density expressed as "module" or "modulus" in millimeters // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. function module_value(pitch=5) = pitch / PI; // Function: adendum() -// Description: The height of the gear tooth above the pitch radius. +// Usage: +// ad = adendum(pitch|mod); +// Description: +// The height of the gear tooth above the pitch radius. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. -function adendum(pitch=5) = module_value(pitch); +// mod = The metric module/modulus of the gear. +// Examples: +// ad = adendum(pitch=5); +// ad = adendum(mod=2); +function adendum(pitch=5, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) + module_value(pitch) * 1.0; // Function: dedendum() -// Description: The depth of the gear tooth valley, below the pitch radius. +// Usage: +// ddn = dedendum(pitch|mod, ); +// Description: +// The depth of the gear tooth valley, below the pitch radius. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // clearance = If given, sets the clearance between meshing teeth. -function dedendum(pitch=5, clearance=undef) = - (clearance==undef)? (1.25 * module_value(pitch)) : (module_value(pitch) + clearance); +// mod = The metric module/modulus of the gear. +// Examples: +// ddn = dedendum(pitch=5); +// ddn = dedendum(mod=2); +function dedendum(pitch=5, clearance, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) + is_undef(clearance)? (1.25 * module_value(pitch)) : + (module_value(pitch) + clearance); // Function: pitch_radius() -// Description: Calculates the pitch radius for the gear. +// Usage: +// pr = pitch_radius(pitch|mod, teeth); +// Description: +// Calculates the pitch radius for the gear. Two mated gears will have their centers spaced apart +// by the sum of the two gear's pitch radii. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // teeth = The number of teeth on the gear. -function pitch_radius(pitch=5, teeth=11) = +// mod = The metric module/modulus of the gear. +// Examples: +// pr = pitch_radius(pitch=5, teeth=11); +// pr = pitch_radius(mod=2, teeth=20); +function pitch_radius(pitch=5, teeth=11, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) pitch * teeth / PI / 2; // Function: outer_radius() +// Usage: +// or = outer_radius(pitch|mod, teeth, , ); // Description: // Calculates the outer radius for the gear. The gear fits entirely within a cylinder of this radius. // Arguments: @@ -98,12 +158,19 @@ function pitch_radius(pitch=5, teeth=11) = // teeth = The number of teeth on the gear. // clearance = If given, sets the clearance between meshing teeth. // interior = If true, calculate for an interior gear. -function outer_radius(pitch=5, teeth=11, clearance=undef, interior=false) = +// mod = The metric module/modulus of the gear. +// Examples: +// or = outer_radius(pitch=5, teeth=20); +// or = outer_radius(mod=2, teeth=16); +function outer_radius(pitch=5, teeth=11, clearance, interior=false, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) pitch_radius(pitch, teeth) + (interior? dedendum(pitch, clearance) : adendum(pitch)); // Function: root_radius() +// Usage: +// rr = root_radius(pitch|mod, teeth, , ); // Description: // Calculates the root radius for the gear, at the base of the dedendum. // Arguments: @@ -111,120 +178,175 @@ function outer_radius(pitch=5, teeth=11, clearance=undef, interior=false) = // teeth = The number of teeth on the gear. // clearance = If given, sets the clearance between meshing teeth. // interior = If true, calculate for an interior gear. -function root_radius(pitch=5, teeth=11, clearance=undef, interior=false) = +// mod = The metric module/modulus of the gear. +// Examples: +// rr = root_radius(pitch=5, teeth=11); +// rr = root_radius(mod=2, teeth=16); +function root_radius(pitch=5, teeth=11, clearance, interior=false, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) pitch_radius(pitch, teeth) - (interior? adendum(pitch) : dedendum(pitch, clearance)); // Function: base_radius() -// Description: Get the base circle for involute teeth. +// Usage: +// br = base_radius(pitch|mod, teeth, ); +// Description: +// Get the base circle for involute teeth, at the base of the teeth. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // teeth = The number of teeth on the gear. -// PA = Pressure angle in degrees. Controls how straight or bulged the tooth sides are. -function base_radius(pitch=5, teeth=11, PA=28) = - pitch_radius(pitch, teeth) * cos(PA); +// pressure_angle = Pressure angle in degrees. Controls how straight or bulged the tooth sides are. +// mod = The metric module/modulus of the gear. +// Examples: +// br = base_radius(pitch=5, teeth=20, pressure_angle=20); +// br = base_radius(mod=2, teeth=18, pressure_angle=20); +function base_radius(pitch=5, teeth=11, pressure_angle=28, mod) = + let( pitch = is_undef(mod) ? pitch : pitch_value(mod) ) + pitch_radius(pitch, teeth) * cos(pressure_angle); -// Function bevel_pitch_angle() +// Function: bevel_pitch_angle() // Usage: -// x = bevel_pitch_angle(teeth, mate_teeth, [drive_angle]); +// ang = bevel_pitch_angle(teeth, mate_teeth, ); // Description: -// Returns the correct pitch angle (bevelang) for a bevel gear with a given number of tooth, that is +// Returns the correct pitch angle for a bevel gear with a given number of tooth, that is // matched to another bevel gear with a (possibly different) number of teeth. // Arguments: // teeth = Number of teeth that this gear has. // mate_teeth = Number of teeth that the matching gear has. -// drive_angle = Angle between the drive shafts of each gear. Usually 90º. +// drive_angle = Angle between the drive shafts of each gear. Default: 90º. +// Examples: +// ang = bevel_pitch_angle(teeth=18, mate_teeth=30); function bevel_pitch_angle(teeth, mate_teeth, drive_angle=90) = atan(sin(drive_angle)/((mate_teeth/teeth)+cos(drive_angle))); +// Function: worm_gear_thickness() +// Usage: +// thick = worm_gear_thickness(pitch|mod, teeth, worm_diam, , , ); +// Description: +// Calculate the thickness of the worm gear. +// Arguments: +// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. Default: 5 +// teeth = Total number of teeth along the rack. Default: 30 +// worm_diam = The pitch diameter of the worm gear to match to. Default: 30 +// worm_arc = The arc of the worm to mate with, in degrees. Default: 60 degrees +// crowning = The amount to oversize the virtual hobbing cutter used to make the teeth, to add a slight crowning to the teeth to make them fir the work easier. Default: 1 +// clearance = Clearance gap at the bottom of the inter-tooth valleys. +// mod = The metric module/modulus of the gear. +// Examples: +// thick = worm_gear_thickness(pitch=5, teeth=36, worm_diam=30); +// thick = worm_gear_thickness(mod=2, teeth=28, worm_diam=25); +function worm_gear_thickness(pitch=5, teeth=30, worm_diam=30, worm_arc=60, crowning=1, clearance, mod) = + let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), + r = worm_diam/2 + crowning, + pitch_thick = r * sin(worm_arc/2) * 2, + pr = pitch_radius(pitch, teeth), + rr = root_radius(pitch, teeth, clearance, false), + pitchoff = (pr-rr) * sin(worm_arc/2), + thickness = pitch_thick + 2*pitchoff + ) thickness; + + function _gear_polar(r,t) = r*[sin(t),cos(t)]; function _gear_iang(r1,r2) = sqrt((r2/r1)*(r2/r1) - 1)/PI*180 - acos(r1/r2); //unwind a string this many degrees to go from radius r1 to radius r2 function _gear_q6(b,s,t,d) = _gear_polar(d,s*(_gear_iang(b,d)+t)); //point at radius d on the involute curve function _gear_q7(f,r,b,r2,t,s) = _gear_q6(b,s,t,(1-f)*max(b,r)+f*r2); //radius a fraction f up the curved side of the tooth -// Section: Modules +// Section: 2D Profiles // Function&Module: gear_tooth_profile() // Usage: As Module -// gear_tooth_profile(pitch, teeth, , , , , ); +// gear_tooth_profile(pitch|mod, teeth, , , , , ); // Usage: As Function -// path = gear_tooth_profile(pitch, teeth, , , , , ); +// path = gear_tooth_profile(pitch|mod, teeth, , , , , ); // Description: // When called as a function, returns the 2D profile path for an individual gear tooth. // When called as a module, creates the 2D profile shape for an individual gear tooth. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. -// teeth = Total number of teeth along the rack -// PA = Controls how straight or bulged the tooth sides are. In degrees. +// teeth = Total number of teeth on the spur gear that this is a tooth for. +// pressure_angle = Pressure Angle. Controls how straight or bulged the tooth sides are. In degrees. // clearance = Gap between top of a tooth on one gear and bottom of valley on a meshing gear (in millimeters) // backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle // interior = If true, create a mask for difference()ing from something else. -// valleys = If true, add the valley bottoms on either side of the tooth. +// valleys = If true, add the valley bottoms on either side of the tooth. Default: true +// center = If true, centers the pitch circle of the tooth profile at the origin. Default: false. +// mod = The metric module/modulus of the gear. // Example(2D): -// gear_tooth_profile(pitch=5, teeth=20, PA=20); +// gear_tooth_profile(pitch=5, teeth=20, pressure_angle=20); +// Example(2D): Metric Gear Tooth +// gear_tooth_profile(mod=2, teeth=20, pressure_angle=20); // Example(2D): -// gear_tooth_profile(pitch=5, teeth=20, PA=20, valleys=true); +// gear_tooth_profile(pitch=5, teeth=20, pressure_angle=20, valleys=false); +// Example(2D): As a function +// stroke(gear_tooth_profile(pitch=5, teeth=20, pressure_angle=20, valleys=false), width=0.1); function gear_tooth_profile( - pitch = 3, - teeth = 11, - PA = 28, + pitch = 3, + teeth = 11, + pressure_angle = 28, clearance = undef, - backlash = 0.0, - interior = false, - valleys = true + backlash = 0.0, + interior = false, + valleys = true, + center = false, + mod ) = let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), p = pitch_radius(pitch, teeth), c = outer_radius(pitch, teeth, clearance, interior), r = root_radius(pitch, teeth, clearance, interior), - b = base_radius(pitch, teeth, PA), + b = base_radius(pitch, teeth, pressure_angle), t = pitch/2-backlash/2, //tooth thickness at pitch circle k = -_gear_iang(b, p) - t/2/p/PI*180, //angle to where involute meets base circle on each side of tooth kk = r, , , , ); +// gear2d(pitch|mod, teeth, , , , , ); // Usage: As Function -// poly = gear2d(pitch, teeth, , , , , ); +// poly = gear2d(pitch|mod, teeth, , , , , ); // Description: // When called as a module, creates a 2D involute spur gear. When called as a function, returns a // 2D path for the perimeter of a 2D involute spur gear. Normally, you should just specify the // first 2 parameters `pitch` and `teeth`, and let the rest be default values. -// Meshing gears must match in `pitch`, `PA`, and `helical`, and be separated by +// Meshing gears must match in `pitch`, `pressure_angle`, and `helical`, and be separated by // the sum of their pitch radii, which can be found with `pitch_radius()`. // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. -// teeth = Total number of teeth along the rack +// teeth = Total number of teeth around the spur gear. // hide = Number of teeth to delete to make this only a fraction of a circle -// PA = Controls how straight or bulged the tooth sides are. In degrees. +// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. // clearance = Gap between top of a tooth on one gear and bottom of valley on a meshing gear (in millimeters) // backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle // interior = If true, create a mask for difference()ing from something else. +// mod = The metric module/modulus of the gear. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // Example(2D): Typical Gear Shape // gear2d(pitch=5, teeth=20); +// Example(2D): Metric Gear +// gear2d(mod=2, teeth=20); // Example(2D): Lower Pressure Angle -// gear2d(pitch=5, teeth=20, PA=20); +// gear2d(pitch=5, teeth=20, pressure_angle=20); // Example(2D): Partial Gear -// gear2d(pitch=5, teeth=20, hide=15, PA=20); +// gear2d(pitch=5, teeth=20, hide=15, pressure_angle=20); +// Example(2D): Called as a Function +// path = gear2d(pitch=8, teeth=16); +// polygon(path); function gear2d( - pitch = 3, - teeth = 11, - hide = 0, - PA = 28, + pitch = 3, + teeth = 11, + hide = 0, + pressure_angle = 28, clearance = undef, - backlash = 0.0, - interior = false, - anchor = CENTER, - spin = 0 + backlash = 0.0, + interior = false, + mod, + anchor = CENTER, + spin = 0 ) = let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), pr = pitch_radius(pitch=pitch, teeth=teeth), pts = concat( [for (tooth = [0:1:teeth-hide-1]) each rot(tooth*360/teeth, planar=true, p=gear_tooth_profile( - pitch = pitch, - teeth = teeth, - PA = PA, + pitch = pitch, + teeth = teeth, + pressure_angle = pressure_angle, clearance = clearance, - backlash = backlash, - interior = interior, - valleys = false + backlash = backlash, + interior = interior, + valleys = false ) ) ], @@ -290,24 +420,26 @@ function gear2d( module gear2d( - pitch = 3, - teeth = 11, - hide = 0, - PA = 28, + pitch = 3, + teeth = 11, + hide = 0, + pressure_angle = 28, clearance = undef, - backlash = 0.0, - interior = false, - anchor = CENTER, - spin = 0 + backlash = 0.0, + interior = false, + mod, + anchor = CENTER, + spin = 0 ) { + pitch = is_undef(mod) ? pitch : pitch_value(mod); path = gear2d( - pitch = pitch, - teeth = teeth, - hide = hide, - PA = PA, + pitch = pitch, + teeth = teeth, + hide = hide, + pressure_angle = pressure_angle, clearance = clearance, - backlash = backlash, - interior = interior + backlash = backlash, + interior = interior ); pr = pitch_radius(pitch=pitch, teeth=teeth); attachable(anchor,spin, two_d=true, r=pr) { @@ -317,23 +449,143 @@ module gear2d( } -// Module: gear() -// Usage: -// gear(pitch, teeth, thickness, , , , , , , , ); +// Function&Module: rack2d() +// Usage: As a Function +// path = rack2d(pitch|mod, teeth, height, , ); +// Usage: As a Module +// rack2d(pitch|mod, teeth, height, , ); +// Description: +// This is used to create a 2D rack, which is a linear bar with teeth that a gear can roll along. +// A rack can mesh with any gear that has the same `pitch` and `pressure_angle`. +// When called as a function, returns a 2D path for the outline of the rack. +// When called as a module, creates a 2D rack shape. +// Arguments: +// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. +// teeth = Total number of teeth along the rack +// height = Height of rack in mm, from tooth top to back of rack. +// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. +// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle +// mod = The metric module/modulus of the gear. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// Anchors: +// "adendum" = At the tips of the teeth, at the center of rack. +// "adendum-left" = At the tips of the teeth, at the left end of the rack. +// "adendum-right" = At the tips of the teeth, at the right end of the rack. +// "dedendum" = At the height of the teeth, at the center of rack. +// "dedendum-left" = At the height of the teeth, at the left end of the rack. +// "dedendum-right" = At the height of the teeth, at the right end of the rack. +// Example(2D): +// rack2d(pitch=5, teeth=10, height=10, pressure_angle=20); +// Example(2D): Called as a Function +// path = rack2d(pitch=8, teeth=8, height=10, pressure_angle=28); +// polygon(path); +function rack2d( + pitch = 5, + teeth = 20, + height = 10, + pressure_angle = 28, + backlash = 0.0, + clearance = undef, + mod, + anchor = CENTER, + spin = 0 +) = + let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), + a = adendum(pitch), + d = dedendum(pitch, clearance) + ) + assert(a+d < height) + let( + xa = a * sin(pressure_angle), + xd = d * sin(pressure_angle), + l = teeth * pitch, + anchors = [ + anchorpt("adendum", [ 0, a,0], BACK), + anchorpt("adendum-left", [-l/2, a,0], LEFT), + anchorpt("adendum-right", [ l/2, a,0], RIGHT), + anchorpt("dedendum", [ 0,-d,0], BACK), + anchorpt("dedendum-left", [-l/2,-d,0], LEFT), + anchorpt("dedendum-right", [ l/2,-d,0], RIGHT), + ], + path = [ + [-(teeth-1)/2 * pitch + -1/2 * pitch, a-height], + [-(teeth-1)/2 * pitch + -1/2 * pitch, -d], + for (i = [0:1:teeth-1]) let( + off = (i-(teeth-1)/2) * pitch + ) each [ + [off + -1/4 * pitch + backlash - xd, -d], + [off + -1/4 * pitch + backlash + xa, a], + [off + 1/4 * pitch - backlash - xa, a], + [off + 1/4 * pitch - backlash + xd, -d], + ], + [ (teeth-1)/2 * pitch + 1/2 * pitch, -d], + [ (teeth-1)/2 * pitch + 1/2 * pitch, a-height], + ] + ) reorient(anchor,spin, two_d=true, size=[l,2*abs(a-height)], anchors=anchors, p=path); + + +module rack2d( + pitch = 5, + teeth = 20, + height = 10, + pressure_angle = 28, + backlash = 0.0, + clearance = undef, + mod, + anchor = CENTER, + spin = 0 +) { + pitch = is_undef(mod) ? pitch : pitch_value(mod); + a = adendum(pitch); + d = dedendum(pitch, clearance); + l = teeth * pitch; + anchors = [ + anchorpt("adendum", [ 0, a,0], BACK), + anchorpt("adendum-left", [-l/2, a,0], LEFT), + anchorpt("adendum-right", [ l/2, a,0], RIGHT), + anchorpt("dedendum", [ 0,-d,0], BACK), + anchorpt("dedendum-left", [-l/2,-d,0], LEFT), + anchorpt("dedendum-right", [ l/2,-d,0], RIGHT), + ]; + path = rack2d( + pitch = pitch, + teeth = teeth, + height = height, + pressure_angle = pressure_angle, + backlash = backlash, + clearance = clearance + ); + attachable(anchor,spin, two_d=true, size=[l, 2*abs(a-height)], anchors=anchors) { + polygon(path); + children(); + } +} + + + +// Section: 3D Gears and Racks + + +// Function&Module: gear() +// Usage: As a Module +// gear(pitch|mod, teeth, thickness, , , , , , , , ); +// Usage: As a Function +// vnf = gear(pitch|mod, teeth, thickness, , , , , , , , ); // Description: // Creates a (potentially helical) involute spur gear. The module `gear()` gives an involute spur // gear, with reasonable defaults for all the parameters. Normally, you should just choose the // first 4 parameters, and let the rest be default values. The module `gear()` gives a gear in the -// XY plane, centered on the origin, with one tooth centered on the positive Y axis. The various -// functions below it take the same parameters, and return various measurements for the gear. The +// XY plane, centered on the origin, with one tooth centered on the positive Y axis. The // most important is `pitch_radius()`, which tells how far apart to space gears that are meshing, // and `outer_radius()`, which gives the size of the region filled by the gear. A gear has a "pitch // circle", which is an invisible circle that cuts through the middle of each tooth (though not the // exact center). In order for two gears to mesh, their pitch circles should just touch. So the // distance between their centers should be `pitch_radius()` for one, plus `pitch_radius()` for the // other, which gives the radii of their pitch circles. In order for two gears to mesh, they must -// have the same `pitch` and `PA` parameters. `pitch` gives the number of millimeters of arc around -// the pitch circle covered by one tooth and one space between teeth. The `PA` controls how flat or +// have the same `pitch` and `pressure_angle` parameters. `pitch` gives the number of millimeters of arc around +// the pitch circle covered by one tooth and one space between teeth. The `pressure_angle` controls how flat or // bulged the sides of the teeth are. Common values include 14.5 degrees and 20 degrees, and // occasionally 25. Though I've seen 28 recommended for plastic gears. Larger numbers bulge out // more, giving stronger teeth, so 28 degrees is the default here. The ratio of `teeth` for two @@ -345,32 +597,35 @@ module gear2d( // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // teeth = Total number of teeth around the entire perimeter // thickness = Thickness of gear in mm -// shaft_diam = Diameter of the hole in the center, in mm +// shaft_diam = Diameter of the hole in the center, in mm. Default: 0 (no shaft hole) // hide = Number of teeth to delete to make this only a fraction of a circle -// PA = Controls how straight or bulged the tooth sides are. In degrees. +// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. // clearance = Clearance gap at the bottom of the inter-tooth valleys. // backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle // helical = Teeth rotate this many degrees from bottom of gear to top. 360 makes the gear a screw with each thread going around once. // slices = Number of vertical layers to divide gear into. Useful for refining gears with `helical`. // scale = Scale of top of gear compared to bottom. Useful for making crown gears. // interior = If true, create a mask for difference()ing from something else. +// mod = The metric module/modulus of the gear. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // Example: Spur Gear // gear(pitch=5, teeth=20, thickness=8, shaft_diam=5); -// Example: Beveled Gear +// Example: Metric Gear +// gear(mod=2, teeth=20, thickness=8, shaft_diam=5); +// Example: Helical Gear // gear(pitch=5, teeth=20, thickness=10, shaft_diam=5, helical=-30, slices=12, $fa=1, $fs=1); -// Example: Assembly of Gears +// Example(2D): Assembly of Gears // n1 = 11; //red gear number of teeth // n2 = 20; //green gear // n3 = 5; //blue gear // n4 = 20; //orange gear // n5 = 8; //gray rack -// pitch = 9; //all meshing gears need the same `pitch` (and the same `PA`) +// pitch = 9; //all meshing gears need the same `pitch` (and the same `pressure_angle`) // thickness = 6; // hole = 3; -// height = 12; +// rack_base = 12; // d1 =pitch_radius(pitch,n1); // d12=pitch_radius(pitch,n1) + pitch_radius(pitch,n2); // d13=pitch_radius(pitch,n1) + pitch_radius(pitch,n3); @@ -380,23 +635,64 @@ module gear2d( // translate([ d13, 0, 0]) rotate([0,0,-($t-n3/4+n1/4+1/2)*360/n3]) color([0.75,0.75,1.00]) gear(pitch,n3,thickness,hole); // translate([ d13, 0, 0]) rotate([0,0,-($t-n3/4+n1/4+1/2)*360/n3]) color([0.75,0.75,1.00]) gear(pitch,n3,thickness,hole); // translate([-d14, 0, 0]) rotate([0,0,-($t-n4/4-n1/4+1/2-floor(n4/4)-3)*360/n4]) color([1.00,0.75,0.50]) gear(pitch,n4,thickness,hole,hide=n4-3); -// translate([(-floor(n5/2)-floor(n1/2)+$t+n1/2)*9, -d1+0.0, 0]) color([0.75,0.75,0.75]) rack(pitch=pitch,teeth=n5,thickness=thickness,height=height,anchor=CENTER); -module gear( - pitch = 3, - teeth = 11, +// translate([(-floor(n5/2)-floor(n1/2)+$t+n1/2)*9, -d1+0.0, 0]) color([0.75,0.75,0.75]) rack(pitch=pitch,teeth=n5,thickness=thickness,height=rack_base,anchor=CENTER,orient=BACK); +function gear( + pitch = 3, + teeth = 11, thickness = 6, - shaft_diam = 3, - hide = 0, - PA = 28, + shaft_diam = 0, + hide = 0, + pressure_angle = 28, clearance = undef, - backlash = 0.0, - helical = 0, - slices = 2, - interior = false, - anchor = CENTER, - spin = 0, - orient = UP + backlash = 0.0, + helical = 0, + slices = 2, + interior = false, + mod, + anchor = CENTER, + spin = 0, + orient = UP +) = + let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), + p = pitch_radius(pitch, teeth), + c = outer_radius(pitch, teeth, clearance, interior), + r = root_radius(pitch, teeth, clearance, interior), + twist = atan2(thickness*tan(helical),p), + rgn = [ + gear2d( + pitch = pitch, + teeth = teeth, + pressure_angle = pressure_angle, + hide = hide, + clearance = clearance, + backlash = backlash, + interior = interior + ), + if (shaft_diam > 0) circle(d=shaft_diam, $fn=max(12,segs(shaft_diam/2))) + ], + vnf = linear_sweep(rgn, height=thickness, center=true) + ) reorient(anchor,spin,orient, h=thickness, r=p, p=vnf); + + +module gear( + pitch = 3, + teeth = 11, + thickness = 6, + shaft_diam = 0, + hide = 0, + pressure_angle = 28, + clearance = undef, + backlash = 0.0, + helical = 0, + slices = 2, + interior = false, + mod, + anchor = CENTER, + spin = 0, + orient = UP ) { + pitch = is_undef(mod) ? pitch : pitch_value(mod); p = pitch_radius(pitch, teeth); c = outer_radius(pitch, teeth, clearance, interior); r = root_radius(pitch, teeth, clearance, interior); @@ -405,13 +701,13 @@ module gear( difference() { linear_extrude(height=thickness, center=true, convexity=10, twist=twist) { gear2d( - pitch = pitch, - teeth = teeth, - PA = PA, - hide = hide, + pitch = pitch, + teeth = teeth, + pressure_angle = pressure_angle, + hide = hide, clearance = clearance, - backlash = backlash, - interior = interior + backlash = backlash, + interior = interior ); } if (shaft_diam > 0) { @@ -424,182 +720,219 @@ module gear( -// Module: bevel_gear() -// Usage: -// bevel_gear(pitch, teeth, face_width, bevelang, , , , , , , , , ); +// Function&Module: bevel_gear() +// Usage: As a Module +// bevel_gear(pitch|mod, teeth, face_width, pitch_angle, , , , , , , , , ); +// Usage: As a Function +// vnf = bevel_gear(pitch|mod, teeth, face_width, pitch_angle, , , , , , , , ); // Description: -// Creates a (potentially spiral) bevel gear. -// The module `bevel_gear()` gives an bevel gear, with reasonable -// defaults for all the parameters. Normally, you should just choose -// the first 4 parameters, and let the rest be default values. The -// module `bevel_gear()` gives a gear in the XY plane, centered on the origin, -// with one tooth centered on the positive Y axis. The various functions -// below it take the same parameters, and return various measurements -// for the gear. The most important is `pitch_radius()`, which tells -// how far apart to space gears that are meshing, and `outer_radius()`, -// which gives the size of the region filled by the gear. A gear has -// a "pitch circle", which is an invisible circle that cuts through -// the middle of each tooth (though not the exact center). In order -// for two gears to mesh, their pitch circles should just touch. So -// the distance between their centers should be `pitch_radius()` for -// one, plus `pitch_radius()` for the other, which gives the radii of -// their pitch circles. -// In order for two gears to mesh, they must have the same `pitch` -// and `PA` parameters. `pitch` gives the number -// of millimeters of arc around the pitch circle covered by one tooth -// and one space between teeth. The `PA` controls how flat or -// bulged the sides of the teeth are. Common values include 14.5 -// degrees and 20 degrees, and occasionally 25. Though I've seen 28 -// recommended for plastic gears. Larger numbers bulge out more, giving -// stronger teeth, so 28 degrees is the default here. -// The ratio of `teeth` for two meshing gears gives how many -// times one will make a full revolution when the the other makes one -// full revolution. If the two numbers are coprime (i.e. are not -// both divisible by the same number greater than 1), then every tooth -// on one gear will meet every tooth on the other, for more even wear. -// So coprime numbers of teeth are good. +// Creates a (potentially spiral) bevel gear. The module `bevel_gear()` gives a bevel gear, with +// reasonable defaults for all the parameters. Normally, you should just choose the first 4 +// parameters, and let the rest be default values. The module `bevel_gear()` gives a gear in the XY +// plane, centered on the origin, with one tooth centered on the positive Y axis. The various +// functions below it take the same parameters, and return various measurements for the gear. The +// most important is `pitch_radius()`, which tells how far apart to space gears that are meshing, +// and `outer_radius()`, which gives the size of the region filled by the gear. A gear has a "pitch +// circle", which is an invisible circle that cuts through the middle of each tooth (though not the +// exact center). In order for two gears to mesh, their pitch circles should just touch. So the +// distance between their centers should be `pitch_radius()` for one, plus `pitch_radius()` for the +// other, which gives the radii of their pitch circles. In order for two gears to mesh, they must +// have the same `pitch` and `pressure_angle` parameters. `pitch` gives the number of millimeters of arc around +// the pitch circle covered by one tooth and one space between teeth. The `pressure_angle` controls how flat or +// bulged the sides of the teeth are. Common values include 14.5 degrees and 20 degrees, and +// occasionally 25. Though I've seen 28 recommended for plastic gears. Larger numbers bulge out +// more, giving stronger teeth, so 28 degrees is the default here. The ratio of `teeth` for two +// meshing gears gives how many times one will make a full revolution when the the other makes one +// full revolution. If the two numbers are coprime (i.e. are not both divisible by the same number +// greater than 1), then every tooth on one gear will meet every tooth on the other, for more even +// wear. So coprime numbers of teeth are good. // Arguments: -// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. -// teeth = Total number of teeth around the entire perimeter -// face_width = Width of the toothed surface in mm, from inside to outside. -// shaft_diam = Diameter of the hole in the center, in mm -// hide = Number of teeth to delete to make this only a fraction of a circle -// PA = Controls how straight or bulged the tooth sides are. In degrees. +// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. Default: 5 +// teeth = Total number of teeth around the entire perimeter. Default: 20 +// face_width = Width of the toothed surface in mm, from inside to outside. Default: 10 +// pitch_angle = Angle of beveled gear face. Default: 45 +// mate_teeth = The number of teeth in the gear that this gear will mate with. Overrides `pitch_angle` if given. +// shaft_diam = Diameter of the hole in the center, in mm. Module use only. Default: 0 (no shaft hole) +// hide = Number of teeth to delete to make this only a fraction of a circle. Default: 0 +// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 28 // clearance = Clearance gap at the bottom of the inter-tooth valleys. -// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle -// bevelang = Angle of beveled gear face. -// spiral_rad = Radius of spiral arc for teeth. If 0, then gear will not be spiral. Default: 0 -// spiral_ang = The base angle for spiral teeth. Default: 0 -// slices = Number of vertical layers to divide gear into. Useful for refining gears with `spiral`. -// scale = Scale of top of gear compared to bottom. Useful for making crown gears. +// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle. Default: 0 +// cutter_radius = Radius of spiral arc for teeth. If 0, then gear will not be spiral. Default: 0 +// spiral_angle = The base angle for spiral teeth. Default: 0 +// left_handed = If true, the gear returned will have a left-handed spiral. Default: false +// slices = Number of vertical layers to divide gear into. Useful for refining gears with `spiral`. Default: 1 // interior = If true, create a mask for difference()ing from something else. +// mod = The metric module/modulus of the gear. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Extra Anchors: +// "apex" = At the pitch cone apex for the bevel gear. +// "pitchbase" = At the natural height of the pitch radius of the beveled gear. +// "flattop" = At the top of the flat top of the bevel gear. // Example: Beveled Gear -// bevel_gear(pitch=5, teeth=36, face_width=10, shaft_diam=5, spiral_rad=-20, spiral_ang=35, bevelang=45, slices=12, $fa=1, $fs=1); -module bevel_gear( - pitch = 3, - teeth = 11, - face_width = 6, - bevelang = 45, - shaft_diam = 3, - hide = 0, - PA = 20, - clearance = undef, - backlash = 0.0, - spiral_rad = 0, - spiral_ang = 0, - slices = 2, - interior = false, - anchor = CENTER, - spin = 0, - orient = UP -) { - thickness = face_width * cos(bevelang); - slices = spiral_rad==0? 1 : slices; - spiral_rad = spiral_rad==0? 10000 : spiral_rad; - p1 = pitch_radius(pitch, teeth); - r1 = root_radius(pitch, teeth, clearance, interior); - c1 = outer_radius(pitch, teeth, clearance, interior); - dx = thickness * tan(bevelang); - dy = (p1-r1) * sin(bevelang); - scl = (p1-dx)/p1; - p2 = pitch_radius(pitch*scl, teeth); - r2 = root_radius(pitch*scl, teeth, clearance, interior); - c2 = outer_radius(pitch*scl, teeth, clearance, interior); - slice_u = 1/slices; - Rm = (p1+p2)/2; - H = spiral_rad * cos(spiral_ang); - V = Rm - abs(spiral_rad) * sin(spiral_ang); - spiral_cp = [H,V,0]; - S = norm(spiral_cp); - theta_r = acos((S*S+spiral_rad*spiral_rad-p1*p1)/(2*S*spiral_rad)) - acos((S*S+spiral_rad*spiral_rad-p2*p2)/(2*S*spiral_rad)); - theta_ro = acos((S*S+spiral_rad*spiral_rad-p1*p1)/(2*S*spiral_rad)) - acos((S*S+spiral_rad*spiral_rad-Rm*Rm)/(2*S*spiral_rad)); - theta_ri = theta_r - theta_ro; - extent_u = 2*(p2-r2)*tan(bevelang) / thickness; - slice_us = concat( - [for (u = [0:slice_u:1+extent_u]) u] - ); - lsus = len(slice_us); - vertices = concat( - [ - for (u=slice_us, tooth=[0:1:teeth-1]) let( - p = lerp(p1,p2,u), - r = lerp(r1,r2,u), - theta = lerp(-theta_ro, theta_ri, u), - profile = gear_tooth_profile( - pitch = pitch*(p/p1), - teeth = teeth, - PA = PA, - clearance = clearance, - backlash = backlash, - interior = interior, - valleys = false - ), - pp = rot(theta, cp=spiral_cp, p=[0,Rm,0]), - ang = atan2(pp.y,pp.x)-90, - pts = apply_list( - path3d(profile), [ - move([0,-p,0]), - rot([0,ang,0]), - rot([bevelang,0,0]), - move(pp), - rot(tooth*360/teeth), - move([0,0,thickness*u]) - ] +// bevel_gear(pitch=5, teeth=36, face_width=10, shaft_diam=5, pitch_angle=45, spiral_angle=0); +// Example: Spiral Beveled Gear and Pinion +// t1 = 16; t2 = 28; +// bevel_gear(pitch=5, teeth=t1, mate_teeth=t2, slices=12, anchor="apex", orient=FWD); +// bevel_gear(pitch=5, teeth=t2, mate_teeth=t1, left_handed=true, slices=12, anchor="apex", spin=180/t2); +function bevel_gear( + pitch = 5, + teeth = 20, + face_width = 10, + pitch_angle = 45, + mate_teeth = undef, + hide = 0, + pressure_angle = 20, + clearance = undef, + backlash = 0.0, + cutter_radius = 30, + spiral_angle = 35, + left_handed = false, + slices = 1, + interior = false, + mod, + anchor = "pitchbase", + spin = 0, + orient = UP +) = + let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), + slices = cutter_radius==0? 1 : slices, + pitch_angle = is_undef(mate_teeth)? pitch_angle : atan(teeth/mate_teeth), + pr = pitch_radius(pitch, teeth), + rr = root_radius(pitch, teeth, clearance, interior), + pitchoff = (pr-rr) * cos(pitch_angle), + ocone_rad = opp_ang_to_hyp(pr, pitch_angle), + icone_rad = ocone_rad - face_width, + cutter_radius = cutter_radius==0? 1000 : cutter_radius, + midpr = (icone_rad + ocone_rad) / 2, + radcp = [0, midpr] + polar_to_xy(cutter_radius, 180+spiral_angle), + angC1 = law_of_cosines(a=cutter_radius, b=norm(radcp), c=ocone_rad), + angC2 = law_of_cosines(a=cutter_radius, b=norm(radcp), c=icone_rad), + radcpang = vang(radcp), + sang = radcpang - (180-angC1), + eang = radcpang - (180-angC2), + slice_us = [for (i=[0:1:slices]) i/slices], + apts = [for (u=slice_us) radcp + polar_to_xy(cutter_radius, lerp(sang,eang,u))], + polars = [for (p=apts) [vang(p)-90, norm(p)]], + profile = gear_tooth_profile( + pitch = pitch, + teeth = teeth, + pressure_angle = pressure_angle, + clearance = clearance, + backlash = backlash, + interior = interior, + valleys = false, + center = true + ), + verts1 = [ + for (polar=polars) [ + let( + u = polar.y / ocone_rad, + m = up((1-u) * pr / tan(pitch_angle)) * + up(pitchoff) * + zrot(polar.x/sin(pitch_angle)) * + back(u * pr) * + xrot(pitch_angle) * + scale(u) ) - ) each pts - ], [ - [0,0,-dy], [0,0,thickness] + for (tooth=[0:1:teeth-1]) + each apply(xflip() * zrot(360*tooth/teeth) * m, path3d(profile)) + ] + ], + thickness = abs(verts1[0][0].z - select(verts1,-1)[0].z), + vertices = [for (x=verts1) down(thickness/2, p=reverse(x))], + sides_vnf = vnf_vertex_array(vertices, caps=false, col_wrap=true, reverse=true), + top_verts = select(vertices,-1), + bot_verts = select(vertices,0), + gear_pts = len(top_verts), + face_pts = gear_pts / teeth, + top_faces =[ + for (i=[0:1:teeth-1], j=[0:1:(face_pts/2)-1]) each [ + [i*face_pts+j, (i+1)*face_pts-j-1, (i+1)*face_pts-j-2], + [i*face_pts+j, (i+1)*face_pts-j-2, i*face_pts+j+1] + ], + for (i=[0:1:teeth-1]) each [ + [gear_pts, (i+1)*face_pts-1, i*face_pts], + [gear_pts, ((i+1)%teeth)*face_pts, (i+1)*face_pts-1] + ] + ], + vnf1 = vnf_merge([ + [ + [each top_verts, [0,0,top_verts[0].z]], + top_faces + ], + [ + [each bot_verts, [0,0,bot_verts[0].z]], + [for (x=top_faces) reverse(x)] + ], + sides_vnf + ]), + vnf = left_handed? vnf1 : xflip(p=vnf1), + anchors = [ + anchorpt("pitchbase", [0,0,pitchoff-thickness/2]), + anchorpt("flattop", [0,0,thickness/2]), + anchorpt("apex", [0,0,hyp_ang_to_opp(ocone_rad,90-pitch_angle)+pitchoff-thickness/2]) ] + ) reorient(anchor,spin,orient, vnf=vnf, extent=true, anchors=anchors, p=vnf); + + +module bevel_gear( + pitch = 3, + teeth = 11, + face_width = 10, + pitch_angle = 45, + mate_teeth, + shaft_diam = 0, + hide = 0, + pressure_angle = 20, + clearance = undef, + backlash = 0.0, + cutter_radius = 30, + spiral_angle = 35, + left_handed = false, + slices = 1, + interior = false, + mod, + anchor = "pitchbase", + spin = 0, + orient = UP +) { + pitch = is_undef(mod) ? pitch : pitch_value(mod); + slices = cutter_radius==0? 1 : slices; + pitch_angle = is_undef(mate_teeth)? pitch_angle : atan(teeth/mate_teeth); + pr = pitch_radius(pitch, teeth); + ipr = pr - face_width*sin(pitch_angle); + rr = root_radius(pitch, teeth, clearance, interior); + pitchoff = (pr-rr) * cos(pitch_angle); + thickness = face_width * cos(pitch_angle); + vnf = bevel_gear( + pitch = pitch, + teeth = teeth, + face_width = face_width, + pitch_angle = pitch_angle, + hide = hide, + pressure_angle = pressure_angle, + clearance = clearance, + backlash = backlash, + cutter_radius = cutter_radius, + spiral_angle = spiral_angle, + left_handed = left_handed, + slices = slices, + interior = interior, + anchor=CENTER ); - lcnt = (len(vertices)-2)/lsus/teeth; - function _gv(layer,tooth,i) = ((layer*teeth)+(tooth%teeth))*lcnt+(i%lcnt); - function _lv(layer,i) = layer*teeth*lcnt+(i%(teeth*lcnt)); - faces = concat( - [ - for (sl=[0:1:lsus-2], i=[0:1:lcnt*teeth-1]) each [ - [_lv(sl,i), _lv(sl+1,i), _lv(sl,i+1)], - [_lv(sl+1,i), _lv(sl+1,i+1), _lv(sl,i+1)] - ] - ], [ - for (tooth=[0:1:teeth-1], i=[0:1:lcnt/2-1]) each [ - [_gv(0,tooth,i), _gv(0,tooth,i+1), _gv(0,tooth,lcnt-1-(i+1))], - [_gv(0,tooth,i), _gv(0,tooth,lcnt-1-(i+1)), _gv(0,tooth,lcnt-1-i)], - [_gv(lsus-1,tooth,i), _gv(lsus-1,tooth,lcnt-1-(i+1)), _gv(lsus-1,tooth,i+1)], - [_gv(lsus-1,tooth,i), _gv(lsus-1,tooth,lcnt-1-i), _gv(lsus-1,tooth,lcnt-1-(i+1))], - ] - ], [ - for (tooth=[0:1:teeth-1]) each [ - [len(vertices)-2, _gv(0,tooth,0), _gv(0,tooth,lcnt-1)], - [len(vertices)-2, _gv(0,tooth,lcnt-1), _gv(0,tooth+1,0)], - [len(vertices)-1, _gv(lsus-1,tooth,lcnt-1), _gv(lsus-1,tooth,0)], - [len(vertices)-1, _gv(lsus-1,tooth+1,0), _gv(lsus-1,tooth,lcnt-1)], - ] - ] - ); - attachable(anchor,spin,orient, r1=p1, r2=p2, l=thickness) { - union() { - difference() { - down(thickness/2) { - polyhedron(points=vertices, faces=faces, convexity=floor(teeth/2)); - } - if (shaft_diam > 0) { - cylinder(h=2*thickness+1, r=shaft_diam/2, center=true, $fn=max(12,segs(shaft_diam/2))); - } - if (bevelang != 0) { - h = (c1-r1)/tan(45); - down(thickness/2+dy) { - difference() { - cube([2*c1/cos(45),2*c1/cos(45),2*h], center=true); - cylinder(h=h, r1=r1-0.5, r2=c1-0.5, center=false, $fn=teeth*4); - } - } - up(thickness/2-0.01) { - cylinder(h=(c2-r2)/tan(45)*5, r1=r2-0.5, r2=lerp(r2-0.5,c2-0.5,5), center=false, $fn=teeth*4); - } - } + anchors = [ + anchorpt("pitchbase", [0,0,pitchoff-thickness/2]), + anchorpt("flattop", [0,0,thickness/2]), + anchorpt("apex", [0,0,adj_ang_to_opp(pr,90-pitch_angle)+pitchoff-thickness/2]) + ]; + attachable(anchor,spin,orient, r1=pr, r2=ipr, h=thickness, anchors=anchors) { + difference() { + vnf_polyhedron(vnf, convexity=teeth); + if (shaft_diam > 0) { + cylinder(h=2*thickness+1, r=shaft_diam/2, center=true, $fn=max(12,segs(shaft_diam/2))); } } children(); @@ -607,20 +940,25 @@ module bevel_gear( } -// Module: rack() -// Usage: -// rack(pitch, teeth, thickness, height, , ); +// Function&Module: rack() +// Usage: As a Module +// rack(pitch|mod, teeth, thickness, height, , ); +// Usage: As a Function +// vnf = rack(pitch|mod, teeth, thickness, height, , ); // Description: -// The module `rack()` gives a rack, which is a bar with teeth. A -// rack can mesh with any gear that has the same `pitch` and -// `PA`. +// This is used to create a 3D rack, which is a linear bar with teeth that a gear can roll along. +// A rack can mesh with any gear that has the same `pitch` and `pressure_angle`. +// When called as a function, returns a 3D [VNF](vnf.scad) for the rack. +// When called as a module, creates a 3D rack shape. // Arguments: -// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. -// teeth = Total number of teeth along the rack -// thickness = Thickness of rack in mm (affects each tooth) -// height = Height of rack in mm, from tooth top to back of rack. -// PA = Controls how straight or bulged the tooth sides are. In degrees. -// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle +// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. Default: 5 +// teeth = Total number of teeth along the rack. Default: 20 +// thickness = Thickness of rack in mm (affects each tooth). Default: 5 +// height = Height of rack in mm, from tooth top to back of rack. Default: 10 +// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 28 +// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle. Default: 0 +// clearance = Clearance gap at the bottom of the inter-tooth valleys. +// mod = The metric module/modulus of the gear. // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -628,63 +966,57 @@ module bevel_gear( // "adendum" = At the tips of the teeth, at the center of rack. // "adendum-left" = At the tips of the teeth, at the left end of the rack. // "adendum-right" = At the tips of the teeth, at the right end of the rack. -// "adendum-top" = At the tips of the teeth, at the top of the rack. -// "adendum-bottom" = At the tips of the teeth, at the bottom of the rack. +// "adendum-back" = At the tips of the teeth, at the back of the rack. +// "adendum-front" = At the tips of the teeth, at the front of the rack. // "dedendum" = At the base of the teeth, at the center of rack. // "dedendum-left" = At the base of the teeth, at the left end of the rack. // "dedendum-right" = At the base of the teeth, at the right end of the rack. -// "dedendum-top" = At the base of the teeth, at the top of the rack. -// "dedendum-bottom" = At the base of the teeth, at the bottom of the rack. +// "dedendum-back" = At the base of the teeth, at the back of the rack. +// "dedendum-front" = At the base of the teeth, at the front of the rack. // Example: -// rack(pitch=5, teeth=10, thickness=5, height=5, PA=20); +// rack(pitch=5, teeth=10, thickness=5, height=5, pressure_angle=20); +// Example: Metric Rack +// rack(mod=2, teeth=10, thickness=5, height=5, pressure_angle=20); module rack( - pitch = 5, - teeth = 20, + pitch = 5, + teeth = 20, thickness = 5, - height = 10, - PA = 28, - backlash = 0.0, - clearance = undef, - anchor = CENTER, - spin = 0, - orient = UP + height = 10, + pressure_angle = 28, + backlash = 0.0, + clearance, + mod, + anchor = CENTER, + spin = 0, + orient = UP ) { + pitch = is_undef(mod) ? pitch : pitch_value(mod); a = adendum(pitch); d = dedendum(pitch, clearance); - xa = a * sin(PA); - xd = d * sin(PA); l = teeth * pitch; anchors = [ - anchorpt("adendum", [0,a,0], BACK), - anchorpt("adendum-left", [-l/2,a,0], LEFT), - anchorpt("adendum-right", [l/2,a,0], RIGHT), - anchorpt("adendum-top", [0,a,thickness/2], UP), - anchorpt("adendum-bottom", [0,a,-thickness/2], DOWN), - anchorpt("dedendum", [0,-d,0], BACK), - anchorpt("dedendum-left", [-l/2,-d,0], LEFT), - anchorpt("dedendum-right", [l/2,-d,0], RIGHT), - anchorpt("dedendum-top", [0,-d,thickness/2], UP), - anchorpt("dedendum-bottom", [0,-d,-thickness/2], DOWN), + anchorpt("adendum", [0,0,a], BACK), + anchorpt("adendum-left", [-l/2,0,a], LEFT), + anchorpt("adendum-right", [ l/2,0,a], RIGHT), + anchorpt("adendum-front", [0,-thickness/2,a], DOWN), + anchorpt("adendum-back", [0, thickness/2,a], UP), + anchorpt("dedendum", [0,0,-d], BACK), + anchorpt("dedendum-left", [-l/2,0,-d], LEFT), + anchorpt("dedendum-right", [ l/2,0,-d], RIGHT), + anchorpt("dedendum-front", [0,-thickness/2,-d], DOWN), + anchorpt("dedendum-back", [0, thickness/2,-d], UP), ]; - attachable(anchor,spin,orient, size=[l, 2*abs(a-height), thickness], anchors=anchors) { - left((teeth-1)*pitch/2) { - linear_extrude(height = thickness, center = true, convexity = 10) { - for (i = [0:1:teeth-1] ) { - translate([i*pitch,0,0]) { - polygon( - points=[ - [-1/2 * pitch - 0.01, a-height], - [-1/2 * pitch, -d], - [-1/4 * pitch + backlash - xd, -d], - [-1/4 * pitch + backlash + xa, a], - [ 1/4 * pitch - backlash - xa, a], - [ 1/4 * pitch - backlash + xd, -d], - [ 1/2 * pitch, -d], - [ 1/2 * pitch + 0.01, a-height], - ] - ); - } - } + attachable(anchor,spin,orient, size=[l, thickness, 2*abs(a-height)], anchors=anchors) { + xrot(90) { + linear_extrude(height=thickness, center=true, convexity=teeth*2) { + rack2d( + pitch = pitch, + teeth = teeth, + height = height, + pressure_angle = pressure_angle, + backlash = backlash, + clearance = clearance + ); } } children(); @@ -692,6 +1024,323 @@ module rack( } +function rack( + pitch = 5, + teeth = 20, + thickness = 5, + height = 10, + pressure_angle = 28, + backlash = 0.0, + clearance, + mod, + anchor = CENTER, + spin = 0, + orient = UP +) = + let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), + a = adendum(pitch), + d = dedendum(pitch, clearance), + l = teeth * pitch, + anchors = [ + anchorpt("adendum", [0,0,a], BACK), + anchorpt("adendum-left", [-l/2,0,a], LEFT), + anchorpt("adendum-right", [ l/2,0,a], RIGHT), + anchorpt("adendum-front", [0,-thickness/2,a], DOWN), + anchorpt("adendum-back", [0, thickness/2,a], UP), + anchorpt("dedendum", [0,0,-d], BACK), + anchorpt("dedendum-left", [-l/2,0,-d], LEFT), + anchorpt("dedendum-right", [ l/2,0,-d], RIGHT), + anchorpt("dedendum-front", [0,-thickness/2,-d], DOWN), + anchorpt("dedendum-back", [0, thickness/2,-d], UP), + ], + path = rack2d( + pitch = pitch, + teeth = teeth, + height = height, + pressure_angle = pressure_angle, + backlash = backlash, + clearance = clearance + ), + vnf = linear_sweep(path, height=thickness, anchor="origin", orient=FWD) + ) reorient(anchor,spin,orient, size=[l, thickness, 2*abs(a-height)], anchors=anchors, p=vnf); + + + +// Function&Module: worm() +// Usage: As a Module +// worm(pitch|mod, d, l, , , , , ); +// Usage: As a Function +// vnf = worm(pitch|mod, d, l, , , , , ); +// Description: +// Creates a worm shape that can be matched to a work gear. +// Arguments: +// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. Default: 5 +// d = The diameter of the worm. Default: 30 +// l = The length of the worm. Default: 100 +// starts = The number of lead starts. Default: 1 +// left_handed = If true, the gear returned will have a left-handed spiral. Default: false +// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 20 +// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle. Default: 0 +// clearance = Clearance gap at the bottom of the inter-tooth valleys. +// mod = The metric module/modulus of the gear. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Example: +// worm(pitch=8, d=30, l=50, $fn=72); +// Example: Multiple Starts. +// worm(pitch=8, d=30, l=50, starts=3, $fn=72); +// Example: Left Handed +// worm(pitch=8, d=30, l=50, starts=3, left_handed=true, $fn=72); +// Example: Called as Function +// vnf = worm(pitch=8, d=35, l=50, starts=2, left_handed=true, pressure_angle=20, $fn=72); +// vnf_polyhedron(vnf); +function worm( + pitch=5, + d=30, l=100, + starts=1, + left_handed=false, + pressure_angle=20, + backlash=0, + clearance, + mod, + anchor=CENTER, + spin=0, + orient=UP +) = + let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), + rack_profile = select(rack2d( + pitch = pitch, + teeth = starts, + height = d, + pressure_angle = pressure_angle, + backlash = backlash, + clearance = clearance + ), 1, -2), + polars = [ + for (i=idx(rack_profile)) let( + p = rack_profile[i], + a = 360 * p.x / pitch / starts + ) [a, p.y + d/2] + ], + maxang = 360 / segs(d/2), + refined_polars = [ + for (i=idx(polars,end=-2)) let( + delta = polars[i+1].x - polars[i].x, + steps = ceil(delta/maxang), + step = delta/steps + ) for (j = [0:1:steps-1]) + [polars[i].x + j*step, lerp(polars[i].y,polars[i+1].y, j/steps)] + ], + cross_sect = [ for (p = refined_polars) polar_to_xy(p.y, p.x) ], + revs = l/pitch/starts, + zsteps = ceil(revs*360/maxang), + zstep = l/zsteps, + astep = revs*360/zsteps, + profiles = [ + for (i=[0:1:zsteps]) let( + z = i*zstep - l/2, + a = i*astep - 360*revs/2 + ) + apply(zrot(a)*up(z), path3d(cross_sect)) + ], + vnf1 = vnf_vertex_array(profiles, caps=true, col_wrap=true, reverse=true, style="alt"), + vnf = left_handed? xflip(p=vnf1) : vnf1 + ) reorient(anchor,spin,orient, d=d, l=l, p=vnf); + + +module worm( + pitch=5, + d=15, l=100, + starts=1, + left_handed=false, + pressure_angle=20, + backlash=0, + clearance, + mod, + anchor=CENTER, + spin=0, + orient=UP +) { + vnf = worm( + pitch=pitch, + starts=starts, + d=d, l=l, + left_handed=left_handed, + pressure_angle=pressure_angle, + backlash=backlash, + clearance=clearance, + mod=mod + ); + attachable(anchor,spin,orient, d=d, l=l) { + vnf_polyhedron(vnf); + children(); + } +} + + +// Function&Module: worm_gear() +// Usage: As a Module +// worm_gear(pitch|mod, teeth, worm_diam, , , , , , , , ); +// Usage: As a Function +// vnf = worm_gear(pitch|mod, teeth, worm_diam, , , , , , , ); +// Description: +// Creates a worm gear to match with a worm. +// Arguments: +// pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. Default: 5 +// teeth = Total number of teeth along the rack. Default: 30 +// worm_diam = The pitch diameter of the worm gear to match to. Default: 30 +// worm_starts = The number of lead starts on the worm gear to match to. Default: 1 +// worm_arc = The arc of the worm to mate with, in degrees. Default: 60 degrees +// crowning = The amount to oversize the virtual hobbing cutter used to make the teeth, to add a slight crowning to the teeth to make them fir the work easier. Default: 1 +// left_handed = If true, the gear returned will have a left-handed spiral. Default: false +// pressure_angle = Controls how straight or bulged the tooth sides are. In degrees. Default: 20 +// backlash = Gap between two meshing teeth, in the direction along the circumference of the pitch circle. Default: 0 +// clearance = Clearance gap at the bottom of the inter-tooth valleys. +// slices = The number of vertical slices to refine the curve of the worm throat. Default: 10 +// mod = The metric module/modulus of the gear. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Example: Right-Handed +// worm_gear(pitch=5, teeth=36, worm_diam=30, worm_starts=1); +// Example: Left-Handed +// worm_gear(pitch=5, teeth=36, worm_diam=30, worm_starts=1, left_handed=true); +// Example: Multiple Starts +// worm_gear(pitch=5, teeth=36, worm_diam=30, worm_starts=4); +// Example: Metric Worm Gear +// worm_gear(mod=25, teeth=32, worm_diam=30, worm_starts=1); +// Example: Called as Function +// vnf = worm_gear(pitch=8, teeth=30, worm_diam=30, worm_starts=1); +// vnf_polyhedron(vnf); +function worm_gear( + pitch = 5, + teeth = 36, + worm_diam = 30, + worm_starts = 1, + worm_arc = 60, + crowning = 1, + left_handed = false, + pressure_angle = 20, + backlash = 0, + clearance, + mod, + slices = 10, + anchor = CENTER, + spin = 0, + orient = UP +) = + assert(worm_arc >= 10 && worm_arc <= 60) + let( + pitch = is_undef(mod) ? pitch : pitch_value(mod), + p = pitch_radius(pitch, teeth), + circ = 2 * PI * p, + r1 = p + worm_diam/2 + crowning, + r2 = worm_diam/2 + crowning, + thickness = worm_gear_thickness(pitch=pitch, teeth=teeth, worm_diam=worm_diam, worm_arc=worm_arc, crowning=crowning, clearance=clearance), + helical = pitch * worm_starts * worm_arc / 360 * 360 / circ, + tooth_profile = reverse(gear_tooth_profile( + pitch = pitch, + teeth = teeth, + pressure_angle = pressure_angle, + clearance = clearance, + backlash = backlash, + valleys = false, + center = true + )), + profiles = [ + for (slice = [0:1:slices]) let( + u = slice/slices - 0.5, + zang = u * worm_arc, + tp = [0,r1,0] - spherical_to_xyz(r2, 90, 90+zang), + zang2 = u * helical + ) [ + for (i = [0:1:teeth]) each + apply( + zrot(-i*360/teeth+zang2) * + move(tp) * + xrot(-zang) * + scale(cos(zang)), + path3d(tooth_profile) + ) + ] + ], + top_verts = select(profiles,-1), + bot_verts = select(profiles,0), + face_pts = len(tooth_profile), + gear_pts = face_pts * teeth, + top_faces =[ + for (i=[0:1:teeth-1], j=[0:1:(face_pts/2)-1]) each [ + [i*face_pts+j, (i+1)*face_pts-j-1, (i+1)*face_pts-j-2], + [i*face_pts+j, (i+1)*face_pts-j-2, i*face_pts+j+1] + ], + for (i=[0:1:teeth-1]) each [ + [gear_pts, (i+1)*face_pts-1, i*face_pts], + [gear_pts, ((i+1)%teeth)*face_pts, (i+1)*face_pts-1] + ] + ], + sides_vnf = vnf_vertex_array(profiles, caps=false, col_wrap=true, style="quincunx"), + vnf1 = vnf_merge([ + [ + [each top_verts, [0,0,top_verts[0].z]], + [for (x=top_faces) reverse(x)] + ], + [ + [each bot_verts, [0,0,bot_verts[0].z]], + top_faces + ], + sides_vnf + ]), + vnf = left_handed? xflip(p=vnf1) : vnf1 + ) reorient(anchor,spin,orient, r=p, l=thickness, p=vnf); + + +module worm_gear( + pitch = 5, + teeth = 36, + worm_diam = 30, + worm_starts = 1, + worm_arc = 60, + crowning = 1, + left_handed = false, + pressure_angle = 20, + backlash = 0, + slices = 10, + clearance, + mod, + shaft_diam = 0, + anchor = CENTER, + spin = 0, + orient = UP +) { + pitch = is_undef(mod) ? pitch : pitch_value(mod); + p = pitch_radius(pitch, teeth); + vnf = worm_gear( + pitch = pitch, + teeth = teeth, + worm_diam = worm_diam, + worm_starts = worm_starts, + worm_arc = worm_arc, + crowning = crowning, + left_handed = left_handed, + pressure_angle = pressure_angle, + backlash = backlash, + slices = slices, + clearance = clearance + ); + thickness = pointlist_bounds(vnf[0])[1].z; + attachable(anchor,spin,orient, r=p, l=thickness) { + difference() { + vnf_polyhedron(vnf); + if (shaft_diam > 0) { + cylinder(d=shaft_diam, l=worm_diam, center=true); + } + } + children(); + } +} + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap - diff --git a/math.scad b/math.scad index 0c847b8..c0ac6ab 100644 --- a/math.scad +++ b/math.scad @@ -904,6 +904,16 @@ function norm_fro(A) = norm(flatten(A)); +// Function: matrix_trace() +// Usage: +// matrix_trace(M) +// Description: +// Computes the trace of a square matrix, the sum of the entries on the diagonal. +function matrix_trace(M) = + assert(is_matrix(M,square=true), "Input to trace must be a square matrix") + [for(i=[0:1:len(M)-1])1] * [for(i=[0:1:len(M)-1]) M[i][i]]; + + // Section: Comparisons and Logic // Function: all_zero() diff --git a/primitives.scad b/primitives.scad index 60f6e5a..8386094 100644 --- a/primitives.scad +++ b/primitives.scad @@ -238,7 +238,7 @@ function cylinder(h, r1, r2, center, l, r, d, d1, d2, anchor, spin=0, orient=UP) // r = Radius of the sphere. // d = Diameter of the sphere. // circum = If true, the sphere is made large enough to circumscribe the sphere of the ideal side. Otherwise inscribes. Default: false (inscribes) -// style = The style of the sphere's construction. One of "orig", "aligned", "stagger", or "icosa". Default: "orig" +// style = The style of the sphere's construction. One of "orig", "aligned", "stagger", "octa", or "icosa". Default: "orig" // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` // orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` @@ -267,11 +267,11 @@ function cylinder(h, r1, r2, center, l, r, d, d1, d2, anchor, spin=0, orient=UP) // Example: Called as Function // vnf = sphere(d=100, style="icosa"); // vnf_polyhedron(vnf); -module sphere(r, d, circum=false, style="aligned", anchor=CENTER, spin=0, orient=UP) +module sphere(r, d, circum=false, style="orig", anchor=CENTER, spin=0, orient=UP) spheroid(r=r, d=d, circum=circum, style=style, anchor=anchor, spin=spin, orient=orient) children(); -function sphere(r, d, circum=false, style="aligned", anchor=CENTER, spin=0, orient=UP) = +function sphere(r, d, circum=false, style="orig", anchor=CENTER, spin=0, orient=UP) = spheroid(r=r, d=d, circum=circum, style=style, anchor=anchor, spin=spin, orient=orient); diff --git a/regions.scad b/regions.scad index d6832e2..b55a4f1 100644 --- a/regions.scad +++ b/regions.scad @@ -296,7 +296,7 @@ function region_faces(region, transform, reverse=false, vnf=EMPTY_VNF) = // Function&Module: linear_sweep() // Usage: -// linear_sweep(path, height, [center], [slices], [twist], [scale], [style], [convexity]); +// linear_sweep(region, height, [center], [slices], [twist], [scale], [style], [convexity]); // Description: // If called as a module, creates a polyhedron that is the linear extrusion of the given 2D region or path. // If called as a function, returns a VNF that can be used to generate a polyhedron of the linear extrusion @@ -304,19 +304,19 @@ function region_faces(region, transform, reverse=false, vnf=EMPTY_VNF) = // that you can use `anchor`, `spin`, `orient` and attachments with it. Also, you can make more refined // twisted extrusions by using `maxseg` to subsample flat faces. // Arguments: -// region = The 2D [Region](regions.scad) that is to be extruded. -// height = The height to extrude the path. Default: 1 +// region = The 2D [Region](regions.scad) or path that is to be extruded. +// height = The height to extrude the region. Default: 1 // center = If true, the created polyhedron will be vertically centered. If false, it will be extruded upwards from the origin. Default: `false` // slices = The number of slices to divide the shape into along the Z axis, to allow refinement of detail, especially when working with a twist. Default: `twist/5` // maxseg = If given, then any long segments of the region will be subdivided to be shorter than this length. This can refine twisting flat faces a lot. Default: `undef` (no subsampling) // twist = The number of degrees to rotate the shape clockwise around the Z axis, as it rises from bottom to top. Default: 0 -// scale = The amound to scale the shape, from bottom to top. Default: 1 +// scale = The amount to scale the shape, from bottom to top. Default: 1 // style = The style to use when triangulating the surface of the object. Valid values are `"default"`, `"alt"`, or `"quincunx"`. -// convexity = Max number of surfaces any single ray could pass through. +// convexity = Max number of surfaces any single ray could pass through. Module use only. // anchor_isect = If true, anchoring it performed by finding where the anchor vector intersects the swept shape. Default: false -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Module use only. Default: `CENTER` -// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Module use only. Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Module use only. Default: `UP` +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` // Example: Extruding a Compound Region. // rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)]; // rgn2 = [square(30,center=false)]; @@ -355,9 +355,11 @@ module linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg, } -function linear_sweep(region, height=1, twist=0, scale=1, slices, maxseg, style="default") = +function linear_sweep(region, height=1, center, twist=0, scale=1, slices, maxseg, style="default", anchor_isect=false, anchor, spin=0, orient=UP) = let( + anchor = get_anchor(anchor,center,BOT,BOT), region = is_path(region)? [region] : region, + cp = mean(pointlist_bounds(flatten(region))), regions = split_nested_region(region), slices = default(slices, floor(twist/5+1)), step = twist/slices, @@ -374,27 +376,28 @@ function linear_sweep(region, height=1, twist=0, scale=1, slices, maxseg, style= ) rot(twist, p=scale([scale,scale],p=path)) ] - ] - ) vnf_merge([ - for (rgn = regions) - for (pathnum = idx(rgn)) let( - p = cleanup_path(rgn[pathnum]), - path = is_undef(maxseg)? p : [ - for (seg=pair_wrap(p)) each - let(steps=ceil(norm(seg.y-seg.x)/maxseg)) - lerp(seg.x, seg.y, [0:1/steps:1-EPSILON]) - ], - verts = [ - for (i=[0:1:slices]) let( - sc = lerp(1, scale, i/slices), - ang = i * step, - h = i * hstep - height/2 - ) scale([sc,sc,1], p=rot(ang, p=path3d(path,h))) - ] - ) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style), - for (rgn = regions) region_faces(rgn, move([0,0,-height/2]), reverse=true), - for (rgn = trgns) region_faces(rgn, move([0,0, height/2]), reverse=false) - ]); + ], + vnf = vnf_merge([ + for (rgn = regions) + for (pathnum = idx(rgn)) let( + p = cleanup_path(rgn[pathnum]), + path = is_undef(maxseg)? p : [ + for (seg=pair_wrap(p)) each + let(steps=ceil(norm(seg.y-seg.x)/maxseg)) + lerp(seg.x, seg.y, [0:1/steps:1-EPSILON]) + ], + verts = [ + for (i=[0:1:slices]) let( + sc = lerp(1, scale, i/slices), + ang = i * step, + h = i * hstep - height/2 + ) scale([sc,sc,1], p=rot(ang, p=path3d(path,h))) + ] + ) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style), + for (rgn = regions) region_faces(rgn, move([0,0,-height/2]), reverse=true), + for (rgn = trgns) region_faces(rgn, move([0,0, height/2]), reverse=false) + ]) + ) reorient(anchor,spin,orient, cp=cp, vnf=vnf, extent=!anchor_isect, p=vnf); diff --git a/scripts/docs_gen.py b/scripts/docs_gen.py index 044fd64..d8849a9 100755 --- a/scripts/docs_gen.py +++ b/scripts/docs_gen.py @@ -294,11 +294,11 @@ def mkdn_esc(txt): while txt: m = quotpat.match(txt) if m: - out += m.group(1).replace(r'_', r'\_') + out += m.group(1).replace(r'_', r'\_').replace(r'&',r'&').replace(r'<', r'<').replace(r'>',r'>') out += m.group(2) txt = m.group(3) else: - out += txt.replace(r'_', r'\_') + out += txt.replace(r'_', r'\_').replace(r'&',r'&').replace(r'<', r'<').replace(r'>',r'>') txt = "" return out diff --git a/scripts/linecount.sh b/scripts/linecount.sh new file mode 100755 index 0000000..234d8cd --- /dev/null +++ b/scripts/linecount.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +lib_comment_lines=$(grep '^// ' *.scad | wc -l) +lib_code_lines=$(grep '^ *[^ /]' *.scad | wc -l) +script_code_lines=$(grep '^ *[^ /]' scripts/*.sh scripts/*.py | wc -l) +example_code_lines=$(grep '^ *[^ /]' examples/*.scad | wc -l) +test_code_lines=$(grep '^ *[^ /]' tests/*.scad | wc -l) +tutorial_lines=$(grep '^ *[^ /]' tutorials/*.md | wc -l) + +y=$(printf "%06d" 13) + +printf "Documentation Lines : %6d\n" $(($lib_comment_lines+$tutorial_lines)) +printf "Example Code Lines : %6d\n" $example_code_lines +printf "Library Code Lines : %6d\n" $lib_code_lines +printf "Support Script Lines: %6d\n" $script_code_lines +printf "Test Code Lines : %6d\n" $test_code_lines + diff --git a/shapes.scad b/shapes.scad index b13ca2b..054053f 100644 --- a/shapes.scad +++ b/shapes.scad @@ -77,29 +77,30 @@ module cuboid( e = corner_edges(edges, corner); cnt = sum(e); r = first_defined([chamfer, rounding, 0]); + c = [min(r,size.x/2), min(r,size.y/2), min(r,size.z/2)]; $fn = is_finite(chamfer)? 4 : segs(r); - translate(vmul(corner,size/2-[r,r,r])) { + translate(vmul(corner,size/2-c)) { if (cnt == 0) { - cube(r*2, center=true); + cube(c*2, center=true); } else if (cnt == 1) { - if (e.x) xcyl(l=r*2, r=r); - if (e.y) ycyl(l=r*2, r=r); - if (e.z) zcyl(l=r*2, r=r); + if (e.x) xcyl(l=c.x*2, r=r); + if (e.y) ycyl(l=c.y*2, r=r); + if (e.z) zcyl(l=c.z*2, r=r); } else if (cnt == 2) { if (!e.x) { intersection() { - ycyl(l=r*2, r=r); - zcyl(l=r*2, r=r); + ycyl(l=c.y*2, r=r); + zcyl(l=c.z*2, r=r); } } else if (!e.y) { intersection() { - xcyl(l=r*2, r=r); - zcyl(l=r*2, r=r); + xcyl(l=c.x*2, r=r); + zcyl(l=c.z*2, r=r); } } else { intersection() { - xcyl(l=r*2, r=r); - ycyl(l=r*2, r=r); + xcyl(l=c.x*2, r=r); + ycyl(l=c.y*2, r=r); } } } else { @@ -107,9 +108,9 @@ module cuboid( spheroid(r=r, style="octa"); } else { intersection() { - xcyl(l=r*2, r=r); - ycyl(l=r*2, r=r); - zcyl(l=r*2, r=r); + xcyl(l=c.x*2, r=r); + ycyl(l=c.y*2, r=r); + zcyl(l=c.z*2, r=r); } } } @@ -1181,14 +1182,14 @@ module spheroid(r, d, circum=false, style="aligned", anchor=CENTER, spin=0, orie if (style=="orig") { rotate_extrude(convexity=2,$fn=sides) { difference() { - oval(r=r, circum=circum, $fn=sides); + oval(r=r, circum=circum, realign=true, $fn=sides); left(r) square(2*r,center=true); } } } else if (style=="aligned") { rotate_extrude(convexity=2,$fn=sides) { difference() { - zrot(180/sides) oval(r=r, circum=circum, $fn=sides); + oval(r=r, circum=circum, $fn=sides); left(r) square(2*r,center=true); } } diff --git a/shapes2d.scad b/shapes2d.scad index 8906650..6b5a6dd 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -269,18 +269,18 @@ module stroke( if (hull) { hull(){ multmatrix(rotmats[i]) { - sphere(d=widths[i]); + sphere(d=widths[i],style="aligned"); } multmatrix(rotmats[i-1]) { - sphere(d=widths[i]); + sphere(d=widths[i],style="aligned"); } } } else { multmatrix(rotmats[i]) { - sphere(d=widths[i]); + sphere(d=widths[i],style="aligned"); } multmatrix(rotmats[i-1]) { - sphere(d=widths[i]); + sphere(d=widths[i],style="aligned"); } } } diff --git a/skin.scad b/skin.scad index d893822..37b5c51 100644 --- a/skin.scad +++ b/skin.scad @@ -298,6 +298,32 @@ include // for (ang = [0:5:360]) // rot([0,ang,0], cp=[100,0,0], p=rot(ang/2, p=path3d(square([1,30],center=true)))) // ], caps=false, slices=0, refine=20); +// Example: This model of two scutoids packed together is based on https://www.thingiverse.com/thing:3024272 by mathgrrl +// sidelen = 10; // Side length of scutoid +// height = 25; // Height of scutoid +// angle = -15; // Angle (twists the entire form) +// push = -5; // Push (translates the base away from the top) +// flare = 1; // Flare (the two pieces will be different unless this is 1) +// midpoint = .5; // Height of the extra vertex (as a fraction of total height); the two pieces will be different unless this is .5) +// pushvec = rot(angle/2,p=push*RIGHT); // Push direction is the the average of the top and bottom mating edges +// pent = path3d(apply(move(pushvec)*rot(angle),pentagon(side=sidelen,align_side=RIGHT,anchor="side0"))); +// hex = path3d(hexagon(side=flare*sidelen, align_side=RIGHT, anchor="side0"),height); +// pentmate = path3d(pentagon(side=flare*sidelen,align_side=LEFT,anchor="side0"),height); +// // Native index would require mapping first and last vertices together, which is not allowed, so shift +// hexmate = polygon_shift( +// path3d(apply(move(pushvec)*rot(angle),hexagon(side=sidelen,align_side=LEFT,anchor="side0"))), +// -1); +// join_vertex = lerp( +// mean(select(hex,1,2)), // midpoint of "extra" hex edge +// mean(select(hexmate,0,1)), // midpoint of "extra" hexmate edge +// midpoint); +// augpent = repeat_entries(pent, [1,2,1,1,1]); // Vertex 1 will split at the top forming a triangular face with the hexagon +// augpent_mate = repeat_entries(pentmate,[2,1,1,1,1]); // For mating pentagon it is vertex 0 that splits +// // Middle is the interpolation between top and bottom except for the join vertex, which is doubled because it splits +// middle = list_set(lerp(augpent,hex,midpoint),[1,2],[join_vertex,join_vertex]); +// middle_mate = list_set(lerp(hexmate,augpent_mate,midpoint), [0,1], [join_vertex,join_vertex]); +// skin([augpent,middle,hex], slices=10, refine=10, sampling="segment"); +// color("green")skin([augpent_mate,middle_mate,hexmate], slices=10,refine=10, sampling="segment"); // Example: If you create a self-intersecting polyhedron the result is invalid. In some cases self-intersection may be obvous. Here is a more subtle example. // skin([ // for (a = [0:30:180]) let( diff --git a/tests/test_affine.scad b/tests/test_affine.scad index a759b56..b9a4e49 100644 --- a/tests/test_affine.scad +++ b/tests/test_affine.scad @@ -252,5 +252,34 @@ module test_apply_list() { test_apply_list(); +module test_rot_decode() { + Tlist = [ + rot(37), + xrot(49), + yrot(88), + rot(37,v=[1,3,3]), + rot(41,v=[2,-3,4]), + rot(180), + xrot(180), + yrot(180), + rot(180, v=[3,2,-5], cp=[3,5,18]), + rot(0.1, v=[1,2,3]), + rot(-47,v=[3,4,5],cp=[9,3,4]), + rot(197,v=[13,4,5],cp=[9,-3,4]), + move([3,4,5]), + move([3,4,5]) * rot(a=56, v=[5,3,-3], cp=[2,3,4]), + ident(4) + ]; + errlist = [for(T = Tlist) + let( + parm = rot_decode(T), + restore = move(parm[3])*rot(a=parm[0],v=parm[1],cp=parm[2]) + ) + norm_fro(restore-T)]; + assert(max(errlist)<1e-13); +} +test_rot_decode(); + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/tests/test_math.scad b/tests/test_math.scad index 9fb7c65..824098a 100644 --- a/tests/test_math.scad +++ b/tests/test_math.scad @@ -550,6 +550,13 @@ module test_determinant() { test_determinant(); +module test_matrix_trace() { + M = [ [6,4,-2,9], [1,-2,8,3], [1,5,7,6], [4,2,5,1] ]; + assert_equal(matrix_trace(M), 6-2+7+1); +} +test_matrix_trace(); + + // Logic diff --git a/tests/test_primitives.scad b/tests/test_primitives.scad index e903617..e8671be 100644 --- a/tests/test_primitives.scad +++ b/tests/test_primitives.scad @@ -51,7 +51,7 @@ test_cylinder(); module test_sphere() { $fn=6; - assert_approx(sphere(r=40), [[[0,0,40],[34.6410161514,0,20],[17.3205080757,30,20],[-17.3205080757,30,20],[-34.6410161514,0,20],[-17.3205080757,-30,20],[17.3205080757,-30,20],[34.6410161514,0,-20],[17.3205080757,30,-20],[-17.3205080757,30,-20],[-34.6410161514,0,-20],[-17.3205080757,-30,-20],[17.3205080757,-30,-20],[0,0,-40]],[[1,0,2],[13,7,8],[2,0,3],[13,8,9],[3,0,4],[13,9,10],[4,0,5],[13,10,11],[5,0,6],[13,11,12],[6,0,1],[13,12,7],[1,2,8],[1,8,7],[2,3,9],[2,9,8],[3,4,10],[3,10,9],[4,5,11],[4,11,10],[5,6,12],[5,12,11],[6,1,7],[6,7,12]]]); + assert_approx(sphere(r=40), [[[20,0,34.6410161514],[10,17.3205080757,34.6410161514],[-10,17.3205080757,34.6410161514],[-20,0,34.6410161514],[-10,-17.3205080757,34.6410161514],[10,-17.3205080757,34.6410161514],[40,0,0],[20,34.6410161514,0],[-20,34.6410161514,0],[-40,0,0],[-20,-34.6410161514,0],[20,-34.6410161514,0],[20,0,-34.6410161514],[10,17.3205080757,-34.6410161514],[-10,17.3205080757,-34.6410161514],[-20,0,-34.6410161514],[-10,-17.3205080757,-34.6410161514],[10,-17.3205080757,-34.6410161514]],[[5,4,3,2,1,0],[12,13,14,15,16,17],[6,0,1],[6,1,7],[7,1,2],[7,2,8],[8,2,3],[8,3,9],[9,3,4],[9,4,10],[10,4,5],[10,5,11],[11,5,0],[11,0,6],[12,6,7],[12,7,13],[13,7,8],[13,8,14],[14,8,9],[14,9,15],[15,9,10],[15,10,16],[16,10,11],[16,11,17],[17,11,6],[17,6,12]]]); assert_approx(sphere(r=40,style="orig"), [[[20,0,34.6410161514],[10,17.3205080757,34.6410161514],[-10,17.3205080757,34.6410161514],[-20,0,34.6410161514],[-10,-17.3205080757,34.6410161514],[10,-17.3205080757,34.6410161514],[40,0,0],[20,34.6410161514,0],[-20,34.6410161514,0],[-40,0,0],[-20,-34.6410161514,0],[20,-34.6410161514,0],[20,0,-34.6410161514],[10,17.3205080757,-34.6410161514],[-10,17.3205080757,-34.6410161514],[-20,0,-34.6410161514],[-10,-17.3205080757,-34.6410161514],[10,-17.3205080757,-34.6410161514]],[[5,4,3,2,1,0],[12,13,14,15,16,17],[6,0,1],[6,1,7],[7,1,2],[7,2,8],[8,2,3],[8,3,9],[9,3,4],[9,4,10],[10,4,5],[10,5,11],[11,5,0],[11,0,6],[12,6,7],[12,7,13],[13,7,8],[13,8,14],[14,8,9],[14,9,15],[15,9,10],[15,10,16],[16,10,11],[16,11,17],[17,11,6],[17,6,12]]]); assert_approx(sphere(r=40,style="aligned"), [[[0,0,40],[34.6410161514,0,20],[17.3205080757,30,20],[-17.3205080757,30,20],[-34.6410161514,0,20],[-17.3205080757,-30,20],[17.3205080757,-30,20],[34.6410161514,0,-20],[17.3205080757,30,-20],[-17.3205080757,30,-20],[-34.6410161514,0,-20],[-17.3205080757,-30,-20],[17.3205080757,-30,-20],[0,0,-40]],[[1,0,2],[13,7,8],[2,0,3],[13,8,9],[3,0,4],[13,9,10],[4,0,5],[13,10,11],[5,0,6],[13,11,12],[6,0,1],[13,12,7],[1,2,8],[1,8,7],[2,3,9],[2,9,8],[3,4,10],[3,10,9],[4,5,11],[4,11,10],[5,6,12],[5,12,11],[6,1,7],[6,7,12]]]); assert_approx(sphere(r=40,style="stagger"), [[[0,0,40],[30,17.3205080757,20],[0,34.6410161514,20],[-30,17.3205080757,20],[-30,-17.3205080757,20],[0,-34.6410161514,20],[30,-17.3205080757,20],[34.6410161514,0,-20],[17.3205080757,30,-20],[-17.3205080757,30,-20],[-34.6410161514,0,-20],[-17.3205080757,-30,-20],[17.3205080757,-30,-20],[0,0,-40]],[[1,0,2],[13,7,8],[2,0,3],[13,8,9],[3,0,4],[13,9,10],[4,0,5],[13,10,11],[5,0,6],[13,11,12],[6,0,1],[13,12,7],[1,2,8],[1,8,7],[2,3,9],[2,9,8],[3,4,10],[3,10,9],[4,5,11],[4,11,10],[5,6,12],[5,12,11],[6,1,7],[6,7,12]]]); diff --git a/version.scad b/version.scad index 89757d8..b3a3b8b 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,446]; +BOSL_VERSION = [2,0,459]; // Section: BOSL Library Version Functions