[User]
Hey, I'm looking to create a parametric OpenSCAD design for a clamp, and I came across this really cool example called the "Falcon Clamp" that uses a compound planetary gearbox. I'd like to be able to customize all sorts of things about it.

Specifically, it should use involute gears and the design should include a couple of planetary gear stages stacked vertically, with sun gears that are solid/fused. I'd need to control the number of planetary gear layers (probably 2 or 3), the heights of these layers (both the drive and output stages), the number of teeth on the planet gears for both stages, the number of teeth on the ring gears for both stages, and a multiplier for the sun gears which effectively scales them. There should be a main shaft diameter parameter, and secondary shafts (for larger sun gears) that I can enable or disable.

The outer diameter of the whole thing should be configurable and also the number of outer teeth and their width should be variable. I need to be able to control the ring wall thickness relative to the pitch radius.

Importantly, I need to control how many planet gears there are in each stage. Also, I need to be able to twist the gear teeth (a helix angle) a certain amount across the height of each layer.

I also need control over the gear depth ratio, the clearance between gears, and the pressure angle. There should be a parameter for the layer height (for ring horizontal splits). And finally, I need the ability to chamfer exposed gears on the top and bottom to help with printing and fingers.

I also want to include jaws. I'd like to be able to control the number of jaw sets, their initial rotation, size, offset, and a taper angle on the outside edge of the jaw. I need to also control the dimple radius and depth ratio on the jaws.

Oh, and then there's the knob. It needs to be fully customizable: the overall diameter, the total height, the number of points (like on a star knob), and the finger hole diameter. Also, the finger points should be optionally tapered. Plus, I want the ability to include nut captures at the top or bottom of the knob stem, so I can use nuts for strength or a bolt.

I understand there needs to be a resolution setting for curves too (minimum angle and size), and the design should be print-in-place. I think the original used MCAD's `involute_gears.scad` library, so please incorporate that. Could you generate the OpenSCAD files for me with all of this functionality?

[Assistant]
FalconClamp.scad:
```
// OpenSCAD Compound Planetary System
// (c) 2019, tmackay
//
// Licensed under a Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license, http://creativecommons.org/licenses/by-sa/4.0.
//include <MCAD/involute_gears.scad>

// Modules, planetary layers
modules = 3; //[2:1:3]
// Height of planetary layers (drive stage)
gear_h = 7.5; //[5:0.5:100]
// Height of planetary layers (output stage)
gear_h2 = 15; //[5:0.5:200]
// Number of teeth in inner planet gears
planet_t = 10; //[10:1:20]
planet_t2 = 14; //[10:1:20]
// Number of teeth in ring gears
ring_t1 = 30; //[30:1:90]
ring_t2 = 41; //[30:1:90]
// Sun gear multiplier
sgm = 1; //[1:1:5]
// Shaft diameter
shaft_d = 8; //[0:0.1:25]
// secondary shafts (for larger sun gears)
shafts = 6; //[0:1:12]
// Outer diameter
outer_d = 60; //[30:300]
// Outer teeth
outer_t = 0; //[0:1:24]
// Width of outer teeth
outer_w=3; //[0:0.1:10]
// Ring wall thickness (relative pitch radius)
wall = 5; //[0:0.1:20]
// Number of planet gears in inner circle
planets = 5; //[3:1:21]
// number of teeth to twist across
nTwist=1; //[0:0.5:5]
// Gear depth ratio
depth_ratio=0.5; //[0:0.05:1]
// Gear clearance
tol=0.2; //[0:0.01:0.5]
// pressure angle
P=30; //[30:60]
// Layer height (for ring horizontal split)
layer_h = 0.2; //[0:0.01:1]
// Chamfer exposed gears, top - watch fingers
ChamferGearsTop = 0;				// [1:No, 0.5:Yes, 0:Half]
// Chamfer exposed gears, bottom - help with elephant's foot/tolerance
ChamferGearsBottom = 0;				// [1:No, 0.5:Yes, 0:Half]
// Number of sets of jaws
jaws = 1; //[0:1:6]
// Jaw Initial Rotation (from closed)
jaw_rot = 180; //[0:180]
// Jaw Size
jaw_size = 20; //[0:100]
// Jaw Offset
jaw_offset = 0; //[0:0.1:100]
// Jaw Taper Angle (outside edge)
jaw_angle = 30; //[0:60]
// Dimple radius
dim_r = 1.1; //[0:0.1:2]
// Dimple depth ratio
dim_d = 0.5; //[0:0.1:1]
//Diameter of the knob, in mm
KnobDiameter		= 30.0;			//[10:0.5:100]
//Thickness of knob, including the stem, in mm:
KnobTotalHeight 	= 15;			//[10:thin,15:normal,30:thick, 40:very thick]
//Number of points on the knob
FingerPoints		= 8;   			//[3,4,5,6,7,8,9,10]
//Diameter of finger holes
FingerHoleDiameter	= 10; //[5:0.5:50]
TaperFingerPoints	= true;			// true

//
// Include Nut Captures at base of stem and/or on top of part
// If using a hex-head bolt for knob, include Top Nut Capture only.
// including both top and bottom captures allows creation of a knob
// with nuts top and bottom for strength/stability when using all-thread rod
//

//Include a nut capture at the top
TopNutCapture = 0;				//[1:Yes , 0:No]
//Include a nut capture at the base
BaseNutCapture = 0/1;				// [1:Yes , 0:No]

// Curve resolution settings, minimum angle
$fa = 5/1;
// Curve resolution settings, minimum size
$fs = 1/1;

// For fused sun gears, we require them to be a multiple of the planet teeth
drive_t = planet_t*sgm;
drive_t2 = planet_t2*sgm;

// Calculate cp based on desired ring wall thickness
cp=(outer_d/2-wall)*360/(drive_t+2*planet_t);

if ((drive_t+ring_t1)%planets)
{
    echo(str("Warning: For even spacing, planets must divide ", drive_t+ring_t1));
}
if ((drive_t2+ring_t2)%planets)
{
    echo(str("Warning: For even spacing (2), planets must divide ", drive_t2+ring_t2));
}

if (drive_t + 2*planet_t != ring_t1) {
    echo(str("Teeth fewer than ideal (ring1): ", drive_t+2*planet_t-ring_t1));
}
if (drive_t2 + 2*planet_t2 != ring_t2) {
    echo(str("Teeth fewer than ideal (ring2): ", drive_t2+2*planet_t2-ring_t2));
}

// Planetary gear ratio for fixed ring: 1:1+R/S
echo(str("Gear ratio of first planetary stage: 1:", 1+ring_t1/drive_t)); // eg. 1:3.1818...

// (Planet/Ring interaction: Nr*wr-Np*wp=(Nr-Np)*wc)
// one revolution of carrier (wc=1) turns planets on their axis
// wp = (Np-Nr)/Np = eg. (10-31)/10=-2.1 turns
// Secondary Planet/Ring interaction
// wr = ((Nr-Np)+Np*wp)/Nr = eg. ((34-11)-11*2.1)/34 = 1/340
// or Nr2/((Nr2-Np2)+Np2*(Np1-Nr1)/Np1)
echo(str("Gear ratio of planet/ring stage: 1:", abs(ring_t2/((ring_t2-planet_t2)+planet_t2*(planet_t-ring_t1)/planet_t)))); // eg. 3.8181..

// Final gear ratio is product of above, eg. 1:1298.18..
echo(str("Input/Output gear ratio: 1:",abs((1+ring_t1/drive_t)*ring_t2/((ring_t2-planet_t2)+planet_t2*(planet_t-ring_t1)/planet_t))));

// what ring should be for teeth to mesh without additional scaling
ring_t1_n = drive_t+2*planet_t;
ring_t2_n = drive_t2+2*planet_t2;

// Coincident planet offsets
cp2=(drive_t+planet_t)*cp/(drive_t2+planet_t2);

// Tolerances for geometry connections.
AT=0.02;
ST=AT*2;
TT=AT/2;

helix_angle=atan(PI*nTwist*cp/90/gear_h);
helix_angle2=atan(PI*nTwist*cp2/90/gear_h);

// Jaws
dim_n = floor(((modules-1)*gear_h+gear_h2)/dim_r/3);
dim_s = ((modules-1)*gear_h+gear_h2)/(dim_n+1);
if(jaws>0)for(k=[0:jaws-1])rotate([0,0,k*360/jaws]){
    for(i=[dim_s/2:dim_s:jaw_size-dim_s/2+AT]){
        for(j=[dim_s/2:dim_s:(modules-1)*gear_h+gear_h2-dim_s/2+AT]){
            translate([outer_d/2+jaw_size-i,jaw_offset,j])
                scale([1,dim_d,1])rotate([90,0,0])sphere(r=dim_r,$fn=6);
        }
    }
    difference(){
        intersection(){
            translate([0,jaw_offset,0])
                cube([outer_d/2+jaw_size,outer_d/2-jaw_offset,(modules-1)*gear_h+gear_h2-AT]);
            rotate([0,0,-jaw_angle])
                cube([outer_d/2+jaw_size,outer_d/2,(modules-1)*gear_h+gear_h2-AT]);
        }
        translate([0,0,-AT])
            cylinder(r=outer_d/2-0.5,h=gear_h+ST);
        translate([0,0,gear_h])
            cylinder(r=outer_d/2+0.5,h=gear_h2+layer_h-TT);
        translate([0,0,gear_h+gear_h2+layer_h-AT])
            cylinder(r=outer_d/2-0.5,h=gear_h+ST);
        for(i=[dim_s:dim_s:jaw_size-dim_s]){
            for(j=[dim_s:dim_s:(modules-1)*gear_h+gear_h2-dim_s+AT]){
                translate([outer_d/2+jaw_size-i,jaw_offset,j])
                    scale([1,dim_d,1])rotate([90,0,0])sphere(r=dim_r,$fn=6);
            }
        }
    }

    rotate([0,0,-jaw_rot])mirror([0,1,0]){
        for(i=[dim_s:dim_s:jaw_size-dim_s]){
            for(j=[dim_s:dim_s:(modules-1)*gear_h+gear_h2-dim_s+AT]){
                translate([outer_d/2+jaw_size-i,jaw_offset,j])
                    scale([1,dim_d,1])rotate([90,0,0])sphere(r=dim_r,$fn=6);
            }
        }
        difference(){
        intersection(){
            translate([0,jaw_offset,0])
                cube([outer_d/2+jaw_size,outer_d/2-jaw_offset,2*gear_h+gear_h2-AT]);
            rotate([0,0,-jaw_angle])
                cube([outer_d/2+jaw_size,outer_d/2,(modules-1)*gear_h+gear_h2-AT]);
            }
            translate([0,0,-AT])
                cylinder(r=outer_d/2+0.5,h=gear_h+layer_h+AT);
            translate([0,0,gear_h])
                cylinder(r=outer_d/2-0.5,h=gear_h2+layer_h-TT);
            translate([0,0,gear_h+gear_h2-TT])
                cylinder(r=outer_d/2+0.5,h=gear_h+ST);
            for(i=[dim_s/2:dim_s:jaw_size-dim_s/2+AT]){
                for(j=[dim_s/2:dim_s:(modules-1)*gear_h+gear_h2-dim_s/2+AT]){
                    translate([outer_d/2+jaw_size-i,jaw_offset,j])
                        scale([1,dim_d,1])rotate([90,0,0])sphere(r=dim_r,$fn=6);
                }
            }
        }
    }
}

// Knob
// by Hank Cowdog
// 2 Feb 2015
//
// based on FastRyan's
// Tension knob
// Thingiverse Thing http://www.thingiverse.com/thing:27502/
// which was downloaded on 2 Feb 2015
//
// GNU General Public License, version 2
// http://www.gnu.org/licenses/gpl-2.0.html
//
//This program is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either version 2
//of the License, or (at your option) any later version.
//
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.
//
//
//
//
//
//
// Finger points are the points on a star knob.
// Finger holes are the notches between each point.
// Larger values make for deeper holes.
// Too many/too deep and you may reduce knob size too much.
// If $TaperFingerPoints is true, the edges will be eased a little, making
// for a nicer knob to hold/use.
//

//Ratio of stem to total height smaller makes for less of a stem under the knob:
//StemHeightPercent =30.0/100.0;			// [0.3:0.8]

// The shaft is for a thru-hole.  Easing the shaft by a small percentage makes for
// easier insertion and makes allowance for ooze on 3D filament printers

//Diameter of the shaft thru-bolt, in mm
ShaftDiameter = shaft_d;

ShaftEasingPercentage = 0/100.0;  // 10% is plenty

NutFlatWidth = 1.75 * ShaftDiameter;
NutHeight =     0.87 * ShaftDiameter;
SineOfSixtyDegrees = 0.86602540378/1.0;

NutPointWidth = NutFlatWidth /SineOfSixtyDegrees;

//StemDiameter= KnobDiameter/2.0;
//StemHeight = KnobTotalHeight  * StemHeightPercent;

EasedShaftDiameter = ShaftDiameter * (1.0+ShaftEasingPercentage);

translate([0,0,(modules-1)*gear_h+gear_h2-ST]){
intersection(){
translate([0,0,KnobTotalHeight])mirror([0,0,1])difference() {
// The whole knob
	cylinder(h=KnobTotalHeight, r=KnobDiameter/2, $fn=50);

// each finger point
	for ( i = [0 : FingerPoints-1] )
	{
    		rotate( i * 360 / FingerPoints, [0, 0, 1])
    		translate([0, (KnobDiameter *.6), -1])
		union() {

// remove the vertical part of the finger hole
    			cylinder(h=KnobTotalHeight+2, r=FingerHoleDiameter/2, $fn=60);

// taper the sides of the finger points
			if(TaperFingerPoints) {
				rotate_extrude(convexity = 10, $fn = 60)
					translate([FingerHoleDiameter/2.0, 0, 0])
					polygon( points=[[2,-3],[-1,6],[-1,-3]] );

			}
		}
	}

// Drill the shaft and nut captures
	translate([0, 0, KnobTotalHeight+1]) scale([1, 1, -1])
	union() {
//The thru-shaft
		cylinder(h=KnobTotalHeight+2, r=EasedShaftDiameter/2., $fn=50);

// Drill the nut capture
		if (1 == BaseNutCapture) {
			cylinder(h=NutHeight + 1, r=NutPointWidth/2.0, $fn=6);
		}
	}

// Drill the 2nd nut capture
	if (1 == TopNutCapture) {
		translate([0, 0, -1])
			cylinder(h=NutHeight + 1, r=NutPointWidth/2.0, $fn=6);
	}

// Torus removal to transition knob into stem
//	translate([0, 0, KnobTotalHeight])
//	rotate_extrude(convexity = 10, $fn = 50)
//		translate([StemDiameter, 0, 0])
//		circle(r = StemHeight, $fn = 50);

// taper the ends of the points
	if(TaperFingerPoints) {
		rotate_extrude(convexity = 10, $fn = 50)
		translate([KnobDiameter/2, 0, 0])
		polygon( points=[[-2,-3],[1,6],[1,-3]] );
	}
}

        if(modules==3)rotate([0,0,180/drive_t*(1-planet_t%2)]){
            linear_extrude(height=KnobTotalHeight,scale=1+KnobTotalHeight/(drive_t*cp/360)*sqrt(3),slices=1)
                gear2D(drive_t,cp*PI/180,P,depth_ratio,tol);
        }
        if(modules==3)rotate([0,0,180/drive_t*(1-planet_t%2)]){
            linear_extrude(height=KnobTotalHeight,scale=1+KnobTotalHeight/(drive_t*cp/360)/1.25,slices=1)
                hull()gear2D(drive_t,cp*PI/180,P,depth_ratio,tol);

        }
        if(modules==2)rotate([0,0,180/drive_t2*(1-planet_t2%2)]){
            linear_extrude(height=KnobTotalHeight,scale=1+KnobTotalHeight/(drive_t2*cp2/360)*sqrt(3),slices=1)
                gear2D(drive_t2,cp2*PI/180,P,depth_ratio,tol);
        }
        if(modules==2)rotate([0,0,180/drive_t2*(1-planet_t2%2)]){
            linear_extrude(height=KnobTotalHeight,scale=1+KnobTotalHeight/(drive_t2*cp2/360)/1.25,slices=1)
                hull()gear2D(drive_t2,cp2*PI/180,P,depth_ratio,tol);

        }
    }
}

// center gears
rotate([0,0,180/drive_t*(1-planet_t%2)])intersection(){
    drivegear(t1=drive_t,bore=shaft_d,rot=180/drive_t*(1-planet_t%2));
    if(ChamferGearsBottom<1)rotate(90/planet_t)
        linear_extrude(height=gear_h,scale=1+gear_h/(drive_t*cp/360)*sqrt(3),slices=1)
            circle($fn=drive_t*2,r=drive_t*cp/360-ChamferGearsBottom*min(cp/(2*tan(P))+tol,depth_ratio*cp*PI/180+tol));
}
translate([0,0,gear_h-TT])intersection(){
    rotate([0,0,180/drive_t2*(1-planet_t2%2)])
        drivegear(t1=drive_t2,bore=shaft_d,cp=cp2,helix_angle=helix_angle2,gear_h=gear_h2,rot=180/drive_t2*(1-planet_t2%2));
    rotate([0,0,180/drive_t*(1-planet_t%2)])
        linear_extrude(height=gear_h2,scale=1+gear_h2/(drive_t*cp/360)*sqrt(3),slices=1)
            gear2D(drive_t,cp*PI/180,P,depth_ratio,tol);
}
if(modules==3)translate([0,0,gear_h+gear_h2-AT])intersection(){
    rotate([0,0,180/drive_t*(1-planet_t%2)])
        drivegear(t1=drive_t,bore=shaft_d,rot=180/drive_t*(1-planet_t%2));
    rotate([0,0,180/drive_t2*(1-planet_t2%2)])
        linear_extrude(height=gear_h,scale=1+gear_h/(drive_t2*cp2/360)*sqrt(3),slices=1)
            gear2D(drive_t2,cp2*PI/180,P,depth_ratio,tol);
}

// planets
planets(t1=planet_t, t2=drive_t,offset=(drive_t+planet_t)*cp/360,n=planets,t=ring_t1+drive_t)
    intersection(){
        planetgear(t1 = planet_t, bore=shaft_d);
        if(ChamferGearsBottom<1)rotate(90/planet_t)
            linear_extrude(height=gear_h,scale=1+gear_h/(planet_t*cp/360)*sqrt(3),slices=1)
                circle($fn=planet_t*2,r=planet_t*cp/360-ChamferGearsBottom*min(cp/(2*tan(P))+tol,depth_ratio*cp*PI/180+tol));
    }

translate([0,0,gear_h-TT])
    planets(t1=planet_t2, t2=drive_t2,offset=(drive_t2+planet_t2)*cp2/360,n=planets,t=ring_t2+drive_t2)
        intersection(){
            planetgear(t1 = planet_t2, bore=shaft_d,cp=cp2,helix_angle=helix_angle2,gear_h=gear_h2);
            linear_extrude(height=gear_h2,scale=1+gear_h2/(planet_t*cp/360)*sqrt(3),slices=1)
                gear2D(planet_t,cp*PI/180,P,depth_ratio,tol);
            if(ChamferGearsTop<1&&modules==2)translate([0,0,gear_h2])rotate(90/planet_t2)mirror([0,0,1])
                linear_extrude(height=gear_h2,scale=1+gear_h2/(planet_t2*cp2/360)*sqrt(3),slices=1)
                    circle($fn=planet_t2*2,r=planet_t2*cp2/360-ChamferGearsBottom*min(cp2/(2*tan(P))+tol,depth_ratio*cp2*PI/180+tol));
        }
if(modules==3)translate([0,0,gear_h+gear_h2-AT])
    planets(t1=planet_t, t2=drive_t,offset=(drive_t+planet_t)*cp/360,n=planets,t=ring_t1+drive_t)
        intersection(){
            planetgear(t1 = planet_t, bore=shaft_d);
            linear_extrude(height=gear_h,scale=1+gear_h/(planet_t2*cp2/360)*sqrt(3),slices=1)
                    gear2D(planet_t2,cp2*PI/180,P,depth_ratio,tol);
            if(ChamferGearsTop<1)translate([0,0,gear_h])rotate(90/planet_t)mirror([0,0,1])
                linear_extrude(height=gear_h,scale=1+gear_h/(planet_t*cp/360)*sqrt(3),slices=1)
                    circle($fn=planet_t*2,r=planet_t*cp/360-ChamferGearsBottom*min(cp/(2*tan(P))+tol,depth_ratio*cp*PI/180+tol));
        }

// rings
s1=ring_t1_n/ring_t1;
s2=ring_t2_n/ring_t2;
ha1=atan(PI*nTwist*s1*cp/90/gear_h); // not sure about this, seems close enough.
ha2=atan(PI*nTwist*s2*cp2/90/gear_h);
difference(){
    gearcase(t1=ring_t1,bore=outer_d/2,cp=s1*cp,helix_angle=ha1);
    if(ChamferGearsBottom<1)translate([0,0,-TT])
        linear_extrude(height=(ring_t1*s1*cp/360)/sqrt(3),scale=0,slices=1)
            if(ChamferGearsTop>0)
                hull()gear2D(ring_t1,s1*cp*PI/180,P,depth_ratio,-tol);
            else
                circle($fn=ring_t1*2,r=ring_t1*s1*cp/360);
}
translate([0,0,gear_h-TT])difference(){
    gearcase(t1=ring_t2,bore=outer_d/2,cp=s2*cp2,helix_angle=ha2,gear_h=gear_h2);
    linear_extrude(height=(ring_t1*s1*cp/360)/sqrt(3),scale=0,slices=1)
        hull()gear2D(ring_t1,s1*cp*PI/180,P,depth_ratio,-tol);
    translate([-outer_d,-outer_d,-TT])cube([2*outer_d,2*outer_d,layer_h+AT]);
    if(ChamferGearsTop<1&&modules==2)translate([0,0,gear_h2+AT])mirror([0,0,1])
        linear_extrude(height=(ring_t2*s2*cp2/360)/sqrt(3),scale=0,slices=1)
            if(ChamferGearsTop>0)
                hull()gear2D(ring_t2,s2*cp2*PI/180,P,depth_ratio,-tol);
            else
                circle($fn=ring_t2*2,r=ring_t2*s2*cp2/360);
}
if(modules==3)translate([0,0,gear_h+gear_h2-AT])
    difference(){
        gearcase(t1=ring_t1,bore=outer_d/2,cp=s1*cp,helix_angle=ha1);
        linear_extrude(height=(ring_t2*s2*cp2/360)/sqrt(3),scale=0,slices=1)
            hull()gear2D(ring_t2,s2*cp2*PI/180,P,depth_ratio,-tol);
        translate([-outer_d,-outer_d,-TT])cube([2*outer_d,2*outer_d,layer_h+AT]);
        if(ChamferGearsTop<1)translate([0,0,gear_h+AT])mirror([0,0,1])
            linear_extrude(height=(ring_t1*s1*cp/360)/sqrt(3),scale=0,slices=1)
                if(ChamferGearsTop>0)
                    hull()gear2D(ring_t1,s1*cp*PI/180,P,depth_ratio,-tol);
                else
                    circle($fn=ring_t1*2,r=ring_t1*s1*cp/360);
    }

// Drive gear with slot
module drivegear(t1=13,reverse=false,bore=2*cos(P)*13*cp/360-wall,rot=0)
{
    planetgear(t1,!reverse,bore,cp=cp,helix_angle=helix_angle,gear_h=gear_h,rot=rot);
}

// reversible herringbone gear with bore hole
module planetgear(t1=13,reverse=false,bore=0,rot=0)
{
    difference()
    {
        translate([0,0,gear_h/2])
        if (reverse) {
            mirror([0,1,0])
                herringbone(t1,PI*cp/180,P,depth_ratio,tol,helix_angle,gear_h);
        } else {
            herringbone(t1,PI*cp/180,P,depth_ratio,tol,helix_angle,gear_h);
        }

        translate([0,0,-TT]){
            rotate([0,0,-rot])
                cylinder(d=bore, h=2*gear_h+AT);
            // Extra speed holes, for strength
            if(bore>0 && bore/4+(t1-2*tan(P))*cp/720>bore)
                for(i = [0:360/shafts:360-360/shafts])rotate([0,0,i-rot])
                    translate([bore/4+(t1-2*tan(P))*cp/720,0,-AT])
                        cylinder(d=bore,h=2*gear_h+AT);
        }
    }
}

// Space out planet gears approximately equally
module planets()
{
    for(i = [0:n-1])
    rotate([0,0,round(i*t/n)*360/t])
        translate([offset,0,0]) rotate([0,0,round(i*t/n)*360/t*t2/t1])
            children();
}

module gearcase(t1=13,teethtwist=1,bore=18*cp/360)
{
    difference() {
        cylinder(r=bore,h=gear_h);
        translate([0,0,-TT])planetgear(t1=t1,gear_h=gear_h+AT,tol=-tol,helix_angle=helix_angle,cp=cp,depth_ratio=depth_ratio,P=P);
	}
            // outer notches
        if(outer_t>0)for(i = [0:360/outer_t:360-360/outer_t])rotate([0,0,i])
            translate([outer_d/2,0,-AT])rotate([0,0,45])
                cylinder(d=outer_w,h=gear_h+AT,$fn=4);

}

// Herringbone gear code, taken from:
// Planetary gear bearing (customizable)
// https://www.thingiverse.com/thing:138222
// Captive Planetary Gear Set: parametric. by terrym is licensed under the Creative Commons - Attribution - Share Alike license.
module herringbone(
	number_of_teeth=15,
	circular_pitch=10,
	pressure_angle=28,
	depth_ratio=1,
	clearance=0,
	helix_angle=0,
	gear_thickness=5){
union(){
    translate([0,0,-TT])
	gear(number_of_teeth,
		circular_pitch,
		pressure_angle,
		depth_ratio,
		clearance,
		helix_angle,
		gear_thickness/2+TT);
	mirror([0,0,1]) translate([0,0,-TT])
		gear(number_of_teeth,
			circular_pitch,
			pressure_angle,
			depth_ratio,
			clearance,
			helix_angle,
			gear_thickness/2+TT);
}}

module gear (
	number_of_teeth=15,
	circular_pitch=10,
	pressure_angle=28,
	depth_ratio=1,
	clearance=0,
	helix_angle=0,
	gear_thickness=5,
	flat=false){
pitch_radius = number_of_teeth*circular_pitch/(2*PI);
twist=tan(helix_angle)*gear_thickness/pitch_radius*180/PI;

flat_extrude(h=gear_thickness,twist=twist,flat=flat)
	gear2D (
		number_of_teeth,
		circular_pitch,
		pressure_angle,
		depth_ratio,
		clearance);
}

module flat_extrude(h,twist,flat){
	if(flat==false)
		linear_extrude(height=h,twist=twist,slices=6)children(0);
	else
		children(0);
}

module gear2D (
	number_of_teeth,
	circular_pitch,
	pressure_angle,
	depth_ratio,
	clearance){
pitch_radius = number_of_teeth*circular_pitch/(2*PI);
base_radius = pitch_radius*cos(pressure_angle);
depth=circular_pitch/(2*tan(pressure_angle));
outer_radius = clearance<0 ? pitch_radius+depth/2-clearance : pitch_radius+depth/2;
root_radius1 = pitch_radius-depth/2-clearance/2;
root_radius = (clearance<0 && root_radius1<base_radius) ? base_radius : root_radius1;
backlash_angle = clearance/(pitch_radius*cos(pressure_angle)) * 180 / PI;
half_thick_angle = 90/number_of_teeth - backlash_angle/2;
pitch_point = involute (base_radius, involute_intersect_angle (base_radius, pitch_radius));
pitch_angle = atan2 (pitch_point[1], pitch_point[0]);
min_radius = max (base_radius,root_radius);

intersection(){
	rotate(90/number_of_teeth)
		circle($fn=number_of_teeth*3,r=pitch_radius+depth_ratio*circular_pitch/2-clearance/2);
	union(){
		rotate(90/number_of_teeth)
			circle($fn=number_of_teeth*2,r=max(root_radius,pitch_radius-depth_ratio*circular_pitch/2-clearance/2));
		for (i = [1:number_of_teeth])rotate(i*360/number_of_teeth){
			halftooth (
				pitch_angle,
				base_radius,
				min_radius,
				outer_radius,
				half_thick_angle);
			mirror([0,1])halftooth (
				pitch_angle,
				base_radius,
				min_radius,
				outer_radius,
				half_thick_angle);
		}
	}
}}

module halftooth (
	pitch_angle,
	base_radius,
	min_radius,
	outer_radius,
	half_thick_angle){
index=[0,1,2,3,4,5];
start_angle = max(involute_intersect_angle (base_radius, min_radius)-5,0);
stop_angle = involute_intersect_angle (base_radius, outer_radius);
angle=index*(stop_angle-start_angle)/index[len(index)-1];
p=[[0,0], // The more of these the smoother the involute shape of the teeth.
	involute(base_radius,angle[0]+start_angle),
	involute(base_radius,angle[1]+start_angle),
	involute(base_radius,angle[2]+start_angle),
	involute(base_radius,angle[3]+start_angle),
	involute(base_radius,angle[4]+start_angle),
	involute(base_radius,angle[5]+start_angle)];

difference(){
	rotate(-pitch_angle-half_thick_angle)polygon(points=p);
	square(2*outer_radius);
}}

// Mathematical Functions
//===============

// Finds the angle of the involute about the base radius at the given distance (radius) from it's center.
//source: http://www.mathhelpforum.com/math-help/geometry/136011-circle-involute-solving-y-any-given-x.html

function involute_intersect_angle (base_radius, radius) = sqrt (pow (radius/base_radius, 2) - 1) * 180 / PI;

// Calculate the involute position for a given base radius and involute angle.

function involute (base_radius, involute_angle) =
[
	base_radius*(cos (involute_angle) + involute_angle*PI/180*sin (involute_angle)),
	base_radius*(sin (involute_angle) - involute_angle*PI/180*cos (involute_angle))
];
```


[User]
I need a parametric design for some PCB tweezers, mainly for use when etching. I'd like to be able to adjust the height, thickness of the arms, the claw length, and the width of the PCB that the tweezers are designed to hold.

The basic design is a pair of claws that come together at an angle, that are attached to two bars that extend backwards. The claws should have a slightly pointed tip for gripping, and the bars should have a handle section at the end, with a cylindrical part in the middle.

I've attached an OpenSCAD file that shows roughly what I'm after. I think the most important parameters are:

*   `ph`: overall height of the tweezers
*   `pt`: thickness of the arms
*   `pl`: length of the claws
*   `pw`: width of the PCB being held

I'd like the design to use the `hull()` function to create a smooth transition between the parts, and ideally avoid boolean operations as much as possible for robustness. Could you generate an OpenSCAD file that implements this design, using a similar parameter naming convention?

[Assistant]
PCB_tweezers_customizer.scad:
```
//height
ph=10; // [6:30]
//thickness
pt=3; // [1:0.1:5]
//claw length
pl=30; // [10:1:100]
//width (pcb)
pw=60; //[0:1:200]

difference()
{
    union()
    {
        rotate([-75,0,0])
        translate([-pt-(pw/2),-pl,0])
        difference()
        {
            translate([-1,0,0])
            cube([pt+1,2*pl,ph]);
            translate([pt,0,2.3])
            scale([1,1,1.5])
            rotate([0,45,0])
            translate([-1,-1,-1])
            cube([2,2*pl,2]);
        }
        rotate([-75,0,0])
        translate([pw/2,-pl,0])
        difference()
        {
            cube([pt+1,2*pl,ph]);
            translate([0,0,2.3])
            scale([1,1,1.5])
            rotate([0,45,0])
            translate([-1,-1,-1])
            cube([2,2*pl,2]);
        }
    }
    translate([-pw,-pw,-2*pw])
    cube(2*pw);
}

translate([-pt-(pw/2),5,0])
cube([pt,25,ph]);
translate([pw/2,5,0])
cube([pt,25,ph]);

hull()
{
    translate([pw/2,30,0])
    cube([pt,0.1,ph]);
    translate([20,50,0])
    cube([pt,0.1,ph]);
}
hull()
{
    translate([-pt-(pw/2),30,0])
    cube([pt,0.1,ph]);
    translate([-20-pt,50,0])
    cube([pt,0.1,ph]);
}

hull()
{
    translate([20,50,0])
    cube([pt,0.1,ph]);
    translate([0,150,0])
    cube([pt,0.1,ph]);
}
hull()
{
    translate([-20-pt,50,0])
    cube([pt,0.1,ph]);
    translate([-pt,150,0])
    cube([pt,0.1,ph]);
}
translate([-pt,150,0])
cube([2*pt,10,ph]);
hull()
{
    translate([20,50,0])
    cube([pt,0.1,ph]);
    translate([0,100,0])
    cube([1,0.1,ph]);
}
hull()
{
    translate([-20-pt,50,0])
    cube([pt,0.1,ph]);
    translate([-1,100,0])
    cube([1,0.1,ph]);
}
translate([0,100,0])
cylinder(r=2,h=ph,$fn=50);

```


[User]
I want to create some 3D printable letter blocks that have different letters visible from different viewpoints. I'm interested in making blocks where each block looks like three different letters, one from the front, one from the right, and one from the top. I'd like a parametric OpenSCAD design that lets me specify the three letters for each block and also control the size and spacing between blocks, as well as the base thickness and the option of adding some small, flat characters between the blocks on the base.

Specifically:

1.  **Letter Input:** I need to be able to specify three strings of characters (one for the front, right, and top view). The design should work with uppercase letters, numbers, spaces, and the following special characters: `$ & < > * + @ / \ ♥ ♠ ♦ ♣ ♪ ♫ ♀ ♂ ← → π`.

2.  **Letter Scaling:** The size of the letters needs to be adjustable using a scaling factor, so I can make the blocks larger or smaller. I'd like a parameter for this that lets me specify a scale like 0.5, 1, 2, etc.

3.  **Letter Spacing:** There should be an option to control the spacing between the blocks, and I'd like to specify it as a percentage of the letter's width (like 0%, 10%, or -5% etc.).

4.  **Base Height:** I want to be able to control the height of a base that the letter blocks sit on. This should be adjustable by a parameter like 2mm or 5mm etc.

5.  **Flat Base Characters:** I'd like an option to add small flat characters onto the base between the blocks. I need to be able to choose what characters are placed, where they're placed (front, back, or both), and control their thickness (as a ratio of the base thickness) and their rotation angle. I'd like an option to randomize the rotation angle of these flat characters, or have them angled in or out.

6.  **Permutations:** I need the ability to generate all six permutations of the front, right, and top orientations automatically (FRT, FTR, RFT, RTF, TFR, TRF) and also control whether the top letter is aligned with the front face or right face. I'd also like an option to pick a single permutation directly.

7.  **Part Selection:** It would be ideal if there was a parameter to generate separate STL files for: the letter blocks only, the base only, the flat characters only, or an STL for all parts combined. This would allow for easy manipulation of individual components after generation.

8.  **Space Character Width**: The width of a space character should be adjustable between a minimal width of 1.2mm and a maximal width (the width of a normal block).

9.  **Fonts and Resolution**: Use the font "Overpass Mono:style=Bold" at a base size of 22. Letter spacing and dimensions should be fine-tuned for this font and size. You can use a lower resolution ($fn) for triple letters if generating all permutations.

Could you provide the OpenSCAD code that can achieve all of this? I don't need any external libraries. I look forward to seeing the results!

[Assistant]
TripleMultiLetterBlocksV2.5.scad:
```
/* Dual Letter Blocks Illusion and Triple Letter Blocks Illusion
   by Lyl3

Creates a series of blocks that are each shaped like two or three different letters
when viewed from two or three orthogonal viewpoints.

This codebase now handles both the dual and the triple letter block illusions published at:
https://www.thingiverse.com/thing:3516901
https://www.thingiverse.com/thing:3530989

The basic parameters are different for each and must be removed from the parameters section
before being published on Thingiverse in order to use the Customizer app.

DualV1.1 Reduced base $fn value from 100 to 50
DualV1.2 Resized and positioned a few special characters so that they can be printed
DualV1.3 Doesn't create post when pair of characters is spaces for both.
         Reduced base $fn value from 50 to 25 and set to 50 for the hull circles. This cuts rendering time in half.
DualV1.4 Made instructions clearer

TripV1.1 Added code to pad strings with spaces if generating all 6 permutations
TripV1.2 Override specified base height and set to zero if generating all 6 permutations
TripV1.3 Module tripleLetterString shifts any block down 0.8 mm (for 20 mm block) if it's "OOO" or "UUU"
TripV1.4 Added measurements and adjustments for *, +, @, /, and \

V2.0 Merged codebase from dual letters and triple letters into single combined codebase
     Added capability to add small characters laying flat between the blocks
     Added capability to create multiple STLs
V2.1 Fixed code to handle separate STL creation when no pad or flat characters are being created
V2.2 Added parameter to adjust thickness of flat chracters on the base
     Added parameter to select which permutation of the strings to create
     Move letter blocks and flat characters down very slightly so that model is one single part
     Separate each of the permutations so that they're easier to examine
V2.3 Added option to rotate the top letters so they can be oriented the same as the right face instead of the same as the front face.
     Removed parameter to select which permutation of the strings to create. Code is still there, but customizer doesn't present the option. Remove enclosing str function if you want the parameter presented.
V2.4 Fixed bug that allowed the base of letters with rounded bottoms to not have a flat bottomed base like they're supposed to have.
    Added option to change width of a space character from minimal (1.2 mm) to maximal (block width), and the minimal width is now always 1.2 mm instead of scaling with the letter scaling.
V2.5 Generate letter blocks for part = 1 even if no base, since the CGAL is cached anyways.
     Added some letter blocks to the STLs for unrequested parts so that they represent what the thing is
     Increased unscaled $fn base from 25 to 40, and decreased to 20 for triple when generating all permutations
*/

/* [Basic Parameters] */

/* --------------- Triple Letter Parameters - remove for Customizer App, comment out locally
*/
mode = 3 + 0;
// Generate all 6 permutations (FRT, FTR, RFT, RTF, TFR, TRF) of letter/word orientations?
all_permutations = "yes"; // [yes,no]
all_combos = (all_permutations == "yes") ? true: false;

// If generating a single permutation, which faces are the strings below specifying?  (this parameter may be useful when running locally so that you can easily cycle through each of the permutations)
which_faces = str("FRT"); // [FRT:1- front-right-top, FTR:2- front-top-right, RFT:3- right-front-top, RTF:4- right-top-front, TFR:5- top-front-right, TRF:6- top-right-front]

//The top letters can be oriented in one of two ways. Do you want to vertically align the top letters with the front letters or with the right letters?
align_top = "front"; // [front,right]

// First string (use only UPPERCASE letters, numbers, space, or the special characters)
string_1 = "B";

// Second string (use only UPPERCASE letters, numbers, space, or the special characters)
string_2 = "E";

// Third string (use only UPPERCASE letters, numbers, space, or the special characters)
string_3 = "G";

// Scale of letters (1 = letters 20 mm tall and spaced 28.28 mm apart if spacing is set to 0% below )
letterScaling = 2.0; // [0.5:0.1:10.0]

// Additional spacing between letters (% of letter width, none needed, can be negative)
additionalSpacing = 0.0; // [-10.0:0.1:30.0]

// Height of base (mm) - setting ignored and overridden to zero if generating all 6 permutations
baseHeight = 4.0; // [0.0:0.1:30.0]
padHeight = (all_combos == true) ? 0 : baseHeight;
// --------------- End of Triple Letter Parameters

/* [Additional Parameters] */

// Where should small characters laying flat between each block be added to the base?
add_base_characters_where = "nowhere"; // [nowhere,front,back,front and back]
// Put them in a vector so they can share the same code: index 0 = front, index 1 = back
tinyLocations = [(add_base_characters_where == "front" || add_base_characters_where == "front and back") ? true: false,
                 (add_base_characters_where == "back" || add_base_characters_where == "front and back") ? true: false];

// What characters should be added to the base? (Multiple characters allowed, defaults to heart if nothing specified, will cycle through the characters for each location)
what_base_characters = "";
baseChars =  (what_base_characters == " " || what_base_characters == "" || what_base_characters == undef) ? chr(9829): what_base_characters;

// How should the characters added to the base be rotated?
rotate_base_characters = -1; // [0:No rotation, -1:Angled in, 1:Angled out, 2:Random]
rotateBaseChars = (rotate_base_characters != 0 ) ? true: false;

// How much should they be rotated (specifies maximum angle if random rotation is selected)
rotation_angle = 30; // [0:5:180]

/* [Advanced Parameters] */

// Which one would you like to see? (When "Create Thing" is clicked, multiple STL files will be created, one for each choice here)
part = 4; // [1:Letter blocks Only, 2:Base only, 3:Small flat characters only, 4:Everything]

// Thickness adjustment ratio for the small flat characters on the base (default is half the thickness of the base)
BaseCharactersThickness = 1.0;  //[0.5:0.1:10]

/* [Hidden] */
fudge = 0.00001;                               //
letterWidth = 18.4;                            // enough space for the W
letterHeight = 21.37;                          // height of typical letter in chosen font size, could be larger
blockSize = (mode == 2) ? letterWidth : 20;
blockHeight = (mode == 2) ? 21.37 : blockSize;
myScale = letterScaling * 20 / blockHeight;    // adjust scaling so that selected scale is based on nice even number
minWidth = (mode == 3 || width_of_space_character == "minimal - 1.2 mm") ? 1.2/myScale : blockSize;      // width of block for a space character

fontName = "Overpass Mono:style=Bold";
fontSize = 22;                                      // chosen as reasonable base size for 3D printing
spacingRatio = 1+(additionalSpacing/100); //
blockWidth = sqrt(2*blockSize*blockSize);           // hypotenuse of the block sides
padWidth = blockWidth * 1.1;                        // additional 10% around the blocks
defaultSpacing = (mode == 2) ? 0.9 : 1.05;          // Default is 10% closer for dual and 5% farther for triple
blockSpace = blockWidth * (defaultSpacing+(additionalSpacing/100));
adjustedPadHeight = padHeight / myScale;      // adjust pad height before being scaled with the letters

StringFront = (mode == 2 || which_faces[0] == "F") ? string_1 : (which_faces[1] == "F") ? string_2 : string_3;
StringRight = (mode == 2 || which_faces[1] == "R") ? string_2 : (which_faces[2] == "R") ? string_3 : string_1;
StringTop =   (mode == 2 || which_faces[2] == "T") ? string_3 : (which_faces[0] == "T") ? string_1 : string_2;

// Settings for the small letters laying flat on the base
// The sizing and spacing fudges were chosen to approximately center it between the letter blocks
tinyFactor = 2.5;                                     // The inverse of the scaling for the small letters
tinyShifts = [-padWidth/2+1.2*letterHeight/tinyFactor/2, padWidth/2-1.3*letterHeight/tinyFactor/2];

maxStringLength = max(len(string_1),len(string_2),len(string_3));

BaseCharsRotation = (rotate_base_characters == 2) ?
                     rands(-rotation_angle,rotation_angle,2*(maxStringLength-1))
                     : concat(
                         [for (r = [1 : 1 : (maxStringLength-1)/2]) -rotation_angle*rotate_base_characters],
                         [for (r = [1 : 1 : (maxStringLength-1)%2]) 0],
                         [for (r = [1 : 1 : (maxStringLength-1)/2]) rotation_angle*rotate_base_characters],
                         [for (r = [1 : 1 : (maxStringLength-1)/2]) -rotation_angle*rotate_base_characters],
                         [for (r = [1 : 1 : (maxStringLength-1)%2]) 0],
                         [for (r = [1 : 1 : (maxStringLength-1)/2]) rotation_angle*rotate_base_characters]);
//echo (BaseCharsRotation);

// Lower resolution for triple letters when all permutations being generated
$fn = letterScaling * ((mode == 3 && all_combos) ? 20 : 40);
/*
Thingiverse customizer can't handle these characters even in comments
so the chr function must be used to enter them
*/
specialCharacters = chr([9829, 9824, 9830, 9827, 9834, 9835, 9792, 9794, 8592, 8594, 960]);
//echo("Special characters:", specialCharacters);  // Uncomment to see what the characters are

/*
Measurements, scaling, and positioning for characters is fine-tuned for
fontName = "Overpass Mono:style=Bold", fontSize = 22

                  A       B       C       D       E      F      G      H      I       J       K      L       M
                  N       O       P       Q       R      S      T      U      V       W       X      Y       Z
                  0       1       2       3       4      5      6      7      8       9
                  $       &       <       >       *      +      @      /      \
                  heart   spade   diamond club    note1  note2  female male   left    right   pi
*/
letterIDs = str("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$&<>*+@/\\", specialCharacters);
letterWidths =   [17.40,  14.78,  15.17,  15.203, 14.35, 14.41, 15.54, 14.17, 12.58,  13.92,  15.60, 13.494, 14.17,
                  14.17,  16.21,  14.68,  16.21,  14.38, 14.68, 16.06, 14.44, 17.40,  18.01,  16.61, 16.18,  14.44,
                  15.48,  14.17,  14.32,  14.62,  16.49, 14.47, 15.33, 14.53, 15.51,  15.05,
                  14.59,  18.35,  14.35,  14.35,  14.26, 14.35, 17.00, 15.60, 15.60,
                  15.75,  15.75,  15.75,  16.06,  12.61, 14.29, 12.03, 12.03, 16.67,  16.67,  18.01];
letterHeights =  [21.37,  21.37,  22.04,  21.37,  21.37, 21.37, 22.10, 21.37, 21.37,  21.74,  21.37, 21.37,  21.37,
                  21.37,  22.10,  21.37,  21.74,  21.37, 22.10, 21.37, 21.74, 21.37,  21.37,  21.37, 21.37,  21.37,
                  22.10,  21.40,  21.74,  22.10,  21.37, 21.74, 22.10, 21.37, 22.10,  22.10,
                  26.87,  22.10,  14.62,  14.62,  14.17, 14.35, 22.10, 25.70, 25.70,
                  20.30,  20.61,  20.76,  20.61,  20.88, 25.22, 21.74, 22.10, 12.79,  12.79,  16.30];
letterXShifts =  [-0.808, -3.15,  -1.71,  -3.052, -3.23, -3.22, -1.69, -3.27, -4.95,  -3.60,  -2.97, -3.438, -3.27,
                  -3.27,  -1.62,  -3.16,  -1.62,  -3.22, -2.80, -1.71, -3.04, -0.81,  -0.44,  -1.32, -1.62,  -3.04,
                  -2.17,   -3.27, -2.86,  -2.885, -1.48, -3,    -2.27, -2.94, -2.12,  -2.47,
                  -2.89,  -0.265, -3.105, -3.105, -3.21, -3.1,  -1.04, -2.0,  -0.5,
                  -1.94,  -1.94,  -1.94,  -1.71,  -4.94, -1.28, -5.63, -5.63, -1.1,   -1.464, -0.44];
letterYShifts =  [0,      0,      0,      0,      0,     0,     0,     0,     0,      0,      0,     0,      0,
                  0,      0,      0,      0,      0,     0,     0,     0,     0,      0,      0,     0,      0,
                  0,      0,      0,      0,      0,     0,     0,     0,     0,      0,
                  2.137,  0,      -4.4,   -4.555, -6.15, -4.6,  0.0,   3.08,  3.08,
                  -2.0,   0,      -2.0,   0,     -1.0,   2.3,   0,     0,     -10.55, -10.55, -0.099];
letterYResizes = [1,      1,      1.015,  1,      1,     1,     1.016, 1,     1,      1.017,  1,     1,      1,
                  1,      1.016,  1,      1,      1,     1.016, 1,     1.017, 1,      1,      1,     1,      1,
                  1.016,  1,      1,      1.016,  1,     1.017, 1.016, 1,     1.016,  1.016,
                  1,      1.015,  1,      1,      1.15,  1,     1.016, 1,     1,
                  1.1,    1,      1.1,    1,      1.025, 1.025, 1,     1,     1.2,    1.2,    1.05];

function width (letter) = ((search(letter,letterIDs)[0])==undef ? blockSize : letterWidths[search(letter,letterIDs)[0]]);
function scaleX (letter) = blockSize / ((search(letter,letterIDs)[0])==undef || mode == 2 ? blockSize : letterWidths[search(letter,letterIDs)[0]]);
function scaleY (letter) = blockHeight / ((search(letter,letterIDs)[0])==undef ? blockHeight : letterHeights[search(letter,letterIDs)[0]]);
function shiftX (letter) = (search(letter,letterIDs)[0])==undef  || mode == 2 ? 0 : letterXShifts[search(letter,letterIDs)[0]];
function shiftY (letter) = (search(letter,letterIDs)[0])==undef ? 0 : letterYShifts[search(letter,letterIDs)[0]];
function resizeY (letter) = (search(letter,letterIDs)[0])==undef ? 1 : letterYResizes[search(letter,letterIDs)[0]];

module extrudeLetter (letter) {
  if (letter != " " && letter != undef)
    scale([1,resizeY(letter),1])                      // Some characters need to be resized because they're too short or too tall
    translate ([shiftX(letter),shiftY(letter),0])     // After scaling to fill the cube, move it into the cube
    scale ([scaleX(letter),scaleY(letter),1])         // Scale the character to fill the cube
    linear_extrude(blockSize) text (letter, size=fontSize, font=fontName);
  else
    if (mode == 2)
      translate ([(blockSize-minWidth)/2,0,0])
        cube([minWidth,blockHeight,blockSize]);
    else
      cube([blockSize,blockSize,blockSize]);
}

module tripleLetterBlock (letter1, letter2, letter3) {
  if ((letter1 != " " && letter1 != undef) || (letter2 != " " && letter2 != undef) || (letter3 != " " && letter3 != undef))
  intersection() {
//  union() {     // Change intersection to union to and use highlight/debug modifer on letter of interest to see scaling & positioning
    cube([blockSize,blockSize,blockHeight]);          // cube needed to ensure flat bottomed characters
    translate ([0,blockSize, 0]) rotate([90,0,0])   // front face: move it to orthogonally into position
      extrudeLetter (letter1);
    rotate([90,0,90])                               // right face: rotate to correct orientation
      extrudeLetter (letter2);
    if (mode == 3) {
      translate ([(align_top=="front")?0:blockSize,0,0]) rotate([0,0,(align_top=="front")?0:90])
        extrudeLetter (letter3);                      // top face is already oriented and position correctly
    }
  }
}

module tripleLetterString (string1, string2, string3, codeMessage) {
  stringLength = max(len(string1),len(string2),len(string3));

  // Create the sequence of letter blocks
  if (codeMessage || part == 4 || part == 1)
      translate([- (blockSpace*(stringLength-1)/2) - blockWidth/2, 0, adjustedPadHeight-fudge])
        for (i = [0:stringLength-1]) {
          color("lightblue") translate ([i*blockSpace,0, ((string1[i]=="O" || string1[i]=="U") &&                                                      (string2[i]=="O" || string2[i]=="U") &&
                                                          (string3[i]=="O" || string3[i]=="U")) ? -0.8 : 0])
            rotate ([0,0,-45]) tripleLetterBlock (string1[i], string2[i], string3[i]);
        }
  // Create a base for the letters
  if (!codeMessage && ((part == 2 || part == 4) && padHeight != 0))
      translate([-blockSpace*(stringLength-1)/2,0,0])
        color("green") linear_extrude (adjustedPadHeight) hull() {
          translate([blockSpace*(stringLength-1),0,0]) circle(d=padWidth, $fn=50*letterScaling);
           circle(d=padWidth, $fn=50*letterScaling);
        }

  // Add in the small characters flat on the base
  if (!codeMessage && ((part == 3 || part == 4) && padHeight != 0))
      for (tl=[0:1]) {
        if (tinyLocations[tl] && padHeight != 0) {
          translate([-(blockSpace*(stringLength)/2), tinyShifts[tl], adjustedPadHeight-fudge])
          for (i = [1:1:stringLength-1]) {
            color("red")
              translate ([i*blockSpace,0, 0])
                rotate ([0,0,(rotateBaseChars) ? BaseCharsRotation[tl*(stringLength-1)+i-1]:0])
                  linear_extrude(BaseCharactersThickness*adjustedPadHeight/2)
                    text (baseChars[(tl*(tinyLocations[0]? 1:0)*(stringLength-1)+i-1)%len(baseChars)], size=fontSize/tinyFactor, font=fontName, valign="center", halign="center");
            }
          }
      }

}

/* Example and testing values go here
string_1 = "ABCDEFGHIJKLM";
string_2 = "NOPQRSTUVWXYZ";

string_1 = "0123456789";
string_2 = "9876543210";

string_1 = "EEEEEEEEEEEE";
string_2 = chr([36, 60, 62, 9829, 9824, 9830, 9827, 9834, 9835, 8592, 8594, 960]);

string_1 = "GOOD";
string_2 = "LUCK";
string_3 = "GRAD";

string_1 = str("I",chr(9829),"U");
string_2 = "MOM";
string_3 = str("I",chr(9829),"U");

string_1 = str(chr(9829),"M");
string_2 = "UM";
string_3 = "IO";

string_1 = "LUCK";
string_2 = "BABE";
string_3 = "GOOD";

string_1 = "BEFHIKLOPRTUWXZ";  // these letters can be combined with itself as all 3 letters in a block
string_2 = "BEFHIKLOPRTUWXZ";
string_3 = "BEFHIKLOPRTUWXZ";

string_1 = "TRIPLE";
string_2 = "TRIPLE";
string_3 = "TRIPLE";

string_1 = "0123456789";    // all digits can be combined with itself as all 3 characters in a block
string_2 = "0123456789";
string_3 = "0123456789";

all_combos = false;
padHeight = 0;
tripleLetterBlock ("B", "E", "G");
linear_extrude(blockSize) text ("A", size=fontSize, font=fontName);  // Used to get letter dimensions
*/

scale ([myScale,myScale,myScale])
if (all_combos == true ) {
  translate ([0,-7.5*padWidth,0]) tripleLetterString (StringFront, StringRight, StringTop);
  translate ([0,-4.5*padWidth,0]) tripleLetterString (StringFront, StringTop,   StringRight);
  translate ([0,-1.5*padWidth,0]) tripleLetterString (StringRight, StringFront, StringTop);
  translate ([0, 1.5*padWidth,0]) tripleLetterString (StringRight, StringTop,   StringFront);
  translate ([0, 4.5*padWidth,0]) tripleLetterString (StringTop,   StringFront, StringRight);
  translate ([0, 7.5*padWidth,0]) tripleLetterString (StringTop,   StringRight, StringFront);
} else
  tripleLetterString (StringFront,StringRight,StringTop,false);

// Make sure each STL created by the Thingiverse Customizer has some content
//if (part == 1 && padHeight == 0)
//  linear_extrude (1) text ("No base added. View model with all parts.", size=fontSize/4, font=fontName, $fn=10);
if (part == 2 && padHeight == 0) {
   if (mode == 2)
      scale ([0.7,0.7,0.7]) translate([46,18,0]) tripleLetterString ("DUAL","WORD",StringTop,true);
   if (mode == 3)
      scale ([0.37,0.37,0.37]) translate([83,30,0]) tripleLetterString ("TRIPLE","TRIPLE","TRIPLE",true);
  linear_extrude (1) text ("No base added.", size=fontSize/4, font=fontName, $fn=10);
}
if (part == 3 && (!tinyLocations[0] && !tinyLocations[1] || padHeight == 0)) {
   if (mode == 2)
      scale ([1.26,1.26,1.26]) translate([46,18,0]) tripleLetterString ("DUAL","WORD",StringTop,true);
   if (mode == 3)
      scale ([0.66,0.66,0.66]) translate([83,24,0]) tripleLetterString ("TRIPLE","TRIPLE","TRIPLE",true);
  linear_extrude (1) text ("No flat characters added.", size=fontSize/4, font=fontName, $fn=10);
}

/* --------------- Dual Letter Parameters - remove for Customizer App, comment out locally
mode = 2 + 0;
all_combos = false + false;

// Letters on left side (use only UPPERCASE letters, numbers, space, or the special characters)
string_1 = "DUAL WORD";

// Letters on right side (use only UPPERCASE letters, numbers, space, or the special characters)
string_2 = "ILLUSION";

string_3 = str("");

// Scale of letters (1 = letters 20 mm tall and spaced 22.2 mm apart if spacing is set to 0% below )
letterScaling = 1.5; // [0.5:0.1:10.0]

// Additional spacing between letters (% of letter width, none needed, can be negative)
additionalSpacing = 0.0; // [-10.0:0.1:30.0]

// Height of base (mm)
baseHeight = 3.0; // [0.0:0.1:30.0]
padHeight = (all_combos == true) ? 0 : baseHeight;

// The width of a space character can't be 0 because the paired character on the other face of the block wouldn't show. By default it's set to 1.2 mm, which is a minimal width for acceptable strength. You can change this to full block width if you prefer.
width_of_space_character = "minimal - 1.2 mm"; // [minimal - 1.2 mm,maximal - full block width]
// --------------- End of Dual Letter Parameters
*/

```


[User]
I'm looking to generate a curved linkage system in OpenSCAD. I've got a basic concept using some trig functions, but I'd like to have a more robust parametric solution.

Here's what I'm aiming for:

*   **The core is a curved linkage:** It should be a planar shape (2D) that is then extruded to a specific thickness. The curve is defined by three points (A, B, and C). Points A and B are horizontally spaced, point A being at (-pitch/2, 0) and point B at (pitch/2, ~0). Point C is offset vertically, at (0, offset_curve).
*   **Circular arc:** I need the shape to be a segment of a circle (an arc). This circle is determined by the three points above (A, B, and C).
*  **Bosses/Pivots:** At each of these points, A, B and C, I need to have a circular "boss" that is essentially an extension of the shape, centered on each of those points. I need a way to control the diameter of each boss separately as well as the diameter of a pivot hole in the middle of the boss.
*   **Linkage Width:** The width of the linkage itself (the "band" making up the arc) needs to be controlled by a parameter.
*   **Blend Radius:** Where the linkage meets the boss, there should be a fillet/blend controlled by a parameter.
*   **Extrusion:** The whole linkage (arc, bosses, and blends) needs to be extruded to a given thickness.
*   **Parameters:** I need to be able to control the following with parameters:
    *   `pitch`: The horizontal distance between points A and B.
    *  `offset_curve`: The vertical offset of point C.
    *  `boss_diameter_A`: The diameter of the boss centered on point A.
    *  `boss_diameter_B`: The diameter of the boss centered on point B.
    *  `boss_diameter_C`: The diameter of the boss centered on point C.
     *  `pivot_diameter_A`: The diameter of the pivot hole on point A.
    *  `pivot_diameter_B`: The diameter of the pivot hole on point B.
     *  `pivot_diameter_C`: The diameter of the pivot hole on point C.
    *   `linkage_width`: The width of the curved linkage band.
    *  `blend_radius`: The radius of the blend between the linkage and the boss.
    *   `linkage_thickness`: The thickness of the extruded shape.

Could you provide me with an OpenSCAD file or set of files that implements this parametric curved linkage? I would prefer to have the functionality of finding the center of the circle from the three points in the core file and not as a utility function in another library. I don't mind if the helper function to find the center of a circle is also parametric in the X,Y plane.

[Assistant]
curved_linkage.scad:
```
pitch=50;
boss_diameter=17;
offset_curve=22;
pivot_diameter=6;
linkage_width=7;
blend_radius=7;
linkage_thickness=9;

module round2d(OR,IR){
    offset(OR)offset(-IR-OR)offset(IR)children();
}

X3=0;   //C
Y3=offset_curve;

X2=pitch/2;   //B
Y2=0.001;

X1=-pitch/2;   //A
Y1=0;

point_A = ([X1,Y1]);
point_B = ([X2,Y2]);
point_C = ([X3,Y3]);

center = circle_by_three_points(point_A, point_B, point_C);
circle_rad = len3(point_C - center);

module pcd_hole(num_holes, pcd_dia,hole_dia,ang) {
    for (i=[0:num_holes-0]) rotate([0,0,i*ang/num_holes]) translate([pcd_dia/2,0,0]) circle(d=hole_dia);
}

$fn=160;

module curved_linkage(){
 round2d(0,blend_radius){

//color("BLACK") translate(point_C) circle(boss_diameter/2);
color("BLUE") translate(point_B) circle(boss_diameter/2);
color("CYAN") translate(point_A) circle(boss_diameter/2);

angle_a=acos((pitch/2)/circle_rad);
full_angle= offset_curve < pitch/2 ? 180-(angle_a*2) : 180+(angle_a*2);

if (full_angle<180){
translate([0,-circle_rad+offset_curve,0]){
 rotate([0,0,+angle_a])
// rotate([0,0,-angle_a])

pcd_hole(250,circle_rad*2,linkage_width,full_angle);
}
}

if (full_angle>180){
translate([0,-circle_rad+offset_curve,0]){
 rotate([0,0,-angle_a])
pcd_hole(250,circle_rad*2,linkage_width,full_angle);
}
}
}
}

module linkage_with_holes(){
difference(){
curved_linkage();
color("BLUE") translate(point_B) circle(pivot_diameter/2);
color("CYAN") translate(point_A) circle(pivot_diameter/2);
}
}

linear_extrude(height=linkage_thickness) linkage_with_holes();

function circle_by_three_points(A, B, C) =
let (
  yD_b = C.y - B.y,  xD_b = C.x - B.x,  yD_a = B.y - A.y,
  xD_a = B.x - A.x,  aS = yD_a / xD_a,  bS = yD_b / xD_b,
  cex = (aS * bS * (A.y - C.y) +
  bS * (A.x + B.x) -    aS * (B.x + C.x)) / (2 * (bS - aS)),
  cey = -1 * (cex - (A.x + B.x) / 2) / aS + (A.y + B.y) / 2
)
[cex, cey];

function len3(v) = sqrt(pow(v.x, 2) + pow(v.y, 2));

```


[User]
I want to make a customizable frog choir that can shout out any message I want. The idea is to have multiple rows of frogs, where each row spells out a word from the message, and each frog has a letter in front of it. So, like if I put in "GOOD JOB", it would make one row that says "GOOD" and a row under it that says "JOB".

The frogs themselves are imported .stl files, and I want to be able to choose the number of frogs used, and which frog specifically for each position. There should be 8 different frog models (named "frog_1.stl", "frog_2.stl", etc.), and it should randomly choose from these. Each frog should be on a little platform, and the letters should be on the platform.

I need to be able to adjust:
*   The spacing between frogs on a line.
*   The height of the platform.
*   The font and font size for the letters.
*   The message to display.

I'd also like to be able to adjust the height of each line of text such that the platform height of each line scales with the line number.

I'm already using two custom libraries in my current code: `sub_str.scad` and `split_str.scad`, which I used to split the message into words. I need to make sure that those are included in the new design. I'm okay if these functions get rolled into the main file.

[Assistant]
frog_choir.scad:
```
text = "GOOD JOB";
frog_dist = 2;
seat_height = 6;
font = "Arial Black";
font_size = 4;

/**
* sub_str.scad
*
* Returns a new string that is a substring of the given string.
*
* @copyright Justin Lin, 2017
* @license https://opensource.org/licenses/lgpl-3.0.html
*
* @see https://openhome.cc/eGossip/OpenSCAD/lib-sub_str.html
*
**/

function sub_str(t, begin, end, result = "") =
    end == undef ? sub_str(t, begin, len(t)) : (
        begin == end ? result : sub_str(t, begin + 1, end, str(result, t[begin]))
    );

/**
* split_str.scad
*
* Splits the given string around matches of the given delimiting character.
* It depends on the sub_str function so remember to include sub_str.scad.
*
* @copyright Justin Lin, 2017
* @license https://opensource.org/licenses/lgpl-3.0.html
*
* @see https://openhome.cc/eGossip/OpenSCAD/lib-split_str.html
*
**/

function _split_t_by(idxs, t, ts = [], i = -1) =
    i == -1 ? _split_t_by(idxs, t, [sub_str(t, 0, idxs[0])], i + 1) : (
        i == len(idxs) - 1 ? concat(ts, sub_str(t, idxs[i] + 1)) :
            _split_t_by(idxs, t, concat(ts, sub_str(t, idxs[i] + 1, idxs[i + 1])), i + 1)
    );

function split_str(t, delimiter) =
    len(search(delimiter, t)) == 0 ?
        [t] : _split_t_by(search(delimiter, t, 0)[0], t);

module one_random_frog() {
    n = round(rands(1, 8, 1)[0]);
    translate([0, 0, -10]) import(str("frog_", n, ".stl"));
}

module one_line_frogs(text, frog_dist, seat_height, font, font_size) {
    $fn = 24;
    frog_width_x = 100.64;
    frog_width_y = 96.93;
    frog_height = 105.87;

    seat_width = 100.64 + frog_dist;
    leng = len(text);
    width = seat_width * leng;

    rotate([-5, 0, 0]) translate([-width / 2 + seat_width / 2, 0, seat_height])
    for(i = [0:leng - 1]) {
        translate([i * seat_width, 0, 0]) one_random_frog();
        translate([i * seat_width, -1, 44]) rotate([71, 0, 0]) linear_extrude(45, scale = 4.5)
            text(text[i], font = font, valign = "center", size = font_size, halign = "center");
    }
    translate([0, frog_width_y / 3, seat_height / 2])
        cube([seat_width * leng, frog_width_y, seat_height], center = true);
}

module multi_line_frogs(texts, seat_height, frog_dist, font, font_size) {
    frog_width_x = 100.64;
    frog_width_y = 96.93;
    frog_height = 105.87;

    t_len = len(texts);

    for(i = [0: t_len - 1]) {
        translate([0, -frog_width_y * i, 0])
            one_line_frogs(texts[i], frog_dist, seat_height * (t_len - i), font, font_size);
    }

}

multi_line_frogs(split_str(text, " "),
    frog_dist = frog_dist,
    seat_height = seat_height,
    font = font,
    font_size = font_size
);

```


[User]
I need a design for a small unit marker, something that can slip onto the base of a miniature. I'm looking to use it for tabletop wargames like Age of Sigmar, to track which models belong to which unit or maybe to show buffs or debuffs.

Specifically, I want a ring-like marker. The bottom of the ring should have a larger diameter than the top, creating a slight cone shape. The bottom of the ring needs to fit snuggly onto a miniature base. I want it to fit well on a 32mm base, but I'd like it to be parametric so I can adjust the sizes myself later. I'd like the ability to adjust the lower and upper diameters independently, as well as the height of the ring.

Also, I'd like to have a slot cut into the side of the ring. This is so the plastic can flex a little bit, making it easier to get the ring on and off the model's base. I need to be able to control the width of that slot, including turning it off entirely.

Please make the design in OpenSCAD, and make sure it's parametric. I don't know what library to use so you'll have to pick, just make sure it's easy to use. I will want to control the number of facets used in the cylinders, so I need $fn to be exposed. Please also make it so that I can add a small amount of extra spacing, as different printers will likely have different needs when printing.

[Assistant]
UnitIndicator.scad:
```

// Insert a bit of extra space to get a better fit. May vary based on printer.
extra_spacing = 1.5;

// The diameter of the base at the bottom
base_diameter_lower = 32;

// The diameter of the base at the top
base_diameter_upper = 28;

// The height of the base
base_diameter_height = 4;

// The width of the slot, this allows the marker to flex a bit making it easier to put on. (Set to 0 for no slot)
slot_width = 3.2;

/* [Hidden] */
$fn = 100;

difference() {

    cylinder(d=base_diameter_lower+extra_spacing,h=base_diameter_height);
    translate([0,0,-0.1])
        cylinder(d2=base_diameter_lower+extra_spacing,d1=base_diameter_upper+extra_spacing,h=base_diameter_height);

    translate([0,0,base_diameter_height*3/4]) {
        cylinder(d=base_diameter_lower+extra_spacing+1,h=base_diameter_height);
    }

    if (slot_width > 0) {
        translate([0,-slot_width/2,-0.1]) {
            cube([base_diameter_lower,slot_width,base_diameter_height*2]);
        }
    }

}
```


[User]
I'm looking to create a parametric inductor core generator in OpenSCAD, and I'd like your help in designing it. I have a basic idea of the parameters I need to control and the general shape, but I'm not sure how to put it all together.

Here's what I'm envisioning:

*   **Core Shape:** The core will be a cylindrical shape with a helical groove for winding the inductor wire. The base should be a cylinder.
*   **Parameters:** I'd like to control these aspects via parameters:
    *   `DIAMETER`: The overall diameter of the core, including any protrusions for the wire.
    *   `WIRE_D`: The diameter of the wire that will be used to wind the inductor.
    *   `PITCH`: The distance between each turn of the wire on the core.
    *   `TURNS`: The number of turns of wire in the helical groove.
    *    `BASE`: The height of the cylindrical base of the core.
*   **Groove Geometry**: I'd like the helical groove to be created using the `tsmthread` module from the `tsmthread` library (version 0.4). Specifically, I think using the `THREAD_BALL2` function, with the wire diameter for the radius should generate a good profile for the wire groove.
*   **Core Solid:** The main body of the core, and a cylinder base below the helical groove, should be created by extruding a circle with the `linear_extrude` function.

*  **Negative Space:** The wire groove is cut out of the base, as a cylinder, using `difference()`. Also, some rectangular shapes should be subtracted to act as wire guides.
*   **Output**: The final output should be a 3D model of the inductor core with the specified parameters, ready to be exported as an STL.

Could you please create the necessary `.scad` file for this? I'd like it to be well-commented, and easy to modify. I've included the header and functions for the `tsmthread` library, and a few other utility functions, so you have all the necessary code. I'd like the program to calculate the radius of the core such that the total diameter of the core (including the wire groove) is equal to the specified `DIAMETER`.

[Assistant]
inductor-core.scad:
```
/** Inductor core generator, using tsmthread 0.4 http://burningsmell.org/3d/lib/tsmthread/ */

function THREAD_BALL2(R)=concat([ [0,0] ],
    [ for(A=[0:30:180]) R*[ -cos(A),sin(A) ]+[0.5,0]
    // , [ 1,0 ]
        ] );

DIAMETER=10;
WIRE_D=1;
PITCH=2;
TURNS=5;
BASE=3;
$fa=1;
$fs=1;

CORE=DIAMETER-(WIRE_D)*4;

difference() {
    union() {
        translate(BASE*[0,0,1])
            tsmthread(DMAJ=DIAMETER, L=PITCH*TURNS, PR=THREAD_BALL2(WIRE_D/(PITCH*2)), PITCH=PITCH);
        linear_extrude(BASE, convexity=3) difference() {
            circle(d=DIAMETER+3);
            for(A=[0:90:360-1]) rotate([0,0,A]) hull() {
                translate([DIAMETER+WIRE_D,0]/2) circle(d=WIRE_D);
                translate([DIAMETER,0]) circle(d=WIRE_D);
            }
        }
    }

    linear_extrude(h=BASE+PITCH*TURNS*3, center=true, convexity=3)
    for(A=[0:90:360-1]) rotate([0,0,A]) hull() {
        translate([CORE+WIRE_D,0]/2) circle(d=WIRE_D);
        circle(d=WIRE_D);
    }

    cylinder(d=CORE, h=BASE+PITCH*TURNS*3, center=true);

    for(Z=BASE+PITCH*TURNS, A=[0,90]) rotate([0,0,A]) translate(Z*[0,0,1])
    cube([WIRE_D,30,WIRE_D*2],center=true);
}

module print_acme() {
    // Tested on ground metal 3/8-10 ACME rod
    // Flange for 3/8"-10 ACME
    // Flange dimensions:
    // https://www.helixlinear.com/Product/PowerAC-38-2-RA-wBronze-Nut/
    translate([40,0]) imperial() difference() {
        taper(d=3/8, h=0.41+0.62, off=1/16, in=true)
            flange(B=0.62, C=0.85,G=0.41,H=1.606+$OD_COMP, F=1.25, F2=.266, off=1/16, open=true);

        comp_thread(DMAJ=3/8, L=2, PITCH=1/10, A=29, H1=0.5/2, H2=0.5/2, in=true);
    }

    // Tested on ground metal TR11x2 rod.
    // TR11x2 flange
    // https://www.omc-stepperonline.com/download/23LS22-2004E-300T.pdf
    difference() {
        intersection() {
            flange(B=17, G=8, F=23, F2=5, C=15+$OD_COMP, H=30+$OD_COMP, holes=[0,180], off=1,open=1);
            taper(d=11, h=25, off=1, in=true)
            translate([30-25-$OD_COMP,0,0]/2) cube([25,40,50],center=true);
        }

        // Compensated thread, otherwise leadscrews need to be oversized a disgusting amount
        comp_thread(11, 30, 2, 30, 0.5/2, 0.5/2, in=true);
    }
}

 /**
 * 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, ACC=[1,0])=let(M=floor(N/len(V))) V[N%len(V)] + M*ACC;

/* Very basic interpolation.  mix(A,B,0)=A, mix(A,B,1)=B, 0 <= X <= 1 */
function mix(A,B,X)=(A*(1-X)) + B*X;

/**
 * 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]));
/* Same, but X for given Y */
function mixvy(V1,V2,Y)=let(OUT=mixv([ V1[1],V1[0] ], [V2[1],V2[0]], Y)) [ OUT[1],OUT[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, ACC=[1,0], N=0)=
//  Speedup that doesn't help much
    (X>1) ? interpolate(V, (X%1),ACC,N)+floor(X)*ACC[1]:
//      Unneeded case
//    (X<0) ? interpolate(V, 1+(X%1),ACC,N)+floor(X)*ACC[1] :
    (X>wrap(V,N+1)[0]) ?
    interpolate(V,X,ACC,N+1)
:   mixv(wrap(V,N,ACC),wrap(V,N+1,ACC),X)[1];

/**
 *  V = [ [X0,Y0], [X1,Y1], ..., [XN,YN] ] where X0 < X1 < X2 ... < XN
 *
 *  Finds N where XN > X.
 */
function binsearch(PR,X,MIN=0,IMAX=-1)=let(MAX=(IMAX<0)?len(PR):IMAX,
    N=floor((MIN+MAX)/2))
    ((MAX-MIN) <= 1) ? N
    :   X < PR[N][0] ?
        binsearch(PR,X,MIN,ceil((MIN+MAX)/2))
    :   binsearch(PR,X,floor((MIN+MAX)/2),MAX);

/**
 *  V = [ [X0,Y0], [X1,Y1], .., [XN,YN] ] where X0 < X1 < ... < XN and XN < 1
 *
 *  Returns N for given XN like binsearch(), except it wraps X > 1 to 0-1
 *  and returns correspondingly higher N.
 */
function binsearch2(PR,X)=binsearch(PR,X%1)+floor(X)*len(PR);

/**
 * Faster lookup for interpolate().  log(N)
 */
function interpolate2(V,X,ADD=[1,0])=V[binsearch(V,(X%1))][1]+floor(X)*ADD[1];

function interpolaten(V,X,ADD=[1,0])=binsearch(V,(X%1))+floor(X);

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_UTS// Thread profile, i.e. ACME, UTS, other
    , STARTS=1,             // Want a crazy 37-start thread?  You're crazy.  but I'll try.
    , TAPER=0,              // Adds an offset of [1,tan(TAPER)] per thread.
    , STUB=0,               // Extends a cylinder below the thread.  In pitch units.
    , STUB_OFF=0            // Reduces the diameter of stub.  In pitch units.
    ) {

    /* Minimum number of radial points required to match thread profile */
    POINTS_MIN=len(PR)*STARTS*2;

    // OpenSCAD-style fragment generation via $fa and $fs.
    // See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features
    function points_r(r)=ceil(max(min(360.0 / $fa, (r*2)*3.14159 / $fs), 5));

    // Rounds X up to a nonzero multiple of Y.
    function roundup(X,Y) = Y*max(1,floor(X/Y) + (((X%Y)>0)?1:0));

    // Points can be forced with $fn
    POINTS=($fn>0) ? $fn :
        // Otherwise, it decides via $fs and $fa, with a minimum of
        // 16, rounded up to POINTS_MIN
        max(roundup(16,POINTS_MIN), roundup(points_r(DMAJ/2),POINTS_MIN));

    if(POINTS % POINTS_MIN )
    {
        echo("WARNING:  DMAJ",DMAJ,"PITCH",PITCH,"STARTS",STARTS, "POINTS", POINTS);
        echo("WARNING:  POINTS should be a multiple of",POINTS_MIN);
        echo("WARNING:  Top and bottom geometry may look ugly");
    }

    ADD=add_npt(TAPER); /* How much radius to add to pitch each rotation */

    // 1*STARTS rows of thread tapper happens which isn't drawn
    // Add this to radius to compensate for it
    TAPER_COMP=STARTS*ADD[1]*PITCH;

    // [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];

    /**
     * The top and bottom are cut off so more height than 0 is needed
     * to generate a thread.
     */
    RING_MIN=(STARTS+1)*len(PR);

    // Find the closest PR[N] for a given X, to estimate thread length
    function minx(PR,X,N=0)=(X>wrap(PR,N+1,ADD)[0])?minx(PR,X,N+1):max(0,N);

    // Calculated number of rings per height.
    RINGS=let(SEG=floor(L/PITCH), X=(L%PITCH)/PITCH) max(RING_MIN+1,
        RING_MIN+SEG*len(PR) + minx(PR,X));

    SHOW=0;     // Debug value.  Offsets top and bottom to highlight any mesh flaws

    /**
     * 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,ADD)[0] + STARTS*(N/POINTS));
    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)=binsearch2(PR,FLAT_B - (N/POINTS)*STARTS)+1;
    // function ringmax(N)=binsearch2(PR,FLAT_T - (N/POINTS)*STARTS);

    // Fast-lookup arrays
    MIN=[for(N=[0:POINTS-1]) binsearch2(PR,FLAT_B - (N/POINTS)*STARTS)+1 ];
    MAX=[for(N=[0:POINTS-1]) binsearch2(PR,FLAT_T - (N/POINTS)*STARTS) ];

    // Array-lookup wrappers which speed up ringmax/ringmin manyfold.
    // binsearch makes them fast enough to be tolerable, but still much better
    function ringmax(N)=MAX[N%POINTS];
    function ringmin(N)=MIN[N%POINTS];

    /**
     * Difficult difficult lemon difficult.
     * Interpolate along the profile to find the points it cuts through
     * the main spiral.
     *
     * Some difficulty is RING coordinates increase P, while N decreases
     * it as it crosses the spiral!
     *
     * Taper must be accounted for as well.
     */
    function radius_flat(RING,N)=
        TAPER_COMP+
        (DMAJ/2)-PITCH*interpolate(PR,wrap(PR,len(PR)*STARTS+RING,ADD)[0] -
            // Modulous because this is all the same taper
            (STARTS*(N/POINTS))%1 - STARTS,ADD)
            // Of course we have to un-taper too, sigh
            -ADD[1]*STARTS*(((N/POINTS)*STARTS)%1);
    /**
     * Radius is generated differently for the top&bottom faces than the spiral because
     * they exist in different coordinate spaces.  Also, the top&bottom faces must be
     * interpolated to fit.
     */
    function cap(RING,ZOFF=0,ROFF=0)=[
        for(N=[0:POINTS-1])
        let(P=N/POINTS, A=-360*P,R=radius_flat(RING,N)+ROFF)
            traj(R,A,zoff(RING,0)+ZOFF)
        ];

    /**
     * Debug function.
     * Draws little outlines at every ring height to compare spiral generation
     * to face generation.
     */
    module test() {
        for(RING=[0:RINGS-1]) {
            POLY=cap(RING, ROFF=0.1);
            LIST=[ [ for(N=[0:POINTS-1]) N ] ];

            polyhedron(POLY,LIST);
        }
    }

    /**
     * Helper array for generating polygon points.
     * DATA[0]+N, N=[0:POINTS-1] for a point on the bottom face
     * DATA[1]+N, N=[0:POINTS-1] for a point on the top face
     * DATA[2]+N + (M*POINTS), N=[0:POINTS-1], M=[0:RINGS-1] for
     * a point on the main spiral.
     */
    DATA=[ 0,                          // 0 = bottom face
           POINTS,                     // 1 = top face
           4*POINTS,                   // 2 = main spiral
           2*POINTS,                   // 3 = stub top
           3*POINTS,                   // 4 = stub bottom
           2*POINTS+POINTS*len(PR),
           2*POINTS+POINTS*len(PR) +
                RINGS*POINTS
        ];

    /**
     * This is it, this is where the magic happens.
     * Given a point in RING,N spiral coordinates, this decides whether it
     * ends up in the top, the spiral, or the bottom so I don't have to.
     *
     * TODO:  Optimize this.  This is a pinch point.
     */
    function point(RING,N)=
        (RING<ringmin(N)) ? DATA[0]+(N%POINTS) // Bottom flat
       :(RING>ringmax(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
     * RINGS - (3*RINGS)-1  Stub
     * (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),
        // Stub top
        [ for(N=[0:POINTS-1]) let(R=(DMAJ/2)-(STUB_OFF*PITCH)) traj(R,-360*(N/POINTS), 1) ],
        // Stub bottom
        [ for(N=[0:POINTS-1]) let(R=(DMAJ/2)-(STUB_OFF*PITCH)) traj(R,-360*(N/POINTS),
           -STUB) ],
        //cap(len(PR)*STARTS, -STUB),

        // Main spiral
        [ for(RING=[0:RINGS-1], N=[0:POINTS-1])
            let(A=-360*(N/POINTS),
                P1=wrap(PR,RING,ADD),
                P2=wrap(PR,RING+len(PR)*STARTS,ADD),
                UV=mix(P1,P2,N/POINTS),
                R=TAPER_COMP+(DMAJ/2)-PITCH*UV[1], Z=UV[0])
                traj(R, A, Z)
        ]);

    /**
     * Remove redundant points from polygons.
     * collapse([0,1,1,2,3,4,0]) == [0,1,2,3,4]
     */
    function collapse(V)=[ for(N=[0:len(V)-1]) if(V[(N+1)%len(V)] != V[N]) V[N] ];

    // Should we use quads here?  Will fewer loops be faster?
    // Probably shouldn't alter the hard-won mesh maker, but
    // can we do more per loops somehow?
    PLIST=concat(
        // Main spiral A
        [   for(N=[0:POINTS-2], RING=[ringmin(N)-1:ringmax(N)])
            pointv([ [RING,N+1],[RING,N],[RING+1,N] ])
        ],
        // Main spiral B
        [   for(N=[0:POINTS-2], RING=[ringmin(N+1)-1:ringmax(N+1)])
            pointv([[RING+1,N+1],[RING,N+1],[RING+1,N]])
        ],
        // stitch A
        [   for(N=POINTS-1, RING=[ringmin(N)-1:ringmax(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=[ringmin(N)-1:ringmax(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 extension
        [ if(STUB) for(WELD=[[0,3],[3,4]], N=[0:POINTS-1])
            [ DATA[WELD[0]]+N, DATA[WELD[0]]+(N+1)%POINTS, DATA[WELD[1]]+(N+1)%POINTS ] ],
        [ if(STUB) for(WELD=[[0,3],[3,4]], N=[0:POINTS-1])
            [ DATA[WELD[1]]+N, DATA[WELD[0]]+N, DATA[WELD[1]]+(N+1)%POINTS ] ],

        // Bottom flat
        [ [ for(N=[0:POINTS-1]) N+DATA[(STUB>0)?4:0] ] ],
        // top flat.  Note reverse direction to mirror the normal.
        [ [ for(N=[0:POINTS-1], N2=POINTS-(N+1)) N2+DATA[1] ] ]
    );

    // Screw it, scale after, PITCH=1 is so much less math
    scale([1,1,PITCH]) translate([0,0,STUB?STUB:-FLAT_B]) polyhedron(POLY, PLIST, convexity=5);
}

// Compensated Threads.
// Threads are thinned by $PE mm.  Diameter is adjusted $OD_COMP/$ID_COMP amount
// depending on whether they're inside or outside threads.
// Only use this with leadscrews.
// A and H values as with prof().
module comp_thread(DMAJ=11, L=20, PITCH=2, A=30, H1=0.5/2, H2=0.5/2, STARTS=1, in=false)
{
    PE2=(in?$PE :-$PE)*(2.0/PITCH)*cos(A);
    echo("PE",PE2,"$PE",$PE);
    PR=prof(A, H1-PE2, H2+PE2);
    echo("comp_thread","DMAJ",DMAJ,"L",L,"PITCH",PITCH,"A",A,"H1",H1,"H2",H2,"=",PR);

   tsmthread(DMAJ+(in?$ID_COMP:$OD_COMP), L=L, PITCH=PITCH, STARTS=STARTS, PR=PR);
}

/* All common nuts and bolts. */
THREAD_UTS =let(H=0.8660) prof(60,H*(7/16), H*(3/8));

/******************************************************************************/
/********************************NPT THREAD************************************/
/******************************************************************************/

function add_npt(TAPER)=[1,tan(TAPER)]; // Why so easy? tan(ANGLE) is the slope of an angle.

/******************************************************************************/
/******************************MISC UTILITIES**********************************/
/******************************************************************************/

/**
 *  i.e. cylinder(d=flat(6)*10,$fn=6)
 *  makes a 10mm flat-to-flat hexagon.
 */
function flat(N=6)=1/cos(180/N);

/**
 *  Scales up objects to imperial inches and and alters $fs accordingly, so
 *  imperial() cylinder(d=1, h=1); renders a satisfying number of facets
 *  instead of freaking 5.
 */
module imperial(F=25.4) {
    // modified $... values will be seen by children
    $fs     =$fs     /F;
    $OD_COMP=$OD_COMP/F;
    $ID_COMP=$ID_COMP/F;
    $PE     =$PE     /F;
    $SCALE  =         F;
    scale(F) children();
}

/**
 * A double-pointed cone or pyramid of exactly 45 degrees for tapers.
 * obeys $fn, $fs, $fa, r, d, center=true in a manner like cylinder().
 * example:
 *
 *  // Outside taper
 *  taper(d=10, h=10, off=1) cylinder(d=10, h=10);
 *
 *  // Inside taper
 *  difference() { cylinder(d=10, h=10); taper(in=true, d=5, h=10, off=1) cylinder(d=5, h=10); }
 */
module taper(d=1,h=1,off=0,r=0, center=false, in=false) {
    function points_r(r)=$fn?$fn:(ceil(max(min(360.0 / $fa, (r*2)*3.14159 / $fs), 5)));

    if(r) taper(r*2, h, off, r=0, center=center, in=in);
    else {
        // Calculate number of fragments same way openscad does
        U=points_r(d/2);

        if(in) difference() {
            children();
            translate(center?[0,0,0]:[0,0,h/2]) union() {
                for(M=[[0,0,1],[0,0,0]]) mirror(M)
                translate([0,0,h/2-d/2-off]) cylinder(d1=0, d2=d*2, h=d, $fn=points_r(d/2));
            }
        }
        else intersection() {
            children();
            translate(center?[0,0,0]:[0,0,h/2]) scale(h+d-off) polyhedron(
                concat( [ for(N=[0:U-1]) 0.5*[cos((N*360)/U),sin((N*360)/U),0] ],
                        // Top and bottom of pyramid
                        [ 0.5*[ 0,0,1], -0.5*[0,0,1] ] ),
                concat( [ for(N=[0:U-1]) [ N, U, (N+1)%U]],
                        [ for(N=[0:U-1]) [ (N+1)%U, U+1, N]]
                ));
        }
    }
}

// Flange pattern as described by this URL:
// https://www.helixlinear.com/Product/PowerAC-38-2-RA-wBronze-Nut/
//
//      /---\   <--------
//      |   |           |
// ---> |>>>|           |
// |    |   |           |
// |    |   \---\ <--   |
// |    |       |   |   |
// F*2  |       |   C   H
// |    |       |   |   |
// |    |   /---/ <--   |
// |    |   |           |
// ---> |>>>|           |
//      |   |           |
//      \---/   <--------
//      ^   ^   ^
//      |-G-|-B-|
//
//      B is height of stem
//      C is diameter of stem
//      F is the radius of the screw pattern
//      F2 is the diameter of the screw holes.
//      G is the thickness of the base
//      H is width of base.
//
//      holes is the pattern of holes to drill.  default is 4 holes, 1 every 90 degrees.
//      when open is true, the holes are sots extending to the edge.
//      off is how much to bevel.
module flange(B,C,F,F2,G,off=0,holes=[45:90:360], open=false) {
    taper(d=H,h=G,off=off) linear_extrude(G, convexity=3) offset(off/2) offset(-off/2) difference() {
        circle(d=H);
        for(A=holes) hull() {
            translate(F*[sin(A),cos(A)]/2) circle(d=F2);
            if(open) translate(3*F*[sin(A),cos(A)]/2) circle(d=F2);
        }
    }
    if(B && C) taper(d=C,h=G+B,off=off) cylinder(d=C, h=G+B);
}

// What the hell is this?  I don't know.
module garnet() intersection_for(A=[[0,0,1],[0,1],[1,0]]) rotate(45*A) cube(10*[1,1,1],center=true);

//garnet();

```


[User]
Okay, so I'm looking to create a parametric trash bag hanger that mounts over a cabinet door. I've seen some existing designs, but I need something that's easily customized to fit different door thicknesses. I'd like to be able to specify the door thickness as a parameter, along with the overall width of the hanger (where the trash bag attaches). I also need it to be able to be mirrored so I can print left and right versions.

Here are some specific features I'd like:

*   **Door Thickness:** This needs to be a parameter, so I can adjust it to fit my cabinets. It will probably be in the 15-30mm range but I want it to be flexible.
*   **Hanger Width:** The width of the part that holds the trash bag should also be a parameter. It should also be in the 10-40mm range.
*   **Mirrored Design:** I need to be able to generate both left and right versions of the hanger.
*   **Over-Door Hook:** The part that hooks over the door should have a slight angle to it, to make it hold more securely and to keep the part pressed against the door
*   **Bag Attachment:** It should have some sort of geometry to hold the trash bag open. Ideally this geometry should include a few different features:
    *   a main section that keeps the bag on the side
    *   some small indentations to keep the bag from sliding up or down
    *   a small rounded corner to allow for some bend without ripping
*   **Solid Construction:** The model should be solid and durable. I think a good base thickness would be around 3mm.
*   **Rounded Edges/Bevels:** Instead of hard corners I’d prefer rounded or beveled edges to make it look nicer and reduce the risk of cracking.
*   **Clearance:** The design should provide enough space for the user to open the door without the hanger hitting the cabinet face. I think about 1cm from the door face should work, but it could be configurable.

I think that's everything. Please generate the necessary OpenSCAD files. I don't need you to create a customizer, but the variables at the top of the file should be easy to edit.

[Assistant]
Parametrable_Trash_Bag_hanger.scad:
```

/* test 14/02/2019 imprimer a 0,3, 40% infill  */

// All values are in mm
door_thickness = 23;
hanger_width = 25;//[10:40]
hanger_thickness = 3*1;
part_side = 0;//[1:left, 0:right]

// from https://wiki.phormix.com/pub/index.php?title=OpenSCAD_Modules
module beveled_rect_top(w,d,h,r){
    union() {
        translate([r,d-r,h/2]) cylinder(r = r, h = h, center = true);
        translate([w-r,d-r,h/2]) cylinder(r = r, h = h, center = true);
        translate([r,d-r, 0]) cube([w-(r*2),r,h]);
        translate([0, 0, 0]) cube([w,d-r,h]);
    }
}

color ("red") translate([part_side?85+door_thickness+hanger_thickness:0,0,part_side?hanger_width:0]) rotate([0,part_side?180:0,0])
linear_extrude (height=hanger_width)
polygon(points=[[0,-8],[0,-13],
		[10,-13],[20,-hanger_thickness*1.5],[70,-hanger_thickness*1.5],
		[71,-11], [76,-27],[85,-40], [85,30], [85+door_thickness,30], [85+0.90*door_thickness,-62],
        [85+0.90*door_thickness+hanger_thickness,-62], [85+door_thickness+hanger_thickness, 30 +hanger_thickness],
        [85-hanger_thickness, 30 + hanger_thickness], [85-hanger_thickness,-10], [78, -10], [79,0], [6,0], [7,-10],
        [hanger_thickness, -13+hanger_thickness], [hanger_thickness, -8]]);

translate([part_side?59:33,-14,0]) cube([16,14,5], false);
color("blue") translate([part_side?75:49,-14,12])
    rotate([0,180,0]) polyhedron(
        points=[[0,0,12], [16,0,12], [16,0,0], [0,0,0], [0,5,0], [16,5,0]],
        faces=[[0,1,2,3],[5,4,3,2],[0,4,5,1],[0,3,4],[5,2,1]]
    );

translate([part_side?85+door_thickness:0,-8,hanger_width]) rotate ([0,90,0]) beveled_rect_top(hanger_width,8, hanger_thickness, 4);
translate([part_side?0.1*door_thickness+hanger_thickness:85+0.9*door_thickness+hanger_thickness,-62,hanger_width]) rotate ([180,90,0]) beveled_rect_top(hanger_width,8, hanger_thickness, 4);
```